badline 0.14.0 → 0.15.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +22 -5
  4. data/README.md +101 -11
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +145 -0
  7. data/doc/snapshot-format.md +244 -46
  8. data/doc/snapshots.md +18 -1
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/bare_player.rb +4 -8
  11. data/lib/badline/audio/machine_player.rb +4 -26
  12. data/lib/badline/c128/banks.rb +1 -3
  13. data/lib/badline/c128/bus.rb +19 -1
  14. data/lib/badline/c128/mmu_pages.rb +7 -0
  15. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  16. data/lib/badline/c64/bus.rb +1 -0
  17. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  18. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  19. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  20. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  21. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  22. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  23. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  24. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  25. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  26. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  27. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  28. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  29. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  30. data/lib/badline/c64/cartridge.rb +3 -3
  31. data/lib/badline/c64/keyed_modules.rb +6 -2
  32. data/lib/badline/c64/true_drives.rb +13 -1
  33. data/lib/badline/c64.rb +6 -0
  34. data/lib/badline/chips/ted/beam.rb +13 -14
  35. data/lib/badline/chips/ted/fetches.rb +199 -0
  36. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  37. data/lib/badline/chips/ted/modes.rb +134 -0
  38. data/lib/badline/chips/ted/palette.rb +43 -0
  39. data/lib/badline/chips/ted/picture.rb +182 -0
  40. data/lib/badline/chips/ted/sound/output.rb +86 -0
  41. data/lib/badline/chips/ted/sound.rb +284 -0
  42. data/lib/badline/chips/ted.rb +53 -6
  43. data/lib/badline/chips/tia.rb +28 -0
  44. data/lib/badline/chips/vic/register_log.rb +4 -1
  45. data/lib/badline/drives/datasette/keyed_state.rb +14 -4
  46. data/lib/badline/drives/datasette.rb +22 -5
  47. data/lib/badline/drives/engine/core.rb +6 -14
  48. data/lib/badline/drives/engine/idle.rb +42 -4
  49. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  50. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  51. data/lib/badline/drives/engine/sleep.rb +1 -0
  52. data/lib/badline/drives/engine.rb +1 -0
  53. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  54. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551/bus.rb +138 -0
  57. data/lib/badline/drives/model1551/cable.rb +104 -0
  58. data/lib/badline/drives/model1551/cpu.rb +14 -0
  59. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  60. data/lib/badline/drives/model1551/port.rb +74 -0
  61. data/lib/badline/drives/model1551/tia.rb +92 -0
  62. data/lib/badline/drives/model1551.rb +170 -0
  63. data/lib/badline/drives/model1571.rb +1 -0
  64. data/lib/badline/drives/model1581.rb +1 -0
  65. data/lib/badline/frontend/boot.rb +23 -2
  66. data/lib/badline/frontend/controls.rb +7 -3
  67. data/lib/badline/frontend/keys.rb +14 -2
  68. data/lib/badline/frontend/media_slots.rb +11 -1
  69. data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
  70. data/lib/badline/frontend/menu/menu_media.rb +102 -14
  71. data/lib/badline/frontend/menu/menu_pages.rb +16 -11
  72. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  73. data/lib/badline/frontend/player/player_controller.rb +11 -4
  74. data/lib/badline/frontend/screen.rb +33 -12
  75. data/lib/badline/frontend/screens.rb +2 -1
  76. data/lib/badline/frontend/sound.rb +4 -4
  77. data/lib/badline/frontend/timeline.rb +19 -6
  78. data/lib/badline/machine/settling.rb +1 -1
  79. data/lib/badline/machine.rb +30 -6
  80. data/lib/badline/media/plus4_media.rb +68 -0
  81. data/lib/badline/media/program.rb +20 -6
  82. data/lib/badline/media/true_drive.rb +7 -5
  83. data/lib/badline/media.rb +33 -12
  84. data/lib/badline/options/family.rb +1 -0
  85. data/lib/badline/options/help.rb +32 -6
  86. data/lib/badline/options/option.rb +2 -1
  87. data/lib/badline/options/table.rb +29 -4
  88. data/lib/badline/options/validation.rb +66 -4
  89. data/lib/badline/options.rb +71 -11
  90. data/lib/badline/plus4/acia/frames.rb +55 -0
  91. data/lib/badline/plus4/acia.rb +276 -16
  92. data/lib/badline/plus4/bus.rb +245 -46
  93. data/lib/badline/plus4/cassette_sense.rb +21 -0
  94. data/lib/badline/plus4/keyboard_port.rb +35 -10
  95. data/lib/badline/plus4/keyed_modules.rb +116 -0
  96. data/lib/badline/plus4/keyed_parts.rb +172 -0
  97. data/lib/badline/plus4/keyed_state.rb +44 -0
  98. data/lib/badline/plus4/ram_expansion.rb +145 -0
  99. data/lib/badline/plus4/sid_card.rb +95 -0
  100. data/lib/badline/plus4/sound_mix.rb +51 -0
  101. data/lib/badline/plus4/tcbm_drive.rb +120 -0
  102. data/lib/badline/plus4/user_port.rb +11 -2
  103. data/lib/badline/plus4.rb +184 -26
  104. data/lib/badline/roms/README +9 -0
  105. data/lib/badline/roms/dos1551.rom +0 -0
  106. data/lib/badline/snapshot/container.rb +2 -0
  107. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  108. data/lib/badline/snapshot/field_doc.rb +48 -7
  109. data/lib/badline/snapshot/image.rb +12 -3
  110. data/lib/badline/snapshot/plus4_setup.rb +81 -0
  111. data/lib/badline/snapshot.rb +9 -4
  112. data/lib/badline/storage/tap.rb +21 -12
  113. data/lib/badline/support/checkpoint.rb +3 -2
  114. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  115. data/lib/badline/version.rb +1 -1
  116. metadata +29 -1
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive's 6510T, a 6510 with an 8-bit port (Port), on the drive's
7
+ # Bus.
8
+ class CPU
9
+ include Badline::Chips::CPU::Core
10
+ include Engine::IdleCPU
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive's own fields in a keyed snapshot module (BL-D8): its
7
+ # model, its clock phase, its cycle count, the 555's count to its
8
+ # next pulse and the byte the gate array latched. A save settles the
9
+ # drive first, so it is awake; a load forgets its orbits.
10
+ module KeyedState
11
+ def save_fields(out)
12
+ settle!
13
+ out.text("model", model_name).int("phase", @phase).int("cycles", @cycles).int("irq_timer", @irq_timer)
14
+ out.int("latched_byte", @mechanism.latched_byte)
15
+ end
16
+
17
+ def load_fields(input)
18
+ settle!
19
+ input.choice("model", [model_name], model_name)
20
+ @phase = input.int("phase", @phase)
21
+ @cycles = input.int("cycles", @cycles)
22
+ @irq_timer = input.int("irq_timer", @irq_timer).clamp(1, IRQ_PERIOD)
23
+ @mechanism.latched_byte = input.byte("latched_byte", @mechanism.latched_byte)
24
+ end
25
+
26
+ # The drive as modules of its own, named from `name` (BL-D8 for
27
+ # device 8): its own fields, its RAM, the CPU's port, the drive's
28
+ # TIA, its interface's TIA in the computer, the CPU, the mechanism
29
+ # and the disk, when one is in.
30
+ def save_modules(sections, name)
31
+ sections << Snapshot::Keyed.section(name) { |out| save_fields(out) }
32
+ sections << Snapshot::Keyed.section("#{name}-RAM") { |out| @bus.save_fields(out) }
33
+ sections << Snapshot::Keyed.section("#{name}-PORT") { |out| @port.save_fields(out) }
34
+ sections << Snapshot::Keyed.section("#{name}-TIA") { |out| @tia.save_fields(out) }
35
+ sections << Snapshot::Keyed.section("#{name}-HOSTTIA") { |out| @host_tia.save_fields(out) }
36
+ sections << Snapshot::Keyed.section("#{name}-CPU") { |out| @cpu.save_fields(out) }
37
+ sections << Snapshot::Keyed.section("#{name}-MECH") { |out| @mechanism.save_fields(out) }
38
+ disk = @mechanism.disk
39
+ sections << Snapshot::Keyed.section("#{name}-DISK") { |out| disk.save_fields(out) } if disk
40
+ sections
41
+ end
42
+
43
+ # The drive from its modules, `input` being its own. No disk module
44
+ # means no disk in the drive. The cable's lines and the mechanism's
45
+ # mode follow the TIAs once both are loaded.
46
+ def load_modules(modules, input, name)
47
+ load_fields(input)
48
+ @bus.load_fields(modules.required("#{name}-RAM"))
49
+ @port.load_fields(modules.required("#{name}-PORT"))
50
+ @tia.load_fields(modules.required("#{name}-TIA"))
51
+ @host_tia.load_fields(modules.required("#{name}-HOSTTIA"))
52
+ @cpu.load_fields(modules.required("#{name}-CPU"))
53
+ disk_input = modules.optional("#{name}-DISK")
54
+ disk = disk_input.nil? ? nil : Model1541::Disk.from_fields(disk_input, detached: modules.detached?)
55
+ @mechanism.load_fields(modules.required("#{name}-MECH"), disk)
56
+ @cable.relatch
57
+ @cpu.restored!
58
+ forget_orbits
59
+ end
60
+ end
61
+
62
+ include KeyedState
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The 6510T's port at $00 (direction) and $01 (data), wired to the
7
+ # mechanism in the bits of the 1541's VIA 2 port B (zimmers.net's 1551
8
+ # memory map, schematic 251860):
9
+ #
10
+ # P0-1 stepper phase (out) P4 write protect (in, 1 = writable)
11
+ # P2 motor on (out) P5-6 bit rate, speed zone 0-3 (out)
12
+ # P3 activity LED (out) P7 BYTE LATCHED (in)
13
+ #
14
+ # An output pin reads back what it drives, and a pin left as an input
15
+ # drives nothing into the mechanism.
16
+ class Port
17
+ WRITE_PROTECT = 0x10
18
+ BYTE_LATCHED = 0x80
19
+
20
+ attr_reader :ddr, :data
21
+
22
+ def initialize(mechanism)
23
+ @mechanism = mechanism
24
+ @ddr = 0
25
+ @data = 0
26
+ end
27
+
28
+ def reset!
29
+ @ddr = 0
30
+ @data = 0
31
+ changed
32
+ end
33
+
34
+ def peek(addr)
35
+ return @ddr if addr.zero?
36
+
37
+ ddr = @ddr
38
+ (@data & ddr) | (inputs & ~ddr & 0xff)
39
+ end
40
+
41
+ def poke(addr, value)
42
+ if addr.zero?
43
+ @ddr = value
44
+ else
45
+ @data = value
46
+ end
47
+ changed
48
+ end
49
+
50
+ # The registers, in a keyed snapshot module (BL-D8-PORT).
51
+ def save_fields(out)
52
+ out.int("ddr", @ddr).int("data", @data)
53
+ end
54
+
55
+ def load_fields(input)
56
+ @ddr = input.byte("ddr", @ddr)
57
+ @data = input.byte("data", @data)
58
+ end
59
+
60
+ private
61
+
62
+ def inputs
63
+ mechanism = @mechanism
64
+ value = (mechanism.read_b(0xff) & WRITE_PROTECT) | (0xff & ~(WRITE_PROTECT | BYTE_LATCHED))
65
+ mechanism.byte_latched? ? value | BYTE_LATCHED : value
66
+ end
67
+
68
+ def changed
69
+ @mechanism.port_b_written(@data & @ddr)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Drives
5
+ class Model1551
6
+ # The drive's 6523 TIA at $4000-$7FFF, its eight registers mirrored
7
+ # (zimmers.net's 1551 memory map, schematic 251860), a Chips::TIA
8
+ # with what the drive's side wires to it:
9
+ #
10
+ # - Port A carries the cable's eight data lines, pulled up.
11
+ # - Port B reads the GCR byte the gate array latched at BYTE READY
12
+ # and drives the byte the head writes.
13
+ # - Port C drives STATUS0 and STATUS1 (PC0-1), DEV (PC2), which picks
14
+ # where the computer's TIA answers, ACK (PC3) and the head's MODE
15
+ # (PC4, low to write), and reads the device number jumper (PC5,
16
+ # high for device 9), SYNC (PC6, low in a SYNC mark) and DAV (PC7).
17
+ #
18
+ # The DOS sets port C's directions to $1F. The cable's lines are
19
+ # pulled up, so an input nothing drives reads 1.
20
+ #
21
+ # Any access to the TIA clears BYTE LATCHED.
22
+ class TIA
23
+ DAV = 0x80
24
+ SYNC = 0x40
25
+ JUMPER = 0x20
26
+ MODE = 0x10
27
+
28
+ # The chip, whose inputs the drive's side sets before each read.
29
+ attr_reader :chip
30
+
31
+ def initialize(mechanism, device:)
32
+ @mechanism = mechanism
33
+ @jumper = device == 9 ? JUMPER : 0
34
+ @chip = Chips::TIA.new
35
+ inputs = @chip.inputs
36
+ inputs[0] = 0xff
37
+ inputs[1] = 0xff
38
+ inputs[2] = 0xff
39
+ mechanism.tia = self
40
+ mode_written
41
+ end
42
+
43
+ def ports = @chip.ports
44
+
45
+ def directions = @chip.directions
46
+
47
+ def inputs = @chip.inputs
48
+
49
+ def reset!
50
+ @chip.reset!
51
+ mode_written
52
+ end
53
+
54
+ def peek(addr)
55
+ mechanism = @mechanism
56
+ mechanism.release_byte_latch
57
+ chip = @chip
58
+ inputs = chip.inputs
59
+ case addr & 0x07
60
+ when 1 then inputs[1] = mechanism.latched_byte
61
+ when 2 then inputs[2] = (inputs[2] & DAV) | (mechanism.sync? ? 0 : SYNC) | @jumper | 0x1f
62
+ end
63
+ chip.peek(addr)
64
+ end
65
+
66
+ def poke(addr, value)
67
+ @mechanism.release_byte_latch
68
+ @chip.poke(addr, value)
69
+ register = addr & 0x07
70
+ mode_written if [2, 5].include?(register)
71
+ end
72
+
73
+ # What port B drives onto the head's write shift register.
74
+ def port_b_output = @chip.output(1)
75
+
76
+ # The chip's registers, in a keyed snapshot module (BL-D8-TIA).
77
+ def save_fields(out) = @chip.save_fields(out)
78
+
79
+ def load_fields(input)
80
+ @chip.load_fields(input)
81
+ end
82
+
83
+ private
84
+
85
+ # MODE selects reading while high, as the 1541's CB2 does.
86
+ def mode_written
87
+ @mechanism.cb2_written(@chip.output(2).anybits?(MODE))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/chips/tia"
4
+ require "badline/drives/model1541"
5
+ require "badline/drives/model1551/cpu"
6
+ require "badline/drives/model1551/port"
7
+ require "badline/drives/model1551/tia"
8
+ require "badline/drives/model1551/bus"
9
+ require "badline/drives/model1551/cable"
10
+ require "badline/drives/model1551/keyed_state"
11
+
12
+ module Badline
13
+ module Drives
14
+ # The 1551 disk drive of the 264 series: a 6510T at 2 MHz running DOS
15
+ # 2.6 from 16 KB of ROM, with 2 KB of RAM, the 1541's GCR mechanism
16
+ # behind a gate array, and a 6523 TIA facing the computer over TCBM, a
17
+ # parallel cable to a second 6523 in the computer's expansion port
18
+ # (Commodore schematic 251860; zimmers.net's 1551 memory map).
19
+ #
20
+ # The 6510T's port drives the mechanism as the 1541's VIA 2 port B does
21
+ # and reads the write-protect sensor and the gate array's BYTE LATCHED
22
+ # (Port). The TIA's port B reads the GCR byte the gate array latched and
23
+ # drives the byte to write, and its port C selects reading or writing
24
+ # and reads SYNC (TIA).
25
+ #
26
+ # The drive comes with its interface for the computer's expansion port,
27
+ # a second 6523 (#host_tia), which the cable joins to the drive's
28
+ # (Cable). The computer puts it at $FEE0 for device 8 and at $FEC0 for
29
+ # device 9.
30
+ #
31
+ # A 555 timer pulls IRQ low for about 7 µs every 8.3 ms, its R1 120K,
32
+ # R2 100R and C1 0.1 µF, which runs the DOS's job loop. Nothing
33
+ # acknowledges it: the line lets go on its own.
34
+ #
35
+ # Drives::Engine::Core clocks it against the host and sleeps through
36
+ # the DOS's idle loop, which counts its passes in $61 (Engine::Idle's
37
+ # pass counter), until the 555 is due to pull IRQ, the count wraps or
38
+ # the computer moves the cable's lines.
39
+ class Model1551
40
+ include Engine::Core
41
+
42
+ CLOCK_HZ = 2_000_000
43
+
44
+ # The 555's cycle in drive cycles: high for 0.693 (R1 + R2) C and low,
45
+ # pulling IRQ, for 0.693 R2 C.
46
+ IRQ_PERIOD = 16_660
47
+ IRQ_LOW = 14
48
+
49
+ # The host cycles, at about 1 MHz, by which a 1551 switched on with the
50
+ # machine has run its DOS's reset, the RAM test and the ROM checksum,
51
+ # and answers TCBM: half a second, with a margin.
52
+ BOOT_CYCLES = 600_000
53
+
54
+ # Where the DOS's idle loop starts over, polling the TIA's port A for
55
+ # a command from the computer.
56
+ IDLE_LOOP = 0xeabd
57
+
58
+ # The byte the idle loop increments on each pass.
59
+ PASS_COUNTER = 0x61
60
+
61
+ def port
62
+ settle!
63
+ @port
64
+ end
65
+
66
+ def tia
67
+ settle!
68
+ @tia
69
+ end
70
+
71
+ # The TIA in the computer's expansion port, which the computer reads
72
+ # and writes without waking the drive.
73
+ attr_reader :host_tia
74
+
75
+ # The cable between the two TIAs.
76
+ def cable
77
+ settle!
78
+ @cable
79
+ end
80
+
81
+ def model_name = "1551"
82
+
83
+ # +rom+ covers $C000-$FFFF, and defaults to the DOS image in the ROM
84
+ # path. +device+ is 8 or 9, as the jumper on the TIA's PC5 sets it.
85
+ def initialize(rom: nil, host_clock_hz: CLOCK_HZ, device: 8, debug: false)
86
+ raise ArgumentError, "a 1551 is device 8 or 9, not #{device}" unless [8, 9].include?(device)
87
+
88
+ @device = device
89
+ @clock_hz = CLOCK_HZ
90
+ @mechanism = Model1541::Mechanism.new
91
+ @mechanism.clock_hz = CLOCK_HZ
92
+ @port = Port.new(@mechanism)
93
+ @tia = TIA.new(@mechanism, device:)
94
+ @host_tia = Chips::TIA.new
95
+ @bus = Bus.new(rom: rom || ROM.load("dos1551.rom", 0xc000), port: @port, tia: @tia)
96
+ @cpu = CPU.new(@bus, debug:)
97
+ @idle_loop = IDLE_LOOP
98
+ @host_clock_hz = host_clock_hz
99
+ @phase = 0
100
+ @cycles = 0
101
+ @irq_timer = IRQ_PERIOD
102
+ init_idle(debug)
103
+ @pass_counter = PASS_COUNTER
104
+ @cable = Cable.new(@host_tia, @tia.chip, self)
105
+ @serial_port = @cable
106
+ end
107
+
108
+ # Puts in a Disk from the .d64 or .g64 image at +path+, write-protected
109
+ # with `read_only`.
110
+ def insert_image(path, read_only: false)
111
+ insert(Model1541::Disk.open(path, read_only:))
112
+ end
113
+
114
+ # The computer's RESET line reaches the interface's TIA and, through
115
+ # the cable, the CPU, its port and the drive's TIA. RAM keeps its
116
+ # contents, and the 555 runs on.
117
+ def reset!
118
+ settle!
119
+ @host_tia.reset!
120
+ @port.reset!
121
+ @tia.reset!
122
+ @cpu.reset!
123
+ end
124
+
125
+ private
126
+
127
+ # One drive cycle: the mechanism turns the disk ahead of the CPU, and
128
+ # the 555 pulls IRQ while its output is low.
129
+ def step
130
+ @mechanism.cycle!
131
+ timer = @irq_timer - 1
132
+ timer = IRQ_PERIOD if timer.zero?
133
+ @irq_timer = timer
134
+ @cpu.irq = timer <= IRQ_LOW
135
+ @cpu.cycle!
136
+ @cycles += 1
137
+ end
138
+
139
+ # A pass of the idle loop reads the TIA's port A, which holds still
140
+ # until the computer moves the cable's lines, and RAM. It repeats
141
+ # while the motor is off and the 555 leaves IRQ alone, for as long as
142
+ # the 555 leaves room (see Engine::Idle).
143
+ def busy_for_pass? = @mechanism.motor_on? || @irq_timer <= IRQ_LOW
144
+
145
+ def quiet_cycles
146
+ quiet = @irq_timer - IRQ_LOW - 1
147
+ quiet.negative? ? 0 : quiet
148
+ end
149
+
150
+ def idle_state
151
+ chip = @tia.chip
152
+ [*@cpu.counting_state, @bus.data, *@mechanism.idle_state, *chip.ports, *chip.directions, *chip.inputs,
153
+ @port.ddr, @port.data]
154
+ end
155
+
156
+ def fast_forward_chips(cycles)
157
+ @irq_timer -= cycles
158
+ end
159
+
160
+ # Every 555 pulse runs the job loop, which reads the port and the TIA,
161
+ # so no orbit (Engine::Orbit) comes round, and the drive keeps no
162
+ # anchors.
163
+ def anchor = nil
164
+
165
+ def counter_state(_touched) = []
166
+
167
+ def skip_chip_orbits(_cycles, _touched) = nil
168
+ end
169
+ end
170
+ end
@@ -26,6 +26,7 @@ module Badline
26
26
  # (CIA#quiet?).
27
27
  class Model1571
28
28
  include Engine::Core
29
+ include Engine::SerialConnection
29
30
  include Engine::VIAs
30
31
  include Engine::FastSerial
31
32
 
@@ -39,6 +39,7 @@ module Badline
39
39
  # on either chip: the drive's CIA gets no TOD pulses (CIA's tod_hz).
40
40
  class Model1581
41
41
  include Engine::Core
42
+ include Engine::SerialConnection
42
43
  include Engine::FastSerial
43
44
 
44
45
  CLOCK_HZ = 2_000_000
@@ -17,7 +17,11 @@ module Badline
17
17
  end
18
18
 
19
19
  computer = nil
20
- problem = media_problem { computer = boot(options) }
20
+ problem = if cartridge?(options)
21
+ cartridge_problem { computer = boot(options) }
22
+ else
23
+ media_problem { computer = boot(options) }
24
+ end
21
25
  unless problem.empty?
22
26
  warn "#{options.program}: #{options.media_path}: #{problem}"
23
27
  return 1
@@ -40,6 +44,21 @@ module Badline
40
44
  e.message
41
45
  end
42
46
 
47
+ # Runs the block that puts a cartridge in, as media_problem does, and
48
+ # also returns the message of the ArgumentError a machine raises over a
49
+ # cartridge with a chip in a bank or slot it doesn't map.
50
+ def self.cartridge_problem(&)
51
+ media_problem(&)
52
+ rescue ArgumentError => e
53
+ e.message
54
+ end
55
+
56
+ # Whether the media the options start is a .crt.
57
+ def self.cartridge?(options)
58
+ media = options.media_path
59
+ !media.nil? && Media::Extensions.kind(media) == :cartridge
60
+ end
61
+
43
62
  # The machine a .vsf snapshot holds, built as the saved one was, or a
44
63
  # new one built from the options with the media attached.
45
64
  def self.boot(options)
@@ -55,11 +74,13 @@ module Badline
55
74
 
56
75
  # A new machine built from the options (Machine.for_options), with
57
76
  # `media`, if not nil, attached and started as the command line does,
58
- # its disk writable if `writable`. The VIC-20 raises
77
+ # its disk writable if `writable`, after the Plus/4's cartridges the
78
+ # options put in its slots. The VIC-20 and the Plus/4 raise
59
79
  # Machine::Unsupported for a .sid tune.
60
80
  def self.start(options, media, writable)
61
81
  computer = build(options, media)
62
82
  Media::TrueDrive.plug(computer) if options.true_drive?
83
+ options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
63
84
  unless media.nil?
64
85
  puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
65
86
  disk: { read_only: !writable })
@@ -117,7 +117,11 @@ module Badline
117
117
  end
118
118
 
119
119
  def host_key(scancode)
120
- @computer.family == :c128 ? Keys.c128_key(scancode, @computer.mode) : Keys.c64_key(scancode)
120
+ case @computer.family
121
+ when :c128 then Keys.c128_key(scancode, @computer.mode)
122
+ when :plus4 then Keys.plus4_key(scancode)
123
+ else Keys.c64_key(scancode)
124
+ end
121
125
  end
122
126
 
123
127
  # RESTORE isn't in the key matrix, so it goes to the machine instead.
@@ -196,8 +200,8 @@ module Badline
196
200
  end
197
201
 
198
202
  def release_all
199
- Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(Keys.c64_key(scancode)) }
200
- Keys::WASD.each_key { |scancode| @computer.keyboard.release(Keys.c64_key(scancode)) }
203
+ Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
204
+ Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
201
205
  Keys::ARROWS.each_value do |direction|
202
206
  @computer.joystick1.release(direction)
203
207
  @computer.joystick2.release(direction)
@@ -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 own keys and, in joystick mode, the joystick
7
- # directions.
6
+ # keys, the C128's and the 264's own keys and, in joystick mode, the
7
+ # joystick directions.
8
8
  module Keys
9
9
  RETURN = 40
10
10
  ESCAPE = 41
@@ -56,6 +56,15 @@ module Badline
56
56
  63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
57
57
  }.freeze
58
58
 
59
+ # The 264's keys where they differ from the C64's: ESC on Esc, with
60
+ # RUN/STOP on Page Down as on the C128, F1-F3 on F1-F3, HELP on F4,
61
+ # Insert or Help, a cursor key on each arrow, and either SHIFT on the
62
+ # one SHIFT. Page Up, RESTORE on the C64, presses nothing.
63
+ PLUS4_OTHERS = {
64
+ 41 => :esc, 78 => :run_stop, 58 => :f1, 59 => :f2, 60 => :f3, 61 => :help, 73 => :help, 117 => :help,
65
+ 79 => :crsr_right, 80 => :crsr_left, 81 => :crsr_down, 82 => :crsr_up, 229 => :lshift, 75 => :none
66
+ }.freeze
67
+
59
68
  ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
60
69
  WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
61
70
 
@@ -77,6 +86,9 @@ module Badline
77
86
  C128_OTHERS[scancode] || c64_key(scancode)
78
87
  end
79
88
 
89
+ # The 264's key for the scancode: one of PLUS4_OTHERS or the C64's.
90
+ def self.plus4_key(scancode) = PLUS4_OTHERS[scancode] || c64_key(scancode)
91
+
80
92
  # The character the key types on a US layout: a letter, a capital
81
93
  # with `shift`, or a digit, and "" for any other key.
82
94
  def self.character(scancode, shift)
@@ -31,8 +31,18 @@ module Badline
31
31
  drive.nil? ? computer.unmount : drive.insert(nil)
32
32
  end
33
33
 
34
- # Takes the cartridge out with the power off.
34
+ # Whether the expansion port has a cartridge in, on the Plus/4 in
35
+ # either cartridge's slots.
36
+ def cartridge?(computer)
37
+ return !computer.cartridge_slots.empty? if computer.family == :plus4
38
+
39
+ !computer.address_bus.cartridge.nil?
40
+ end
41
+
42
+ # Takes the cartridge out with the power off, on the Plus/4 both.
35
43
  def remove_cartridge(computer)
44
+ return computer.detach_cartridges if computer.family == :plus4
45
+
36
46
  computer.address_bus.detach_cartridge
37
47
  computer.power_cycle!
38
48
  end
@@ -37,7 +37,12 @@ module Badline
37
37
  # :program, or :snapshot, which starts in the saves folder.
38
38
  def browse(kind)
39
39
  @browsing = kind
40
- @browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, EXTENSIONS.fetch(kind))
40
+ extensions = case kind
41
+ when :cartridge then @media.cartridge_extensions
42
+ when :program then programs
43
+ else EXTENSIONS.fetch(kind)
44
+ end
45
+ @browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
41
46
  end
42
47
 
43
48
  # Asks for a save's name, starting from `name`.
@@ -55,15 +60,15 @@ module Badline
55
60
  def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
56
61
 
57
62
  # Puts a dropped disk or tape in its device, or asks before a
58
- # cartridge or a program starts, and says whether it asks. Other
59
- # files are left alone.
63
+ # cartridge, a Plus/4's raw ROM image among them, or a program
64
+ # starts, and says whether it asks. Other files are left alone.
60
65
  def drop(path)
61
66
  extension = File.extname(path).downcase
62
67
  return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
63
68
  return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
64
- return false unless EXTENSIONS[:program].include?(extension)
69
+ return false unless programs.include?(extension)
65
70
 
66
- @confirmation.ask(extension == ".crt" ? :cartridge : :start, path)
71
+ @confirmation.ask(@media.cartridge_extensions.include?(extension) ? :cartridge : :start, path)
67
72
  true
68
73
  end
69
74
 
@@ -97,6 +102,10 @@ module Badline
97
102
 
98
103
  private
99
104
 
105
+ # What Quick open and a drop start: every medium, and the cartridges
106
+ # the machine takes.
107
+ def programs = (EXTENSIONS[:program] + @media.cartridge_extensions).uniq
108
+
100
109
  # Puts the disk or tape picked in, or asks before a cartridge or
101
110
  # Quick open power cycles the machine, and leaves the browser, as Esc
102
111
  # does.