badline 0.15.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -0
- data/CLAUDE.md +7 -0
- data/README.md +40 -10
- data/doc/media.md +19 -0
- data/doc/snapshot-format.md +61 -13
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/machine_player.rb +2 -14
- data/lib/badline/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- data/lib/badline/chips/tia.rb +16 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +8 -3
- data/lib/badline/drives/engine/core.rb +6 -14
- data/lib/badline/drives/engine/idle.rb +42 -4
- data/lib/badline/drives/engine/idle_cpu.rb +18 -0
- data/lib/badline/drives/engine/serial_connection.rb +25 -0
- data/lib/badline/drives/engine/sleep.rb +1 -0
- data/lib/badline/drives/engine.rb +1 -0
- data/lib/badline/drives/model1541/byte_ready.rb +19 -3
- data/lib/badline/drives/model1541/mechanism.rb +10 -1
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551/bus.rb +138 -0
- data/lib/badline/drives/model1551/cable.rb +104 -0
- data/lib/badline/drives/model1551/cpu.rb +14 -0
- data/lib/badline/drives/model1551/keyed_state.rb +65 -0
- data/lib/badline/drives/model1551/port.rb +74 -0
- data/lib/badline/drives/model1551/tia.rb +92 -0
- data/lib/badline/drives/model1551.rb +170 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/frontend/boot.rb +20 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +13 -5
- data/lib/badline/frontend/menu/menu_media.rb +92 -14
- data/lib/badline/frontend/menu/menu_pages.rb +11 -6
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- data/lib/badline/machine.rb +9 -5
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/options/help.rb +4 -2
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +14 -2
- data/lib/badline/options/validation.rb +49 -9
- data/lib/badline/options.rb +34 -2
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +181 -45
- data/lib/badline/plus4/keyed_modules.rb +22 -5
- data/lib/badline/plus4/keyed_parts.rb +33 -4
- data/lib/badline/plus4/keyed_state.rb +5 -2
- data/lib/badline/plus4/ram_expansion.rb +145 -0
- data/lib/badline/plus4/sid_card.rb +95 -0
- data/lib/badline/plus4/sound_mix.rb +51 -0
- data/lib/badline/plus4/tcbm_drive.rb +120 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +103 -21
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- data/lib/badline/snapshot/field_doc.rb +7 -5
- data/lib/badline/snapshot/plus4_setup.rb +52 -10
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/version.rb +1 -1
- metadata +15 -1
|
@@ -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
|
|
@@ -17,7 +17,11 @@ module Badline
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
computer = nil
|
|
20
|
-
problem =
|
|
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)
|
|
@@ -37,7 +37,11 @@ module Badline
|
|
|
37
37
|
# :program, or :snapshot, which starts in the saves folder.
|
|
38
38
|
def browse(kind)
|
|
39
39
|
@browsing = kind
|
|
40
|
-
extensions =
|
|
40
|
+
extensions = case kind
|
|
41
|
+
when :cartridge then @media.cartridge_extensions
|
|
42
|
+
when :program then programs
|
|
43
|
+
else EXTENSIONS.fetch(kind)
|
|
44
|
+
end
|
|
41
45
|
@browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
|
|
42
46
|
end
|
|
43
47
|
|
|
@@ -56,15 +60,15 @@ module Badline
|
|
|
56
60
|
def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
|
|
57
61
|
|
|
58
62
|
# Puts a dropped disk or tape in its device, or asks before a
|
|
59
|
-
# cartridge
|
|
60
|
-
# 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.
|
|
61
65
|
def drop(path)
|
|
62
66
|
extension = File.extname(path).downcase
|
|
63
67
|
return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
|
|
64
68
|
return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
|
|
65
|
-
return false unless
|
|
69
|
+
return false unless programs.include?(extension)
|
|
66
70
|
|
|
67
|
-
@confirmation.ask(extension
|
|
71
|
+
@confirmation.ask(@media.cartridge_extensions.include?(extension) ? :cartridge : :start, path)
|
|
68
72
|
true
|
|
69
73
|
end
|
|
70
74
|
|
|
@@ -98,6 +102,10 @@ module Badline
|
|
|
98
102
|
|
|
99
103
|
private
|
|
100
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
|
+
|
|
101
109
|
# Puts the disk or tape picked in, or asks before a cartridge or
|
|
102
110
|
# Quick open power cycles the machine, and leaves the browser, as Esc
|
|
103
111
|
# does.
|
|
@@ -10,17 +10,29 @@ module Badline
|
|
|
10
10
|
# wrong for the page to show.
|
|
11
11
|
class MenuMedia
|
|
12
12
|
attr_reader :problem
|
|
13
|
-
attr_writer :computer
|
|
14
13
|
|
|
15
14
|
# Takes the disk set from the command line's `media_path` when it's a
|
|
16
|
-
# list
|
|
17
|
-
|
|
15
|
+
# list, and the names of the cartridges it and `cartridges`, the
|
|
16
|
+
# Plus/4's ROM images by slot (Options#cartridges), put in the first
|
|
17
|
+
# machine.
|
|
18
|
+
def initialize(media_path, writable, cartridges)
|
|
18
19
|
@list = Media::DiskList.list?(media_path) ? media_path : ""
|
|
19
20
|
@writable = writable
|
|
20
21
|
@set_path = ""
|
|
21
22
|
@set = []
|
|
22
23
|
@problem = ""
|
|
23
24
|
@computer = nil
|
|
25
|
+
@command_line = cartridges.map { |slot, path| [slot, path] } + [["", media_path]]
|
|
26
|
+
@names = {}
|
|
27
|
+
@named = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def computer=(computer)
|
|
31
|
+
@computer = computer
|
|
32
|
+
return unless @named.nil?
|
|
33
|
+
|
|
34
|
+
@command_line.each { |slot, path| note_cartridge(computer, path.dup, slot) }
|
|
35
|
+
@named = computer
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def writable? = @writable
|
|
@@ -97,7 +109,9 @@ module Badline
|
|
|
97
109
|
# Swaps the cartridge for the one at `path`, which power cycles the
|
|
98
110
|
# machine, and says whether it went in.
|
|
99
111
|
def attach_cartridge(path)
|
|
100
|
-
|
|
112
|
+
attached = cartridge_succeeds? { Media.attach(@computer, path) }
|
|
113
|
+
note_cartridge(@computer, path, "") if attached
|
|
114
|
+
attached
|
|
101
115
|
end
|
|
102
116
|
|
|
103
117
|
# What a save of the machine is named after: the cartridge, the disk,
|
|
@@ -122,26 +136,36 @@ module Badline
|
|
|
122
136
|
end
|
|
123
137
|
|
|
124
138
|
# The name the cartridge in the expansion port gives itself, or an
|
|
125
|
-
# empty one. A Plus/4's
|
|
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
|
|
141
|
+
# cartridges with a ROM in, and a slot filled from elsewhere, such as
|
|
142
|
+
# a snapshot, by the slot.
|
|
126
143
|
def cartridge_name
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
case @computer.family
|
|
145
|
+
when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
|
|
146
|
+
when :vic20 then named? ? @names.fetch("", "") : ""
|
|
147
|
+
else
|
|
148
|
+
cartridge = @computer.address_bus.cartridge
|
|
149
|
+
cartridge.nil? ? "" : cartridge.name
|
|
150
|
+
end
|
|
131
151
|
end
|
|
132
152
|
|
|
133
153
|
# Takes the cartridge out with the power off.
|
|
134
|
-
def remove_cartridge
|
|
154
|
+
def remove_cartridge
|
|
155
|
+
@names = {}
|
|
156
|
+
MediaSlots.remove_cartridge(@computer)
|
|
157
|
+
end
|
|
135
158
|
|
|
136
159
|
# A new machine started on `path`, as the command line would, or nil
|
|
137
160
|
# when it won't start.
|
|
138
161
|
def start(options, path)
|
|
139
162
|
computer = nil
|
|
140
|
-
|
|
141
|
-
computer =
|
|
142
|
-
|
|
163
|
+
if Media.kind(@computer, path) == :cartridge
|
|
164
|
+
cartridge_succeeds? { computer = started(options, path) }
|
|
165
|
+
else
|
|
166
|
+
succeeds? { computer = started(options, path) }
|
|
143
167
|
end
|
|
144
|
-
|
|
168
|
+
@problem.empty? ? computer : nil
|
|
145
169
|
end
|
|
146
170
|
|
|
147
171
|
private
|
|
@@ -153,6 +177,60 @@ module Badline
|
|
|
153
177
|
@problem.empty?
|
|
154
178
|
end
|
|
155
179
|
|
|
180
|
+
# Runs the block as #succeeds? does, keeping a cartridge's chip in a
|
|
181
|
+
# bank or slot the machine doesn't map as the problem too.
|
|
182
|
+
def cartridge_succeeds?(&)
|
|
183
|
+
@problem = Frontend.cartridge_problem(&)
|
|
184
|
+
@problem.empty?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The machine Frontend.start started on `path`, with the names of
|
|
188
|
+
# the cartridges in it noted.
|
|
189
|
+
def started(options, path)
|
|
190
|
+
computer = Frontend.start(options, path, @writable)
|
|
191
|
+
list(path)
|
|
192
|
+
@names = {}
|
|
193
|
+
options.cartridges.each { |slot, image| note_cartridge(computer, image.dup, slot) }
|
|
194
|
+
note_cartridge(computer, path, "")
|
|
195
|
+
@named = computer
|
|
196
|
+
computer
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def named? = @computer.equal?(@named)
|
|
200
|
+
|
|
201
|
+
def slot_name(slot) = named? ? @names.fetch(slot, slot.upcase) : slot.upcase
|
|
202
|
+
|
|
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.
|
|
207
|
+
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
|
+
|
|
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
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A .crt's own name, or else the file's, without its extension.
|
|
229
|
+
def file_name(path)
|
|
230
|
+
name = Media::Extensions.kind(path) == :cartridge ? Storage::CRTFile.new(path, machine: nil).name : ""
|
|
231
|
+
name.empty? ? File.basename(path, File.extname(path)) : name
|
|
232
|
+
end
|
|
233
|
+
|
|
156
234
|
# Keeps `path` as the list the set comes from, if it's a list.
|
|
157
235
|
def list(path)
|
|
158
236
|
return unless Media::DiskList.list?(path)
|
|
@@ -29,7 +29,7 @@ module Badline
|
|
|
29
29
|
@recent = []
|
|
30
30
|
@painter = painter
|
|
31
31
|
@buttons = buttons
|
|
32
|
-
@media = MenuMedia.new(media_path, options.writable
|
|
32
|
+
@media = MenuMedia.new(media_path, options.writable?, options.cartridges)
|
|
33
33
|
@reu_kb = options.reu
|
|
34
34
|
@computer = nil
|
|
35
35
|
@controls = nil
|
|
@@ -122,9 +122,9 @@ module Badline
|
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def draw_expansion
|
|
125
|
+
info("CARTRIDGE", @media.cartridge_name)
|
|
125
126
|
return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
|
|
126
127
|
|
|
127
|
-
info("CARTRIDGE", @media.cartridge_name)
|
|
128
128
|
row("INSERT...", :insert_cartridge)
|
|
129
129
|
row("REMOVE", :remove_cartridge)
|
|
130
130
|
return if plus4?
|
|
@@ -137,7 +137,8 @@ module Badline
|
|
|
137
137
|
info("RAM EXPANSION", "REU, #{@reu_kb} KB")
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
-
# The SID's model is a setting,
|
|
140
|
+
# The SID's model is a setting, the C64's SID's or a Plus/4's SID
|
|
141
|
+
# card's, and other sound chips have none.
|
|
141
142
|
def draw_sound
|
|
142
143
|
if @sound.on?
|
|
143
144
|
toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
|
|
@@ -147,7 +148,7 @@ module Badline
|
|
|
147
148
|
model = @computer.sound_source.model
|
|
148
149
|
return unless SID_MODELS.include?(model)
|
|
149
150
|
|
|
150
|
-
toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
|
|
151
|
+
toggle(plus4? ? "SID CARD" : "SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
|
|
151
152
|
end
|
|
152
153
|
|
|
153
154
|
# Saving now and by name, loading a named save, and the quicksaves
|
|
@@ -166,10 +167,14 @@ module Badline
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
# The C128 names its model and the mode it runs in first, and the
|
|
169
|
-
# Plus/4 its model and RAM.
|
|
170
|
+
# Plus/4 its model and RAM, and its RAM expansion if it has one.
|
|
170
171
|
def draw_power
|
|
171
172
|
info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
|
|
172
|
-
|
|
173
|
+
if plus4?
|
|
174
|
+
info("MACHINE", @computer.model_label)
|
|
175
|
+
expansion = @computer.ram_expansion_label
|
|
176
|
+
info("RAM EXPANSION", expansion) if expansion
|
|
177
|
+
end
|
|
173
178
|
row("RESET", :reset)
|
|
174
179
|
row("POWER CYCLE", :power_cycle)
|
|
175
180
|
skip
|
|
@@ -15,7 +15,8 @@ module Badline
|
|
|
15
15
|
# way: #wait handles the window's clicks, keys and drops between frames
|
|
16
16
|
# and redraws it at the display's pace. The SID model buttons switch
|
|
17
17
|
# the chips playing on the spot, and each tune after it starts on the
|
|
18
|
-
# same choice. AUTO gives each of a tune's SIDs its own model.
|
|
18
|
+
# same choice. AUTO gives each of a tune's SIDs its own model. It
|
|
19
|
+
# quits once it has drawn `frames` frames (--frames), unless that's 0.
|
|
19
20
|
class PlayerController < Audio::Terminal
|
|
20
21
|
FRAME = 1.0 / 50
|
|
21
22
|
|
|
@@ -30,8 +31,10 @@ module Badline
|
|
|
30
31
|
Keys::TAB => :view, 6 => :chip
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
33
|
-
def initialize(input:, output:)
|
|
34
|
-
super
|
|
34
|
+
def initialize(input:, output:, frames: 0)
|
|
35
|
+
super(input:, output:)
|
|
36
|
+
@frame_limit = frames
|
|
37
|
+
@frames = 0
|
|
35
38
|
@screen = PlayerScreen.new
|
|
36
39
|
@player = nil
|
|
37
40
|
@tune_file = nil
|
|
@@ -73,7 +76,10 @@ module Badline
|
|
|
73
76
|
actions = poll
|
|
74
77
|
deadline = now + seconds
|
|
75
78
|
loop do
|
|
76
|
-
|
|
79
|
+
if now >= @next_draw
|
|
80
|
+
draw
|
|
81
|
+
actions << :quit if @frames == @frame_limit
|
|
82
|
+
end
|
|
77
83
|
left = deadline - now
|
|
78
84
|
break if left <= 0 || !actions.empty?
|
|
79
85
|
|
|
@@ -211,6 +217,7 @@ module Badline
|
|
|
211
217
|
|
|
212
218
|
def draw
|
|
213
219
|
@next_draw = now + FRAME
|
|
220
|
+
@frames += 1
|
|
214
221
|
@screen.draw(@state, @player, @history, @tune_file, SIDTune::STIL::Credit.at(@credits, @state.played))
|
|
215
222
|
end
|
|
216
223
|
|
data/lib/badline/machine.rb
CHANGED
|
@@ -93,7 +93,7 @@ module Badline
|
|
|
93
93
|
def self.for_options(options, media, &)
|
|
94
94
|
family = options.family
|
|
95
95
|
return vic20_for(media, options.model, options.ram, &) if family == :vic20
|
|
96
|
-
return plus4_for(media, options
|
|
96
|
+
return plus4_for(media, options) if family == :plus4
|
|
97
97
|
|
|
98
98
|
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
99
99
|
return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
|
|
@@ -127,13 +127,17 @@ module Badline
|
|
|
127
127
|
end
|
|
128
128
|
private_class_method :vic20_for
|
|
129
129
|
|
|
130
|
-
# A Plus/4 with
|
|
131
|
-
# the model's own
|
|
132
|
-
|
|
130
|
+
# A Plus/4 with the RAM --ram names (Plus4::Model::RAM_SIZES), or with
|
|
131
|
+
# the model's own without it, the true drive --drive-type names, the
|
|
132
|
+
# SID card --sid-card names, where --sid-card-address puts it, and the
|
|
133
|
+
# RAM expansion --ram-expansion names.
|
|
134
|
+
def self.plus4_for(media, options)
|
|
133
135
|
unsupported = !media.nil? && Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
134
136
|
raise Unsupported, "doesn't go in a Plus/4" if unsupported
|
|
135
137
|
|
|
136
|
-
Plus4.new(model
|
|
138
|
+
Plus4.new(model: options.model, ram: options.ram_kb, true_drive_model: options.drive_type,
|
|
139
|
+
sid_card: options.sid_card, sid_card_address: options.sid_card_address || 0xfd40,
|
|
140
|
+
ram_expansion: options.ram_expansion)
|
|
137
141
|
end
|
|
138
142
|
private_class_method :plus4_for
|
|
139
143
|
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Media
|
|
5
5
|
# Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
|
|
6
|
-
# the C128 its 1571, or
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# the C128 its 1571, or on the Plus/4 a 1551 if it asks for one, or a
|
|
7
|
+
# 1581. Once a true drive is plugged in as device 8, disk media go into
|
|
8
|
+
# it rather than into the traps, so a LOAD runs the DOS and every byte
|
|
9
|
+
# crosses the serial bus or TCBM. A .g64 holds the
|
|
9
10
|
# disk's raw GCR, which only a true drive reads, so it plugs one in
|
|
10
11
|
# when there's none yet. The 1541 reads only .d64 and .g64 images, the
|
|
11
12
|
# 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
|
|
@@ -52,9 +53,10 @@ module Badline
|
|
|
52
53
|
raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
drive ||= computer.plug_true_drive
|
|
57
|
+
drive.insert_image(path, read_only:)
|
|
56
58
|
computer.unmount
|
|
57
|
-
"Inserted #{path} in the #{
|
|
59
|
+
"Inserted #{path} in the #{drive.model_name} as device 8"
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
private
|