badline 0.8.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +6 -4
- data/README.md +60 -19
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +199 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +79 -33
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +13 -92
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +52 -12
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +50 -33
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +51 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +63 -10
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +63 -64
- data/lib/badline/frontend/boot.rb +34 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +70 -16
- data/lib/badline/frontend/menu_pages.rb +47 -42
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +52 -37
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +4 -4
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +17 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +24 -25
- data/lib/badline/kernal_trap/routine.rb +17 -5
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +38 -24
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +13 -3
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +39 -20
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +10 -6
- data/lib/badline/options/help.rb +25 -13
- data/lib/badline/options/table.rb +15 -1
- data/lib/badline/options/validation.rb +63 -3
- data/lib/badline/options.rb +37 -34
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +37 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +15 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +10 -4
- data/lib/badline/storage/tap.rb +44 -8
- data/lib/badline/storage/unavailable.rb +3 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline/vic.rb +34 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- data/lib/badline.rb +2 -38
- metadata +37 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -12,17 +12,6 @@ module Badline
|
|
|
12
12
|
class Serial < Routine
|
|
13
13
|
attr_accessor :device
|
|
14
14
|
|
|
15
|
-
ROUTINES = {
|
|
16
|
-
0xed09 => :talk,
|
|
17
|
-
0xed0c => :listen,
|
|
18
|
-
0xedb9 => :second,
|
|
19
|
-
0xedc7 => :tksa,
|
|
20
|
-
0xeddd => :ciout,
|
|
21
|
-
0xedef => :untalk,
|
|
22
|
-
0xedfe => :unlisten,
|
|
23
|
-
0xee13 => :acptr
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
15
|
# Frame type in the high nibble of the secondary address
|
|
27
16
|
OPEN = 0xf0
|
|
28
17
|
CLOSE = 0xe0
|
|
@@ -34,8 +23,8 @@ module Badline
|
|
|
34
23
|
|
|
35
24
|
NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
|
|
36
25
|
|
|
37
|
-
def initialize(cpu:, bus:, drive:, device: DEVICE)
|
|
38
|
-
super(cpu:, bus:)
|
|
26
|
+
def initialize(cpu:, bus:, layout:, drive:, device: DEVICE)
|
|
27
|
+
super(cpu:, bus:, layout:)
|
|
39
28
|
@drive = drive
|
|
40
29
|
@device = device
|
|
41
30
|
@listening = false
|
|
@@ -63,14 +52,27 @@ module Badline
|
|
|
63
52
|
end
|
|
64
53
|
|
|
65
54
|
def install
|
|
66
|
-
|
|
67
|
-
@cpu.install_trap(address) { call_routine(routine) if
|
|
55
|
+
routines.each do |address, routine|
|
|
56
|
+
@cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
|
|
68
57
|
end
|
|
69
58
|
self
|
|
70
59
|
end
|
|
71
60
|
|
|
72
61
|
private
|
|
73
62
|
|
|
63
|
+
def routines
|
|
64
|
+
{
|
|
65
|
+
@layout.talk => :talk,
|
|
66
|
+
@layout.listen => :listen,
|
|
67
|
+
@layout.second => :second,
|
|
68
|
+
@layout.tksa => :tksa,
|
|
69
|
+
@layout.ciout => :ciout,
|
|
70
|
+
@layout.untalk => :untalk,
|
|
71
|
+
@layout.unlisten => :unlisten,
|
|
72
|
+
@layout.acptr => :acptr
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
74
76
|
def call_routine(routine)
|
|
75
77
|
case routine
|
|
76
78
|
when :talk then talk
|
|
@@ -85,17 +87,24 @@ module Badline
|
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
88
|
-
# in the secondary address that follows.
|
|
90
|
+
# in the secondary address that follows. Each byte sent under ATN,
|
|
91
|
+
# and each byte of a frame, times its handshake with the serial timer
|
|
92
|
+
# as ISOUR does.
|
|
89
93
|
def talk
|
|
90
94
|
@talk_channel = nil
|
|
91
95
|
@talking = @cpu.a == @device
|
|
92
|
-
|
|
96
|
+
sent_under_atn if @talking
|
|
93
97
|
end
|
|
94
98
|
|
|
95
99
|
def listen
|
|
96
100
|
@listen_channel = nil
|
|
97
101
|
@listening = @cpu.a == @device
|
|
98
|
-
|
|
102
|
+
sent_under_atn if @listening
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def sent_under_atn
|
|
106
|
+
time_serial_byte(ISOUR_TIMEOUT)
|
|
107
|
+
return_to_caller
|
|
99
108
|
end
|
|
100
109
|
|
|
101
110
|
def second
|
|
@@ -104,19 +113,22 @@ module Badline
|
|
|
104
113
|
@frame = @cpu.a & 0xf0
|
|
105
114
|
@listen_channel = @cpu.a & 0x0f
|
|
106
115
|
@buffer = []
|
|
107
|
-
|
|
116
|
+
sent_under_atn
|
|
108
117
|
end
|
|
109
118
|
|
|
110
119
|
def tksa
|
|
111
120
|
return unless @talking
|
|
112
121
|
|
|
113
122
|
@talk_channel = @cpu.a & 0x0f
|
|
114
|
-
|
|
123
|
+
sent_under_atn
|
|
115
124
|
end
|
|
116
125
|
|
|
126
|
+
# CIOUT holds each byte back until the next one, or the UNLISTEN,
|
|
127
|
+
# comes, so the first byte of a frame sends nothing.
|
|
117
128
|
def ciout
|
|
118
129
|
return unless @listening
|
|
119
130
|
|
|
131
|
+
time_serial_byte(ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
|
|
120
132
|
@buffer << @cpu.a
|
|
121
133
|
update_status(DEVICE_NOT_PRESENT) unless accepted?
|
|
122
134
|
@cpu.status.carry = false
|
|
@@ -135,6 +147,7 @@ module Badline
|
|
|
135
147
|
|
|
136
148
|
@listening = false
|
|
137
149
|
deliver_frame
|
|
150
|
+
time_serial_byte(ISOUR_TIMEOUT)
|
|
138
151
|
release_bus
|
|
139
152
|
end
|
|
140
153
|
|
|
@@ -142,14 +155,14 @@ module Badline
|
|
|
142
155
|
return unless @talking
|
|
143
156
|
|
|
144
157
|
@talking = false
|
|
158
|
+
time_serial_byte(ISOUR_TIMEOUT)
|
|
145
159
|
release_bus
|
|
146
160
|
end
|
|
147
161
|
|
|
148
|
-
# UNLSN and UNTLK end by releasing ATN, the clock and the data line
|
|
149
|
-
#
|
|
162
|
+
# UNLSN and UNTLK end by releasing ATN, the clock and the data line,
|
|
163
|
+
# and leave the port's last read in the accumulator.
|
|
150
164
|
def release_bus
|
|
151
|
-
@
|
|
152
|
-
@cpu.a = @bus.peek(0xdd00) & 0xdf
|
|
165
|
+
@cpu.a = @layout.release_serial_lines(@bus)
|
|
153
166
|
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
154
167
|
@cpu.status.zero = @cpu.a.zero?
|
|
155
168
|
return_to_caller
|
|
@@ -159,6 +172,7 @@ module Badline
|
|
|
159
172
|
return unless @talking
|
|
160
173
|
|
|
161
174
|
byte, status = received
|
|
175
|
+
time_serial_byte(ACPTR_TIMEOUT)
|
|
162
176
|
@cpu.a = byte
|
|
163
177
|
update_status(status)
|
|
164
178
|
@cpu.status.carry = false
|
data/lib/badline/kernal_trap.rb
CHANGED
data/lib/badline/keyboard.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
-
# The
|
|
5
|
-
# port B.
|
|
4
|
+
# The key matrix spanning CIA 1's two ports: rows on port A, columns on
|
|
5
|
+
# port B. Row lines beyond the eighth sit above port A's bits in +rows+.
|
|
6
6
|
#
|
|
7
7
|
# A pressed key shorts its row line to its column line, so whichever side is
|
|
8
8
|
# pulled low drags the other down with it. Scanning works in either
|
|
@@ -19,22 +19,25 @@ module Badline
|
|
|
19
19
|
cursor_left: %i[lshift cursor_h]
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
|
+
# The C64's wiring: one row of keys per port A line, in port B column
|
|
23
|
+
# order.
|
|
24
|
+
C64_MATRIX = [
|
|
25
|
+
%i[delete return cursor_h f7 f1 f3 f5 cursor_v],
|
|
26
|
+
%i[3 w a 4 z s e lshift],
|
|
27
|
+
%i[5 r d 6 c f t x],
|
|
28
|
+
%i[7 y g 8 b h u v],
|
|
29
|
+
%i[9 i j 0 m k o n],
|
|
30
|
+
%i[+ p l - . : @ ,],
|
|
31
|
+
%i[£ * ; clr_home rshift = up /],
|
|
32
|
+
%i[1 left control 2 space cbm q run_stop]
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
22
35
|
attr_reader :keys, :matrix
|
|
23
36
|
|
|
24
|
-
def initialize
|
|
37
|
+
def initialize(matrix: C64_MATRIX)
|
|
25
38
|
@keys = []
|
|
26
39
|
@row_masks = nil
|
|
27
|
-
|
|
28
|
-
@matrix = [
|
|
29
|
-
%i[delete return cursor_h f7 f1 f3 f5 cursor_v],
|
|
30
|
-
%i[3 w a 4 z s e lshift],
|
|
31
|
-
%i[5 r d 6 c f t x],
|
|
32
|
-
%i[7 y g 8 b h u v],
|
|
33
|
-
%i[9 i j 0 m k o n],
|
|
34
|
-
%i[+ p l - . : @ ,],
|
|
35
|
-
%i[£ * ; clr_home rshift = up /],
|
|
36
|
-
%i[1 left control 2 space cbm q run_stop]
|
|
37
|
-
]
|
|
40
|
+
@matrix = matrix
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def press(key)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
# = Machine
|
|
5
|
+
#
|
|
6
|
+
# The machines badline builds by family, as the command line's
|
|
7
|
+
# subcommand names them: the C64, Computer, and the VIC-20, Vic20.
|
|
8
|
+
#
|
|
9
|
+
# A machine answers what the front end, Media, the runners and the
|
|
10
|
+
# snapshots call on it:
|
|
11
|
+
#
|
|
12
|
+
# - Identity: +family+, the family it was built as.
|
|
13
|
+
# - Clocking: +run_cycles(n)+, +run_until(limit) { ... }+, +cycles+,
|
|
14
|
+
# +on_init+ and +init_threshold+, the cycle its handlers run at.
|
|
15
|
+
# - Lifecycle: +reset!+ and +power_cycle!+.
|
|
16
|
+
# - Video: +video+, the chip the screen reads (+display+, +width+,
|
|
17
|
+
# +dirty_lines+, +clear_dirty_lines!+ and +palette+), and +timing+, a
|
|
18
|
+
# Timing.
|
|
19
|
+
# - Sound: +sound_source+, which answers +record(rate:)+,
|
|
20
|
+
# +drain_samples+ and +model+.
|
|
21
|
+
# - Input: +keyboard+, +joystick1+, +joystick2+, +control_ports+,
|
|
22
|
+
# +press_restore+ and +release_restore+.
|
|
23
|
+
# - Media: +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
|
|
24
|
+
# +mounted?+, +datasette+, +attach_cartridge+,
|
|
25
|
+
# +press_cartridge_button+, +release_cartridge_button+,
|
|
26
|
+
# +attach_drive1541+, +drive1541+, and +address_bus+ for its
|
|
27
|
+
# +cartridge+ and +detach_cartridge+.
|
|
28
|
+
# - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
|
|
29
|
+
# +install_debug_register+ and +capture_output+.
|
|
30
|
+
#
|
|
31
|
+
# The VIC-20 answers the identity, clocking, lifecycle, video, sound and
|
|
32
|
+
# input parts, with its one joystick as both +joystick1+ and +joystick2+
|
|
33
|
+
# and nil for +control_ports+, and of the rest +ram+, +load_prg+,
|
|
34
|
+
# +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
|
|
35
|
+
# +attach_drive1541+, +drive1541+, +install_debug_register+ and
|
|
36
|
+
# +capture_output+.
|
|
37
|
+
module Machine
|
|
38
|
+
# A new machine of `family`, :c64 or :vic20, the model of it `model`
|
|
39
|
+
# names. A C64's model is one of Model::ALL, with its SID `sid_model`
|
|
40
|
+
# when not nil and an REU of `reu` K when not nil. A VIC-20's is "pal",
|
|
41
|
+
# with the RAM expansion `ram` names (Vic20::Bus::RAM_CONFIGURATIONS),
|
|
42
|
+
# unexpanded when nil.
|
|
43
|
+
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
44
|
+
return vic20(model, ram) if family == :vic20
|
|
45
|
+
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
46
|
+
|
|
47
|
+
profile = Model.named(model)
|
|
48
|
+
Computer.new(vic_model: profile.vic_model, cia_model: profile.cia_model,
|
|
49
|
+
sid_model: sid_model || profile.sid_model, region: profile.region, reu:,
|
|
50
|
+
kernal: profile.kernal, datasette: profile.datasette)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.vic20(model, ram)
|
|
54
|
+
raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
|
|
55
|
+
|
|
56
|
+
Vic20.new(ram: ram || :unexpanded)
|
|
57
|
+
end
|
|
58
|
+
private_class_method :vic20
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# A list of the disks of a set: an .m3u playlist, one path a line, or
|
|
6
|
+
# a VICE .vfl flip list, whose paths follow a `UNIT 8` line. Paths are
|
|
7
|
+
# relative to the list or absolute, and blank lines and lines starting
|
|
8
|
+
# with # are skipped. Only the disk images the list names that are
|
|
9
|
+
# there count.
|
|
10
|
+
module DiskList
|
|
11
|
+
# A list naming no disk image that's there.
|
|
12
|
+
class Error < ArgumentError; end
|
|
13
|
+
|
|
14
|
+
EXTENSIONS = %w[.m3u .vfl].freeze
|
|
15
|
+
|
|
16
|
+
def self.list?(path) = EXTENSIONS.include?(File.extname(path).downcase)
|
|
17
|
+
|
|
18
|
+
# The disk `path` puts in the drive: a list's first disk, or the
|
|
19
|
+
# path itself.
|
|
20
|
+
def self.disk(path)
|
|
21
|
+
return path unless list?(path)
|
|
22
|
+
|
|
23
|
+
disks = disks(path)
|
|
24
|
+
raise Error, "#{path} lists no disk image that's there" if disks.empty?
|
|
25
|
+
|
|
26
|
+
disks.first
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The disk images the list names that are there, in its order.
|
|
30
|
+
def self.disks(list)
|
|
31
|
+
directory = File.dirname(list)
|
|
32
|
+
found = []
|
|
33
|
+
entries(list).each do |entry|
|
|
34
|
+
disk = entry.start_with?("/") ? entry : File.join(directory, entry)
|
|
35
|
+
found << disk if DiskSet::EXTENSIONS.include?(File.extname(disk).downcase) && File.file?(disk)
|
|
36
|
+
end
|
|
37
|
+
found
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The paths of the list's lines, only unit 8's for a .vfl, which
|
|
41
|
+
# starts on unit 8.
|
|
42
|
+
def self.entries(list)
|
|
43
|
+
flip = File.extname(list).casecmp?(".vfl")
|
|
44
|
+
unit = true
|
|
45
|
+
found = []
|
|
46
|
+
File.foreach(list) do |line|
|
|
47
|
+
text = line.strip
|
|
48
|
+
next if text.empty? || text.start_with?("#")
|
|
49
|
+
|
|
50
|
+
if flip && text.match?(/\AUNIT\s+\d+\z/i)
|
|
51
|
+
unit = text.split.last.to_i == 8
|
|
52
|
+
elsif unit
|
|
53
|
+
found << text
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
found
|
|
57
|
+
rescue SystemCallError
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The disks of the first list in the disk's folder that names it,
|
|
62
|
+
# with the disk as given, or none.
|
|
63
|
+
def self.beside(path)
|
|
64
|
+
directory = File.dirname(path)
|
|
65
|
+
own = File.expand_path(path)
|
|
66
|
+
lists = DiskSet.children(directory).sort.select { |name| list?(name) }
|
|
67
|
+
sets = lists.map { |name| disks(File.join(directory, name)) }
|
|
68
|
+
set = sets.find { |listed| listed.any? { |disk| File.expand_path(disk) == own } }
|
|
69
|
+
return [] if set.nil?
|
|
70
|
+
|
|
71
|
+
set.map { |disk| File.expand_path(disk) == own ? path : disk }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -8,16 +8,26 @@ module Badline
|
|
|
8
8
|
# by those markers. A name with no marker can end in a number or a
|
|
9
9
|
# letter, as in game_1.d64 and game_2.d64, which counts only when the
|
|
10
10
|
# folder has the first of the set too.
|
|
11
|
+
#
|
|
12
|
+
# A list in the disk's folder that names it (DiskList) gives its set
|
|
13
|
+
# ahead of the names.
|
|
11
14
|
module DiskSet
|
|
12
15
|
KINDS = %w[disk disc side part].freeze
|
|
13
16
|
EXTENSIONS = %w[.d64 .d71 .d81 .g64].freeze
|
|
14
17
|
|
|
15
18
|
# The paths of the set's disks in order, or just `path` when it
|
|
16
|
-
# belongs to none.
|
|
19
|
+
# belongs to none. A list's set is the disks it lists.
|
|
17
20
|
def self.around(path)
|
|
18
|
-
|
|
19
|
-
return [path] unless EXTENSIONS.include?(
|
|
21
|
+
return DiskList.disks(path) if DiskList.list?(path)
|
|
22
|
+
return [path] unless EXTENSIONS.include?(File.extname(path).downcase)
|
|
23
|
+
|
|
24
|
+
listed = DiskList.beside(path)
|
|
25
|
+
listed.empty? ? named(path) : listed
|
|
26
|
+
end
|
|
20
27
|
|
|
28
|
+
# The set of the disk's name.
|
|
29
|
+
def self.named(path)
|
|
30
|
+
extension = File.extname(path).downcase
|
|
21
31
|
marked = !parse(stem(path))[1].negative?
|
|
22
32
|
base = key(stem(path), marked)[0]
|
|
23
33
|
found = siblings(File.dirname(path), extension, base, marked)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# A program loading into a VIC-20 once it has booted (Vic20Media).
|
|
6
|
+
module Vic20Basic
|
|
7
|
+
# Where BASIC starts with each RAM expansion it can start in, as
|
|
8
|
+
# TXTTAB holds it.
|
|
9
|
+
BASIC_STARTS = { 0x1001 => :unexpanded, 0x0401 => :"3k", 0x1201 => :"8k" }.freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# A BASIC program, one that loads at a start of BASIC or a byte
|
|
13
|
+
# ahead of it, goes to this machine's start of BASIC. Anything else
|
|
14
|
+
# goes where it says, and doesn't RUN.
|
|
15
|
+
def load_program(machine, data, autostart:)
|
|
16
|
+
load_addr = data[0] | (data[1] << 8)
|
|
17
|
+
basic_start = machine.basic_start
|
|
18
|
+
ahead = [0, 1].find { |offset| BASIC_STARTS.key?(load_addr + offset) || load_addr + offset == basic_start }
|
|
19
|
+
return machine.load_prg(data) unless ahead
|
|
20
|
+
|
|
21
|
+
load_basic(machine, data, basic_start - ahead, load_addr)
|
|
22
|
+
machine.type_text("run\r") if autostart
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Puts a BASIC program at `address`, relinking its lines when that
|
|
28
|
+
# isn't where it was saved from, as BASIC's LOAD does, and sets the
|
|
29
|
+
# end of the program, where BASIC's variables start, and the end
|
|
30
|
+
# address the KERNAL's LOAD leaves.
|
|
31
|
+
def load_basic(machine, data, address, saved_at)
|
|
32
|
+
ram = machine.ram
|
|
33
|
+
ram.write(address, data[2..])
|
|
34
|
+
end_addr = address + data.length - 2
|
|
35
|
+
relink(ram, machine.basic_start, end_addr) unless address == saved_at
|
|
36
|
+
[0x2d, 0xae].each { |pointer| ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Points each line's link at the line after it, as BASIC's LINKPRG
|
|
40
|
+
# does: past the zero that ends the line's text. A link with a zero
|
|
41
|
+
# high byte ends the program.
|
|
42
|
+
def relink(ram, line, end_addr)
|
|
43
|
+
while line + 4 < end_addr && ram.peek(line + 1).positive?
|
|
44
|
+
following = line + 4
|
|
45
|
+
following += 1 while following < end_addr && ram.peek(following).positive?
|
|
46
|
+
following += 1
|
|
47
|
+
ram.write(line, [following & 0xff, following >> 8])
|
|
48
|
+
line = following
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# A VIC-20 cartridge kept as programs, one per ROM chip, each loading
|
|
6
|
+
# at the block the chip goes in. The chip in BLK5 at $A000 starts the
|
|
7
|
+
# cartridge, and a bigger cartridge has more in BLK1 to BLK3. The files
|
|
8
|
+
# of one cartridge share a name, with each block's address in its
|
|
9
|
+
# extension (game.a0, game.60) or its name (game-a000.prg,
|
|
10
|
+
# game-6000.prg).
|
|
11
|
+
module Vic20Cartridge
|
|
12
|
+
BLOCKS = [0xa000, 0x2000, 0x4000, 0x6000].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# The ROM chips, as CRTFile::Chips, of the cartridge `data`, the
|
|
16
|
+
# program at `path`, is a part of, or nil when it isn't one: a
|
|
17
|
+
# program loading at $A000, or one loading at the start of BLK1 to
|
|
18
|
+
# BLK3 with an $A000 part beside it.
|
|
19
|
+
def chips(path, data)
|
|
20
|
+
own = load_address(data)
|
|
21
|
+
return unless BLOCKS.include?(own)
|
|
22
|
+
|
|
23
|
+
parts = BLOCKS.filter_map { |address| address == own ? data : part(path, own, address) }
|
|
24
|
+
return unless parts.any? { |part| load_address(part) == 0xa000 }
|
|
25
|
+
|
|
26
|
+
parts.map { |part| rom_chip(part) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A program's bytes, unwrapped from a .p00.
|
|
30
|
+
def program_bytes(path)
|
|
31
|
+
bytes = File.binread(path).bytes
|
|
32
|
+
Storage::P00.wraps?(bytes) ? Storage::P00.data(bytes) : bytes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def load_address(data)
|
|
38
|
+
data[0] | (data[1] << 8) if data.length > 2
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def part(path, own, address)
|
|
42
|
+
name = part_names(path, own, address).find { |candidate| File.file?(candidate) }
|
|
43
|
+
return unless name
|
|
44
|
+
|
|
45
|
+
part = program_bytes(name)
|
|
46
|
+
part if load_address(part) == address
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `path` with the block at `own` in its extension or its name
|
|
50
|
+
# swapped for the one at `address`, its letters in either case.
|
|
51
|
+
def part_names(path, own, address)
|
|
52
|
+
extension = File.extname(path)
|
|
53
|
+
base = path[0, path.length - extension.length]
|
|
54
|
+
own_tag = own.to_s(16)
|
|
55
|
+
tags = [address.to_s(16), address.to_s(16).upcase].uniq
|
|
56
|
+
return tags.map { |tag| "#{base}.#{tag[0, 2]}" } if extension.downcase == ".#{own_tag[0, 2]}"
|
|
57
|
+
|
|
58
|
+
name = File.basename(base)
|
|
59
|
+
at = name.downcase.index(own_tag)
|
|
60
|
+
return [] unless at
|
|
61
|
+
|
|
62
|
+
tags.map { |tag| File.join(File.dirname(base), name[0, at] + tag + name[(at + 4)..]) + extension }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A ROM chip of whole pages, the last padded with $FF.
|
|
66
|
+
def rom_chip(part)
|
|
67
|
+
data = part[2..]
|
|
68
|
+
data += [0xff] * (-data.length % 0x100)
|
|
69
|
+
Storage::CRTFile::Chip.new(chip_type: 0, bank: 0, address: load_address(part), data:)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# Media on a VIC-20. A program loads once the KERNAL has booted: a
|
|
6
|
+
# BASIC program at the start of BASIC, wherever the RAM puts it, and
|
|
7
|
+
# RUNs. A program that loads into BLK5 at $A000 is a cartridge's ROM
|
|
8
|
+
# instead, as are the parts of a set that includes one
|
|
9
|
+
# (Vic20Cartridge), and goes in with a power cycle, as a .crt does.
|
|
10
|
+
# Disks mount on device 8 through the KERNAL traps, or go in a true
|
|
11
|
+
# 1541 (TrueDrive), and tapes play on the datasette.
|
|
12
|
+
module Vic20Media
|
|
13
|
+
# The expansions that fill BLK1, BLK2 and BLK3 one block after
|
|
14
|
+
# another, each with the last address it fills.
|
|
15
|
+
BLOCK_EXPANSIONS = [[0x3fff, :"8k"], [0x5fff, :"16k"], [0x7fff, :"24k"]].freeze
|
|
16
|
+
|
|
17
|
+
# What a VIC-20 doesn't take.
|
|
18
|
+
UNSUPPORTED = %w[.sid].freeze
|
|
19
|
+
|
|
20
|
+
# The platform byte of a VIC-20 tape (Storage::TAP#platform).
|
|
21
|
+
TAPE_PLATFORM = 1
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def attach(machine, path, autostart:, **options)
|
|
25
|
+
extension = File.extname(path).downcase
|
|
26
|
+
raise ArgumentError, "#{path} doesn't go in a VIC-20" if UNSUPPORTED.include?(extension)
|
|
27
|
+
|
|
28
|
+
if TrueDrive.takes?(machine, path)
|
|
29
|
+
attach_true_drive(machine, path, options.fetch(:disk, {}), autostart:)
|
|
30
|
+
elsif File.directory?(path)
|
|
31
|
+
machine.mount(Storage::HostDirectory.new(path))
|
|
32
|
+
"Mounted #{path} as device 8"
|
|
33
|
+
elsif extension == ".crt"
|
|
34
|
+
attach_cartridge(machine, path, Storage::CRTFile.new(path, machine: :vic20).chips)
|
|
35
|
+
elsif extension == ".tap"
|
|
36
|
+
attach_tape(machine, path, autostart:)
|
|
37
|
+
elsif MOUNT_TYPES.key?(extension)
|
|
38
|
+
attach_storage(machine, path, options.fetch(:disk, {}), autostart:)
|
|
39
|
+
else
|
|
40
|
+
attach_prg(machine, path, autostart:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The RAM expansion (Vic20::Bus::RAM_CONFIGURATIONS) a VIC-20 needs
|
|
45
|
+
# for the program at `path`, or for the first program on the disk
|
|
46
|
+
# or in the directory there, by where it loads. A BASIC program
|
|
47
|
+
# needs the expansion its start of BASIC comes with, and more of
|
|
48
|
+
# BLK1 to BLK3 when it runs past $3FFF. So does a machine-language
|
|
49
|
+
# program loading into those blocks. Anything else, cartridges
|
|
50
|
+
# included, runs unexpanded.
|
|
51
|
+
def ram_for(path)
|
|
52
|
+
data = first_program(path)
|
|
53
|
+
return :unexpanded unless data && data.length > 2 && !Vic20Cartridge.chips(path, data)
|
|
54
|
+
|
|
55
|
+
load_addr = data[0] | (data[1] << 8)
|
|
56
|
+
if load_addr == 0x1201 || load_addr.between?(0x2000, 0x7fff)
|
|
57
|
+
expansion_reaching(load_addr + data.length - 3)
|
|
58
|
+
else
|
|
59
|
+
Vic20Basic::BASIC_STARTS.fetch(load_addr, :unexpanded)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def expansion_reaching(last)
|
|
66
|
+
BLOCK_EXPANSIONS.each { |reach, ram| return ram if last <= reach }
|
|
67
|
+
BLOCK_EXPANSIONS.last.last
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def first_program(path)
|
|
71
|
+
extension = File.extname(path).downcase
|
|
72
|
+
if File.directory?(path) || MOUNT_TYPES.key?(extension)
|
|
73
|
+
open_storage(path).read_file("*")
|
|
74
|
+
elsif File.file?(path) && !%w[.sid .crt .tap .g64].include?(extension)
|
|
75
|
+
Vic20Cartridge.program_bytes(path)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def open_storage(path, disk = {})
|
|
80
|
+
return Storage::HostDirectory.new(path) if File.directory?(path)
|
|
81
|
+
|
|
82
|
+
storage = MOUNT_TYPES.fetch(File.extname(path).downcase)
|
|
83
|
+
storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attach_cartridge(machine, path, chips)
|
|
87
|
+
machine.attach_cartridge(chips)
|
|
88
|
+
"Attached cartridge #{path}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A tape made for another machine plays all the same, and says so.
|
|
92
|
+
def attach_tape(machine, path, autostart:)
|
|
93
|
+
tape = Storage::TAP.new(path)
|
|
94
|
+
machine.datasette.insert(tape)
|
|
95
|
+
machine.datasette.play!
|
|
96
|
+
machine.type_text(TAPE_AUTOSTART) if autostart
|
|
97
|
+
message = "Inserted #{path} in the datasette"
|
|
98
|
+
tape.platform == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the VIC-20"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def attach_true_drive(machine, path, disk, autostart:)
|
|
102
|
+
message = TrueDrive.insert(machine, path, read_only: disk.fetch(:read_only, false))
|
|
103
|
+
machine.type_text(disk_autostart(first_program(path))) if autostart
|
|
104
|
+
message
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def attach_storage(machine, path, disk, autostart:)
|
|
108
|
+
storage = open_storage(path, disk)
|
|
109
|
+
machine.mount(storage)
|
|
110
|
+
machine.type_text(disk_autostart(storage.read_file("*"))) if autostart
|
|
111
|
+
"Mounted #{path} as device 8"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A BASIC program loads relocated to the start of BASIC, and anything
|
|
115
|
+
# else where it says.
|
|
116
|
+
def disk_autostart(data)
|
|
117
|
+
basic = data && data.length > 2 && Vic20Basic::BASIC_STARTS.key?(data[0] | (data[1] << 8))
|
|
118
|
+
basic ? %(lO"*",8\rrun\r) : AUTOSTART
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def attach_prg(machine, path, autostart:)
|
|
122
|
+
bytes = Vic20Cartridge.program_bytes(path)
|
|
123
|
+
chips = Vic20Cartridge.chips(path, bytes)
|
|
124
|
+
return attach_cartridge(machine, path, chips) if chips
|
|
125
|
+
|
|
126
|
+
machine.on_init { Vic20Basic.load_program(machine, bytes, autostart:) }
|
|
127
|
+
"Loading #{path}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|