badline 0.15.0 → 0.16.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive's 6523 TIA at $4000-$7FFF, its eight registers mirrored
7
+ # (zimmers.net's 1551 memory map, schematic 251860):
8
+ #
9
+ # - Port A carries the cable's eight data lines, pulled up.
10
+ # - Port B reads the GCR byte the gate array latched at BYTE READY
11
+ # and drives the byte the head writes.
12
+ # - Port C drives STATUS0 and STATUS1 (PC0-1), DEV (PC2), which picks
13
+ # where the computer's TIA answers, ACK (PC3) and the head's MODE
14
+ # (PC4, low to write), and reads the device number jumper (PC5,
15
+ # high for device 9), SYNC (PC6, low in a SYNC mark) and DAV (PC7).
16
+ #
17
+ # The DOS sets port C's directions to $1F. The cable's lines are
18
+ # pulled up, so an input nothing drives reads 1.
19
+ #
20
+ # Any access to the TIA clears BYTE LATCHED.
21
+ class TIA < Chips::TIA
22
+ DAV = 0x80
23
+ SYNC = 0x40
24
+ JUMPER = 0x20
25
+ MODE = 0x10
26
+
27
+ def initialize(mechanism, device:)
28
+ @mechanism = mechanism
29
+ @jumper = device == 9 ? JUMPER : 0
30
+ mechanism.tia = self
31
+ super()
32
+ @inputs[0] = 0xff
33
+ @inputs[1] = 0xff
34
+ @inputs[2] = 0xff
35
+ end
36
+
37
+ def reset!
38
+ super
39
+ mode_written
40
+ end
41
+
42
+ def peek(addr)
43
+ mechanism = @mechanism
44
+ mechanism.release_byte_latch
45
+ case addr & 0x07
46
+ when 1 then @inputs[1] = mechanism.latched_byte
47
+ when 2 then @inputs[2] = (@inputs[2] & DAV) | (mechanism.sync? ? 0 : SYNC) | @jumper | 0x1f
48
+ end
49
+ super
50
+ end
51
+
52
+ def poke(addr, value)
53
+ @mechanism.release_byte_latch
54
+ super
55
+ register = addr & 0x07
56
+ mode_written if [2, 5].include?(register)
57
+ end
58
+
59
+ # What port B drives onto the head's write shift register.
60
+ def port_b_output = output(1)
61
+
62
+ private
63
+
64
+ # MODE selects reading while high, as the 1541's CB2 does.
65
+ def mode_written
66
+ @mechanism.cb2_written(output(2).anybits?(MODE))
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/tia"
4
+ require "badline/drives/model1541"
5
+ require "badline/drives/model1551/cpu"
6
+ require "badline/drives/model1551/port"
7
+ require "badline/drives/model1551/tia"
8
+ require "badline/drives/model1551/bus"
9
+ require "badline/drives/model1551/cable"
10
+ require "badline/drives/model1551/keyed_state"
11
+
12
+ module Badline
13
+ module Drives
14
+ # The 1551 disk drive of the 264 series: a 6510T at 2 MHz running DOS
15
+ # 2.6 from 16 KB of ROM, with 2 KB of RAM, the 1541's GCR mechanism
16
+ # behind a gate array, and a 6523 TIA facing the computer over TCBM, a
17
+ # parallel cable to a second 6523 in the computer's expansion port
18
+ # (Commodore schematic 251860; zimmers.net's 1551 memory map).
19
+ #
20
+ # The 6510T's port drives the mechanism as the 1541's VIA 2 port B does
21
+ # and reads the write-protect sensor and the gate array's BYTE LATCHED
22
+ # (Port). The TIA's port B reads the GCR byte the gate array latched and
23
+ # drives the byte to write, and its port C selects reading or writing
24
+ # and reads SYNC (TIA).
25
+ #
26
+ # The drive comes with its interface for the computer's expansion port,
27
+ # a second 6523 (#host_tia), which the cable joins to the drive's
28
+ # (Cable). The computer puts it at $FEE0 for device 8 and at $FEC0 for
29
+ # device 9, as the drive's DEV line (PC2) says: high for device 8 and
30
+ # low for device 9. The DOS sets DEV from the jumper at reset, and again
31
+ # when a command renumbers the drive. The device number follows each
32
+ # change on the line (#device).
33
+ #
34
+ # A 555 timer pulls IRQ low for about 7 µs every 8.3 ms, its R1 120K,
35
+ # R2 100R and C1 0.1 µF, which runs the DOS's job loop. Nothing
36
+ # acknowledges it: the line lets go on its own.
37
+ #
38
+ # Drives::Engine::Core clocks it against the host and sleeps through
39
+ # the DOS's idle loop, which counts its passes in $61 (Engine::Idle's
40
+ # pass counter), until the 555 is due to pull IRQ, the count wraps or
41
+ # the computer moves the cable's lines.
42
+ class Model1551
43
+ include Engine::Core
44
+
45
+ CLOCK_HZ = 2_000_000
46
+
47
+ # The 555's cycle in drive cycles: high for 0.693 (R1 + R2) C and low,
48
+ # pulling IRQ, for 0.693 R2 C.
49
+ IRQ_PERIOD = 16_660
50
+ IRQ_LOW = 14
51
+
52
+ # The host cycles, at about 1 MHz, by which a 1551 switched on with the
53
+ # machine has run its DOS's reset, the RAM test and the ROM checksum,
54
+ # and answers TCBM: half a second, with a margin.
55
+ BOOT_CYCLES = 600_000
56
+
57
+ # Where the DOS's idle loop starts over, polling the TIA's port A for
58
+ # a command from the computer.
59
+ IDLE_LOOP = 0xeabd
60
+
61
+ # The byte the idle loop increments on each pass.
62
+ PASS_COUNTER = 0x61
63
+
64
+ def port
65
+ settle!
66
+ @port
67
+ end
68
+
69
+ def tia
70
+ settle!
71
+ @tia
72
+ end
73
+
74
+ # The TIA in the computer's expansion port, which the computer reads
75
+ # and writes without waking the drive.
76
+ attr_reader :host_tia
77
+
78
+ # The cable between the two TIAs.
79
+ def cable
80
+ settle!
81
+ @cable
82
+ end
83
+
84
+ def model_name = "1551"
85
+
86
+ # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
87
+ # path. +device+ is 8 or 9, as the jumper on the TIA's PC5 sets it.
88
+ def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
89
+ raise ArgumentError, "a 1551 is device 8 or 9, not #{device}" unless [8, 9].include?(device)
90
+
91
+ @device = device
92
+ @clock_hz = CLOCK_HZ
93
+ @mechanism = Model1541::Mechanism.new
94
+ @mechanism.clock_hz = CLOCK_HZ
95
+ @port = Port.new(@mechanism)
96
+ @tia = TIA.new(@mechanism, device:)
97
+ @host_tia = Chips::TIA.new
98
+ @bus = Bus.new(rom: rom || ROM.load("dos1551.rom", 0xc000), port: @port, tia: @tia)
99
+ @cpu = CPU.new(@bus, debug:)
100
+ @idle_loop = IDLE_LOOP
101
+ @host_clock_hz = host_clock_hz
102
+ @phase = 0
103
+ @cycles = 0
104
+ @irq_timer = IRQ_PERIOD
105
+ init_idle(debug)
106
+ @pass_counter = PASS_COUNTER
107
+ @dev_high = true
108
+ @on_renumber = nil
109
+ @cable = Cable.new(@host_tia, @tia, self)
110
+ @serial_port = @cable
111
+ end
112
+
113
+ # Calls the block with the old device number each time the DEV line
114
+ # moves the drive to the other one.
115
+ def on_renumber(&block)
116
+ @on_renumber = block
117
+ end
118
+
119
+ # The cable's DEV line, high or not. A change moves the drive to
120
+ # device 8 while it's high, and to device 9 while it's low.
121
+ def dev_line(high)
122
+ return if high == @dev_high
123
+
124
+ @dev_high = high
125
+ old = @device
126
+ @device = high ? 8 : 9
127
+ @on_renumber&.call(old) unless @device == old
128
+ end
129
+
130
+ # Puts in a Disk from the .d64 or .g64 image at +path+, write-protected
131
+ # with `read_only`.
132
+ def insert_image(path, read_only: false)
133
+ insert(Model1541::Disk.open(path, read_only:))
134
+ end
135
+
136
+ # The computer's RESET line reaches the interface's TIA and, through
137
+ # the cable, the CPU, its port and the drive's TIA. RAM keeps its
138
+ # contents, and the 555 runs on.
139
+ def reset!
140
+ settle!
141
+ @host_tia.reset!
142
+ @port.reset!
143
+ @tia.reset!
144
+ @cpu.reset!
145
+ end
146
+
147
+ private
148
+
149
+ # One drive cycle: the mechanism turns the disk ahead of the CPU, and
150
+ # the 555 pulls IRQ while its output is low.
151
+ def step
152
+ @mechanism.cycle!
153
+ timer = @irq_timer - 1
154
+ timer = IRQ_PERIOD if timer.zero?
155
+ @irq_timer = timer
156
+ @cpu.irq = timer <= IRQ_LOW
157
+ @cpu.cycle!
158
+ @cycles += 1
159
+ end
160
+
161
+ # A pass of the idle loop reads the TIA's port A, which holds still
162
+ # until the computer moves the cable's lines, and RAM. It repeats
163
+ # while the motor is off and the 555 leaves IRQ alone, for as long as
164
+ # the 555 leaves room (see Engine::Idle).
165
+ def busy_for_pass? = @mechanism.motor_on? || @irq_timer <= IRQ_LOW
166
+
167
+ def quiet_cycles
168
+ quiet = @irq_timer - IRQ_LOW - 1
169
+ quiet.negative? ? 0 : quiet
170
+ end
171
+
172
+ def idle_state
173
+ tia = @tia
174
+ [*@cpu.counting_state, @bus.data, *@mechanism.idle_state, *tia.ports, *tia.directions, *tia.inputs,
175
+ @port.ddr, @port.data]
176
+ end
177
+
178
+ def fast_forward_chips(cycles)
179
+ @irq_timer -= cycles
180
+ end
181
+
182
+ # Every 555 pulse runs the job loop, which reads the port and the TIA,
183
+ # so no orbit (Engine::Orbit) comes round, and the drive keeps no
184
+ # anchors.
185
+ def anchor = nil
186
+
187
+ def counter_state(_touched) = []
188
+
189
+ def skip_chip_orbits(_cycles, _touched) = nil
190
+ end
191
+ end
192
+ end
@@ -26,6 +26,7 @@ module Badline
26
26
  # (CIA#quiet?).
27
27
  class Model1571
28
28
  include Engine::Core
29
+ include Engine::SerialConnection
29
30
  include Engine::VIAs
30
31
  include Engine::FastSerial
31
32
 
@@ -39,6 +39,7 @@ module Badline
39
39
  # on either chip: the drive's CIA gets no TOD pulses (CIA's tod_hz).
40
40
  class Model1581
41
41
  include Engine::Core
42
+ include Engine::SerialConnection
42
43
  include Engine::FastSerial
43
44
 
44
45
  CLOCK_HZ = 2_000_000
@@ -4,5 +4,6 @@
4
4
  # 1571 stays out of the core: badline/c128 requires it.
5
5
  require "badline/drives/datasette"
6
6
  require "badline/drives/serial_bus"
7
+ require "badline/drives/ieee_bus"
7
8
  require "badline/drives/model1541"
8
9
  require "badline/drives/model1581"
@@ -112,7 +112,7 @@ module Badline
112
112
  end
113
113
 
114
114
  def handle_key(scancode, down)
115
- if [Keys::TAB, Keys::F8, Keys::F9, Keys::F10].include?(scancode)
115
+ if [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
116
116
  handle_toggle(scancode) if down
117
117
  elsif down && @snapshots.key(scancode)
118
118
  swap(@snapshots.computer)
@@ -17,7 +17,11 @@ module Badline
17
17
  end
18
18
 
19
19
  computer = nil
20
- problem = media_problem { computer = boot(options) }
20
+ problem = if cartridge?(options)
21
+ cartridge_problem { computer = boot(options) }
22
+ else
23
+ media_problem { computer = boot(options) }
24
+ end
21
25
  unless problem.empty?
22
26
  warn "#{options.program}: #{options.media_path}: #{problem}"
23
27
  return 1
@@ -40,6 +44,21 @@ module Badline
40
44
  e.message
41
45
  end
42
46
 
47
+ # Runs the block that puts a cartridge in, as media_problem does, and
48
+ # also returns the message of the ArgumentError a machine raises over a
49
+ # cartridge with a chip in a bank or slot it doesn't map.
50
+ def self.cartridge_problem(&)
51
+ media_problem(&)
52
+ rescue ArgumentError => e
53
+ e.message
54
+ end
55
+
56
+ # Whether the media the options start is a .crt.
57
+ def self.cartridge?(options)
58
+ media = options.media_path
59
+ !media.nil? && Media::Extensions.kind(media) == :cartridge
60
+ end
61
+
43
62
  # The machine a .vsf snapshot holds, built as the saved one was, or a
44
63
  # new one built from the options with the media attached.
45
64
  def self.boot(options)
@@ -7,7 +7,9 @@ module Badline
7
7
  # - The keys: every host key goes to the machine's keyboard, or, as Tab
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
- # #swap_ports swaps them. The VIC-20's one joystick takes both.
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).
11
13
  # - A pot device the pause menu plugs into a C64 control port: a 1351
12
14
  # mouse or a pair of paddles, which the host mouse drives. Motion
13
15
  # turns the paddles or moves the mouse, and the host's left and right
@@ -30,6 +32,7 @@ module Badline
30
32
  @mouse = Input::Mouse1351.new
31
33
  @paddles = Input::Paddles.new
32
34
  @held = {}
35
+ @pet_keyboard = pet_keyboard(computer)
33
36
  end
34
37
 
35
38
  # Routes the input to another machine, the pot device going into its
@@ -37,6 +40,7 @@ module Badline
37
40
  def computer=(computer)
38
41
  release_all
39
42
  @computer = computer
43
+ @pet_keyboard = pet_keyboard(computer)
40
44
  attach_pot_device
41
45
  end
42
46
 
@@ -48,10 +52,16 @@ module Badline
48
52
  release_all
49
53
  end
50
54
 
55
+ # Switches between the keyboard and the joysticks, on a machine with
56
+ # joysticks.
51
57
  def toggle_keys
52
- self.joystick_mode = !@joystick
58
+ self.joystick_mode = !@joystick if tab_toggles?
53
59
  end
54
60
 
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
64
+
55
65
  # Plugs in one of POTS, in place of the one before.
56
66
  def plug(pot)
57
67
  @pot = pot
@@ -106,6 +116,8 @@ module Badline
106
116
  # A host key lets go of the key it pressed, though the C128 has
107
117
  # switched between C64 and C128 mode since and maps it elsewhere.
108
118
  def machine_key(scancode, down)
119
+ return @pet_keyboard.key(scancode, down) if @computer.family == :pet
120
+
109
121
  @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
110
122
  key = @held[scancode]
111
123
  @held.delete(scancode) unless down
@@ -147,7 +159,7 @@ module Badline
147
159
  # A fresh pot device on its port, and none on the other. A machine
148
160
  # without the ports takes none.
149
161
  def attach_pot_device
150
- ports = @computer.control_ports
162
+ ports = @computer.family == :pet ? nil : @computer.control_ports
151
163
  if ports.nil?
152
164
  @pot = :none
153
165
  return
@@ -199,7 +211,15 @@ module Badline
199
211
  down ? joystick.press(direction) : joystick.release(direction)
200
212
  end
201
213
 
214
+ # The PET's keyboard for a PET, and for any other machine one it never
215
+ # uses.
216
+ def pet_keyboard(computer)
217
+ PETKeyboard.new(computer.keyboard, computer.family == :pet ? computer.model.keyboard : :graphics)
218
+ end
219
+
202
220
  def release_all
221
+ return @pet_keyboard.release_all if @computer.family == :pet
222
+
203
223
  Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
204
224
  Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
205
225
  Keys::ARROWS.each_value do |direction|
@@ -7,14 +7,15 @@ 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.
10
+ # reports one added or removed. The PET has no joysticks, and the
11
+ # controllers steer two that nothing reads.
11
12
  class Gamepads
12
13
  PORTS = [2, 1].freeze
13
14
 
14
15
  def initialize(computer, verbose)
15
16
  @verbose = verbose
16
17
  @controllers = []
17
- @ports = [PadPort.new(computer.joystick2), PadPort.new(computer.joystick1)]
18
+ @ports = [PadPort.new(joystick(computer, 2)), PadPort.new(joystick(computer, 1))]
18
19
  @buttons = Array.new(PadPort::BUTTON_COUNT, 0)
19
20
  @available = SDL.SDL_InitSubSystem(SDL::INIT_GAMECONTROLLER).zero?
20
21
  puts "No game controllers: #{SDL.SDL_GetError}" if @verbose && !@available
@@ -24,7 +25,7 @@ module Badline
24
25
  # what they hold on the next poll.
25
26
  def computer=(computer)
26
27
  @ports.each(&:release)
27
- @ports = [PadPort.new(computer.joystick2), PadPort.new(computer.joystick1)]
28
+ @ports = [PadPort.new(joystick(computer, 2)), PadPort.new(joystick(computer, 1))]
28
29
  end
29
30
 
30
31
  def rescan
@@ -51,6 +52,12 @@ module Badline
51
52
 
52
53
  private
53
54
 
55
+ def joystick(computer, port)
56
+ return Input::Joystick.new if computer.family == :pet
57
+
58
+ port == 1 ? computer.joystick1 : computer.joystick2
59
+ end
60
+
54
61
  def open_controller(index)
55
62
  controller = SDL.SDL_GameControllerOpen(index)
56
63
  # SDL returns NULL when the controller won't open, which Spinel's FFI
@@ -65,6 +65,59 @@ module Badline
65
65
  79 => :crsr_right, 80 => :crsr_left, 81 => :crsr_down, 82 => :crsr_up, 229 => :lshift, 75 => :none
66
66
  }.freeze
67
67
 
68
+ # The characters the keys other than the letters type on a US
69
+ # layout, unshifted and then shifted, and the keypad's.
70
+ US_CHARACTERS = {
71
+ 30 => "1!", 31 => "2@", 32 => "3#", 33 => "4$", 34 => "5%", 35 => "6^", 36 => "7&", 37 => "8*", 38 => "9(",
72
+ 39 => "0)", 45 => "-_", 46 => "=+", 47 => "[{", 48 => "]}", 49 => "\\|", 51 => ";:", 52 => "'\"", 53 => "`~",
73
+ 54 => ",<", 55 => ".>", 56 => "/?", 84 => "//", 85 => "**", 86 => "--", 87 => "++", 89 => "11", 90 => "22",
74
+ 91 => "33", 92 => "44", 93 => "55", 94 => "66", 95 => "77", 96 => "88", 97 => "99", 98 => "00", 99 => "..",
75
+ 103 => "=="
76
+ }.freeze
77
+
78
+ # The PET's keys for the characters a host key types, on either
79
+ # keyboard without SHIFT: each has a key of its own on the graphics
80
+ # keyboard, where SHIFT turns it into a graphics character. ↑ and ←
81
+ # are on ^, and on _ and `.
82
+ PET_GRAPHICS_CHARACTERS = {
83
+ "!" => :!, "\"" => :'"', "#" => :"#", "$" => :"$", "%" => :%, "&" => :&, "'" => :"'", "(" => :"(",
84
+ ")" => :")", "\\" => :"\\", "^" => :up, "_" => :left, "`" => :left, "/" => :/, ":" => :":", "*" => :*,
85
+ ";" => :";", "?" => :"?", "," => :",", "+" => :+, "@" => :"@", "[" => :"[", "]" => :"]", "<" => :<,
86
+ ">" => :>, "-" => :-, "=" => :"=", "." => :".", "1" => :"1", "2" => :"2", "3" => :"3", "4" => :"4",
87
+ "5" => :"5", "6" => :"6", "7" => :"7", "8" => :"8", "9" => :"9", "0" => :"0"
88
+ }.freeze
89
+
90
+ # The business keyboard's: the characters it types without SHIFT,
91
+ # then those it types with it.
92
+ PET_BUSINESS_CHARACTERS = {
93
+ "1" => :"1", "2" => :"2", "3" => :"3", "4" => :"4", "5" => :"5", "6" => :"6", "7" => :"7", "8" => :"8",
94
+ "9" => :"9", "0" => :"0", "-" => :-, ":" => :":", ";" => :";", "," => :",", "." => :".", "/" => :/,
95
+ "@" => :"@", "[" => :"[", "]" => :"]", "\\" => :"\\", "^" => :up, "_" => :left, "`" => :left
96
+ }.freeze
97
+
98
+ PET_BUSINESS_SHIFTED = {
99
+ "!" => :"1", "\"" => :"2", "#" => :"3", "$" => :"4", "%" => :"5", "&" => :"6", "'" => :"7", "(" => :"8",
100
+ ")" => :"9", "=" => :-, "*" => :":", "+" => :";", "<" => :",", ">" => :".", "?" => :/
101
+ }.freeze
102
+
103
+ # The PET's keys that don't type a character, which keep the host's
104
+ # SHIFT: RETURN, INST/DEL on Backspace, CLR/HOME on Home, RUN/STOP on
105
+ # Page Down, and RVS on Control. Esc is RUN/STOP on the graphics
106
+ # keyboard. The business keyboard's ESC, TAB, REPEAT and keypad are
107
+ # on Esc, Tab, Page Up and the keypad.
108
+ PET_KEYS = {
109
+ 40 => :return, 88 => :return, 42 => :delete, 44 => :space, 74 => :clr_home, 78 => :run_stop, 224 => :rvs,
110
+ 228 => :rvs
111
+ }.freeze
112
+
113
+ PET_BUSINESS_KEYS = {
114
+ 41 => :escape, 43 => :tab, 75 => :repeat, 89 => :kp1, 90 => :kp2, 91 => :kp3, 92 => :kp4, 93 => :kp5,
115
+ 94 => :kp6, 95 => :kp7, 96 => :kp8, 97 => :kp9, 98 => :kp0, 99 => :kp_period
116
+ }.freeze
117
+
118
+ # The cursor keys, CRSR ⇔ and CRSR ⇕, each with SHIFT for left or up.
119
+ PET_CURSORS = { RIGHT => :cursor_h, LEFT => :cursor_h, DOWN => :cursor_v, UP => :cursor_v }.freeze
120
+
68
121
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
69
122
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
70
123
 
@@ -89,6 +142,41 @@ module Badline
89
142
  # The 264's key for the scancode: one of PLUS4_OTHERS or the C64's.
90
143
  def self.plus4_key(scancode) = PLUS4_OTHERS[scancode] || c64_key(scancode)
91
144
 
145
+ # The PET key for the scancode on the +keyboard+, :graphics or
146
+ # :business, with the host's SHIFT down if +shift+, and how the
147
+ # PET's SHIFT goes with it: :on or :off for a key that types the host
148
+ # key's character or moves the cursor its way only so, and :host to
149
+ # follow the host's. A letter is the PET's letter, and any other key
150
+ # the PET key that types the same character, or :none.
151
+ def self.pet_key(scancode, shift, keyboard)
152
+ return [LETTERS[scancode - 4], :host] if scancode.between?(4, 29)
153
+
154
+ cursor = PET_CURSORS[scancode]
155
+ return [cursor, [LEFT, UP].include?(scancode) ? :on : :off] unless cursor.nil?
156
+
157
+ business = keyboard == :business
158
+ key = business ? PET_BUSINESS_KEYS[scancode] : nil
159
+ key = PET_KEYS[scancode] if key.nil?
160
+ key = :run_stop if key.nil? && scancode == ESCAPE
161
+ return [key, :host] unless key.nil?
162
+
163
+ pet_character(scancode, shift, business)
164
+ end
165
+
166
+ # The PET key that types the character the scancode types on a US
167
+ # layout, and the SHIFT it takes.
168
+ def self.pet_character(scancode, shift, business)
169
+ characters = US_CHARACTERS[scancode]
170
+ return %i[none host] if characters.nil?
171
+
172
+ character = characters[shift ? 1 : 0]
173
+ unshifted = business ? PET_BUSINESS_CHARACTERS[character] : PET_GRAPHICS_CHARACTERS[character]
174
+ return [unshifted, :off] unless unshifted.nil?
175
+
176
+ shifted = business ? PET_BUSINESS_SHIFTED[character] : nil
177
+ shifted.nil? ? %i[none host] : [shifted, :on]
178
+ end
179
+
92
180
  # The character the key types on a US layout: a letter, a capital
93
181
  # with `shift`, or a digit, and "" for any other key.
94
182
  def self.character(scancode, shift)
@@ -35,6 +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
39
 
39
40
  !computer.address_bus.cartridge.nil?
40
41
  end
@@ -37,7 +37,12 @@ module Badline
37
37
  # :program, or :snapshot, which starts in the saves folder.
38
38
  def browse(kind)
39
39
  @browsing = kind
40
- extensions = kind == :cartridge ? @media.cartridge_extensions : EXTENSIONS.fetch(kind)
40
+ extensions = case kind
41
+ when :cartridge then @media.cartridge_extensions
42
+ when :disk then @media.disk_extensions
43
+ when :program then programs
44
+ else EXTENSIONS.fetch(kind)
45
+ end
41
46
  @browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
42
47
  end
43
48
 
@@ -56,15 +61,15 @@ module Badline
56
61
  def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
57
62
 
58
63
  # Puts a dropped disk or tape in its device, or asks before a
59
- # cartridge or a program starts, and says whether it asks. Other
60
- # files are left alone.
64
+ # cartridge, a Plus/4's raw ROM image among them, or a program
65
+ # starts, and says whether it asks. Other files are left alone.
61
66
  def drop(path)
62
67
  extension = File.extname(path).downcase
63
- return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
68
+ return @media.insert(:disk, path) && false if @media.disk_extensions.include?(extension)
64
69
  return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
65
- return false unless EXTENSIONS[:program].include?(extension)
70
+ return false unless programs.include?(extension)
66
71
 
67
- @confirmation.ask(extension == ".crt" ? :cartridge : :start, path)
72
+ @confirmation.ask(@media.cartridge_extensions.include?(extension) ? :cartridge : :start, path)
68
73
  true
69
74
  end
70
75
 
@@ -98,6 +103,10 @@ module Badline
98
103
 
99
104
  private
100
105
 
106
+ # What Quick open and a drop start: every medium, and the cartridges
107
+ # the machine takes.
108
+ def programs = (EXTENSIONS[:program] + @media.cartridge_extensions).uniq
109
+
101
110
  # Puts the disk or tape picked in, or asks before a cartridge or
102
111
  # Quick open power cycles the machine, and leaves the browser, as Esc
103
112
  # does.