badline 0.9.0 → 0.10.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 +48 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/library.md +15 -1
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +17 -3
- data/lib/badline/address_bus/fittings.rb +38 -0
- data/lib/badline/address_bus/pla.rb +160 -0
- data/lib/badline/address_bus/roms.rb +12 -5
- data/lib/badline/address_bus.rb +15 -90
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +47 -7
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +52 -37
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +3 -0
- data/lib/badline/datasette.rb +39 -1
- 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 +62 -64
- data/lib/badline/frontend/boot.rb +33 -8
- 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_media.rb +49 -24
- data/lib/badline/frontend/menu_pages.rb +23 -40
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +30 -33
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +1 -1
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/sound.rb +11 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +2 -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 +16 -27
- data/lib/badline/kernal_trap/routine.rb +11 -27
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +21 -19
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +28 -14
- data/lib/badline/machine.rb +60 -0
- 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 +32 -18
- data/lib/badline/model.rb +29 -12
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +12 -2
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +51 -0
- data/lib/badline/roms/c64gs.rom +0 -0
- data/lib/badline/roms/kernal-pet64.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/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +15 -10
- data/lib/badline/snapshot/state.rb +5 -3
- data/lib/badline/snapshot/vice.rb +2 -1
- 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 +3 -0
- data/lib/badline/storage/tap.rb +38 -4
- data/lib/badline/storage/unavailable.rb +2 -0
- 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 +55 -17
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +48 -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
- metadata +31 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -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
|
data/lib/badline/media.rb
CHANGED
|
@@ -4,6 +4,9 @@ require "badline/media/true_drive"
|
|
|
4
4
|
require "badline/media/queue"
|
|
5
5
|
require "badline/media/disk_set"
|
|
6
6
|
require "badline/media/disk_list"
|
|
7
|
+
require "badline/media/vic20_basic"
|
|
8
|
+
require "badline/media/vic20_cartridge"
|
|
9
|
+
require "badline/media/vic20_media"
|
|
7
10
|
|
|
8
11
|
module Badline
|
|
9
12
|
module Media
|
|
@@ -30,25 +33,13 @@ module Badline
|
|
|
30
33
|
# autostart then loads through it.
|
|
31
34
|
#
|
|
32
35
|
# An .m3u or .vfl list of disks attaches the first disk it lists
|
|
33
|
-
# (DiskList).
|
|
34
|
-
|
|
36
|
+
# (DiskList). A machine other than the C64 takes its own media
|
|
37
|
+
# (Vic20Media).
|
|
38
|
+
def attach(computer, path, autostart: true, subtune: nil, **)
|
|
35
39
|
path = DiskList.disk(path)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
computer.mount(Storage::HostDirectory.new(path))
|
|
40
|
-
"Mounted #{path} as device 8"
|
|
41
|
-
elsif File.extname(path).downcase == ".crt"
|
|
42
|
-
attach_cartridge(computer, path, options.fetch(:cartridge, {}))
|
|
43
|
-
elsif File.extname(path).downcase == ".sid"
|
|
44
|
-
attach_sid(computer, path, autostart:, subtune:)
|
|
45
|
-
elsif File.extname(path).downcase == ".tap"
|
|
46
|
-
attach_tape(computer, path, autostart:)
|
|
47
|
-
elsif MOUNT_TYPES.key?(File.extname(path).downcase)
|
|
48
|
-
attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
|
|
49
|
-
else
|
|
50
|
-
attach_prg(computer, path, autostart:)
|
|
51
|
-
end
|
|
40
|
+
return Vic20Media.attach(computer, path, autostart:, **) unless computer.is_a?(Computer)
|
|
41
|
+
|
|
42
|
+
attach_c64(computer, path, autostart:, subtune:, **)
|
|
52
43
|
end
|
|
53
44
|
|
|
54
45
|
# Swaps the disk in device 8 for a disk image or a host directory,
|
|
@@ -69,6 +60,10 @@ module Badline
|
|
|
69
60
|
"Inserted #{path} in device 8"
|
|
70
61
|
end
|
|
71
62
|
|
|
63
|
+
# The RAM expansion a VIC-20 for `path` should be built with
|
|
64
|
+
# (Vic20Media.ram_for), a key of Vic20::Bus::RAM_CONFIGURATIONS.
|
|
65
|
+
def vic20_ram_for(path) = Vic20Media.ram_for(path)
|
|
66
|
+
|
|
72
67
|
# The SID a machine for `path` should be built with. A .sid tune names
|
|
73
68
|
# its own; everything else gets `otherwise`.
|
|
74
69
|
def sid_model(path, otherwise: :mos6581)
|
|
@@ -79,6 +74,25 @@ module Badline
|
|
|
79
74
|
|
|
80
75
|
private
|
|
81
76
|
|
|
77
|
+
def attach_c64(computer, path, autostart:, subtune:, **options)
|
|
78
|
+
if TrueDrive.takes?(computer, path)
|
|
79
|
+
attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
|
|
80
|
+
elsif File.directory?(path)
|
|
81
|
+
computer.mount(Storage::HostDirectory.new(path))
|
|
82
|
+
"Mounted #{path} as device 8"
|
|
83
|
+
elsif File.extname(path).downcase == ".crt"
|
|
84
|
+
attach_cartridge(computer, path, options.fetch(:cartridge, {}))
|
|
85
|
+
elsif File.extname(path).downcase == ".sid"
|
|
86
|
+
attach_sid(computer, path, autostart:, subtune:)
|
|
87
|
+
elsif File.extname(path).downcase == ".tap"
|
|
88
|
+
attach_tape(computer, path, autostart:)
|
|
89
|
+
elsif MOUNT_TYPES.key?(File.extname(path).downcase)
|
|
90
|
+
attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
|
|
91
|
+
else
|
|
92
|
+
attach_prg(computer, path, autostart:)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
82
96
|
def disk?(path)
|
|
83
97
|
storage = MOUNT_TYPES[File.extname(path).downcase]
|
|
84
98
|
File.directory?(path) || (!storage.nil? && storage < Storage::DiskImage)
|
data/lib/badline/model.rb
CHANGED
|
@@ -8,39 +8,56 @@ module Badline
|
|
|
8
8
|
# a datasette. The names are x64sc's.
|
|
9
9
|
module Model
|
|
10
10
|
# Each field but the name is a keyword Computer.new takes.
|
|
11
|
-
Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette)
|
|
11
|
+
Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette, :board)
|
|
12
12
|
|
|
13
13
|
# The breadbin PAL C64: the 6569 VIC-II, 6526 CIAs and the 6581 SID.
|
|
14
14
|
C64 = Profile.new(name: "c64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
15
|
-
region: Region::PAL, kernal: :c64, datasette: true)
|
|
15
|
+
region: Region::PAL, kernal: :c64, datasette: true, board: :c64)
|
|
16
16
|
|
|
17
17
|
# The PAL C64C: the HMOS 8565 VIC-II, 6526A CIAs and the 8580 SID.
|
|
18
18
|
C64C = Profile.new(name: "c64c", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
|
|
19
|
-
region: Region::PAL, kernal: :c64, datasette: true)
|
|
19
|
+
region: Region::PAL, kernal: :c64, datasette: true, board: :c64)
|
|
20
20
|
|
|
21
21
|
# The NTSC C64 with the 6567R8 VIC-II.
|
|
22
22
|
NTSC = Profile.new(name: "ntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
23
|
-
region: Region::NTSC, kernal: :c64, datasette: true)
|
|
23
|
+
region: Region::NTSC, kernal: :c64, datasette: true, board: :c64)
|
|
24
24
|
|
|
25
25
|
# The NTSC C64C: the 8562, the 6567R8's HMOS successor, with the
|
|
26
26
|
# C64C's CIAs and SID.
|
|
27
27
|
NEW_NTSC = Profile.new(name: "newntsc", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
|
|
28
|
-
region: Region::NTSC, kernal: :c64, datasette: true)
|
|
28
|
+
region: Region::NTSC, kernal: :c64, datasette: true, board: :c64)
|
|
29
29
|
|
|
30
30
|
# The first NTSC C64s, with the 6567R56A VIC-II.
|
|
31
31
|
OLD_NTSC = Profile.new(name: "oldntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
32
|
-
region: Region::NTSC_OLD, kernal: :c64, datasette: true)
|
|
32
|
+
region: Region::NTSC_OLD, kernal: :c64, datasette: true, board: :c64)
|
|
33
33
|
|
|
34
34
|
# The Drean C64 of Argentina, on PAL-N with the 6572 VIC-II.
|
|
35
35
|
DREAN = Profile.new(name: "drean", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
36
|
-
region: Region::DREAN, kernal: :c64, datasette: true)
|
|
36
|
+
region: Region::DREAN, kernal: :c64, datasette: true, board: :c64)
|
|
37
37
|
|
|
38
38
|
# The portable SX-64: a PAL C64's chips with its own KERNAL, a 1541
|
|
39
39
|
# built in as device 8 and no cassette port.
|
|
40
40
|
SX64 = Profile.new(name: "sx64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
41
|
-
region: Region::PAL, kernal: :sx64, datasette: false)
|
|
41
|
+
region: Region::PAL, kernal: :sx64, datasette: false, board: :c64)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
# The PET 64, or Educator 64: a PAL C64's board in a PET's case, with
|
|
44
|
+
# its own KERNAL and a green monochrome monitor built in.
|
|
45
|
+
PET64 = Profile.new(name: "pet64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
46
|
+
region: Region::PAL, kernal: :pet64, datasette: true, board: :pet64)
|
|
47
|
+
|
|
48
|
+
# The C64GS games console: a PAL C64C's board with its own BASIC and
|
|
49
|
+
# KERNAL, which show an animation asking for a cartridge when it has
|
|
50
|
+
# none, in a case with no keyboard and no cassette port.
|
|
51
|
+
C64GS = Profile.new(name: "c64gs", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
|
|
52
|
+
region: Region::PAL, kernal: :gs, datasette: false, board: :gs)
|
|
53
|
+
|
|
54
|
+
# The MAX Machine, or Ultimax, of Japan: the 6566, an NTSC VIC-II
|
|
55
|
+
# for static RAM, which runs here as the first NTSC C64s' 6567R56A,
|
|
56
|
+
# one CIA, 2K of RAM and no ROMs, so it runs only a cartridge's.
|
|
57
|
+
ULTIMAX = Profile.new(name: "ultimax", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
|
|
58
|
+
region: Region::NTSC_OLD, kernal: :c64, datasette: true, board: :max)
|
|
59
|
+
|
|
60
|
+
ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64, PET64, C64GS, ULTIMAX].freeze
|
|
44
61
|
|
|
45
62
|
def self.named(name)
|
|
46
63
|
model = ALL.find { |candidate| candidate.name == name }
|
|
@@ -49,13 +66,13 @@ module Badline
|
|
|
49
66
|
model
|
|
50
67
|
end
|
|
51
68
|
|
|
52
|
-
# The model whose chips, region, KERNAL and
|
|
53
|
-
# nil for none.
|
|
69
|
+
# The model whose chips, region, KERNAL, datasette and board `machine`
|
|
70
|
+
# has, or nil for none.
|
|
54
71
|
def self.of(machine)
|
|
55
72
|
ALL.find do |model|
|
|
56
73
|
model.vic_model == machine.vic_model && model.cia_model == machine.cia_model &&
|
|
57
74
|
model.sid_model == machine.sid_model && model.region == machine.region &&
|
|
58
|
-
model.kernal == machine.kernal && model.datasette == machine.datasette
|
|
75
|
+
model.kernal == machine.kernal && model.datasette == machine.datasette && model.board == machine.board
|
|
59
76
|
end
|
|
60
77
|
end
|
|
61
78
|
end
|
|
@@ -6,10 +6,11 @@ module Badline
|
|
|
6
6
|
# `frame` frames have run. An event given at several frames becomes
|
|
7
7
|
# one Event per frame.
|
|
8
8
|
class Event
|
|
9
|
-
# The actions, and whether each takes an argument.
|
|
9
|
+
# The actions, and whether each takes an argument. menu's page may be
|
|
10
|
+
# left out.
|
|
10
11
|
ACTIONS = {
|
|
11
|
-
"key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true,
|
|
12
|
-
"reset" => false, "freeze" => false, "quit" => false
|
|
12
|
+
"key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true, "menu" => true,
|
|
13
|
+
"reset" => false, "freeze" => false, "resume" => false, "quit" => false
|
|
13
14
|
}.freeze
|
|
14
15
|
|
|
15
16
|
# The frames a key or the freeze button is held for.
|
|
@@ -79,6 +80,7 @@ module Badline
|
|
|
79
80
|
|
|
80
81
|
def self.valid?(action, argument)
|
|
81
82
|
return false unless ACTIONS.key?(action)
|
|
83
|
+
return true if action == "menu"
|
|
82
84
|
return argument.empty? unless ACTIONS[action]
|
|
83
85
|
return EJECTABLE.include?(argument) if action == "eject"
|
|
84
86
|
return insertable?(argument) if action == "insert"
|
data/lib/badline/options/help.rb
CHANGED
|
@@ -37,11 +37,17 @@ module Badline
|
|
|
37
37
|
|
|
38
38
|
def banner
|
|
39
39
|
<<~BANNER
|
|
40
|
-
Usage: #{@program} [options] [media]
|
|
40
|
+
Usage: #{@program} [c64] [options] [media]
|
|
41
|
+
#{@program} vic20 [options] [media]
|
|
41
42
|
#{@program} --headless [options] tune.sid
|
|
42
43
|
#{@program} [options] tune.sid --audio-out FILE
|
|
43
44
|
#{@program} sid [options] FILE|DIR...
|
|
44
45
|
|
|
46
|
+
Commands:
|
|
47
|
+
c64 Run a C64, the one --model names (the default)
|
|
48
|
+
vic20 Run a PAL VIC-20, with the RAM --ram names
|
|
49
|
+
sid Play .sid tunes and directories of them
|
|
50
|
+
|
|
45
51
|
Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
|
|
46
52
|
.g64 disk image for the true 1541, an .m3u or .vfl list of disk
|
|
47
53
|
images, a .t64 tape archive, a .tap tape, a .crt cartridge, a .sid
|
|
@@ -80,8 +86,10 @@ module Badline
|
|
|
80
86
|
frames. type=TEXT types it, with \\n for RETURN. insert=FILE swaps
|
|
81
87
|
in a disk, a list's first disk, a tape or a cartridge, and
|
|
82
88
|
eject=disk, tape or cartridge takes one out. screenshot=FILE saves
|
|
83
|
-
the frame as a .bmp, with the frame's number in place of %d.
|
|
84
|
-
|
|
89
|
+
the frame as a .bmp, with the frame's number in place of %d. menu
|
|
90
|
+
opens the pause menu, and menu=PAGE opens it on snapshots, drive,
|
|
91
|
+
datasette, expansion, ports, sound or power. Its frames still count,
|
|
92
|
+
and resume closes it. reset, freeze and quit take no argument.
|
|
85
93
|
BANNER
|
|
86
94
|
end
|
|
87
95
|
|
|
@@ -5,7 +5,15 @@ module Badline
|
|
|
5
5
|
DEFAULT_RATE = 44_100
|
|
6
6
|
|
|
7
7
|
# The names Badline::Model builds, in the order the help lists them.
|
|
8
|
-
MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64].freeze
|
|
8
|
+
MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64 pet64 c64gs ultimax].freeze
|
|
9
|
+
|
|
10
|
+
# The VIC-20's models, as xvic names its video standards. Only PAL is
|
|
11
|
+
# emulated.
|
|
12
|
+
VIC20_MODELS = %w[pal ntsc].freeze
|
|
13
|
+
|
|
14
|
+
# The VIC-20 RAM expansions --ram takes, in the order the help lists
|
|
15
|
+
# them.
|
|
16
|
+
RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
|
|
9
17
|
|
|
10
18
|
SECTIONS = ["Options", "Window options", "Options without the window", "Testing options"].freeze
|
|
11
19
|
|
|
@@ -28,9 +36,11 @@ module Badline
|
|
|
28
36
|
section: "Window options", needs: :window),
|
|
29
37
|
Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
|
|
30
38
|
section: "Window options", needs: :window),
|
|
31
|
-
Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first})",
|
|
39
|
+
Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal",
|
|
32
40
|
section: "Window options", needs: :window),
|
|
33
41
|
Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
|
|
42
|
+
Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded)",
|
|
43
|
+
section: "Window options", needs: :window),
|
|
34
44
|
Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
|
|
35
45
|
section: "Window options", needs: :window),
|
|
36
46
|
Option.new("--verbose", "Print the display, sound and gamepad setup and the frame timing",
|
|
@@ -2,13 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Options
|
|
5
|
-
# The checks Options#parse makes
|
|
6
|
-
#
|
|
7
|
-
# exist.
|
|
5
|
+
# The checks Options#parse makes: that each value reads as its option
|
|
6
|
+
# takes it, and once every argument has been read, that the options
|
|
7
|
+
# fit the mode, the numbers are positive and the files exist.
|
|
8
8
|
module Validation
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
+
def sid_model_for(value)
|
|
12
|
+
return if value == "auto"
|
|
13
|
+
raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
|
|
14
|
+
|
|
15
|
+
SID_MODELS[value]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ram_configuration(value)
|
|
19
|
+
raise Error, "invalid argument: --ram #{value}" unless RAM_CONFIGURATIONS.key?(value)
|
|
20
|
+
|
|
21
|
+
RAM_CONFIGURATIONS[value]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reu_size(value)
|
|
25
|
+
raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
|
|
26
|
+
|
|
27
|
+
value.to_i
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def number(flag, value)
|
|
31
|
+
raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
|
|
32
|
+
|
|
33
|
+
value.to_i
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def decimal(flag, value)
|
|
37
|
+
raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
|
|
38
|
+
|
|
39
|
+
value.to_f
|
|
40
|
+
end
|
|
41
|
+
|
|
11
42
|
def validate
|
|
43
|
+
validate_family
|
|
12
44
|
validate_mode
|
|
13
45
|
validate_models
|
|
14
46
|
validate_numbers
|
|
@@ -26,6 +58,24 @@ module Badline
|
|
|
26
58
|
end
|
|
27
59
|
end
|
|
28
60
|
|
|
61
|
+
def validate_family
|
|
62
|
+
if @family == :vic20
|
|
63
|
+
validate_vic20
|
|
64
|
+
elsif !@ram.nil?
|
|
65
|
+
raise Error, "--ram needs vic20"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The VIC-20 is a PAL one, in the window, without the C64's SID and
|
|
70
|
+
# REU.
|
|
71
|
+
def validate_vic20
|
|
72
|
+
raise Error, "the NTSC VIC-20 isn't emulated yet" if @models.include?("ntsc")
|
|
73
|
+
|
|
74
|
+
raise Error, "--sid needs the C64" unless @sid_model.nil?
|
|
75
|
+
raise Error, "--reu needs the C64" unless @reu.nil?
|
|
76
|
+
raise Error, "the VIC-20 needs the window" if headless?
|
|
77
|
+
end
|
|
78
|
+
|
|
29
79
|
# `sid` takes directories as well as tunes.
|
|
30
80
|
def validate_tune(path)
|
|
31
81
|
return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
|
|
@@ -37,7 +87,8 @@ module Badline
|
|
|
37
87
|
# --model and --ntsc may be given more than once, but only for one
|
|
38
88
|
# model.
|
|
39
89
|
def validate_models
|
|
40
|
-
|
|
90
|
+
names = @family == :vic20 ? VIC20_MODELS : MODELS
|
|
91
|
+
unknown = @models.find { |name| !names.include?(name) }
|
|
41
92
|
raise Error, "invalid argument: --model #{unknown}" unless unknown.nil?
|
|
42
93
|
raise Error, "conflicting models: #{@models.uniq.join(' and ')}" unless @models.uniq.size <= 1
|
|
43
94
|
end
|