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
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model2031
6
+ # VIA 1's side of the IEEE-488 bus (Drives::IEEEBus), through a 75160
7
+ # for the data lines and a 75161 for the control lines (schematic
8
+ # 2031038, sheet 2):
9
+ #
10
+ # PA0-7 DIO1-8 PB4 T/R, high to talk
11
+ # PB0 ATN acknowledge (ATNA) PB5 HD SEL, unused
12
+ # PB1 NRFD PB6 DAV
13
+ # PB2 NDAC PB7 ATN in, high while ATN is low
14
+ # PB3 EOI CA1 ATN in, as PB7
15
+ #
16
+ # The ports read and drive the lines' levels, 0 for a low line. T/R
17
+ # turns the transceivers: talking, the drive drives the data lines,
18
+ # DAV and EOI, and reads NRFD and NDAC; listening, the other way round.
19
+ # EOI turns back to the bus while ATN is low. A pin the transceiver
20
+ # drives the bus from reads the drive's own side, high but for what
21
+ # the VIA or a jumper pulls low.
22
+ #
23
+ # An XOR of ATN in and ATNA drives two 7406s that pull NRFD and NDAC
24
+ # low on the bus, so the drive answers ATN before the DOS gets to it,
25
+ # and holds the bus until the DOS acknowledges. ATNA reaches the XOR
26
+ # only while PB0 is an output, and is low there otherwise, which
27
+ # leaves the bus alone while the DOS reads the jumpers.
28
+ #
29
+ # The device number jumpers are diodes from PB0 and PB1 to CA2: with
30
+ # CA2 low, a fitted diode pulls its line low, and the DOS reads the
31
+ # device number off the two at reset, 8 with both fitted. Lifting the
32
+ # one on PB0 adds 1, and the one on PB1 2.
33
+ #
34
+ # The drive takes the controller's lines a host cycle late, as the
35
+ # serial drives do (Engine::SerialLines): latch_host takes them at the
36
+ # end of each host cycle.
37
+ class IEEEPort
38
+ ATNA = 0x01
39
+ NRFD = 0x02
40
+ NDAC = 0x04
41
+ EOI = 0x08
42
+ TALK = 0x10
43
+ DAV = 0x40
44
+ ATN_IN = 0x80
45
+
46
+ attr_reader :bus
47
+
48
+ # The lines the controller pulled, as latch_host took them.
49
+ attr_reader :host
50
+
51
+ def initialize(device:, bus:)
52
+ raise ArgumentError, "a 2031 is device 8 to 11, not #{device}" unless device.between?(8, 11)
53
+
54
+ @diodes = ~(device - 8) & 0x03
55
+ @via = nil
56
+ @host = 0
57
+ self.bus = bus
58
+ end
59
+
60
+ # The drive's VIA 1, whose outputs it drives the bus with.
61
+ attr_writer :via
62
+
63
+ def bus=(bus)
64
+ @bus = bus
65
+ latch_host
66
+ end
67
+
68
+ # Takes the controller's lines for the drive cycles of the next host
69
+ # cycle.
70
+ def latch_host
71
+ @host = @bus.host_lines
72
+ end
73
+
74
+ # Whether the controller has moved ATN since the last latch_host.
75
+ def atn_moved? = (@bus.host_lines ^ @host).anybits?(IEEEBus::ATN)
76
+
77
+ def atn_low? = @host.anybits?(IEEEBus::ATN)
78
+
79
+ # The data lines, while the transceiver listens.
80
+ def read_a(_lines) = talking? ? 0xff : ~@bus.low_lines(@host) & 0xff
81
+
82
+ # The control lines the transceiver turns towards the drive, and
83
+ # ATN.
84
+ def read_b(_lines)
85
+ low = @bus.low_lines(@host)
86
+ value = 0x7f
87
+ if talking?
88
+ value &= ~NRFD if low.anybits?(IEEEBus::NRFD)
89
+ value &= ~NDAC if low.anybits?(IEEEBus::NDAC)
90
+ value &= ~EOI if low.anybits?(IEEEBus::EOI) && low.anybits?(IEEEBus::ATN)
91
+ else
92
+ value &= ~EOI if low.anybits?(IEEEBus::EOI)
93
+ value &= ~DAV if low.anybits?(IEEEBus::DAV)
94
+ end
95
+ value |= ATN_IN if low.anybits?(IEEEBus::ATN)
96
+ value & jumpers
97
+ end
98
+
99
+ # The lines the drive pulls low while ATN is low or not, as bus bits.
100
+ def ieee_low_lines(atn)
101
+ via = @via
102
+ lines = via.port_b_output & jumpers
103
+ low = 0
104
+ if lines.anybits?(TALK)
105
+ low |= ~via.port_a_output & IEEEBus::DIO
106
+ low |= IEEEBus::DAV if lines.nobits?(DAV)
107
+ low |= IEEEBus::EOI if lines.nobits?(EOI) && !atn
108
+ else
109
+ low |= IEEEBus::NRFD if lines.nobits?(NRFD)
110
+ low |= IEEEBus::NDAC if lines.nobits?(NDAC)
111
+ end
112
+ low |= IEEEBus::NRFD | IEEEBus::NDAC if atn != (lines & via.port_b_directions).anybits?(ATNA)
113
+ low
114
+ end
115
+
116
+ # The controller's lines as the port latched them, under `prefix`.
117
+ def save_fields(out, prefix) = out.int("#{prefix}host", @host)
118
+
119
+ def load_fields(input, prefix)
120
+ @host = input.int("#{prefix}host", @host)
121
+ end
122
+
123
+ private
124
+
125
+ def talking? = @via.port_b_output.anybits?(TALK)
126
+
127
+ # PB0 and PB1 as the jumper diodes leave them: pulled low through
128
+ # each fitted one while CA2 is low.
129
+ def jumpers = @via.ca2_output ? 0xff : ~@diodes & 0xff
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model2031
6
+ # The drive's own fields in a keyed snapshot module (BL-D8): its
7
+ # model, its clock phase and cycle count, the SO pulse due, and the
8
+ # controller's lines as the IEEE-488 port latched them under ieee.*. A
9
+ # save settles the drive first, so it is awake; a load forgets its
10
+ # orbits.
11
+ module KeyedState
12
+ def save_fields(out)
13
+ settle!
14
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles)
15
+ out.bool("so_pending", @mechanism.so_pending)
16
+ @serial_port.save_fields(out, "ieee.")
17
+ out
18
+ end
19
+
20
+ def load_fields(input)
21
+ settle!
22
+ input.choice("model", [model_name], model_name)
23
+ @phase = input.int("phase", @phase)
24
+ @cycles = input.int("cycles", @cycles)
25
+ @mechanism.so_pending = input.bool("so_pending", @mechanism.so_pending)
26
+ @serial_port.load_fields(input, "ieee.")
27
+ end
28
+
29
+ # The drive as modules of its own, named from `name` (BL-D8 for
30
+ # device 8): its own fields, its RAM, each VIA, the CPU, the
31
+ # mechanism and the disk, when one is in, as the 1541's.
32
+ def save_modules(sections, name)
33
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
34
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
35
+ sections << Snapshot::Keyed.section("#{name}-VIA1") { |out| @via1.save_fields(out) }
36
+ sections << Snapshot::Keyed.section("#{name}-VIA2") { |out| @via2.save_fields(out) }
37
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
38
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
39
+ disk = @mechanism.disk
40
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
41
+ sections
42
+ end
43
+
44
+ # The drive from its modules, `input` being its own. No disk module
45
+ # means no disk in the drive.
46
+ def load_modules(modules, input, name)
47
+ load_fields(input)
48
+ @bus.load_fields(modules.required("#{name}-RAM"))
49
+ @via1.load_fields(modules.required("#{name}-VIA1"))
50
+ @via2.load_fields(modules.required("#{name}-VIA2"))
51
+ @cpu.load_fields(modules.required("#{name}-CPU"))
52
+ disk = modules.optional("#{name}-DISK")
53
+ mechanism = modules.required("#{name}-MECH")
54
+ @mechanism.load_fields(mechanism,
55
+ disk.nil? ? nil : Model1541::Disk.from_fields(disk, detached: modules.detached?))
56
+ @via1.restored!
57
+ @via2.restored!
58
+ @cpu.restored!
59
+ forget_orbits
60
+ end
61
+ end
62
+
63
+ include KeyedState
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model2031
6
+ # VIA 1, facing the IEEE-488 bus (IEEEPort), a class of its own so
7
+ # its peripheral keeps one type, apart from the serial drives'.
8
+ class IEEEVIA < Chips::VIA
9
+ # DDRB, whose PB0 says whether the VIA drives ATNA (IEEEPort).
10
+ def port_b_directions = @ddrb
11
+ end
12
+
13
+ # The drive CPU's bus, decoded as the 1541's (Model1541::Bus), with
14
+ # VIA 1 an IEEEVIA.
15
+ class Bus < Model1541::Bus
16
+ end
17
+
18
+ # The drive's 6502, on the drive's Bus.
19
+ class CPU
20
+ include Badline::Chips::CPU::Core
21
+ include Engine::IdleCPU
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model1541"
4
+ require "badline/drives/ieee_bus"
5
+ require "badline/drives/model2031/parts"
6
+ require "badline/drives/model2031/ieee_port"
7
+ require "badline/drives/model2031/keyed_state"
8
+
9
+ module Badline
10
+ module Drives
11
+ # The 2031 disk drive of the PET and CBM: the 1541's board with an
12
+ # IEEE-488 interface in place of the serial one (Commodore schematic
13
+ # 2031038). A 6502 at 1 MHz runs DOS 2.6 from 16 KB of ROM, with 2 KB
14
+ # of RAM, VIA 1 at $1800 on the IEEE-488 bus (IEEEPort) and VIA 2 at
15
+ # $1C00 running the 1541's GCR Mechanism, decoded as the 1541's.
16
+ #
17
+ # Drives::Engine::Core clocks it against the host and sleeps through
18
+ # the DOS's idle loop, as the 1541's, until ATN moves or a timer is due
19
+ # (Engine::VIAs). The bus reads the lines it pulls without waking it.
20
+ class Model2031
21
+ include Engine::Core
22
+ include Engine::VIAs
23
+
24
+ CLOCK_HZ = 1_000_000
25
+
26
+ # Where the DOS's idle loop starts over (see Drives::Engine::Idle).
27
+ IDLE_LOOP = 0xec4d
28
+
29
+ # The host cycles by which a 2031 switched on with the machine has run
30
+ # its DOS's RAM test and ROM checksums and reads its jumpers.
31
+ BOOT_CYCLES = 1_300_000
32
+
33
+ attr_reader :ieee_bus
34
+
35
+ def model_name = "2031"
36
+
37
+ # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
38
+ # path. +device+ is the number the jumpers set, 8 to 11.
39
+ def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
40
+ @device = device
41
+ @ieee_bus = IEEEBus.new
42
+ @serial_port = IEEEPort.new(device:, bus: @ieee_bus)
43
+ @via1 = IEEEVIA.new(start: 0x1800, peripheral: @serial_port)
44
+ @serial_port.via = @via1
45
+ @mechanism = Model1541::Mechanism.new
46
+ @via2 = Model1541::DiskVIA.new(start: 0x1c00, mechanism: @mechanism)
47
+ @bus = Bus.new(rom: rom || ROM.load("dos2031.rom", 0xc000), via1: @via1, via2: @via2)
48
+ @cpu = CPU.new(@bus, debug:)
49
+ @clock_hz = CLOCK_HZ
50
+ @idle_loop = IDLE_LOOP
51
+ @host_clock_hz = host_clock_hz
52
+ @phase = 0
53
+ @cycles = 0
54
+ init_idle(debug)
55
+ @via1.ca1 = false
56
+ @via1.reset!
57
+ connect(@ieee_bus)
58
+ end
59
+
60
+ # Plugs the drive into an IEEE-488 bus, leaving the one it was on.
61
+ def connect(ieee_bus)
62
+ settle!
63
+ @ieee_bus.detach(self)
64
+ @ieee_bus = ieee_bus
65
+ @serial_port.bus = ieee_bus
66
+ ieee_bus.attach(self)
67
+ hear_atn
68
+ end
69
+
70
+ # The lines the drive pulls on the bus while ATN is low or not
71
+ # (IEEEPort). They hold still while it sleeps, so reading them leaves
72
+ # it asleep.
73
+ def ieee_low_lines(atn) = @serial_port.ieee_low_lines(atn)
74
+
75
+ private
76
+
77
+ # One drive cycle, as the 1541's (Model1541#step): the mechanism, then
78
+ # ATN on VIA 1's CA1, the VIAs and the CPU.
79
+ def step
80
+ so = @mechanism.take_so
81
+ @mechanism.cycle!
82
+ @via1.ca1 = @serial_port.atn_low?
83
+ @via1.cycle!
84
+ @via2.cycle!
85
+ @cpu.irq = @via1.irq? || @via2.irq?
86
+ @cpu.so! if so
87
+ @cpu.cycle!
88
+ @cycles += 1
89
+ end
90
+ end
91
+ end
92
+ end
@@ -4,5 +4,6 @@
4
4
  # 1571 stays out of the core: badline/c128 requires it.
5
5
  require "badline/drives/datasette"
6
6
  require "badline/drives/serial_bus"
7
+ require "badline/drives/ieee_bus"
7
8
  require "badline/drives/model1541"
8
9
  require "badline/drives/model1581"
@@ -112,7 +112,7 @@ module Badline
112
112
  end
113
113
 
114
114
  def handle_key(scancode, down)
115
- if [Keys::TAB, Keys::F8, Keys::F9, Keys::F10].include?(scancode)
115
+ if [Keys::F8, Keys::F9, Keys::F10].include?(scancode) || (scancode == Keys::TAB && @controls.tab_toggles?)
116
116
  handle_toggle(scancode) if down
117
117
  elsif down && @snapshots.key(scancode)
118
118
  swap(@snapshots.computer)
@@ -7,7 +7,9 @@ module Badline
7
7
  # - The keys: every host key goes to the machine's keyboard, or, as Tab
8
8
  # switches, the arrow cluster and space drive one joystick and WASD
9
9
  # and left shift the other. The arrows start on joystick 2, and
10
- # #swap_ports swaps them. The VIC-20's one joystick takes both.
10
+ # #swap_ports swaps them. The VIC-20's one joystick takes both. The
11
+ # PET has no joystick, and its keys go by the character they type
12
+ # (PETKeyboard).
11
13
  # - A pot device the pause menu plugs into a C64 control port: a 1351
12
14
  # mouse or a pair of paddles, which the host mouse drives. Motion
13
15
  # turns the paddles or moves the mouse, and the host's left and right
@@ -30,6 +32,7 @@ module Badline
30
32
  @mouse = Input::Mouse1351.new
31
33
  @paddles = Input::Paddles.new
32
34
  @held = {}
35
+ @pet_keyboard = pet_keyboard(computer)
33
36
  end
34
37
 
35
38
  # Routes the input to another machine, the pot device going into its
@@ -37,6 +40,7 @@ module Badline
37
40
  def computer=(computer)
38
41
  release_all
39
42
  @computer = computer
43
+ @pet_keyboard = pet_keyboard(computer)
40
44
  attach_pot_device
41
45
  end
42
46
 
@@ -48,10 +52,16 @@ module Badline
48
52
  release_all
49
53
  end
50
54
 
55
+ # Switches between the keyboard and the joysticks, on a machine with
56
+ # joysticks.
51
57
  def toggle_keys
52
- self.joystick_mode = !@joystick
58
+ self.joystick_mode = !@joystick if tab_toggles?
53
59
  end
54
60
 
61
+ # Whether Tab switches the keys to the joysticks, or on the PET, which
62
+ # has none, is a key of its own.
63
+ def tab_toggles? = @computer.family != :pet
64
+
55
65
  # Plugs in one of POTS, in place of the one before.
56
66
  def plug(pot)
57
67
  @pot = pot
@@ -106,6 +116,8 @@ module Badline
106
116
  # A host key lets go of the key it pressed, though the C128 has
107
117
  # switched between C64 and C128 mode since and maps it elsewhere.
108
118
  def machine_key(scancode, down)
119
+ return @pet_keyboard.key(scancode, down) if @computer.family == :pet
120
+
109
121
  @held[scancode] = host_key(scancode) if down || !@held.key?(scancode)
110
122
  key = @held[scancode]
111
123
  @held.delete(scancode) unless down
@@ -147,7 +159,7 @@ module Badline
147
159
  # A fresh pot device on its port, and none on the other. A machine
148
160
  # without the ports takes none.
149
161
  def attach_pot_device
150
- ports = @computer.control_ports
162
+ ports = @computer.family == :pet ? nil : @computer.control_ports
151
163
  if ports.nil?
152
164
  @pot = :none
153
165
  return
@@ -199,7 +211,15 @@ module Badline
199
211
  down ? joystick.press(direction) : joystick.release(direction)
200
212
  end
201
213
 
214
+ # The PET's keyboard for a PET, and for any other machine one it never
215
+ # uses.
216
+ def pet_keyboard(computer)
217
+ PETKeyboard.new(computer.keyboard, computer.family == :pet ? computer.model.keyboard : :graphics)
218
+ end
219
+
202
220
  def release_all
221
+ return @pet_keyboard.release_all if @computer.family == :pet
222
+
203
223
  Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
204
224
  Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
205
225
  Keys::ARROWS.each_value do |direction|
@@ -7,14 +7,15 @@ module Badline
7
7
  # second one joystick 1. PadPort holds the mapping.
8
8
  #
9
9
  # The controllers are polled every frame, and rescanned whenever SDL
10
- # reports one added or removed.
10
+ # reports one added or removed. The PET has no joysticks, and the
11
+ # controllers steer two that nothing reads.
11
12
  class Gamepads
12
13
  PORTS = [2, 1].freeze
13
14
 
14
15
  def initialize(computer, verbose)
15
16
  @verbose = verbose
16
17
  @controllers = []
17
- @ports = [PadPort.new(computer.joystick2), PadPort.new(computer.joystick1)]
18
+ @ports = [PadPort.new(joystick(computer, 2)), PadPort.new(joystick(computer, 1))]
18
19
  @buttons = Array.new(PadPort::BUTTON_COUNT, 0)
19
20
  @available = SDL.SDL_InitSubSystem(SDL::INIT_GAMECONTROLLER).zero?
20
21
  puts "No game controllers: #{SDL.SDL_GetError}" if @verbose && !@available
@@ -24,7 +25,7 @@ module Badline
24
25
  # what they hold on the next poll.
25
26
  def computer=(computer)
26
27
  @ports.each(&:release)
27
- @ports = [PadPort.new(computer.joystick2), PadPort.new(computer.joystick1)]
28
+ @ports = [PadPort.new(joystick(computer, 2)), PadPort.new(joystick(computer, 1))]
28
29
  end
29
30
 
30
31
  def rescan
@@ -51,6 +52,12 @@ module Badline
51
52
 
52
53
  private
53
54
 
55
+ def joystick(computer, port)
56
+ return Input::Joystick.new if computer.family == :pet
57
+
58
+ port == 1 ? computer.joystick1 : computer.joystick2
59
+ end
60
+
54
61
  def open_controller(index)
55
62
  controller = SDL.SDL_GameControllerOpen(index)
56
63
  # SDL returns NULL when the controller won't open, which Spinel's FFI
@@ -65,6 +65,59 @@ module Badline
65
65
  79 => :crsr_right, 80 => :crsr_left, 81 => :crsr_down, 82 => :crsr_up, 229 => :lshift, 75 => :none
66
66
  }.freeze
67
67
 
68
+ # The characters the keys other than the letters type on a US
69
+ # layout, unshifted and then shifted, and the keypad's.
70
+ US_CHARACTERS = {
71
+ 30 => "1!", 31 => "2@", 32 => "3#", 33 => "4$", 34 => "5%", 35 => "6^", 36 => "7&", 37 => "8*", 38 => "9(",
72
+ 39 => "0)", 45 => "-_", 46 => "=+", 47 => "[{", 48 => "]}", 49 => "\\|", 51 => ";:", 52 => "'\"", 53 => "`~",
73
+ 54 => ",<", 55 => ".>", 56 => "/?", 84 => "//", 85 => "**", 86 => "--", 87 => "++", 89 => "11", 90 => "22",
74
+ 91 => "33", 92 => "44", 93 => "55", 94 => "66", 95 => "77", 96 => "88", 97 => "99", 98 => "00", 99 => "..",
75
+ 103 => "=="
76
+ }.freeze
77
+
78
+ # The PET's keys for the characters a host key types, on either
79
+ # keyboard without SHIFT: each has a key of its own on the graphics
80
+ # keyboard, where SHIFT turns it into a graphics character. ↑ and ←
81
+ # are on ^, and on _ and `.
82
+ PET_GRAPHICS_CHARACTERS = {
83
+ "!" => :!, "\"" => :'"', "#" => :"#", "$" => :"$", "%" => :%, "&" => :&, "'" => :"'", "(" => :"(",
84
+ ")" => :")", "\\" => :"\\", "^" => :up, "_" => :left, "`" => :left, "/" => :/, ":" => :":", "*" => :*,
85
+ ";" => :";", "?" => :"?", "," => :",", "+" => :+, "@" => :"@", "[" => :"[", "]" => :"]", "<" => :<,
86
+ ">" => :>, "-" => :-, "=" => :"=", "." => :".", "1" => :"1", "2" => :"2", "3" => :"3", "4" => :"4",
87
+ "5" => :"5", "6" => :"6", "7" => :"7", "8" => :"8", "9" => :"9", "0" => :"0"
88
+ }.freeze
89
+
90
+ # The business keyboard's: the characters it types without SHIFT,
91
+ # then those it types with it.
92
+ PET_BUSINESS_CHARACTERS = {
93
+ "1" => :"1", "2" => :"2", "3" => :"3", "4" => :"4", "5" => :"5", "6" => :"6", "7" => :"7", "8" => :"8",
94
+ "9" => :"9", "0" => :"0", "-" => :-, ":" => :":", ";" => :";", "," => :",", "." => :".", "/" => :/,
95
+ "@" => :"@", "[" => :"[", "]" => :"]", "\\" => :"\\", "^" => :up, "_" => :left, "`" => :left
96
+ }.freeze
97
+
98
+ PET_BUSINESS_SHIFTED = {
99
+ "!" => :"1", "\"" => :"2", "#" => :"3", "$" => :"4", "%" => :"5", "&" => :"6", "'" => :"7", "(" => :"8",
100
+ ")" => :"9", "=" => :-, "*" => :":", "+" => :";", "<" => :",", ">" => :".", "?" => :/
101
+ }.freeze
102
+
103
+ # The PET's keys that don't type a character, which keep the host's
104
+ # SHIFT: RETURN, INST/DEL on Backspace, CLR/HOME on Home, RUN/STOP on
105
+ # Page Down, and RVS on Control. Esc is RUN/STOP on the graphics
106
+ # keyboard. The business keyboard's ESC, TAB, REPEAT and keypad are
107
+ # on Esc, Tab, Page Up and the keypad.
108
+ PET_KEYS = {
109
+ 40 => :return, 88 => :return, 42 => :delete, 44 => :space, 74 => :clr_home, 78 => :run_stop, 224 => :rvs,
110
+ 228 => :rvs
111
+ }.freeze
112
+
113
+ PET_BUSINESS_KEYS = {
114
+ 41 => :escape, 43 => :tab, 75 => :repeat, 89 => :kp1, 90 => :kp2, 91 => :kp3, 92 => :kp4, 93 => :kp5,
115
+ 94 => :kp6, 95 => :kp7, 96 => :kp8, 97 => :kp9, 98 => :kp0, 99 => :kp_period
116
+ }.freeze
117
+
118
+ # The cursor keys, CRSR ⇔ and CRSR ⇕, each with SHIFT for left or up.
119
+ PET_CURSORS = { RIGHT => :cursor_h, LEFT => :cursor_h, DOWN => :cursor_v, UP => :cursor_v }.freeze
120
+
68
121
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
69
122
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
70
123
 
@@ -89,6 +142,41 @@ module Badline
89
142
  # The 264's key for the scancode: one of PLUS4_OTHERS or the C64's.
90
143
  def self.plus4_key(scancode) = PLUS4_OTHERS[scancode] || c64_key(scancode)
91
144
 
145
+ # The PET key for the scancode on the +keyboard+, :graphics or
146
+ # :business, with the host's SHIFT down if +shift+, and how the
147
+ # PET's SHIFT goes with it: :on or :off for a key that types the host
148
+ # key's character or moves the cursor its way only so, and :host to
149
+ # follow the host's. A letter is the PET's letter, and any other key
150
+ # the PET key that types the same character, or :none.
151
+ def self.pet_key(scancode, shift, keyboard)
152
+ return [LETTERS[scancode - 4], :host] if scancode.between?(4, 29)
153
+
154
+ cursor = PET_CURSORS[scancode]
155
+ return [cursor, [LEFT, UP].include?(scancode) ? :on : :off] unless cursor.nil?
156
+
157
+ business = keyboard == :business
158
+ key = business ? PET_BUSINESS_KEYS[scancode] : nil
159
+ key = PET_KEYS[scancode] if key.nil?
160
+ key = :run_stop if key.nil? && scancode == ESCAPE
161
+ return [key, :host] unless key.nil?
162
+
163
+ pet_character(scancode, shift, business)
164
+ end
165
+
166
+ # The PET key that types the character the scancode types on a US
167
+ # layout, and the SHIFT it takes.
168
+ def self.pet_character(scancode, shift, business)
169
+ characters = US_CHARACTERS[scancode]
170
+ return %i[none host] if characters.nil?
171
+
172
+ character = characters[shift ? 1 : 0]
173
+ unshifted = business ? PET_BUSINESS_CHARACTERS[character] : PET_GRAPHICS_CHARACTERS[character]
174
+ return [unshifted, :off] unless unshifted.nil?
175
+
176
+ shifted = business ? PET_BUSINESS_SHIFTED[character] : nil
177
+ shifted.nil? ? %i[none host] : [shifted, :on]
178
+ end
179
+
92
180
  # The character the key types on a US layout: a letter, a capital
93
181
  # with `shift`, or a digit, and "" for any other key.
94
182
  def self.character(scancode, shift)
@@ -35,6 +35,7 @@ module Badline
35
35
  # either cartridge's slots.
36
36
  def cartridge?(computer)
37
37
  return !computer.cartridge_slots.empty? if computer.family == :plus4
38
+ return false if computer.family == :pet
38
39
 
39
40
  !computer.address_bus.cartridge.nil?
40
41
  end
@@ -39,6 +39,7 @@ module Badline
39
39
  @browsing = kind
40
40
  extensions = case kind
41
41
  when :cartridge then @media.cartridge_extensions
42
+ when :disk then @media.disk_extensions
42
43
  when :program then programs
43
44
  else EXTENSIONS.fetch(kind)
44
45
  end
@@ -64,7 +65,7 @@ module Badline
64
65
  # starts, and says whether it asks. Other files are left alone.
65
66
  def drop(path)
66
67
  extension = File.extname(path).downcase
67
- return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
68
+ return @media.insert(:disk, path) && false if @media.disk_extensions.include?(extension)
68
69
  return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
69
70
  return false unless programs.include?(extension)
70
71