badline 0.13.0 → 0.15.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 (286) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -1
  3. data/CLAUDE.md +42 -12
  4. data/CONTRIBUTING.md +4 -3
  5. data/README.md +76 -13
  6. data/doc/media.md +2 -1
  7. data/doc/pinned-behaviour.md +174 -0
  8. data/doc/snapshot-format.md +1197 -0
  9. data/doc/snapshots.md +90 -40
  10. data/lib/badline/audio/bare_player.rb +46 -17
  11. data/lib/badline/audio/machine_player.rb +6 -10
  12. data/lib/badline/audio/renderer.rb +32 -21
  13. data/lib/badline/c128/banks.rb +7 -6
  14. data/lib/badline/c128/bus/keyed_state.rb +45 -0
  15. data/lib/badline/c128/bus.rb +36 -4
  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 +27 -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/extra_sids.rb +80 -0
  31. data/lib/badline/c64/bus/keyed_state.rb +41 -0
  32. data/lib/badline/c64/bus.rb +3 -1
  33. data/lib/badline/c64/cartridge/action_replay.rb +6 -15
  34. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  35. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  36. data/lib/badline/c64/cartridge/easy_flash.rb +0 -15
  37. data/lib/badline/c64/cartridge/epyx_fastload.rb +0 -8
  38. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -12
  39. data/lib/badline/c64/cartridge/flash.rb +3 -3
  40. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  41. data/lib/badline/c64/cartridge/g_mod2.rb +0 -12
  42. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  43. data/lib/badline/c64/cartridge/geo_ram.rb +0 -16
  44. data/lib/badline/c64/cartridge/kcs_power.rb +0 -8
  45. data/lib/badline/c64/cartridge/keyed_state.rb +514 -0
  46. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  47. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  48. data/lib/badline/c64/cartridge/pagefox.rb +1 -10
  49. data/lib/badline/c64/cartridge/retro_replay.rb +0 -8
  50. data/lib/badline/c64/cartridge/rgcd.rb +2 -11
  51. data/lib/badline/c64/cartridge/super_games.rb +3 -12
  52. data/lib/badline/c64/cartridge/zaxxon.rb +1 -16
  53. data/lib/badline/c64/cartridge.rb +4 -30
  54. data/lib/badline/c64/kernal_traps.rb +28 -25
  55. data/lib/badline/c64/keyed_modules.rb +115 -0
  56. data/lib/badline/c64/keyed_state.rb +68 -0
  57. data/lib/badline/c64/reu/dma.rb +0 -4
  58. data/lib/badline/c64/reu/keyed_state.rb +140 -0
  59. data/lib/badline/c64/reu/ram.rb +0 -15
  60. data/lib/badline/c64/reu.rb +1 -3
  61. data/lib/badline/c64/true_drives.rb +56 -5
  62. data/lib/badline/c64.rb +7 -3
  63. data/lib/badline/chips/cia/interrupt_register.rb +0 -17
  64. data/lib/badline/chips/cia/keyed_parts.rb +213 -0
  65. data/lib/badline/chips/cia/serial.rb +0 -4
  66. data/lib/badline/chips/cia/time_of_day.rb +0 -22
  67. data/lib/badline/chips/cia/timer.rb +0 -20
  68. data/lib/badline/chips/cia.rb +2 -2
  69. data/lib/badline/chips/cpu/core.rb +18 -2
  70. data/lib/badline/chips/cpu/keyed_state.rb +177 -0
  71. data/lib/badline/chips/cpu/microcode.rb +16 -0
  72. data/lib/badline/chips/cpu/port.rb +20 -3
  73. data/lib/badline/chips/cpu.rb +5 -0
  74. data/lib/badline/chips/sid/decimator.rb +10 -8
  75. data/lib/badline/chips/sid/envelope.rb +1 -6
  76. data/lib/badline/chips/sid/filter.rb +0 -14
  77. data/lib/badline/chips/sid/keyed_parts.rb +286 -0
  78. data/lib/badline/chips/sid/voice.rb +0 -10
  79. data/lib/badline/chips/sid/waveform.rb +0 -2
  80. data/lib/badline/chips/sid.rb +1 -2
  81. data/lib/badline/chips/ted/beam.rb +118 -0
  82. data/lib/badline/chips/ted/fetches.rb +199 -0
  83. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  84. data/lib/badline/chips/ted/modes.rb +134 -0
  85. data/lib/badline/chips/ted/palette.rb +43 -0
  86. data/lib/badline/chips/ted/picture.rb +182 -0
  87. data/lib/badline/chips/ted/sound/output.rb +86 -0
  88. data/lib/badline/chips/ted/sound.rb +284 -0
  89. data/lib/badline/chips/ted/timers.rb +66 -0
  90. data/lib/badline/chips/ted.rb +190 -0
  91. data/lib/badline/chips/tia.rb +61 -0
  92. data/lib/badline/chips/vdc/keyed_parts.rb +105 -0
  93. data/lib/badline/chips/vdc/painter.rb +0 -12
  94. data/lib/badline/chips/vdc/raster.rb +0 -16
  95. data/lib/badline/chips/vdc/window.rb +0 -9
  96. data/lib/badline/chips/vdc.rb +1 -2
  97. data/lib/badline/chips/via/control_lines.rb +0 -15
  98. data/lib/badline/chips/via/interrupt_register.rb +0 -9
  99. data/lib/badline/chips/via/keyed_parts.rb +163 -0
  100. data/lib/badline/chips/via/shift_register.rb +0 -14
  101. data/lib/badline/chips/via/timer1.rb +0 -13
  102. data/lib/badline/chips/via/timer2.rb +0 -18
  103. data/lib/badline/chips/via.rb +1 -2
  104. data/lib/badline/chips/vic/border_mask.rb +0 -10
  105. data/lib/badline/chips/vic/collisions.rb +0 -11
  106. data/lib/badline/chips/vic/color_patches.rb +0 -11
  107. data/lib/badline/chips/vic/display_state.rb +0 -19
  108. data/lib/badline/chips/vic/graphics_shifter.rb +0 -4
  109. data/lib/badline/chips/vic/keyed_parts.rb +228 -0
  110. data/lib/badline/chips/vic/keyed_sprites.rb +225 -0
  111. data/lib/badline/chips/vic/keyed_state.rb +102 -0
  112. data/lib/badline/chips/vic/register_log.rb +0 -18
  113. data/lib/badline/chips/vic/registers.rb +0 -9
  114. data/lib/badline/chips/vic/sequencer.rb +0 -2
  115. data/lib/badline/chips/vic/sprite/internal_bus.rb +0 -11
  116. data/lib/badline/chips/vic/sprite.rb +0 -2
  117. data/lib/badline/chips/vic/sprites.rb +0 -3
  118. data/lib/badline/chips/vic.rb +6 -2
  119. data/lib/badline/chips/vic_i/keyed_parts.rb +142 -0
  120. data/lib/badline/chips/vic_i/painter.rb +0 -4
  121. data/lib/badline/chips/vic_i/sound.rb +38 -46
  122. data/lib/badline/chips/vic_i.rb +2 -2
  123. data/lib/badline/chips/z80/core.rb +2 -2
  124. data/lib/badline/chips/z80/keyed_state.rb +73 -0
  125. data/lib/badline/chips.rb +10 -0
  126. data/lib/badline/core.rb +4 -21
  127. data/lib/badline/drives/datasette/keyed_state.rb +43 -0
  128. data/lib/badline/drives/datasette.rb +62 -12
  129. data/lib/badline/drives/engine/core.rb +3 -0
  130. data/lib/badline/drives/engine/serial_lines.rb +4 -7
  131. data/lib/badline/drives/model1541/bus.rb +0 -13
  132. data/lib/badline/drives/model1541/disk.rb +9 -8
  133. data/lib/badline/drives/model1541/keyed_state.rb +251 -0
  134. data/lib/badline/drives/model1541/mechanism.rb +0 -2
  135. data/lib/badline/drives/model1541.rb +1 -2
  136. data/lib/badline/drives/model1571/bus.rb +0 -13
  137. data/lib/badline/drives/model1571/keyed_state.rb +108 -0
  138. data/lib/badline/drives/model1571/wd1770.rb +0 -14
  139. data/lib/badline/drives/model1571.rb +1 -2
  140. data/lib/badline/drives/model1581/bus.rb +0 -13
  141. data/lib/badline/drives/model1581/disk.rb +0 -4
  142. data/lib/badline/drives/model1581/keyed_state.rb +280 -0
  143. data/lib/badline/drives/model1581/mechanism.rb +0 -21
  144. data/lib/badline/drives/model1581/slot.rb +0 -18
  145. data/lib/badline/drives/model1581/wd1772.rb +0 -2
  146. data/lib/badline/drives/model1581.rb +2 -2
  147. data/lib/badline/drives.rb +8 -0
  148. data/lib/badline/frontend/app.rb +3 -3
  149. data/lib/badline/frontend/boot.rb +4 -2
  150. data/lib/badline/frontend/controls.rb +25 -5
  151. data/lib/badline/frontend/drive_led.rb +44 -21
  152. data/lib/badline/frontend/keys.rb +14 -2
  153. data/lib/badline/frontend/media_slots.rb +11 -1
  154. data/lib/badline/frontend/menu/menu_dialogs.rb +4 -3
  155. data/lib/badline/frontend/menu/menu_media.rb +13 -3
  156. data/lib/badline/frontend/menu/menu_pages.rb +8 -8
  157. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  158. data/lib/badline/frontend/screen.rb +33 -12
  159. data/lib/badline/frontend/screens.rb +2 -1
  160. data/lib/badline/frontend/snapshots.rb +12 -1
  161. data/lib/badline/frontend/sound.rb +4 -4
  162. data/lib/badline/frontend/timeline.rb +43 -12
  163. data/lib/badline/input/matrix_typist.rb +98 -0
  164. data/lib/badline/input.rb +1 -0
  165. data/lib/badline/kernal_trap/chrout.rb +45 -0
  166. data/lib/badline/kernal_trap/dos/channel.rb +0 -27
  167. data/lib/badline/kernal_trap/dos/channels.rb +0 -25
  168. data/lib/badline/kernal_trap/dos/memory.rb +0 -9
  169. data/lib/badline/kernal_trap/dos/status.rb +0 -8
  170. data/lib/badline/kernal_trap/dos/write_file.rb +0 -18
  171. data/lib/badline/kernal_trap/dos.rb +0 -19
  172. data/lib/badline/kernal_trap/file_routine.rb +4 -4
  173. data/lib/badline/kernal_trap/keyed_state.rb +150 -0
  174. data/lib/badline/kernal_trap/layout.rb +75 -11
  175. data/lib/badline/kernal_trap/load.rb +14 -16
  176. data/lib/badline/kernal_trap/routine.rb +12 -0
  177. data/lib/badline/kernal_trap/save.rb +8 -18
  178. data/lib/badline/kernal_trap/serial.rb +1 -17
  179. data/lib/badline/kernal_trap.rb +2 -0
  180. data/lib/badline/machine/keyboard_buffer.rb +49 -0
  181. data/lib/badline/machine/settling.rb +29 -0
  182. data/lib/badline/machine.rb +29 -6
  183. data/lib/badline/media/boot_disk.rb +14 -2
  184. data/lib/badline/media/extensions.rb +10 -0
  185. data/lib/badline/media/plus4_media.rb +68 -0
  186. data/lib/badline/media/program.rb +20 -6
  187. data/lib/badline/media.rb +33 -12
  188. data/lib/badline/memory.rb +3 -6
  189. data/lib/badline/options/family.rb +6 -2
  190. data/lib/badline/options/help.rb +66 -16
  191. data/lib/badline/options/table.rb +17 -2
  192. data/lib/badline/options/validation.rb +26 -2
  193. data/lib/badline/options.rb +42 -9
  194. data/lib/badline/plus4/acia.rb +45 -0
  195. data/lib/badline/plus4/bus.rb +375 -0
  196. data/lib/badline/plus4/cassette_sense.rb +21 -0
  197. data/lib/badline/plus4/cpu.rb +10 -0
  198. data/lib/badline/plus4/cpu_port.rb +93 -0
  199. data/lib/badline/plus4/keyboard_port.rb +76 -0
  200. data/lib/badline/plus4/keyed_modules.rb +99 -0
  201. data/lib/badline/plus4/keyed_parts.rb +143 -0
  202. data/lib/badline/plus4/keyed_state.rb +41 -0
  203. data/lib/badline/plus4/model.rb +38 -0
  204. data/lib/badline/plus4/user_port.rb +31 -0
  205. data/lib/badline/plus4.rb +240 -0
  206. data/lib/badline/roms/README +38 -0
  207. data/lib/badline/roms/plus4/basic.rom +0 -0
  208. data/lib/badline/roms/plus4/function-high.rom +0 -0
  209. data/lib/badline/roms/plus4/function-low.rom +0 -0
  210. data/lib/badline/roms/plus4/kernal-ntsc.rom +0 -0
  211. data/lib/badline/roms/plus4/kernal-pal.rom +0 -0
  212. data/lib/badline/snapshot/c128_setup.rb +9 -20
  213. data/lib/badline/snapshot/c64_setup.rb +28 -24
  214. data/lib/badline/snapshot/container.rb +6 -0
  215. data/lib/badline/snapshot/field_doc/markdown.rb +113 -0
  216. data/lib/badline/snapshot/field_doc.rb +148 -0
  217. data/lib/badline/snapshot/image.rb +143 -28
  218. data/lib/badline/snapshot/keyed/cursor.rb +40 -0
  219. data/lib/badline/snapshot/keyed/modules.rb +66 -0
  220. data/lib/badline/snapshot/keyed/reader.rb +188 -0
  221. data/lib/badline/snapshot/keyed/recorder.rb +88 -0
  222. data/lib/badline/snapshot/keyed/report.rb +26 -0
  223. data/lib/badline/snapshot/keyed/writer.rb +55 -0
  224. data/lib/badline/snapshot/keyed.rb +64 -0
  225. data/lib/badline/snapshot/machine_state.rb +17 -64
  226. data/lib/badline/snapshot/plus4_setup.rb +39 -0
  227. data/lib/badline/snapshot/storage_setup.rb +29 -34
  228. data/lib/badline/snapshot/vic20_setup.rb +6 -15
  229. data/lib/badline/snapshot/vice/reu1764.rb +2 -2
  230. data/lib/badline/snapshot/vice.rb +7 -31
  231. data/lib/badline/snapshot.rb +48 -20
  232. data/lib/badline/storage/crt_file.rb +3 -1
  233. data/lib/badline/storage/g64_image.rb +9 -4
  234. data/lib/badline/storage/tap.rb +57 -20
  235. data/lib/badline/support/checkpoint.rb +3 -2
  236. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  237. data/lib/badline/version.rb +1 -1
  238. data/lib/badline/vic20/attachments.rb +3 -0
  239. data/lib/badline/vic20/bus/keyed_state.rb +52 -0
  240. data/lib/badline/vic20/bus.rb +1 -2
  241. data/lib/badline/vic20/keyed_modules.rb +73 -0
  242. data/lib/badline/vic20/keyed_state.rb +45 -0
  243. data/lib/badline/vic20.rb +5 -5
  244. data/lib/badline.rb +1 -0
  245. metadata +75 -42
  246. data/lib/badline/c128/bus/saved_state.rb +0 -53
  247. data/lib/badline/c128/saved_state.rb +0 -117
  248. data/lib/badline/c64/bus/saved_state.rb +0 -68
  249. data/lib/badline/c64/cartridge/flash/saved_state.rb +0 -40
  250. data/lib/badline/c64/cartridge/retro_replay/saved_state.rb +0 -47
  251. data/lib/badline/c64/cartridge/saved_state.rb +0 -63
  252. data/lib/badline/c64/reu/dma/saved_state.rb +0 -52
  253. data/lib/badline/c64/reu/saved_state.rb +0 -55
  254. data/lib/badline/c64/saved_state.rb +0 -112
  255. data/lib/badline/chips/cia/saved_state.rb +0 -49
  256. data/lib/badline/chips/cia/serial/saved_state.rb +0 -45
  257. data/lib/badline/chips/cpu/saved_state.rb +0 -117
  258. data/lib/badline/chips/sid/envelope/saved_state.rb +0 -70
  259. data/lib/badline/chips/sid/filter/saved_state.rb +0 -42
  260. data/lib/badline/chips/sid/saved_state.rb +0 -52
  261. data/lib/badline/chips/sid/waveform/saved_state.rb +0 -68
  262. data/lib/badline/chips/vdc/saved_state.rb +0 -36
  263. data/lib/badline/chips/via/saved_state.rb +0 -44
  264. data/lib/badline/chips/vic/graphics_shifter/saved_state.rb +0 -40
  265. data/lib/badline/chips/vic/saved_state.rb +0 -82
  266. data/lib/badline/chips/vic/sequencer/saved_state.rb +0 -91
  267. data/lib/badline/chips/vic/sprite/saved_state.rb +0 -66
  268. data/lib/badline/chips/vic/sprites/saved_state.rb +0 -59
  269. data/lib/badline/chips/vic_i/painter/saved_state.rb +0 -40
  270. data/lib/badline/chips/vic_i/saved_state.rb +0 -55
  271. data/lib/badline/chips/z80/saved_state.rb +0 -71
  272. data/lib/badline/chrout_trap.rb +0 -43
  273. data/lib/badline/debug_register.rb +0 -28
  274. data/lib/badline/drives/datasette/saved_state.rb +0 -37
  275. data/lib/badline/drives/model1541/disk/saved_state.rb +0 -88
  276. data/lib/badline/drives/model1541/mechanism/saved_state.rb +0 -72
  277. data/lib/badline/drives/model1541/saved_state.rb +0 -44
  278. data/lib/badline/drives/model1571/saved_state.rb +0 -47
  279. data/lib/badline/drives/model1581/disk/saved_state.rb +0 -89
  280. data/lib/badline/drives/model1581/saved_state.rb +0 -42
  281. data/lib/badline/drives/model1581/wd1772/saved_state.rb +0 -68
  282. data/lib/badline/keyboard_buffer.rb +0 -33
  283. data/lib/badline/snapshot/setup.rb +0 -31
  284. data/lib/badline/snapshot/state.rb +0 -179
  285. data/lib/badline/vic20/bus/saved_state.rb +0 -35
  286. data/lib/badline/vic20/saved_state.rb +0 -110
@@ -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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "badline/snapshot/container"
5
+
6
+ module Badline
7
+ module Snapshot
8
+ # A snapshot module as keyed, typed records, which a later badline
9
+ # reads by name: one it doesn't know is skipped, and one that isn't
10
+ # there leaves the value a freshly built machine has.
11
+ #
12
+ # The payload is deflated. Inside, each record is the key's length and
13
+ # the key (ASCII, lower snake case with dots between sub-units), a
14
+ # one-byte type tag, then the value's length and the value. Lengths are
15
+ # BER-compressed (Ruby's pack "w"), and so are integers, each folded to
16
+ # a natural number (0, -1, 1, -2 ... as 0, 1, 2, 3 ...). The value's
17
+ # length lets a reader skip a tag it doesn't know.
18
+ #
19
+ # A module's header version is MAJOR.MINOR. The major number is this
20
+ # record encoding, and a reader refuses another one. Fields carry no
21
+ # version: a changed meaning gets a new key.
22
+ module Keyed
23
+ MAJOR = 1
24
+ MINOR = 0
25
+
26
+ INT = 0x01
27
+ BOOL = 0x02
28
+ BYTES = 0x03
29
+ INTS = 0x04
30
+ TEXT = 0x05
31
+
32
+ KEY = /\A[a-z0-9_]+(\.[a-z0-9_]+)*\z/
33
+
34
+ # The values an int record holds: what a 64-bit integer holds once
35
+ # zigzag doubles it, which a native build's integers can't exceed.
36
+ VALUES = (-(1 << 62))..((1 << 62) - 1)
37
+
38
+ module_function
39
+
40
+ def ber(number) = [number].pack("w")
41
+
42
+ # Folds an Integer in VALUES to a natural number, as a
43
+ # native build's 64-bit integers hold it.
44
+ def zigzag(value) = value.negative? ? ((-value - 1) << 1) | 1 : value << 1
45
+
46
+ def unzigzag(folded) = folded.odd? ? -(folded >> 1) - 1 : folded >> 1
47
+
48
+ # The module `name`, with the records the block writes to the Writer
49
+ # it yields.
50
+ def section(name)
51
+ out = Writer.new
52
+ yield out
53
+ out.section(name)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ require "badline/snapshot/keyed/cursor"
60
+ require "badline/snapshot/keyed/writer"
61
+ require "badline/snapshot/keyed/report"
62
+ require "badline/snapshot/keyed/reader"
63
+ require "badline/snapshot/keyed/recorder"
64
+ require "badline/snapshot/keyed/modules"
@@ -2,83 +2,36 @@
2
2
 
3
3
  module Badline
4
4
  module Snapshot
5
- # The BADLINE module: a State from C64#snapshot, which restores the
6
- # machine exactly where it was saved. VICE skips a module it doesn't
7
- # know.
5
+ # The BADLINE module badline 0.13 and earlier saved the machine in, a
6
+ # positional layout only the version that wrote it reads. All that is
7
+ # left of it is reading which version that was, for the refusal
8
+ # (Image#load).
8
9
  #
9
10
  # The payload is deflated. Inside, a little-endian 32-bit length leads
10
11
  # 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.
12
+ # values and of strings, each string's length, then the values. The
13
+ # strings follow, one after another: the family's marker first, then
14
+ # the badline version.
17
15
  module MachineState
18
16
  NAME = "BADLINE"
19
- MAJOR = 2
20
- MINOR = 0
21
-
22
- # The values a 64-bit integer holds once encode doubles them, which a
23
- # native build's integers can't exceed.
24
- VALUES = (-(1 << 62))..((1 << 62) - 1)
25
17
 
26
18
  module_function
27
19
 
28
- def section(state)
29
- Section.new(name: NAME, major: MAJOR, minor: MINOR, data: Zlib.deflate(encode(state)))
30
- end
31
-
32
- def state(section)
33
- unless section.major == MAJOR
34
- raise FormatError, "#{NAME} #{section.version} was written by another badline version, " \
35
- "and this one reads #{MAJOR}.#{MINOR}"
36
- end
37
-
38
- decode(Zlib.inflate(section.data))
39
- rescue Zlib::Error => e
40
- raise FormatError, "#{NAME} is damaged: #{e.message}"
41
- end
42
-
43
- def encode(state)
44
- values = state.values
45
- strings = state.strings
46
- numbers = [values.length, strings.length]
47
- strings.each { |string| numbers << string.bytesize }
48
- values.each do |value|
49
- raise RangeError, "#{value} is too large for a snapshot" unless VALUES.cover?(value)
50
-
51
- numbers << (value.negative? ? (-2 * value) - 1 : 2 * value)
52
- end
53
- index = numbers.pack("w*")
54
- [index.bytesize].pack("V") + index + strings.join
55
- end
56
-
57
- def decode(bytes)
58
- raise FormatError, "#{NAME} ends early" if bytes.bytesize < 4
20
+ # The badline version that saved the module, or an empty string when
21
+ # it is too damaged to say.
22
+ def written_by(section)
23
+ bytes = Zlib.inflate(section.data)
24
+ return "" if bytes.bytesize < 4
59
25
 
60
26
  length = bytes.byteslice(0, 4).unpack1("V").to_i
61
27
  numbers = bytes.byteslice(4, length).to_s.unpack("w*").map(&:to_i)
62
- value_count = numbers.fetch(0, 0)
63
28
  string_count = numbers.fetch(1, 0)
64
- raise FormatError, "#{NAME} ends early" if numbers.length != 2 + string_count + value_count
65
-
66
- State.new(values(numbers, 2 + string_count), strings(bytes, 4 + length, numbers.slice(2, string_count)))
67
- end
68
-
69
- def values(numbers, from)
70
- Array.new(numbers.length - from) do |i|
71
- folded = numbers[from + i]
72
- folded.odd? ? -((folded + 1) / 2) : folded / 2
73
- end
74
- end
75
-
76
- def strings(bytes, pos, lengths)
77
- lengths.map do |length|
78
- raise FormatError, "#{NAME} ends early" if pos + length > bytes.bytesize
29
+ return "" if string_count < 2 || numbers.length < 4
79
30
 
80
- bytes.byteslice(pos, length).to_s.tap { pos += length }
81
- end
31
+ pos = 4 + length + numbers[2]
32
+ bytes.byteslice(pos, numbers[3]).to_s
33
+ rescue Zlib::Error
34
+ ""
82
35
  end
83
36
  end
84
37
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Snapshot
5
+ # How a 264 series machine was built: its model's name, one of
6
+ # Plus4::Model::ALL, which also says whether the Plus/4's function ROMs
7
+ # are fitted, its RAM in K, and whether a 1551's TCBM interface is
8
+ # plugged in, the arguments Plus4.new takes.
9
+ Plus4Setup = Data.define(:model, :ram_kb, :tcbm)
10
+
11
+ class Plus4Setup
12
+ MODELS = Plus4::Model::ALL.map(&:name).freeze
13
+
14
+ # The setup of `machine`.
15
+ def self.of(machine) = new(model: machine.model.name, ram_kb: machine.ram_kb, tcbm: !machine.tias.empty?)
16
+
17
+ # The model by name, the RAM and the TCBM interface, in a keyed
18
+ # snapshot module.
19
+ def write_fields(out)
20
+ out.text("model", model).int("ram_kb", ram_kb).bool("tcbm", tcbm)
21
+ end
22
+
23
+ def self.read_fields(input)
24
+ model = input.choice("model", MODELS, "plus4")
25
+ ram_kb = input.int("ram_kb", Plus4::Model.named(model).ram_kb)
26
+ unless Plus4::Model::RAM_SIZES.include?(ram_kb)
27
+ raise FormatError, "the snapshot is of a Plus/4 with #{ram_kb}K of RAM, which badline doesn't build"
28
+ end
29
+
30
+ new(model:, ram_kb:, tcbm: input.bool("tcbm", false))
31
+ end
32
+
33
+ # A machine built this way, at power-on.
34
+ def build = Plus4.new(model:, ram: ram_kb, tcbm:)
35
+
36
+ def to_s = "#{model} with #{ram_kb}K#{' and a TCBM interface' if tcbm}"
37
+ end
38
+ end
39
+ end