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
@@ -10,17 +10,29 @@ module Badline
10
10
  # wrong for the page to show.
11
11
  class MenuMedia
12
12
  attr_reader :problem
13
- attr_writer :computer
14
13
 
15
14
  # Takes the disk set from the command line's `media_path` when it's a
16
- # list.
17
- def initialize(media_path, writable)
15
+ # list, and the names of the cartridges it and `cartridges`, the
16
+ # Plus/4's ROM images by slot (Options#cartridges), put in the first
17
+ # machine.
18
+ def initialize(media_path, writable, cartridges)
18
19
  @list = Media::DiskList.list?(media_path) ? media_path : ""
19
20
  @writable = writable
20
21
  @set_path = ""
21
22
  @set = []
22
23
  @problem = ""
23
24
  @computer = nil
25
+ @command_line = cartridges.map { |slot, path| [slot, path] } + [["", media_path]]
26
+ @names = {}
27
+ @named = nil
28
+ end
29
+
30
+ def computer=(computer)
31
+ @computer = computer
32
+ return unless @named.nil?
33
+
34
+ @command_line.each { |slot, path| note_cartridge(computer, path.dup, slot) }
35
+ @named = computer
24
36
  end
25
37
 
26
38
  def writable? = @writable
@@ -87,26 +99,37 @@ module Badline
87
99
  insert(:disk, set[index + step])
88
100
  end
89
101
 
90
- # The extensions of the cartridges the machine takes: .crt, and on
91
- # 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.
92
104
  def cartridge_extensions
93
- extensions = Media::Extensions.of(%i[cartridge])
105
+ extensions = Media::Extensions.of(@computer.family == :pet ? %i[none] : %i[cartridge])
94
106
  @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
95
107
  end
96
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
+
97
118
  # Swaps the cartridge for the one at `path`, which power cycles the
98
119
  # machine, and says whether it went in.
99
120
  def attach_cartridge(path)
100
- succeeds? { Media.attach(@computer, path) }
121
+ attached = cartridge_succeeds? { Media.attach(@computer, path) }
122
+ note_cartridge(@computer, path, "") if attached
123
+ attached
101
124
  end
102
125
 
103
126
  # What a save of the machine is named after: the cartridge, the disk,
104
127
  # the tape, or BASIC, without the bracketed parts of a file's name. A
105
128
  # VIC-20's are named after its disk, or else the machine, and a
106
- # Plus/4's after its disk or tape.
129
+ # Plus/4's or a PET's after its disk or tape.
107
130
  def game_name
108
131
  vic20 = @computer.family == :vic20
109
- name = vic20 || @computer.family == :plus4 ? "" : cartridge_name
132
+ name = vic20 || %i[plus4 pet].include?(@computer.family) ? "" : cartridge_name
110
133
  disk = disk_path
111
134
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
112
135
  name = tape_name if name.empty? && !vic20
@@ -122,26 +145,38 @@ module Badline
122
145
  end
123
146
 
124
147
  # The name the cartridge in the expansion port gives itself, or an
125
- # empty one. A Plus/4's names the slots with a ROM in them.
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
151
+ # cartridges with a ROM in, and a slot filled from elsewhere, such as
152
+ # a snapshot, by the slot.
126
153
  def cartridge_name
127
- return @computer.cartridge_slots.map(&:upcase).join(", ") if @computer.family == :plus4
128
-
129
- cartridge = @computer.address_bus.cartridge
130
- cartridge.nil? ? "" : cartridge.name
154
+ case @computer.family
155
+ when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
156
+ when :vic20 then @computer.cartridge_name
157
+ when :pet then ""
158
+ else
159
+ cartridge = @computer.address_bus.cartridge
160
+ cartridge.nil? ? "" : cartridge.name
161
+ end
131
162
  end
132
163
 
133
164
  # Takes the cartridge out with the power off.
134
- def remove_cartridge = MediaSlots.remove_cartridge(@computer)
165
+ def remove_cartridge
166
+ @names = {}
167
+ MediaSlots.remove_cartridge(@computer)
168
+ end
135
169
 
136
170
  # A new machine started on `path`, as the command line would, or nil
137
171
  # when it won't start.
138
172
  def start(options, path)
139
173
  computer = nil
140
- started = succeeds? do
141
- computer = Frontend.start(options, path, @writable)
142
- list(path)
174
+ if Media.kind(@computer, path) == :cartridge
175
+ cartridge_succeeds? { computer = started(options, path) }
176
+ else
177
+ succeeds? { computer = started(options, path) }
143
178
  end
144
- started ? computer : nil
179
+ @problem.empty? ? computer : nil
145
180
  end
146
181
 
147
182
  private
@@ -153,6 +188,48 @@ module Badline
153
188
  @problem.empty?
154
189
  end
155
190
 
191
+ # Runs the block as #succeeds? does, keeping a cartridge's chip in a
192
+ # bank or slot the machine doesn't map as the problem too.
193
+ def cartridge_succeeds?(&)
194
+ @problem = Frontend.cartridge_problem(&)
195
+ @problem.empty?
196
+ end
197
+
198
+ # The machine Frontend.start started on `path`, with the names of
199
+ # the cartridges in it noted.
200
+ def started(options, path)
201
+ computer = Frontend.start(options, path, @writable)
202
+ list(path)
203
+ @names = {}
204
+ options.cartridges.each { |slot, image| note_cartridge(computer, image.dup, slot) }
205
+ note_cartridge(computer, path, "")
206
+ @named = computer
207
+ computer
208
+ end
209
+
210
+ def named? = @computer.equal?(@named)
211
+
212
+ def slot_name(slot) = named? ? @names.fetch(slot, slot.upcase) : slot.upcase
213
+
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.
218
+ def note_cartridge(computer, path, slot)
219
+ return unless computer.family == :plus4 && Media.kind(computer, path) == :cartridge
220
+
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
224
+ end
225
+ end
226
+
227
+ # A .crt's own name, or else the file's, without its extension.
228
+ def file_name(path)
229
+ name = Media::Extensions.kind(path) == :cartridge ? Storage::CRTFile.new(path, machine: nil).name : ""
230
+ name.empty? ? File.basename(path, File.extname(path)) : name
231
+ end
232
+
156
233
  # Keeps `path` as the list the set comes from, if it's a list.
157
234
  def list(path)
158
235
  return unless Media::DiskList.list?(path)
@@ -29,7 +29,7 @@ module Badline
29
29
  @recent = []
30
30
  @painter = painter
31
31
  @buttons = buttons
32
- @media = MenuMedia.new(media_path, options.writable?)
32
+ @media = MenuMedia.new(media_path, options.writable?, options.cartridges)
33
33
  @reu_kb = options.reu
34
34
  @computer = nil
35
35
  @controls = nil
@@ -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,9 +125,11 @@ module Badline
122
125
  end
123
126
 
124
127
  def draw_expansion
125
- return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
128
+ return draw_pet_memory if pet?
126
129
 
127
130
  info("CARTRIDGE", @media.cartridge_name)
131
+ return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
132
+
128
133
  row("INSERT...", :insert_cartridge)
129
134
  row("REMOVE", :remove_cartridge)
130
135
  return if plus4?
@@ -137,7 +142,8 @@ module Badline
137
142
  info("RAM EXPANSION", "REU, #{@reu_kb} KB")
138
143
  end
139
144
 
140
- # The SID's model is a setting, and other sound chips have none.
145
+ # The SID's model is a setting, the C64's SID's or a Plus/4's SID
146
+ # card's, and other sound chips have none.
141
147
  def draw_sound
142
148
  if @sound.on?
143
149
  toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
@@ -147,7 +153,13 @@ module Badline
147
153
  model = @computer.sound_source.model
148
154
  return unless SID_MODELS.include?(model)
149
155
 
150
- toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
156
+ toggle(plus4? ? "SID CARD" : "SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
157
+ end
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" : "")
151
163
  end
152
164
 
153
165
  # Saving now and by name, loading a named save, and the quicksaves
@@ -165,11 +177,17 @@ module Badline
165
177
  end
166
178
  end
167
179
 
168
- # The C128 names its model and the mode it runs in first, and the
169
- # Plus/4 its model and RAM.
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.
170
183
  def draw_power
171
184
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
172
- info("MACHINE", @computer.model_label) if plus4?
185
+ info("MACHINE", @computer.model_label) if pet?
186
+ if plus4?
187
+ info("MACHINE", @computer.model_label)
188
+ expansion = @computer.ram_expansion_label
189
+ info("RAM EXPANSION", expansion) if expansion
190
+ end
173
191
  row("RESET", :reset)
174
192
  row("POWER CYCLE", :power_cycle)
175
193
  skip
@@ -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
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The host's keys on a PET's key matrix, by the character each types
6
+ # (Keys.pet_key), so the host's " types the PET's " on either keyboard.
7
+ #
8
+ # The PET's SHIFT follows the host's, except while a key down needs it
9
+ # one way to type its character: then it is held that way until the
10
+ # key goes up, the last such key deciding.
11
+ #
12
+ # With either Alt down, a key that types a character presses the PET
13
+ # key its unshifted character is on with the PET's SHIFT, so the
14
+ # graphics on the graphics keyboard's symbol keys can be typed.
15
+ class PETKeyboard
16
+ LSHIFT = 225
17
+ RSHIFT = 229
18
+ LALT = 226
19
+ RALT = 230
20
+
21
+ # +layout+ is the PET's keyboard, :graphics or :business.
22
+ def initialize(keyboard, layout)
23
+ @keyboard = keyboard
24
+ @layout = layout
25
+ @held = {}
26
+ @left_shift = false
27
+ @right_shift = false
28
+ @left_alt = false
29
+ @right_alt = false
30
+ @forced = -1
31
+ @forced_shift = false
32
+ end
33
+
34
+ def key(scancode, down)
35
+ if [LSHIFT, RSHIFT].include?(scancode)
36
+ shift_key(scancode, down)
37
+ elsif [LALT, RALT].include?(scancode)
38
+ alt_key(scancode, down)
39
+ elsif down
40
+ key_down(scancode)
41
+ else
42
+ key_up(scancode)
43
+ end
44
+ end
45
+
46
+ # Lets go of every key, SHIFT among them.
47
+ def release_all
48
+ @held.each_value { |key| @keyboard.release(key) }
49
+ @held = {}
50
+ @left_shift = false
51
+ @right_shift = false
52
+ @left_alt = false
53
+ @right_alt = false
54
+ @forced = -1
55
+ sync_shift
56
+ end
57
+
58
+ private
59
+
60
+ def key_down(scancode)
61
+ key_up(scancode) if @held.key?(scancode)
62
+ key, shift = alt? ? shifted_key(scancode) : Keys.pet_key(scancode, shifted?, @layout)
63
+ return if key == :none
64
+
65
+ unless shift == :host
66
+ @forced = scancode
67
+ @forced_shift = shift == :on
68
+ end
69
+ sync_shift
70
+ @held[scancode] = key
71
+ @keyboard.press(key)
72
+ end
73
+
74
+ def key_up(scancode)
75
+ key = @held.delete(scancode)
76
+ return if key.nil?
77
+
78
+ @keyboard.release(key)
79
+ return unless @forced == scancode
80
+
81
+ @forced = -1
82
+ sync_shift
83
+ end
84
+
85
+ def shift_key(scancode, down)
86
+ if scancode == LSHIFT
87
+ @left_shift = down
88
+ else
89
+ @right_shift = down
90
+ end
91
+ sync_shift
92
+ end
93
+
94
+ def alt_key(scancode, down)
95
+ if scancode == LALT
96
+ @left_alt = down
97
+ else
98
+ @right_alt = down
99
+ end
100
+ end
101
+
102
+ # The PET key for the scancode with Alt down: for a letter or a key
103
+ # that types a character, the key its unshifted character is on, with
104
+ # SHIFT, and for any other key the one it presses without Alt.
105
+ def shifted_key(scancode)
106
+ key, shift = Keys.pet_key(scancode, false, @layout)
107
+ return [key, shift] if key == :none
108
+ return [key, shift] unless scancode.between?(4, 29) || Keys::US_CHARACTERS.key?(scancode)
109
+
110
+ [key, :on]
111
+ end
112
+
113
+ def shifted? = @left_shift || @right_shift
114
+
115
+ def alt? = @left_alt || @right_alt
116
+
117
+ def sync_shift
118
+ shift = @forced == -1 ? shifted? : @forced_shift
119
+ @keyboard.release(:lshift)
120
+ @keyboard.press(:lshift) if shift
121
+ end
122
+ end
123
+ end
124
+ end
@@ -15,7 +15,8 @@ module Badline
15
15
  # way: #wait handles the window's clicks, keys and drops between frames
16
16
  # and redraws it at the display's pace. The SID model buttons switch
17
17
  # the chips playing on the spot, and each tune after it starts on the
18
- # same choice. AUTO gives each of a tune's SIDs its own model.
18
+ # same choice. AUTO gives each of a tune's SIDs its own model. It
19
+ # quits once it has drawn `frames` frames (--frames), unless that's 0.
19
20
  class PlayerController < Audio::Terminal
20
21
  FRAME = 1.0 / 50
21
22
 
@@ -30,8 +31,10 @@ module Badline
30
31
  Keys::TAB => :view, 6 => :chip
31
32
  }.freeze
32
33
 
33
- def initialize(input:, output:)
34
- super
34
+ def initialize(input:, output:, frames: 0)
35
+ super(input:, output:)
36
+ @frame_limit = frames
37
+ @frames = 0
35
38
  @screen = PlayerScreen.new
36
39
  @player = nil
37
40
  @tune_file = nil
@@ -73,7 +76,10 @@ module Badline
73
76
  actions = poll
74
77
  deadline = now + seconds
75
78
  loop do
76
- draw if now >= @next_draw
79
+ if now >= @next_draw
80
+ draw
81
+ actions << :quit if @frames == @frame_limit
82
+ end
77
83
  left = deadline - now
78
84
  break if left <= 0 || !actions.empty?
79
85
 
@@ -211,6 +217,7 @@ module Badline
211
217
 
212
218
  def draw
213
219
  @next_draw = now + FRAME
220
+ @frames += 1
214
221
  @screen.draw(@state, @player, @history, @tune_file, SIDTune::STIL::Credit.at(@credits, @state.played))
215
222
  end
216
223
 
@@ -71,7 +71,8 @@ module Badline
71
71
  else
72
72
  timing = @computer.timing
73
73
  @screen = Screen.new(@computer.video, timing.crop,
74
- pixel_width: timing.pixel_width, frame_lines: timing.lines_per_frame)
74
+ pixel_width: timing.pixel_width, dots: timing.dots,
75
+ frame_lines: timing.lines_per_frame)
75
76
  end
76
77
  end
77
78
 
@@ -39,19 +39,22 @@ module Badline
39
39
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
40
40
 
41
41
  # Whether the family's machine has a key of the name. The Plus/4 has
42
- # no RESTORE.
42
+ # no RESTORE, and the PET neither that nor joysticks.
43
43
  def self.key?(name, family)
44
- (name == "restore" && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
45
- !keyboard_key(name, family).nil? || !joystick_direction(name).nil?
44
+ pet = family == :pet
45
+ (name == "restore" && !pet && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
46
+ !keyboard_key(name, family).nil? || (!pet && !joystick_direction(name).nil?)
46
47
  end
47
48
 
48
49
  # The family's Keyboard key for a name such as space, return or a,
49
- # on the C128 esc or keypad1, or on the Plus/4 help or crsr_up. The
50
- # shifted cursor keys, such as cursor_up, are the C64's and the
51
- # C128's.
50
+ # on the C128 esc or keypad1, on the Plus/4 help or crsr_up, or on
51
+ # the PET rvs or kp1, of either of its keyboards. The shifted cursor
52
+ # keys, such as cursor_up, are the C64's and the C128's.
52
53
  def self.keyboard_key(name, family)
54
+ return nil if name == "none"
55
+
53
56
  key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
54
- return key unless key.nil? && family != :plus4
57
+ return key unless key.nil? && !%i[plus4 pet].include?(family)
55
58
 
56
59
  Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
57
60
  end
@@ -60,6 +63,7 @@ module Badline
60
63
  case family
61
64
  when :c128 then C128::KEYBOARD_MATRIX
62
65
  when :plus4 then Plus4::KeyboardPort::MATRIX
66
+ when :pet then PET::KeyboardPorts::GRAPHICS_MATRIX + PET::KeyboardPorts::BUSINESS_MATRIX
63
67
  else Input::Keyboard::C64_MATRIX
64
68
  end
65
69
  end
@@ -223,8 +227,11 @@ module Badline
223
227
  puts "Ejected the #{what}"
224
228
  end
225
229
 
226
- # Whether there's one in to eject. A VIC-20 has no cartridge to take out yet.
227
- def ejectable?(computer, what) = (computer.family != :vic20 || what != "cartridge") && inserted?(computer, what)
230
+ # Whether there's one in to eject. A VIC-20 has no cartridge to take
231
+ # out yet, and a PET no expansion port.
232
+ def ejectable?(computer, what)
233
+ (what != "cartridge" || !%i[vic20 pet].include?(computer.family)) && inserted?(computer, what)
234
+ end
228
235
 
229
236
  def inserted?(computer, what)
230
237
  return !computer.datasette.tape.nil? if what == "tape"
@@ -15,6 +15,7 @@ require "badline/frontend/audio_device"
15
15
  require "badline/frontend/sound"
16
16
  require "badline/frontend/pacer"
17
17
  require "badline/frontend/frame_report"
18
+ require "badline/frontend/pet_keyboard"
18
19
  require "badline/frontend/controls"
19
20
  require "badline/frontend/pad_port"
20
21
  require "badline/frontend/gamepads"
@@ -19,7 +19,7 @@ module Badline
19
19
  @channels.close(secondary)
20
20
  return report(FILE_EXISTS)
21
21
  end
22
- return refuse_write(secondary) unless writable_disk?
22
+ return refuse_write(secondary) unless writable_disk? || channel_save?(secondary)
23
23
 
24
24
  open_writer(secondary, WriteFile.new(file, type || :seq, mode: replace ? :replace : :write))
25
25
  end
@@ -41,6 +41,8 @@ module Badline
41
41
 
42
42
  def writable_disk? = @storage.respond_to?(:writable?) && @storage.writable?
43
43
 
44
+ def channel_save?(secondary) = @channel_saves && secondary == 1 && saves?
45
+
44
46
  # Runs a write to the disk and reports how it went. Returns whether
45
47
  # the disk took it.
46
48
  def writing
@@ -68,7 +68,12 @@ module Badline
68
68
 
69
69
  # A drive powers on reporting its DOS version, as a reset does. It
70
70
  # can take the RAM of a drive it replaces, and initializes its disk.
71
- def initialize(storage, memory = Memory.new)
71
+ # With +channel_saves+, SAVE's channel, secondary address 1, takes a
72
+ # program for storage that saves only whole files, such as a host
73
+ # directory, and saves it when the channel closes: a PET's SAVE sends
74
+ # it through the bus with no trap of its own.
75
+ def initialize(storage, memory = Memory.new, channel_saves: false)
76
+ @channel_saves = channel_saves
72
77
  @channels = Channels.new
73
78
  @status = Status.new
74
79
  @memory = memory