badline 0.14.0 → 0.15.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 +23 -0
- data/CLAUDE.md +15 -5
- data/README.md +68 -8
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +188 -38
- data/doc/snapshots.md +18 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +2 -12
- data/lib/badline/c128/banks.rb +1 -3
- data/lib/badline/c128/bus.rb +19 -1
- data/lib/badline/c128/mmu_pages.rb +7 -0
- data/lib/badline/c64/bus/extra_sids.rb +80 -0
- data/lib/badline/c64/bus.rb +1 -0
- data/lib/badline/c64/cartridge/action_replay.rb +6 -2
- data/lib/badline/c64/cartridge/comal80.rb +1 -0
- data/lib/badline/c64/cartridge/dinamic.rb +3 -1
- data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
- data/lib/badline/c64/cartridge/fun_play.rb +2 -1
- data/lib/badline/c64/cartridge/game_system.rb +2 -1
- data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
- data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
- data/lib/badline/c64/cartridge/ocean.rb +1 -0
- data/lib/badline/c64/cartridge/pagefox.rb +1 -0
- data/lib/badline/c64/cartridge/rgcd.rb +2 -1
- data/lib/badline/c64/cartridge/super_games.rb +3 -2
- data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
- data/lib/badline/c64/cartridge.rb +3 -3
- data/lib/badline/c64/keyed_modules.rb +6 -2
- data/lib/badline/chips/ted/beam.rb +13 -14
- data/lib/badline/chips/ted/fetches.rb +199 -0
- data/lib/badline/chips/ted/keyed_state.rb +156 -0
- data/lib/badline/chips/ted/modes.rb +134 -0
- data/lib/badline/chips/ted/palette.rb +43 -0
- data/lib/badline/chips/ted/picture.rb +182 -0
- data/lib/badline/chips/ted/sound/output.rb +86 -0
- data/lib/badline/chips/ted/sound.rb +284 -0
- data/lib/badline/chips/ted.rb +53 -6
- data/lib/badline/chips/tia.rb +12 -0
- data/lib/badline/drives/datasette/keyed_state.rb +7 -2
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/frontend/boot.rb +3 -1
- data/lib/badline/frontend/controls.rb +7 -3
- data/lib/badline/frontend/keys.rb +14 -2
- data/lib/badline/frontend/media_slots.rb +11 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
- data/lib/badline/frontend/menu/menu_media.rb +13 -3
- data/lib/badline/frontend/menu/menu_pages.rb +8 -8
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/screen.rb +33 -12
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/sound.rb +4 -4
- data/lib/badline/frontend/timeline.rb +19 -6
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +26 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +30 -6
- data/lib/badline/options/table.rb +15 -2
- data/lib/badline/options/validation.rb +26 -4
- data/lib/badline/options.rb +37 -9
- data/lib/badline/plus4/bus.rb +77 -14
- data/lib/badline/plus4/cassette_sense.rb +21 -0
- data/lib/badline/plus4/keyboard_port.rb +35 -10
- data/lib/badline/plus4/keyed_modules.rb +99 -0
- data/lib/badline/plus4/keyed_parts.rb +143 -0
- data/lib/badline/plus4/keyed_state.rb +41 -0
- data/lib/badline/plus4.rb +92 -16
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
- data/lib/badline/snapshot/field_doc.rb +46 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +39 -0
- data/lib/badline/snapshot.rb +9 -4
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/storage/tap.rb +21 -12
- data/lib/badline/support/checkpoint.rb +3 -2
- data/lib/badline/support/plus4_checkpoint.rb +42 -0
- data/lib/badline/version.rb +1 -1
- metadata +15 -1
|
@@ -2,26 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Media
|
|
5
|
-
# A program loading into a C64 or a
|
|
6
|
-
# (Media.attach). The VIC-20 has its own (Vic20Basic).
|
|
5
|
+
# A program loading into a C64, a C128 or a 264 series machine once it
|
|
6
|
+
# has booted (Media.attach). The VIC-20 has its own (Vic20Basic).
|
|
7
7
|
module Program
|
|
8
|
+
# Where BASIC 3.5 starts on the 264 series.
|
|
9
|
+
PLUS4_BASIC_START = 0x1001
|
|
10
|
+
|
|
8
11
|
class << self
|
|
9
12
|
# Loads the program where it says, and RUNs one that loads at the
|
|
10
13
|
# start of BASIC.
|
|
11
14
|
def load(computer, data, autostart:)
|
|
12
15
|
load_addr = computer.load_prg(data)
|
|
16
|
+
start, text_end, load_end = basic(computer)
|
|
13
17
|
# Run only makes sense for programs at BASIC start, or a byte ahead
|
|
14
|
-
# of it, where BASIC keeps the zero before its first line.
|
|
15
|
-
# starts at $1C01 and keeps the end of its text at $1210.
|
|
16
|
-
start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
|
|
18
|
+
# of it, where BASIC keeps the zero before its first line.
|
|
17
19
|
return unless autostart && (load_addr == start || load_addr == start - 1)
|
|
18
20
|
|
|
19
21
|
# The end of the program, where BASIC's variables start, and where
|
|
20
22
|
# the KERNAL's LOAD leaves its end address.
|
|
21
23
|
end_addr = load_addr + data.length - 2
|
|
22
|
-
[text_end,
|
|
24
|
+
[text_end, load_end].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
|
|
23
25
|
computer.type_text("run\r")
|
|
24
26
|
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Where BASIC starts, the pointer to the end of its text and the
|
|
31
|
+
# KERNAL's end address (EAL). BASIC 7.0 starts at $1C01 and keeps
|
|
32
|
+
# the end of its text at $1210, and the 264 KERNAL keeps EAL at $9D.
|
|
33
|
+
def basic(computer)
|
|
34
|
+
return [PLUS4_BASIC_START, 0x2d, KernalTrap::PLUS4_ZERO_PAGE.end_address] if computer.family == :plus4
|
|
35
|
+
return [0x1c01, 0x1210, 0xae] if computer.family == :c128 && computer.mode == :c128
|
|
36
|
+
|
|
37
|
+
[BASIC_START, 0x2d, 0xae]
|
|
38
|
+
end
|
|
25
39
|
end
|
|
26
40
|
end
|
|
27
41
|
end
|
data/lib/badline/media.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "badline/media/disk_list"
|
|
|
10
10
|
require "badline/media/vic20_basic"
|
|
11
11
|
require "badline/media/vic20_cartridge"
|
|
12
12
|
require "badline/media/vic20_media"
|
|
13
|
+
require "badline/media/plus4_media"
|
|
13
14
|
require "badline/media/tune"
|
|
14
15
|
require "badline/media/program"
|
|
15
16
|
|
|
@@ -44,13 +45,14 @@ module Badline
|
|
|
44
45
|
# An .m3u or .vfl list of disks attaches the first disk it lists
|
|
45
46
|
# (DiskList). Every machine takes each kind of medium the same way,
|
|
46
47
|
# except that the VIC-20 (Vic20Media) has cartridges, programs and a
|
|
47
|
-
# disk autostart of its own, and takes no .sid tunes
|
|
48
|
-
#
|
|
48
|
+
# disk autostart of its own, and takes no .sid tunes, and the 264
|
|
49
|
+
# series loads programs at the start of its BASIC (Program), takes
|
|
50
|
+
# .crt cartridges and raw .bin ROM images in its two cartridges'
|
|
51
|
+
# slots (Plus4Media), and no .sid tunes either. The
|
|
52
|
+
# C128 in C64 mode takes a C64's.
|
|
49
53
|
def attach(computer, path, autostart: true, subtune: nil, **)
|
|
50
54
|
path = DiskList.disk(path)
|
|
51
|
-
|
|
52
|
-
raise ArgumentError, "#{path} doesn't go in a VIC-20"
|
|
53
|
-
end
|
|
55
|
+
refuse_unsupported(computer, path)
|
|
54
56
|
|
|
55
57
|
BootDisk.attach(computer, path, **) || attach_kind(computer, path, autostart:, subtune:, **)
|
|
56
58
|
end
|
|
@@ -89,10 +91,12 @@ module Badline
|
|
|
89
91
|
# What the medium at `path` is to `computer`, which attach goes by:
|
|
90
92
|
# :true_drive for a disk that goes in a true drive (TrueDrive),
|
|
91
93
|
# :directory, :storage for a disk image or a .t64 that the traps
|
|
92
|
-
# mount,
|
|
94
|
+
# mount, :cartridge for a raw ROM image on the 264 series
|
|
95
|
+
# (Plus4Media), and otherwise its kind by its extension (Extensions).
|
|
93
96
|
def kind(computer, path)
|
|
94
97
|
return :true_drive if TrueDrive.takes?(computer, path)
|
|
95
98
|
return :directory if File.directory?(path)
|
|
99
|
+
return :cartridge if computer.family == :plus4 && Plus4Media.rom_image?(path)
|
|
96
100
|
|
|
97
101
|
kind = Extensions.kind(path)
|
|
98
102
|
%i[disk archive].include?(kind) ? :storage : kind
|
|
@@ -116,6 +120,16 @@ module Badline
|
|
|
116
120
|
|
|
117
121
|
private
|
|
118
122
|
|
|
123
|
+
# The VIC-20 and the 264 series have no SID to play a .sid tune on.
|
|
124
|
+
def refuse_unsupported(computer, path)
|
|
125
|
+
return unless Vic20Media::UNSUPPORTED.include?(File.extname(path).downcase)
|
|
126
|
+
|
|
127
|
+
case computer.family
|
|
128
|
+
when :vic20 then raise ArgumentError, "#{path} doesn't go in a VIC-20"
|
|
129
|
+
when :plus4 then raise ArgumentError, "#{path} doesn't go in a Plus/4"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
119
133
|
def attach_kind(computer, path, autostart:, subtune:, **options)
|
|
120
134
|
disk = options.fetch(:disk, {})
|
|
121
135
|
case kind(computer, path)
|
|
@@ -151,25 +165,32 @@ module Badline
|
|
|
151
165
|
computer.family == :vic20 ? Vic20Media.disk_autostart(yield) : AUTOSTART
|
|
152
166
|
end
|
|
153
167
|
|
|
154
|
-
# A VIC-20 cartridge is its ROM chips.
|
|
168
|
+
# A VIC-20 or 264 series cartridge is its ROM chips. On the 264, the
|
|
169
|
+
# options' `slot` names the slot a raw ROM image goes in
|
|
170
|
+
# (Plus4Media.cartridge_chips).
|
|
155
171
|
def attach_cartridge(computer, path, options)
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
case computer.family
|
|
173
|
+
when :vic20 then computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
|
|
174
|
+
when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
|
|
158
175
|
else
|
|
159
176
|
computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
|
|
160
177
|
end
|
|
161
178
|
"Attached cartridge #{path}"
|
|
162
179
|
end
|
|
163
180
|
|
|
164
|
-
# A VIC-20 says when the tape was made for another machine
|
|
165
|
-
# (Vic20Media.tape_message).
|
|
181
|
+
# A VIC-20 or a 264 says when the tape was made for another machine
|
|
182
|
+
# (Vic20Media.tape_message, Plus4Media.tape_message).
|
|
166
183
|
def attach_tape(computer, path, autostart:)
|
|
167
184
|
tape = Storage::TAP.new(path)
|
|
168
185
|
computer.datasette.insert(tape)
|
|
169
186
|
computer.datasette.play!
|
|
170
187
|
computer.type_text(TAPE_AUTOSTART) if autostart
|
|
171
188
|
message = "Inserted #{path} in the datasette"
|
|
172
|
-
computer.family
|
|
189
|
+
case computer.family
|
|
190
|
+
when :vic20 then Vic20Media.tape_message(tape, message)
|
|
191
|
+
when :plus4 then Plus4Media.tape_message(tape, message)
|
|
192
|
+
else message
|
|
193
|
+
end
|
|
173
194
|
end
|
|
174
195
|
|
|
175
196
|
# A VIC-20 has programs of its own (Vic20Media.attach_program).
|
data/lib/badline/options/help.rb
CHANGED
|
@@ -16,6 +16,16 @@ module Badline
|
|
|
16
16
|
filter cycle by cycle, which is exact but takes about twice as long.
|
|
17
17
|
TEXT
|
|
18
18
|
|
|
19
|
+
# The article for +name+: "an" where it starts with a vowel sound. A
|
|
20
|
+
# name of letters with a vowel among them is read as a word, and any
|
|
21
|
+
# other letter by letter, so .sid and .tap take "a", and .m3u and .lnx
|
|
22
|
+
# "an".
|
|
23
|
+
def self.article(name)
|
|
24
|
+
word = name.delete_prefix(".").downcase
|
|
25
|
+
vowels = word.match?(/\A[a-z]*[aeiou][a-z]*\z/) ? "aeiou" : "aefhilmnorsx8"
|
|
26
|
+
vowels.include?(word[0]) ? "an" : "a"
|
|
27
|
+
end
|
|
28
|
+
|
|
19
29
|
# What --help prints: the usage, then the options this build takes,
|
|
20
30
|
# section by section, laid out as OptionParser did.
|
|
21
31
|
def help
|
|
@@ -38,14 +48,19 @@ module Badline
|
|
|
38
48
|
# The media the window opens, by their extensions (Media::Extensions),
|
|
39
49
|
# wrapped as the banner is.
|
|
40
50
|
def media_kinds
|
|
41
|
-
text = "Media can be
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"cartridge,
|
|
51
|
+
text = "Media can be #{listed(Media::Extensions::PROGRAMS)} program, #{listed(%i[disk])} disk image, " \
|
|
52
|
+
"#{listed(%i[gcr])} disk image for a true drive, #{listed(%i[disk_list])} list of disk images, " \
|
|
53
|
+
"#{listed(%i[archive])} tape archive, #{listed(%i[tape])} tape, #{listed(%i[cartridge])} " \
|
|
54
|
+
"cartridge, #{listed(%i[tune])} tune, #{listed(%w[.vsf])} snapshot, or a directory to mount as device 8."
|
|
45
55
|
wrap(text)
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
# The extensions of the kinds, or the extensions themselves, after the
|
|
59
|
+
# article the first takes.
|
|
60
|
+
def listed(kinds)
|
|
61
|
+
extensions = kinds.first.is_a?(Symbol) ? Media::Extensions.of(kinds) : kinds
|
|
62
|
+
"#{Options.article(extensions.first)} #{extensions.join('/')}"
|
|
63
|
+
end
|
|
49
64
|
|
|
50
65
|
# The words of `text` in lines of at most 70 characters.
|
|
51
66
|
def wrap(text)
|
|
@@ -68,6 +83,7 @@ module Badline
|
|
|
68
83
|
Usage: #{@program} [c64] [options] [media]
|
|
69
84
|
#{@program} vic20 [options] [media]
|
|
70
85
|
#{@program} c128 [options] [media]
|
|
86
|
+
#{@program} plus4 [options] [media]
|
|
71
87
|
#{@program} --headless [options] tune.sid
|
|
72
88
|
#{@program} [options] tune.sid --audio-out FILE
|
|
73
89
|
#{@program} sid [options] FILE|DIR...
|
|
@@ -76,6 +92,7 @@ module Badline
|
|
|
76
92
|
c64 Run a C64, the one --model names (the default)
|
|
77
93
|
vic20 Run a PAL VIC-20, with the RAM --ram names
|
|
78
94
|
c128 Run a C128 in C128 mode, the one --model names
|
|
95
|
+
plus4 Run a Plus/4 or a C16, the one --model names
|
|
79
96
|
sid Play .sid tunes and directories of them
|
|
80
97
|
|
|
81
98
|
#{media_kinds}
|
|
@@ -91,6 +108,12 @@ module Badline
|
|
|
91
108
|
tune opens in the SID player, as `sid` plays it, unless an option
|
|
92
109
|
of the emulator's window asks for the machine.
|
|
93
110
|
|
|
111
|
+
The Plus/4 and the C16 take programs, disks, tapes, and cartridges as
|
|
112
|
+
.crt files or raw .bin ROM images. A .bin goes in cartridge 1, its
|
|
113
|
+
low slot for up to 16K and both slots for 32K, and --c1lo, --c1hi,
|
|
114
|
+
--c2lo and --c2hi put one in the slot they name. There Escape is ESC,
|
|
115
|
+
Page Down RUN/STOP and F4 HELP.
|
|
116
|
+
|
|
94
117
|
--headless plays a .sid tune on the host's audio device without the
|
|
95
118
|
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
96
119
|
container follows the file's extension, .wav or .aiff. Played on a
|
|
@@ -111,7 +134,8 @@ module Badline
|
|
|
111
134
|
--at and --script run events once a number of frames has run, as
|
|
112
135
|
in --at 3500:key=space or --at 250,500:screenshot=shot%05d.bmp.
|
|
113
136
|
key=NAME presses a key (space, return, a, f1, run_stop, restore, and
|
|
114
|
-
on the C128 esc, help, keypad1 and the rest
|
|
137
|
+
on the C128 esc, help, keypad1 and the rest, and on the Plus/4
|
|
138
|
+
esc, help and f1 to f3) or a joystick's
|
|
115
139
|
direction or fire (joy1-up, joy2-fire) for #{Event::HOLD} frames, and
|
|
116
140
|
pushes the C128's caps_lock or forty_eighty, which lock. type=TEXT
|
|
117
141
|
types it, with \\n for RETURN. insert=FILE swaps in a disk, a list's
|
|
@@ -14,6 +14,10 @@ module Badline
|
|
|
14
14
|
# The C128's models, as C128::Model names them.
|
|
15
15
|
C128_MODELS = %w[c128 c128ntsc c128dcr c128dcrntsc].freeze
|
|
16
16
|
|
|
17
|
+
# The 264 series' models, as Plus4::Model names them, with the C116 as
|
|
18
|
+
# another name for the C16.
|
|
19
|
+
PLUS4_MODELS = %w[plus4 plus4ntsc c16 c16ntsc c116].freeze
|
|
20
|
+
|
|
17
21
|
# The VIC-20 RAM expansions --ram takes, in the order the help lists
|
|
18
22
|
# them.
|
|
19
23
|
RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
|
|
@@ -40,12 +44,21 @@ module Badline
|
|
|
40
44
|
Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
|
|
41
45
|
section: "Window options", needs: :window),
|
|
42
46
|
Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal; " \
|
|
43
|
-
"C128: #{C128_MODELS.join(', ')}",
|
|
47
|
+
"C128: #{C128_MODELS.join(', ')}; Plus/4: #{PLUS4_MODELS.join(', ')}",
|
|
44
48
|
section: "Window options", needs: :window),
|
|
45
49
|
Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
|
|
46
50
|
Option.new("--c64", "Start the C128 in C64 mode, holding C= at power-on",
|
|
47
51
|
section: "Window options", needs: :window),
|
|
48
|
-
Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded)"
|
|
52
|
+
Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded); " \
|
|
53
|
+
"Plus/4 RAM: 16k, 32k or 64k (default: the model's own)",
|
|
54
|
+
section: "Window options", needs: :window),
|
|
55
|
+
Option.new("--c1lo FILE", "Put a .bin ROM image in the Plus/4's cartridge 1, low slot ($8000)",
|
|
56
|
+
section: "Window options", needs: :window),
|
|
57
|
+
Option.new("--c1hi FILE", "Put a .bin ROM image in cartridge 1's high slot ($C000)",
|
|
58
|
+
section: "Window options", needs: :window),
|
|
59
|
+
Option.new("--c2lo FILE", "Put a .bin ROM image in cartridge 2's low slot ($8000)",
|
|
60
|
+
section: "Window options", needs: :window),
|
|
61
|
+
Option.new("--c2hi FILE", "Put a .bin ROM image in cartridge 2's high slot ($C000)",
|
|
49
62
|
section: "Window options", needs: :window),
|
|
50
63
|
Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
|
|
51
64
|
section: "Window options", needs: :window),
|
|
@@ -21,6 +21,12 @@ module Badline
|
|
|
21
21
|
RAM_CONFIGURATIONS[value]
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def plus4_ram(value)
|
|
25
|
+
raise Error, "invalid argument: --ram #{value}" unless PLUS4_RAM.key?(value)
|
|
26
|
+
|
|
27
|
+
PLUS4_RAM[value]
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
def reu_size(value)
|
|
25
31
|
raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
|
|
26
32
|
|
|
@@ -59,12 +65,15 @@ module Badline
|
|
|
59
65
|
end
|
|
60
66
|
|
|
61
67
|
def validate_family
|
|
62
|
-
raise Error, "the Plus/4 isn't emulated yet" if @family == :plus4
|
|
63
|
-
|
|
64
68
|
if @family == :vic20
|
|
65
69
|
validate_vic20
|
|
66
70
|
elsif !@ram.nil?
|
|
67
|
-
raise Error, "--ram needs vic20"
|
|
71
|
+
raise Error, "--ram needs vic20 or plus4"
|
|
72
|
+
end
|
|
73
|
+
if @family == :plus4
|
|
74
|
+
validate_plus4
|
|
75
|
+
elsif !@cartridges.empty?
|
|
76
|
+
raise Error, "--#{@cartridges.keys.first} needs plus4"
|
|
68
77
|
end
|
|
69
78
|
if @family == :c128
|
|
70
79
|
validate_c128
|
|
@@ -91,6 +100,19 @@ module Badline
|
|
|
91
100
|
raise Error, "the VIC-20 needs the window" if headless?
|
|
92
101
|
end
|
|
93
102
|
|
|
103
|
+
# The Plus/4 runs in the window, without the C64's SID and REU, its
|
|
104
|
+
# NTSC models have names of their own, and its cartridge slots take
|
|
105
|
+
# raw ROM images.
|
|
106
|
+
def validate_plus4
|
|
107
|
+
raise Error, "--ntsc needs the C64; the NTSC Plus/4 is --model plus4ntsc" if @models.include?("ntsc")
|
|
108
|
+
raise Error, "--sid needs the C64" unless @sid_model.nil?
|
|
109
|
+
raise Error, "--reu needs the C64" unless @reu.nil?
|
|
110
|
+
raise Error, "the Plus/4 needs the window" if headless?
|
|
111
|
+
|
|
112
|
+
image = @cartridges.values.find { |path| !File.extname(path).casecmp?(".bin") }
|
|
113
|
+
raise Error, "not a .bin ROM image: #{image}" unless image.nil?
|
|
114
|
+
end
|
|
115
|
+
|
|
94
116
|
# `sid` takes directories as well as tunes.
|
|
95
117
|
def validate_tune(path)
|
|
96
118
|
return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
|
|
@@ -120,7 +142,7 @@ module Badline
|
|
|
120
142
|
end
|
|
121
143
|
|
|
122
144
|
def validate_paths
|
|
123
|
-
missing = tune_paths.find { |path| !File.exist?(path) }
|
|
145
|
+
missing = (tune_paths + @cartridges.values).find { |path| !File.exist?(path) }
|
|
124
146
|
raise Error, "no such file or directory: #{missing}" unless missing.nil?
|
|
125
147
|
raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
|
|
126
148
|
end
|
data/lib/badline/options.rb
CHANGED
|
@@ -12,9 +12,9 @@ module Badline
|
|
|
12
12
|
# Either opens the window for any media, or with --headless or
|
|
13
13
|
# --audio-out plays or renders a .sid tune without one. `sid`, given
|
|
14
14
|
# first, plays .sid tunes and directories of them in the terminal.
|
|
15
|
-
# `c64`, `vic20` or `
|
|
16
|
-
# without one the family is the C64, or the C128 for a program
|
|
17
|
-
# where C128 BASIC starts.
|
|
15
|
+
# `c64`, `vic20`, `c128` or `plus4`, given first, names the machine
|
|
16
|
+
# family, and without one the family is the C64, or the C128 for a program
|
|
17
|
+
# that loads where C128 BASIC starts. The native build adds --no-sound and
|
|
18
18
|
# --version, and badline-ruby adds --disable-jit.
|
|
19
19
|
#
|
|
20
20
|
# TABLE lists the options, and both the parser and the help read it. It
|
|
@@ -48,6 +48,13 @@ module Badline
|
|
|
48
48
|
"32k" => :"32k", "all" => :all
|
|
49
49
|
}.freeze
|
|
50
50
|
|
|
51
|
+
# The Plus/4's RAM sizes as --ram names them, in K.
|
|
52
|
+
PLUS4_RAM = { "16k" => 16, "32k" => 32, "64k" => 64 }.freeze
|
|
53
|
+
|
|
54
|
+
# The Plus/4's cartridge slots, by the options that fill them and
|
|
55
|
+
# xplus4's names (Media::Plus4Media::SLOTS).
|
|
56
|
+
CARTRIDGE_SLOTS = { "--c1lo" => "c1lo", "--c1hi" => "c1hi", "--c2lo" => "c2lo", "--c2hi" => "c2hi" }.freeze
|
|
57
|
+
|
|
51
58
|
# A Float as OptionParser took one: digits with an optional sign,
|
|
52
59
|
# fraction and exponent.
|
|
53
60
|
DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
|
|
@@ -57,14 +64,22 @@ module Badline
|
|
|
57
64
|
attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
|
|
58
65
|
:audio_out, :seconds, :songlengths, :filter_chunk, :timeline
|
|
59
66
|
|
|
60
|
-
# The machine family to build (Badline::Machine), :c64, :vic20
|
|
61
|
-
# :
|
|
67
|
+
# The machine family to build (Badline::Machine), :c64, :vic20, :c128
|
|
68
|
+
# or :plus4.
|
|
62
69
|
attr_reader :family
|
|
63
70
|
|
|
64
71
|
# The VIC-20's RAM expansion --ram names, a key of
|
|
65
72
|
# Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
|
|
66
73
|
attr_reader :ram
|
|
67
74
|
|
|
75
|
+
# The Plus/4's RAM in K --ram names, one of PLUS4_RAM's, or nil without
|
|
76
|
+
# --ram.
|
|
77
|
+
attr_reader :ram_kb
|
|
78
|
+
|
|
79
|
+
# The Plus/4's cartridge ROM images --c1lo, --c1hi, --c2lo and --c2hi
|
|
80
|
+
# name, by slot: { "c1lo" => path }.
|
|
81
|
+
attr_reader :cartridges
|
|
82
|
+
|
|
68
83
|
def self.parse(argv, native: false) = new(native:).parse(argv)
|
|
69
84
|
|
|
70
85
|
# The native build plays the SID in the window unless --no-sound says
|
|
@@ -84,6 +99,8 @@ module Badline
|
|
|
84
99
|
@models = []
|
|
85
100
|
@reu = nil
|
|
86
101
|
@ram = nil
|
|
102
|
+
@ram_kb = nil
|
|
103
|
+
@cartridges = {}
|
|
87
104
|
@autostart = true
|
|
88
105
|
@writable = false
|
|
89
106
|
@sound = native
|
|
@@ -178,8 +195,8 @@ module Badline
|
|
|
178
195
|
def jit? = @jit
|
|
179
196
|
|
|
180
197
|
# The name of the model to build: a C64 Badline::Model names, a VIC-20
|
|
181
|
-
# of VIC20_MODELS
|
|
182
|
-
# ntsc.
|
|
198
|
+
# of VIC20_MODELS, a C128 of C128_MODELS or a Plus/4 of PLUS4_MODELS.
|
|
199
|
+
# --ntsc is short for --model ntsc.
|
|
183
200
|
def model = @models.first || family_models.first
|
|
184
201
|
|
|
185
202
|
# Whether --c64 asks the C128 to start in C64 mode, holding C= at
|
|
@@ -201,7 +218,8 @@ module Badline
|
|
|
201
218
|
# A .sid on its own plays in the SID player, as `sid` would, unless an
|
|
202
219
|
# option of the emulator's window asks for the machine.
|
|
203
220
|
def play_lone_tune
|
|
204
|
-
return if @sid_command || @family
|
|
221
|
+
return if @sid_command || %i[vic20 plus4].include?(@family) || headless? || !@window_only.empty?
|
|
222
|
+
return if @media_path.nil?
|
|
205
223
|
|
|
206
224
|
@sid_command = File.extname(@media_path).casecmp?(".sid")
|
|
207
225
|
end
|
|
@@ -254,7 +272,8 @@ module Badline
|
|
|
254
272
|
when "--sid" then @sid_model = sid_model_for(value)
|
|
255
273
|
when "--model" then @models << value
|
|
256
274
|
when "--reu" then @reu = reu_size(value)
|
|
257
|
-
when "--ram" then
|
|
275
|
+
when "--ram" then ram_option(value)
|
|
276
|
+
when "--c1lo", "--c1hi", "--c2lo", "--c2hi" then @cartridges[CARTRIDGE_SLOTS.fetch(name)] = value
|
|
258
277
|
when "--frames" then @frames = number(flag, value)
|
|
259
278
|
when "--screenshot" then @screenshot = value
|
|
260
279
|
when "--save-snapshot" then @save_snapshot = value
|
|
@@ -267,6 +286,15 @@ module Badline
|
|
|
267
286
|
end
|
|
268
287
|
end
|
|
269
288
|
|
|
289
|
+
# --ram names the VIC-20's RAM expansion, or the Plus/4's RAM.
|
|
290
|
+
def ram_option(value)
|
|
291
|
+
if @family == :plus4
|
|
292
|
+
@ram_kb = plus4_ram(value)
|
|
293
|
+
else
|
|
294
|
+
@ram = ram_configuration(value)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
270
298
|
def switch(name)
|
|
271
299
|
case name
|
|
272
300
|
when "--no-autostart" then @autostart = false
|
data/lib/badline/plus4/bus.rb
CHANGED
|
@@ -28,6 +28,13 @@ module Badline
|
|
|
28
28
|
# RAM smaller than 64K repeats through the 64K, as the C16's 16K does
|
|
29
29
|
# without A14 and A15. Where nothing answers, a read finds the last byte
|
|
30
30
|
# on the bus.
|
|
31
|
+
#
|
|
32
|
+
# The TED fetches through the same banks: the video matrix and the
|
|
33
|
+
# attributes from ROM or RAM at $8000-$FFFF as the CPU sees it, with no
|
|
34
|
+
# I/O in the way (#ted_fetch), and character and bitmap data from RAM,
|
|
35
|
+
# or from ROM with $FF12 bit 2 set, where below $8000 and in an empty
|
|
36
|
+
# bank it finds the last byte on the bus (#ted_pattern_fetch, Plus/4
|
|
37
|
+
# openmem). Each byte it fetches is left on the bus.
|
|
31
38
|
class Bus
|
|
32
39
|
include Addressable
|
|
33
40
|
|
|
@@ -75,12 +82,14 @@ module Badline
|
|
|
75
82
|
|
|
76
83
|
# Page $FD: the ACIA, the user port, the keyboard port and the ROM
|
|
77
84
|
# bank latch, by the address's bits 4-7. +acia+ and +user_port+ are
|
|
78
|
-
# nil where the machine has none
|
|
85
|
+
# nil where the machine has none, and +cassette_sense+ answers in the
|
|
86
|
+
# user port's place on a machine without one (CassetteSense).
|
|
79
87
|
class IOPage
|
|
80
|
-
def initialize(keyboard_port:, acia: nil, user_port: nil)
|
|
88
|
+
def initialize(keyboard_port:, acia: nil, user_port: nil, cassette_sense: nil)
|
|
81
89
|
@bus = nil
|
|
82
90
|
@acia = acia
|
|
83
91
|
@user_port = user_port
|
|
92
|
+
@cassette_sense = cassette_sense
|
|
84
93
|
@keyboard_port = keyboard_port
|
|
85
94
|
end
|
|
86
95
|
|
|
@@ -93,7 +102,7 @@ module Badline
|
|
|
93
102
|
def peek(addr)
|
|
94
103
|
case addr & 0xf0
|
|
95
104
|
when 0x00 then @acia ? @acia.peek(addr) : @bus.data
|
|
96
|
-
when 0x10 then
|
|
105
|
+
when 0x10 then user_port_peek(addr)
|
|
97
106
|
when 0x30 then @keyboard_port.peek(addr)
|
|
98
107
|
else @bus.data
|
|
99
108
|
end
|
|
@@ -107,6 +116,15 @@ module Badline
|
|
|
107
116
|
when 0xd0 then @bus.select_banks(addr)
|
|
108
117
|
end
|
|
109
118
|
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def user_port_peek(addr)
|
|
123
|
+
return @user_port.peek(addr) if @user_port
|
|
124
|
+
return @cassette_sense.read(@bus.data) if @cassette_sense
|
|
125
|
+
|
|
126
|
+
@bus.data
|
|
127
|
+
end
|
|
110
128
|
end
|
|
111
129
|
|
|
112
130
|
# VICE's debug cartridge register at $FDCF, a spot in the I/O page
|
|
@@ -211,6 +229,8 @@ module Badline
|
|
|
211
229
|
@data = 0
|
|
212
230
|
@read_pages = Array.new(256, @open_bus)
|
|
213
231
|
@write_pages = Array.new(256, @ram_view)
|
|
232
|
+
@ted_pages = Array.new(256, @ram_view)
|
|
233
|
+
@ted_rom_pages = Array.new(256, @open_bus)
|
|
214
234
|
@low_bank = @high_bank = 0
|
|
215
235
|
@rom_selected = true
|
|
216
236
|
@top_memory = @ram_view
|
|
@@ -259,21 +279,45 @@ module Badline
|
|
|
259
279
|
map_reads
|
|
260
280
|
end
|
|
261
281
|
|
|
262
|
-
# Puts a cartridge's ROM chips in
|
|
263
|
-
#
|
|
264
|
-
#
|
|
282
|
+
# Puts a cartridge's ROM chips in their slots, as a PLUS4 CARTRIDGE
|
|
283
|
+
# .crt numbers them: a chip's bank picks cartridge 1 (0) or 2 (1), in
|
|
284
|
+
# ROM banks 2 and 3, and its address the low ($8000) or the high
|
|
285
|
+
# ($C000) slot. A chip shorter than 16K repeats through the slot.
|
|
286
|
+
# Slots the chips don't name keep what they held.
|
|
265
287
|
def attach_cartridge(chips)
|
|
266
288
|
chips.each do |chip|
|
|
267
|
-
|
|
289
|
+
raise ArgumentError, "no Plus/4 cartridge slot in bank #{chip.bank}" unless chip.bank.between?(0, 1)
|
|
290
|
+
|
|
291
|
+
rom = chip.data
|
|
292
|
+
length = rom.length
|
|
293
|
+
data = Array.new(0x4000) { |i| length.zero? ? 0xff : rom[i % length] }
|
|
268
294
|
if chip.address < 0xc000
|
|
269
|
-
@low_roms[2] = ROM.new(data, length: 0x4000, start: 0x8000)
|
|
295
|
+
@low_roms[2 + chip.bank] = ROM.new(data, length: 0x4000, start: 0x8000)
|
|
270
296
|
else
|
|
271
|
-
@high_roms[2] = ROM.new(data, length: 0x4000, start: 0xc000)
|
|
297
|
+
@high_roms[2 + chip.bank] = ROM.new(data, length: 0x4000, start: 0xc000)
|
|
272
298
|
end
|
|
273
299
|
end
|
|
274
300
|
map_reads
|
|
275
301
|
end
|
|
276
302
|
|
|
303
|
+
# Empties both cartridges' slots, ROM banks 2 and 3, which then read
|
|
304
|
+
# as the open bus.
|
|
305
|
+
def detach_cartridges
|
|
306
|
+
@low_roms[2] = @low_roms[3] = @high_roms[2] = @high_roms[3] = nil
|
|
307
|
+
map_reads
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The slots that hold a cartridge's ROM, by xplus4's names: c1lo,
|
|
311
|
+
# c1hi, c2lo and c2hi.
|
|
312
|
+
def cartridge_slots
|
|
313
|
+
slots = []
|
|
314
|
+
slots << "c1lo" unless @low_roms[2].nil?
|
|
315
|
+
slots << "c1hi" unless @high_roms[2].nil?
|
|
316
|
+
slots << "c2lo" unless @low_roms[3].nil?
|
|
317
|
+
slots << "c2hi" unless @high_roms[3].nil?
|
|
318
|
+
slots
|
|
319
|
+
end
|
|
320
|
+
|
|
277
321
|
def peek(addr)
|
|
278
322
|
@data = @read_pages[addr >> 8].peek(addr)
|
|
279
323
|
end
|
|
@@ -283,6 +327,16 @@ module Badline
|
|
|
283
327
|
@write_pages[addr >> 8].poke(addr, value)
|
|
284
328
|
end
|
|
285
329
|
|
|
330
|
+
# A TED fetch of the video matrix or the attributes.
|
|
331
|
+
def ted_fetch(addr)
|
|
332
|
+
@data = @ted_pages[addr >> 8].peek(addr)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# A TED fetch of character or bitmap data, from ROM when +rom+.
|
|
336
|
+
def ted_pattern_fetch(addr, rom)
|
|
337
|
+
@data = rom ? @ted_rom_pages[addr >> 8].peek(addr) : @ram_view.peek(addr)
|
|
338
|
+
end
|
|
339
|
+
|
|
286
340
|
private
|
|
287
341
|
|
|
288
342
|
def map_writes
|
|
@@ -295,17 +349,26 @@ module Badline
|
|
|
295
349
|
def map_reads
|
|
296
350
|
@read_pages.fill(@ram_view, 0x00, 0xfd)
|
|
297
351
|
@read_pages[0x00] = @zero_page
|
|
298
|
-
map_roms if @rom_selected
|
|
352
|
+
map_roms(@read_pages) if @rom_selected
|
|
299
353
|
@top_memory = @rom_selected ? @high_roms[@high_bank] || @open_bus : @ram_view
|
|
300
354
|
@read_pages[0xfd] = @io_page
|
|
301
355
|
@read_pages[0xfe] = @tcbm_page
|
|
302
356
|
@read_pages[0xff] = @top_page
|
|
357
|
+
map_ted_reads
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def map_ted_reads
|
|
361
|
+
@ted_pages.fill(@ram_view, 0x80, 0x80)
|
|
362
|
+
map_roms(@ted_pages) if @rom_selected
|
|
363
|
+
map_roms(@ted_rom_pages)
|
|
364
|
+
@ted_pages.fill(@high_roms[@high_bank] || @open_bus, 0xfd, 3) if @rom_selected
|
|
365
|
+
@ted_rom_pages.fill(@high_roms[@high_bank] || @open_bus, 0xfd, 3)
|
|
303
366
|
end
|
|
304
367
|
|
|
305
|
-
def map_roms
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
368
|
+
def map_roms(pages)
|
|
369
|
+
pages.fill(@low_roms[@low_bank] || @open_bus, 0x80, 0x40)
|
|
370
|
+
pages.fill(@high_roms[@high_bank] || @open_bus, 0xc0, 0x3c)
|
|
371
|
+
pages[0xfc] = @high_roms[0]
|
|
309
372
|
end
|
|
310
373
|
end
|
|
311
374
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Plus4
|
|
5
|
+
# The C16's and C116's cassette sense, which has no 6529 to go through:
|
|
6
|
+
# a read of $FD10-$FD1F enables a 74LS125 that drives the sense line
|
|
7
|
+
# onto data bit 2, low while a key is down on the deck (C16 schematic
|
|
8
|
+
# 251788, sheet 1, U11). Nothing drives the other bits, which keep the
|
|
9
|
+
# last byte on the bus, and a write goes nowhere.
|
|
10
|
+
class CassetteSense
|
|
11
|
+
TAPE_SENSE = 0x04
|
|
12
|
+
|
|
13
|
+
def initialize(datasette)
|
|
14
|
+
@datasette = datasette
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The byte a read finds, from +data+, the last byte on the bus.
|
|
18
|
+
def read(data) = @datasette.sense_low? ? data & ~TAPE_SENSE : data | TAPE_SENSE
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|