badline 0.15.1 → 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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +102 -6
  5. data/doc/pinned-behaviour.md +67 -0
  6. data/doc/snapshot-format.md +213 -18
  7. data/doc/snapshots.md +14 -0
  8. data/lib/badline/c64/common.rb +1 -1
  9. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  10. data/lib/badline/chips/crtc.rb +163 -0
  11. data/lib/badline/chips/pia/control_lines.rb +138 -0
  12. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  13. data/lib/badline/chips/pia.rb +138 -0
  14. data/lib/badline/chips/vic/register_log.rb +1 -4
  15. data/lib/badline/core.rb +3 -0
  16. data/lib/badline/drives/ieee_bus.rb +28 -0
  17. data/lib/badline/drives/model1551/cable.rb +5 -2
  18. data/lib/badline/drives/model1551/tia.rb +13 -34
  19. data/lib/badline/drives/model1551.rb +26 -4
  20. data/lib/badline/drives.rb +1 -0
  21. data/lib/badline/frontend/app.rb +1 -1
  22. data/lib/badline/frontend/controls.rb +23 -3
  23. data/lib/badline/frontend/gamepads.rb +10 -3
  24. data/lib/badline/frontend/keys.rb +88 -0
  25. data/lib/badline/frontend/media_slots.rb +1 -0
  26. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  27. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  28. data/lib/badline/frontend/menu/menu_pages.rb +15 -2
  29. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  30. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  31. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  32. data/lib/badline/frontend/screens.rb +2 -1
  33. data/lib/badline/frontend/timeline.rb +16 -9
  34. data/lib/badline/frontend.rb +1 -0
  35. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  36. data/lib/badline/kernal_trap/dos.rb +6 -1
  37. data/lib/badline/kernal_trap/frames.rb +117 -0
  38. data/lib/badline/kernal_trap/ieee.rb +148 -0
  39. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  40. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  41. data/lib/badline/kernal_trap/routine.rb +3 -53
  42. data/lib/badline/kernal_trap/serial.rb +4 -102
  43. data/lib/badline/kernal_trap/steps.rb +64 -0
  44. data/lib/badline/kernal_trap.rb +4 -0
  45. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  46. data/lib/badline/machine/settling.rb +1 -1
  47. data/lib/badline/machine.rb +28 -6
  48. data/lib/badline/media/extensions.rb +5 -2
  49. data/lib/badline/media/pet_media.rb +40 -0
  50. data/lib/badline/media/program.rb +14 -2
  51. data/lib/badline/media/vic20_media.rb +2 -2
  52. data/lib/badline/media.rb +36 -16
  53. data/lib/badline/options/family.rb +17 -6
  54. data/lib/badline/options/help.rb +19 -3
  55. data/lib/badline/options/table.rb +14 -2
  56. data/lib/badline/options/validation.rb +68 -5
  57. data/lib/badline/options.rb +31 -19
  58. data/lib/badline/pet/bus/expansion.rb +106 -0
  59. data/lib/badline/pet/bus.rb +204 -0
  60. data/lib/badline/pet/canvas.rb +97 -0
  61. data/lib/badline/pet/cassettes.rb +63 -0
  62. data/lib/badline/pet/characters.rb +26 -0
  63. data/lib/badline/pet/cpu.rb +10 -0
  64. data/lib/badline/pet/crtc_video.rb +142 -0
  65. data/lib/badline/pet/discrete_video.rb +83 -0
  66. data/lib/badline/pet/ieee_ports.rb +66 -0
  67. data/lib/badline/pet/ieee_traps.rb +75 -0
  68. data/lib/badline/pet/keyboard_ports.rb +74 -0
  69. data/lib/badline/pet/keyed_modules.rb +85 -0
  70. data/lib/badline/pet/keyed_parts.rb +123 -0
  71. data/lib/badline/pet/keyed_state.rb +42 -0
  72. data/lib/badline/pet/model.rb +113 -0
  73. data/lib/badline/pet/rom_sets.rb +65 -0
  74. data/lib/badline/pet/sound.rb +128 -0
  75. data/lib/badline/pet/via.rb +10 -0
  76. data/lib/badline/pet/via_ports.rb +19 -0
  77. data/lib/badline/pet.rb +226 -0
  78. data/lib/badline/plus4/keyed_state.rb +5 -3
  79. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  80. data/lib/badline/plus4.rb +1 -1
  81. data/lib/badline/roms/README +137 -0
  82. data/lib/badline/roms/pet/basic1.rom +0 -0
  83. data/lib/badline/roms/pet/basic2.rom +0 -0
  84. data/lib/badline/roms/pet/basic4.rom +0 -0
  85. data/lib/badline/roms/pet/characters1.rom +0 -0
  86. data/lib/badline/roms/pet/characters2.rom +0 -0
  87. data/lib/badline/roms/pet/editor1.rom +0 -0
  88. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  89. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  90. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  91. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  92. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  93. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  94. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  95. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  96. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  97. data/lib/badline/roms/pet/kernal1.rom +0 -0
  98. data/lib/badline/roms/pet/kernal2.rom +0 -0
  99. data/lib/badline/roms/pet/kernal4.rom +0 -0
  100. data/lib/badline/snapshot/container.rb +2 -0
  101. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  102. data/lib/badline/snapshot/field_doc.rb +16 -3
  103. data/lib/badline/snapshot/image.rb +14 -7
  104. data/lib/badline/snapshot/pet_setup.rb +71 -0
  105. data/lib/badline/snapshot/storage_setup.rb +7 -1
  106. data/lib/badline/snapshot.rb +9 -5
  107. data/lib/badline/storage/d80_image.rb +48 -0
  108. data/lib/badline/storage/d82_image.rb +21 -0
  109. data/lib/badline/storage/tap.rb +16 -3
  110. data/lib/badline/storage.rb +2 -0
  111. data/lib/badline/support/pet_checkpoint.rb +38 -0
  112. data/lib/badline/timing.rb +5 -4
  113. data/lib/badline/version.rb +1 -1
  114. data/lib/badline/vic20/attachments.rb +7 -2
  115. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  116. data/lib/badline/vic20/bus.rb +5 -0
  117. data/lib/badline/vic20.rb +1 -1
  118. metadata +55 -1
@@ -4,8 +4,7 @@ module Badline
4
4
  module Drives
5
5
  class Model1551
6
6
  # The drive's 6523 TIA at $4000-$7FFF, its eight registers mirrored
7
- # (zimmers.net's 1551 memory map, schematic 251860), a Chips::TIA
8
- # with what the drive's side wires to it:
7
+ # (zimmers.net's 1551 memory map, schematic 251860):
9
8
  #
10
9
  # - Port A carries the cable's eight data lines, pulled up.
11
10
  # - Port B reads the GCR byte the gate array latched at BYTE READY
@@ -19,72 +18,52 @@ module Badline
19
18
  # pulled up, so an input nothing drives reads 1.
20
19
  #
21
20
  # Any access to the TIA clears BYTE LATCHED.
22
- class TIA
21
+ class TIA < Chips::TIA
23
22
  DAV = 0x80
24
23
  SYNC = 0x40
25
24
  JUMPER = 0x20
26
25
  MODE = 0x10
27
26
 
28
- # The chip, whose inputs the drive's side sets before each read.
29
- attr_reader :chip
30
-
31
27
  def initialize(mechanism, device:)
32
28
  @mechanism = mechanism
33
29
  @jumper = device == 9 ? JUMPER : 0
34
- @chip = Chips::TIA.new
35
- inputs = @chip.inputs
36
- inputs[0] = 0xff
37
- inputs[1] = 0xff
38
- inputs[2] = 0xff
39
30
  mechanism.tia = self
40
- mode_written
31
+ super()
32
+ @inputs[0] = 0xff
33
+ @inputs[1] = 0xff
34
+ @inputs[2] = 0xff
41
35
  end
42
36
 
43
- def ports = @chip.ports
44
-
45
- def directions = @chip.directions
46
-
47
- def inputs = @chip.inputs
48
-
49
37
  def reset!
50
- @chip.reset!
38
+ super
51
39
  mode_written
52
40
  end
53
41
 
54
42
  def peek(addr)
55
43
  mechanism = @mechanism
56
44
  mechanism.release_byte_latch
57
- chip = @chip
58
- inputs = chip.inputs
59
45
  case addr & 0x07
60
- when 1 then inputs[1] = mechanism.latched_byte
61
- when 2 then inputs[2] = (inputs[2] & DAV) | (mechanism.sync? ? 0 : SYNC) | @jumper | 0x1f
46
+ when 1 then @inputs[1] = mechanism.latched_byte
47
+ when 2 then @inputs[2] = (@inputs[2] & DAV) | (mechanism.sync? ? 0 : SYNC) | @jumper | 0x1f
62
48
  end
63
- chip.peek(addr)
49
+ super
64
50
  end
65
51
 
66
52
  def poke(addr, value)
67
53
  @mechanism.release_byte_latch
68
- @chip.poke(addr, value)
54
+ super
69
55
  register = addr & 0x07
70
56
  mode_written if [2, 5].include?(register)
71
57
  end
72
58
 
73
59
  # What port B drives onto the head's write shift register.
74
- def port_b_output = @chip.output(1)
75
-
76
- # The chip's registers, in a keyed snapshot module (BL-D8-TIA).
77
- def save_fields(out) = @chip.save_fields(out)
78
-
79
- def load_fields(input)
80
- @chip.load_fields(input)
81
- end
60
+ def port_b_output = output(1)
82
61
 
83
62
  private
84
63
 
85
64
  # MODE selects reading while high, as the 1541's CB2 does.
86
65
  def mode_written
87
- @mechanism.cb2_written(@chip.output(2).anybits?(MODE))
66
+ @mechanism.cb2_written(output(2).anybits?(MODE))
88
67
  end
89
68
  end
90
69
  end
@@ -26,7 +26,10 @@ module Badline
26
26
  # The drive comes with its interface for the computer's expansion port,
27
27
  # a second 6523 (#host_tia), which the cable joins to the drive's
28
28
  # (Cable). The computer puts it at $FEE0 for device 8 and at $FEC0 for
29
- # device 9.
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).
30
33
  #
31
34
  # A 555 timer pulls IRQ low for about 7 µs every 8.3 ms, its R1 120K,
32
35
  # R2 100R and C1 0.1 µF, which runs the DOS's job loop. Nothing
@@ -101,10 +104,29 @@ module Badline
101
104
  @irq_timer = IRQ_PERIOD
102
105
  init_idle(debug)
103
106
  @pass_counter = PASS_COUNTER
104
- @cable = Cable.new(@host_tia, @tia.chip, self)
107
+ @dev_high = true
108
+ @on_renumber = nil
109
+ @cable = Cable.new(@host_tia, @tia, self)
105
110
  @serial_port = @cable
106
111
  end
107
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
+
108
130
  # Puts in a Disk from the .d64 or .g64 image at +path+, write-protected
109
131
  # with `read_only`.
110
132
  def insert_image(path, read_only: false)
@@ -148,8 +170,8 @@ module Badline
148
170
  end
149
171
 
150
172
  def idle_state
151
- chip = @tia.chip
152
- [*@cpu.counting_state, @bus.data, *@mechanism.idle_state, *chip.ports, *chip.directions, *chip.inputs,
173
+ tia = @tia
174
+ [*@cpu.counting_state, @bus.data, *@mechanism.idle_state, *tia.ports, *tia.directions, *tia.inputs,
153
175
  @port.ddr, @port.data]
154
176
  end
155
177
 
@@ -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)
@@ -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
@@ -39,6 +39,7 @@ module Badline
39
39
  @browsing = kind
40
40
  extensions = case kind
41
41
  when :cartridge then @media.cartridge_extensions
42
+ when :disk then @media.disk_extensions
42
43
  when :program then programs
43
44
  else EXTENSIONS.fetch(kind)
44
45
  end
@@ -64,7 +65,7 @@ module Badline
64
65
  # starts, and says whether it asks. Other files are left alone.
65
66
  def drop(path)
66
67
  extension = File.extname(path).downcase
67
- return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
68
+ return @media.insert(:disk, path) && false if @media.disk_extensions.include?(extension)
68
69
  return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
69
70
  return false unless programs.include?(extension)
70
71
 
@@ -99,13 +99,22 @@ module Badline
99
99
  insert(:disk, set[index + step])
100
100
  end
101
101
 
102
- # The extensions of the cartridges the machine takes: .crt, and on
103
- # the Plus/4 raw .bin ROM images too.
102
+ # The extensions of the cartridges the machine takes: .crt, on the
103
+ # Plus/4 raw .bin ROM images too, and none on the PET.
104
104
  def cartridge_extensions
105
- extensions = Media::Extensions.of(%i[cartridge])
105
+ extensions = Media::Extensions.of(@computer.family == :pet ? %i[none] : %i[cartridge])
106
106
  @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
107
107
  end
108
108
 
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
+ def disk_extensions
113
+ return Media::Extensions.of(%i[disk ieee_disk archive disk_list]) if @computer.family == :pet
114
+
115
+ Media::Extensions::DEVICE8
116
+ end
117
+
109
118
  # Swaps the cartridge for the one at `path`, which power cycles the
110
119
  # machine, and says whether it went in.
111
120
  def attach_cartridge(path)
@@ -117,10 +126,10 @@ module Badline
117
126
  # What a save of the machine is named after: the cartridge, the disk,
118
127
  # the tape, or BASIC, without the bracketed parts of a file's name. A
119
128
  # VIC-20's are named after its disk, or else the machine, and a
120
- # Plus/4's after its disk or tape.
129
+ # Plus/4's or a PET's after its disk or tape.
121
130
  def game_name
122
131
  vic20 = @computer.family == :vic20
123
- name = vic20 || @computer.family == :plus4 ? "" : cartridge_name
132
+ name = vic20 || %i[plus4 pet].include?(@computer.family) ? "" : cartridge_name
124
133
  disk = disk_path
125
134
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
126
135
  name = tape_name if name.empty? && !vic20
@@ -136,14 +145,16 @@ module Badline
136
145
  end
137
146
 
138
147
  # The name the cartridge in the expansion port gives itself, or an
139
- # empty one. A VIC-20's or a Plus/4's is the name of the file it came
140
- # from: a .crt's own, or else the file's. A Plus/4 names each of its
148
+ # empty one. A VIC-20 keeps its own (Vic20#cartridge_name). A Plus/4's
149
+ # is the name of the file it came from: a .crt's own, or else the
150
+ # file's. It names each of its
141
151
  # cartridges with a ROM in, and a slot filled from elsewhere, such as
142
152
  # a snapshot, by the slot.
143
153
  def cartridge_name
144
154
  case @computer.family
145
155
  when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
146
- when :vic20 then named? ? @names.fetch("", "") : ""
156
+ when :vic20 then @computer.cartridge_name
157
+ when :pet then ""
147
158
  else
148
159
  cartridge = @computer.address_bus.cartridge
149
160
  cartridge.nil? ? "" : cartridge.name
@@ -200,28 +211,16 @@ module Badline
200
211
 
201
212
  def slot_name(slot) = named? ? @names.fetch(slot, slot.upcase) : slot.upcase
202
213
 
203
- # Keeps the name of the cartridge at `path` in `computer` by the
204
- # slots it fills: a Plus/4's by xplus4's names, a raw image's in
205
- # `slot` if it names one, and a VIC-20's in the one slot "". Other
206
- # media, and other machines' cartridges, leave the names alone.
214
+ # Keeps the name of the cartridge at `path` in a Plus/4 by the slots
215
+ # it fills, by xplus4's names, a raw image's in `slot` if it names
216
+ # one. Other media, and other machines' cartridges, leave the names
217
+ # alone.
207
218
  def note_cartridge(computer, path, slot)
208
- case computer.family
209
- when :plus4
210
- return unless Media.kind(computer, path) == :cartridge
211
-
212
- name = file_name(path)
213
- Media::Plus4Media.cartridge_chips(path, slot: slot.empty? ? nil : slot).each do |chip|
214
- @names["c#{chip.bank + 1}#{chip.address < 0xc000 ? 'lo' : 'hi'}"] = name
215
- end
216
- when :vic20 then @names[""] = file_name(path) if vic20_cartridge?(path)
217
- end
218
- end
219
+ return unless computer.family == :plus4 && Media.kind(computer, path) == :cartridge
219
220
 
220
- def vic20_cartridge?(path)
221
- case Media::Extensions.kind(path)
222
- when :cartridge then true
223
- when :program then File.file?(path) && !Media::Vic20Cartridge.chips(path, Media.program_bytes(path)).nil?
224
- else false
221
+ name = file_name(path)
222
+ Media::Plus4Media.cartridge_chips(path, slot: slot.empty? ? nil : slot).each do |chip|
223
+ @names["c#{chip.bank + 1}#{chip.address < 0xc000 ? 'lo' : 'hi'}"] = name
225
224
  end
226
225
  end
227
226
 
@@ -85,6 +85,9 @@ module Badline
85
85
  # Whether the machine is a C128, whose F8 switches its screens.
86
86
  def c128? = @computer.family == :c128
87
87
 
88
+ # Whether the machine is a PET, whose ports page shows its keyboard.
89
+ def pet? = @computer.family == :pet
90
+
88
91
  private
89
92
 
90
93
  def vic20? = @computer.family == :vic20
@@ -122,6 +125,8 @@ module Badline
122
125
  end
123
126
 
124
127
  def draw_expansion
128
+ return draw_pet_memory if pet?
129
+
125
130
  info("CARTRIDGE", @media.cartridge_name)
126
131
  return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
127
132
 
@@ -151,6 +156,12 @@ module Badline
151
156
  toggle(plus4? ? "SID CARD" : "SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
152
157
  end
153
158
 
159
+ # The PET's memory: its RAM, and the 8096's 64K board.
160
+ def draw_pet_memory
161
+ info("RAM", "#{@computer.ram_kb}K")
162
+ info("EXPANSION RAM", @computer.model.expansion ? "64K BOARD" : "")
163
+ end
164
+
154
165
  # Saving now and by name, loading a named save, and the quicksaves
155
166
  # and autosaves, newest first, which a press loads.
156
167
  def draw_snapshots
@@ -166,10 +177,12 @@ module Badline
166
177
  end
167
178
  end
168
179
 
169
- # The C128 names its model and the mode it runs in first, and the
170
- # Plus/4 its model and RAM, and its RAM expansion if it has one.
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.
171
183
  def draw_power
172
184
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
185
+ info("MACHINE", @computer.model_label) if pet?
173
186
  if plus4?
174
187
  info("MACHINE", @computer.model_label)
175
188
  expansion = @computer.ram_expansion_label
@@ -5,18 +5,28 @@ module Badline
5
5
  # The pause menu's PORTS page, which MenuPages draws: what each C64
6
6
  # control port has plugged in, a joystick or a pot device, and whether
7
7
  # the keys type or drive the joysticks. The VIC-20 has one port, a
8
- # joystick's, and the Plus/4 two joysticks and no pot devices.
8
+ # joystick's, and the Plus/4 two joysticks and no pot devices. The PET
9
+ # has no ports, and the page, KEYBOARD there, says where its keys are
10
+ # instead.
9
11
  module MenuPorts
10
12
  PORT_DEVICES = %i[joystick mouse paddles].freeze
11
13
  PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
12
14
  PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
13
15
  POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
14
16
 
17
+ # Where the PET's keys without a character of their own are, on both
18
+ # keyboards, and then on the graphics one and on the business one.
19
+ PET_KEYS = ["PAGE DOWN: RUN/STOP", "HOME: CLR/HOME", "BACKSPACE: INST/DEL", "CTRL: RVS",
20
+ "ARROWS: CURSOR", "^: UP ARROW", "_: LEFT ARROW"].freeze
21
+ PET_GRAPHICS_KEYS = ["ESC: RUN/STOP", "SHIFT+LETTER: GRAPHIC", "ALT+SYMBOL: GRAPHIC"].freeze
22
+ PET_BUSINESS_KEYS = ["ESC: ESC", "TAB: TAB", "PAGE UP: REPEAT", "KEYPAD: KEYPAD", "ALT+KEY: SHIFT+KEY"].freeze
23
+
15
24
  private
16
25
 
17
26
  def draw_ports
18
27
  return draw_vic20_port if @computer.family == :vic20
19
28
  return draw_plus4_ports if @computer.family == :plus4
29
+ return draw_pet_keys if @computer.family == :pet
20
30
 
21
31
  toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
22
32
  toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
@@ -49,6 +59,17 @@ module Badline
49
59
  info("WASD, LEFT SHIFT", "JOYSTICK #{3 - arrows}")
50
60
  end
51
61
 
62
+ # The PET's keyboard, whose keys type the characters on the host's
63
+ # (PETKeyboard), and the keys that stand for the rest of its keys.
64
+ def draw_pet_keys
65
+ business = @computer.model.keyboard == :business
66
+ info("KEYBOARD", business ? "BUSINESS" : "GRAPHICS")
67
+ (PET_KEYS + (business ? PET_BUSINESS_KEYS : PET_GRAPHICS_KEYS)).each do |line|
68
+ @painter.text(@left, @top, line, MenuTheme::TEXT)
69
+ @top += MenuPages::LINE
70
+ end
71
+ end
72
+
52
73
  # The toggle between the keyboard named and the joysticks, and
53
74
  # whether the keys drive the joysticks.
54
75
  def draw_keys(keyboard)
@@ -238,6 +238,7 @@ module Badline
238
238
  def draw_sections
239
239
  top = @top + 32
240
240
  SECTIONS.each_with_index do |name, index|
241
+ name = "KEYBOARD" if PAGES[index] == :ports && @pages.pet?
241
242
  @buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
242
243
  top += Buttons::HEIGHT + 2
243
244
  end