badline 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. metadata +72 -2
@@ -3,8 +3,8 @@
3
3
  module Badline
4
4
  module Frontend
5
5
  # SDL scancodes follow key positions on a US layout. They map onto the C64
6
- # keys, the C128's and the 264's own keys and, in joystick mode, the
7
- # joystick directions.
6
+ # keys, the C128's, the 264's, the PET's and the CBM-II's own keys and,
7
+ # in joystick mode, the joystick directions.
8
8
  module Keys
9
9
  RETURN = 40
10
10
  ESCAPE = 41
@@ -118,6 +118,41 @@ module Badline
118
118
  # The cursor keys, CRSR ⇔ and CRSR ⇕, each with SHIFT for left or up.
119
119
  PET_CURSORS = { RIGHT => :cursor_h, LEFT => :cursor_h, DOWN => :cursor_v, UP => :cursor_v }.freeze
120
120
 
121
+ # The CBM-II's keys for the characters a host key types: those it
122
+ # types without SHIFT, then those it types with it, as the KERNAL's
123
+ # key tables place them. ← is on _ and `, and SHIFT turns it into \.
124
+ CBM2_CHARACTERS = {
125
+ "1" => :"1", "2" => :"2", "3" => :"3", "4" => :"4", "5" => :"5", "6" => :"6", "7" => :"7", "8" => :"8",
126
+ "9" => :"9", "0" => :"0", "-" => :-, "=" => :"=", "[" => :"[", "]" => :"]", ";" => :";", "'" => :"'",
127
+ "," => :",", "." => :".", "/" => :/, "_" => :left_arrow, "`" => :left_arrow
128
+ }.freeze
129
+
130
+ CBM2_SHIFTED = {
131
+ "!" => :"1", "@" => :"2", "#" => :"3", "$" => :"4", "%" => :"5", "^" => :"6", "&" => :"7", "*" => :"8",
132
+ "(" => :"9", ")" => :"0", "+" => :"=", ":" => :";", "\"" => :"'", "<" => :",", ">" => :".", "?" => :/,
133
+ "\\" => :left_arrow
134
+ }.freeze
135
+
136
+ # The CBM-II's keys that don't type a character, which keep the
137
+ # host's SHIFT: RETURN, INST/DEL on Backspace, CLR/HOME on Home, ESC,
138
+ # TAB, RUN/STOP on F12, CTRL on Control, the four cursor keys on the
139
+ # arrows, F1-F8 on F1-F8, and the keypad on the host's.
140
+ CBM2_KEYS = {
141
+ 40 => :return, 42 => :delete, 44 => :space, 74 => :clr_home, 41 => :escape, 43 => :tab, 69 => :run_stop,
142
+ 224 => :control, 228 => :control, 79 => :cursor_right, 80 => :cursor_left, 81 => :cursor_down,
143
+ 82 => :cursor_up, 58 => :f1, 59 => :f2, 60 => :f3, 61 => :f4, 62 => :f5, 63 => :f6, 64 => :f7, 65 => :f8,
144
+ 84 => :kp_divide, 85 => :kp_multiply, 86 => :kp_minus, 87 => :kp_plus, 88 => :kp_enter, 89 => :kp1,
145
+ 90 => :kp2, 91 => :kp3, 92 => :kp4, 93 => :kp5, 94 => :kp6, 95 => :kp7, 96 => :kp8, 97 => :kp9,
146
+ 98 => :kp0, 99 => :kp_period
147
+ }.freeze
148
+
149
+ # The CBM-II's keys that Alt and a host key press: C= on C, CE on E,
150
+ # NORM/GRAPH on G, OFF/RVS on R, π on P, 00 on 0, and F9 and F10 on
151
+ # F9 and F10, which open the pause menu and mute without Alt.
152
+ CBM2_ALT_KEYS = {
153
+ 6 => :cbm, 8 => :ce, 10 => :norm_graph, 21 => :off_rvs, 19 => :pi, 39 => :kp00, 66 => :f9, 67 => :f10
154
+ }.freeze
155
+
121
156
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
122
157
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
123
158
 
@@ -177,6 +212,30 @@ module Badline
177
212
  shifted.nil? ? %i[none host] : [shifted, :on]
178
213
  end
179
214
 
215
+ # The CBM-II key for the scancode, with the host's SHIFT down if
216
+ # +shift+, and how the CBM-II's SHIFT goes with it, as pet_key gives
217
+ # them.
218
+ def self.cbm2_key(scancode, shift)
219
+ return [LETTERS[scancode - 4], :host] if scancode.between?(4, 29)
220
+
221
+ key = CBM2_KEYS[scancode]
222
+ return [key, :host] unless key.nil?
223
+
224
+ characters = US_CHARACTERS[scancode]
225
+ return %i[none host] if characters.nil?
226
+
227
+ character = characters[shift ? 1 : 0]
228
+ unshifted = CBM2_CHARACTERS[character]
229
+ return [unshifted, :off] unless unshifted.nil?
230
+
231
+ shifted = CBM2_SHIFTED[character]
232
+ shifted.nil? ? %i[none host] : [shifted, :on]
233
+ end
234
+
235
+ # The CBM-II key Alt and the scancode press, which keeps the host's
236
+ # SHIFT, or :none.
237
+ def self.cbm2_alt_key(scancode) = [CBM2_ALT_KEYS[scancode] || :none, :host]
238
+
180
239
  # The character the key types on a US layout: a letter, a capital
181
240
  # with `shift`, or a digit, and "" for any other key.
182
241
  def self.character(scancode, shift)
@@ -35,7 +35,7 @@ module Badline
35
35
  # either cartridge's slots.
36
36
  def cartridge?(computer)
37
37
  return !computer.cartridge_slots.empty? if computer.family == :plus4
38
- return false if computer.family == :pet
38
+ return false if %i[pet cbm2].include?(computer.family)
39
39
 
40
40
  !computer.address_bus.cartridge.nil?
41
41
  end
@@ -50,6 +50,8 @@ module Badline
50
50
  Media.insert_disk(@computer, disk, read_only: !@writable)
51
51
  list(path)
52
52
  else
53
+ raise Machine::Unsupported, "the CBM-II has no datasette" if @computer.family == :cbm2
54
+
53
55
  @computer.datasette.insert(Storage::TAP.new(path))
54
56
  end
55
57
  end
@@ -100,17 +102,18 @@ module Badline
100
102
  end
101
103
 
102
104
  # The extensions of the cartridges the machine takes: .crt, on the
103
- # Plus/4 raw .bin ROM images too, and none on the PET.
105
+ # Plus/4 raw .bin ROM images too, and none on the PET, or on the
106
+ # CBM-II, whose cartridge goes in from the command line.
104
107
  def cartridge_extensions
105
- extensions = Media::Extensions.of(@computer.family == :pet ? %i[none] : %i[cartridge])
108
+ extensions = Media::Extensions.of(%i[pet cbm2].include?(@computer.family) ? %i[none] : %i[cartridge])
106
109
  @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
107
110
  end
108
111
 
109
- # The extensions of what goes in device 8: on the PET the 8050's and
110
- # 8250's disk images besides the others, and no raw GCR, which only
111
- # a serial true drive reads.
112
+ # The extensions of what goes in device 8: on the PET and the CBM-II
113
+ # the 8050's and 8250's disk images besides the others, and no raw
114
+ # GCR, which only a serial true drive reads.
112
115
  def disk_extensions
113
- return Media::Extensions.of(%i[disk ieee_disk archive disk_list]) if @computer.family == :pet
116
+ return Media::Extensions.of(%i[disk ieee_disk archive disk_list]) if %i[pet cbm2].include?(@computer.family)
114
117
 
115
118
  Media::Extensions::DEVICE8
116
119
  end
@@ -125,14 +128,14 @@ module Badline
125
128
 
126
129
  # What a save of the machine is named after: the cartridge, the disk,
127
130
  # the tape, or BASIC, without the bracketed parts of a file's name. A
128
- # VIC-20's are named after its disk, or else the machine, and a
129
- # Plus/4's or a PET's after its disk or tape.
131
+ # VIC-20's are named after its disk, or else the machine, a Plus/4's
132
+ # or a PET's after its disk or tape, and a CBM-II's after its disk.
130
133
  def game_name
131
134
  vic20 = @computer.family == :vic20
132
- name = vic20 || %i[plus4 pet].include?(@computer.family) ? "" : cartridge_name
135
+ name = vic20 || %i[plus4 pet cbm2].include?(@computer.family) ? "" : cartridge_name
133
136
  disk = disk_path
134
137
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
135
- name = tape_name if name.empty? && !vic20
138
+ name = tape_name if name.empty? && !vic20 && @computer.family != :cbm2
136
139
  name = name.sub(/\s*[(\[].*\z/, "").strip
137
140
  return name unless name.empty?
138
141
 
@@ -154,7 +157,7 @@ module Badline
154
157
  case @computer.family
155
158
  when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
156
159
  when :vic20 then @computer.cartridge_name
157
- when :pet then ""
160
+ when :pet, :cbm2 then ""
158
161
  else
159
162
  cartridge = @computer.address_bus.cartridge
160
163
  cartridge.nil? ? "" : cartridge.name
@@ -88,6 +88,14 @@ module Badline
88
88
  # Whether the machine is a PET, whose ports page shows its keyboard.
89
89
  def pet? = @computer.family == :pet
90
90
 
91
+ # Whether the machine is a CBM-II, whose ports page shows its keyboard
92
+ # too.
93
+ def cbm2? = @computer.family == :cbm2
94
+
95
+ # Whether the machine is the CBM-II's B series, whose ports page is
96
+ # its keyboard's alone.
97
+ def b_series? = cbm2? && !Controls.p500?(@computer)
98
+
91
99
  private
92
100
 
93
101
  def vic20? = @computer.family == :vic20
@@ -112,7 +120,10 @@ module Badline
112
120
  info("DEVICE", drive.nil? ? "FAST LOADING" : "#{drive.model_name}, TRUE DRIVE")
113
121
  end
114
122
 
123
+ # The CBM-II has no datasette.
115
124
  def draw_datasette
125
+ return info("TAPE", "") if cbm2?
126
+
116
127
  datasette = @computer.datasette
117
128
  tape = datasette.tape
118
129
  info("TAPE", tape.nil? ? "" : File.basename(tape.path))
@@ -126,6 +137,7 @@ module Badline
126
137
 
127
138
  def draw_expansion
128
139
  return draw_pet_memory if pet?
140
+ return info("RAM", "#{@computer.model.ram_kb}K") if cbm2?
129
141
 
130
142
  info("CARTRIDGE", @media.cartridge_name)
131
143
  return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
@@ -186,12 +198,14 @@ module Badline
186
198
  end
187
199
  end
188
200
 
189
- # The C128 names its model and the mode it runs in first, the Plus/4
190
- # and the PET their model and RAM, and the Plus/4 its RAM expansion if
191
- # it has one.
201
+ # The C128 names its model and the mode it runs in first, the Plus/4,
202
+ # the PET and the CBM-II their model and RAM, the Plus/4 its RAM
203
+ # expansion if it has one, and the CBM-II its KERNAL, the B series'
204
+ # 901244 or the P500's 901234.
192
205
  def draw_power
193
206
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
194
- info("MACHINE", @computer.model_label) if pet?
207
+ info("MACHINE", @computer.model_label.upcase) if pet? || cbm2?
208
+ info("KERNAL", "#{Controls.p500?(@computer) ? 901_234 : 901_244}-#{@computer.kernal.upcase}") if cbm2?
195
209
  if plus4?
196
210
  info("MACHINE", @computer.model_label)
197
211
  expansion = @computer.ram_expansion_label
@@ -6,8 +6,9 @@ module Badline
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
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
+ # and the CBM-II's B series have no ports, and the page, KEYBOARD
10
+ # there, says where their keys are instead. The P500 has two joysticks,
11
+ # and the page says where its keys are while they type.
11
12
  module MenuPorts
12
13
  PORT_DEVICES = %i[joystick mouse paddles].freeze
13
14
  PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
@@ -21,15 +22,21 @@ module Badline
21
22
  PET_GRAPHICS_KEYS = ["ESC: RUN/STOP", "SHIFT+LETTER: GRAPHIC", "ALT+SYMBOL: GRAPHIC"].freeze
22
23
  PET_BUSINESS_KEYS = ["ESC: ESC", "TAB: TAB", "PAGE UP: REPEAT", "KEYPAD: KEYPAD", "ALT+KEY: SHIFT+KEY"].freeze
23
24
 
25
+ # Where the CBM-II's keys without a character of their own are.
26
+ CBM2_KEYS = ["ESC: ESC", "F12: RUN/STOP", "HOME: CLR/HOME", "BACKSPACE: INST/DEL", "F1-F8: F1-F8",
27
+ "ALT+F9, ALT+F10: F9, F10", "KEYPAD: KEYPAD", "ALT+0: KEYPAD 00", "ALT+C: C= ALT+E: CE",
28
+ "ALT+G: NORM/GRAPH", "ALT+R: OFF/RVS ALT+P: PI", "_: LEFT ARROW"].freeze
29
+
24
30
  private
25
31
 
26
32
  def draw_ports
27
33
  return draw_vic20_port if @computer.family == :vic20
28
34
  return draw_plus4_ports if @computer.family == :plus4
29
35
  return draw_pet_keys if @computer.family == :pet
36
+ return draw_p500_ports if Controls.p500?(@computer)
37
+ return draw_cbm2_keys if @computer.family == :cbm2
30
38
 
31
- toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
32
- toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
39
+ draw_pot_ports
33
40
  return unless draw_keys(@computer.family == :c128 ? "C128" : "C64")
34
41
 
35
42
  row("SWAP JOYSTICKS", :swap)
@@ -70,6 +77,35 @@ module Badline
70
77
  end
71
78
  end
72
79
 
80
+ # What each control port has plugged in, a joystick, a mouse or
81
+ # paddles.
82
+ def draw_pot_ports
83
+ toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
84
+ toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
85
+ end
86
+
87
+ # The P500's two ports, its joysticks one for each set of keys, and
88
+ # while the keys type, where its keys are.
89
+ def draw_p500_ports
90
+ draw_pot_ports
91
+ return draw_cbm2_keys unless draw_keys("P500")
92
+
93
+ row("SWAP JOYSTICKS", :swap)
94
+ skip
95
+ arrows = @controls.arrows_port
96
+ info("ARROWS, SPACE", "JOYSTICK #{arrows}")
97
+ info("WASD, LEFT SHIFT", "JOYSTICK #{3 - arrows}")
98
+ end
99
+
100
+ # The CBM-II's keyboard, which types as the PET's does, and the keys
101
+ # that stand for the rest of its keys.
102
+ def draw_cbm2_keys
103
+ CBM2_KEYS.each do |line|
104
+ @painter.text(@left, @top, line, MenuTheme::TEXT)
105
+ @top += MenuPages::LINE
106
+ end
107
+ end
108
+
73
109
  # The toggle between the keyboard named and the joysticks, and
74
110
  # whether the keys drive the joysticks.
75
111
  def draw_keys(keyboard)
@@ -238,7 +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
+ name = "KEYBOARD" if PAGES[index] == :ports && (@pages.pet? || @pages.b_series?)
242
242
  @buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
243
243
  top += Buttons::HEIGHT + 2
244
244
  end
@@ -12,16 +12,22 @@ module Badline
12
12
  # With either Alt down, a key that types a character presses the PET
13
13
  # key its unshifted character is on with the PET's SHIFT, so the
14
14
  # graphics on the graphics keyboard's symbol keys can be typed.
15
+ #
16
+ # The CBM-II's keyboard goes the same way (Keys.cbm2_key), and there
17
+ # Alt with a key presses one of the keys the host has none for
18
+ # (Keys.cbm2_alt_key).
15
19
  class PETKeyboard
16
20
  LSHIFT = 225
17
21
  RSHIFT = 229
18
22
  LALT = 226
19
23
  RALT = 230
20
24
 
21
- # +layout+ is the PET's keyboard, :graphics or :business.
25
+ # +layout+ is the PET's keyboard, :graphics or :business, or :cbm2
26
+ # for the CBM-II's.
22
27
  def initialize(keyboard, layout)
23
28
  @keyboard = keyboard
24
29
  @layout = layout
30
+ @shift = layout == :cbm2 ? :shift : :lshift
25
31
  @held = {}
26
32
  @left_shift = false
27
33
  @right_shift = false
@@ -43,6 +49,11 @@ module Badline
43
49
  end
44
50
  end
45
51
 
52
+ # Whether a key the scancode pressed is still down.
53
+ def holds?(scancode) = @held.key?(scancode)
54
+
55
+ def alt? = @left_alt || @right_alt
56
+
46
57
  # Lets go of every key, SHIFT among them.
47
58
  def release_all
48
59
  @held.each_value { |key| @keyboard.release(key) }
@@ -59,7 +70,7 @@ module Badline
59
70
 
60
71
  def key_down(scancode)
61
72
  key_up(scancode) if @held.key?(scancode)
62
- key, shift = alt? ? shifted_key(scancode) : Keys.pet_key(scancode, shifted?, @layout)
73
+ key, shift = alt? ? shifted_key(scancode) : layout_key(scancode, shifted?)
63
74
  return if key == :none
64
75
 
65
76
  unless shift == :host
@@ -99,10 +110,17 @@ module Badline
99
110
  end
100
111
  end
101
112
 
113
+ def layout_key(scancode, shift)
114
+ @layout == :cbm2 ? Keys.cbm2_key(scancode, shift) : Keys.pet_key(scancode, shift, @layout)
115
+ end
116
+
102
117
  # The PET key for the scancode with Alt down: for a letter or a key
103
118
  # 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.
119
+ # SHIFT, and for any other key the one it presses without Alt. The
120
+ # CBM-II's is the one Keys.cbm2_alt_key names.
105
121
  def shifted_key(scancode)
122
+ return Keys.cbm2_alt_key(scancode) if @layout == :cbm2
123
+
106
124
  key, shift = Keys.pet_key(scancode, false, @layout)
107
125
  return [key, shift] if key == :none
108
126
  return [key, shift] unless scancode.between?(4, 29) || Keys::US_CHARACTERS.key?(scancode)
@@ -112,12 +130,10 @@ module Badline
112
130
 
113
131
  def shifted? = @left_shift || @right_shift
114
132
 
115
- def alt? = @left_alt || @right_alt
116
-
117
133
  def sync_shift
118
134
  shift = @forced == -1 ? shifted? : @forced_shift
119
- @keyboard.release(:lshift)
120
- @keyboard.press(:lshift) if shift
135
+ @keyboard.release(@shift)
136
+ @keyboard.press(@shift) if shift
121
137
  end
122
138
  end
123
139
  end
@@ -25,7 +25,8 @@ module Badline
25
25
  LOCKS = %w[caps_lock forty_eighty].freeze
26
26
 
27
27
  # What's wrong with the events for the family's machine, the first
28
- # key or menu page that names nothing, or an empty string.
28
+ # key or menu page that names nothing, or an empty string. The
29
+ # CBM-II's P500 is family :p500 here, for its joysticks.
29
30
  def self.error(events, family)
30
31
  event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument, family) }
31
32
  return "no such key: #{event.argument}" unless event.nil?
@@ -38,23 +39,26 @@ module Badline
38
39
  # sound. An empty one is its first.
39
40
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
40
41
 
41
- # Whether the family's machine has a key of the name. The Plus/4 has
42
- # no RESTORE, and the PET neither that nor joysticks.
42
+ # Whether the family's machine has a key of the name, the CBM-II's
43
+ # P500's as family :p500. The Plus/4 has no RESTORE, the P500 none
44
+ # either, and the PET and the CBM-II's B series neither that nor
45
+ # joysticks.
43
46
  def self.key?(name, family)
44
- pet = family == :pet
45
- (name == "restore" && !pet && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
47
+ pet = %i[pet cbm2].include?(family)
48
+ (name == "restore" && !pet && !%i[plus4 p500].include?(family)) || (family == :c128 && LOCKS.include?(name)) ||
46
49
  !keyboard_key(name, family).nil? || (!pet && !joystick_direction(name).nil?)
47
50
  end
48
51
 
49
52
  # The family's Keyboard key for a name such as space, return or a,
50
53
  # 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.
54
+ # the PET rvs or kp1, of either of its keyboards, or on the CBM-II
55
+ # escape, norm_graph or kp00. The shifted cursor keys, such as
56
+ # cursor_up, are the C64's and the C128's.
53
57
  def self.keyboard_key(name, family)
54
58
  return nil if name == "none"
55
59
 
56
60
  key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
57
- return key unless key.nil? && !%i[plus4 pet].include?(family)
61
+ return key unless key.nil? && !%i[plus4 pet cbm2 p500].include?(family)
58
62
 
59
63
  Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
60
64
  end
@@ -64,6 +68,7 @@ module Badline
64
68
  when :c128 then C128::KEYBOARD_MATRIX
65
69
  when :plus4 then Plus4::KeyboardPort::MATRIX
66
70
  when :pet then PET::KeyboardPorts::GRAPHICS_MATRIX + PET::KeyboardPorts::BUSINESS_MATRIX
71
+ when :cbm2, :p500 then CBM2::KeyboardPorts::MATRIX
67
72
  else Input::Keyboard::C64_MATRIX
68
73
  end
69
74
  end
@@ -228,9 +233,13 @@ module Badline
228
233
  end
229
234
 
230
235
  # Whether there's one in to eject. A VIC-20 has no cartridge to take
231
- # out yet, and a PET no expansion port.
236
+ # out yet, a PET no expansion port, and a CBM-II no datasette, and
237
+ # its cartridge stays in.
232
238
  def ejectable?(computer, what)
233
- (what != "cartridge" || !%i[vic20 pet].include?(computer.family)) && inserted?(computer, what)
239
+ family = computer.family
240
+ return false if family == :cbm2 && what != "disk"
241
+
242
+ (what != "cartridge" || !%i[vic20 pet].include?(family)) && inserted?(computer, what)
234
243
  end
235
244
 
236
245
  def inserted?(computer, what)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ class DOS
6
+ # N, R and C: the commands that format a disk, rename a file and copy
7
+ # files. Each takes its names after a colon. A name can't hold a
8
+ # wildcard, and R and C take their new name before an "=". The DOS
9
+ # here has one drive, so a file on drive 1 is never found, and C
10
+ # without a colon, which copies a whole disk between the drives of a
11
+ # dual drive, and D, which duplicates one, are invalid commands, as on
12
+ # a single drive.
13
+ module DiskCommands
14
+ WILDCARD = /[*?]/
15
+ OTHER_DRIVE = /\A[1-9]\d*:/
16
+ DRIVE_ZERO = /\A0+:/
17
+
18
+ private
19
+
20
+ # N with an ID after a comma formats the disk, and without one only
21
+ # clears it. The ID is the two bytes after the comma, which takes the
22
+ # carriage return PRINT# ends the command with when the ID is one
23
+ # character. A comma with nothing after it gives no ID.
24
+ def new_disk(text)
25
+ colon = text.index(":")
26
+ return report(NO_FILE_GIVEN) unless colon
27
+
28
+ command = text.chomp("\r")
29
+ name, id = command[(colon + 1)..].split(",", 2)
30
+ return report(SYNTAX_ERROR) if name.to_s.match?(/[*?=]/)
31
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:format)
32
+
33
+ id = id.to_s.empty? ? nil : text.bytes[(command.index(",") + 1), 2]
34
+ writing { @storage.format(name.to_s, id && (id + [Storage::DiskImage::NAME_PADDING]).first(2)) }
35
+ end
36
+
37
+ # R takes the new name, then the file's.
38
+ def rename_file(text)
39
+ new_name, old_names = file_names(text, NO_FILE_GIVEN)
40
+ return unless new_name
41
+ return report(SYNTAX_ERROR) unless old_names.one?
42
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:rename)
43
+ return report(FILE_NOT_FOUND) if old_names.first.match?(OTHER_DRIVE)
44
+
45
+ writing { @storage.rename(Storage.strip_drive_prefix(old_names.first), new_name) }
46
+ end
47
+
48
+ # C takes the new name, then the files it joins, separated by
49
+ # commas. A first file named with drive 0 that is the new file
50
+ # itself has the others added to its end.
51
+ def copy_file(text)
52
+ new_name, old_names = file_names(text, INVALID_COMMAND)
53
+ return unless new_name
54
+ return report(WRITE_PROTECT_ON) unless @storage.respond_to?(:copy)
55
+ return report(FILE_NOT_FOUND) if old_names.any? { |name| name.match?(OTHER_DRIVE) }
56
+
57
+ names = old_names.map { |name| Storage.strip_drive_prefix(name) }
58
+ in_place = old_names.first.match?(DRIVE_ZERO) && names.first == new_name
59
+ writing { in_place ? @storage.concat(new_name, names.drop(1)) : @storage.copy(new_name, names) }
60
+ end
61
+
62
+ # The new name and the old ones of "new=old,...", or nil once it
63
+ # has reported what's wrong.
64
+ def file_names(text, missing_colon)
65
+ colon = text.index(":")
66
+ return report(missing_colon) unless colon
67
+
68
+ new_name, rest = text[(colon + 1)..].split("=", 2)
69
+ new_name = new_name.to_s
70
+ return report(SYNTAX_ERROR) if rest.nil? || new_name.include?(",")
71
+
72
+ old_names = rest.empty? ? [""] : rest.split(",", -1)
73
+ return report(SYNTAX_ERROR) if [new_name, *old_names].any? { |name| name.match?(WILDCARD) }
74
+
75
+ [new_name, old_names]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -15,8 +15,11 @@ module Badline
15
15
  28 => "WRITE ERROR",
16
16
  29 => "DISK ID MISMATCH",
17
17
  30 => "SYNTAX ERROR",
18
+ 31 => "SYNTAX ERROR",
19
+ 34 => "SYNTAX ERROR",
18
20
  62 => "FILE NOT FOUND",
19
21
  63 => "FILE EXISTS",
22
+ 64 => "FILE TYPE MISMATCH",
20
23
  65 => "NO BLOCK",
21
24
  66 => "ILLEGAL TRACK OR SECTOR",
22
25
  70 => "NO CHANNEL",
@@ -3,6 +3,7 @@
3
3
  require "badline/kernal_trap/dos/block_commands"
4
4
  require "badline/kernal_trap/dos/channel"
5
5
  require "badline/kernal_trap/dos/channels"
6
+ require "badline/kernal_trap/dos/disk_commands"
6
7
  require "badline/kernal_trap/dos/memory"
7
8
  require "badline/kernal_trap/dos/parameters"
8
9
  require "badline/kernal_trap/dos/status"
@@ -15,10 +16,12 @@ module Badline
15
16
  # command channel and its status message. `U1` reads a raw block into a
16
17
  # buffer channel, which is how block-access loaders bypass the KERNAL's
17
18
  # LOAD. A disk image whose host file takes writes takes files, blocks
18
- # and BAM changes too. Other disks are write-protected, though storage
19
- # that can write files still takes a SAVE handed over whole.
19
+ # and BAM changes too, and N, R and C format it, rename and copy files.
20
+ # Other disks are write-protected, though storage that can write files
21
+ # still takes a SAVE handed over whole.
20
22
  class DOS
21
23
  include BlockCommands
24
+ include DiskCommands
22
25
  include Writes
23
26
 
24
27
  COMMAND_CHANNEL = 15
@@ -28,6 +31,8 @@ module Badline
28
31
  FILES_SCRATCHED = 1
29
32
  WRITE_PROTECT_ON = 26
30
33
  SYNTAX_ERROR = 30
34
+ INVALID_COMMAND = 31
35
+ NO_FILE_GIVEN = 34
31
36
  FILE_NOT_FOUND = 62
32
37
  FILE_EXISTS = 63
33
38
  NO_BLOCK = 65
@@ -60,11 +65,15 @@ module Badline
60
65
  /\AV/i => :initialized,
61
66
  /\AB-A\s*:?\s*(.*)/i => :block_allocate,
62
67
  /\AB-F\s*:?\s*(.*)/i => :block_free,
63
- /\AS[^:]*:(.*)/im => :scratch
68
+ /\AS[^:]*:(.*)/im => :scratch,
69
+ /\AN(.*)/im => :new_disk,
70
+ /\AR(.*)/im => :rename_file,
71
+ /\AC(.*)/im => :copy_file,
72
+ /\AD/i => :invalid
64
73
  }.freeze
65
74
 
66
75
  # Commands that take their argument as text rather than numbers.
67
- TEXT_COMMANDS = %i[scratch].freeze
76
+ TEXT_COMMANDS = %i[scratch new_disk rename_file copy_file].freeze
68
77
 
69
78
  # A drive powers on reporting its DOS version, as a reset does. It
70
79
  # can take the RAM of a drive it replaces, and initializes its disk.
@@ -197,7 +206,7 @@ module Badline
197
206
  return report(SYNTAX_ERROR) unless action
198
207
 
199
208
  action = USER_TABLE[(command.getbyte(1) - 1) & 0x0f] if action == :user
200
- argument = pattern.match(command)[1]
209
+ argument = pattern.match(action == :new_disk ? text.lstrip : command)[1]
201
210
  run_command(action, TEXT_COMMANDS.include?(action) ? argument : Parameters.parse(argument))
202
211
  end
203
212
 
@@ -218,6 +227,10 @@ module Badline
218
227
  when :block_allocate then block_allocate(arguments)
219
228
  when :block_free then block_free(arguments)
220
229
  when :scratch then scratch(arguments)
230
+ when :new_disk then new_disk(arguments)
231
+ when :rename_file then rename_file(arguments)
232
+ when :copy_file then copy_file(arguments)
233
+ when :invalid then report(INVALID_COMMAND)
221
234
  when :cold_reset then reset(cold: true)
222
235
  when :warm_reset then reset(cold: false)
223
236
  when :memory_write then memory_write(arguments)
@@ -54,7 +54,9 @@ module Badline
54
54
  address_device if @listening
55
55
  end
56
56
 
57
- def addressed? = @bus.peek(zero_page.device) == @device
57
+ # The device TALK or LISTEN addresses: the one in A or in FA, as the
58
+ # layout says.
59
+ def addressed? = (@layout.device_in_a ? @cpu.a : @bus.peek(zero_page.device)) == @device
58
60
 
59
61
  # TALK and LISTEN end with the command sent under ATN, which stays
60
62
  # low, and A holding $FF.
@@ -116,7 +118,7 @@ module Badline
116
118
  def acptr
117
119
  return unless @talking
118
120
 
119
- @layout.listen_to_talker(@bus)
121
+ @layout.byte_received(@bus)
120
122
  byte, status = received
121
123
  time_serial_byte(Routine::ACPTR_TIMEOUT)
122
124
  @cpu.a = byte