badline 0.16.1 → 0.18.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +213 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +265 -26
  6. data/doc/media.md +5 -2
  7. data/doc/pinned-behaviour.md +189 -0
  8. data/doc/snapshot-format.md +616 -74
  9. data/doc/snapshots.md +192 -24
  10. data/exe/badline-ruby +5 -0
  11. data/lib/badline/audio/terminal.rb +34 -2
  12. data/lib/badline/c128/keyed_state.rb +2 -0
  13. data/lib/badline/c128.rb +1 -0
  14. data/lib/badline/c64/keyed_state.rb +2 -0
  15. data/lib/badline/c64.rb +1 -0
  16. data/lib/badline/cbm2/acia.rb +10 -0
  17. data/lib/badline/cbm2/bus.rb +263 -0
  18. data/lib/badline/cbm2/cpu.rb +58 -0
  19. data/lib/badline/cbm2/crtc_video.rb +133 -0
  20. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  21. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  22. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  23. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  24. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  25. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  26. data/lib/badline/cbm2/keyed_state.rb +95 -0
  27. data/lib/badline/cbm2/model.rb +44 -0
  28. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  29. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  30. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  31. data/lib/badline/cbm2/p500/model.rb +39 -0
  32. data/lib/badline/cbm2/p500/pages.rb +57 -0
  33. data/lib/badline/cbm2/p500.rb +264 -0
  34. data/lib/badline/cbm2/sid_page.rb +19 -0
  35. data/lib/badline/cbm2.rb +270 -0
  36. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  37. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  38. data/lib/badline/chips/acia.rb +307 -0
  39. data/lib/badline/chips/cia/timer.rb +2 -2
  40. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  41. data/lib/badline/chips/crtc.rb +3 -2
  42. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  43. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  44. data/lib/badline/chips/m6809/core.rb +149 -0
  45. data/lib/badline/chips/m6809/indexed.rb +82 -0
  46. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  47. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  48. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  49. data/lib/badline/chips/m6809/operations.rb +258 -0
  50. data/lib/badline/chips/m6809/plans.rb +143 -0
  51. data/lib/badline/chips/m6809/registers.rb +82 -0
  52. data/lib/badline/chips/m6809/stack.rb +140 -0
  53. data/lib/badline/chips/m6809.rb +13 -0
  54. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  55. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  56. data/lib/badline/chips/riot/timer.rb +91 -0
  57. data/lib/badline/chips/riot.rb +162 -0
  58. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  59. data/lib/badline/chips/rriot.rb +134 -0
  60. data/lib/badline/chips/sid/filter.rb +4 -4
  61. data/lib/badline/chips/sid/waveform.rb +6 -6
  62. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  63. data/lib/badline/chips/tpi.rb +233 -0
  64. data/lib/badline/chips/vic/bank.rb +60 -5
  65. data/lib/badline/chips/vic/sprite.rb +4 -3
  66. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  67. data/lib/badline/chips/vic_i.rb +84 -21
  68. data/lib/badline/data_path.rb +3 -3
  69. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  70. data/lib/badline/drives/datasette.rb +27 -3
  71. data/lib/badline/drives/ieee_models.rb +54 -0
  72. data/lib/badline/drives/model1001.rb +37 -0
  73. data/lib/badline/drives/model2031.rb +6 -0
  74. data/lib/badline/drives/model4040/disk.rb +62 -0
  75. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  76. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  77. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  78. data/lib/badline/drives/model4040/parts.rb +20 -0
  79. data/lib/badline/drives/model4040.rb +179 -0
  80. data/lib/badline/drives/model8050/board.rb +77 -0
  81. data/lib/badline/drives/model8050/disk.rb +163 -0
  82. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  83. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  84. data/lib/badline/drives/model8050/idle.rb +181 -0
  85. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  86. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  87. data/lib/badline/drives/model8050/keyed_state.rb +163 -0
  88. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  89. data/lib/badline/drives/model8050/parts.rb +37 -0
  90. data/lib/badline/drives/model8050.rb +131 -0
  91. data/lib/badline/drives/model9060/idle.rb +149 -0
  92. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  93. data/lib/badline/drives/model9060/parts.rb +98 -0
  94. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  95. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  96. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  97. data/lib/badline/drives/model9060.rb +195 -0
  98. data/lib/badline/frontend/app.rb +18 -3
  99. data/lib/badline/frontend/boot.rb +4 -3
  100. data/lib/badline/frontend/controls.rb +107 -15
  101. data/lib/badline/frontend/gamepads.rb +3 -3
  102. data/lib/badline/frontend/keys.rb +97 -2
  103. data/lib/badline/frontend/kim1_panel.rb +208 -0
  104. data/lib/badline/frontend/media_slots.rb +8 -1
  105. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  106. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  107. data/lib/badline/frontend/menu/menu_media.rb +47 -14
  108. data/lib/badline/frontend/menu/menu_pages.rb +113 -4
  109. data/lib/badline/frontend/menu/menu_ports.rb +77 -4
  110. data/lib/badline/frontend/menu/pause_menu.rb +6 -2
  111. data/lib/badline/frontend/pet_keyboard.rb +45 -7
  112. data/lib/badline/frontend/screen.rb +1 -1
  113. data/lib/badline/frontend/screens.rb +40 -2
  114. data/lib/badline/frontend/teletype_pane.rb +152 -0
  115. data/lib/badline/frontend/timeline.rb +49 -11
  116. data/lib/badline/frontend.rb +3 -0
  117. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  118. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  119. data/lib/badline/kernal_trap/dos.rb +18 -5
  120. data/lib/badline/kernal_trap/ieee.rb +4 -2
  121. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  122. data/lib/badline/kim1/bus.rb +117 -0
  123. data/lib/badline/kim1/cpu.rb +25 -0
  124. data/lib/badline/kim1/host_terminal.rb +74 -0
  125. data/lib/badline/kim1/key_entry.rb +74 -0
  126. data/lib/badline/kim1/keyed_modules.rb +53 -0
  127. data/lib/badline/kim1/keyed_state.rb +133 -0
  128. data/lib/badline/kim1/panel.rb +176 -0
  129. data/lib/badline/kim1/rriots.rb +28 -0
  130. data/lib/badline/kim1/sound.rb +29 -0
  131. data/lib/badline/kim1/tape.rb +116 -0
  132. data/lib/badline/kim1/teletype.rb +193 -0
  133. data/lib/badline/kim1.rb +190 -0
  134. data/lib/badline/machine/ieee_drives.rb +171 -0
  135. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  136. data/lib/badline/machine/line_sound.rb +122 -0
  137. data/lib/badline/machine/settling.rb +4 -0
  138. data/lib/badline/machine.rb +62 -14
  139. data/lib/badline/media/cbm2_media.rb +29 -0
  140. data/lib/badline/media/extensions.rb +8 -4
  141. data/lib/badline/media/kim1_media.rb +150 -0
  142. data/lib/badline/media/pet_media.rb +3 -1
  143. data/lib/badline/media/program.rb +6 -3
  144. data/lib/badline/media/true_drive.rb +51 -25
  145. data/lib/badline/media.rb +23 -8
  146. data/lib/badline/options/family.rb +108 -6
  147. data/lib/badline/options/help.rb +58 -12
  148. data/lib/badline/options/table.rb +60 -14
  149. data/lib/badline/options/validation.rb +71 -11
  150. data/lib/badline/options.rb +46 -13
  151. data/lib/badline/pet/bus.rb +16 -2
  152. data/lib/badline/pet/cassettes.rb +22 -1
  153. data/lib/badline/pet/ieee_drive.rb +5 -64
  154. data/lib/badline/pet/ieee_ports.rb +7 -0
  155. data/lib/badline/pet/keyboard_ports.rb +9 -2
  156. data/lib/badline/pet/keyed_state.rb +4 -0
  157. data/lib/badline/pet/model.rb +11 -2
  158. data/lib/badline/pet/rom_sets.rb +1 -0
  159. data/lib/badline/pet/sound.rb +5 -105
  160. data/lib/badline/pet/superpet/acia.rb +12 -0
  161. data/lib/badline/pet/superpet/board.rb +221 -0
  162. data/lib/badline/pet/superpet/dongle.rb +81 -0
  163. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  164. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  165. data/lib/badline/pet/superpet/m6809.rb +32 -0
  166. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  167. data/lib/badline/pet/superpet.rb +216 -0
  168. data/lib/badline/pet.rb +33 -6
  169. data/lib/badline/plus4/acia.rb +4 -298
  170. data/lib/badline/plus4/keyed_parts.rb +0 -44
  171. data/lib/badline/plus4/keyed_state.rb +2 -0
  172. data/lib/badline/plus4.rb +2 -0
  173. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  174. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  175. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  176. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  177. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  178. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  179. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  180. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  181. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  182. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  183. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  184. data/lib/badline/roms/README +277 -0
  185. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  186. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  187. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  188. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  189. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  190. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  191. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  192. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  193. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  194. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  195. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  196. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  197. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  198. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  199. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  200. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  201. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  202. data/lib/badline/snapshot/container.rb +9 -2
  203. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  204. data/lib/badline/snapshot/field_doc.rb +59 -3
  205. data/lib/badline/snapshot/image.rb +65 -9
  206. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  207. data/lib/badline/snapshot/pet_setup.rb +27 -11
  208. data/lib/badline/snapshot/storage_setup.rb +4 -1
  209. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  210. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  211. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  212. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  213. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  214. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  215. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  216. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  217. data/lib/badline/snapshot/vice/cias.rb +13 -9
  218. data/lib/badline/snapshot/vice/p500.rb +96 -0
  219. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  220. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  221. data/lib/badline/snapshot/vice/pet.rb +20 -10
  222. data/lib/badline/snapshot/vice/pet_cpu.rb +42 -5
  223. data/lib/badline/snapshot/vice/pet_crtc.rb +25 -17
  224. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  225. data/lib/badline/snapshot/vice/pet_drive8050.rb +336 -0
  226. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  227. data/lib/badline/snapshot/vice/pet_restore.rb +100 -7
  228. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  229. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  230. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  231. data/lib/badline/snapshot/vice.rb +3 -0
  232. data/lib/badline/snapshot.rb +15 -3
  233. data/lib/badline/status.rb +30 -30
  234. data/lib/badline/storage/crt_file.rb +7 -1
  235. data/lib/badline/storage/d64_image.rb +7 -0
  236. data/lib/badline/storage/d67_image.rb +55 -0
  237. data/lib/badline/storage/d80_image.rb +17 -1
  238. data/lib/badline/storage/d81_image.rb +14 -0
  239. data/lib/badline/storage/d90_image.rb +180 -0
  240. data/lib/badline/storage/disk_image/bam.rb +18 -3
  241. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  242. data/lib/badline/storage/disk_image/writing.rb +37 -2
  243. data/lib/badline/storage/disk_image.rb +2 -0
  244. data/lib/badline/storage/kim_tape.rb +254 -0
  245. data/lib/badline/storage/papertape.rb +90 -0
  246. data/lib/badline/storage.rb +5 -0
  247. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  248. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  249. data/lib/badline/support/p500_checkpoint.rb +35 -0
  250. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  251. data/lib/badline/version.rb +1 -1
  252. data/lib/badline/vic20/keyed_modules.rb +6 -3
  253. data/lib/badline/vic20/keyed_state.rb +6 -4
  254. data/lib/badline/vic20.rb +26 -12
  255. data/lib/badline.rb +2 -0
  256. metadata +148 -2
@@ -105,22 +105,37 @@ module Badline
105
105
  @controls.mouse_motion(SDL.event_xrel(SDL.event), SDL.event_yrel(SDL.event))
106
106
  when SDL::MOUSEBUTTONDOWN, SDL::MOUSEBUTTONUP
107
107
  @controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
108
+ click_panel(type == SDL::MOUSEBUTTONDOWN) if @computer.family == :kim1 && !@computer.tty?
108
109
  when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
109
110
  @gamepads.rescan
110
111
  when SDL::DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
111
112
  end
112
113
  end
113
114
 
115
+ # The window keeps F8, F9, F10, Tab and the snapshot keys for itself,
116
+ # but for the keys the machine claims (Controls#claims?).
114
117
  def handle_key(scancode, down)
115
- if [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
118
+ claimed = @controls.claims?(scancode)
119
+ if !claimed && toggle?(scancode)
116
120
  handle_toggle(scancode) if down
117
- elsif down && @snapshots.key(scancode)
121
+ elsif !claimed && down && @snapshots.key(scancode)
118
122
  swap(@snapshots.computer)
119
- elsif !Snapshots::KEYS.include?(scancode)
123
+ elsif claimed || !Snapshots::KEYS.include?(scancode)
120
124
  @controls.key(scancode, down)
121
125
  end
122
126
  end
123
127
 
128
+ # A click on the KIM-1's panel presses the key under the pointer, and
129
+ # its release lets go.
130
+ def click_panel(down)
131
+ event = SDL.event
132
+ @controls.panel_click(@screens.kim1_panel.key_at(SDL.event_x(event), SDL.event_y(event)), down)
133
+ end
134
+
135
+ def toggle?(scancode)
136
+ [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
137
+ end
138
+
124
139
  # Runs the restored machine in place of the one before: the screen,
125
140
  # the drive LED, the controls, the gamepads and the sound go over to
126
141
  # it.
@@ -10,7 +10,7 @@ module Badline
10
10
  # attach, an --at key names nothing or an event fails, which it warns
11
11
  # about.
12
12
  def self.run(options)
13
- error = Timeline.error(options.timeline, options.family)
13
+ error = Timeline.error(options.timeline, options.p500? ? :p500 : options.family)
14
14
  unless error.empty?
15
15
  warn "#{options.program}: #{error}"
16
16
  return 1
@@ -39,8 +39,9 @@ module Badline
39
39
  ""
40
40
  rescue Machine::Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Drives::Datasette::Missing,
41
41
  SystemCallError, SIDTune::File::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
42
+ Storage::KIMTape::FormatError,
42
43
  Storage::CRTFile::FormatError, Storage::G64Image::FormatError, C64::Cartridge::UnsupportedTypeError,
43
- Snapshot::FormatError => e
44
+ Storage::Papertape::FormatError, Snapshot::FormatError => e
44
45
  e.message
45
46
  end
46
47
 
@@ -83,7 +84,7 @@ module Badline
83
84
  options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
84
85
  unless media.nil?
85
86
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
86
- disk: { read_only: !writable })
87
+ disk: { read_only: !writable }, kim1: Media::KIM1Media.loading(options))
87
88
  end
88
89
  computer
89
90
  end
@@ -8,10 +8,13 @@ module Badline
8
8
  # switches, the arrow cluster and space drive one joystick and WASD
9
9
  # and left shift the other. The arrows start on joystick 2, and
10
10
  # #swap_ports swaps them. The VIC-20's one joystick takes both. The
11
- # PET has no joystick, and its keys go by the character they type
12
- # (PETKeyboard).
13
- # - A pot device the pause menu plugs into a C64 control port: a 1351
14
- # mouse or a pair of paddles, which the host mouse drives. Motion
11
+ # PET's and the CBM-II's keys go by the character they type
12
+ # (PETKeyboard), and the PET and the CBM-II's B series have no
13
+ # joystick. The KIM-1's host keys press its panel's (Keys::KIM1_KEYS),
14
+ # and so does a click on one the window draws.
15
+ # - A pot device the pause menu plugs into a control port of a C64, a
16
+ # C128 or a P500: a 1351 mouse or a pair of paddles, which the host
17
+ # mouse drives. Motion
15
18
  # turns the paddles or moves the mouse, and the host's left and right
16
19
  # buttons go to whichever lines the device puts them on.
17
20
  class Controls
@@ -24,6 +27,21 @@ module Badline
24
27
  # The pot device plugged in, one of POTS.
25
28
  attr_reader :pot, :arrows_port
26
29
 
30
+ # Whether the machine has joysticks: all but the PET and the CBM-II's
31
+ # B series.
32
+ def self.joysticks?(computer)
33
+ family = computer.family
34
+ family != :pet && family != :kim1 && (family != :cbm2 || p500?(computer))
35
+ end
36
+
37
+ # Whether the machine is the CBM-II's P500, which has the B series'
38
+ # keyboard and two joysticks.
39
+ def self.p500?(computer) = computer.is_a?(CBM2::P500)
40
+
41
+ # Whether the machine's keys go by the character they type: the PET's
42
+ # and the CBM-II's.
43
+ def self.symbolic?(computer) = %i[pet cbm2].include?(computer.family)
44
+
27
45
  def initialize(computer)
28
46
  @computer = computer
29
47
  @joystick = false
@@ -32,6 +50,8 @@ module Badline
32
50
  @mouse = Input::Mouse1351.new
33
51
  @paddles = Input::Paddles.new
34
52
  @held = {}
53
+ @clicked = ""
54
+ @shifts = 0
35
55
  @pet_keyboard = pet_keyboard(computer)
36
56
  end
37
57
 
@@ -58,9 +78,19 @@ module Badline
58
78
  self.joystick_mode = !@joystick if tab_toggles?
59
79
  end
60
80
 
61
- # Whether Tab switches the keys to the joysticks, or on the PET, which
62
- # has none, is a key of its own.
63
- def tab_toggles? = @computer.family != :pet
81
+ # Whether Tab switches the keys to the joysticks, or on a machine
82
+ # without them is a key of its own.
83
+ def tab_toggles? = Controls.joysticks?(@computer)
84
+
85
+ # Whether the machine takes a key the window would otherwise keep for
86
+ # itself: on the CBM-II, F8, RUN/STOP on F12, Alt with F9 or F10, and
87
+ # the release of any key it pressed.
88
+ def claims?(scancode)
89
+ return false unless @computer.family == :cbm2
90
+ return true if @pet_keyboard.holds?(scancode) || [Keys::F8, Keys::F12].include?(scancode)
91
+
92
+ @pet_keyboard.alt? && [Keys::F9, Keys::F10].include?(scancode)
93
+ end
64
94
 
65
95
  # Plugs in one of POTS, in place of the one before.
66
96
  def plug(pot)
@@ -100,6 +130,18 @@ module Badline
100
130
  paddles? ? @paddles.move(delta_x, delta_y) : @mouse.move(delta_x, delta_y)
101
131
  end
102
132
 
133
+ # Presses the KIM-1 key KIM1Panel names `name` for a click on it, or
134
+ # lets go of the key the last click pressed.
135
+ def panel_click(name, down)
136
+ if down
137
+ @clicked = name
138
+ kim1_key(name, true) unless name.empty?
139
+ else
140
+ kim1_key(@clicked, false) unless @clicked.empty?
141
+ @clicked = ""
142
+ end
143
+ end
144
+
103
145
  def mouse_button(button, down)
104
146
  name = MOUSE_BUTTONS[button]
105
147
  return unless name && pot_device?
@@ -116,7 +158,11 @@ module Badline
116
158
  # A host key lets go of the key it pressed, though the C128 has
117
159
  # switched between C64 and C128 mode since and maps it elsewhere.
118
160
  def machine_key(scancode, down)
119
- return @pet_keyboard.key(scancode, down) if @computer.family == :pet
161
+ if Controls.symbolic?(@computer)
162
+ @pet_keyboard.positional = @computer.family == :pet && @computer.apl_characters? if down
163
+ return @pet_keyboard.key(scancode, down)
164
+ end
165
+ return kim1_input(scancode, down) if @computer.family == :kim1
120
166
 
121
167
  @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
122
168
  key = @held[scancode]
@@ -154,12 +200,45 @@ module Badline
154
200
  end
155
201
  end
156
202
 
203
+ # The keys type on the teletype with the KIM-1's jumper fitted, and
204
+ # press the panel's keys without it.
205
+ def kim1_input(scancode, down)
206
+ return kim1_key(Keys.kim1_key(scancode), down) unless @computer.tty?
207
+ return shift(down) if [225, 229].include?(scancode)
208
+
209
+ text = Keys.kim1_teletype(scancode, @shifts.positive?)
210
+ @computer.teletype.type(text) if down && !text.empty?
211
+ end
212
+
213
+ def shift(down)
214
+ @shifts += down ? 1 : -1
215
+ @shifts = 0 if @shifts.negative?
216
+ end
217
+
218
+ # ST pulls NMI and RS resets as they go down, and T flips the SST
219
+ # switch. The other keys stay down until let go.
220
+ def kim1_key(name, down)
221
+ case name
222
+ when "" then nil
223
+ when "ST" then @computer.press_stop if down
224
+ when "RS" then @computer.press_reset if down
225
+ when "SST" then @computer.sst = !@computer.sst if down
226
+ else
227
+ if down
228
+ @computer.press_key(name)
229
+ elsif @computer.panel.held == name
230
+ @computer.release_key
231
+ end
232
+ end
233
+ end
234
+
157
235
  def paddles? = POTS.index(@pot) >= 3
158
236
 
159
237
  # A fresh pot device on its port, and none on the other. A machine
160
238
  # without the ports takes none.
161
239
  def attach_pot_device
162
- ports = @computer.family == :pet ? nil : @computer.control_ports
240
+ portless = Controls.symbolic?(@computer) || @computer.family == :kim1
241
+ ports = Controls.p500?(@computer) || !portless ? @computer.control_ports : nil
163
242
  if ports.nil?
164
243
  @pot = :none
165
244
  return
@@ -211,17 +290,30 @@ module Badline
211
290
  down ? joystick.press(direction) : joystick.release(direction)
212
291
  end
213
292
 
214
- # The PET's keyboard for a PET, and for any other machine one it never
215
- # uses.
293
+ # The PET's keyboard for a PET or a CBM-II, and for any other machine
294
+ # one it never uses, on a keyboard of its own for the KIM-1, which has
295
+ # none.
216
296
  def pet_keyboard(computer)
217
- PETKeyboard.new(computer.keyboard, computer.family == :pet ? computer.model.keyboard : :graphics)
297
+ layout = case computer.family
298
+ when :pet then computer.model.keyboard
299
+ when :cbm2 then :cbm2
300
+ else :graphics
301
+ end
302
+ keyboard = computer.family == :kim1 ? Input::Keyboard.new : computer.keyboard
303
+ PETKeyboard.new(keyboard, layout)
218
304
  end
219
305
 
220
306
  def release_all
221
- return @pet_keyboard.release_all if @computer.family == :pet
307
+ return @computer.release_key if @computer.family == :kim1
308
+
309
+ if Controls.symbolic?(@computer)
310
+ @pet_keyboard.release_all
311
+ else
312
+ Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
313
+ Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
314
+ end
315
+ return unless Controls.joysticks?(@computer)
222
316
 
223
- Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
224
- Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
225
317
  Keys::ARROWS.each_value do |direction|
226
318
  @computer.joystick1.release(direction)
227
319
  @computer.joystick2.release(direction)
@@ -7,8 +7,8 @@ module Badline
7
7
  # second one joystick 1. PadPort holds the mapping.
8
8
  #
9
9
  # The controllers are polled every frame, and rescanned whenever SDL
10
- # reports one added or removed. The PET has no joysticks, and the
11
- # controllers steer two that nothing reads.
10
+ # reports one added or removed. The PET and the CBM-II's B series have
11
+ # no joysticks, and the controllers steer two that nothing reads.
12
12
  class Gamepads
13
13
  PORTS = [2, 1].freeze
14
14
 
@@ -53,7 +53,7 @@ module Badline
53
53
  private
54
54
 
55
55
  def joystick(computer, port)
56
- return Input::Joystick.new if computer.family == :pet
56
+ return Input::Joystick.new unless Controls.joysticks?(computer)
57
57
 
58
58
  port == 1 ? computer.joystick1 : computer.joystick2
59
59
  end
@@ -3,8 +3,8 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # SDL scancodes follow key positions on a US layout. They map onto the C64
6
- # keys, the C128's and the 264's own keys and, in joystick mode, the
7
- # joystick directions.
6
+ # keys, the C128's, the 264's, the PET's and the CBM-II's own keys and,
7
+ # in joystick mode, the joystick directions.
8
8
  module Keys
9
9
  RETURN = 40
10
10
  ESCAPE = 41
@@ -118,6 +118,54 @@ module Badline
118
118
  # The cursor keys, CRSR ⇔ and CRSR ⇕, each with SHIFT for left or up.
119
119
  PET_CURSORS = { RIGHT => :cursor_h, LEFT => :cursor_h, DOWN => :cursor_v, UP => :cursor_v }.freeze
120
120
 
121
+ # The CBM-II's keys for the characters a host key types: those it
122
+ # types without SHIFT, then those it types with it, as the KERNAL's
123
+ # key tables place them. ← is on _ and `, and SHIFT turns it into \.
124
+ CBM2_CHARACTERS = {
125
+ "1" => :"1", "2" => :"2", "3" => :"3", "4" => :"4", "5" => :"5", "6" => :"6", "7" => :"7", "8" => :"8",
126
+ "9" => :"9", "0" => :"0", "-" => :-, "=" => :"=", "[" => :"[", "]" => :"]", ";" => :";", "'" => :"'",
127
+ "," => :",", "." => :".", "/" => :/, "_" => :left_arrow, "`" => :left_arrow
128
+ }.freeze
129
+
130
+ CBM2_SHIFTED = {
131
+ "!" => :"1", "@" => :"2", "#" => :"3", "$" => :"4", "%" => :"5", "^" => :"6", "&" => :"7", "*" => :"8",
132
+ "(" => :"9", ")" => :"0", "+" => :"=", ":" => :";", "\"" => :"'", "<" => :",", ">" => :".", "?" => :/,
133
+ "\\" => :left_arrow
134
+ }.freeze
135
+
136
+ # The CBM-II's keys that don't type a character, which keep the
137
+ # host's SHIFT: RETURN, INST/DEL on Backspace, CLR/HOME on Home, ESC,
138
+ # TAB, RUN/STOP on F12, CTRL on Control, the four cursor keys on the
139
+ # arrows, F1-F8 on F1-F8, and the keypad on the host's.
140
+ CBM2_KEYS = {
141
+ 40 => :return, 42 => :delete, 44 => :space, 74 => :clr_home, 41 => :escape, 43 => :tab, 69 => :run_stop,
142
+ 224 => :control, 228 => :control, 79 => :cursor_right, 80 => :cursor_left, 81 => :cursor_down,
143
+ 82 => :cursor_up, 58 => :f1, 59 => :f2, 60 => :f3, 61 => :f4, 62 => :f5, 63 => :f6, 64 => :f7, 65 => :f8,
144
+ 84 => :kp_divide, 85 => :kp_multiply, 86 => :kp_minus, 87 => :kp_plus, 88 => :kp_enter, 89 => :kp1,
145
+ 90 => :kp2, 91 => :kp3, 92 => :kp4, 93 => :kp5, 94 => :kp6, 95 => :kp7, 96 => :kp8, 97 => :kp9,
146
+ 98 => :kp0, 99 => :kp_period
147
+ }.freeze
148
+
149
+ # The CBM-II's keys that Alt and a host key press: C= on C, CE on E,
150
+ # NORM/GRAPH on G, OFF/RVS on R, π on P, 00 on 0, and F9 and F10 on
151
+ # F9 and F10, which open the pause menu and mute without Alt.
152
+ CBM2_ALT_KEYS = {
153
+ 6 => :cbm, 8 => :ce, 10 => :norm_graph, 21 => :off_rvs, 19 => :pi, 39 => :kp00, 66 => :f9, 67 => :f10
154
+ }.freeze
155
+
156
+ # The KIM-1's keys, by the names KIM1::Panel::KEYS gives them, with
157
+ # ST, RS and the SST switch: the hex keys on the digits, the keypad's
158
+ # and A-F, AD on comma, DA on period, + on space, = and the keypad's
159
+ # +, GO on G, Return and Enter, PC on P, ST on S and Esc, RS on R and
160
+ # SST on T.
161
+ KIM1_KEYS = {
162
+ 4 => "A", 5 => "B", 6 => "C", 7 => "D", 8 => "E", 9 => "F", 30 => "1", 31 => "2", 32 => "3", 33 => "4",
163
+ 34 => "5", 35 => "6", 36 => "7", 37 => "8", 38 => "9", 39 => "0", 89 => "1", 90 => "2", 91 => "3",
164
+ 92 => "4", 93 => "5", 94 => "6", 95 => "7", 96 => "8", 97 => "9", 98 => "0", 54 => "AD", 55 => "DA",
165
+ 44 => "+", 46 => "+", 87 => "+", 10 => "GO", 40 => "GO", 88 => "GO", 19 => "PC", 22 => "ST", 41 => "ST",
166
+ 21 => "RS", 23 => "SST"
167
+ }.freeze
168
+
121
169
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
122
170
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
123
171
 
@@ -177,6 +225,30 @@ module Badline
177
225
  shifted.nil? ? %i[none host] : [shifted, :on]
178
226
  end
179
227
 
228
+ # The CBM-II key for the scancode, with the host's SHIFT down if
229
+ # +shift+, and how the CBM-II's SHIFT goes with it, as pet_key gives
230
+ # them.
231
+ def self.cbm2_key(scancode, shift)
232
+ return [LETTERS[scancode - 4], :host] if scancode.between?(4, 29)
233
+
234
+ key = CBM2_KEYS[scancode]
235
+ return [key, :host] unless key.nil?
236
+
237
+ characters = US_CHARACTERS[scancode]
238
+ return %i[none host] if characters.nil?
239
+
240
+ character = characters[shift ? 1 : 0]
241
+ unshifted = CBM2_CHARACTERS[character]
242
+ return [unshifted, :off] unless unshifted.nil?
243
+
244
+ shifted = CBM2_SHIFTED[character]
245
+ shifted.nil? ? %i[none host] : [shifted, :on]
246
+ end
247
+
248
+ # The CBM-II key Alt and the scancode press, which keeps the host's
249
+ # SHIFT, or :none.
250
+ def self.cbm2_alt_key(scancode) = [CBM2_ALT_KEYS[scancode] || :none, :host]
251
+
180
252
  # The character the key types on a US layout: a letter, a capital
181
253
  # with `shift`, or a digit, and "" for any other key.
182
254
  def self.character(scancode, shift)
@@ -190,6 +262,29 @@ module Badline
190
262
  end
191
263
  end
192
264
 
265
+ # What the KIM-1's teletype sends for the keys that type no
266
+ # character: RETURN, RUBOUT on Backspace and Delete, and LINE FEED on
267
+ # Down.
268
+ KIM1_TELETYPE_KEYS = { 40 => "\r", 88 => "\r", 42 => "\x7f", 76 => "\x7f", 81 => "\n" }.freeze
269
+
270
+ # What the KIM-1's teletype sends for the scancode, with the host's
271
+ # SHIFT down if `shift`: a key of KIM1_TELETYPE_KEYS, or the
272
+ # character the key types on a US layout, or an empty string.
273
+ def self.kim1_teletype(scancode, shift)
274
+ sent = KIM1_TELETYPE_KEYS[scancode]
275
+ return sent unless sent.nil?
276
+ return " " if scancode == SPACE
277
+ return character(scancode, shift) if scancode.between?(4, 29)
278
+
279
+ characters = US_CHARACTERS[scancode]
280
+ return "" if characters.nil?
281
+
282
+ characters[shift ? 1 : 0]
283
+ end
284
+
285
+ # The KIM-1 key for the scancode, or an empty string.
286
+ def self.kim1_key(scancode) = KIM1_KEYS[scancode] || ""
287
+
193
288
  def self.arrows(scancode) = ARROWS[scancode] || :none
194
289
 
195
290
  def self.wasd(scancode) = WASD[scancode] || :none
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The KIM-1's panel as the window shows it, in place of a video chip's
6
+ # picture: the six LED digits, the keypad laid out as the board's
7
+ # (User Manual, figure 3.6), the SST switch, and the host keys that
8
+ # press each. It answers what a Screen reads of a video chip: a palette
9
+ # index a pixel, the lines changed, and the palette.
10
+ #
11
+ # Each frame #follow takes the cycles each segment was lit
12
+ # (KIM1::Panel#lit_cycles). A segment lit FULL_DUTY of the time, as
13
+ # the monitor lights each digit, shows at full brightness, and one lit
14
+ # less dimmer, through a square root, as the eye sees it. The level
15
+ # follows over SMOOTHING_CYCLES, the eye's persistence, so the frames'
16
+ # beat against the program's scan doesn't flicker.
17
+ class KIM1Panel
18
+ WIDTH = 384
19
+ HEIGHT = 272
20
+
21
+ BOARD = 0
22
+ LED = 1
23
+ LED_LEVELS = 16
24
+ WINDOW = 17
25
+ KEY = 18
26
+ KEY_HELD = 19
27
+ KEY_TEXT = 20
28
+ HINT = 21
29
+ TITLE = 22
30
+ SWITCH_ON = 23
31
+
32
+ # The board's green, the LED segments from unlit to full, the black
33
+ # behind them, the keys, and the text.
34
+ PALETTE = ([0x123c22] + Array.new(LED_LEVELS) do |level|
35
+ red = 0x2c + (((0xff - 0x2c) * level) / (LED_LEVELS - 1))
36
+ rest = 0x08 + ((0x28 * level) / (LED_LEVELS - 1))
37
+ (red << 16) | (rest << 8) | rest
38
+ end + [0x000000, 0xe8e0c8, 0x8c8470, 0x101010, 0xa8c8b0, 0xffffff, 0xffd040] +
39
+ Array.new(8, 0)).freeze
40
+
41
+ FULL_DUTY = 0.15
42
+ SMOOTHING_CYCLES = 40_000.0
43
+
44
+ # Where the digits go: the left edge of each, and their top.
45
+ DIGIT_LEFT = [56, 100, 144, 188, 252, 296].freeze
46
+ DIGIT_TOP = 30
47
+ THICKNESS = 4
48
+ LENGTH = 20
49
+
50
+ # Each segment's place in a digit, a to g: left, top, width, height.
51
+ SEGMENTS = [
52
+ [THICKNESS, 0, LENGTH, THICKNESS],
53
+ [THICKNESS + LENGTH, THICKNESS, THICKNESS, LENGTH],
54
+ [THICKNESS + LENGTH, (2 * THICKNESS) + LENGTH, THICKNESS, LENGTH],
55
+ [THICKNESS, (2 * THICKNESS) + (2 * LENGTH), LENGTH, THICKNESS],
56
+ [0, (2 * THICKNESS) + LENGTH, THICKNESS, LENGTH],
57
+ [0, THICKNESS, THICKNESS, LENGTH],
58
+ [THICKNESS, THICKNESS + LENGTH, LENGTH, THICKNESS]
59
+ ].freeze
60
+
61
+ # The keypad, top row first, as on the board, with the SST switch at
62
+ # the end of the first row.
63
+ KEYPAD = [%w[GO ST RS SST], %w[AD DA PC +], %w[C D E F], %w[8 9 A B], %w[4 5 6 7], %w[0 1 2 3]].freeze
64
+ KEYPAD_LEFT = 24
65
+ KEYPAD_TOP = 100
66
+ KEY_WIDTH = 40
67
+ KEY_HEIGHT = 22
68
+ KEY_GAP = 6
69
+
70
+ # The host keys that press the panel's.
71
+ HINTS = ["0-9, A-F: HEX", "COMMA: AD", "PERIOD: DA", "SPACE, +: +", "G, RETURN: GO", "P: PC",
72
+ "S, ESC: ST", "R: RS", "T: SST", "CLICK: KEY", "F9: MENU"].freeze
73
+ HINT_LEFT = 216
74
+ HINT_TOP = 104
75
+ HINT_LINE = 13
76
+
77
+ attr_reader :display, :dirty_lines
78
+
79
+ def initialize
80
+ @display = Array.new(WIDTH * HEIGHT, BOARD)
81
+ @dirty_lines = Array.new(HEIGHT, true)
82
+ @font = ROM.read("character.rom")
83
+ @brightness = Array.new(KIM1::Panel::DIGITS * 7, 0.0)
84
+ @levels = Array.new(KIM1::Panel::DIGITS * 7, -1)
85
+ @held = ""
86
+ @sst = false
87
+ @cycles = 0
88
+ draw_board
89
+ end
90
+
91
+ def width = WIDTH
92
+
93
+ def height = HEIGHT
94
+
95
+ def palette = PALETTE
96
+
97
+ # The whole picture: the window has nothing to crop.
98
+ def crop = [0, 0, WIDTH, HEIGHT]
99
+
100
+ def clear_dirty_lines!
101
+ @dirty_lines.map! { false }
102
+ end
103
+
104
+ # Brings the picture up to the KIM-1 `computer`: the digits from the
105
+ # segments' lit cycles since the last frame, and the key held and
106
+ # the SST switch.
107
+ def follow(computer)
108
+ cycles = computer.cycles
109
+ elapsed = cycles - @cycles
110
+ @cycles = cycles
111
+ lit = computer.panel.lit_cycles
112
+ light(lit, elapsed) if elapsed.positive?
113
+ held = computer.panel.held.to_s
114
+ draw_keys(held, computer.sst) if held != @held || computer.sst != @sst
115
+ end
116
+
117
+ # The name of the panel's key at the window pixel `left`, `top`, as
118
+ # KIM1::Panel::KEYS names it, ST, RS or SST, or an empty string.
119
+ def key_at(left, top)
120
+ across = left - KEYPAD_LEFT
121
+ down = top - KEYPAD_TOP
122
+ column = across / (KEY_WIDTH + KEY_GAP)
123
+ row = down / (KEY_HEIGHT + KEY_GAP)
124
+ return "" if across.negative? || down.negative? || column > 3 || row >= KEYPAD.size
125
+ return "" if across % (KEY_WIDTH + KEY_GAP) >= KEY_WIDTH || down % (KEY_HEIGHT + KEY_GAP) >= KEY_HEIGHT
126
+
127
+ KEYPAD[row][column]
128
+ end
129
+
130
+ # The brightness, 0 to 1, of each segment, seven to a digit.
131
+ def brightness = @brightness.dup
132
+
133
+ private
134
+
135
+ def light(lit, elapsed)
136
+ smoothing = 1.0 - Math.exp(-elapsed / SMOOTHING_CYCLES)
137
+ lit.each_with_index do |cycles, index|
138
+ duty = cycles.fdiv(elapsed) / FULL_DUTY
139
+ target = Math.sqrt([duty, 1.0].min)
140
+ level = @brightness[index] + (smoothing * (target - @brightness[index]))
141
+ @brightness[index] = level
142
+ shade = (level * (LED_LEVELS - 1)).round
143
+ next if shade == @levels[index]
144
+
145
+ @levels[index] = shade
146
+ draw_segment(index / 7, index % 7, LED + shade)
147
+ end
148
+ end
149
+
150
+ def draw_board
151
+ text(16, 8, "MOS KIM-1", TITLE)
152
+ box(40, 22, 304, 64, WINDOW)
153
+ DIGIT_LEFT.each_index { |digit| 7.times { |segment| draw_segment(digit, segment, LED) } }
154
+ @levels.map! { 0 }
155
+ HINTS.each_with_index { |hint, line| text(HINT_LEFT, HINT_TOP + (line * HINT_LINE), hint, HINT) }
156
+ draw_keys("", false)
157
+ end
158
+
159
+ def draw_segment(digit, segment, color)
160
+ place = SEGMENTS[segment]
161
+ box(DIGIT_LEFT[digit] + place[0], DIGIT_TOP + place[1], place[2], place[3], color)
162
+ end
163
+
164
+ def draw_keys(held, sst)
165
+ @held = held
166
+ @sst = sst
167
+ KEYPAD.each_with_index do |names, row|
168
+ names.each_with_index do |name, column|
169
+ on = name == "SST" ? sst : name == held
170
+ draw_key(KEYPAD_LEFT + (column * (KEY_WIDTH + KEY_GAP)), KEYPAD_TOP + (row * (KEY_HEIGHT + KEY_GAP)),
171
+ name, on)
172
+ end
173
+ end
174
+ end
175
+
176
+ def draw_key(left, top, name, on)
177
+ face = on ? KEY_HELD : KEY
178
+ face = on ? SWITCH_ON : KEY if name == "SST"
179
+ box(left, top, KEY_WIDTH, KEY_HEIGHT, face)
180
+ text(left + ((KEY_WIDTH - (name.size * 8)) / 2), top + ((KEY_HEIGHT - 8) / 2), name, KEY_TEXT)
181
+ end
182
+
183
+ def box(left, top, width, height, color)
184
+ height.times do |row|
185
+ line = top + row
186
+ @display.fill(color, (line * WIDTH) + left, width)
187
+ @dirty_lines[line] = true
188
+ end
189
+ end
190
+
191
+ # Draws `string` from `left`, `top` in the character ROM's second set.
192
+ def text(left, top, string, color)
193
+ string.bytes.each_with_index do |byte, index|
194
+ glyph(left + (index * 8), top, Painter::SCREEN_CODES[byte & 0x7f], color)
195
+ end
196
+ end
197
+
198
+ def glyph(left, top, code, color)
199
+ 8.times do |row|
200
+ bits = @font[0x800 + (code * 8) + row]
201
+ line = top + row
202
+ 8.times { |dot| @display[(line * WIDTH) + left + dot] = color if bits.anybits?(0x80 >> dot) }
203
+ @dirty_lines[line] = true
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -26,6 +26,13 @@ module Badline
26
26
  path.empty? ? path : File.expand_path(path)
27
27
  end
28
28
 
29
+ # The model of the true drive on device 8, or an empty one for the
30
+ # traps.
31
+ def drive_model(computer)
32
+ drive = Media::TrueDrive.drive(computer)
33
+ drive.nil? ? "" : drive.model_name
34
+ end
35
+
29
36
  def eject_disk(computer)
30
37
  drive = Media::TrueDrive.drive(computer)
31
38
  drive.nil? ? computer.unmount : drive.insert(nil)
@@ -35,7 +42,7 @@ module Badline
35
42
  # either cartridge's slots.
36
43
  def cartridge?(computer)
37
44
  return !computer.cartridge_slots.empty? if computer.family == :plus4
38
- return false if computer.family == :pet
45
+ return false if %i[pet cbm2 kim1].include?(computer.family)
39
46
 
40
47
  !computer.address_bus.cartridge.nil?
41
48
  end