badline 0.16.0 → 0.17.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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +130 -17
  5. data/doc/media.md +14 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +379 -70
  8. data/doc/snapshots.md +185 -10
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/ieee_bus.rb +50 -4
  46. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  47. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  48. data/lib/badline/drives/model2031/parts.rb +25 -0
  49. data/lib/badline/drives/model2031.rb +92 -0
  50. data/lib/badline/drives/model8050/disk.rb +163 -0
  51. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  52. data/lib/badline/drives/model8050/idle.rb +177 -0
  53. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  54. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  55. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  56. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  57. data/lib/badline/drives/model8050/parts.rb +37 -0
  58. data/lib/badline/drives/model8050.rb +243 -0
  59. data/lib/badline/frontend/app.rb +10 -3
  60. data/lib/badline/frontend/boot.rb +1 -1
  61. data/lib/badline/frontend/controls.rb +51 -15
  62. data/lib/badline/frontend/gamepads.rb +3 -3
  63. data/lib/badline/frontend/keys.rb +61 -2
  64. data/lib/badline/frontend/media_slots.rb +1 -1
  65. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  66. data/lib/badline/frontend/menu/menu_pages.rb +29 -6
  67. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  68. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  69. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  70. data/lib/badline/frontend/timeline.rb +19 -10
  71. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  72. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  73. data/lib/badline/kernal_trap/dos.rb +18 -5
  74. data/lib/badline/kernal_trap/ieee.rb +7 -2
  75. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  76. data/lib/badline/machine/ieee_drives.rb +194 -0
  77. data/lib/badline/machine/settling.rb +4 -0
  78. data/lib/badline/machine.rb +26 -8
  79. data/lib/badline/media/cbm2_media.rb +29 -0
  80. data/lib/badline/media/pet_media.rb +5 -5
  81. data/lib/badline/media/program.rb +6 -3
  82. data/lib/badline/media/true_drive.rb +50 -8
  83. data/lib/badline/media.rb +21 -8
  84. data/lib/badline/options/family.rb +15 -3
  85. data/lib/badline/options/help.rb +24 -6
  86. data/lib/badline/options/table.rb +25 -5
  87. data/lib/badline/options/validation.rb +47 -7
  88. data/lib/badline/options.rb +48 -15
  89. data/lib/badline/pet/bus/expansion.rb +99 -5
  90. data/lib/badline/pet/bus.rb +30 -12
  91. data/lib/badline/pet/cassettes.rb +22 -1
  92. data/lib/badline/pet/characters.rb +8 -2
  93. data/lib/badline/pet/crtc_video.rb +20 -7
  94. data/lib/badline/pet/ieee_drive.rb +17 -0
  95. data/lib/badline/pet/ieee_ports.rb +19 -1
  96. data/lib/badline/pet/ieee_traps.rb +10 -5
  97. data/lib/badline/pet/keyed_modules.rb +4 -1
  98. data/lib/badline/pet/keyed_parts.rb +10 -6
  99. data/lib/badline/pet/model.rb +56 -6
  100. data/lib/badline/pet/rom_sets.rb +30 -6
  101. data/lib/badline/pet.rb +45 -16
  102. data/lib/badline/plus4/acia.rb +4 -298
  103. data/lib/badline/plus4/keyed_parts.rb +0 -44
  104. data/lib/badline/plus4.rb +1 -0
  105. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  106. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  107. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  108. data/lib/badline/roms/README +144 -1
  109. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  110. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  111. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  112. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  113. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  114. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  115. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  116. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  117. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  118. data/lib/badline/roms/dos2031.rom +0 -0
  119. data/lib/badline/roms/pet/characters-din.rom +0 -0
  120. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  121. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  122. data/lib/badline/snapshot/container.rb +9 -1
  123. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  124. data/lib/badline/snapshot/field_doc.rb +39 -3
  125. data/lib/badline/snapshot/image.rb +79 -15
  126. data/lib/badline/snapshot/pet_setup.rb +30 -13
  127. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  128. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  129. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  130. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  131. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  132. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  133. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  134. data/lib/badline/snapshot/vice/cias.rb +13 -9
  135. data/lib/badline/snapshot/vice/p500.rb +96 -0
  136. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  137. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  138. data/lib/badline/snapshot/vice/pet.rb +121 -0
  139. data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
  140. data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
  141. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  142. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  143. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  144. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  145. data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
  146. data/lib/badline/snapshot/vice/pet_via.rb +165 -0
  147. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  148. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  149. data/lib/badline/snapshot/vice.rb +4 -0
  150. data/lib/badline/snapshot.rb +11 -3
  151. data/lib/badline/storage/crt_file.rb +7 -1
  152. data/lib/badline/storage/d64_image.rb +7 -0
  153. data/lib/badline/storage/d80_image.rb +17 -1
  154. data/lib/badline/storage/d81_image.rb +14 -0
  155. data/lib/badline/storage/disk_image/bam.rb +18 -3
  156. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  157. data/lib/badline/storage/disk_image/writing.rb +35 -0
  158. data/lib/badline/storage/disk_image.rb +2 -0
  159. data/lib/badline/storage.rb +1 -0
  160. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  161. data/lib/badline/support/p500_checkpoint.rb +35 -0
  162. data/lib/badline/version.rb +1 -1
  163. data/lib/badline.rb +1 -0
  164. metadata +87 -2
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/riot"
4
+ require "badline/chips/rriot"
5
+ require "badline/drives/ieee_bus"
6
+ require "badline/drives/engine"
7
+ require "badline/drives/model8050/ip_bus"
8
+ require "badline/drives/model8050/fdc_bus"
9
+ require "badline/drives/model8050/parts"
10
+ require "badline/drives/model8050/ieee_port"
11
+ require "badline/drives/model8050/disk"
12
+ require "badline/drives/model8050/mechanism"
13
+ require "badline/drives/model8050/idle"
14
+ require "badline/drives/model8050/keyed_state"
15
+
16
+ module Badline
17
+ module Drives
18
+ # The 8050 dual disk drive of the PET and CBM-II, two drives on one
19
+ # IEEE-488 unit (schematic 8050001; the 8050/8250 service manual).
20
+ #
21
+ # Two 6502s share the board, clocked at 1 MHz from one 12 MHz crystal
22
+ # on opposite phases. The interface processor (IP) runs DOS 2.7 from
23
+ # 16 KB of ROM at $C000, with two 6532s (IPBus): their RAM is its zero
24
+ # page and stack, and their ports face the IEEE-488 bus (IEEEPort). The
25
+ # floppy controller (FDC) runs the 1 KB program in its 6530's ROM, with
26
+ # a VIA and the 6530 facing the drives (FDCBus, Mechanism). The two
27
+ # meet in 4 KB of common RAM, which each reaches in its own half of
28
+ # the cycle: the IP leaves jobs there, and the FDC runs them.
29
+ #
30
+ # Reset starts both. The FDC waits on the common RAM for the IP, which
31
+ # tests it, copies the FDC's start-up code into it ($C370) and waits for
32
+ # the FDC to answer, before both settle into their idle loops.
33
+ #
34
+ # The drive clocks against the host through a phase accumulator, as
35
+ # Drives::Engine::Core does: each host cycle adds 1 MHz of phase, and
36
+ # every whole host period in it runs a cycle of both processors. While
37
+ # both processors idle, it sleeps instead (Idle), and the readers of its
38
+ # parts catch it up first.
39
+ class Model8050
40
+ include Idle
41
+
42
+ CLOCK_HZ = 1_000_000
43
+
44
+ # The host cycles by which a unit switched on with the machine has
45
+ # run the IP's RAM test and the FDC's start-up and reads its jumpers.
46
+ BOOT_CYCLES = 1_500_000
47
+
48
+ attr_reader :device, :ieee_bus
49
+
50
+ def ip_cpu = settled(@ip_cpu)
51
+
52
+ def fdc_cpu = settled(@fdc_cpu)
53
+
54
+ def ip_bus = settled(@ip_bus)
55
+
56
+ def fdc_bus = settled(@fdc_bus)
57
+
58
+ def common = settled(@common)
59
+
60
+ def data_riot = settled(@data_riot)
61
+
62
+ def control_riot = settled(@control_riot)
63
+
64
+ def via = settled(@via)
65
+
66
+ def rriot = settled(@rriot)
67
+
68
+ def mechanism = settled(@mechanism)
69
+
70
+ def cycles
71
+ settle!
72
+ @cycles
73
+ end
74
+
75
+ attr_writer :host_clock_hz
76
+
77
+ def model_name = "8050"
78
+
79
+ # The sides each drive's disk has.
80
+ def sides = 1
81
+
82
+ # +dos_rom+ covers $C000-$FFFF and +fdc_rom+ is the 6530's mask, each
83
+ # defaulting to the image in the ROM path: DOS 2.7 and the mask for
84
+ # MPI mechanisms. +device+ is the number the jumpers set, 8 to 15.
85
+ def initialize(dos_rom: nil, fdc_rom: nil, host_clock_hz: CLOCK_HZ, device: 8)
86
+ @device = device
87
+ @host_clock_hz = host_clock_hz
88
+ @phase = 0
89
+ @cycles = 0
90
+ build_ip(dos_rom || ROM.load("8050/dos-2.7.rom", 0xc000))
91
+ build_fdc(fdc_rom || ROM.read("8050/fdc-901869-01.rom"))
92
+ @ip_cpu = IPCPU.new(@ip_bus)
93
+ @fdc_cpu = FDCCPU.new(@fdc_bus)
94
+ init_idle
95
+ @ieee_bus.attach(self)
96
+ end
97
+
98
+ # Plugs the unit into an IEEE-488 bus, leaving the one it was on.
99
+ def connect(ieee_bus)
100
+ settle!
101
+ @ieee_bus.detach(self)
102
+ @ieee_bus = ieee_bus
103
+ @ieee_port.bus = ieee_bus
104
+ ieee_bus.attach(self)
105
+ end
106
+
107
+ # The lines the unit pulls on the bus while ATN is low or not
108
+ # (IEEEPort).
109
+ def ieee_low_lines(atn) = @ieee_port.ieee_low_lines(atn)
110
+
111
+ # The controller pushed its lines into the bus.
112
+ def host_written! = nil
113
+
114
+ # Which LEDs are lit: bit 0 drive 1's, bit 1 drive 0's, bit 2 the
115
+ # error LED. They hold still while the unit sleeps, so reading them
116
+ # leaves it asleep.
117
+ def leds = @ieee_port.leds
118
+
119
+ def led_on? = leds.anybits?(0x03)
120
+
121
+ # Puts a Disk in +drive+, 0 or 1. Nil takes the disk out.
122
+ def insert(disk, drive: 0)
123
+ settle!
124
+ @mechanism.insert(disk, drive)
125
+ end
126
+
127
+ # Puts in the .d80 or .d82 image at +path+, write-protected with
128
+ # `read_only`.
129
+ def insert_image(path, drive: 0, read_only: false)
130
+ image_class = File.extname(path).casecmp?(".d82") ? Storage::D82Image : Storage::D80Image
131
+ insert(Disk.new(image_class.new(path, read_only:)), drive:)
132
+ end
133
+
134
+ # The Disk in +drive+, or nil.
135
+ def disk(drive = 0) = @mechanism.disk(drive)
136
+
137
+ # Stores what the heads wrote in the disks' images, as the motors
138
+ # stopping do.
139
+ def flush
140
+ settle!
141
+ @mechanism.flush
142
+ end
143
+
144
+ def power_led_on? = true
145
+
146
+ # Runs the drive cycles that fall in one host cycle, then takes the
147
+ # controller's lines for the next. Asleep, the unit owes the cycles
148
+ # instead, and wakes once the controller has moved a line.
149
+ def host_cycle!
150
+ phase = @phase + CLOCK_HZ
151
+ while phase >= @host_clock_hz
152
+ @asleep ? owe_cycle : run_cycle
153
+ phase -= @host_clock_hz
154
+ end
155
+ @phase = phase
156
+ settle! if @asleep && @ieee_port.lines_moved?
157
+ @ieee_port.latch_host
158
+ end
159
+
160
+ def cycle!
161
+ settle!
162
+ run_cycle
163
+ end
164
+
165
+ # The bus's IFC line resets both processors and every chip. RAM keeps
166
+ # its contents.
167
+ def reset!
168
+ settle!
169
+ @data_riot.reset!
170
+ @control_riot.reset!
171
+ @via.reset!
172
+ @rriot.reset!
173
+ @ip_cpu.reset!
174
+ @fdc_cpu.reset!
175
+ end
176
+
177
+ def inspect
178
+ "#<#{self.class.name} cycles=#{@cycles} ip=(#{@ip_cpu.inspect}) fdc=(#{@fdc_cpu.inspect})>"
179
+ end
180
+
181
+ private
182
+
183
+ def settled(part)
184
+ settle!
185
+ part
186
+ end
187
+
188
+ def run_cycle
189
+ step
190
+ watch_loops if @idle_skip
191
+ end
192
+
193
+ def build_ip(rom)
194
+ @common = Memory.new([], length: 0x1000)
195
+ @ieee_bus = IEEEBus.new
196
+ @ieee_port = IEEEPort.new(device: @device, bus: @ieee_bus)
197
+ @data_riot = DataRIOT.new(peripheral: DataLines.new(@ieee_port))
198
+ @control_riot = ControlRIOT.new(peripheral: ControlLines.new(@ieee_port))
199
+ @ieee_port.data_riot = @data_riot
200
+ @ieee_port.control_riot = @control_riot
201
+ @ip_bus = IPBus.new(rom:, common: @common, data_riot: @data_riot, control_riot: @control_riot)
202
+ end
203
+
204
+ def build_fdc(rom)
205
+ @mechanism = Mechanism.new(sides:)
206
+ @via = ControllerVIA.new(start: 0x0040, peripheral: ReadLines.new(@mechanism))
207
+ @rriot = ControllerRRIOT.new(rom:, peripheral: StatusLines.new(@mechanism))
208
+ @fdc_bus = FDCBus.new(common: @common, via: @via, rriot: @rriot)
209
+ @mechanism.via = @via
210
+ @mechanism.rriot = @rriot
211
+ end
212
+
213
+ # One cycle of the board: the drives, then the chips, then the IP and
214
+ # the FDC, each in its own half of the cycle. ATN reaches UE1's PA7 as
215
+ # the port latched it. UE1 pulls the IP's IRQ, and the VIA and the
216
+ # 6530 the FDC's. Byte ready sets the FDC's V flag through SO for the
217
+ # next cycle's instruction step, as on the 1541.
218
+ def step
219
+ so = @mechanism.take_so
220
+ @mechanism.cycle!
221
+ @control_riot.pa7 = @ieee_port.atn_low?
222
+ @data_riot.cycle!
223
+ @control_riot.cycle!
224
+ @via.cycle!
225
+ @rriot.cycle!
226
+ @ip_cpu.irq = @control_riot.irq?
227
+ @ip_cpu.cycle!
228
+ @fdc_cpu.irq = @via.irq? || @rriot.irq?
229
+ @fdc_cpu.so! if so
230
+ @fdc_cpu.cycle!
231
+ @cycles += 1
232
+ end
233
+ end
234
+
235
+ # The 8250: the 8050's board driving double-sided mechanisms, 154 tracks
236
+ # a disk, with jumper E3 grounding the 6530's PB6 to say so.
237
+ class Model8250 < Model8050
238
+ def model_name = "8250"
239
+
240
+ def sides = 2
241
+ end
242
+ end
243
+ end
@@ -111,16 +111,23 @@ module Badline
111
111
  end
112
112
  end
113
113
 
114
+ # The window keeps F8, F9, F10, Tab and the snapshot keys for itself,
115
+ # but for the keys the machine claims (Controls#claims?).
114
116
  def handle_key(scancode, down)
115
- if [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
117
+ claimed = @controls.claims?(scancode)
118
+ if !claimed && toggle?(scancode)
116
119
  handle_toggle(scancode) if down
117
- elsif down && @snapshots.key(scancode)
120
+ elsif !claimed && down && @snapshots.key(scancode)
118
121
  swap(@snapshots.computer)
119
- elsif !Snapshots::KEYS.include?(scancode)
122
+ elsif claimed || !Snapshots::KEYS.include?(scancode)
120
123
  @controls.key(scancode, down)
121
124
  end
122
125
  end
123
126
 
127
+ def toggle?(scancode)
128
+ [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
129
+ end
130
+
124
131
  # Runs the restored machine in place of the one before: the screen,
125
132
  # the drive LED, the controls, the gamepads and the sound go over to
126
133
  # 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
@@ -8,10 +8,12 @@ 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.
14
+ # - A pot device the pause menu plugs into a control port of a C64, a
15
+ # C128 or a P500: a 1351 mouse or a pair of paddles, which the host
16
+ # mouse drives. Motion
15
17
  # turns the paddles or moves the mouse, and the host's left and right
16
18
  # buttons go to whichever lines the device puts them on.
17
19
  class Controls
@@ -24,6 +26,21 @@ module Badline
24
26
  # The pot device plugged in, one of POTS.
25
27
  attr_reader :pot, :arrows_port
26
28
 
29
+ # Whether the machine has joysticks: all but the PET and the CBM-II's
30
+ # B series.
31
+ def self.joysticks?(computer)
32
+ family = computer.family
33
+ family != :pet && (family != :cbm2 || p500?(computer))
34
+ end
35
+
36
+ # Whether the machine is the CBM-II's P500, which has the B series'
37
+ # keyboard and two joysticks.
38
+ def self.p500?(computer) = computer.is_a?(CBM2::P500)
39
+
40
+ # Whether the machine's keys go by the character they type: the PET's
41
+ # and the CBM-II's.
42
+ def self.symbolic?(computer) = %i[pet cbm2].include?(computer.family)
43
+
27
44
  def initialize(computer)
28
45
  @computer = computer
29
46
  @joystick = false
@@ -58,9 +75,19 @@ module Badline
58
75
  self.joystick_mode = !@joystick if tab_toggles?
59
76
  end
60
77
 
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
78
+ # Whether Tab switches the keys to the joysticks, or on a machine
79
+ # without them is a key of its own.
80
+ def tab_toggles? = Controls.joysticks?(@computer)
81
+
82
+ # Whether the machine takes a key the window would otherwise keep for
83
+ # itself: on the CBM-II, F8, RUN/STOP on F12, Alt with F9 or F10, and
84
+ # the release of any key it pressed.
85
+ def claims?(scancode)
86
+ return false unless @computer.family == :cbm2
87
+ return true if @pet_keyboard.holds?(scancode) || [Keys::F8, Keys::F12].include?(scancode)
88
+
89
+ @pet_keyboard.alt? && [Keys::F9, Keys::F10].include?(scancode)
90
+ end
64
91
 
65
92
  # Plugs in one of POTS, in place of the one before.
66
93
  def plug(pot)
@@ -116,7 +143,7 @@ module Badline
116
143
  # A host key lets go of the key it pressed, though the C128 has
117
144
  # switched between C64 and C128 mode since and maps it elsewhere.
118
145
  def machine_key(scancode, down)
119
- return @pet_keyboard.key(scancode, down) if @computer.family == :pet
146
+ return @pet_keyboard.key(scancode, down) if Controls.symbolic?(@computer)
120
147
 
121
148
  @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
122
149
  key = @held[scancode]
@@ -159,7 +186,7 @@ module Badline
159
186
  # A fresh pot device on its port, and none on the other. A machine
160
187
  # without the ports takes none.
161
188
  def attach_pot_device
162
- ports = @computer.family == :pet ? nil : @computer.control_ports
189
+ ports = Controls.p500?(@computer) || !Controls.symbolic?(@computer) ? @computer.control_ports : nil
163
190
  if ports.nil?
164
191
  @pot = :none
165
192
  return
@@ -211,17 +238,26 @@ module Badline
211
238
  down ? joystick.press(direction) : joystick.release(direction)
212
239
  end
213
240
 
214
- # The PET's keyboard for a PET, and for any other machine one it never
215
- # uses.
241
+ # The PET's keyboard for a PET or a CBM-II, and for any other machine
242
+ # one it never uses.
216
243
  def pet_keyboard(computer)
217
- PETKeyboard.new(computer.keyboard, computer.family == :pet ? computer.model.keyboard : :graphics)
244
+ layout = case computer.family
245
+ when :pet then computer.model.keyboard
246
+ when :cbm2 then :cbm2
247
+ else :graphics
248
+ end
249
+ PETKeyboard.new(computer.keyboard, layout)
218
250
  end
219
251
 
220
252
  def release_all
221
- return @pet_keyboard.release_all if @computer.family == :pet
253
+ if Controls.symbolic?(@computer)
254
+ @pet_keyboard.release_all
255
+ else
256
+ Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
257
+ Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
258
+ end
259
+ return unless Controls.joysticks?(@computer)
222
260
 
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
261
  Keys::ARROWS.each_value do |direction|
226
262
  @computer.joystick1.release(direction)
227
263
  @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,41 @@ 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
+
121
156
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
122
157
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
123
158
 
@@ -177,6 +212,30 @@ module Badline
177
212
  shifted.nil? ? %i[none host] : [shifted, :on]
178
213
  end
179
214
 
215
+ # The CBM-II key for the scancode, with the host's SHIFT down if
216
+ # +shift+, and how the CBM-II's SHIFT goes with it, as pet_key gives
217
+ # them.
218
+ def self.cbm2_key(scancode, shift)
219
+ return [LETTERS[scancode - 4], :host] if scancode.between?(4, 29)
220
+
221
+ key = CBM2_KEYS[scancode]
222
+ return [key, :host] unless key.nil?
223
+
224
+ characters = US_CHARACTERS[scancode]
225
+ return %i[none host] if characters.nil?
226
+
227
+ character = characters[shift ? 1 : 0]
228
+ unshifted = CBM2_CHARACTERS[character]
229
+ return [unshifted, :off] unless unshifted.nil?
230
+
231
+ shifted = CBM2_SHIFTED[character]
232
+ shifted.nil? ? %i[none host] : [shifted, :on]
233
+ end
234
+
235
+ # The CBM-II key Alt and the scancode press, which keeps the host's
236
+ # SHIFT, or :none.
237
+ def self.cbm2_alt_key(scancode) = [CBM2_ALT_KEYS[scancode] || :none, :host]
238
+
180
239
  # The character the key types on a US layout: a letter, a capital
181
240
  # with `shift`, or a digit, and "" for any other key.
182
241
  def self.character(scancode, shift)
@@ -35,7 +35,7 @@ module Badline
35
35
  # either cartridge's slots.
36
36
  def cartridge?(computer)
37
37
  return !computer.cartridge_slots.empty? if computer.family == :plus4
38
- return false if computer.family == :pet
38
+ return false if %i[pet cbm2].include?(computer.family)
39
39
 
40
40
  !computer.address_bus.cartridge.nil?
41
41
  end
@@ -50,6 +50,8 @@ module Badline
50
50
  Media.insert_disk(@computer, disk, read_only: !@writable)
51
51
  list(path)
52
52
  else
53
+ raise Machine::Unsupported, "the CBM-II has no datasette" if @computer.family == :cbm2
54
+
53
55
  @computer.datasette.insert(Storage::TAP.new(path))
54
56
  end
55
57
  end
@@ -100,17 +102,18 @@ module Badline
100
102
  end
101
103
 
102
104
  # The extensions of the cartridges the machine takes: .crt, on the
103
- # Plus/4 raw .bin ROM images too, and none on the PET.
105
+ # Plus/4 raw .bin ROM images too, and none on the PET, or on the
106
+ # CBM-II, whose cartridge goes in from the command line.
104
107
  def cartridge_extensions
105
- extensions = Media::Extensions.of(@computer.family == :pet ? %i[none] : %i[cartridge])
108
+ extensions = Media::Extensions.of(%i[pet cbm2].include?(@computer.family) ? %i[none] : %i[cartridge])
106
109
  @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
107
110
  end
108
111
 
109
- # The extensions of what goes in device 8: on the PET the 8050's and
110
- # 8250's disk images besides the others, and no raw GCR, which only
111
- # a serial true drive reads.
112
+ # The extensions of what goes in device 8: on the PET and the CBM-II
113
+ # the 8050's and 8250's disk images besides the others, and no raw
114
+ # GCR, which only a serial true drive reads.
112
115
  def disk_extensions
113
- return Media::Extensions.of(%i[disk ieee_disk archive disk_list]) if @computer.family == :pet
116
+ return Media::Extensions.of(%i[disk ieee_disk archive disk_list]) if %i[pet cbm2].include?(@computer.family)
114
117
 
115
118
  Media::Extensions::DEVICE8
116
119
  end
@@ -125,14 +128,14 @@ module Badline
125
128
 
126
129
  # What a save of the machine is named after: the cartridge, the disk,
127
130
  # the tape, or BASIC, without the bracketed parts of a file's name. A
128
- # VIC-20's are named after its disk, or else the machine, and a
129
- # Plus/4's or a PET's after its disk or tape.
131
+ # VIC-20's are named after its disk, or else the machine, a Plus/4's
132
+ # or a PET's after its disk or tape, and a CBM-II's after its disk.
130
133
  def game_name
131
134
  vic20 = @computer.family == :vic20
132
- name = vic20 || %i[plus4 pet].include?(@computer.family) ? "" : cartridge_name
135
+ name = vic20 || %i[plus4 pet cbm2].include?(@computer.family) ? "" : cartridge_name
133
136
  disk = disk_path
134
137
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
135
- name = tape_name if name.empty? && !vic20
138
+ name = tape_name if name.empty? && !vic20 && @computer.family != :cbm2
136
139
  name = name.sub(/\s*[(\[].*\z/, "").strip
137
140
  return name unless name.empty?
138
141
 
@@ -154,7 +157,7 @@ module Badline
154
157
  case @computer.family
155
158
  when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
156
159
  when :vic20 then @computer.cartridge_name
157
- when :pet then ""
160
+ when :pet, :cbm2 then ""
158
161
  else
159
162
  cartridge = @computer.address_bus.cartridge
160
163
  cartridge.nil? ? "" : cartridge.name
@@ -88,6 +88,14 @@ module Badline
88
88
  # Whether the machine is a PET, whose ports page shows its keyboard.
89
89
  def pet? = @computer.family == :pet
90
90
 
91
+ # Whether the machine is a CBM-II, whose ports page shows its keyboard
92
+ # too.
93
+ def cbm2? = @computer.family == :cbm2
94
+
95
+ # Whether the machine is the CBM-II's B series, whose ports page is
96
+ # its keyboard's alone.
97
+ def b_series? = cbm2? && !Controls.p500?(@computer)
98
+
91
99
  private
92
100
 
93
101
  def vic20? = @computer.family == :vic20
@@ -112,7 +120,10 @@ module Badline
112
120
  info("DEVICE", drive.nil? ? "FAST LOADING" : "#{drive.model_name}, TRUE DRIVE")
113
121
  end
114
122
 
123
+ # The CBM-II has no datasette.
115
124
  def draw_datasette
125
+ return info("TAPE", "") if cbm2?
126
+
116
127
  datasette = @computer.datasette
117
128
  tape = datasette.tape
118
129
  info("TAPE", tape.nil? ? "" : File.basename(tape.path))
@@ -126,6 +137,7 @@ module Badline
126
137
 
127
138
  def draw_expansion
128
139
  return draw_pet_memory if pet?
140
+ return info("RAM", "#{@computer.model.ram_kb}K") if cbm2?
129
141
 
130
142
  info("CARTRIDGE", @media.cartridge_name)
131
143
  return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
@@ -156,10 +168,19 @@ module Badline
156
168
  toggle(plus4? ? "SID CARD" : "SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
157
169
  end
158
170
 
159
- # The PET's memory: its RAM, and the 8096's 64K board.
171
+ # The PET's memory: its RAM, the 8096's 64K board, and the 8296's
172
+ # 128K with the jumpers that put RAM under its ROMs.
160
173
  def draw_pet_memory
174
+ model = @computer.model
175
+ return draw_8296_memory if model.cbm8296?
176
+
161
177
  info("RAM", "#{@computer.ram_kb}K")
162
- info("EXPANSION RAM", @computer.model.expansion ? "64K BOARD" : "")
178
+ info("EXPANSION RAM", model.expansion ? "64K BOARD" : "")
179
+ end
180
+
181
+ def draw_8296_memory
182
+ info("RAM", "128K, 64K OF IT BANKED")
183
+ info("JUMPERS", @computer.jumpers.join(", ").upcase)
163
184
  end
164
185
 
165
186
  # Saving now and by name, loading a named save, and the quicksaves
@@ -177,12 +198,14 @@ module Badline
177
198
  end
178
199
  end
179
200
 
180
- # The C128 names its model and the mode it runs in first, the Plus/4
181
- # and the PET their model and RAM, and the Plus/4 its RAM expansion if
182
- # it has one.
201
+ # The C128 names its model and the mode it runs in first, the Plus/4,
202
+ # the PET and the CBM-II their model and RAM, the Plus/4 its RAM
203
+ # expansion if it has one, and the CBM-II its KERNAL, the B series'
204
+ # 901244 or the P500's 901234.
183
205
  def draw_power
184
206
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
185
- info("MACHINE", @computer.model_label) if pet?
207
+ info("MACHINE", @computer.model_label.upcase) if pet? || cbm2?
208
+ info("KERNAL", "#{Controls.p500?(@computer) ? 901_234 : 901_244}-#{@computer.kernal.upcase}") if cbm2?
186
209
  if plus4?
187
210
  info("MACHINE", @computer.model_label)
188
211
  expansion = @computer.ram_expansion_label