badline 0.13.0 → 0.14.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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/CLAUDE.md +27 -7
  4. data/CONTRIBUTING.md +4 -3
  5. data/README.md +8 -5
  6. data/doc/media.md +2 -1
  7. data/doc/pinned-behaviour.md +29 -0
  8. data/doc/snapshot-format.md +1047 -0
  9. data/doc/snapshots.md +72 -39
  10. data/lib/badline/audio/bare_player.rb +50 -17
  11. data/lib/badline/audio/machine_player.rb +15 -9
  12. data/lib/badline/audio/renderer.rb +32 -21
  13. data/lib/badline/c128/banks.rb +6 -3
  14. data/lib/badline/c128/bus/keyed_state.rb +45 -0
  15. data/lib/badline/c128/bus.rb +17 -3
  16. data/lib/badline/c128/drives.rb +6 -25
  17. data/lib/badline/c128/keyed_modules.rb +117 -0
  18. data/lib/badline/c128/keyed_state.rb +91 -0
  19. data/lib/badline/c128/keys.rb +9 -0
  20. data/lib/badline/c128/mmu.rb +9 -8
  21. data/lib/badline/c128/mmu_pages.rb +20 -0
  22. data/lib/badline/c128/modes.rb +19 -9
  23. data/lib/badline/c128/z80_turns.rb +1 -1
  24. data/lib/badline/c128.rb +8 -8
  25. data/lib/badline/c64/attachments.rb +0 -44
  26. data/lib/badline/c64/banked_ram/plus256k.rb +18 -10
  27. data/lib/badline/c64/banked_ram/plus60k.rb +8 -6
  28. data/lib/badline/c64/banked_ram/unexpanded.rb +2 -2
  29. data/lib/badline/c64/bus/debug_register.rb +32 -0
  30. data/lib/badline/c64/bus/keyed_state.rb +41 -0
  31. data/lib/badline/c64/bus.rb +2 -1
  32. data/lib/badline/c64/cartridge/action_replay.rb +0 -13
  33. data/lib/badline/c64/cartridge/easy_flash.rb +0 -15
  34. data/lib/badline/c64/cartridge/epyx_fastload.rb +0 -8
  35. data/lib/badline/c64/cartridge/final_cartridge3.rb +0 -12
  36. data/lib/badline/c64/cartridge/flash.rb +3 -3
  37. data/lib/badline/c64/cartridge/g_mod2.rb +0 -12
  38. data/lib/badline/c64/cartridge/geo_ram.rb +0 -16
  39. data/lib/badline/c64/cartridge/kcs_power.rb +0 -8
  40. data/lib/badline/c64/cartridge/keyed_state.rb +393 -0
  41. data/lib/badline/c64/cartridge/pagefox.rb +0 -10
  42. data/lib/badline/c64/cartridge/retro_replay.rb +0 -8
  43. data/lib/badline/c64/cartridge/rgcd.rb +0 -10
  44. data/lib/badline/c64/cartridge/super_games.rb +0 -10
  45. data/lib/badline/c64/cartridge/zaxxon.rb +0 -16
  46. data/lib/badline/c64/cartridge.rb +1 -27
  47. data/lib/badline/c64/kernal_traps.rb +28 -25
  48. data/lib/badline/c64/keyed_modules.rb +111 -0
  49. data/lib/badline/c64/keyed_state.rb +68 -0
  50. data/lib/badline/c64/reu/dma.rb +0 -4
  51. data/lib/badline/c64/reu/keyed_state.rb +140 -0
  52. data/lib/badline/c64/reu/ram.rb +0 -15
  53. data/lib/badline/c64/reu.rb +1 -3
  54. data/lib/badline/c64/true_drives.rb +56 -5
  55. data/lib/badline/c64.rb +7 -3
  56. data/lib/badline/chips/cia/interrupt_register.rb +0 -17
  57. data/lib/badline/chips/cia/keyed_parts.rb +213 -0
  58. data/lib/badline/chips/cia/serial.rb +0 -4
  59. data/lib/badline/chips/cia/time_of_day.rb +0 -22
  60. data/lib/badline/chips/cia/timer.rb +0 -20
  61. data/lib/badline/chips/cia.rb +2 -2
  62. data/lib/badline/chips/cpu/core.rb +18 -2
  63. data/lib/badline/chips/cpu/keyed_state.rb +177 -0
  64. data/lib/badline/chips/cpu/microcode.rb +16 -0
  65. data/lib/badline/chips/cpu/port.rb +20 -3
  66. data/lib/badline/chips/cpu.rb +5 -0
  67. data/lib/badline/chips/sid/decimator.rb +10 -8
  68. data/lib/badline/chips/sid/envelope.rb +1 -6
  69. data/lib/badline/chips/sid/filter.rb +0 -14
  70. data/lib/badline/chips/sid/keyed_parts.rb +286 -0
  71. data/lib/badline/chips/sid/voice.rb +0 -10
  72. data/lib/badline/chips/sid/waveform.rb +0 -2
  73. data/lib/badline/chips/sid.rb +1 -2
  74. data/lib/badline/chips/ted/beam.rb +119 -0
  75. data/lib/badline/chips/ted/timers.rb +66 -0
  76. data/lib/badline/chips/ted.rb +143 -0
  77. data/lib/badline/chips/tia.rb +49 -0
  78. data/lib/badline/chips/vdc/keyed_parts.rb +105 -0
  79. data/lib/badline/chips/vdc/painter.rb +0 -12
  80. data/lib/badline/chips/vdc/raster.rb +0 -16
  81. data/lib/badline/chips/vdc/window.rb +0 -9
  82. data/lib/badline/chips/vdc.rb +1 -2
  83. data/lib/badline/chips/via/control_lines.rb +0 -15
  84. data/lib/badline/chips/via/interrupt_register.rb +0 -9
  85. data/lib/badline/chips/via/keyed_parts.rb +163 -0
  86. data/lib/badline/chips/via/shift_register.rb +0 -14
  87. data/lib/badline/chips/via/timer1.rb +0 -13
  88. data/lib/badline/chips/via/timer2.rb +0 -18
  89. data/lib/badline/chips/via.rb +1 -2
  90. data/lib/badline/chips/vic/border_mask.rb +0 -10
  91. data/lib/badline/chips/vic/collisions.rb +0 -11
  92. data/lib/badline/chips/vic/color_patches.rb +0 -11
  93. data/lib/badline/chips/vic/display_state.rb +0 -19
  94. data/lib/badline/chips/vic/graphics_shifter.rb +0 -4
  95. data/lib/badline/chips/vic/keyed_parts.rb +228 -0
  96. data/lib/badline/chips/vic/keyed_sprites.rb +225 -0
  97. data/lib/badline/chips/vic/keyed_state.rb +102 -0
  98. data/lib/badline/chips/vic/register_log.rb +0 -18
  99. data/lib/badline/chips/vic/registers.rb +0 -9
  100. data/lib/badline/chips/vic/sequencer.rb +0 -2
  101. data/lib/badline/chips/vic/sprite/internal_bus.rb +0 -11
  102. data/lib/badline/chips/vic/sprite.rb +0 -2
  103. data/lib/badline/chips/vic/sprites.rb +0 -3
  104. data/lib/badline/chips/vic.rb +6 -2
  105. data/lib/badline/chips/vic_i/keyed_parts.rb +142 -0
  106. data/lib/badline/chips/vic_i/painter.rb +0 -4
  107. data/lib/badline/chips/vic_i/sound.rb +38 -46
  108. data/lib/badline/chips/vic_i.rb +2 -2
  109. data/lib/badline/chips/z80/core.rb +2 -2
  110. data/lib/badline/chips/z80/keyed_state.rb +73 -0
  111. data/lib/badline/chips.rb +10 -0
  112. data/lib/badline/core.rb +4 -21
  113. data/lib/badline/drives/datasette/keyed_state.rb +38 -0
  114. data/lib/badline/drives/datasette.rb +40 -7
  115. data/lib/badline/drives/engine/core.rb +3 -0
  116. data/lib/badline/drives/engine/serial_lines.rb +4 -7
  117. data/lib/badline/drives/model1541/bus.rb +0 -13
  118. data/lib/badline/drives/model1541/disk.rb +9 -8
  119. data/lib/badline/drives/model1541/keyed_state.rb +251 -0
  120. data/lib/badline/drives/model1541/mechanism.rb +0 -2
  121. data/lib/badline/drives/model1541.rb +1 -2
  122. data/lib/badline/drives/model1571/bus.rb +0 -13
  123. data/lib/badline/drives/model1571/keyed_state.rb +108 -0
  124. data/lib/badline/drives/model1571/wd1770.rb +0 -14
  125. data/lib/badline/drives/model1571.rb +1 -2
  126. data/lib/badline/drives/model1581/bus.rb +0 -13
  127. data/lib/badline/drives/model1581/disk.rb +0 -4
  128. data/lib/badline/drives/model1581/keyed_state.rb +280 -0
  129. data/lib/badline/drives/model1581/mechanism.rb +0 -21
  130. data/lib/badline/drives/model1581/slot.rb +0 -18
  131. data/lib/badline/drives/model1581/wd1772.rb +0 -2
  132. data/lib/badline/drives/model1581.rb +2 -2
  133. data/lib/badline/drives.rb +8 -0
  134. data/lib/badline/frontend/app.rb +3 -3
  135. data/lib/badline/frontend/boot.rb +1 -1
  136. data/lib/badline/frontend/controls.rb +19 -3
  137. data/lib/badline/frontend/drive_led.rb +44 -21
  138. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -2
  139. data/lib/badline/frontend/snapshots.rb +12 -1
  140. data/lib/badline/frontend/timeline.rb +28 -10
  141. data/lib/badline/input/matrix_typist.rb +98 -0
  142. data/lib/badline/input.rb +1 -0
  143. data/lib/badline/kernal_trap/chrout.rb +45 -0
  144. data/lib/badline/kernal_trap/dos/channel.rb +0 -27
  145. data/lib/badline/kernal_trap/dos/channels.rb +0 -25
  146. data/lib/badline/kernal_trap/dos/memory.rb +0 -9
  147. data/lib/badline/kernal_trap/dos/status.rb +0 -8
  148. data/lib/badline/kernal_trap/dos/write_file.rb +0 -18
  149. data/lib/badline/kernal_trap/dos.rb +0 -19
  150. data/lib/badline/kernal_trap/file_routine.rb +4 -4
  151. data/lib/badline/kernal_trap/keyed_state.rb +150 -0
  152. data/lib/badline/kernal_trap/layout.rb +75 -11
  153. data/lib/badline/kernal_trap/load.rb +14 -16
  154. data/lib/badline/kernal_trap/routine.rb +12 -0
  155. data/lib/badline/kernal_trap/save.rb +8 -18
  156. data/lib/badline/kernal_trap/serial.rb +1 -17
  157. data/lib/badline/kernal_trap.rb +2 -0
  158. data/lib/badline/machine/keyboard_buffer.rb +49 -0
  159. data/lib/badline/machine/settling.rb +29 -0
  160. data/lib/badline/machine.rb +5 -2
  161. data/lib/badline/media/boot_disk.rb +14 -2
  162. data/lib/badline/media/extensions.rb +10 -0
  163. data/lib/badline/memory.rb +3 -6
  164. data/lib/badline/options/family.rb +5 -2
  165. data/lib/badline/options/help.rb +42 -16
  166. data/lib/badline/options/table.rb +2 -0
  167. data/lib/badline/options/validation.rb +2 -0
  168. data/lib/badline/options.rb +6 -1
  169. data/lib/badline/plus4/acia.rb +45 -0
  170. data/lib/badline/plus4/bus.rb +312 -0
  171. data/lib/badline/plus4/cpu.rb +10 -0
  172. data/lib/badline/plus4/cpu_port.rb +93 -0
  173. data/lib/badline/plus4/keyboard_port.rb +51 -0
  174. data/lib/badline/plus4/model.rb +38 -0
  175. data/lib/badline/plus4/user_port.rb +31 -0
  176. data/lib/badline/plus4.rb +164 -0
  177. data/lib/badline/roms/README +38 -0
  178. data/lib/badline/roms/plus4/basic.rom +0 -0
  179. data/lib/badline/roms/plus4/function-high.rom +0 -0
  180. data/lib/badline/roms/plus4/function-low.rom +0 -0
  181. data/lib/badline/roms/plus4/kernal-ntsc.rom +0 -0
  182. data/lib/badline/roms/plus4/kernal-pal.rom +0 -0
  183. data/lib/badline/snapshot/c128_setup.rb +9 -20
  184. data/lib/badline/snapshot/c64_setup.rb +28 -24
  185. data/lib/badline/snapshot/container.rb +4 -0
  186. data/lib/badline/snapshot/field_doc/markdown.rb +112 -0
  187. data/lib/badline/snapshot/field_doc.rb +109 -0
  188. data/lib/badline/snapshot/image.rb +134 -28
  189. data/lib/badline/snapshot/keyed/cursor.rb +40 -0
  190. data/lib/badline/snapshot/keyed/modules.rb +66 -0
  191. data/lib/badline/snapshot/keyed/reader.rb +188 -0
  192. data/lib/badline/snapshot/keyed/recorder.rb +88 -0
  193. data/lib/badline/snapshot/keyed/report.rb +26 -0
  194. data/lib/badline/snapshot/keyed/writer.rb +55 -0
  195. data/lib/badline/snapshot/keyed.rb +64 -0
  196. data/lib/badline/snapshot/machine_state.rb +17 -64
  197. data/lib/badline/snapshot/storage_setup.rb +29 -34
  198. data/lib/badline/snapshot/vic20_setup.rb +6 -15
  199. data/lib/badline/snapshot/vice/reu1764.rb +2 -2
  200. data/lib/badline/snapshot/vice.rb +7 -31
  201. data/lib/badline/snapshot.rb +43 -20
  202. data/lib/badline/storage/crt_file.rb +3 -1
  203. data/lib/badline/storage/g64_image.rb +8 -3
  204. data/lib/badline/storage/tap.rb +43 -15
  205. data/lib/badline/version.rb +1 -1
  206. data/lib/badline/vic20/attachments.rb +3 -0
  207. data/lib/badline/vic20/bus/keyed_state.rb +52 -0
  208. data/lib/badline/vic20/bus.rb +1 -2
  209. data/lib/badline/vic20/keyed_modules.rb +73 -0
  210. data/lib/badline/vic20/keyed_state.rb +45 -0
  211. data/lib/badline/vic20.rb +5 -5
  212. data/lib/badline.rb +1 -0
  213. metadata +61 -42
  214. data/lib/badline/c128/bus/saved_state.rb +0 -53
  215. data/lib/badline/c128/saved_state.rb +0 -117
  216. data/lib/badline/c64/bus/saved_state.rb +0 -68
  217. data/lib/badline/c64/cartridge/flash/saved_state.rb +0 -40
  218. data/lib/badline/c64/cartridge/retro_replay/saved_state.rb +0 -47
  219. data/lib/badline/c64/cartridge/saved_state.rb +0 -63
  220. data/lib/badline/c64/reu/dma/saved_state.rb +0 -52
  221. data/lib/badline/c64/reu/saved_state.rb +0 -55
  222. data/lib/badline/c64/saved_state.rb +0 -112
  223. data/lib/badline/chips/cia/saved_state.rb +0 -49
  224. data/lib/badline/chips/cia/serial/saved_state.rb +0 -45
  225. data/lib/badline/chips/cpu/saved_state.rb +0 -117
  226. data/lib/badline/chips/sid/envelope/saved_state.rb +0 -70
  227. data/lib/badline/chips/sid/filter/saved_state.rb +0 -42
  228. data/lib/badline/chips/sid/saved_state.rb +0 -52
  229. data/lib/badline/chips/sid/waveform/saved_state.rb +0 -68
  230. data/lib/badline/chips/vdc/saved_state.rb +0 -36
  231. data/lib/badline/chips/via/saved_state.rb +0 -44
  232. data/lib/badline/chips/vic/graphics_shifter/saved_state.rb +0 -40
  233. data/lib/badline/chips/vic/saved_state.rb +0 -82
  234. data/lib/badline/chips/vic/sequencer/saved_state.rb +0 -91
  235. data/lib/badline/chips/vic/sprite/saved_state.rb +0 -66
  236. data/lib/badline/chips/vic/sprites/saved_state.rb +0 -59
  237. data/lib/badline/chips/vic_i/painter/saved_state.rb +0 -40
  238. data/lib/badline/chips/vic_i/saved_state.rb +0 -55
  239. data/lib/badline/chips/z80/saved_state.rb +0 -71
  240. data/lib/badline/chrout_trap.rb +0 -43
  241. data/lib/badline/debug_register.rb +0 -28
  242. data/lib/badline/drives/datasette/saved_state.rb +0 -37
  243. data/lib/badline/drives/model1541/disk/saved_state.rb +0 -88
  244. data/lib/badline/drives/model1541/mechanism/saved_state.rb +0 -72
  245. data/lib/badline/drives/model1541/saved_state.rb +0 -44
  246. data/lib/badline/drives/model1571/saved_state.rb +0 -47
  247. data/lib/badline/drives/model1581/disk/saved_state.rb +0 -89
  248. data/lib/badline/drives/model1581/saved_state.rb +0 -42
  249. data/lib/badline/drives/model1581/wd1772/saved_state.rb +0 -68
  250. data/lib/badline/keyboard_buffer.rb +0 -33
  251. data/lib/badline/snapshot/setup.rb +0 -31
  252. data/lib/badline/snapshot/state.rb +0 -179
  253. data/lib/badline/vic20/bus/saved_state.rb +0 -35
  254. data/lib/badline/vic20/saved_state.rb +0 -110
@@ -2,37 +2,109 @@
2
2
 
3
3
  module Badline
4
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)
5
+ # What a restore applied and what it left out: module names and a line
6
+ # for each thing left out or changed, saying why. For a badline
7
+ # snapshot, `modules` holds each BL-* module's Keyed::Report, which
8
+ # give the fields per module this badline doesn't know, the modules it
9
+ # expected and didn't find, and the values it read other than as
10
+ # written, and `written_by` the badline version that saved it.
11
+ class Report
12
+ attr_reader :applied, :ignored, :written_by
13
+
14
+ def initialize(applied:, ignored:, modules: [], written_by: "")
15
+ @applied = applied
16
+ @ignored = ignored
17
+ @modules = modules
18
+ @written_by = written_by
19
+ end
20
+
21
+ # The keys nothing read, by module, for the modules that had any.
22
+ def unknown
23
+ @modules.reject { |report| report.unknown.empty? }.to_h { |report| [report.name, report.unknown] }
24
+ end
25
+
26
+ # The modules the snapshot didn't have.
27
+ def missing = @modules.select(&:missing).map(&:name)
8
28
 
9
- # A snapshot read from a file.
29
+ # A line for each value read other than as written.
30
+ def adjusted = @modules.flat_map { |report| report.adjusted.map { |line| "#{report.name}: #{line}" } }
31
+ end
32
+
33
+ # A snapshot: the container and its modules, read from a file or taken
34
+ # of a machine in memory (.of).
10
35
  class Image
36
+ MACHINE = "BL-MACHINE"
37
+ FAMILIES = { "c64" => "C64", "vic20" => "VIC-20", "c128" => "C128" }.freeze
38
+
11
39
  attr_reader :container
12
40
 
13
- def initialize(container)
41
+ # `name` is what a refusal calls the snapshot, such as its file's name.
42
+ def initialize(container, name: "the snapshot")
14
43
  @container = container
44
+ @name = name
45
+ end
46
+
47
+ # The machine's snapshot, saved as a file saves it: the machine runs
48
+ # on to the end of the instruction its CPU is in, and of an REU
49
+ # transfer under way (Machine::Settling), then writes its BL-*
50
+ # modules.
51
+ def self.of(computer)
52
+ computer.settle_for_snapshot!
53
+ new(Container.new(computer.save_modules, machine: machine_name(computer)))
54
+ end
55
+
56
+ # The snapshot with VICE's modules ahead of the BL-* ones for a C64,
57
+ # so x64sc opens it too.
58
+ def self.exported(computer)
59
+ computer.settle_for_snapshot!
60
+ sections = computer.save_modules
61
+ sections = Vice.export(computer) + sections if computer.family == :c64
62
+ new(Container.new(sections, machine: machine_name(computer)))
63
+ end
64
+
65
+ # The machine as VICE's emulators name it.
66
+ def self.machine_name(computer)
67
+ case computer.family
68
+ when :vic20 then Container::VIC20
69
+ when :c128 then Container::C128
70
+ else Container::MACHINE
71
+ end
72
+ end
73
+
74
+ # How the machine was built, from the family BL-MACHINE, read by
75
+ # `machine`, names.
76
+ def self.setup(machine)
77
+ case machine.choice("family", FAMILIES.keys, "c64")
78
+ when "vic20" then Vic20Setup.read_fields(machine)
79
+ when "c128" then C128Setup.read_fields(machine)
80
+ else C64Setup.read_fields(machine)
81
+ end
15
82
  end
16
83
 
17
84
  def sections = @container.sections
18
85
 
19
- # Whether badline wrote it, with the whole machine in a BADLINE module.
20
- def badline? = !section.nil?
86
+ def to_s = @container.to_s
21
87
 
22
- # The State the BADLINE module holds.
23
- def state = MachineState.state(section)
88
+ def ==(other) = other.is_a?(Image) && to_s == other.to_s
89
+
90
+ # Whether badline wrote it, with the whole machine in BL-* modules.
91
+ def badline? = !@container[MACHINE].nil?
24
92
 
25
93
  # 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. xvic's and x128's own modules aren't read.
29
- # Yields a line for each thing left out, or warns it without a block.
30
- def load(&)
94
+ # badline snapshot restores from its BL-* modules alone, and a VICE
95
+ # one through the modules badline reads. xvic's and x128's own
96
+ # modules aren't read. Yields a line for each thing left out, or
97
+ # warns it without a block. `detached` restores it without the host's
98
+ # files (Keyed::Modules).
99
+ def load(detached: false, &)
100
+ refuse_old_layout
31
101
  raise FormatError, "badline reads only its own VIC-20 snapshots, not xvic's" if vic20? && !badline?
32
102
  raise FormatError, "badline reads only its own C128 snapshots, not x128's" if c128? && !badline?
103
+ return load_vice(&) unless badline?
33
104
 
34
- computer = badline? ? opening { Setup.restored(state) } : Vice.setup(@container).build
35
- tell(badline? ? state_report(computer) : Vice::Restore.apply(@container, computer), &)
105
+ modules = Keyed::Modules.new(sections, detached:)
106
+ computer = opening { Image.setup(modules.machine).build.load_modules(modules) }
107
+ tell(report(modules, computer), &)
36
108
  computer
37
109
  end
38
110
 
@@ -42,25 +114,55 @@ module Badline
42
114
  # Whether the machine is a C128, as the container names it.
43
115
  def c128? = @container.machine == Container::C128
44
116
 
45
- # Restores `computer` from the snapshot, and returns the Report. A
46
- # snapshot that fails leaves the machine as it was.
117
+ # Restores `computer` from the snapshot, and returns the Report. The
118
+ # snapshot goes into a new machine first, so one that fails, or is of
119
+ # a machine built another way, leaves `computer` as it was.
47
120
  def restore(computer, &)
48
- report = badline? ? restore_state(computer) : Vice::Restore.import(@container, computer)
121
+ refuse_old_layout
122
+ report = badline? ? restore_modules(computer) : Vice::Restore.import(@container, computer)
49
123
  tell(report, &)
50
124
  report
51
125
  end
52
126
 
53
127
  private
54
128
 
55
- def section = @container[MachineState::NAME]
129
+ def load_vice(&)
130
+ computer = Vice.setup(@container).build
131
+ tell(Vice::Restore.apply(@container, computer), &)
132
+ computer
133
+ end
56
134
 
57
- def tell(report)
58
- report.ignored.each { |line| block_given? ? yield(line) : warn("badline: #{line}") }
135
+ # The snapshot goes into a new machine first, so one that fails
136
+ # leaves `computer` as it was.
137
+ def restore_modules(computer)
138
+ modules = Keyed::Modules.new(sections)
139
+ family = modules.machine.choice("family", FAMILIES.keys, "c64")
140
+ unless family == computer.family.to_s
141
+ raise FormatError, "the snapshot is of a #{FAMILIES.fetch(family, family)}, " \
142
+ "not a #{FAMILIES.fetch(computer.family.to_s)}"
143
+ end
144
+
145
+ trial = Keyed::Modules.new(sections)
146
+ opening { Image.setup(trial.machine).build.load_modules(trial) }
147
+ opening { computer.load_modules(modules) }
148
+ report(modules, computer)
59
149
  end
60
150
 
61
- def restore_state(computer)
62
- opening { computer.restore(state) }
63
- state_report(computer)
151
+ # A snapshot from a badline before the keyed modules, which only the
152
+ # version that saved it reads.
153
+ def refuse_old_layout
154
+ old = @container[MachineState::NAME]
155
+ return if old.nil? || badline?
156
+
157
+ version = MachineState.written_by(old)
158
+ layout = "in the old snapshot layout, which this badline can't read."
159
+ raise FormatError, "#{@name} was saved by an earlier badline #{layout}" if version.empty?
160
+
161
+ raise FormatError, "#{@name} was saved by badline #{version} #{layout} It still opens in badline #{version}."
162
+ end
163
+
164
+ def tell(report)
165
+ report.ignored.each { |line| block_given? ? yield(line) : warn("badline: #{line}") }
64
166
  end
65
167
 
66
168
  def opening
@@ -69,10 +171,14 @@ module Badline
69
171
  raise FormatError, "a file the snapshot names won't open: #{e.message}"
70
172
  end
71
173
 
72
- def state_report(computer)
174
+ def report(modules, computer)
175
+ written_by = modules.machine.text("written_by", "")
176
+ reports = modules.readers.map(&:report)
177
+ ignored = reports.flat_map(&:lines)
178
+ ignored.concat(modules.unread.map { |name| "#{name}: badline doesn't read this module, left out" })
73
179
  lost = computer.init_handlers_lost
74
- ignored = lost.positive? ? ["#{lost} on_init handler(s) the saved machine had yet to run, left out"] : []
75
- Report.new(applied: [MachineState::NAME], ignored:)
180
+ ignored << "#{lost} on_init handler(s) the saved machine had yet to run, left out" if lost.positive?
181
+ Report.new(applied: reports.reject(&:missing).map(&:name), ignored:, modules: reports, written_by:)
76
182
  end
77
183
  end
78
184
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Keyed
6
+ # Reads BER numbers and byte runs from a module's payload, or from one
7
+ # record's value, failing on anything cut short.
8
+ class Cursor
9
+ def initialize(bytes, name)
10
+ @bytes = bytes
11
+ @name = name
12
+ @pos = 0
13
+ end
14
+
15
+ def done? = @pos >= @bytes.bytesize
16
+
17
+ def remaining = @bytes.bytesize - @pos
18
+
19
+ def ber
20
+ value = 0
21
+ loop do
22
+ raise FormatError, "#{@name} holds a number too large for a snapshot" if value >= (1 << 56)
23
+
24
+ byte = take(1).getbyte(0).to_i
25
+ value = (value << 7) | (byte & 0x7f)
26
+ return value if byte < 0x80
27
+ end
28
+ end
29
+
30
+ def int = Keyed.unzigzag(ber)
31
+
32
+ def take(count)
33
+ raise FormatError, "#{@name} ends early, at byte #{@bytes.bytesize}" if count > remaining
34
+
35
+ @bytes.byteslice(@pos, count).to_s.tap { @pos += count }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Keyed
6
+ # A snapshot's BL-* modules by name, which a family reads in its own
7
+ # fixed order, whatever order the file has them in. Each module is
8
+ # parsed once, and its Reader kept for the Report.
9
+ #
10
+ # A `detached` load restores a machine that opens no host file and
11
+ # writes none: a disk image comes from the bytes the snapshot holds,
12
+ # write-protected, and a directory device 8 serves is left out.
13
+ class Modules
14
+ attr_reader :readers
15
+
16
+ def initialize(sections, detached: false)
17
+ @sections = {}
18
+ sections.each { |section| @sections[section.name] = section }
19
+ @parsed = {}
20
+ @readers = []
21
+ @detached = detached
22
+ end
23
+
24
+ def detached? = @detached
25
+
26
+ # BL-MACHINE, which every family writes first.
27
+ def machine = required("BL-MACHINE")
28
+
29
+ # The module a family always writes, or a Reader standing in for it,
30
+ # reported missing.
31
+ def required(name)
32
+ reader = @parsed[name]
33
+ return reader if reader
34
+
35
+ section = @sections[name]
36
+ keep(name, section.nil? ? missing(name) : parse(section))
37
+ end
38
+
39
+ # The module, or nil when the snapshot has none, as it has none for a
40
+ # cartridge or a drive that isn't there.
41
+ def optional(name)
42
+ reader = @parsed[name]
43
+ return reader if reader
44
+
45
+ section = @sections[name]
46
+ section.nil? ? nil : keep(name, parse(section))
47
+ end
48
+
49
+ # The BL-* modules no family read, from a later badline.
50
+ def unread = @sections.keys.select { |name| name.start_with?("BL-") && !@parsed.key?(name) }
51
+
52
+ private
53
+
54
+ def parse(section) = Reader.parse(section)
55
+
56
+ def missing(name) = Reader.missing(name)
57
+
58
+ def keep(name, reader)
59
+ @parsed[name] = reader
60
+ @readers << reader
61
+ reader
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Keyed
6
+ # Parses a module's records into one Hash per type, so every value
7
+ # read has a single type in Spinel. Each read takes the current value
8
+ # as its default, which in a machine built for the load is power-on,
9
+ # and returns it when the record is missing or of another type.
10
+ #
11
+ # A Reader with no records stands in for a module the snapshot
12
+ # doesn't have, so every load takes the same path.
13
+ class Reader
14
+ # The tag of each key the module's records hold.
15
+ attr_reader :name, :tags
16
+
17
+ # Whether it stands in for a module the snapshot doesn't have.
18
+ def missing? = @missing
19
+
20
+ # The records in `section`, which must be in this encoding.
21
+ def self.parse(section)
22
+ unless section.major == MAJOR
23
+ raise FormatError, "#{section.name} #{section.version} was written by a newer badline's encoding, " \
24
+ "and this one reads #{MAJOR}.#{MINOR}"
25
+ end
26
+
27
+ new(section.name, Zlib.inflate(section.data))
28
+ rescue Zlib::Error => e
29
+ raise FormatError, "#{section.name} is damaged: #{e.message}"
30
+ end
31
+
32
+ # Stands in for a module the snapshot doesn't have.
33
+ def self.missing(name) = new(name, "".b, missing: true)
34
+
35
+ def initialize(name, payload = "".b, missing: false)
36
+ @name = name
37
+ @missing = missing
38
+ @ints = {}
39
+ @bools = {}
40
+ @bytes = {}
41
+ @lists = {}
42
+ @texts = {}
43
+ @tags = {}
44
+ @read = {}
45
+ @adjusted = []
46
+ @unknown_tags = 0
47
+ parse(Cursor.new(payload, name))
48
+ end
49
+
50
+ def int(key, default) = fetch_int(key, default)
51
+
52
+ def word(key, default) = fetch_masked(key, default, 0xffff)
53
+
54
+ def byte(key, default) = fetch_masked(key, default, 0xff)
55
+
56
+ # An int cut to the bits a latch holds, reported if it was wider.
57
+ def masked(key, default, mask) = fetch_masked(key, default, mask)
58
+
59
+ def bool(key, default) = present?(key, "a bool", @bools.key?(key)) ? @bools.fetch(key) : default
60
+
61
+ # Nil when the record is missing.
62
+ def optional_int(key) = present?(key, "an int", @ints.key?(key)) ? @ints.fetch(key) : nil
63
+
64
+ def ints(key, default) = present?(key, "a list of ints", @lists.key?(key)) ? @lists.fetch(key) : default
65
+
66
+ # A binary String.
67
+ def bytes(key, default) = fetch_bytes(key, default)
68
+
69
+ # Reads a hardware array into `array` in place. A shorter list
70
+ # leaves the rest as it was and a longer one is cut, and both are
71
+ # reported.
72
+ def ints_into(key, array)
73
+ return array unless present?(key, "a list of ints", @lists.key?(key))
74
+
75
+ fill(key, array, @lists.fetch(key))
76
+ end
77
+
78
+ def bytes_into(key, array)
79
+ return array unless present?(key, "bytes", @bytes.key?(key))
80
+
81
+ fill(key, array, @bytes.fetch(key).bytes)
82
+ end
83
+
84
+ def text(key, default) = fetch_text(key, default)
85
+
86
+ # The text, if it is one of `names`, and otherwise `default`,
87
+ # reported.
88
+ def choice(key, names, default)
89
+ value = fetch_text(key, default)
90
+ return value if names.include?(value)
91
+
92
+ @adjusted << "#{key} #{value.inspect} is not one of #{names.join(', ')}, left at #{default}"
93
+ default
94
+ end
95
+
96
+ # Reports something a unit's load left out or changed beyond one
97
+ # field, such as a line batch from another renderer.
98
+ def adjust(line)
99
+ @adjusted << line
100
+ self
101
+ end
102
+
103
+ # The keys of records nothing read.
104
+ def unknown = @tags.keys.reject { |key| @read.key?(key) }
105
+
106
+ def report
107
+ Report.new(name:, missing: @missing, unknown:, adjusted: @adjusted.dup, unknown_tags: @unknown_tags)
108
+ end
109
+
110
+ private
111
+
112
+ def fetch_int(key, default) = present?(key, "an int", @ints.key?(key)) ? @ints.fetch(key) : default
113
+
114
+ def fetch_masked(key, default, mask)
115
+ value = fetch_int(key, default)
116
+ return value if value & mask == value
117
+
118
+ @adjusted << "#{key} #{value} is wider than its latch, masked to #{value & mask}"
119
+ value & mask
120
+ end
121
+
122
+ def fetch_bytes(key, default) = present?(key, "bytes", @bytes.key?(key)) ? @bytes.fetch(key) : default
123
+
124
+ def fetch_text(key, default) = present?(key, "text", @texts.key?(key)) ? @texts.fetch(key) : default
125
+
126
+ # Whether the key has a record of the type read, `there`. A record
127
+ # of another type counts as missing, and is reported.
128
+ def present?(key, type, there)
129
+ @read[key] = true if there || @tags.key?(key)
130
+ @adjusted << "#{key} is not #{type}, left at its default" if !there && @tags.key?(key)
131
+ there
132
+ end
133
+
134
+ def fill(key, array, values)
135
+ unless values.length == array.length
136
+ what = values.length < array.length ? "the rest left as they were" : "the rest cut"
137
+ @adjusted << "#{key} holds #{values.length} values where #{array.length} fit, #{what}"
138
+ end
139
+ [values.length, array.length].min.times { |index| array[index] = values[index] }
140
+ array
141
+ end
142
+
143
+ def parse(cursor)
144
+ until cursor.done?
145
+ key = cursor.take(cursor.ber)
146
+ tag = cursor.take(1).getbyte(0).to_i
147
+ store(key, tag, cursor.take(cursor.ber))
148
+ end
149
+ end
150
+
151
+ def store(key, tag, value)
152
+ unless tag.between?(INT, TEXT)
153
+ @unknown_tags += 1
154
+ return
155
+ end
156
+
157
+ forget(key)
158
+ @tags[key] = tag
159
+ case tag
160
+ when INT then @ints[key] = Cursor.new(value, "#{@name} #{key}").int
161
+ when BOOL then @bools[key] = value.getbyte(0).to_i != 0
162
+ when BYTES then @bytes[key] = value
163
+ when INTS then @lists[key] = list(Cursor.new(value, "#{@name} #{key}"))
164
+ else @texts[key] = value.force_encoding(Encoding::UTF_8)
165
+ end
166
+ end
167
+
168
+ def forget(key)
169
+ return unless @tags.key?(key)
170
+
171
+ @adjusted << "#{key} appears more than once, the last one counts"
172
+ @ints.delete(key)
173
+ @bools.delete(key)
174
+ @bytes.delete(key)
175
+ @lists.delete(key)
176
+ @texts.delete(key)
177
+ end
178
+
179
+ def list(cursor)
180
+ count = cursor.ber
181
+ raise FormatError, "#{@name} ends early, in a list of #{count}" if count > cursor.remaining
182
+
183
+ Array.new(count) { cursor.int }
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Keyed
6
+ # A Reader that notes every read: the module, the key, the type read
7
+ # and the default passed. Loading a machine at power-on through
8
+ # Recorders lists each module's fields with their power-on values
9
+ # (FieldDoc).
10
+ class Recorder < Reader
11
+ Field = Data.define(:module_name, :key, :type, :default)
12
+
13
+ attr_reader :fields
14
+
15
+ def initialize(name, payload = "".b, missing: false)
16
+ super
17
+ @fields = []
18
+ end
19
+
20
+ def int(key, default)
21
+ note(key, "int", default)
22
+ super
23
+ end
24
+
25
+ def word(key, default)
26
+ note(key, "word", default)
27
+ super
28
+ end
29
+
30
+ def byte(key, default)
31
+ note(key, "byte", default)
32
+ super
33
+ end
34
+
35
+ def masked(key, default, mask)
36
+ note(key, "int & 0x#{mask.to_s(16)}", default)
37
+ super
38
+ end
39
+
40
+ def bool(key, default)
41
+ note(key, "bool", default)
42
+ super
43
+ end
44
+
45
+ def optional_int(key)
46
+ note(key, "int, optional", nil)
47
+ super
48
+ end
49
+
50
+ def ints(key, default)
51
+ note(key, "ints", default.dup)
52
+ super
53
+ end
54
+
55
+ def bytes(key, default)
56
+ note(key, "bytes", default.dup)
57
+ super
58
+ end
59
+
60
+ def ints_into(key, array)
61
+ note(key, "ints", array.dup)
62
+ super
63
+ end
64
+
65
+ def bytes_into(key, array)
66
+ note(key, "bytes", array.dup)
67
+ super
68
+ end
69
+
70
+ def text(key, default)
71
+ note(key, "text", default)
72
+ super
73
+ end
74
+
75
+ def choice(key, names, default)
76
+ note(key, "text: #{names.join(' ')}", default)
77
+ super
78
+ end
79
+
80
+ private
81
+
82
+ def note(key, type, default)
83
+ @fields << Field.new(module_name: name, key:, type:, default:)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Keyed
6
+ # What reading a module left out or changed: whether the snapshot
7
+ # had the module at all, the keys nothing read, a line for each value
8
+ # read other than as written, and how many records had a tag this
9
+ # badline doesn't know.
10
+ Report = Data.define(:name, :missing, :unknown, :adjusted, :unknown_tags) do
11
+ # Lines for the user. A missing module and adjusted values always
12
+ # get one, and unknown keys and tags only when `verbose`, since a
13
+ # snapshot from a later badline has them on every load.
14
+ def lines(verbose: false)
15
+ lines = missing ? ["#{name} is missing, left at power-on"] : []
16
+ lines.concat(adjusted.map { |line| "#{name}: #{line}" })
17
+ return lines unless verbose
18
+
19
+ lines << "#{name}: #{unknown.length} unknown field(s): #{unknown.join(', ')}" unless unknown.empty?
20
+ lines << "#{name}: #{unknown_tags} field(s) of an unknown type, skipped" if unknown_tags.positive?
21
+ lines
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ module Keyed
6
+ # Appends records to a module's payload. Each method returns self.
7
+ class Writer
8
+ def initialize
9
+ @out = String.new(encoding: Encoding::BINARY)
10
+ end
11
+
12
+ # Raises TypeError unless `value` is an Integer, and RangeError
13
+ # outside VALUES.
14
+ def int(key, value) = record(key, INT, Keyed.ber(fold(key, value)))
15
+
16
+ def bool(key, value) = record(key, BOOL, value ? "\x01".b : "\x00".b)
17
+
18
+ # An Array of 0..255, or a binary String.
19
+ def bytes(key, values) = record(key, BYTES, values.is_a?(String) ? values.b : values.pack("C*"))
20
+
21
+ def ints(key, values)
22
+ value = Keyed.ber(values.length)
23
+ values.each { |number| value << Keyed.ber(fold(key, number)) }
24
+ record(key, INTS, value)
25
+ end
26
+
27
+ def text(key, value) = record(key, TEXT, value.b)
28
+
29
+ # Writes nothing for nil, which a reader reads back as nil.
30
+ def optional_int(key, value) = value.nil? ? self : int(key, value)
31
+
32
+ # The records written so far.
33
+ def to_s = @out.dup
34
+
35
+ def section(name) = Section.new(name:, major: MAJOR, minor: MINOR, data: Zlib.deflate(@out))
36
+
37
+ private
38
+
39
+ def fold(key, value)
40
+ raise TypeError, "#{key} is not an Integer: #{value.inspect}" unless value.is_a?(Integer)
41
+ raise RangeError, "#{key} #{value} is too large for a snapshot" unless VALUES.cover?(value)
42
+
43
+ Keyed.zigzag(value)
44
+ end
45
+
46
+ def record(key, tag, value)
47
+ raise ArgumentError, "#{key.inspect} is not a snapshot key" unless key.match?(KEY)
48
+
49
+ @out << Keyed.ber(key.bytesize) << key.b << [tag].pack("C") << Keyed.ber(value.bytesize) << value
50
+ self
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end