badline 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/CLAUDE.md +23 -23
  4. data/CONTRIBUTING.md +5 -0
  5. data/README.md +59 -1
  6. data/doc/pinned-behaviour.md +4 -2
  7. data/exe/badline-ruby +2 -1
  8. data/lib/badline/address_bus/saved_state.rb +72 -0
  9. data/lib/badline/address_bus.rb +1 -0
  10. data/lib/badline/audio/stream.rb +6 -0
  11. data/lib/badline/cartridge/action_replay.rb +15 -0
  12. data/lib/badline/cartridge/comal80.rb +8 -0
  13. data/lib/badline/cartridge/dinamic.rb +8 -0
  14. data/lib/badline/cartridge/easy_flash.rb +20 -1
  15. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  16. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  17. data/lib/badline/cartridge/flash.rb +41 -0
  18. data/lib/badline/cartridge/fun_play.rb +8 -0
  19. data/lib/badline/cartridge/g_mod2.rb +12 -0
  20. data/lib/badline/cartridge/game_system.rb +8 -0
  21. data/lib/badline/cartridge/geo_ram.rb +16 -0
  22. data/lib/badline/cartridge/magic_desk.rb +8 -0
  23. data/lib/badline/cartridge/ocean.rb +8 -0
  24. data/lib/badline/cartridge/pagefox.rb +12 -0
  25. data/lib/badline/cartridge/retro_replay.rb +44 -1
  26. data/lib/badline/cartridge/rgcd.rb +10 -0
  27. data/lib/badline/cartridge/super_games.rb +10 -0
  28. data/lib/badline/cartridge/zaxxon.rb +16 -0
  29. data/lib/badline/cartridge.rb +72 -0
  30. data/lib/badline/cia/interrupt_register.rb +17 -0
  31. data/lib/badline/cia/serial.rb +44 -0
  32. data/lib/badline/cia/state.rb +47 -0
  33. data/lib/badline/cia/timer.rb +33 -0
  34. data/lib/badline/cia.rb +24 -2
  35. data/lib/badline/computer/attachments.rb +132 -0
  36. data/lib/badline/computer/saved_state.rb +116 -0
  37. data/lib/badline/computer.rb +8 -31
  38. data/lib/badline/cpu/microcode.rb +8 -0
  39. data/lib/badline/cpu/saved_state.rb +115 -0
  40. data/lib/badline/cpu.rb +2 -0
  41. data/lib/badline/datasette.rb +27 -0
  42. data/lib/badline/drive1541/bus.rb +89 -13
  43. data/lib/badline/drive1541/disk.rb +7 -2
  44. data/lib/badline/drive1541/disk_state.rb +80 -0
  45. data/lib/badline/drive1541/idle.rb +9 -57
  46. data/lib/badline/drive1541/mechanism.rb +2 -0
  47. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  48. data/lib/badline/drive1541/orbit.rb +81 -0
  49. data/lib/badline/drive1541/rotation.rb +10 -0
  50. data/lib/badline/drive1541/serial_port.rb +10 -0
  51. data/lib/badline/drive1541/sleep.rb +114 -0
  52. data/lib/badline/drive1541.rb +52 -2
  53. data/lib/badline/gui/application.rb +41 -14
  54. data/lib/badline/gui/gamepads.rb +7 -0
  55. data/lib/badline/gui/snapshots.rb +67 -0
  56. data/lib/badline/gui.rb +1 -0
  57. data/lib/badline/iec_bus.rb +16 -0
  58. data/lib/badline/kernal_trap/channel.rb +27 -0
  59. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  60. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  61. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  62. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  63. data/lib/badline/kernal_trap/drive.rb +19 -0
  64. data/lib/badline/kernal_trap/save.rb +8 -0
  65. data/lib/badline/kernal_trap/serial.rb +16 -0
  66. data/lib/badline/memory.rb +11 -0
  67. data/lib/badline/options.rb +3 -2
  68. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  71. data/lib/badline/reu/dma.rb +36 -0
  72. data/lib/badline/reu/ram.rb +15 -0
  73. data/lib/badline/reu/saved_state.rb +52 -0
  74. data/lib/badline/reu.rb +4 -0
  75. data/lib/badline/sdl/events.rb +2 -0
  76. data/lib/badline/sid/envelope/state.rb +68 -0
  77. data/lib/badline/sid/envelope.rb +7 -0
  78. data/lib/badline/sid/filter.rb +38 -0
  79. data/lib/badline/sid/voice.rb +10 -0
  80. data/lib/badline/sid/waveform/state.rb +66 -0
  81. data/lib/badline/sid/waveform.rb +2 -0
  82. data/lib/badline/sid.rb +34 -0
  83. data/lib/badline/snapshot/container.rb +117 -0
  84. data/lib/badline/snapshot/fields.rb +54 -0
  85. data/lib/badline/snapshot/image.rb +70 -0
  86. data/lib/badline/snapshot/machine_state.rb +77 -0
  87. data/lib/badline/snapshot/setup.rb +58 -0
  88. data/lib/badline/snapshot/state.rb +178 -0
  89. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  90. data/lib/badline/snapshot/vice/cias.rb +89 -0
  91. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  92. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  93. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  94. data/lib/badline/snapshot/vice/restore.rb +125 -0
  95. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  96. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  97. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  98. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  99. data/lib/badline/snapshot/vice.rb +74 -0
  100. data/lib/badline/snapshot.rb +52 -0
  101. data/lib/badline/storage/crt_file.rb +9 -3
  102. data/lib/badline/storage/d64_image.rb +2 -0
  103. data/lib/badline/storage/d71_image.rb +2 -0
  104. data/lib/badline/storage/d81_image.rb +2 -0
  105. data/lib/badline/storage/disk_image.rb +11 -2
  106. data/lib/badline/storage/host_directory.rb +5 -0
  107. data/lib/badline/storage/t64.rb +6 -0
  108. data/lib/badline/storage/tap.rb +10 -1
  109. data/lib/badline/storage/unavailable.rb +20 -0
  110. data/lib/badline/storage.rb +34 -0
  111. data/lib/badline/time_of_day.rb +56 -0
  112. data/lib/badline/version.rb +1 -1
  113. data/lib/badline/via/control_lines.rb +15 -0
  114. data/lib/badline/via/fast_forward.rb +15 -0
  115. data/lib/badline/via/interrupt_register.rb +9 -0
  116. data/lib/badline/via/shift_register.rb +13 -0
  117. data/lib/badline/via/state.rb +42 -0
  118. data/lib/badline/via/timer1.rb +19 -0
  119. data/lib/badline/via/timer2.rb +24 -0
  120. data/lib/badline/via.rb +2 -0
  121. data/lib/badline/vic/border_mask.rb +10 -0
  122. data/lib/badline/vic/collisions.rb +11 -0
  123. data/lib/badline/vic/color_patches.rb +11 -0
  124. data/lib/badline/vic/display_state.rb +29 -1
  125. data/lib/badline/vic/graphics_shifter.rb +26 -0
  126. data/lib/badline/vic/register_log.rb +18 -0
  127. data/lib/badline/vic/registers.rb +13 -2
  128. data/lib/badline/vic/sequencer.rb +12 -0
  129. data/lib/badline/vic/sequencer_state.rb +89 -0
  130. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  131. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  132. data/lib/badline/vic/sprite.rb +2 -0
  133. data/lib/badline/vic/sprites.rb +3 -0
  134. data/lib/badline/vic/sprites_state.rb +57 -0
  135. data/lib/badline/vic.rb +94 -10
  136. data/lib/badline.rb +1 -0
  137. metadata +37 -1
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class SID
5
+ class Waveform
6
+ # A snapshot of an oscillator: its phase, the noise register and the
7
+ # output latch, and the same fields as reSID keeps them, for VICE's
8
+ # SIDEXTENDED module. The model's tables and the voices it syncs with
9
+ # are the SID's wiring.
10
+ module State
11
+ # The fields reSID keeps for an oscillator between cycles: the phase,
12
+ # the noise register, its shift pipeline and reset countdown, the
13
+ # floating output's countdown and the pulse level.
14
+ def resid_fields
15
+ [@accumulator, @shift_register, @shift_pipeline, @shift_register_reset, @floating_ttl, @pulse]
16
+ end
17
+
18
+ # Takes the fields resid_fields gives from `fields`, from `at` on.
19
+ def restore_resid_fields(fields, at)
20
+ @accumulator = fields[at]
21
+ @shift_register = fields[at + 1]
22
+ @shift_pipeline = fields[at + 2]
23
+ @shift_register_reset = fields[at + 3]
24
+ @floating_ttl = fields[at + 4]
25
+ @pulse = fields[at + 5]
26
+ @stale = true
27
+ end
28
+
29
+ def save_state(out)
30
+ out.int(@accumulator).int(@delayed_triangle).int(@delayed_sawtooth)
31
+ out.int(@shift_register).int(@shift_register_reset).int(@shift_pipeline)
32
+ out.int(@frequency).int(@pulse_width).int(@selected)
33
+ [@test, @ring, @sync, @msb_rising].each { |flag| out.boolean(flag) }
34
+ out.int(@floating).int(@floating_ttl).int(@pulse).int(@output).boolean(@stale)
35
+ end
36
+
37
+ def load_state(input)
38
+ @accumulator = input.int
39
+ @delayed_triangle = input.int
40
+ @delayed_sawtooth = input.int
41
+ @shift_register = input.int
42
+ @shift_register_reset = input.int
43
+ @shift_pipeline = input.int
44
+ @frequency = input.int
45
+ @pulse_width = input.int
46
+ @selected = input.int
47
+ load_latches(input)
48
+ end
49
+
50
+ private
51
+
52
+ def load_latches(input)
53
+ @test = input.boolean?
54
+ @ring = input.boolean?
55
+ @sync = input.boolean?
56
+ @msb_rising = input.boolean?
57
+ @floating = input.int
58
+ @floating_ttl = input.int
59
+ @pulse = input.int
60
+ @output = input.int
61
+ @stale = input.boolean?
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,6 +3,7 @@
3
3
  require "badline/sid/waveform/combined"
4
4
  require "badline/sid/waveform/fast_forward"
5
5
  require "badline/sid/waveform/noise_writeback"
6
+ require "badline/sid/waveform/state"
6
7
 
7
8
  module Badline
8
9
  class SID
@@ -27,6 +28,7 @@ module Badline
27
28
  class Waveform
28
29
  include FastForward
29
30
  include NoiseWriteback
31
+ include State
30
32
 
31
33
  # All bits high at power up, odd ones stored inverted (SID/oscinit).
32
34
  POWER_ON_ACCUMULATOR = 0x555555
data/lib/badline/sid.rb CHANGED
@@ -125,6 +125,17 @@ module Badline
125
125
  @voices
126
126
  end
127
127
 
128
+ # Runs the DSP up to the current cycle.
129
+ def catch_up! = catch_up
130
+
131
+ # The byte the data bus holds and the cycles until it fades.
132
+ def bus_state = [@bus_value, @bus_ttl]
133
+
134
+ def restore_bus(value, ttl)
135
+ @bus_value = value
136
+ @bus_ttl = ttl
137
+ end
138
+
128
139
  def filter
129
140
  catch_up
130
141
  @filter
@@ -178,6 +189,29 @@ module Badline
178
189
  # Register contents as written, for save states and debugging.
179
190
  def register(reg) = @registers.peek(reg)
180
191
 
192
+ # The registers, the data bus, the voices and the filter, and the span
193
+ # the DSP has yet to catch up on, as it stands: saving leaves the SID
194
+ # as it was. Whether it synthesizes or records is the host's.
195
+ def save_state(out)
196
+ out.marker("SID")
197
+ @registers.save_state(out)
198
+ out.int(@bus_value).int(@bus_ttl).int(@pending_cycles).ints(@deferred_writes)
199
+ @voices.each { |voice| voice.save_state(out) }
200
+ @filter.save_state(out)
201
+ end
202
+
203
+ def load_state(input)
204
+ input.marker("SID")
205
+ @registers.load_state(input)
206
+ @bus_value = input.int
207
+ @bus_ttl = input.int
208
+ @pending_cycles = input.int
209
+ input.ints_into(@deferred_writes)
210
+ @voices.each { |voice| voice.load_state(input) }
211
+ @filter.load_state(input)
212
+ update_span_rules
213
+ end
214
+
181
215
  private
182
216
 
183
217
  def latch(value)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module Badline
6
+ module Snapshot
7
+ # One module of a snapshot: its name, its version and its payload,
8
+ # without the 22-byte module header.
9
+ Section = Data.define(:name, :major, :minor, :data) do
10
+ def version = "#{major}.#{minor}"
11
+
12
+ def to_s = "#{name} #{version}"
13
+ end
14
+
15
+ # The .vsf container as the VICE manual describes it: a magic string,
16
+ # the file format version and a machine name, an optional VICE version
17
+ # block, then modules one after another, each a 16-byte name, a major
18
+ # and minor version and a little-endian length that counts the header.
19
+ # A gzipped file reads as the file inside it, as VICE's own does.
20
+ class Container
21
+ MAGIC = "VICE Snapshot File\x1a".b
22
+ VERSION_MAGIC = "VICE Version\x1a".b
23
+ NAME_LENGTH = 16
24
+ MODULE_HEADER = NAME_LENGTH + 6
25
+ FORMAT_MAJOR = 2
26
+ FORMAT_MINOR = 0
27
+ # x64sc names its machine C64SC, and refuses a snapshot naming another.
28
+ MACHINE = "C64SC"
29
+ GZIP_MAGIC = "\x1f\x8b".b
30
+
31
+ # The VICE version block, when VICE wrote the file: four version
32
+ # bytes and a revision, kept as read.
33
+ attr_reader :machine, :sections, :vice_version
34
+
35
+ def self.parse(bytes)
36
+ bytes = bytes.b
37
+ bytes = gunzip(bytes) if bytes.start_with?(GZIP_MAGIC)
38
+ raise FormatError, "not a VICE snapshot" unless bytes.start_with?(MAGIC)
39
+
40
+ Reader.new(bytes).container
41
+ end
42
+
43
+ def self.gunzip(bytes)
44
+ Zlib.gunzip(bytes).b
45
+ rescue Zlib::Error => e
46
+ raise FormatError, "the gzipped snapshot is damaged: #{e.message}"
47
+ end
48
+
49
+ def initialize(sections = [], machine: MACHINE, vice_version: nil)
50
+ @machine = machine
51
+ @sections = sections
52
+ @vice_version = vice_version
53
+ end
54
+
55
+ def [](name) = @sections.find { |section| section.name == name }
56
+
57
+ def to_s
58
+ header = MAGIC + [FORMAT_MAJOR, FORMAT_MINOR].pack("CC") + padded(@machine)
59
+ header += VERSION_MAGIC + @vice_version if @vice_version
60
+ header + @sections.map { |section| encode(section) }.join
61
+ end
62
+
63
+ private
64
+
65
+ def padded(name)
66
+ raise ArgumentError, "#{name} is longer than #{NAME_LENGTH} bytes" if name.bytesize > NAME_LENGTH
67
+
68
+ name.b.ljust(NAME_LENGTH, "\0")
69
+ end
70
+
71
+ def encode(section)
72
+ padded(section.name) + [section.major, section.minor, MODULE_HEADER + section.data.bytesize].pack("CCV") +
73
+ section.data.b
74
+ end
75
+
76
+ # Splits the bytes of a snapshot into its header and modules.
77
+ class Reader
78
+ def initialize(bytes)
79
+ @bytes = bytes
80
+ @pos = MAGIC.bytesize + 2
81
+ end
82
+
83
+ def container
84
+ machine = take(NAME_LENGTH).delete("\0")
85
+ version = vice_version
86
+ sections = []
87
+ sections << section while @pos < @bytes.bytesize
88
+ Container.new(sections, machine:, vice_version: version)
89
+ end
90
+
91
+ private
92
+
93
+ def take(length)
94
+ raise FormatError, "snapshot ends early, at byte #{@bytes.bytesize}" if @pos + length > @bytes.bytesize
95
+
96
+ @bytes.byteslice(@pos, length).tap { @pos += length }
97
+ end
98
+
99
+ # VICE 2.4.30 and later write their version after the machine name.
100
+ def vice_version
101
+ return unless @bytes.byteslice(@pos, VERSION_MAGIC.bytesize) == VERSION_MAGIC
102
+
103
+ @pos += VERSION_MAGIC.bytesize
104
+ take(8)
105
+ end
106
+
107
+ def section
108
+ name = take(NAME_LENGTH).delete("\0")
109
+ major, minor, length = take(6).unpack("CCV")
110
+ raise FormatError, "module #{name} has a bad length #{length}" if length < MODULE_HEADER
111
+
112
+ Section.new(name:, major:, minor:, data: take(length - MODULE_HEADER))
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # Little-endian fields for VICE modules, written one after another as
6
+ # VICE's SMW_B, SMW_W, SMW_DW, SMW_QW and SMW_BA write them.
7
+ class FieldWriter
8
+ def initialize
9
+ @out = String.new(encoding: Encoding::BINARY)
10
+ end
11
+
12
+ def byte(value) = tap { @out << [value & 0xff].pack("C") }
13
+ def word(value) = tap { @out << [value & 0xffff].pack("v") }
14
+ def dword(value) = tap { @out << [value & 0xffff_ffff].pack("V") }
15
+ def qword(value) = tap { @out << [value & 0xffff_ffff_ffff_ffff].pack("Q<") }
16
+ def flag(value) = byte(value ? 1 : 0)
17
+
18
+ def bytes(values)
19
+ tap { @out << (values.is_a?(String) ? values.b : values.map { |v| v & 0xff }.pack("C*")) }
20
+ end
21
+
22
+ def zeros(count) = tap { @out << ("\0".b * count) }
23
+
24
+ def section(name, major, minor) = Section.new(name:, major:, minor:, data: @out.dup)
25
+ end
26
+
27
+ # Reads a VICE module's fields in the order they were written.
28
+ class FieldReader
29
+ def initialize(section)
30
+ @section = section
31
+ @data = section.data
32
+ @pos = 0
33
+ end
34
+
35
+ def byte = take(1).unpack1("C").to_i
36
+ def word = take(2).unpack1("v").to_i
37
+ def dword = take(4).unpack1("V").to_i
38
+ def qword = take(8).unpack1("Q<").to_i
39
+ def flag? = !byte.zero?
40
+ def bytes(count) = take(count).bytes
41
+ def skip(count) = tap { take(count) }
42
+
43
+ private
44
+
45
+ def take(count)
46
+ if @pos + count > @data.bytesize
47
+ raise FormatError, "#{@section.name} #{@section.version} ends early, at byte #{@data.bytesize}"
48
+ end
49
+
50
+ @data.byteslice(@pos, count).to_s.tap { @pos += count }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # What a restore applied and what it left out: module names, and a line
6
+ # for each thing left out, saying why.
7
+ Report = Data.define(:applied, :ignored)
8
+
9
+ # A snapshot read from a file.
10
+ class Image
11
+ attr_reader :container
12
+
13
+ def initialize(container)
14
+ @container = container
15
+ end
16
+
17
+ def sections = @container.sections
18
+
19
+ # Whether badline wrote it, with the whole machine in a BADLINE module.
20
+ def badline? = !section.nil?
21
+
22
+ # The State the BADLINE module holds.
23
+ def state = MachineState.state(section)
24
+
25
+ # A new machine, built as the snapshot's was and restored from it. A
26
+ # badline snapshot restores from its BADLINE module alone, which
27
+ # carries everything its VICE modules do, and a VICE one through the
28
+ # modules badline reads. Yields a line for each thing left out, or
29
+ # warns it without a block.
30
+ def load(&)
31
+ computer = badline? ? opening { Computer.restored(state) } : Vice.setup(@container).build
32
+ tell(badline? ? state_report(computer) : Vice::Restore.apply(@container, computer), &)
33
+ computer
34
+ end
35
+
36
+ # Restores `computer` from the snapshot, and returns the Report. A
37
+ # snapshot that fails leaves the machine as it was.
38
+ def restore(computer, &)
39
+ report = badline? ? restore_state(computer) : Vice::Restore.import(@container, computer)
40
+ tell(report, &)
41
+ report
42
+ end
43
+
44
+ private
45
+
46
+ def section = @container[MachineState::NAME]
47
+
48
+ def tell(report)
49
+ report.ignored.each { |line| block_given? ? yield(line) : warn("badline: #{line}") }
50
+ end
51
+
52
+ def restore_state(computer)
53
+ opening { computer.restore(state) }
54
+ state_report(computer)
55
+ end
56
+
57
+ def opening
58
+ yield
59
+ rescue SystemCallError => e
60
+ raise FormatError, "a file the snapshot names won't open: #{e.message}"
61
+ end
62
+
63
+ def state_report(computer)
64
+ lost = computer.init_handlers_lost
65
+ ignored = lost.positive? ? ["#{lost} on_init handler(s) the saved machine had yet to run, left out"] : []
66
+ Report.new(applied: [MachineState::NAME], ignored:)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # The BADLINE module: a State from Computer#snapshot, which restores the
6
+ # machine exactly where it was saved. VICE skips a module it doesn't
7
+ # know.
8
+ #
9
+ # The payload is deflated. Inside, a little-endian 32-bit length leads
10
+ # a run of BER-compressed integers (Ruby's pack "w"): the number of
11
+ # values and of strings, each string's length, then the values, each
12
+ # folded to a natural number (0, -1, 1, -2 ... as 0, 1, 2, 3 ...). The
13
+ # strings follow, one after another.
14
+ #
15
+ # A State only restores in the badline version that wrote it, since
16
+ # each chip writes its fields as it holds them.
17
+ module MachineState
18
+ NAME = "BADLINE"
19
+ MAJOR = 2
20
+ MINOR = 0
21
+
22
+ module_function
23
+
24
+ def section(state)
25
+ Section.new(name: NAME, major: MAJOR, minor: MINOR, data: Zlib.deflate(encode(state)))
26
+ end
27
+
28
+ def state(section)
29
+ unless section.major == MAJOR
30
+ raise FormatError, "#{NAME} #{section.version} was written by another badline version, " \
31
+ "and this one reads #{MAJOR}.#{MINOR}"
32
+ end
33
+
34
+ decode(Zlib.inflate(section.data))
35
+ rescue Zlib::Error => e
36
+ raise FormatError, "#{NAME} is damaged: #{e.message}"
37
+ end
38
+
39
+ def encode(state)
40
+ values = state.values
41
+ strings = state.strings
42
+ numbers = [values.length, strings.length]
43
+ strings.each { |string| numbers << string.bytesize }
44
+ values.each { |value| numbers << (value.negative? ? (-2 * value) - 1 : 2 * value) }
45
+ index = numbers.pack("w*")
46
+ [index.bytesize].pack("V") + index + strings.join
47
+ end
48
+
49
+ def decode(bytes)
50
+ raise FormatError, "#{NAME} ends early" if bytes.bytesize < 4
51
+
52
+ length = bytes.byteslice(0, 4).unpack1("V").to_i
53
+ numbers = bytes.byteslice(4, length).to_s.unpack("w*").map(&:to_i)
54
+ value_count = numbers.fetch(0, 0)
55
+ string_count = numbers.fetch(1, 0)
56
+ raise FormatError, "#{NAME} ends early" if numbers.length != 2 + string_count + value_count
57
+
58
+ State.new(values(numbers, 2 + string_count), strings(bytes, 4 + length, numbers.slice(2, string_count)))
59
+ end
60
+
61
+ def values(numbers, from)
62
+ Array.new(numbers.length - from) do |i|
63
+ folded = numbers[from + i]
64
+ folded.odd? ? -((folded + 1) / 2) : folded / 2
65
+ end
66
+ end
67
+
68
+ def strings(bytes, pos, lengths)
69
+ lengths.map do |length|
70
+ raise FormatError, "#{NAME} ends early" if pos + length > bytes.bytesize
71
+
72
+ bytes.byteslice(pos, length).to_s.tap { pos += length }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # How a machine was built: its VIC, CIA and SID models, its region, its
6
+ # RAM expansion and the size of its REU, the arguments Computer.new
7
+ # takes. A State starts with them, so a machine to restore it into can
8
+ # be built first.
9
+ Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu)
10
+
11
+ class Setup
12
+ SID_MODELS = %i[mos6581 mos8580].freeze
13
+ REGIONS = [Region::PAL, Region::NTSC].freeze
14
+ RAM_EXPANSIONS = [:none, *RAMExpansion::TYPES.keys].freeze
15
+
16
+ def self.of(address_bus)
17
+ new(vic_model: address_bus.vic.model, cia_model: address_bus.cia1.model, sid_model: address_bus.sid.model,
18
+ region: address_bus.region, ram_expansion: address_bus.ram_expansion.type, reu: address_bus.reu&.size_kb)
19
+ end
20
+
21
+ def self.read(input)
22
+ new(vic_model: VIC::MODELS.fetch(input.int), cia_model: CIA::MODELS.fetch(input.int),
23
+ sid_model: SID_MODELS.fetch(input.int), region: REGIONS.fetch(input.int),
24
+ ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int))
25
+ rescue IndexError
26
+ raise FormatError, "the state names a chip model, region or RAM expansion badline doesn't know"
27
+ end
28
+
29
+ def self.ram_expansion(name) = name == :none ? nil : name
30
+
31
+ # An REU's size in K, written as 0 for none.
32
+ def self.reu(size_kb)
33
+ return nil if size_kb.zero?
34
+ return size_kb if REU::SIZES_KB.include?(size_kb)
35
+
36
+ raise FormatError, "the state names a #{size_kb}K REU, which badline doesn't build"
37
+ end
38
+
39
+ def write(out)
40
+ out.int(VIC::MODELS.index(vic_model)).int(CIA::MODELS.index(cia_model))
41
+ out.int(SID_MODELS.index(sid_model)).int(REGIONS.index(region))
42
+ out.int(RAM_EXPANSIONS.index(ram_expansion || :none)).int(reu || 0)
43
+ end
44
+
45
+ # A machine built this way, at power-on.
46
+ def build
47
+ Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:)
48
+ end
49
+
50
+ def to_s
51
+ parts = [vic_model, cia_model, sid_model, region.name]
52
+ parts << ram_expansion if ram_expansion
53
+ parts << "a #{reu}K REU" if reu
54
+ parts.join(", ")
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # A file that isn't a snapshot badline reads, is cut short, or holds a
6
+ # state that doesn't line up with the machine reading it.
7
+ class FormatError < StandardError; end
8
+
9
+ # A machine's state, held in memory: the Integers each chip writes in
10
+ # turn, and the byte strings for memories, names and paths. Booleans
11
+ # are written as 0 and 1. Computer#snapshot takes one and
12
+ # Computer#restore puts it back.
13
+ class State
14
+ # The layout the chips write their fields in. It goes up whenever a
15
+ # save_state writes something else, so a State in another layout
16
+ # fails before anything is read into a machine.
17
+ SCHEMA = 1
18
+
19
+ attr_reader :values, :strings
20
+
21
+ def initialize(values, strings)
22
+ @values = values
23
+ @strings = strings
24
+ end
25
+
26
+ def ==(other)
27
+ other.is_a?(State) && values == other.values && strings == other.strings
28
+ end
29
+ end
30
+
31
+ # Writes a State. Each chip's save_state writes its fields through one
32
+ # of these, and its load_state reads them back from a StateReader in
33
+ # the same order.
34
+ class StateWriter
35
+ attr_reader :state
36
+
37
+ def initialize
38
+ @values = []
39
+ @strings = []
40
+ @state = State.new(@values, @strings)
41
+ end
42
+
43
+ def int(value)
44
+ raise TypeError, "not an Integer: #{value.inspect}" unless value.is_a?(Integer)
45
+
46
+ @values << value.to_i
47
+ self
48
+ end
49
+
50
+ def boolean(value)
51
+ @values << (value ? 1 : 0)
52
+ self
53
+ end
54
+
55
+ def optional_int(value)
56
+ return boolean(false) if value.nil?
57
+
58
+ boolean(true).int(value)
59
+ end
60
+
61
+ def ints(values)
62
+ int(values.length)
63
+ values.each { |value| int(value) }
64
+ self
65
+ end
66
+
67
+ def booleans(values)
68
+ int(values.length)
69
+ values.each { |value| boolean(value) }
70
+ self
71
+ end
72
+
73
+ # Bytes, 0 to 255 each, kept as one string.
74
+ def blob(values)
75
+ @strings << values.pack("C*")
76
+ self
77
+ end
78
+
79
+ def string(value)
80
+ @strings << value.to_s.b
81
+ self
82
+ end
83
+
84
+ def optional_string(value)
85
+ return boolean(false) if value.nil?
86
+
87
+ boolean(true).string(value)
88
+ end
89
+
90
+ # A name a reader checks for, so a state that has gone out of step
91
+ # fails there instead of loading the wrong fields.
92
+ def marker(name) = string(name)
93
+
94
+ # The State's layout and the badline version writing it, which a
95
+ # reader checks first (StateReader#check_stamp).
96
+ def stamp = int(State::SCHEMA).string(VERSION)
97
+ end
98
+
99
+ # Reads a State back in the order a StateWriter wrote it.
100
+ #
101
+ # A detached reader restores a machine that opens no host file and
102
+ # writes none, for a copy the machine runs on its own: a disk image
103
+ # comes from the bytes the state holds, write-protected, a directory
104
+ # or .t64 archive device 8 serves and a tape are left out, and a true
105
+ # drive's disk keeps the state's tracks without its image file.
106
+ class StateReader
107
+ def initialize(state, detached: false)
108
+ @values = state.values
109
+ @strings = state.strings
110
+ @value = 0
111
+ @string = 0
112
+ @detached = detached
113
+ end
114
+
115
+ def detached? = @detached
116
+
117
+ def int
118
+ raise FormatError, "the state ends early, at value #{@value}" if @value >= @values.length
119
+
120
+ value = @values[@value]
121
+ @value += 1
122
+ value
123
+ end
124
+
125
+ def boolean?
126
+ value = int
127
+ raise FormatError, "expected a boolean in the state, found #{value}" unless value.between?(0, 1)
128
+
129
+ value == 1
130
+ end
131
+
132
+ def optional_int = boolean? ? int : nil
133
+
134
+ def ints = Array.new(int) { int }
135
+
136
+ def booleans = Array.new(int) { boolean? }
137
+
138
+ # Reads into an existing array, in place, so whatever else holds it
139
+ # sees the values.
140
+ def ints_into(array) = array.replace(ints)
141
+
142
+ def booleans_into(array) = array.replace(booleans)
143
+
144
+ def blob = string.bytes
145
+
146
+ def blob_into(array) = array.replace(blob)
147
+
148
+ def string
149
+ raise FormatError, "the state ends early, at string #{@string}" if @string >= @strings.length
150
+
151
+ value = @strings[@string]
152
+ @string += 1
153
+ value
154
+ end
155
+
156
+ def optional_string = boolean? ? string : nil
157
+
158
+ def marker(name)
159
+ found = string
160
+ raise FormatError, "expected #{name} in the state, found #{found[0, 20].inspect}" unless found == name
161
+ end
162
+
163
+ # Fails unless the State was written in this layout by this badline
164
+ # version.
165
+ def check_stamp
166
+ schema = int
167
+ version = string
168
+ return if schema == State::SCHEMA && version == VERSION
169
+
170
+ raise FormatError, "the state was written by badline #{version.inspect} in layout #{schema}, " \
171
+ "and this is badline #{VERSION}, layout #{State::SCHEMA}"
172
+ end
173
+
174
+ # Whether every value and string has been read.
175
+ def finished? = @value == @values.length && @string == @strings.length
176
+ end
177
+ end
178
+ end