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
@@ -87,6 +87,13 @@ module Badline
87
87
  insert(:disk, set[index + step])
88
88
  end
89
89
 
90
+ # The extensions of the cartridges the machine takes: .crt, and on
91
+ # the Plus/4 raw .bin ROM images too.
92
+ def cartridge_extensions
93
+ extensions = Media::Extensions.of(%i[cartridge])
94
+ @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
95
+ end
96
+
90
97
  # Swaps the cartridge for the one at `path`, which power cycles the
91
98
  # machine, and says whether it went in.
92
99
  def attach_cartridge(path)
@@ -95,10 +102,11 @@ module Badline
95
102
 
96
103
  # What a save of the machine is named after: the cartridge, the disk,
97
104
  # the tape, or BASIC, without the bracketed parts of a file's name. A
98
- # VIC-20's are named after its disk, or else the machine.
105
+ # VIC-20's are named after its disk, or else the machine, and a
106
+ # Plus/4's after its disk or tape.
99
107
  def game_name
100
108
  vic20 = @computer.family == :vic20
101
- name = vic20 ? "" : cartridge_name
109
+ name = vic20 || @computer.family == :plus4 ? "" : cartridge_name
102
110
  disk = disk_path
103
111
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
104
112
  name = tape_name if name.empty? && !vic20
@@ -114,8 +122,10 @@ module Badline
114
122
  end
115
123
 
116
124
  # The name the cartridge in the expansion port gives itself, or an
117
- # empty one.
125
+ # empty one. A Plus/4's names the slots with a ROM in them.
118
126
  def cartridge_name
127
+ return @computer.cartridge_slots.map(&:upcase).join(", ") if @computer.family == :plus4
128
+
119
129
  cartridge = @computer.address_bus.cartridge
120
130
  cartridge.nil? ? "" : cartridge.name
121
131
  end
@@ -89,11 +89,7 @@ module Badline
89
89
 
90
90
  def vic20? = @computer.family == :vic20
91
91
 
92
- # Says in the dim colour that the VIC-20 hasn't got what the page
93
- # shows.
94
- def note
95
- @painter.text(@left, @top, "NOT ON THE VIC-20 YET", MenuTheme::DIM)
96
- end
92
+ def plus4? = @computer.family == :plus4
97
93
 
98
94
  def draw_drive
99
95
  media = @media
@@ -128,10 +124,12 @@ module Badline
128
124
  def draw_expansion
129
125
  return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
130
126
 
131
- cartridge = @computer.address_bus.cartridge
132
- info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
127
+ info("CARTRIDGE", @media.cartridge_name)
133
128
  row("INSERT...", :insert_cartridge)
134
129
  row("REMOVE", :remove_cartridge)
130
+ return if plus4?
131
+
132
+ cartridge = @computer.address_bus.cartridge
135
133
  row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer? && @computer.family == :c64
136
134
  return if @reu_kb.nil?
137
135
 
@@ -167,9 +165,11 @@ module Badline
167
165
  end
168
166
  end
169
167
 
170
- # The C128 names its model and the mode it runs in first.
168
+ # The C128 names its model and the mode it runs in first, and the
169
+ # Plus/4 its model and RAM.
171
170
  def draw_power
172
171
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
172
+ info("MACHINE", @computer.model_label) if plus4?
173
173
  row("RESET", :reset)
174
174
  row("POWER CYCLE", :power_cycle)
175
175
  skip
@@ -5,7 +5,7 @@ module Badline
5
5
  # The pause menu's PORTS page, which MenuPages draws: what each C64
6
6
  # control port has plugged in, a joystick or a pot device, and whether
7
7
  # the keys type or drive the joysticks. The VIC-20 has one port, a
8
- # joystick's.
8
+ # joystick's, and the Plus/4 two joysticks and no pot devices.
9
9
  module MenuPorts
10
10
  PORT_DEVICES = %i[joystick mouse paddles].freeze
11
11
  PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
@@ -16,6 +16,7 @@ module Badline
16
16
 
17
17
  def draw_ports
18
18
  return draw_vic20_port if @computer.family == :vic20
19
+ return draw_plus4_ports if @computer.family == :plus4
19
20
 
20
21
  toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
21
22
  toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
@@ -37,6 +38,17 @@ module Badline
37
38
  info("WASD, LEFT SHIFT", "JOYSTICK")
38
39
  end
39
40
 
41
+ # The Plus/4's two joysticks, one for each set of keys.
42
+ def draw_plus4_ports
43
+ return unless draw_keys("PLUS/4")
44
+
45
+ row("SWAP JOYSTICKS", :swap)
46
+ skip
47
+ arrows = @controls.arrows_port
48
+ info("ARROWS, SPACE", "JOYSTICK #{arrows}")
49
+ info("WASD, LEFT SHIFT", "JOYSTICK #{3 - arrows}")
50
+ end
51
+
40
52
  # The toggle between the keyboard named and the joysticks, and
41
53
  # whether the keys drive the joysticks.
42
54
  def draw_keys(keyboard)
@@ -4,9 +4,10 @@ module Badline
4
4
  module Frontend
5
5
  # The visible part of the video chip's display, the crop of the
6
6
  # machine's timing, repacked for an SDL texture in XRGB8888 in the
7
- # chip's palette. Spinel hands an Array of Integers to C as 64-bit
8
- # words, as badline/ffi does on CRuby, so each word carries two
9
- # neighbouring pixels, the left one in the low half.
7
+ # chip's palette, of 16 colours or the TED's 128. Spinel hands an Array
8
+ # of Integers to C as 64-bit words, as badline/ffi does on CRuby, so
9
+ # each word carries two neighbouring pixels, the left one in the low
10
+ # half.
10
11
  #
11
12
  # Each pixel shows `pixel_width` of the window's square pixels wide,
12
13
  # or with `dots`, that many pixels share one, as the VDC's dots do. The
@@ -14,18 +15,21 @@ module Badline
14
15
  # least MIN_WIDTH window pixels wide, and at least as high as PAL's C64
15
16
  # crop, 272 lines, so a C64 shows in the same window whatever its
16
17
  # region. A crop with fewer lines or pixels sits in the middle of the
17
- # texture, between black bands. The VIC-20's 284 by 284 crop fills its
18
- # own.
18
+ # texture, between black bands. The VIC-20's 284 by 284 crop and the
19
+ # TED's 384 by 288 fill their own.
20
+ #
21
+ # With `frame_lines`, the crop's lines run on past the frame's last
22
+ # line to its first, as the TED's do: its top border is the end of
23
+ # the frame.
19
24
  class Screen
20
25
  MIN_WIDTH = Region::PAL.crop[2]
21
26
  MIN_HEIGHT = Region::PAL.crop[3]
22
27
 
23
28
  attr_reader :pixels, :width, :height
24
29
 
25
- def initialize(video, crop, pixel_width: 1, dots: 1)
30
+ def initialize(video, crop, pixel_width: 1, dots: 1, frame_lines: 0)
26
31
  @video = video
27
32
  @col_offset = crop[0]
28
- @row_offset = crop[1]
29
33
  @lines = crop[3]
30
34
  @columns = crop[2] & ~1
31
35
  @pixel_width = pixel_width
@@ -37,8 +41,10 @@ module Badline
37
41
  @pair_count = @columns / 2
38
42
  @margin = (@row_words - @pair_count) / 2
39
43
  @band = (@height - @lines) / 2
40
- palette = video.palette
41
- @pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
44
+ top = crop[1]
45
+ wrap = frame_lines.zero? ? top + @lines : frame_lines
46
+ @sources = Array.new(@lines) { |row| (row + top) % wrap }
47
+ pair_up(video.palette)
42
48
  @pixels = Array.new(@height * @row_words, 0)
43
49
  pack_all
44
50
  end
@@ -54,9 +60,10 @@ module Badline
54
60
  video = @video
55
61
  dirty = video.dirty_lines
56
62
  display = video.display
63
+ sources = @sources
57
64
  row = 0
58
65
  while row < @lines
59
- pack_row(display, row) if dirty[row + @row_offset]
66
+ pack_row(display, row) if dirty[sources[row]]
60
67
  row += 1
61
68
  end
62
69
  video.clear_dirty_lines!
@@ -64,6 +71,18 @@ module Badline
64
71
 
65
72
  private
66
73
 
74
+ # The packed word for each pair of palette indices, the left one in
75
+ # the low bits.
76
+ def pair_up(palette)
77
+ colors = palette.size
78
+ shift = 4
79
+ shift += 1 while (1 << shift) < colors
80
+ mask = colors - 1
81
+ @shift = shift
82
+ @mask = mask
83
+ @pairs = Array.new(colors << shift) { |pair| palette[pair & mask] | (palette[(pair >> shift) & mask] << 32) }
84
+ end
85
+
67
86
  # Repacks every line, as a screen built for a display the chip has
68
87
  # already drawn starts with all of it.
69
88
  def pack_all
@@ -78,11 +97,13 @@ module Badline
78
97
  def pack_row(display, row)
79
98
  pixels = @pixels
80
99
  pairs = @pairs
81
- from = ((row + @row_offset) * @stride) + @col_offset
100
+ mask = @mask
101
+ shift = @shift
102
+ from = (@sources[row] * @stride) + @col_offset
82
103
  to = ((row + @band) * @row_words) + @margin
83
104
  last = to + @pair_count
84
105
  while to < last
85
- pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
106
+ pixels[to] = pairs[(display[from] & mask) | ((display[from + 1] & mask) << shift)]
86
107
  from += 2
87
108
  to += 1
88
109
  end
@@ -70,7 +70,8 @@ module Badline
70
70
  @screen = Screen.new(vdc, vdc.crop, dots: 2)
71
71
  else
72
72
  timing = @computer.timing
73
- @screen = Screen.new(@computer.video, timing.crop, pixel_width: timing.pixel_width)
73
+ @screen = Screen.new(@computer.video, timing.crop,
74
+ pixel_width: timing.pixel_width, frame_lines: timing.lines_per_frame)
74
75
  end
75
76
  end
76
77
 
@@ -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}"
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The machine's sound source, the C64's SID or the VIC-20's VIC, on
6
- # SDL's audio queue, as mono signed 16-bit samples. Both take
7
- # #record(rate:, clock_hz:) and #drain_samples.
5
+ # The machine's sound source, the C64's SID, the VIC-20's VIC or the
6
+ # Plus/4's TED, on SDL's audio queue, as mono signed 16-bit samples.
7
+ # Each takes #record(rate:, clock_hz:) and #drain_samples.
8
8
  # Once the device plays, it is the clock: #wait holds each frame until
9
9
  # the queue is down to `AHEAD`, so the machine runs at the device's pace.
10
10
  #
@@ -16,7 +16,7 @@ module Badline
16
16
  #
17
17
  # Each frame's samples go to a mono AudioDevice. The chip's cycles are
18
18
  # converted at `clock_hz`, the machine's clock, so an NTSC or Drean
19
- # machine plays at its own pitch.
19
+ # machine plays at its own pitch, and the TED's at its single clock.
20
20
  class Sound
21
21
  RATE = 44_100
22
22
  AHEAD = 0.08
@@ -21,10 +21,13 @@ module Badline
21
21
  @failed = false
22
22
  end
23
23
 
24
- # What's wrong with the events, the first key or menu page that
25
- # names nothing, or an empty string.
26
- def self.error(events)
27
- event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument) }
24
+ # The C128's two locking keys, outside its matrix.
25
+ LOCKS = %w[caps_lock forty_eighty].freeze
26
+
27
+ # What's wrong with the events for the family's machine, the first
28
+ # key or menu page that names nothing, or an empty string.
29
+ def self.error(events, family)
30
+ event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument, family) }
28
31
  return "no such key: #{event.argument}" unless event.nil?
29
32
 
30
33
  event = events.find { |candidate| candidate.action == "menu" && !page?(candidate.argument) }
@@ -35,12 +38,30 @@ module Badline
35
38
  # sound. An empty one is its first.
36
39
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
37
40
 
38
- def self.key?(name) = name == "restore" || !keyboard_key(name).nil? || !joystick_direction(name).nil?
41
+ # Whether the family's machine has a key of the name. The Plus/4 has
42
+ # no RESTORE.
43
+ def self.key?(name, family)
44
+ (name == "restore" && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
45
+ !keyboard_key(name, family).nil? || !joystick_direction(name).nil?
46
+ end
47
+
48
+ # The family's Keyboard key for a name such as space, return or a,
49
+ # on the C128 esc or keypad1, or on the Plus/4 help or crsr_up. The
50
+ # shifted cursor keys, such as cursor_up, are the C64's and the
51
+ # C128's.
52
+ def self.keyboard_key(name, family)
53
+ key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
54
+ return key unless key.nil? && family != :plus4
55
+
56
+ Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
57
+ end
39
58
 
40
- # The Keyboard's key for a name such as space, return or a.
41
- def self.keyboard_key(name)
42
- key = Input::Keyboard.new.matrix.flatten.find { |candidate| candidate.to_s == name }
43
- key.nil? ? Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name } : key
59
+ def self.keyboard_matrix(family)
60
+ case family
61
+ when :c128 then C128::KEYBOARD_MATRIX
62
+ when :plus4 then Plus4::KeyboardPort::MATRIX
63
+ else Input::Keyboard::C64_MATRIX
64
+ end
44
65
  end
45
66
 
46
67
  # The direction of joy1-NAME or joy2-NAME, such as up or fire.
@@ -130,6 +151,8 @@ module Badline
130
151
 
131
152
  def press(computer, event)
132
153
  name = event.argument
154
+ return lock(computer, name) if LOCKS.include?(name)
155
+
133
156
  if name == "restore"
134
157
  computer.press_restore
135
158
  return hold(event)
@@ -137,13 +160,21 @@ module Badline
137
160
 
138
161
  direction = Timeline.joystick_direction(name)
139
162
  if direction.nil?
140
- computer.keyboard.press(Timeline.keyboard_key(name))
163
+ computer.keyboard.press(Timeline.keyboard_key(name, computer.family))
141
164
  else
142
165
  joystick(computer, name).press(direction)
143
166
  end
144
167
  hold(event)
145
168
  end
146
169
 
170
+ # CAPS LOCK and 40/80 DISPLAY lock: a push puts one down if it was up,
171
+ # and up if it was down.
172
+ def lock(computer, name)
173
+ return unless computer.family == :c128
174
+
175
+ name == "caps_lock" ? computer.push_caps_lock : computer.push_display_key
176
+ end
177
+
147
178
  # The VIC-20 and the C128 have no cartridge with a button yet.
148
179
  def freeze(computer, event)
149
180
  return unless computer.family == :c64
@@ -164,7 +195,7 @@ module Badline
164
195
 
165
196
  direction = Timeline.joystick_direction(name)
166
197
  if direction.nil?
167
- computer.keyboard.release(Timeline.keyboard_key(name))
198
+ computer.keyboard.release(Timeline.keyboard_key(name, computer.family))
168
199
  else
169
200
  joystick(computer, name).release(direction)
170
201
  end
@@ -197,7 +228,7 @@ module Badline
197
228
 
198
229
  def inserted?(computer, what)
199
230
  return !computer.datasette.tape.nil? if what == "tape"
200
- return !computer.address_bus.cartridge.nil? if what == "cartridge"
231
+ return MediaSlots.cartridge?(computer) if what == "cartridge"
201
232
 
202
233
  MediaSlots.disk?(computer)
203
234
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Input
5
+ # Types PETSCII codes on a Keyboard's matrix, one key at a time, for
6
+ # software that scans the keys itself instead of reading a KERNAL's
7
+ # keyboard buffer: each code's keys go down for HOLD cycles, then up for
8
+ # GAP cycles. Codes it has no key for are skipped.
9
+ class MatrixTypist
10
+ HOLD = 100_000
11
+ GAP = 40_000
12
+
13
+ # The keys typed without SHIFT, by PETSCII code.
14
+ PLAIN = {
15
+ 0x0d => :return, 0x20 => :space, 0x2a => :*, 0x2b => :+, 0x2c => :",", 0x2d => :-, 0x2e => :".",
16
+ 0x2f => :/, 0x3a => :":", 0x3b => :";", 0x3d => :"=", 0x40 => :"@"
17
+ }.freeze
18
+
19
+ # The keys typed with SHIFT, by PETSCII code.
20
+ SHIFTED = {
21
+ 0x21 => :"1", 0x22 => :"2", 0x23 => :"3", 0x24 => :"4", 0x25 => :"5", 0x26 => :"6", 0x27 => :"7",
22
+ 0x28 => :"8", 0x29 => :"9", 0x3c => :",", 0x3e => :".", 0x3f => :/, 0x5b => :":", 0x5d => :";"
23
+ }.freeze
24
+
25
+ DIGITS = %i[0 1 2 3 4 5 6 7 8 9].freeze
26
+ LETTERS = %i[a b c d e f g h i j k l m n o p q r s t u v w x y z].freeze
27
+
28
+ # The keys that type the PETSCII code, SHIFT first, or none.
29
+ def self.keys(code)
30
+ return [DIGITS[code - 0x30]] if code.between?(0x30, 0x39)
31
+ return [LETTERS[code - 0x41]] if code.between?(0x41, 0x5a)
32
+ return [:lshift, LETTERS[code - 0xc1]] if code.between?(0xc1, 0xda)
33
+ return [PLAIN[code]] if PLAIN.key?(code)
34
+ return [:lshift, SHIFTED[code]] if SHIFTED.key?(code)
35
+
36
+ []
37
+ end
38
+
39
+ def initialize(keyboard)
40
+ @keyboard = keyboard
41
+ @held = []
42
+ @code = 0
43
+ @wait = 0
44
+ @typing = false
45
+ end
46
+
47
+ # Whether it is part way through codes: from the first key down until
48
+ # the last one is up and its gap has passed.
49
+ def typing? = @typing
50
+
51
+ # A cycle of typing +codes+, taking each code off its front as its keys
52
+ # go down.
53
+ def cycle!(codes)
54
+ @wait -= 1 if @wait.positive?
55
+ return unless @wait.zero?
56
+
57
+ if @held.empty?
58
+ press(codes)
59
+ else
60
+ @held.each { |key| @keyboard.release(key) }
61
+ @held = []
62
+ @wait = GAP
63
+ end
64
+ end
65
+
66
+ # Where it is in its typing, in a keyed snapshot module under `prefix`:
67
+ # the code whose keys are down, if any, the cycles left before they
68
+ # go up or the next code's go down, and whether it is part way
69
+ # through. The keys go down again on load.
70
+ def save_fields(out, prefix)
71
+ out.optional_int("#{prefix}code", @held.empty? ? nil : @code)
72
+ out.int("#{prefix}wait", @wait).bool("#{prefix}typing", @typing)
73
+ end
74
+
75
+ def load_fields(input, prefix)
76
+ @held.each { |key| @keyboard.release(key) }
77
+ code = input.optional_int("#{prefix}code")
78
+ @code = code || 0
79
+ @held = code.nil? ? [] : MatrixTypist.keys(code)
80
+ @held.each { |key| @keyboard.press(key) }
81
+ @wait = input.int("#{prefix}wait", @wait)
82
+ @typing = input.bool("#{prefix}typing", @typing)
83
+ end
84
+
85
+ private
86
+
87
+ def press(codes)
88
+ @typing = !codes.empty?
89
+ return unless @typing
90
+
91
+ @code = codes.shift
92
+ @held = MatrixTypist.keys(@code)
93
+ @held.each { |key| @keyboard.press(key) }
94
+ @wait = @held.empty? ? 1 : HOLD
95
+ end
96
+ end
97
+ end
98
+ end
data/lib/badline/input.rb CHANGED
@@ -5,3 +5,4 @@ require "badline/input/joystick"
5
5
  require "badline/input/control_ports"
6
6
  require "badline/input/mouse1351"
7
7
  require "badline/input/paddles"
8
+ require "badline/input/matrix_typist"
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # Observe-only PC trap on the KERNAL CHROUT routine ($FFD2). Records each
6
+ # character written while the layout finds the KERNAL mapped in, then lets execution fall
7
+ # through to the ROM, so the screen output is unaffected. Used for headless
8
+ # capture of program output.
9
+ class Chrout
10
+ ADDRESS = 0xffd2
11
+
12
+ attr_reader :output
13
+
14
+ # The KERNAL layout whose mapping the trap checks.
15
+ attr_writer :layout
16
+
17
+ def initialize(cpu:, bus:, layout:)
18
+ @cpu = cpu
19
+ @bus = bus
20
+ @layout = layout
21
+ @output = +""
22
+ end
23
+
24
+ def call
25
+ @output << ascii(@cpu.a) if @layout.kernal_mapped?(@bus)
26
+ end
27
+
28
+ def inspect
29
+ "#<#{self.class.name} output=#{@output.inspect}>"
30
+ end
31
+
32
+ private
33
+
34
+ def ascii(byte)
35
+ case byte
36
+ when 0x0d then "\n"
37
+ when 0x41..0x5a then (byte + 0x20).chr
38
+ when 0xc1..0xda then (byte - 0x80).chr
39
+ when 0x20..0x7e then byte.chr
40
+ else ""
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -81,33 +81,6 @@ module Badline
81
81
  data ? new(data) : new([], error: [ILLEGAL_TRACK_OR_SECTOR, track, sector])
82
82
  end
83
83
 
84
- # Reads what save_state wrote after the flag that marks a file open
85
- # for writing (Channels.channel_from_state). A buffer channel
86
- # takes its block in `ram`, the drive's RAM, again.
87
- def self.from_state(input, ram)
88
- new.tap { |channel| channel.load_state(input, ram) }
89
- end
90
-
91
- # A buffer channel's bytes are the drive's RAM, which the drive
92
- # writes itself.
93
- def save_state(out)
94
- out.boolean(false).boolean(@writable).int(@base).int(@length).int(@pointer)
95
- out.optional_int(@lead).optional_int(@buffer).boolean(!@error.nil?)
96
- out.ints(@error) if @error
97
- out.blob(@bytes) unless @writable
98
- end
99
-
100
- def load_state(input, ram)
101
- @writable = input.boolean?
102
- @base = input.int
103
- @length = input.int
104
- @pointer = input.int
105
- @lead = input.optional_int
106
- @buffer = input.optional_int
107
- @error = input.boolean? ? input.ints : nil
108
- @bytes = @writable ? ram : input.blob
109
- end
110
-
111
84
  def replace(bytes, length = bytes.length)
112
85
  @bytes = bytes
113
86
  rewind(length)
@@ -22,31 +22,6 @@ module Badline
22
22
 
23
23
  def clear = @channels.clear
24
24
 
25
- # Each open channel with its secondary address, in the order they
26
- # opened.
27
- def save_state(out)
28
- out.int(@channels.length)
29
- @channels.each do |secondary, channel|
30
- out.int(secondary)
31
- channel.save_state(out)
32
- end
33
- end
34
-
35
- # Buffer channels take their block in the drive's RAM again.
36
- def load_state(input, memory)
37
- @channels.clear
38
- input.int.times do
39
- secondary = input.int
40
- @channels[secondary] = Channels.channel_from_state(input, memory.ram)
41
- end
42
- end
43
-
44
- # A channel save_state wrote: a file open for writing, or a Channel
45
- # whose buffer, if it has one, is in `ram`.
46
- def self.channel_from_state(input, ram)
47
- input.boolean? ? WriteFile.from_state(input) : Channel.from_state(input, ram)
48
- end
49
-
50
25
  # Opens a buffer channel on the buffer a "#n" name asks for, or on
51
26
  # the highest free one. Returns nil when that buffer isn't free,
52
27
  # which leaves a channel asking again for its own buffer open.
@@ -62,15 +62,6 @@ module Badline
62
62
  load_buffer(Channels::BAM_BUFFER, block) if block
63
63
  end
64
64
 
65
- def save_state(out)
66
- out.blob(@ram).boolean(@bam)
67
- end
68
-
69
- def load_state(input)
70
- input.blob_into(@ram)
71
- @bam = input.boolean?
72
- end
73
-
74
65
  # Whether the BAM has been read since the last reset.
75
66
  def bam? = @bam
76
67
 
@@ -37,14 +37,6 @@ module Badline
37
37
  nil
38
38
  end
39
39
 
40
- def save_state(out)
41
- @channel.save_state(out)
42
- end
43
-
44
- def load_state(input)
45
- @channel = Channels.channel_from_state(input, [])
46
- end
47
-
48
40
  def replace(bytes)
49
41
  @channel.replace(bytes)
50
42
  end
@@ -14,14 +14,6 @@ module Badline
14
14
  TYPES = %i[del seq prg usr rel].freeze
15
15
  MODES = %i[write replace append].freeze
16
16
 
17
- # Reads what save_state wrote after the flag that marks a file
18
- # open for writing.
19
- def self.from_state(input)
20
- name = input.string
21
- type = TYPES.fetch(input.int).to_sym
22
- new(name, type, mode: MODES.fetch(input.int).to_sym).tap { |file| file.load_state(input) }
23
- end
24
-
25
17
  def initialize(name, type, mode: :write)
26
18
  @name = name
27
19
  @type = type
@@ -29,16 +21,6 @@ module Badline
29
21
  @bytes = []
30
22
  end
31
23
 
32
- def save_state(out)
33
- out.boolean(true).string(@name).int(TYPES.index(@type)).int(MODES.index(@mode))
34
- out.blob(@bytes).optional_int(@buffer)
35
- end
36
-
37
- def load_state(input)
38
- @bytes = input.blob
39
- @buffer = input.optional_int
40
- end
41
-
42
24
  def write(bytes) = @bytes.concat(bytes)
43
25
 
44
26
  def writable? = true