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
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/machine/clocking"
4
+ require "badline/machine/keyboard_buffer"
5
+ require "badline/machine/settling"
4
6
 
5
7
  module Badline
6
8
  # = Machine
7
9
  #
8
10
  # The machines badline builds by family, as the command line's
9
11
  # subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, and the
10
- # C128 in C64 mode, C128.
12
+ # C128 in C64 mode, C128, and the 264 series, Plus4.
11
13
  #
12
14
  # A machine answers what the front end, Media, the runners and the
13
15
  # snapshots call on it:
@@ -47,21 +49,31 @@ module Badline
47
49
  # +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
48
50
  # C128D's 1571, its true drive.
49
51
  #
52
+ # The Plus/4 answers the identity, clocking, lifecycle, video, sound and
53
+ # input parts, with nil for +control_ports+ and no RESTORE, and of the
54
+ # rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+, +mounted?+,
55
+ # +datasette+, +attach_cartridge+, +attach_drive1541+, +drive1541+,
56
+ # +true_drive+, +plug_true_drive+, the 1581's, +install_debug_register+
57
+ # and +capture_output+, and besides +model_label+, +cartridge_slots+ and
58
+ # +detach_cartridges+ for its two cartridges.
59
+ #
50
60
  # Each machine includes Machine::Clocking for the clocking part.
51
61
  module Machine
52
62
  # Media the machine named doesn't take.
53
63
  class Unsupported < ArgumentError; end
54
64
 
55
- # A new machine of `family`, :c64, :vic20 or :c128, the model of it
56
- # `model` names. A C64's model is one of Model::ALL, with its SID
65
+ # A new machine of `family`, :c64, :vic20, :c128 or :plus4, the model
66
+ # of it `model` names. A C64's model is one of Model::ALL, with its SID
57
67
  # `sid_model` when not nil and an REU of `reu` K when not nil. A
58
68
  # VIC-20's is "pal", with the RAM expansion `ram` names
59
69
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
60
70
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
61
- # for C64 mode.
71
+ # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
72
+ # the model's own RAM.
62
73
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
63
74
  return vic20(model, ram) if family == :vic20
64
75
  return c128(model, sid_model, :c64) if family == :c128
76
+ return Plus4.new(model:) if family == :plus4
65
77
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
66
78
 
67
79
  profile = Model.named(model)
@@ -77,10 +89,11 @@ module Badline
77
89
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
78
90
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
79
91
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
80
- # Unsupported for media a VIC-20 doesn't take.
92
+ # Unsupported for media a VIC-20 or a Plus/4 doesn't take.
81
93
  def self.for_options(options, media, &)
82
94
  family = options.family
83
95
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
96
+ return plus4_for(media, options.model, options.ram_kb) if family == :plus4
84
97
 
85
98
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
86
99
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -114,12 +127,22 @@ module Badline
114
127
  end
115
128
  private_class_method :vic20_for
116
129
 
130
+ # A Plus/4 with `ram_kb` K of RAM (Plus4::Model::RAM_SIZES), or with
131
+ # the model's own when nil.
132
+ def self.plus4_for(media, model, ram_kb)
133
+ unsupported = !media.nil? && Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
134
+ raise Unsupported, "doesn't go in a Plus/4" if unsupported
135
+
136
+ Plus4.new(model:, ram: ram_kb)
137
+ end
138
+ private_class_method :plus4_for
139
+
117
140
  def self.c64_media?(media)
118
141
  return false if media.nil? || !File.file?(media)
119
142
 
120
143
  extension = File.extname(media).downcase
121
144
  return true if extension == ".sid"
122
- return false unless %w[.prg .p00].include?(extension)
145
+ return false unless Media::Extensions::PROGRAMS.include?(extension)
123
146
 
124
147
  [Media::BASIC_START, Media::BASIC_START - 1].include?(Storage::P00.load_address(media))
125
148
  end
@@ -4,7 +4,8 @@ module Badline
4
4
  module Media
5
5
  # A C128 boot disk. At power-on the C128 KERNAL reads track 1, sector 0
6
6
  # of the disk in device 8 and runs it when it starts with "CBM", as boot
7
- # games have it. The 1581's DOS first loads and runs a USR file named
7
+ # games have it, from a sector image or from the raw GCR of a .g64 or
8
+ # .g71. The 1581's DOS first loads and runs a USR file named
8
9
  # COPYRIGHT CBM 86 when the disk has one, which is how CP/M's 1581 disks
9
10
  # hand the KERNAL their boot sector, so a .d81 with that file boots too.
10
11
  #
@@ -15,7 +16,7 @@ module Badline
15
16
  module BootDisk
16
17
  SIGNATURE = "CBM".bytes.freeze
17
18
  AUTO_BOOT = "copyright cbm 86"
18
- TYPES = Extensions.of(%i[disk])
19
+ TYPES = Extensions.of(%i[disk gcr])
19
20
 
20
21
  class << self
21
22
  def takes?(computer, path)
@@ -37,6 +38,7 @@ module Badline
37
38
  def boot?(path)
38
39
  type = File.extname(path).downcase
39
40
  return false unless TYPES.include?(type) && File.file?(path)
41
+ return gcr_signature(path) == SIGNATURE if Extensions.kind(path) == :gcr
40
42
 
41
43
  image = Media.open_storage(path, { read_only: true })
42
44
  block = image.read_block(1, 0)
@@ -44,6 +46,16 @@ module Badline
44
46
 
45
47
  type == ".d81" && !image.read_file(AUTO_BOOT, type: :usr).nil?
46
48
  end
49
+
50
+ private
51
+
52
+ # The first three bytes of track 1, sector 0 as the drive reads them
53
+ # off the GCR of a .g64 or .g71, or nil when the sector isn't there.
54
+ def gcr_signature(path)
55
+ track = Drives::Model1541::Disk.open(path, read_only: true).track(2)
56
+ data = track && Drives::Model1541::SectorReader.read(track.bytes, 1)[0]&.data
57
+ data && data[1, 3]
58
+ end
47
59
  end
48
60
  end
49
61
  end
@@ -28,6 +28,16 @@ module Badline
28
28
 
29
29
  # What goes in device 8 through the traps.
30
30
  MOUNTABLE = of(%i[disk archive])
31
+
32
+ # What goes in device 8, a true drive's or the traps', and the lists
33
+ # of disks to put there.
34
+ DEVICE8 = of(%i[disk gcr archive disk_list])
35
+
36
+ # Programs, which load on their own.
37
+ PROGRAMS = %w[.prg .p00].freeze
38
+
39
+ # Every medium Media.attach takes by its extension.
40
+ ALL = (KINDS.keys + PROGRAMS).freeze
31
41
  end
32
42
  end
33
43
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What the 264 series does with media of its own (Media.attach). A
6
+ # cartridge is a PLUS4 CARTRIDGE .crt, or a raw ROM image (.bin) that
7
+ # goes in the slot it's given, or by its size in cartridge 1's.
8
+ module Plus4Media
9
+ # The platform byte of a C16 or Plus/4 tape (Storage::TAP#platform).
10
+ TAPE_PLATFORM = 2
11
+
12
+ # The extension of a raw cartridge ROM image.
13
+ ROM_IMAGE = ".bin"
14
+
15
+ # The cartridge slots by xplus4's names, each as the bank and address
16
+ # a .crt's CHIP packet gives it (Plus4::Bus#attach_cartridge).
17
+ SLOTS = { "c1lo" => [0, 0x8000], "c1hi" => [0, 0xc000], "c2lo" => [1, 0x8000], "c2hi" => [1, 0xc000] }.freeze
18
+
19
+ SLOT_SIZE = 0x4000
20
+
21
+ class << self
22
+ # A tape made for another machine plays all the same, and says so.
23
+ def tape_message(tape, message)
24
+ tape.platform == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the Plus/4"
25
+ end
26
+
27
+ # Whether the file at `path` is a raw cartridge ROM image.
28
+ def rom_image?(path) = File.extname(path).downcase == ROM_IMAGE
29
+
30
+ # The ROM chips, as Storage::CRTFile::Chips, of the cartridge at
31
+ # `path`: a .crt's, or a raw image's (#rom_chips).
32
+ def cartridge_chips(path, slot: nil)
33
+ return rom_chips(File.binread(path).bytes, slot:) if rom_image?(path)
34
+
35
+ Storage::CRTFile.new(path, machine: :plus4).chips
36
+ end
37
+
38
+ # A raw ROM image's chips: one in `slot`, one of SLOTS, when it's
39
+ # named. Otherwise up to 16K goes in cartridge 1's low slot, and
40
+ # up to 32K fills its high slot too.
41
+ def rom_chips(bytes, slot: nil)
42
+ return [slot_chip(bytes, slot)] if slot
43
+
44
+ size = bytes.length
45
+ unless size.between?(1, 2 * SLOT_SIZE)
46
+ raise Machine::Unsupported, "a #{size}-byte image is no Plus/4 cartridge"
47
+ end
48
+
49
+ chips = [chip(0, 0x8000, bytes[0, SLOT_SIZE])]
50
+ chips << chip(0, 0xc000, bytes[SLOT_SIZE..]) if size > SLOT_SIZE
51
+ chips
52
+ end
53
+
54
+ private
55
+
56
+ def slot_chip(bytes, slot)
57
+ bank, address = SLOTS.fetch(slot) { raise ArgumentError, "no Plus/4 cartridge slot named #{slot}" }
58
+ size = bytes.length
59
+ raise Machine::Unsupported, "a #{size}-byte image doesn't fit a 16K slot" unless size.between?(1, SLOT_SIZE)
60
+
61
+ chip(bank, address, bytes)
62
+ end
63
+
64
+ def chip(bank, address, data) = Storage::CRTFile::Chip.new(chip_type: 0, bank:, address:, data:)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -2,26 +2,40 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # A program loading into a C64 or a C128 once it has booted
6
- # (Media.attach). The VIC-20 has its own (Vic20Basic).
5
+ # A program loading into a C64, a C128 or a 264 series machine once it
6
+ # has booted (Media.attach). The VIC-20 has its own (Vic20Basic).
7
7
  module Program
8
+ # Where BASIC 3.5 starts on the 264 series.
9
+ PLUS4_BASIC_START = 0x1001
10
+
8
11
  class << self
9
12
  # Loads the program where it says, and RUNs one that loads at the
10
13
  # start of BASIC.
11
14
  def load(computer, data, autostart:)
12
15
  load_addr = computer.load_prg(data)
16
+ start, text_end, load_end = basic(computer)
13
17
  # Run only makes sense for programs at BASIC start, or a byte ahead
14
- # of it, where BASIC keeps the zero before its first line. BASIC 7.0
15
- # starts at $1C01 and keeps the end of its text at $1210.
16
- start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
18
+ # of it, where BASIC keeps the zero before its first line.
17
19
  return unless autostart && (load_addr == start || load_addr == start - 1)
18
20
 
19
21
  # The end of the program, where BASIC's variables start, and where
20
22
  # the KERNAL's LOAD leaves its end address.
21
23
  end_addr = load_addr + data.length - 2
22
- [text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
24
+ [text_end, load_end].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
23
25
  computer.type_text("run\r")
24
26
  end
27
+
28
+ private
29
+
30
+ # Where BASIC starts, the pointer to the end of its text and the
31
+ # KERNAL's end address (EAL). BASIC 7.0 starts at $1C01 and keeps
32
+ # the end of its text at $1210, and the 264 KERNAL keeps EAL at $9D.
33
+ def basic(computer)
34
+ return [PLUS4_BASIC_START, 0x2d, KernalTrap::PLUS4_ZERO_PAGE.end_address] if computer.family == :plus4
35
+ return [0x1c01, 0x1210, 0xae] if computer.family == :c128 && computer.mode == :c128
36
+
37
+ [BASIC_START, 0x2d, 0xae]
38
+ end
25
39
  end
26
40
  end
27
41
  end
data/lib/badline/media.rb CHANGED
@@ -10,6 +10,7 @@ require "badline/media/disk_list"
10
10
  require "badline/media/vic20_basic"
11
11
  require "badline/media/vic20_cartridge"
12
12
  require "badline/media/vic20_media"
13
+ require "badline/media/plus4_media"
13
14
  require "badline/media/tune"
14
15
  require "badline/media/program"
15
16
 
@@ -44,13 +45,14 @@ module Badline
44
45
  # An .m3u or .vfl list of disks attaches the first disk it lists
45
46
  # (DiskList). Every machine takes each kind of medium the same way,
46
47
  # except that the VIC-20 (Vic20Media) has cartridges, programs and a
47
- # disk autostart of its own, and takes no .sid tunes. The C128 in C64
48
- # mode takes a C64's.
48
+ # disk autostart of its own, and takes no .sid tunes, and the 264
49
+ # series loads programs at the start of its BASIC (Program), takes
50
+ # .crt cartridges and raw .bin ROM images in its two cartridges'
51
+ # slots (Plus4Media), and no .sid tunes either. The
52
+ # C128 in C64 mode takes a C64's.
49
53
  def attach(computer, path, autostart: true, subtune: nil, **)
50
54
  path = DiskList.disk(path)
51
- if computer.family == :vic20 && Vic20Media::UNSUPPORTED.include?(File.extname(path).downcase)
52
- raise ArgumentError, "#{path} doesn't go in a VIC-20"
53
- end
55
+ refuse_unsupported(computer, path)
54
56
 
55
57
  BootDisk.attach(computer, path, **) || attach_kind(computer, path, autostart:, subtune:, **)
56
58
  end
@@ -89,10 +91,12 @@ module Badline
89
91
  # What the medium at `path` is to `computer`, which attach goes by:
90
92
  # :true_drive for a disk that goes in a true drive (TrueDrive),
91
93
  # :directory, :storage for a disk image or a .t64 that the traps
92
- # mount, and otherwise its kind by its extension (Extensions).
94
+ # mount, :cartridge for a raw ROM image on the 264 series
95
+ # (Plus4Media), and otherwise its kind by its extension (Extensions).
93
96
  def kind(computer, path)
94
97
  return :true_drive if TrueDrive.takes?(computer, path)
95
98
  return :directory if File.directory?(path)
99
+ return :cartridge if computer.family == :plus4 && Plus4Media.rom_image?(path)
96
100
 
97
101
  kind = Extensions.kind(path)
98
102
  %i[disk archive].include?(kind) ? :storage : kind
@@ -116,6 +120,16 @@ module Badline
116
120
 
117
121
  private
118
122
 
123
+ # The VIC-20 and the 264 series have no SID to play a .sid tune on.
124
+ def refuse_unsupported(computer, path)
125
+ return unless Vic20Media::UNSUPPORTED.include?(File.extname(path).downcase)
126
+
127
+ case computer.family
128
+ when :vic20 then raise ArgumentError, "#{path} doesn't go in a VIC-20"
129
+ when :plus4 then raise ArgumentError, "#{path} doesn't go in a Plus/4"
130
+ end
131
+ end
132
+
119
133
  def attach_kind(computer, path, autostart:, subtune:, **options)
120
134
  disk = options.fetch(:disk, {})
121
135
  case kind(computer, path)
@@ -151,25 +165,32 @@ module Badline
151
165
  computer.family == :vic20 ? Vic20Media.disk_autostart(yield) : AUTOSTART
152
166
  end
153
167
 
154
- # A VIC-20 cartridge is its ROM chips.
168
+ # A VIC-20 or 264 series cartridge is its ROM chips. On the 264, the
169
+ # options' `slot` names the slot a raw ROM image goes in
170
+ # (Plus4Media.cartridge_chips).
155
171
  def attach_cartridge(computer, path, options)
156
- if computer.family == :vic20
157
- computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
172
+ case computer.family
173
+ when :vic20 then computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
174
+ when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
158
175
  else
159
176
  computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
160
177
  end
161
178
  "Attached cartridge #{path}"
162
179
  end
163
180
 
164
- # A VIC-20 says when the tape was made for another machine
165
- # (Vic20Media.tape_message).
181
+ # A VIC-20 or a 264 says when the tape was made for another machine
182
+ # (Vic20Media.tape_message, Plus4Media.tape_message).
166
183
  def attach_tape(computer, path, autostart:)
167
184
  tape = Storage::TAP.new(path)
168
185
  computer.datasette.insert(tape)
169
186
  computer.datasette.play!
170
187
  computer.type_text(TAPE_AUTOSTART) if autostart
171
188
  message = "Inserted #{path} in the datasette"
172
- computer.family == :vic20 ? Vic20Media.tape_message(tape, message) : message
189
+ case computer.family
190
+ when :vic20 then Vic20Media.tape_message(tape, message)
191
+ when :plus4 then Plus4Media.tape_message(tape, message)
192
+ else message
193
+ end
173
194
  end
174
195
 
175
196
  # A VIC-20 has programs of its own (Vic20Media.attach_program).
@@ -29,13 +29,10 @@ module Badline
29
29
  Array(bytes).each_with_index { |b, i| poke(addr + i, b) }
30
30
  end
31
31
 
32
- def save_state(out)
33
- out.blob(@storage)
34
- end
32
+ # Every byte as one record of a keyed snapshot module, under `key`.
33
+ def save_fields(out, key) = out.bytes(key, @storage)
35
34
 
36
- def load_state(input)
37
- input.blob_into(@storage)
38
- end
35
+ def load_fields(input, key) = input.bytes_into(key, @storage)
39
36
 
40
37
  # Refills the whole memory as if newly built with `initial`
41
38
  def clear!(initial = [])
@@ -12,13 +12,14 @@ module Badline
12
12
 
13
13
  private
14
14
 
15
- # Takes `sid`, `c64`, `vic20` or `c128` when given first.
15
+ # Takes `sid`, `c64`, `vic20`, `c128` or `plus4` when given first.
16
16
  def subcommand(args)
17
17
  case args.first
18
18
  when "sid" then @sid_command = true
19
19
  when "c64" then @family = :c64
20
20
  when "vic20" then @family = :vic20
21
21
  when "c128" then @family = :c128
22
+ when "plus4" then @family = :plus4
22
23
  else return
23
24
  end
24
25
  @family_named = !@sid_command
@@ -29,6 +30,7 @@ module Badline
29
30
  case @family
30
31
  when :vic20 then VIC20_MODELS
31
32
  when :c128 then C128_MODELS
33
+ when :plus4 then PLUS4_MODELS
32
34
  else MODELS
33
35
  end
34
36
  end
@@ -37,7 +39,9 @@ module Badline
37
39
  # the C128's.
38
40
  def pick_family
39
41
  return if @family_named || @sid_command || @media_path.nil?
40
- return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
42
+
43
+ program = Media::Extensions::PROGRAMS.include?(File.extname(@media_path).downcase)
44
+ return unless program && File.file?(@media_path)
41
45
 
42
46
  @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
43
47
  end
@@ -16,6 +16,16 @@ module Badline
16
16
  filter cycle by cycle, which is exact but takes about twice as long.
17
17
  TEXT
18
18
 
19
+ # The article for +name+: "an" where it starts with a vowel sound. A
20
+ # name of letters with a vowel among them is read as a word, and any
21
+ # other letter by letter, so .sid and .tap take "a", and .m3u and .lnx
22
+ # "an".
23
+ def self.article(name)
24
+ word = name.delete_prefix(".").downcase
25
+ vowels = word.match?(/\A[a-z]*[aeiou][a-z]*\z/) ? "aeiou" : "aefhilmnorsx8"
26
+ vowels.include?(word[0]) ? "an" : "a"
27
+ end
28
+
19
29
  # What --help prints: the usage, then the options this build takes,
20
30
  # section by section, laid out as OptionParser did.
21
31
  def help
@@ -35,11 +45,45 @@ module Badline
35
45
 
36
46
  def describe(option) = @native && option.name == "--sound" ? "#{option.text} (default)" : option.text
37
47
 
48
+ # The media the window opens, by their extensions (Media::Extensions),
49
+ # wrapped as the banner is.
50
+ def media_kinds
51
+ text = "Media can be #{listed(Media::Extensions::PROGRAMS)} program, #{listed(%i[disk])} disk image, " \
52
+ "#{listed(%i[gcr])} disk image for a true drive, #{listed(%i[disk_list])} list of disk images, " \
53
+ "#{listed(%i[archive])} tape archive, #{listed(%i[tape])} tape, #{listed(%i[cartridge])} " \
54
+ "cartridge, #{listed(%i[tune])} tune, #{listed(%w[.vsf])} snapshot, or a directory to mount as device 8."
55
+ wrap(text)
56
+ end
57
+
58
+ # The extensions of the kinds, or the extensions themselves, after the
59
+ # article the first takes.
60
+ def listed(kinds)
61
+ extensions = kinds.first.is_a?(Symbol) ? Media::Extensions.of(kinds) : kinds
62
+ "#{Options.article(extensions.first)} #{extensions.join('/')}"
63
+ end
64
+
65
+ # The words of `text` in lines of at most 70 characters.
66
+ def wrap(text)
67
+ lines = [""]
68
+ text.split.each do |word|
69
+ line = lines.last
70
+ if line.empty?
71
+ lines[-1] = word
72
+ elsif line.size + word.size < 70
73
+ lines[-1] = "#{line} #{word}"
74
+ else
75
+ lines << word
76
+ end
77
+ end
78
+ lines.join("\n")
79
+ end
80
+
38
81
  def banner
39
82
  <<~BANNER
40
83
  Usage: #{@program} [c64] [options] [media]
41
84
  #{@program} vic20 [options] [media]
42
85
  #{@program} c128 [options] [media]
86
+ #{@program} plus4 [options] [media]
43
87
  #{@program} --headless [options] tune.sid
44
88
  #{@program} [options] tune.sid --audio-out FILE
45
89
  #{@program} sid [options] FILE|DIR...
@@ -48,13 +92,11 @@ module Badline
48
92
  c64 Run a C64, the one --model names (the default)
49
93
  vic20 Run a PAL VIC-20, with the RAM --ram names
50
94
  c128 Run a C128 in C128 mode, the one --model names
95
+ plus4 Run a Plus/4 or a C16, the one --model names
51
96
  sid Play .sid tunes and directories of them
52
97
 
53
- Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
54
- .g64 disk image for the true 1541, an .m3u or .vfl list of disk
55
- images, a .t64 tape archive, a .tap tape, a .crt cartridge, a .sid
56
- tune, a .vsf snapshot, or a directory to mount as device 8. It
57
- opens in the emulator window. There F11 quicksaves the machine to
98
+ #{media_kinds}
99
+ It opens in the emulator window. There F11 quicksaves the machine to
58
100
  the next of five slots in badline's data folder, replacing the
59
101
  oldest, and F12 restores the newest quicksave or named save. The
60
102
  window also autosaves every two minutes of play, which only the
@@ -66,6 +108,12 @@ module Badline
66
108
  tune opens in the SID player, as `sid` plays it, unless an option
67
109
  of the emulator's window asks for the machine.
68
110
 
111
+ The Plus/4 and the C16 take programs, disks, tapes, and cartridges as
112
+ .crt files or raw .bin ROM images. A .bin goes in cartridge 1, its
113
+ low slot for up to 16K and both slots for 32K, and --c1lo, --c1hi,
114
+ --c2lo and --c2hi put one in the slot they name. There Escape is ESC,
115
+ Page Down RUN/STOP and F4 HELP.
116
+
69
117
  --headless plays a .sid tune on the host's audio device without the
70
118
  window, and --audio-out renders it to 16-bit PCM instead. The
71
119
  container follows the file's extension, .wav or .aiff. Played on a
@@ -85,17 +133,19 @@ module Badline
85
133
 
86
134
  --at and --script run events once a number of frames has run, as
87
135
  in --at 3500:key=space or --at 250,500:screenshot=shot%05d.bmp.
88
- key=NAME presses a C64 key (space, return, a, f1, run_stop, restore)
89
- or a joystick's direction or fire (joy1-up, joy2-fire) for #{Event::HOLD}
90
- frames. type=TEXT types it, with \\n for RETURN. insert=FILE swaps
91
- in a disk, a list's first disk, a tape or a cartridge, and
92
- eject=disk, tape or cartridge takes one out. screenshot=FILE saves
93
- the frame as a .bmp, with the frame's number in place of %d. menu
94
- opens the pause menu, and menu=PAGE opens it on snapshots, drive,
95
- datasette, expansion, ports, sound or power. Its frames still count,
96
- and resume closes it. display=vdc or display=vic shows the C128's
97
- 80 or 40 column screen, as F8 switches them. reset, freeze and quit
98
- take no argument.
136
+ key=NAME presses a key (space, return, a, f1, run_stop, restore, and
137
+ on the C128 esc, help, keypad1 and the rest, and on the Plus/4
138
+ esc, help and f1 to f3) or a joystick's
139
+ direction or fire (joy1-up, joy2-fire) for #{Event::HOLD} frames, and
140
+ pushes the C128's caps_lock or forty_eighty, which lock. type=TEXT
141
+ types it, with \\n for RETURN. insert=FILE swaps in a disk, a list's
142
+ first disk, a tape or a cartridge, and eject=disk, tape or
143
+ cartridge takes one out. screenshot=FILE saves the frame as a .bmp,
144
+ with the frame's number in place of %d. menu opens the pause menu,
145
+ and menu=PAGE opens it on snapshots, drive, datasette, expansion,
146
+ ports, sound or power. Its frames still count, and resume closes
147
+ it. display=vdc or display=vic shows the C128's 80 or 40 column
148
+ screen, as F8 switches them. reset, freeze and quit take no argument.
99
149
  BANNER
100
150
  end
101
151
 
@@ -14,6 +14,10 @@ module Badline
14
14
  # The C128's models, as C128::Model names them.
15
15
  C128_MODELS = %w[c128 c128ntsc c128dcr c128dcrntsc].freeze
16
16
 
17
+ # The 264 series' models, as Plus4::Model names them, with the C116 as
18
+ # another name for the C16.
19
+ PLUS4_MODELS = %w[plus4 plus4ntsc c16 c16ntsc c116].freeze
20
+
17
21
  # The VIC-20 RAM expansions --ram takes, in the order the help lists
18
22
  # them.
19
23
  RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
@@ -40,12 +44,21 @@ module Badline
40
44
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
41
45
  section: "Window options", needs: :window),
42
46
  Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal; " \
43
- "C128: #{C128_MODELS.join(', ')}",
47
+ "C128: #{C128_MODELS.join(', ')}; Plus/4: #{PLUS4_MODELS.join(', ')}",
44
48
  section: "Window options", needs: :window),
45
49
  Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
46
50
  Option.new("--c64", "Start the C128 in C64 mode, holding C= at power-on",
47
51
  section: "Window options", needs: :window),
48
- Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded)",
52
+ Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded); " \
53
+ "Plus/4 RAM: 16k, 32k or 64k (default: the model's own)",
54
+ section: "Window options", needs: :window),
55
+ Option.new("--c1lo FILE", "Put a .bin ROM image in the Plus/4's cartridge 1, low slot ($8000)",
56
+ section: "Window options", needs: :window),
57
+ Option.new("--c1hi FILE", "Put a .bin ROM image in cartridge 1's high slot ($C000)",
58
+ section: "Window options", needs: :window),
59
+ Option.new("--c2lo FILE", "Put a .bin ROM image in cartridge 2's low slot ($8000)",
60
+ section: "Window options", needs: :window),
61
+ Option.new("--c2hi FILE", "Put a .bin ROM image in cartridge 2's high slot ($C000)",
49
62
  section: "Window options", needs: :window),
50
63
  Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
51
64
  section: "Window options", needs: :window),
@@ -75,6 +88,8 @@ module Badline
75
88
  section: "Testing options", needs: :window),
76
89
  Option.new("--save-snapshot FILE", "Save the machine as a .vsf snapshot after the last frame",
77
90
  section: "Testing options", needs: :window),
91
+ Option.new("--vice", "Add VICE's modules to --save-snapshot's C64 snapshot, for x64sc",
92
+ section: "Testing options", needs: :window),
78
93
  Option.new("--at FRAME:EVENT", "Run EVENT once FRAME frames have run (see above)",
79
94
  section: "Testing options", needs: :window),
80
95
  Option.new("--script FILE", "Run the events in FILE, one FRAME:EVENT a line",
@@ -21,6 +21,12 @@ module Badline
21
21
  RAM_CONFIGURATIONS[value]
22
22
  end
23
23
 
24
+ def plus4_ram(value)
25
+ raise Error, "invalid argument: --ram #{value}" unless PLUS4_RAM.key?(value)
26
+
27
+ PLUS4_RAM[value]
28
+ end
29
+
24
30
  def reu_size(value)
25
31
  raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
26
32
 
@@ -62,7 +68,12 @@ module Badline
62
68
  if @family == :vic20
63
69
  validate_vic20
64
70
  elsif !@ram.nil?
65
- raise Error, "--ram needs vic20"
71
+ raise Error, "--ram needs vic20 or plus4"
72
+ end
73
+ if @family == :plus4
74
+ validate_plus4
75
+ elsif !@cartridges.empty?
76
+ raise Error, "--#{@cartridges.keys.first} needs plus4"
66
77
  end
67
78
  if @family == :c128
68
79
  validate_c128
@@ -89,6 +100,19 @@ module Badline
89
100
  raise Error, "the VIC-20 needs the window" if headless?
90
101
  end
91
102
 
103
+ # The Plus/4 runs in the window, without the C64's SID and REU, its
104
+ # NTSC models have names of their own, and its cartridge slots take
105
+ # raw ROM images.
106
+ def validate_plus4
107
+ raise Error, "--ntsc needs the C64; the NTSC Plus/4 is --model plus4ntsc" if @models.include?("ntsc")
108
+ raise Error, "--sid needs the C64" unless @sid_model.nil?
109
+ raise Error, "--reu needs the C64" unless @reu.nil?
110
+ raise Error, "the Plus/4 needs the window" if headless?
111
+
112
+ image = @cartridges.values.find { |path| !File.extname(path).casecmp?(".bin") }
113
+ raise Error, "not a .bin ROM image: #{image}" unless image.nil?
114
+ end
115
+
92
116
  # `sid` takes directories as well as tunes.
93
117
  def validate_tune(path)
94
118
  return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
@@ -118,7 +142,7 @@ module Badline
118
142
  end
119
143
 
120
144
  def validate_paths
121
- missing = tune_paths.find { |path| !File.exist?(path) }
145
+ missing = (tune_paths + @cartridges.values).find { |path| !File.exist?(path) }
122
146
  raise Error, "no such file or directory: #{missing}" unless missing.nil?
123
147
  raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
124
148
  end