badline 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -1
  3. data/CLAUDE.md +27 -7
  4. data/CONTRIBUTING.md +4 -3
  5. data/README.md +8 -5
  6. data/doc/media.md +2 -1
  7. data/doc/pinned-behaviour.md +29 -0
  8. data/doc/snapshot-format.md +1047 -0
  9. data/doc/snapshots.md +72 -39
  10. data/lib/badline/audio/bare_player.rb +50 -17
  11. data/lib/badline/audio/machine_player.rb +15 -9
  12. data/lib/badline/audio/renderer.rb +32 -21
  13. data/lib/badline/c128/banks.rb +6 -3
  14. data/lib/badline/c128/bus/keyed_state.rb +45 -0
  15. data/lib/badline/c128/bus.rb +17 -3
  16. data/lib/badline/c128/drives.rb +6 -25
  17. data/lib/badline/c128/keyed_modules.rb +117 -0
  18. data/lib/badline/c128/keyed_state.rb +91 -0
  19. data/lib/badline/c128/keys.rb +9 -0
  20. data/lib/badline/c128/mmu.rb +9 -8
  21. data/lib/badline/c128/mmu_pages.rb +20 -0
  22. data/lib/badline/c128/modes.rb +19 -9
  23. data/lib/badline/c128/z80_turns.rb +1 -1
  24. data/lib/badline/c128.rb +8 -8
  25. data/lib/badline/c64/attachments.rb +0 -44
  26. data/lib/badline/c64/banked_ram/plus256k.rb +18 -10
  27. data/lib/badline/c64/banked_ram/plus60k.rb +8 -6
  28. data/lib/badline/c64/banked_ram/unexpanded.rb +2 -2
  29. data/lib/badline/c64/bus/debug_register.rb +32 -0
  30. data/lib/badline/c64/bus/keyed_state.rb +41 -0
  31. data/lib/badline/c64/bus.rb +2 -1
  32. data/lib/badline/c64/cartridge/action_replay.rb +0 -13
  33. data/lib/badline/c64/cartridge/easy_flash.rb +0 -15
  34. data/lib/badline/c64/cartridge/epyx_fastload.rb +0 -8
  35. data/lib/badline/c64/cartridge/final_cartridge3.rb +0 -12
  36. data/lib/badline/c64/cartridge/flash.rb +3 -3
  37. data/lib/badline/c64/cartridge/g_mod2.rb +0 -12
  38. data/lib/badline/c64/cartridge/geo_ram.rb +0 -16
  39. data/lib/badline/c64/cartridge/kcs_power.rb +0 -8
  40. data/lib/badline/c64/cartridge/keyed_state.rb +393 -0
  41. data/lib/badline/c64/cartridge/pagefox.rb +0 -10
  42. data/lib/badline/c64/cartridge/retro_replay.rb +0 -8
  43. data/lib/badline/c64/cartridge/rgcd.rb +0 -10
  44. data/lib/badline/c64/cartridge/super_games.rb +0 -10
  45. data/lib/badline/c64/cartridge/zaxxon.rb +0 -16
  46. data/lib/badline/c64/cartridge.rb +1 -27
  47. data/lib/badline/c64/kernal_traps.rb +28 -25
  48. data/lib/badline/c64/keyed_modules.rb +111 -0
  49. data/lib/badline/c64/keyed_state.rb +68 -0
  50. data/lib/badline/c64/reu/dma.rb +0 -4
  51. data/lib/badline/c64/reu/keyed_state.rb +140 -0
  52. data/lib/badline/c64/reu/ram.rb +0 -15
  53. data/lib/badline/c64/reu.rb +1 -3
  54. data/lib/badline/c64/true_drives.rb +56 -5
  55. data/lib/badline/c64.rb +7 -3
  56. data/lib/badline/chips/cia/interrupt_register.rb +0 -17
  57. data/lib/badline/chips/cia/keyed_parts.rb +213 -0
  58. data/lib/badline/chips/cia/serial.rb +0 -4
  59. data/lib/badline/chips/cia/time_of_day.rb +0 -22
  60. data/lib/badline/chips/cia/timer.rb +0 -20
  61. data/lib/badline/chips/cia.rb +2 -2
  62. data/lib/badline/chips/cpu/core.rb +18 -2
  63. data/lib/badline/chips/cpu/keyed_state.rb +177 -0
  64. data/lib/badline/chips/cpu/microcode.rb +16 -0
  65. data/lib/badline/chips/cpu/port.rb +20 -3
  66. data/lib/badline/chips/cpu.rb +5 -0
  67. data/lib/badline/chips/sid/decimator.rb +10 -8
  68. data/lib/badline/chips/sid/envelope.rb +1 -6
  69. data/lib/badline/chips/sid/filter.rb +0 -14
  70. data/lib/badline/chips/sid/keyed_parts.rb +286 -0
  71. data/lib/badline/chips/sid/voice.rb +0 -10
  72. data/lib/badline/chips/sid/waveform.rb +0 -2
  73. data/lib/badline/chips/sid.rb +1 -2
  74. data/lib/badline/chips/ted/beam.rb +119 -0
  75. data/lib/badline/chips/ted/timers.rb +66 -0
  76. data/lib/badline/chips/ted.rb +143 -0
  77. data/lib/badline/chips/tia.rb +49 -0
  78. data/lib/badline/chips/vdc/keyed_parts.rb +105 -0
  79. data/lib/badline/chips/vdc/painter.rb +0 -12
  80. data/lib/badline/chips/vdc/raster.rb +0 -16
  81. data/lib/badline/chips/vdc/window.rb +0 -9
  82. data/lib/badline/chips/vdc.rb +1 -2
  83. data/lib/badline/chips/via/control_lines.rb +0 -15
  84. data/lib/badline/chips/via/interrupt_register.rb +0 -9
  85. data/lib/badline/chips/via/keyed_parts.rb +163 -0
  86. data/lib/badline/chips/via/shift_register.rb +0 -14
  87. data/lib/badline/chips/via/timer1.rb +0 -13
  88. data/lib/badline/chips/via/timer2.rb +0 -18
  89. data/lib/badline/chips/via.rb +1 -2
  90. data/lib/badline/chips/vic/border_mask.rb +0 -10
  91. data/lib/badline/chips/vic/collisions.rb +0 -11
  92. data/lib/badline/chips/vic/color_patches.rb +0 -11
  93. data/lib/badline/chips/vic/display_state.rb +0 -19
  94. data/lib/badline/chips/vic/graphics_shifter.rb +0 -4
  95. data/lib/badline/chips/vic/keyed_parts.rb +228 -0
  96. data/lib/badline/chips/vic/keyed_sprites.rb +225 -0
  97. data/lib/badline/chips/vic/keyed_state.rb +102 -0
  98. data/lib/badline/chips/vic/register_log.rb +0 -18
  99. data/lib/badline/chips/vic/registers.rb +0 -9
  100. data/lib/badline/chips/vic/sequencer.rb +0 -2
  101. data/lib/badline/chips/vic/sprite/internal_bus.rb +0 -11
  102. data/lib/badline/chips/vic/sprite.rb +0 -2
  103. data/lib/badline/chips/vic/sprites.rb +0 -3
  104. data/lib/badline/chips/vic.rb +6 -2
  105. data/lib/badline/chips/vic_i/keyed_parts.rb +142 -0
  106. data/lib/badline/chips/vic_i/painter.rb +0 -4
  107. data/lib/badline/chips/vic_i/sound.rb +38 -46
  108. data/lib/badline/chips/vic_i.rb +2 -2
  109. data/lib/badline/chips/z80/core.rb +2 -2
  110. data/lib/badline/chips/z80/keyed_state.rb +73 -0
  111. data/lib/badline/chips.rb +10 -0
  112. data/lib/badline/core.rb +4 -21
  113. data/lib/badline/drives/datasette/keyed_state.rb +38 -0
  114. data/lib/badline/drives/datasette.rb +40 -7
  115. data/lib/badline/drives/engine/core.rb +3 -0
  116. data/lib/badline/drives/engine/serial_lines.rb +4 -7
  117. data/lib/badline/drives/model1541/bus.rb +0 -13
  118. data/lib/badline/drives/model1541/disk.rb +9 -8
  119. data/lib/badline/drives/model1541/keyed_state.rb +251 -0
  120. data/lib/badline/drives/model1541/mechanism.rb +0 -2
  121. data/lib/badline/drives/model1541.rb +1 -2
  122. data/lib/badline/drives/model1571/bus.rb +0 -13
  123. data/lib/badline/drives/model1571/keyed_state.rb +108 -0
  124. data/lib/badline/drives/model1571/wd1770.rb +0 -14
  125. data/lib/badline/drives/model1571.rb +1 -2
  126. data/lib/badline/drives/model1581/bus.rb +0 -13
  127. data/lib/badline/drives/model1581/disk.rb +0 -4
  128. data/lib/badline/drives/model1581/keyed_state.rb +280 -0
  129. data/lib/badline/drives/model1581/mechanism.rb +0 -21
  130. data/lib/badline/drives/model1581/slot.rb +0 -18
  131. data/lib/badline/drives/model1581/wd1772.rb +0 -2
  132. data/lib/badline/drives/model1581.rb +2 -2
  133. data/lib/badline/drives.rb +8 -0
  134. data/lib/badline/frontend/app.rb +3 -3
  135. data/lib/badline/frontend/boot.rb +1 -1
  136. data/lib/badline/frontend/controls.rb +19 -3
  137. data/lib/badline/frontend/drive_led.rb +44 -21
  138. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -2
  139. data/lib/badline/frontend/snapshots.rb +12 -1
  140. data/lib/badline/frontend/timeline.rb +28 -10
  141. data/lib/badline/input/matrix_typist.rb +98 -0
  142. data/lib/badline/input.rb +1 -0
  143. data/lib/badline/kernal_trap/chrout.rb +45 -0
  144. data/lib/badline/kernal_trap/dos/channel.rb +0 -27
  145. data/lib/badline/kernal_trap/dos/channels.rb +0 -25
  146. data/lib/badline/kernal_trap/dos/memory.rb +0 -9
  147. data/lib/badline/kernal_trap/dos/status.rb +0 -8
  148. data/lib/badline/kernal_trap/dos/write_file.rb +0 -18
  149. data/lib/badline/kernal_trap/dos.rb +0 -19
  150. data/lib/badline/kernal_trap/file_routine.rb +4 -4
  151. data/lib/badline/kernal_trap/keyed_state.rb +150 -0
  152. data/lib/badline/kernal_trap/layout.rb +75 -11
  153. data/lib/badline/kernal_trap/load.rb +14 -16
  154. data/lib/badline/kernal_trap/routine.rb +12 -0
  155. data/lib/badline/kernal_trap/save.rb +8 -18
  156. data/lib/badline/kernal_trap/serial.rb +1 -17
  157. data/lib/badline/kernal_trap.rb +2 -0
  158. data/lib/badline/machine/keyboard_buffer.rb +49 -0
  159. data/lib/badline/machine/settling.rb +29 -0
  160. data/lib/badline/machine.rb +5 -2
  161. data/lib/badline/media/boot_disk.rb +14 -2
  162. data/lib/badline/media/extensions.rb +10 -0
  163. data/lib/badline/memory.rb +3 -6
  164. data/lib/badline/options/family.rb +5 -2
  165. data/lib/badline/options/help.rb +42 -16
  166. data/lib/badline/options/table.rb +2 -0
  167. data/lib/badline/options/validation.rb +2 -0
  168. data/lib/badline/options.rb +6 -1
  169. data/lib/badline/plus4/acia.rb +45 -0
  170. data/lib/badline/plus4/bus.rb +312 -0
  171. data/lib/badline/plus4/cpu.rb +10 -0
  172. data/lib/badline/plus4/cpu_port.rb +93 -0
  173. data/lib/badline/plus4/keyboard_port.rb +51 -0
  174. data/lib/badline/plus4/model.rb +38 -0
  175. data/lib/badline/plus4/user_port.rb +31 -0
  176. data/lib/badline/plus4.rb +164 -0
  177. data/lib/badline/roms/README +38 -0
  178. data/lib/badline/roms/plus4/basic.rom +0 -0
  179. data/lib/badline/roms/plus4/function-high.rom +0 -0
  180. data/lib/badline/roms/plus4/function-low.rom +0 -0
  181. data/lib/badline/roms/plus4/kernal-ntsc.rom +0 -0
  182. data/lib/badline/roms/plus4/kernal-pal.rom +0 -0
  183. data/lib/badline/snapshot/c128_setup.rb +9 -20
  184. data/lib/badline/snapshot/c64_setup.rb +28 -24
  185. data/lib/badline/snapshot/container.rb +4 -0
  186. data/lib/badline/snapshot/field_doc/markdown.rb +112 -0
  187. data/lib/badline/snapshot/field_doc.rb +109 -0
  188. data/lib/badline/snapshot/image.rb +134 -28
  189. data/lib/badline/snapshot/keyed/cursor.rb +40 -0
  190. data/lib/badline/snapshot/keyed/modules.rb +66 -0
  191. data/lib/badline/snapshot/keyed/reader.rb +188 -0
  192. data/lib/badline/snapshot/keyed/recorder.rb +88 -0
  193. data/lib/badline/snapshot/keyed/report.rb +26 -0
  194. data/lib/badline/snapshot/keyed/writer.rb +55 -0
  195. data/lib/badline/snapshot/keyed.rb +64 -0
  196. data/lib/badline/snapshot/machine_state.rb +17 -64
  197. data/lib/badline/snapshot/storage_setup.rb +29 -34
  198. data/lib/badline/snapshot/vic20_setup.rb +6 -15
  199. data/lib/badline/snapshot/vice/reu1764.rb +2 -2
  200. data/lib/badline/snapshot/vice.rb +7 -31
  201. data/lib/badline/snapshot.rb +43 -20
  202. data/lib/badline/storage/crt_file.rb +3 -1
  203. data/lib/badline/storage/g64_image.rb +8 -3
  204. data/lib/badline/storage/tap.rb +43 -15
  205. data/lib/badline/version.rb +1 -1
  206. data/lib/badline/vic20/attachments.rb +3 -0
  207. data/lib/badline/vic20/bus/keyed_state.rb +52 -0
  208. data/lib/badline/vic20/bus.rb +1 -2
  209. data/lib/badline/vic20/keyed_modules.rb +73 -0
  210. data/lib/badline/vic20/keyed_state.rb +45 -0
  211. data/lib/badline/vic20.rb +5 -5
  212. data/lib/badline.rb +1 -0
  213. metadata +61 -42
  214. data/lib/badline/c128/bus/saved_state.rb +0 -53
  215. data/lib/badline/c128/saved_state.rb +0 -117
  216. data/lib/badline/c64/bus/saved_state.rb +0 -68
  217. data/lib/badline/c64/cartridge/flash/saved_state.rb +0 -40
  218. data/lib/badline/c64/cartridge/retro_replay/saved_state.rb +0 -47
  219. data/lib/badline/c64/cartridge/saved_state.rb +0 -63
  220. data/lib/badline/c64/reu/dma/saved_state.rb +0 -52
  221. data/lib/badline/c64/reu/saved_state.rb +0 -55
  222. data/lib/badline/c64/saved_state.rb +0 -112
  223. data/lib/badline/chips/cia/saved_state.rb +0 -49
  224. data/lib/badline/chips/cia/serial/saved_state.rb +0 -45
  225. data/lib/badline/chips/cpu/saved_state.rb +0 -117
  226. data/lib/badline/chips/sid/envelope/saved_state.rb +0 -70
  227. data/lib/badline/chips/sid/filter/saved_state.rb +0 -42
  228. data/lib/badline/chips/sid/saved_state.rb +0 -52
  229. data/lib/badline/chips/sid/waveform/saved_state.rb +0 -68
  230. data/lib/badline/chips/vdc/saved_state.rb +0 -36
  231. data/lib/badline/chips/via/saved_state.rb +0 -44
  232. data/lib/badline/chips/vic/graphics_shifter/saved_state.rb +0 -40
  233. data/lib/badline/chips/vic/saved_state.rb +0 -82
  234. data/lib/badline/chips/vic/sequencer/saved_state.rb +0 -91
  235. data/lib/badline/chips/vic/sprite/saved_state.rb +0 -66
  236. data/lib/badline/chips/vic/sprites/saved_state.rb +0 -59
  237. data/lib/badline/chips/vic_i/painter/saved_state.rb +0 -40
  238. data/lib/badline/chips/vic_i/saved_state.rb +0 -55
  239. data/lib/badline/chips/z80/saved_state.rb +0 -71
  240. data/lib/badline/chrout_trap.rb +0 -43
  241. data/lib/badline/debug_register.rb +0 -28
  242. data/lib/badline/drives/datasette/saved_state.rb +0 -37
  243. data/lib/badline/drives/model1541/disk/saved_state.rb +0 -88
  244. data/lib/badline/drives/model1541/mechanism/saved_state.rb +0 -72
  245. data/lib/badline/drives/model1541/saved_state.rb +0 -44
  246. data/lib/badline/drives/model1571/saved_state.rb +0 -47
  247. data/lib/badline/drives/model1581/disk/saved_state.rb +0 -89
  248. data/lib/badline/drives/model1581/saved_state.rb +0 -42
  249. data/lib/badline/drives/model1581/wd1772/saved_state.rb +0 -68
  250. data/lib/badline/keyboard_buffer.rb +0 -33
  251. data/lib/badline/snapshot/setup.rb +0 -31
  252. data/lib/badline/snapshot/state.rb +0 -179
  253. data/lib/badline/vic20/bus/saved_state.rb +0 -35
  254. data/lib/badline/vic20/saved_state.rb +0 -110
@@ -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,11 +38,16 @@ 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
+ def self.key?(name, family)
42
+ name == "restore" || (family == :c128 && LOCKS.include?(name)) ||
43
+ !keyboard_key(name, family).nil? || !joystick_direction(name).nil?
44
+ end
39
45
 
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 }
46
+ # The family's Keyboard key for a name such as space, return or a,
47
+ # or on the C128 esc or keypad1.
48
+ def self.keyboard_key(name, family)
49
+ matrix = family == :c128 ? C128::KEYBOARD_MATRIX : Input::Keyboard::C64_MATRIX
50
+ key = matrix.flatten.find { |candidate| candidate.to_s == name }
43
51
  key.nil? ? Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name } : key
44
52
  end
45
53
 
@@ -130,6 +138,8 @@ module Badline
130
138
 
131
139
  def press(computer, event)
132
140
  name = event.argument
141
+ return lock(computer, name) if LOCKS.include?(name)
142
+
133
143
  if name == "restore"
134
144
  computer.press_restore
135
145
  return hold(event)
@@ -137,13 +147,21 @@ module Badline
137
147
 
138
148
  direction = Timeline.joystick_direction(name)
139
149
  if direction.nil?
140
- computer.keyboard.press(Timeline.keyboard_key(name))
150
+ computer.keyboard.press(Timeline.keyboard_key(name, computer.family))
141
151
  else
142
152
  joystick(computer, name).press(direction)
143
153
  end
144
154
  hold(event)
145
155
  end
146
156
 
157
+ # CAPS LOCK and 40/80 DISPLAY lock: a push puts one down if it was up,
158
+ # and up if it was down.
159
+ def lock(computer, name)
160
+ return unless computer.family == :c128
161
+
162
+ name == "caps_lock" ? computer.push_caps_lock : computer.push_display_key
163
+ end
164
+
147
165
  # The VIC-20 and the C128 have no cartridge with a button yet.
148
166
  def freeze(computer, event)
149
167
  return unless computer.family == :c64
@@ -164,7 +182,7 @@ module Badline
164
182
 
165
183
  direction = Timeline.joystick_direction(name)
166
184
  if direction.nil?
167
- computer.keyboard.release(Timeline.keyboard_key(name))
185
+ computer.keyboard.release(Timeline.keyboard_key(name, computer.family))
168
186
  else
169
187
  joystick(computer, name).release(direction)
170
188
  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
@@ -147,25 +147,6 @@ module Badline
147
147
  # vector, as U: does.
148
148
  def reset! = reset(cold: true)
149
149
 
150
- # The mounted storage's setup (Snapshot::StorageSetup), then the drive's
151
- # RAM, its status and its open channels.
152
- def save_state(out)
153
- Snapshot::StorageSetup.write(@storage, out)
154
- out.marker("TRAP DRIVE")
155
- @memory.save_state(out)
156
- @status.save_state(out)
157
- @channels.save_state(out)
158
- end
159
-
160
- # Takes the state after the storage's setup, into a drive mounted on
161
- # the reopened storage.
162
- def load_state(input)
163
- input.marker("TRAP DRIVE")
164
- @memory.load_state(input)
165
- @status.load_state(input)
166
- @channels.load_state(input, @memory)
167
- end
168
-
169
150
  # Returns the next byte and whether it is the channel's last, or nil
170
151
  # when there is nothing left to send.
171
152
  def read(secondary)
@@ -6,17 +6,17 @@ module Badline
6
6
  private
7
7
 
8
8
  def active?
9
- kernal_mapped? && @bus.peek(0xba) == DEVICE
9
+ kernal_mapped? && @bus.peek(zero_page.device) == DEVICE
10
10
  end
11
11
 
12
12
  def filename
13
13
  Storage.strip_drive_prefix(full_filename)
14
14
  end
15
15
 
16
- # Filename pointer at $BB/$BC, length at $B7
16
+ # The file name FNADR points to, FNLEN bytes long.
17
17
  def full_filename
18
- pointer = uint16(@bus.peek(0xbb), @bus.peek(0xbc))
19
- bytes = Array.new(@bus.peek(0xb7)) do |i|
18
+ pointer = peek_word(zero_page.filename_address)
19
+ bytes = Array.new(@bus.peek(zero_page.filename_length)) do |i|
20
20
  @layout.filename_byte(@bus, (pointer + i) & 0xffff)
21
21
  end
22
22
  Storage.ascii(bytes)
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ class DOS
6
+ class Channel
7
+ # A channel's fields under `prefix`: whether it is a buffer in the
8
+ # drive's RAM, where it starts and ends, the pointer, the byte it
9
+ # leads with and its buffer's number, the error it reports once its
10
+ # bytes run out, and a file's bytes.
11
+ def save_fields(out, prefix)
12
+ out.text("#{prefix}kind", "channel").bool("#{prefix}writable", @writable).int("#{prefix}base", @base)
13
+ out.int("#{prefix}length", @length).int("#{prefix}pointer", @pointer).optional_int("#{prefix}lead", @lead)
14
+ out.optional_int("#{prefix}buffer", @buffer)
15
+ error = @error
16
+ out.ints("#{prefix}error", error) if error
17
+ out.bytes("#{prefix}bytes", @bytes) unless @writable
18
+ end
19
+
20
+ # A buffer channel's bytes are the drive's `ram`.
21
+ def load_fields(input, prefix, ram)
22
+ @writable = input.bool("#{prefix}writable", false)
23
+ @base = input.int("#{prefix}base", 0)
24
+ @length = input.int("#{prefix}length", 0)
25
+ @pointer = input.int("#{prefix}pointer", 0)
26
+ @lead = input.optional_int("#{prefix}lead")
27
+ @buffer = input.optional_int("#{prefix}buffer")
28
+ error = input.ints("#{prefix}error", [])
29
+ @error = error.empty? ? nil : error.dup
30
+ @bytes = @writable ? ram : input.bytes("#{prefix}bytes", "".b).bytes
31
+ self
32
+ end
33
+ end
34
+
35
+ class WriteFile
36
+ # A file open for writing, under `prefix`: its name, type and mode,
37
+ # the bytes written so far and the byte held back.
38
+ def save_fields(out, prefix)
39
+ out.text("#{prefix}kind", "write_file").bytes("#{prefix}name", @name.b).text("#{prefix}type", @type.to_s)
40
+ out.text("#{prefix}mode", @mode.to_s).bytes("#{prefix}bytes", @bytes).optional_int("#{prefix}held", @buffer)
41
+ end
42
+
43
+ def self.from_fields(input, prefix)
44
+ type = input.choice("#{prefix}type", TYPES.map(&:to_s), "prg")
45
+ mode = input.choice("#{prefix}mode", MODES.map(&:to_s), "write")
46
+ new(input.bytes("#{prefix}name", "".b), TYPES.find { |each| each.to_s == type } || :prg,
47
+ mode: MODES.find { |each| each.to_s == mode } || :write).tap do |file|
48
+ file.load_fields(input, prefix)
49
+ end
50
+ end
51
+
52
+ def load_fields(input, prefix)
53
+ @bytes = input.bytes("#{prefix}bytes", "".b).bytes
54
+ @buffer = input.optional_int("#{prefix}held")
55
+ end
56
+ end
57
+
58
+ class Channels
59
+ # The channels open, by secondary address, each under channel<SA>.
60
+ def save_fields(out)
61
+ out.ints("channels", @channels.keys)
62
+ @channels.each { |secondary, channel| channel.save_fields(out, "channel#{secondary}.") }
63
+ end
64
+
65
+ def load_fields(input, ram)
66
+ @channels.clear
67
+ input.ints("channels", []).each do |secondary|
68
+ @channels[secondary] = Channels.channel_from_fields(input, "channel#{secondary}.", ram)
69
+ end
70
+ end
71
+
72
+ def self.channel_from_fields(input, prefix, ram)
73
+ return WriteFile.from_fields(input, prefix) if input.text("#{prefix}kind", "channel") == "write_file"
74
+
75
+ Channel.new.load_fields(input, prefix, ram)
76
+ end
77
+ end
78
+
79
+ class Status
80
+ # The error channel, under status.
81
+ def save_fields(out) = @channel.save_fields(out, "status.")
82
+
83
+ def load_fields(input)
84
+ input.choice("status.kind", ["channel"], "channel")
85
+ @channel = Channel.new.load_fields(input, "status.", [])
86
+ end
87
+ end
88
+
89
+ class Memory
90
+ # The drive's RAM, and whether the BAM has been read.
91
+ def save_fields(out)
92
+ out.bytes("ram", @ram).bool("bam", @bam)
93
+ end
94
+
95
+ def load_fields(input)
96
+ input.bytes_into("ram", @ram)
97
+ @bam = input.bool("bam", @bam)
98
+ end
99
+ end
100
+
101
+ # The storage device 8 serves.
102
+ attr_reader :storage
103
+
104
+ # The DOS's fields in a keyed snapshot module (BL-TRAPDOS), after the
105
+ # storage's under storage.*: its RAM, the error channel and the
106
+ # channels open.
107
+ def save_fields(out)
108
+ @memory.save_fields(out)
109
+ @status.save_fields(out)
110
+ @channels.save_fields(out)
111
+ end
112
+
113
+ def load_fields(input)
114
+ @memory.load_fields(input)
115
+ @status.load_fields(input)
116
+ @channels.load_fields(input, @memory.ram)
117
+ end
118
+ end
119
+
120
+ class Serial
121
+ # The serial trap's fields under `prefix`: whether device 8 listens
122
+ # or talks, on which channel, the frame it is in and the bytes it has
123
+ # gathered.
124
+ def save_fields(out, prefix)
125
+ out.bool("#{prefix}listening", @listening).bool("#{prefix}talking", @talking)
126
+ out.optional_int("#{prefix}listen_channel", @listen_channel)
127
+ out.optional_int("#{prefix}talk_channel", @talk_channel).int("#{prefix}frame", @frame)
128
+ out.bytes("#{prefix}buffer", @buffer)
129
+ end
130
+
131
+ def load_fields(input, prefix)
132
+ @listening = input.bool("#{prefix}listening", @listening)
133
+ @talking = input.bool("#{prefix}talking", @talking)
134
+ @listen_channel = input.optional_int("#{prefix}listen_channel")
135
+ @talk_channel = input.optional_int("#{prefix}talk_channel")
136
+ @frame = input.int("#{prefix}frame", @frame)
137
+ @buffer = input.bytes("#{prefix}buffer", "".b).bytes
138
+ end
139
+ end
140
+
141
+ class Save
142
+ # Whether a SAVE is under way, under `prefix`.
143
+ def save_fields(out, prefix) = out.bool("#{prefix}saving", @saving)
144
+
145
+ def load_fields(input, prefix)
146
+ @saving = input.bool("#{prefix}saving", @saving)
147
+ end
148
+ end
149
+ end
150
+ end