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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "badline/drives/model1581/disk/saved_state"
4
-
5
3
  module Badline
6
4
  module Drives
7
5
  class Model1581
@@ -24,8 +22,6 @@ module Badline
24
22
  # file. A track formatted another way stays on the disk alone. The disk
25
23
  # is write-protected when the image won't take writes.
26
24
  class Disk
27
- include SavedState
28
-
29
25
  CYLINDERS = 80
30
26
  SIDES = 2
31
27
 
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1581
6
+ class WD1772
7
+ PHASE_NAMES = %w[idle spin_up stepped settled search read_data write_gate write_data read_track track_index
8
+ write_track motor_idle index_interrupt settle_search read_address write_track_start
9
+ starting].freeze
10
+ MODE_NAMES = %w[read write address verify_track].freeze
11
+
12
+ # The controller in a keyed snapshot module (BL-D8-FDC): its own
13
+ # cycle count, the step of the command running and what the search
14
+ # is for by name, the registers and lines, the command's counters,
15
+ # and when its next step, its deadline and the next ID field fall,
16
+ # in its cycles from now, each absent when it never does.
17
+ def save_fields(out)
18
+ out.int("now", @now).text("phase", PHASE_NAMES.fetch(@phase)).text("mode", MODE_NAMES.fetch(@mode))
19
+ [["due_in", @due], ["deadline_in", @deadline], ["id_in", @id_at]].each do |key, at|
20
+ out.int(key, at - @now) unless at == NEVER
21
+ end
22
+ out.int("command", @command).int("track", @track).int("sector", @sector).int("data", @data)
23
+ out.int("status", @status).int("steps", @steps).int("count", @count).int("after_spin_up", @after_spin_up)
24
+ out.int("found", @found).int("index", @index).int("crc", @crc).int("write_id", @write_id).int("size", @size)
25
+ save_flag_fields(out)
26
+ out.bytes("bytes", @bytes).ints("syncs", @syncs.map { |sync| sync ? 1 : 0 })
27
+ end
28
+
29
+ def load_fields(input)
30
+ @now = input.int("now", @now)
31
+ @phase = PHASE_NAMES.index(input.choice("phase", PHASE_NAMES, "idle")).to_i
32
+ @mode = MODE_NAMES.index(input.choice("mode", MODE_NAMES, "read")).to_i
33
+ @due = at_fields(input, "due_in")
34
+ @deadline = at_fields(input, "deadline_in")
35
+ @id_at = at_fields(input, "id_in")
36
+ load_register_fields(input)
37
+ load_flag_fields(input)
38
+ @bytes = input.bytes("bytes", "".b).bytes
39
+ @syncs = input.ints("syncs", []).map { |sync| sync == 1 }
40
+ end
41
+
42
+ private
43
+
44
+ def at_fields(input, key)
45
+ cycles = input.optional_int(key)
46
+ cycles.nil? ? NEVER : @now + cycles
47
+ end
48
+
49
+ def load_register_fields(input)
50
+ @command = input.byte("command", @command)
51
+ @track = input.byte("track", @track)
52
+ @sector = input.byte("sector", @sector)
53
+ @data = input.byte("data", @data)
54
+ @status = input.byte("status", @status)
55
+ @steps = input.int("steps", @steps)
56
+ @count = input.int("count", @count)
57
+ @after_spin_up = input.int("after_spin_up", @after_spin_up)
58
+ @found = input.int("found", @found)
59
+ @index = input.int("index", @index)
60
+ @crc = input.int("crc", @crc)
61
+ @write_id = input.int("write_id", @write_id)
62
+ @size = input.int("size", @size)
63
+ end
64
+
65
+ def save_flag_fields(out)
66
+ out.bool("type1", @type1).bool("drq", @drq).bool("intrq", @intrq).bool("motor_on", @motor_on)
67
+ out.bool("inward", @inward).bool("crc_good", @crc_good).bool("gate_open", @gate_open).bool("late", @late)
68
+ out.bool("settling", @settling)
69
+ end
70
+
71
+ def load_flag_fields(input)
72
+ @type1 = input.bool("type1", @type1)
73
+ @drq = input.bool("drq", @drq)
74
+ @intrq = input.bool("intrq", @intrq)
75
+ @motor_on = input.bool("motor_on", @motor_on)
76
+ @inward = input.bool("inward", @inward)
77
+ @crc_good = input.bool("crc_good", @crc_good)
78
+ @gate_open = input.bool("gate_open", @gate_open)
79
+ @late = input.bool("late", @late)
80
+ @settling = input.bool("settling", @settling)
81
+ end
82
+ end
83
+
84
+ class Mechanism
85
+ # The head, the motor and the disk-change switch in a keyed snapshot
86
+ # module (BL-D8-MECH), with the disk's angle as it stands `now`, on
87
+ # the controller's clock.
88
+ def save_fields(out, now)
89
+ out.int("cylinder", @cylinder).int("side", @side).int("angle", angle(now)).bool("motor", @motor)
90
+ out.bool("changed", @changed)
91
+ end
92
+
93
+ # Puts `disk` under the head without flushing the one there, as a
94
+ # snapshot load replaces it.
95
+ def load_fields(input, disk, now)
96
+ @disk = disk
97
+ @cylinder = input.int("cylinder", @cylinder)
98
+ @side = input.masked("side", @side, 1)
99
+ @angle = input.int("angle", @angle)
100
+ @since = now
101
+ @motor = input.bool("motor", @motor)
102
+ @changed = input.bool("changed", @changed)
103
+ end
104
+ end
105
+
106
+ class Disk
107
+ # A disk's fields in a keyed snapshot module (BL-D8-DISK): the image
108
+ # file's path and how it was opened, the image as last flushed,
109
+ # embedded, each track the head wrote since the disk went in, sector
110
+ # by sector as the WD1772 sees them, and the tracks written since the
111
+ # last flush.
112
+ module KeyedState
113
+ # The image's path, expanded, and whether it was opened read-only,
114
+ # for a disk from Disk.open. Nil for a disk made another way.
115
+ attr_reader :path, :read_only
116
+
117
+ # Notes the image's path and how it was opened.
118
+ def opened(path, read_only)
119
+ @path = File.expand_path(path)
120
+ @read_only = read_only
121
+ self
122
+ end
123
+
124
+ def save_fields(out)
125
+ path = @path
126
+ out.text("path", path) if path
127
+ out.bool("read_only", @read_only == true)
128
+ image = @image
129
+ out.text("image.path", image.path.to_s).bytes("image", image.contents) if image
130
+ changed = @changed.keys.sort
131
+ out.ints("changed", changed)
132
+ changed.each { |index| save_track_fields(out, "track#{index}.", @tracks[index]) }
133
+ out.ints("written", @written.keys)
134
+ end
135
+
136
+ def load_track_fields(input)
137
+ @changed.clear
138
+ input.ints("changed", []).each do |index|
139
+ @tracks[index] = load_track_sectors(input, "track#{index}.")
140
+ @changed[index] = true
141
+ end
142
+ @written.clear
143
+ input.ints("written", []).each { |index| @written[index] = true }
144
+ self
145
+ end
146
+
147
+ private
148
+
149
+ def save_track_fields(out, prefix, track)
150
+ out.int("#{prefix}sectors", track.sectors.length)
151
+ track.sectors.each_with_index do |sector, number|
152
+ save_field_fields(out, "#{prefix}sector#{number}.id.", sector.id)
153
+ data = sector.data
154
+ save_field_fields(out, "#{prefix}sector#{number}.data.", data) if data
155
+ end
156
+ end
157
+
158
+ def save_field_fields(out, prefix, field)
159
+ out.int("#{prefix}mark", field.mark).bytes("#{prefix}bytes", field.bytes)
160
+ out.bool("#{prefix}good", field.good).bool("#{prefix}deleted", field.deleted)
161
+ end
162
+
163
+ def load_track_sectors(input, prefix)
164
+ Track.new(Array.new(input.int("#{prefix}sectors", 0)) do |number|
165
+ id = load_field_fields(input, "#{prefix}sector#{number}.id.")
166
+ data = input.optional_int("#{prefix}sector#{number}.data.mark")
167
+ Track::Sector.new(id, data.nil? ? nil : load_field_fields(input, "#{prefix}sector#{number}.data."))
168
+ end)
169
+ end
170
+
171
+ def load_field_fields(input, prefix)
172
+ Track::Field.new(input.int("#{prefix}mark", 0), input.bytes("#{prefix}bytes", "".b).bytes,
173
+ input.bool("#{prefix}good", true), input.bool("#{prefix}deleted", false))
174
+ end
175
+ end
176
+
177
+ include KeyedState
178
+
179
+ # The disk a keyed snapshot module describes: its embedded image,
180
+ # with the tracks the module holds laid over it. The image takes
181
+ # writes back to its file only while that file is there and the
182
+ # reader isn't `detached`. With no image, the disk is the tracks
183
+ # alone.
184
+ def self.from_fields(input, detached: false)
185
+ path = input.text("path", "")
186
+ read_only = input.bool("read_only", false)
187
+ bytes = input.bytes("image", "".b).bytes
188
+ image_path = input.text("image.path", "")
189
+ disk = bytes.empty? ? Disk.new : Disk.new(image_from(image_path, bytes, read_only || detached))
190
+ disk.opened(path, read_only || bytes.empty?) unless path.empty?
191
+ disk.load_track_fields(input)
192
+ end
193
+
194
+ def self.image_from(path, bytes, read_only)
195
+ Storage::D81Image.new(File.file?(path) ? path : "", read_only:, bytes:)
196
+ end
197
+ end
198
+
199
+ class Bus
200
+ # The drive's RAM and the byte last on its data bus, in a keyed
201
+ # snapshot module (BL-D8-RAM).
202
+ def save_fields(out)
203
+ out.int("data", @data).bytes("ram", @ram.read(0, @ram.length))
204
+ end
205
+
206
+ def load_fields(input)
207
+ @data = input.byte("data", @data)
208
+ ram = @ram.read(0, @ram.length)
209
+ @ram.write(0, input.bytes_into("ram", ram))
210
+ @watching = false
211
+ end
212
+ end
213
+
214
+ # The drive's own fields in a keyed snapshot module (BL-D8): its
215
+ # model, its clock phase and cycle count, the ATN level it last saw
216
+ # and the host's lines as the serial port latched them. The fast
217
+ # serial direction and the ports' lines come from the 8520 once it
218
+ # is loaded.
219
+ module KeyedState
220
+ def save_fields(out)
221
+ settle!
222
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles).bool("atn", @atn)
223
+ @serial_port.save_fields(out, "serial.")
224
+ out
225
+ end
226
+
227
+ def load_fields(input)
228
+ settle!
229
+ input.choice("model", [model_name], model_name)
230
+ @phase = input.int("phase", @phase)
231
+ @cycles = input.int("cycles", @cycles)
232
+ @atn = input.bool("atn", @atn)
233
+ @serial_port.load_fields(input, "serial.")
234
+ end
235
+
236
+ def restored!
237
+ @fast_serial_out = @cia.port_b_lines.anybits?(FAST_SERIAL_OUT)
238
+ ports_written
239
+ push_fast_output(@fast_serial_out ? fast_pins : 0)
240
+ forget_orbits
241
+ end
242
+
243
+ # The drive as modules of its own, as the 1541's, with the 8520 and
244
+ # the WD1772 in place of the VIAs.
245
+ def save_modules(sections, name)
246
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
247
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
248
+ sections << Snapshot::Keyed.section("#{name}-CIA") { |out| @cia.save_fields(out) }
249
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
250
+ sections << Snapshot::Keyed.section("#{name}-FDC") { |out| @fdc.save_fields(out) }
251
+ now = @fdc.now
252
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out, now) }
253
+ disk = @mechanism.disk
254
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
255
+ sections
256
+ end
257
+
258
+ # The drive from its modules, `input` being its own. The mechanism
259
+ # takes the disk's angle on the WD1772's clock, so it loads after
260
+ # the controller.
261
+ def load_modules(modules, input, name)
262
+ load_fields(input)
263
+ @bus.load_fields(modules.required("#{name}-RAM"))
264
+ @cia.load_fields(modules.required("#{name}-CIA"))
265
+ @cpu.load_fields(modules.required("#{name}-CPU"))
266
+ @fdc.load_fields(modules.required("#{name}-FDC"))
267
+ disk = modules.optional("#{name}-DISK")
268
+ mechanism = modules.required("#{name}-MECH")
269
+ @mechanism.load_fields(mechanism, disk.nil? ? nil : Disk.from_fields(disk, detached: modules.detached?),
270
+ @fdc.now)
271
+ @cia.restored!
272
+ @cpu.restored!
273
+ restored!
274
+ end
275
+ end
276
+
277
+ include KeyedState
278
+ end
279
+ end
280
+ end
@@ -92,27 +92,6 @@ module Badline
92
92
  def write(track)
93
93
  @disk&.write(@cylinder, @side, track)
94
94
  end
95
-
96
- # The disk (Disk#save_state) and the motor, the head and the turn.
97
- def save_state(out)
98
- out.boolean(!@disk.nil?)
99
- @disk&.save_state(out)
100
- out.int(@cylinder).int(@side).int(@angle).int(@since).boolean(@motor).boolean(@changed)
101
- end
102
-
103
- # Puts the state back, reusing the disk in when it's the one the state
104
- # names (Disk::SavedState.load). A disk taken out is flushed.
105
- def load_state(input)
106
- disk = input.boolean? ? Disk::SavedState.load(input, @disk) : nil
107
- flush unless disk.equal?(@disk)
108
- @disk = disk
109
- @cylinder = input.int
110
- @side = input.int
111
- @angle = input.int
112
- @since = input.int
113
- @motor = input.boolean?
114
- @changed = input.boolean?
115
- end
116
95
  end
117
96
  end
118
97
  end
@@ -55,24 +55,6 @@ module Badline
55
55
 
56
56
  # Whether the 1581 is device 8.
57
57
  def drive1581_on_device8? = @drive1581&.device == KernalTrap::Routine::DEVICE
58
-
59
- # The 1581 on the serial bus, with its device number.
60
- def save_drive1581(out)
61
- out.boolean(!@drive1581.nil?)
62
- return unless @drive1581
63
-
64
- out.int(@drive1581.device)
65
- @drive1581.save_state(out)
66
- end
67
-
68
- def load_drive1581(input)
69
- return detach_drive1581 unless input.boolean?
70
-
71
- device = input.int
72
- detach_drive1581 if @drive1581 && @drive1581.device != device
73
- attach_drive1581(Model1581.new(device:)) unless @drive1581
74
- @drive1581.load_state(input)
75
- end
76
58
  end
77
59
  end
78
60
  end
@@ -6,7 +6,6 @@ require "badline/drives/model1581/wd1772/stepping"
6
6
  require "badline/drives/model1581/wd1772/sectors"
7
7
  require "badline/drives/model1581/wd1772/sector_writes"
8
8
  require "badline/drives/model1581/wd1772/tracks"
9
- require "badline/drives/model1581/wd1772/saved_state"
10
9
 
11
10
  module Badline
12
11
  module Drives
@@ -45,7 +44,6 @@ module Badline
45
44
  include Sectors
46
45
  include SectorWrites
47
46
  include Tracks
48
- include SavedState
49
47
 
50
48
  BYTE = 64
51
49
  START = 32
@@ -9,7 +9,8 @@ require "badline/drives/model1581/wd1772"
9
9
  require "badline/drives/model1581/cpu"
10
10
  require "badline/drives/model1581/serial_port"
11
11
  require "badline/drives/model1581/bus"
12
- require "badline/drives/model1581/saved_state"
12
+ require "badline/drives/model1581/keyed_state"
13
+ require "badline/drives/model1581/slot"
13
14
 
14
15
  module Badline
15
16
  module Drives
@@ -39,7 +40,6 @@ module Badline
39
40
  class Model1581
40
41
  include Engine::Core
41
42
  include Engine::FastSerial
42
- include SavedState
43
43
 
44
44
  CLOCK_HZ = 2_000_000
45
45
 
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The drives and the serial bus badline/core's machines share. The C128's
4
+ # 1571 stays out of the core: badline/c128 requires it.
5
+ require "badline/drives/datasette"
6
+ require "badline/drives/serial_bus"
7
+ require "badline/drives/model1541"
8
+ require "badline/drives/model1581"
@@ -21,7 +21,7 @@ module Badline
21
21
  @timeline = timeline
22
22
  @snapshots = Snapshots.new(computer, options)
23
23
  @screens = Screens.new(computer)
24
- @led = DriveLed.for(computer)
24
+ @led = DriveLed.new(computer)
25
25
  @controls = Controls.new(computer)
26
26
  @window = MachineWindow.new(@screens.screen, vsync: @pacer.vsync?)
27
27
  @pacer.fit(@window.refresh_rate) if @pacer.vsync?
@@ -128,7 +128,7 @@ module Badline
128
128
  @computer = computer
129
129
  @screens.computer = computer
130
130
  fit_window
131
- @led = DriveLed.for(computer)
131
+ @led = DriveLed.new(computer)
132
132
  @controls.computer = computer
133
133
  @gamepads.computer = computer
134
134
  @sound.switch(computer.sound_source, computer.timing.clock_hz)
@@ -220,7 +220,7 @@ module Badline
220
220
  renderer = @window.renderer
221
221
  SDL.SDL_RenderClear(renderer)
222
222
  SDL.SDL_RenderCopy(renderer, @window.texture, nil, nil)
223
- @led&.draw(renderer, @window.width, @window.height)
223
+ @led.draw(renderer, @window.width, @window.height)
224
224
  @timeline.screenshots(@frames + 1).each { |path| Screenshot.write(renderer, path) }
225
225
  SDL.SDL_RenderPresent(renderer)
226
226
  end
@@ -10,7 +10,7 @@ module Badline
10
10
  # attach, an --at key names nothing or an event fails, which it warns
11
11
  # about.
12
12
  def self.run(options)
13
- error = Timeline.error(options.timeline)
13
+ error = Timeline.error(options.timeline, options.family)
14
14
  unless error.empty?
15
15
  warn "#{options.program}: #{error}"
16
16
  return 1
@@ -29,6 +29,7 @@ module Badline
29
29
  @arrows_port = 2
30
30
  @mouse = Input::Mouse1351.new
31
31
  @paddles = Input::Paddles.new
32
+ @held = {}
32
33
  end
33
34
 
34
35
  # Routes the input to another machine, the pot device going into its
@@ -73,10 +74,8 @@ module Badline
73
74
  def key(scancode, down)
74
75
  if joystick_mode? && Keys.joystick?(scancode)
75
76
  joystick_key(scancode, down)
76
- elsif @computer.family == :c128
77
- c128_key(Keys.c128_key(scancode, @computer.mode), down)
78
77
  else
79
- c64_key(Keys.c64_key(scancode), down)
78
+ machine_key(scancode, down)
80
79
  end
81
80
  end
82
81
 
@@ -104,6 +103,23 @@ module Badline
104
103
 
105
104
  private
106
105
 
106
+ # A host key lets go of the key it pressed, though the C128 has
107
+ # switched between C64 and C128 mode since and maps it elsewhere.
108
+ def machine_key(scancode, down)
109
+ @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
110
+ key = @held[scancode]
111
+ @held.delete(scancode) unless down
112
+ if @computer.family == :c128
113
+ c128_key(key, down)
114
+ else
115
+ c64_key(key, down)
116
+ end
117
+ end
118
+
119
+ def host_key(scancode)
120
+ @computer.family == :c128 ? Keys.c128_key(scancode, @computer.mode) : Keys.c64_key(scancode)
121
+ end
122
+
107
123
  # RESTORE isn't in the key matrix, so it goes to the machine instead.
108
124
  def c64_key(key, down)
109
125
  if key == :restore
@@ -2,48 +2,71 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The true drive's red LED, drawn over the bottom right corner of the
6
- # border: bright while the DOS lights it, as it does while a file is
7
- # open or after an error, and dim otherwise.
5
+ # The true drive's LEDs, drawn over the bottom right corner of the
6
+ # border: the red activity LED, bright while the DOS lights it, as it
7
+ # does while a file is open or after an error, and dim otherwise, and
8
+ # left of it the green power LED, which a 1581 flashes after an error.
9
+ # They follow whichever true drive the machine has, one plugged in
10
+ # after start among them, and are drawn only while it has one.
8
11
  class DriveLed
9
12
  WIDTH = 12
10
13
  HEIGHT = 4
11
14
 
15
+ # The power LED's distance left of the activity LED.
16
+ POWER_OFFSET = WIDTH + 4
17
+
12
18
  LIT = [0xff, 0x20, 0x20].freeze
13
19
  DARK = [0x40, 0x00, 0x00].freeze
20
+ POWER_LIT = [0x20, 0xff, 0x20].freeze
21
+ POWER_DARK = [0x00, 0x40, 0x00].freeze
14
22
 
15
- # The LED of the machine's true drive, or nil without one.
16
- def self.for(computer)
17
- drive = computer.true_drive
18
- drive ? new(drive) : nil
19
- end
20
-
21
- # Sets SDL.led_rect to where the LED goes on a screen `width` by
22
- # `height`.
23
- def self.place(width, height)
23
+ # Sets SDL.led_rect to where the activity LED goes on a screen `width`
24
+ # by `height`, or with `offset` that many pixels left of it.
25
+ def self.place(width, height, offset = 0)
24
26
  rect = SDL.led_rect
25
- SDL.rect_x(rect, width - WIDTH - 8)
27
+ SDL.rect_x(rect, width - WIDTH - 8 - offset)
26
28
  SDL.rect_y(rect, height - HEIGHT - 6)
27
29
  SDL.rect_w(rect, WIDTH)
28
30
  SDL.rect_h(rect, HEIGHT)
29
31
  end
30
32
 
31
- def initialize(drive)
32
- @drive = drive
33
+ def initialize(computer)
34
+ @computer = computer
33
35
  end
34
36
 
35
- def lit? = @drive.led_on?
37
+ # Whether the machine has a true drive, and so LEDs to draw.
38
+ def shown? = !@computer.true_drive.nil?
39
+
40
+ def lit?
41
+ drive = @computer.true_drive
42
+ drive.nil? ? false : drive.led_on?
43
+ end
44
+
45
+ def power_lit?
46
+ drive = @computer.true_drive
47
+ drive.nil? ? false : drive.power_led_on?
48
+ end
36
49
 
37
50
  def color = lit? ? LIT : DARK
38
51
 
39
- # Fills the LED's rectangle on a screen `width` by `height` in its
40
- # colour, then puts back the black that RenderClear clears with.
52
+ def power_color = power_lit? ? POWER_LIT : POWER_DARK
53
+
54
+ # Fills the LEDs' rectangles on a screen `width` by `height` in their
55
+ # colours, then puts back the black that RenderClear clears with.
41
56
  def draw(renderer, width, height)
42
- DriveLed.place(width, height)
43
- rgb = color
57
+ return unless shown?
58
+
59
+ fill(renderer, width, height, 0, color)
60
+ fill(renderer, width, height, POWER_OFFSET, power_color)
61
+ SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
62
+ end
63
+
64
+ private
65
+
66
+ def fill(renderer, width, height, offset, rgb)
67
+ DriveLed.place(width, height, offset)
44
68
  SDL.SDL_SetRenderDrawColor(renderer, rgb[0], rgb[1], rgb[2], 255)
45
69
  SDL.SDL_RenderFillRect(renderer, SDL.led_rect)
46
- SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
47
70
  end
48
71
  end
49
72
  end
@@ -8,8 +8,8 @@ module Badline
8
8
  # name. A disk or tape picked goes straight in, and a save loads.
9
9
  class MenuDialogs
10
10
  EXTENSIONS = {
11
- disk: %w[.d64 .d71 .d81 .g64 .t64 .m3u .vfl], tape: %w[.tap], cartridge: %w[.crt],
12
- program: %w[.d64 .d71 .d81 .g64 .t64 .m3u .vfl .tap .crt .prg .p00 .sid], snapshot: %w[.vsf]
11
+ disk: Media::Extensions::DEVICE8, tape: Media::Extensions.of(%i[tape]),
12
+ cartridge: Media::Extensions.of(%i[cartridge]), program: Media::Extensions::ALL, snapshot: %w[.vsf]
13
13
  }.freeze
14
14
  TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
15
15
  program: "QUICK OPEN", snapshot: "LOAD A SAVE" }.freeze
@@ -20,6 +20,7 @@ module Badline
20
20
  def initialize(computer, options)
21
21
  @computer = computer
22
22
  @at_finish = options.save_snapshot
23
+ @vice = options.vice?
23
24
  @autosave = options.frames.zero?
24
25
  @autosave_at = AUTOSAVE_FRAMES
25
26
  end
@@ -128,11 +129,21 @@ module Badline
128
129
  end
129
130
 
130
131
  def finish
131
- save(@at_finish) unless @at_finish.empty?
132
+ return if @at_finish.empty?
133
+
134
+ @vice ? export(@at_finish) : save(@at_finish)
132
135
  end
133
136
 
134
137
  private
135
138
 
139
+ # Saves the machine with VICE's modules too, and warns when it can't.
140
+ def export(path)
141
+ @computer.export_snapshot(path)
142
+ puts "Saved #{path} for badline and VICE"
143
+ rescue Snapshot::FormatError, SystemCallError => e
144
+ warn "badline: #{path}: #{e.message}"
145
+ end
146
+
136
147
  def restore_from(path)
137
148
  @computer = Snapshot.load(path) { |line| puts line }
138
149
  puts "Restored #{name_of(path)} from #{path}"