badline 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -0
  3. data/CLAUDE.md +15 -7
  4. data/README.md +142 -16
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +274 -31
  8. data/doc/snapshots.md +14 -0
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/machine_player.rb +2 -14
  11. data/lib/badline/c64/common.rb +1 -1
  12. data/lib/badline/c64/true_drives.rb +13 -1
  13. data/lib/badline/c64.rb +6 -0
  14. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  15. data/lib/badline/chips/crtc.rb +163 -0
  16. data/lib/badline/chips/pia/control_lines.rb +138 -0
  17. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  18. data/lib/badline/chips/pia.rb +138 -0
  19. data/lib/badline/chips/tia.rb +16 -0
  20. data/lib/badline/core.rb +3 -0
  21. data/lib/badline/drives/datasette/keyed_state.rb +8 -3
  22. data/lib/badline/drives/engine/core.rb +6 -14
  23. data/lib/badline/drives/engine/idle.rb +42 -4
  24. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  25. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  26. data/lib/badline/drives/engine/sleep.rb +1 -0
  27. data/lib/badline/drives/engine.rb +1 -0
  28. data/lib/badline/drives/ieee_bus.rb +28 -0
  29. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  30. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  31. data/lib/badline/drives/model1541.rb +1 -0
  32. data/lib/badline/drives/model1551/bus.rb +138 -0
  33. data/lib/badline/drives/model1551/cable.rb +107 -0
  34. data/lib/badline/drives/model1551/cpu.rb +14 -0
  35. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  36. data/lib/badline/drives/model1551/port.rb +74 -0
  37. data/lib/badline/drives/model1551/tia.rb +71 -0
  38. data/lib/badline/drives/model1551.rb +192 -0
  39. data/lib/badline/drives/model1571.rb +1 -0
  40. data/lib/badline/drives/model1581.rb +1 -0
  41. data/lib/badline/drives.rb +1 -0
  42. data/lib/badline/frontend/app.rb +1 -1
  43. data/lib/badline/frontend/boot.rb +20 -1
  44. data/lib/badline/frontend/controls.rb +23 -3
  45. data/lib/badline/frontend/gamepads.rb +10 -3
  46. data/lib/badline/frontend/keys.rb +88 -0
  47. data/lib/badline/frontend/media_slots.rb +1 -0
  48. data/lib/badline/frontend/menu/menu_dialogs.rb +15 -6
  49. data/lib/badline/frontend/menu/menu_media.rb +96 -19
  50. data/lib/badline/frontend/menu/menu_pages.rb +25 -7
  51. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  52. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  53. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  54. data/lib/badline/frontend/player/player_controller.rb +11 -4
  55. data/lib/badline/frontend/screens.rb +2 -1
  56. data/lib/badline/frontend/timeline.rb +16 -9
  57. data/lib/badline/frontend.rb +1 -0
  58. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  59. data/lib/badline/kernal_trap/dos.rb +6 -1
  60. data/lib/badline/kernal_trap/frames.rb +117 -0
  61. data/lib/badline/kernal_trap/ieee.rb +148 -0
  62. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  63. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  64. data/lib/badline/kernal_trap/routine.rb +3 -53
  65. data/lib/badline/kernal_trap/serial.rb +4 -102
  66. data/lib/badline/kernal_trap/steps.rb +64 -0
  67. data/lib/badline/kernal_trap.rb +4 -0
  68. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  69. data/lib/badline/machine/settling.rb +1 -1
  70. data/lib/badline/machine.rb +37 -11
  71. data/lib/badline/media/extensions.rb +5 -2
  72. data/lib/badline/media/pet_media.rb +40 -0
  73. data/lib/badline/media/program.rb +14 -2
  74. data/lib/badline/media/true_drive.rb +7 -5
  75. data/lib/badline/media/vic20_media.rb +2 -2
  76. data/lib/badline/media.rb +36 -16
  77. data/lib/badline/options/family.rb +17 -6
  78. data/lib/badline/options/help.rb +23 -5
  79. data/lib/badline/options/option.rb +2 -1
  80. data/lib/badline/options/table.rb +28 -4
  81. data/lib/badline/options/validation.rb +117 -14
  82. data/lib/badline/options.rb +65 -21
  83. data/lib/badline/pet/bus/expansion.rb +106 -0
  84. data/lib/badline/pet/bus.rb +204 -0
  85. data/lib/badline/pet/canvas.rb +97 -0
  86. data/lib/badline/pet/cassettes.rb +63 -0
  87. data/lib/badline/pet/characters.rb +26 -0
  88. data/lib/badline/pet/cpu.rb +10 -0
  89. data/lib/badline/pet/crtc_video.rb +142 -0
  90. data/lib/badline/pet/discrete_video.rb +83 -0
  91. data/lib/badline/pet/ieee_ports.rb +66 -0
  92. data/lib/badline/pet/ieee_traps.rb +75 -0
  93. data/lib/badline/pet/keyboard_ports.rb +74 -0
  94. data/lib/badline/pet/keyed_modules.rb +85 -0
  95. data/lib/badline/pet/keyed_parts.rb +123 -0
  96. data/lib/badline/pet/keyed_state.rb +42 -0
  97. data/lib/badline/pet/model.rb +113 -0
  98. data/lib/badline/pet/rom_sets.rb +65 -0
  99. data/lib/badline/pet/sound.rb +128 -0
  100. data/lib/badline/pet/via.rb +10 -0
  101. data/lib/badline/pet/via_ports.rb +19 -0
  102. data/lib/badline/pet.rb +226 -0
  103. data/lib/badline/plus4/acia/frames.rb +55 -0
  104. data/lib/badline/plus4/acia.rb +276 -16
  105. data/lib/badline/plus4/bus.rb +181 -45
  106. data/lib/badline/plus4/keyed_modules.rb +22 -5
  107. data/lib/badline/plus4/keyed_parts.rb +33 -4
  108. data/lib/badline/plus4/keyed_state.rb +10 -5
  109. data/lib/badline/plus4/ram_expansion.rb +145 -0
  110. data/lib/badline/plus4/sid_card.rb +95 -0
  111. data/lib/badline/plus4/sound_mix.rb +51 -0
  112. data/lib/badline/plus4/tcbm_drive.rb +134 -0
  113. data/lib/badline/plus4/user_port.rb +11 -2
  114. data/lib/badline/plus4.rb +104 -22
  115. data/lib/badline/roms/README +146 -0
  116. data/lib/badline/roms/dos1551.rom +0 -0
  117. data/lib/badline/roms/pet/basic1.rom +0 -0
  118. data/lib/badline/roms/pet/basic2.rom +0 -0
  119. data/lib/badline/roms/pet/basic4.rom +0 -0
  120. data/lib/badline/roms/pet/characters1.rom +0 -0
  121. data/lib/badline/roms/pet/characters2.rom +0 -0
  122. data/lib/badline/roms/pet/editor1.rom +0 -0
  123. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  124. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  125. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  126. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  127. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  128. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  129. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  130. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  131. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  132. data/lib/badline/roms/pet/kernal1.rom +0 -0
  133. data/lib/badline/roms/pet/kernal2.rom +0 -0
  134. data/lib/badline/roms/pet/kernal4.rom +0 -0
  135. data/lib/badline/snapshot/container.rb +2 -0
  136. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  137. data/lib/badline/snapshot/field_doc.rb +21 -6
  138. data/lib/badline/snapshot/image.rb +14 -7
  139. data/lib/badline/snapshot/pet_setup.rb +71 -0
  140. data/lib/badline/snapshot/plus4_setup.rb +52 -10
  141. data/lib/badline/snapshot/storage_setup.rb +7 -1
  142. data/lib/badline/snapshot.rb +9 -5
  143. data/lib/badline/storage/d80_image.rb +48 -0
  144. data/lib/badline/storage/d82_image.rb +21 -0
  145. data/lib/badline/storage/g64_image.rb +1 -1
  146. data/lib/badline/storage/tap.rb +16 -3
  147. data/lib/badline/storage.rb +2 -0
  148. data/lib/badline/support/pet_checkpoint.rb +38 -0
  149. data/lib/badline/timing.rb +5 -4
  150. data/lib/badline/version.rb +1 -1
  151. data/lib/badline/vic20/attachments.rb +7 -2
  152. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  153. data/lib/badline/vic20/bus.rb +5 -0
  154. data/lib/badline/vic20.rb +1 -1
  155. metadata +69 -1
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # Hannes's RAM expansion, 256K or 1M in 64K banks, and Csory's 256K on
6
+ # the same board, from Solder's register notes and the board's GAL
7
+ # equations (plus4world, "256K RAM (Hannes)").
8
+ #
9
+ # The control register is a 6529 at $FD16, which reads back what was
10
+ # written. The expansion decodes $FD10-$FD1F further, so the user port
11
+ # answers only at $FD10, and the rest of the range is open.
12
+ #
13
+ # - Bits 0-1, and bits 2-3 on the 1M: the bank the CPU sees above the
14
+ # switch bound. All ones is the standard bank, the RAM below the
15
+ # bound, which bits 0-3 pick inverted: bank 3 of the 256K and bank
16
+ # 15 of the 1M.
17
+ # - Bit 6: the TED fetches from the CPU's bank when set, and from the
18
+ # standard bank when clear.
19
+ # - Bit 7: the switch bound, $4000 when set, $1000 when clear.
20
+ # - Bits 4-5: on the Csory board, two different bits switch Csory's
21
+ # mapping on (the notes' gate version; its table names 10 and its
22
+ # GAL 01).
23
+ #
24
+ # The register is all ones at power-on and after a reset, so the
25
+ # machine starts in the standard bank, with the TED following it.
26
+ #
27
+ # Csory's mapping puts one 16K block of the 256K, picked by bits 0-3
28
+ # of a write-only latch at $FD15, in the 16K window of the CPU's 64K
29
+ # that bits 6-7 pick. The standard bank holds blocks 0-3. The TED sees
30
+ # the block too while bit 6 of $FD16 is set. On the Csory board the
31
+ # TED reads the KERNAL wherever it fetches from the high ROM, whichever
32
+ # bank the ROM latch selects.
33
+ class RAMExpansion
34
+ # Each expansion by name: its 64K banks, and whether it has Csory's
35
+ # mapping.
36
+ KINDS = { hannes256: [4, false], hannes1m: [16, false], csory256: [4, true] }.freeze
37
+
38
+ # Whose design each is, as the pause menu names it.
39
+ NAMES = { hannes256: "HANNES", hannes1m: "HANNES", csory256: "CSORY" }.freeze
40
+
41
+ CONTROL = 0x06
42
+ CSORY_LATCH = 0x05
43
+
44
+ # The register at $FD16, and Csory's latch at $FD15.
45
+ attr_reader :kind, :control, :csory_latch
46
+
47
+ # +ram+ is the machine's own 64K, the standard bank, and +view+ the
48
+ # Bus::MirroredRAM the bus reads it through.
49
+ def initialize(kind, ram, view)
50
+ @kind = kind
51
+ count, csory = KINDS.fetch(kind)
52
+ @csory = csory
53
+ @mask = count - 1
54
+ size = (count - 1) * 0x10000
55
+ @extra = Memory.new(Array.new(size, Bus::RAM_POWER_ON), length: size, start: 0)
56
+ @banks = [view] + Array.new(count - 1) { |index| Bus::MirroredRAM.new(@extra, 0xffff, index * 0x10000) }
57
+ @blocks = csory ? Array.new(16) { |block| block_view(ram, block) } : []
58
+ @control = 0xff
59
+ @csory_latch = 0
60
+ end
61
+
62
+ # The size in K.
63
+ def size_kb = (@mask + 1) * 64
64
+
65
+ # Whether the TED reads the KERNAL for the whole high ROM.
66
+ def kernal_for_ted? = @csory
67
+
68
+ # A write to $FD11-$FD1F: the control register at $FD16, and on the
69
+ # Csory board, Csory's latch at $FD15.
70
+ def poke(addr, value)
71
+ case addr & 0x0f
72
+ when CONTROL then @control = value
73
+ when CSORY_LATCH then @csory_latch = value if @csory
74
+ end
75
+ end
76
+
77
+ # The RESET line sets the register, as power-on does. Csory's latch
78
+ # keeps its value.
79
+ def reset!
80
+ @control = 0xff
81
+ end
82
+
83
+ def power_on!
84
+ @extra.clear!(Array.new(@extra.length, Bus::RAM_POWER_ON))
85
+ @csory_latch = 0
86
+ reset!
87
+ end
88
+
89
+ # Lays the RAM the CPU sees out in +cpu+ and the RAM the TED sees in
90
+ # +ted+, a view for each of the 256 pages.
91
+ def map(cpu, ted)
92
+ control = @control
93
+ bound = control.anybits?(0x80) ? 0x40 : 0x10
94
+ standard = @banks[0]
95
+ cpu.fill(standard, 0x00, bound)
96
+ cpu.fill(@banks[~control & @mask], bound, 0x100 - bound)
97
+ follows = control.anybits?(0x40)
98
+ if follows
99
+ page = 0
100
+ while page < 0x100
101
+ ted[page] = cpu[page]
102
+ page += 1
103
+ end
104
+ else
105
+ ted.fill(standard, 0x00, 0x100)
106
+ end
107
+ return unless csory_on?
108
+
109
+ latch = @csory_latch
110
+ block = @blocks[latch & 0x0f]
111
+ window = (latch >> 6) * 0x40
112
+ cpu.fill(block, window, 0x40)
113
+ ted.fill(block, window, 0x40) if follows
114
+ end
115
+
116
+ # The banks beyond the standard one, and Csory's latch, in a keyed
117
+ # snapshot module (BL-RAMEXP).
118
+ def save_fields(out)
119
+ out.int("control", @control)
120
+ out.int("csory_latch", @csory_latch) if @csory
121
+ @extra.save_fields(out, "ram")
122
+ end
123
+
124
+ def load_fields(input)
125
+ @control = input.byte("control", @control)
126
+ @csory_latch = input.byte("csory_latch", @csory_latch)
127
+ @extra.load_fields(input, "ram")
128
+ end
129
+
130
+ private
131
+
132
+ def csory_on?
133
+ control = @control
134
+ @csory && control.anybits?(0x10) != control.anybits?(0x20)
135
+ end
136
+
137
+ # Block +block+ of the 256K, from the standard bank's four on.
138
+ def block_view(ram, block)
139
+ return Bus::MirroredRAM.new(ram, 0x3fff, block * 0x4000) if block < 4
140
+
141
+ Bus::MirroredRAM.new(@extra, 0x3fff, (block - 4) * 0x4000)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # A SID card's SID at $FD40-$FD5F, or at $FE80-$FE9F as Csory's card
6
+ # puts it, clocked once a TED cycle, as the cards clock it from the
7
+ # TED's single clock. The SID counts the
8
+ # cycles that have passed whenever something touches it, rather than
9
+ # every cycle: a CPU access runs it through the cycle the access is in,
10
+ # and the host, between cycles, up to the cycle about to start.
11
+ # `clock` answers the machine's cycles.
12
+ class SIDCard
13
+ # Where a card can put its SID.
14
+ ADDRESSES = [0xfd40, 0xfe80].freeze
15
+
16
+ attr_reader :sid, :address
17
+
18
+ def initialize(clock, model, address = ADDRESSES.first)
19
+ raise ArgumentError, format("no SID card at $%04X", address) unless ADDRESSES.include?(address)
20
+
21
+ @clock = clock
22
+ @address = address
23
+ @sid = Chips::SID.new(model:, at: address)
24
+ @clocked = 0
25
+ end
26
+
27
+ def model = @sid.model
28
+
29
+ def model=(model)
30
+ catch_up!
31
+ @sid.model = model
32
+ end
33
+
34
+ def peek(addr)
35
+ run_to(@clock.cycles + 1)
36
+ @sid.peek(addr)
37
+ end
38
+
39
+ def poke(addr, value)
40
+ run_to(@clock.cycles + 1)
41
+ @sid.poke(addr, value)
42
+ end
43
+
44
+ # The RES line, from the host between cycles.
45
+ def reset!
46
+ run_to(@clock.cycles)
47
+ @sid.reset!
48
+ end
49
+
50
+ # Runs the SID, its voices and its filter, up to the current cycle.
51
+ def catch_up!
52
+ run_to(@clock.cycles)
53
+ @sid.catch_up!
54
+ end
55
+
56
+ def record(rate:, clock_hz:)
57
+ run_to(@clock.cycles)
58
+ @sid.record(rate:, clock_hz:)
59
+ end
60
+
61
+ def drain_samples
62
+ run_to(@clock.cycles)
63
+ @sid.drain_samples
64
+ end
65
+
66
+ # The SID's fields in a keyed snapshot module (BL-SID), saved once the
67
+ # machine has settled it (#catch_up!).
68
+ def save_fields(out) = @sid.save_fields(out)
69
+
70
+ def load_fields(input)
71
+ @sid.load_fields(input)
72
+ end
73
+
74
+ # The SID counts on from the machine's cycle the snapshot was taken at.
75
+ def restored!
76
+ @sid.restored!
77
+ @clocked = @clock.cycles
78
+ end
79
+
80
+ private
81
+
82
+ def run_to(target)
83
+ clocked = @clocked
84
+ return if clocked >= target
85
+
86
+ sid = @sid
87
+ while clocked < target
88
+ sid.cycle!
89
+ clocked += 1
90
+ end
91
+ @clocked = clocked
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # What a front end plays from a 264 with a SID card fitted: the TED's
6
+ # sound and the card's SID, each recorded at the audio rate and added
7
+ # together, clipped to a signed 16-bit sample. A sample one source has
8
+ # closed waits for the other's. The SID's model is the mix's, as it
9
+ # is a C64's sound source's, for the pause menu's Sound page.
10
+ class SoundMix
11
+ def initialize(ted_sound, sid)
12
+ @ted_sound = ted_sound
13
+ @sid = sid
14
+ @ted_waiting = []
15
+ @sid_waiting = []
16
+ end
17
+
18
+ def model = @sid.model
19
+
20
+ def model=(model)
21
+ @sid.model = model
22
+ end
23
+
24
+ # Starts both sources recording at `rate` samples a second, their
25
+ # cycles taken to run at `clock_hz`.
26
+ def record(rate:, clock_hz:)
27
+ @ted_sound.record(rate:, clock_hz:)
28
+ @sid.record(rate:, clock_hz:)
29
+ @ted_waiting = []
30
+ @sid_waiting = []
31
+ end
32
+
33
+ # Runs both sources up to the current cycle.
34
+ def catch_up!
35
+ @ted_sound.catch_up!
36
+ @sid.catch_up!
37
+ end
38
+
39
+ # The mixed samples both sources have closed since the last drain.
40
+ def drain_samples
41
+ ted = @ted_waiting.concat(@ted_sound.drain_samples)
42
+ sid = @sid_waiting.concat(@sid.drain_samples)
43
+ count = [ted.length, sid.length].min
44
+ mixed = Array.new(count) { |index| (ted[index] + sid[index]).clamp(-0x8000, 0x7fff) }
45
+ @ted_waiting = ted[count, ted.length - count] || []
46
+ @sid_waiting = sid[count, sid.length - count] || []
47
+ mixed
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Plus4
5
+ # The 264 series' true drives: a Drives::Model1541 and a
6
+ # Drives::Model1581 on the serial bus (C64::TrueDrives), and a
7
+ # Drives::Model1551 on TCBM, its interface's TIA in the expansion port
8
+ # at $FEE0 for device 8 or $FEC0 for device 9. Each sits in a field of
9
+ # its own, so none makes another's per-cycle call a union.
10
+ #
11
+ # A device number has one drive at a time: a 1551 takes the place of a
12
+ # serial drive with its number, and the other way round.
13
+ module TCBMDrive
14
+ # The true drive models plug_true_drive builds.
15
+ TRUE_DRIVE_MODELS = %w[1541 1551].freeze
16
+
17
+ # The 1551 on TCBM, or nil.
18
+ attr_reader :drive1551
19
+
20
+ # The drive plug_true_drive plugs in, "1541" or "1551".
21
+ attr_reader :true_drive_model
22
+
23
+ # Plugs in a Drives::Model1551, its interface's TIA where its device
24
+ # number puts it, in place of the TCBM interface's own there, and in
25
+ # place of a serial drive with its number. The traps stop answering
26
+ # device 8 when the drive is device 8.
27
+ def attach_drive1551(drive)
28
+ detach_drive1551
29
+ device = drive.device
30
+ detach_drive1541 if @drive1541&.device == device
31
+ detach_drive1581 if @drive1581&.device == device
32
+ drive.host_clock_hz = timing.clock_hz
33
+ @drive1551 = drive
34
+ @bus.plug_tia(drive.device, drive.host_tia)
35
+ drive.on_renumber { |old| drive1551_renumbered(old) }
36
+ @serial_trap&.device = serial_trap_device
37
+ @init_threshold = [@init_threshold, Badline::Drives::Model1551::BOOT_CYCLES].max if @cycles < @init_threshold
38
+ end
39
+
40
+ # Unplugs the Drives::Model1551, leaving the TCBM interface's own TIA,
41
+ # if there is one, in its place.
42
+ def detach_drive1551
43
+ drive = @drive1551
44
+ return unless drive
45
+
46
+ @bus.plug_tia(drive.device, tcbm_tia(drive.device))
47
+ drive.on_renumber
48
+ @drive1551 = nil
49
+ @serial_trap&.device = serial_trap_device
50
+ end
51
+
52
+ # The true drive: a 1581 ahead of a 1551 ahead of a 1541.
53
+ def true_drive = @drive1581 || @drive1551 || @drive1541
54
+
55
+ # Plugs in the model true_drive_model names as device 8, in place of
56
+ # the drive there, and returns it.
57
+ def plug_true_drive
58
+ return plug_drive1551 if @true_drive_model == "1551"
59
+
60
+ unplug_device8
61
+ detach_drive1581 if drive1581_on_device8?
62
+ Badline::Drives::Model1541.new.tap { |drive| attach_drive1541(drive) }
63
+ end
64
+
65
+ # Plugs in a 1551 as device 8, in place of the drive there, and
66
+ # returns it.
67
+ def plug_drive1551
68
+ Badline::Drives::Model1551.new.tap { |drive| attach_drive1551(drive) }
69
+ end
70
+
71
+ private
72
+
73
+ def init_drives(true_drive_model)
74
+ unless TRUE_DRIVE_MODELS.include?(true_drive_model)
75
+ raise ArgumentError, "no true drive model #{true_drive_model} for the Plus/4"
76
+ end
77
+
78
+ @true_drive_model = true_drive_model
79
+ @drive1541 = nil
80
+ @drive1551 = nil
81
+ @drive1581 = nil
82
+ end
83
+
84
+ def save_drive1551_modules(sections)
85
+ drive = @drive1551
86
+ drive&.save_modules(sections, "BL-D#{drive.device}")
87
+ sections
88
+ end
89
+
90
+ def load_drive1551_modules(modules, input, device)
91
+ return input.adjust("a 1551 as device #{device}, left out") unless [8, 9].include?(device)
92
+
93
+ detach_drive1551 if @drive1551 && @drive1551.device != device
94
+ attach_drive1551(Badline::Drives::Model1551.new(device:)) unless @drive1551
95
+ @drive1551.load_modules(modules, input, "BL-D#{device}")
96
+ end
97
+
98
+ # The 1551's DEV line moved it from device +old+ to the other: its
99
+ # interface's TIA moves with it, and the TCBM interface's own takes
100
+ # its old place.
101
+ def drive1551_renumbered(old)
102
+ drive = @drive1551
103
+ return unless drive
104
+
105
+ @bus.plug_tia(old, tcbm_tia(old))
106
+ @bus.plug_tia(drive.device, drive.host_tia)
107
+ @serial_trap&.device = serial_trap_device
108
+ end
109
+
110
+ # The TCBM interface's own TIA for +device+, or nil without one.
111
+ def tcbm_tia(device)
112
+ tias = @tias
113
+ return nil if tias.empty?
114
+
115
+ device == 9 ? tias[1] : tias[0]
116
+ end
117
+
118
+ # Device 8, unless a true drive is device 8.
119
+ def serial_trap_device
120
+ device = KernalTrap::Routine::DEVICE
121
+ on_device8 = @drive1551&.device == device || @drive1541&.device == device || drive1581_on_device8?
122
+ on_device8 ? nil : device
123
+ end
124
+
125
+ # Unplugs the 1541 and the 1551 if either is device 8, for a 1581 to
126
+ # take its place.
127
+ def unplug_device8
128
+ device = KernalTrap::Routine::DEVICE
129
+ detach_drive1541 if @drive1541&.device == device
130
+ detach_drive1551 if @drive1551&.device == device
131
+ end
132
+ end
133
+ end
134
+ end
@@ -6,22 +6,31 @@ module Badline
6
6
  # latch: a line reads low where the latch or the outside pulls it. With
7
7
  # nothing plugged into the user port, the outside pulls only bit 2, the
8
8
  # cassette sense switch, low while a key is down on the deck. The
9
- # KERNAL's IRQ reads it there ($CFBF).
9
+ # KERNAL's IRQ reads it there ($CFBF), and takes P1 as RS-232's clear
10
+ # to send, sending through the ACIA only while it reads high.
11
+ #
12
+ # A device on the user port drives P0-P7 through #inputs, low where its
13
+ # bit is clear.
10
14
  class UserPort
11
15
  TAPE_SENSE = 0x04
12
16
 
13
17
  attr_reader :output
18
+ attr_accessor :inputs
14
19
 
15
20
  def initialize(datasette)
16
21
  @datasette = datasette
17
22
  @output = 0xff
23
+ @inputs = 0xff
18
24
  end
19
25
 
20
26
  def reset!
21
27
  @output = 0xff
22
28
  end
23
29
 
24
- def peek(_addr) = @datasette.sense_low? ? @output & ~TAPE_SENSE : @output
30
+ def peek(_addr)
31
+ lines = @output & @inputs
32
+ @datasette.sense_low? ? lines & ~TAPE_SENSE : lines
33
+ end
25
34
 
26
35
  def poke(_addr, value)
27
36
  @output = value