badline 0.15.1 → 0.16.1

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -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,21 @@ 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, the 8096's 64K board, and the 8296's
160
+ # 128K with the jumpers that put RAM under its ROMs.
161
+ def draw_pet_memory
162
+ model = @computer.model
163
+ return draw_8296_memory if model.cbm8296?
164
+
165
+ info("RAM", "#{@computer.ram_kb}K")
166
+ info("EXPANSION RAM", model.expansion ? "64K BOARD" : "")
167
+ end
168
+
169
+ def draw_8296_memory
170
+ info("RAM", "128K, 64K OF IT BANKED")
171
+ info("JUMPERS", @computer.jumpers.join(", ").upcase)
172
+ end
173
+
154
174
  # Saving now and by name, loading a named save, and the quicksaves
155
175
  # and autosaves, newest first, which a press loads.
156
176
  def draw_snapshots
@@ -166,10 +186,12 @@ module Badline
166
186
  end
167
187
  end
168
188
 
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.
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.
171
192
  def draw_power
172
193
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
194
+ info("MACHINE", @computer.model_label) if pet?
173
195
  if plus4?
174
196
  info("MACHINE", @computer.model_label)
175
197
  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
@@ -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
@@ -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
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # The frames a drive takes and sends through the traps on a KERNAL's
6
+ # eight bus primitives, shared by the serial bus's (Serial) and the
7
+ # IEEE-488 bus's (IEEE): the traps going in and out, the bytes a
8
+ # listener gathers into a frame for the DOS, the bytes a talker sends,
9
+ # and the status they leave. The class that includes it, with Steps,
10
+ # answers each primitive and keeps @dos, @listening, @talking,
11
+ # @listen_channel, @talk_channel, @frame and @buffer.
12
+ module Frames
13
+ # Frame type in the high nibble of the secondary address
14
+ OPEN = 0xf0
15
+ CLOSE = 0xe0
16
+
17
+ # ST bits
18
+ DEVICE_NOT_PRESENT = 0x80
19
+ EOI = 0x40
20
+ READ_TIMEOUT = 0x02
21
+
22
+ NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
23
+
24
+ def install
25
+ routines.each do |address, routine|
26
+ @cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
27
+ end
28
+ self
29
+ end
30
+
31
+ # Takes the traps out again, so the KERNAL's own routines run.
32
+ def uninstall
33
+ routines.each_key { |address| @cpu.remove_trap(address) }
34
+ end
35
+
36
+ private
37
+
38
+ def routines
39
+ {
40
+ @layout.talk => :talk,
41
+ @layout.listen => :listen,
42
+ @layout.second => :second,
43
+ @layout.tksa => :tksa,
44
+ @layout.ciout => :ciout,
45
+ @layout.untalk => :untalk,
46
+ @layout.unlisten => :unlisten,
47
+ @layout.acptr => :acptr
48
+ }
49
+ end
50
+
51
+ def call_routine(routine)
52
+ case routine
53
+ when :talk then talk
54
+ when :listen then listen
55
+ when :second then second
56
+ when :tksa then tksa
57
+ when :ciout then ciout
58
+ when :untalk then untalk
59
+ when :unlisten then unlisten
60
+ when :acptr then acptr
61
+ end
62
+ end
63
+
64
+ # CIOUT holds each byte back until the next one, or the UNLISTEN,
65
+ # comes, so the first byte of a frame sends nothing.
66
+ def ciout
67
+ return unless @listening
68
+
69
+ time_serial_byte(Routine::ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
70
+ @buffer << @cpu.a
71
+ update_status(DEVICE_NOT_PRESENT) unless accepted?
72
+ @cpu.status.carry = false
73
+ return_to_caller
74
+ end
75
+
76
+ # The drive acknowledges every byte of an open or close frame, but
77
+ # takes data only on a channel it has open. Otherwise nothing holds
78
+ # the data line when a byte starts, so the KERNAL finds no device.
79
+ def accepted?
80
+ [OPEN, CLOSE].include?(@frame) || @dos.listening?(@listen_channel)
81
+ end
82
+
83
+ # UNLSN and UNTLK end by releasing the bus, and leave the port's last
84
+ # read in the accumulator.
85
+ def release_bus
86
+ @cpu.a = @layout.release_serial_lines(@bus)
87
+ @cpu.status.negative = @cpu.a.anybits?(0x80)
88
+ @cpu.status.zero = @cpu.a.zero?
89
+ return_to_caller
90
+ end
91
+
92
+ def deliver_frame
93
+ return unless @listen_channel
94
+
95
+ case @frame
96
+ when OPEN then @dos.open(@listen_channel, @buffer.pack("C*"))
97
+ when CLOSE then @dos.close(@listen_channel)
98
+ else @dos.write(@listen_channel, @buffer)
99
+ end
100
+ @buffer = []
101
+ end
102
+
103
+ def received
104
+ byte, eoi = @dos.read(@talk_channel)
105
+ return NO_DATA unless byte
106
+
107
+ [byte, eoi ? EOI : 0]
108
+ end
109
+
110
+ def update_status(bits)
111
+ return if bits.zero?
112
+
113
+ @bus.poke(zero_page.status, @bus.peek(zero_page.status) | bits)
114
+ end
115
+ end
116
+ end
117
+ end