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,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
@@ -55,11 +55,13 @@ module Badline
55
55
 
56
56
  # A new machine built from the options (Machine.for_options), with
57
57
  # `media`, if not nil, attached and started as the command line does,
58
- # its disk writable if `writable`. The VIC-20 raises
58
+ # its disk writable if `writable`, after the Plus/4's cartridges the
59
+ # options put in its slots. The VIC-20 and the Plus/4 raise
59
60
  # Machine::Unsupported for a .sid tune.
60
61
  def self.start(options, media, writable)
61
62
  computer = build(options, media)
62
63
  Media::TrueDrive.plug(computer) if options.true_drive?
64
+ options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
63
65
  unless media.nil?
64
66
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
65
67
  disk: { read_only: !writable })
@@ -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,27 @@ 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
+ case @computer.family
121
+ when :c128 then Keys.c128_key(scancode, @computer.mode)
122
+ when :plus4 then Keys.plus4_key(scancode)
123
+ else Keys.c64_key(scancode)
124
+ end
125
+ end
126
+
107
127
  # RESTORE isn't in the key matrix, so it goes to the machine instead.
108
128
  def c64_key(key, down)
109
129
  if key == :restore
@@ -180,8 +200,8 @@ module Badline
180
200
  end
181
201
 
182
202
  def release_all
183
- Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(Keys.c64_key(scancode)) }
184
- Keys::WASD.each_key { |scancode| @computer.keyboard.release(Keys.c64_key(scancode)) }
203
+ Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
204
+ Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
185
205
  Keys::ARROWS.each_value do |direction|
186
206
  @computer.joystick1.release(direction)
187
207
  @computer.joystick2.release(direction)
@@ -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
@@ -3,8 +3,8 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # SDL scancodes follow key positions on a US layout. They map onto the C64
6
- # keys, the C128's own keys and, in joystick mode, the joystick
7
- # directions.
6
+ # keys, the C128's and the 264's own keys and, in joystick mode, the
7
+ # joystick directions.
8
8
  module Keys
9
9
  RETURN = 40
10
10
  ESCAPE = 41
@@ -56,6 +56,15 @@ module Badline
56
56
  63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
57
57
  }.freeze
58
58
 
59
+ # The 264's keys where they differ from the C64's: ESC on Esc, with
60
+ # RUN/STOP on Page Down as on the C128, F1-F3 on F1-F3, HELP on F4,
61
+ # Insert or Help, a cursor key on each arrow, and either SHIFT on the
62
+ # one SHIFT. Page Up, RESTORE on the C64, presses nothing.
63
+ PLUS4_OTHERS = {
64
+ 41 => :esc, 78 => :run_stop, 58 => :f1, 59 => :f2, 60 => :f3, 61 => :help, 73 => :help, 117 => :help,
65
+ 79 => :crsr_right, 80 => :crsr_left, 81 => :crsr_down, 82 => :crsr_up, 229 => :lshift, 75 => :none
66
+ }.freeze
67
+
59
68
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
60
69
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
61
70
 
@@ -77,6 +86,9 @@ module Badline
77
86
  C128_OTHERS[scancode] || c64_key(scancode)
78
87
  end
79
88
 
89
+ # The 264's key for the scancode: one of PLUS4_OTHERS or the C64's.
90
+ def self.plus4_key(scancode) = PLUS4_OTHERS[scancode] || c64_key(scancode)
91
+
80
92
  # The character the key types on a US layout: a letter, a capital
81
93
  # with `shift`, or a digit, and "" for any other key.
82
94
  def self.character(scancode, shift)
@@ -31,8 +31,18 @@ module Badline
31
31
  drive.nil? ? computer.unmount : drive.insert(nil)
32
32
  end
33
33
 
34
- # Takes the cartridge out with the power off.
34
+ # Whether the expansion port has a cartridge in, on the Plus/4 in
35
+ # either cartridge's slots.
36
+ def cartridge?(computer)
37
+ return !computer.cartridge_slots.empty? if computer.family == :plus4
38
+
39
+ !computer.address_bus.cartridge.nil?
40
+ end
41
+
42
+ # Takes the cartridge out with the power off, on the Plus/4 both.
35
43
  def remove_cartridge(computer)
44
+ return computer.detach_cartridges if computer.family == :plus4
45
+
36
46
  computer.address_bus.detach_cartridge
37
47
  computer.power_cycle!
38
48
  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
@@ -37,7 +37,8 @@ module Badline
37
37
  # :program, or :snapshot, which starts in the saves folder.
38
38
  def browse(kind)
39
39
  @browsing = kind
40
- @browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, EXTENSIONS.fetch(kind))
40
+ extensions = kind == :cartridge ? @media.cartridge_extensions : EXTENSIONS.fetch(kind)
41
+ @browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
41
42
  end
42
43
 
43
44
  # Asks for a save's name, starting from `name`.