badline 0.12.0 → 0.12.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 +8 -0
- data/lib/badline/address_bus/saved_state.rb +4 -10
- data/lib/badline/address_bus.rb +8 -27
- data/lib/badline/audio/bare_player.rb +1 -1
- data/lib/badline/c128/bus.rb +13 -35
- data/lib/badline/c128/bus_state.rb +5 -5
- data/lib/badline/c128.rb +5 -64
- data/lib/badline/computer/attachments.rb +16 -45
- data/lib/badline/computer/common.rb +34 -0
- data/lib/badline/computer/kernal_traps.rb +65 -5
- data/lib/badline/computer/true_drives.rb +12 -23
- data/lib/badline/computer.rb +5 -68
- data/lib/badline/core.rb +2 -2
- data/lib/badline/cpu_port.rb +112 -0
- data/lib/badline/drive1541/disk.rb +14 -17
- data/lib/badline/drive1541/disk_state.rb +7 -15
- data/lib/badline/drive1581/wd1772.rb +1 -1
- data/lib/badline/frontend/boot.rb +8 -59
- data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
- data/lib/badline/kernal_trap/load.rb +1 -1
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap.rb +1 -1
- data/lib/badline/machine/clocking.rb +50 -0
- data/lib/badline/machine.rb +58 -2
- data/lib/badline/media/boot_disk.rb +2 -2
- data/lib/badline/media/disk_list.rb +3 -3
- data/lib/badline/media/disk_set.rb +1 -1
- data/lib/badline/media/extensions.rb +33 -0
- data/lib/badline/media/program.rb +28 -0
- data/lib/badline/media/true_drive.rb +3 -2
- data/lib/badline/media/tune.rb +35 -0
- data/lib/badline/media/vic20_cartridge.rb +1 -7
- data/lib/badline/media/vic20_media.rb +38 -80
- data/lib/badline/media.rb +82 -79
- data/lib/badline/options/event.rb +3 -1
- data/lib/badline/sid.rb +1 -1
- data/lib/badline/snapshot/machine_state.rb +9 -1
- data/lib/badline/snapshot/state.rb +5 -4
- data/lib/badline/snapshot.rb +11 -9
- data/lib/badline/storage/d64_image.rb +15 -7
- data/lib/badline/storage/disk_image/writing.rb +2 -2
- data/lib/badline/storage/disk_image.rb +1 -1
- data/lib/badline/storage/host_directory.rb +1 -1
- data/lib/badline/storage/t64.rb +1 -1
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +1 -1
- data/lib/badline/vic/border_mask.rb +1 -1
- data/lib/badline/vic/collisions.rb +1 -1
- data/lib/badline/vic/color_patches.rb +1 -1
- data/lib/badline/vic/display_state.rb +1 -1
- data/lib/badline/vic/graphics_mode.rb +1 -1
- data/lib/badline/vic/graphics_shifter.rb +1 -1
- data/lib/badline/vic/register_log.rb +1 -1
- data/lib/badline/vic/registers.rb +1 -1
- data/lib/badline/vic/sequencer.rb +1 -1
- data/lib/badline/vic/sequencer_output.rb +1 -1
- data/lib/badline/vic/sequencer_state.rb +1 -1
- data/lib/badline/vic/sprite/internal_bus.rb +1 -1
- data/lib/badline/vic/sprite/saved_state.rb +1 -1
- data/lib/badline/vic/sprite/shifter.rb +1 -1
- data/lib/badline/vic/sprite/timing.rb +1 -1
- data/lib/badline/vic/sprite.rb +1 -1
- data/lib/badline/vic/sprites.rb +1 -1
- data/lib/badline/vic/sprites_state.rb +1 -1
- data/lib/badline/vic.rb +6 -5
- data/lib/badline/vic20/attachments.rb +10 -80
- data/lib/badline/vic20/saved_state.rb +0 -30
- data/lib/badline/vic20.rb +1 -37
- metadata +8 -3
- data/lib/badline/cycleable.rb +0 -38
data/lib/badline/machine.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/machine/clocking"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
# = Machine
|
|
5
7
|
#
|
|
@@ -44,16 +46,22 @@ module Badline
|
|
|
44
46
|
# +press_caps_lock+, +release_caps_lock+, +press_display_key+,
|
|
45
47
|
# +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
|
|
46
48
|
# C128D's 1571, its true drive.
|
|
49
|
+
#
|
|
50
|
+
# Each machine includes Machine::Clocking for the clocking part.
|
|
47
51
|
module Machine
|
|
52
|
+
# Media the machine named doesn't take.
|
|
53
|
+
class Unsupported < ArgumentError; end
|
|
54
|
+
|
|
48
55
|
# A new machine of `family`, :c64, :vic20 or :c128, the model of it
|
|
49
56
|
# `model` names. A C64's model is one of Model::ALL, with its SID
|
|
50
57
|
# `sid_model` when not nil and an REU of `reu` K when not nil. A
|
|
51
58
|
# VIC-20's is "pal", with the RAM expansion `ram` names
|
|
52
59
|
# (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
|
|
53
|
-
# one of C128::Model::ALL, with its SID `sid_model` when not nil
|
|
60
|
+
# one of C128::Model::ALL, with its SID `sid_model` when not nil, built
|
|
61
|
+
# for C64 mode.
|
|
54
62
|
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
55
63
|
return vic20(model, ram) if family == :vic20
|
|
56
|
-
return
|
|
64
|
+
return c128(model, sid_model, :c64) if family == :c128
|
|
57
65
|
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
58
66
|
|
|
59
67
|
profile = Model.named(model)
|
|
@@ -62,11 +70,59 @@ module Badline
|
|
|
62
70
|
kernal: profile.kernal, datasette: profile.datasette, board: profile.board)
|
|
63
71
|
end
|
|
64
72
|
|
|
73
|
+
# The machine the command line's `options` name, to start `media`, a
|
|
74
|
+
# path or nil. A C64 or a C128 without a SID named takes the one a .sid
|
|
75
|
+
# tune names. The C128 is built for C128 mode, with C= held through the
|
|
76
|
+
# reset, so its KERNAL starts C64 mode, for --c64 (+c64_mode?+) or media
|
|
77
|
+
# only C64 mode runs: a .sid tune or a program that loads at the C64's
|
|
78
|
+
# BASIC start. A VIC-20 without RAM named gets the expansion the media
|
|
79
|
+
# needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
|
|
80
|
+
# Unsupported for media a VIC-20 doesn't take.
|
|
81
|
+
def self.for_options(options, media, &)
|
|
82
|
+
family = options.family
|
|
83
|
+
return vic20_for(media, options.model, options.ram, &) if family == :vic20
|
|
84
|
+
|
|
85
|
+
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
86
|
+
return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
|
|
87
|
+
|
|
88
|
+
machine = c128(options.model, sid_model, :c128)
|
|
89
|
+
machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
|
|
90
|
+
machine
|
|
91
|
+
end
|
|
92
|
+
|
|
65
93
|
def self.vic20(model, ram)
|
|
66
94
|
raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
|
|
67
95
|
|
|
68
96
|
Vic20.new(ram: ram || :unexpanded)
|
|
69
97
|
end
|
|
70
98
|
private_class_method :vic20
|
|
99
|
+
|
|
100
|
+
def self.c128(model, sid_model, mode) = C128.new(model:, sid_model:, mode:)
|
|
101
|
+
private_class_method :c128
|
|
102
|
+
|
|
103
|
+
def self.vic20_for(media, model, ram)
|
|
104
|
+
unless media.nil?
|
|
105
|
+
unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
106
|
+
raise Unsupported, "doesn't go in a VIC-20" if unsupported
|
|
107
|
+
|
|
108
|
+
unless ram
|
|
109
|
+
ram = Media::Vic20Media.ram_for(media)
|
|
110
|
+
yield "RAM expansion: #{ram}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
build(:vic20, model:, ram:)
|
|
114
|
+
end
|
|
115
|
+
private_class_method :vic20_for
|
|
116
|
+
|
|
117
|
+
def self.c64_media?(media)
|
|
118
|
+
return false if media.nil? || !File.file?(media)
|
|
119
|
+
|
|
120
|
+
extension = File.extname(media).downcase
|
|
121
|
+
return true if extension == ".sid"
|
|
122
|
+
return false unless %w[.prg .p00].include?(extension)
|
|
123
|
+
|
|
124
|
+
[Media::BASIC_START, Media::BASIC_START - 1].include?(Storage::P00.load_address(media))
|
|
125
|
+
end
|
|
126
|
+
private_class_method :c64_media?
|
|
71
127
|
end
|
|
72
128
|
end
|
|
@@ -15,7 +15,7 @@ module Badline
|
|
|
15
15
|
module BootDisk
|
|
16
16
|
SIGNATURE = "CBM".bytes.freeze
|
|
17
17
|
AUTO_BOOT = "copyright cbm 86"
|
|
18
|
-
TYPES = %
|
|
18
|
+
TYPES = Extensions.of(%i[disk])
|
|
19
19
|
|
|
20
20
|
class << self
|
|
21
21
|
def takes?(computer, path)
|
|
@@ -38,7 +38,7 @@ module Badline
|
|
|
38
38
|
type = File.extname(path).downcase
|
|
39
39
|
return false unless TYPES.include?(type) && File.file?(path)
|
|
40
40
|
|
|
41
|
-
image =
|
|
41
|
+
image = Media.open_storage(path, { read_only: true })
|
|
42
42
|
block = image.read_block(1, 0)
|
|
43
43
|
return true if block && block[0, 3] == SIGNATURE
|
|
44
44
|
|
|
@@ -11,9 +11,9 @@ module Badline
|
|
|
11
11
|
# A list naming no disk image that's there.
|
|
12
12
|
class Error < ArgumentError; end
|
|
13
13
|
|
|
14
|
-
EXTENSIONS = %
|
|
14
|
+
EXTENSIONS = Extensions.of(%i[disk_list])
|
|
15
15
|
|
|
16
|
-
def self.list?(path) =
|
|
16
|
+
def self.list?(path) = Extensions.kind(path) == :disk_list
|
|
17
17
|
|
|
18
18
|
# The disk `path` puts in the drive: a list's first disk, or the
|
|
19
19
|
# path itself.
|
|
@@ -32,7 +32,7 @@ module Badline
|
|
|
32
32
|
found = []
|
|
33
33
|
entries(list).each do |entry|
|
|
34
34
|
disk = entry.start_with?("/") ? entry : File.join(directory, entry)
|
|
35
|
-
found << disk if
|
|
35
|
+
found << disk if Extensions::DISKS.include?(File.extname(disk).downcase) && File.file?(disk)
|
|
36
36
|
end
|
|
37
37
|
found
|
|
38
38
|
end
|
|
@@ -13,7 +13,7 @@ module Badline
|
|
|
13
13
|
# ahead of the names.
|
|
14
14
|
module DiskSet
|
|
15
15
|
KINDS = %w[disk disc side part].freeze
|
|
16
|
-
EXTENSIONS =
|
|
16
|
+
EXTENSIONS = Extensions::DISKS
|
|
17
17
|
|
|
18
18
|
# The paths of the set's disks in order, or just `path` when it
|
|
19
19
|
# belongs to none. A list's set is the disks it lists.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# The kind of medium each file extension names, which every check of a
|
|
6
|
+
# path's extension reads. A file with any other extension is a program.
|
|
7
|
+
# It loads on its own, for Options.
|
|
8
|
+
#
|
|
9
|
+
# - :disk, a disk image that the traps mount and a true drive reads
|
|
10
|
+
# - :gcr, a disk's raw GCR, which only a true drive reads
|
|
11
|
+
# - :archive, a .t64, which the traps mount read-only
|
|
12
|
+
# - :tape, :cartridge and :tune
|
|
13
|
+
# - :disk_list, a list of the disks of a set (DiskList)
|
|
14
|
+
module Extensions
|
|
15
|
+
KINDS = {
|
|
16
|
+
".d64" => :disk, ".d71" => :disk, ".d81" => :disk, ".g64" => :gcr, ".g71" => :gcr, ".t64" => :archive,
|
|
17
|
+
".tap" => :tape, ".crt" => :cartridge, ".sid" => :tune, ".m3u" => :disk_list, ".vfl" => :disk_list
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# The kind of the medium at `path`, by its extension.
|
|
21
|
+
def self.kind(path) = KINDS.fetch(File.extname(path).downcase, :program)
|
|
22
|
+
|
|
23
|
+
# The extensions of the kinds given.
|
|
24
|
+
def self.of(kinds) = KINDS.keys.select { |extension| kinds.include?(KINDS[extension]) }.freeze
|
|
25
|
+
|
|
26
|
+
# Disk images, which a drive reads.
|
|
27
|
+
DISKS = of(%i[disk gcr])
|
|
28
|
+
|
|
29
|
+
# What goes in device 8 through the traps.
|
|
30
|
+
MOUNTABLE = of(%i[disk archive])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# A program loading into a C64 or a C128 once it has booted
|
|
6
|
+
# (Media.attach). The VIC-20 has its own (Vic20Basic).
|
|
7
|
+
module Program
|
|
8
|
+
class << self
|
|
9
|
+
# Loads the program where it says, and RUNs one that loads at the
|
|
10
|
+
# start of BASIC.
|
|
11
|
+
def load(computer, data, autostart:)
|
|
12
|
+
load_addr = computer.load_prg(data)
|
|
13
|
+
# 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. BASIC 7.0
|
|
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]
|
|
17
|
+
return unless autostart && (load_addr == start || load_addr == start - 1)
|
|
18
|
+
|
|
19
|
+
# The end of the program, where BASIC's variables start, and where
|
|
20
|
+
# the KERNAL's LOAD leaves its end address.
|
|
21
|
+
end_addr = load_addr + data.length - 2
|
|
22
|
+
[text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
|
|
23
|
+
computer.type_text("run\r")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -32,9 +32,10 @@ module Badline
|
|
|
32
32
|
# other disk media, which the traps would mount, when a true drive
|
|
33
33
|
# is device 8.
|
|
34
34
|
def takes?(computer, path)
|
|
35
|
-
|
|
35
|
+
kind = Extensions.kind(path)
|
|
36
|
+
return true if kind == :gcr
|
|
36
37
|
|
|
37
|
-
!drive(computer).nil? && (File.directory?(path) ||
|
|
38
|
+
!drive(computer).nil? && (File.directory?(path) || %i[disk archive].include?(kind))
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
# Puts the image in the drive on device 8, plugging one in that
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# A .sid tune on a machine with a SID (Media.attach): its data and the
|
|
6
|
+
# player the tune boots with go in RAM once the KERNAL has booted, and
|
|
7
|
+
# the autostart types what starts it.
|
|
8
|
+
module Tune
|
|
9
|
+
class << self
|
|
10
|
+
# Plays `subtune`, or the tune's own start subtune, and says what
|
|
11
|
+
# plays. A tune for more than one SID raises FormatError.
|
|
12
|
+
def attach(computer, path, autostart:, subtune:)
|
|
13
|
+
tune = Storage::SIDFile.new(path)
|
|
14
|
+
if tune.sids > 1
|
|
15
|
+
raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
|
|
19
|
+
computer.on_init { start(computer, tune, autostart:, subtune:) }
|
|
20
|
+
title = tune.name.empty? ? path : tune.name
|
|
21
|
+
title += " (subtune #{subtune})" if tune.subtunes > 1
|
|
22
|
+
autostart ? "Playing #{title}" : "Loaded #{title}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def start(computer, tune, autostart:, subtune:)
|
|
28
|
+
computer.ram.write(tune.load_address, tune.data)
|
|
29
|
+
tune.boot_memory(subtune:).each { |address, bytes| computer.ram.write(address, bytes) }
|
|
30
|
+
computer.type_text(tune.boot_command) if autostart
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -26,12 +26,6 @@ module Badline
|
|
|
26
26
|
parts.map { |part| rom_chip(part) }
|
|
27
27
|
end
|
|
28
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
29
|
private
|
|
36
30
|
|
|
37
31
|
def load_address(data)
|
|
@@ -42,7 +36,7 @@ module Badline
|
|
|
42
36
|
name = part_names(path, own, address).find { |candidate| File.file?(candidate) }
|
|
43
37
|
return unless name
|
|
44
38
|
|
|
45
|
-
part = program_bytes(name)
|
|
39
|
+
part = Media.program_bytes(name)
|
|
46
40
|
part if load_address(part) == address
|
|
47
41
|
end
|
|
48
42
|
|
|
@@ -2,45 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Media
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# RUNs. A program that loads into
|
|
8
|
-
# instead, as are the parts of a set
|
|
9
|
-
# (Vic20Cartridge), and goes in with a power cycle,
|
|
10
|
-
# Disks mount on device 8 through the KERNAL traps, or
|
|
11
|
-
# 1541 (TrueDrive), and tapes play on the datasette.
|
|
5
|
+
# What a VIC-20 does with media of its own (Media.attach). A program
|
|
6
|
+
# loads once the KERNAL has booted: a BASIC program at the start of
|
|
7
|
+
# BASIC, wherever the RAM puts it, and RUNs. A program that loads into
|
|
8
|
+
# BLK5 at $A000 is a cartridge's ROM instead, as are the parts of a set
|
|
9
|
+
# that includes one (Vic20Cartridge), and goes in with a power cycle,
|
|
10
|
+
# as a .crt does. Disks mount on device 8 through the KERNAL traps, or
|
|
11
|
+
# go in a true 1541 (TrueDrive), and tapes play on the datasette.
|
|
12
12
|
module Vic20Media
|
|
13
13
|
# The expansions that fill BLK1, BLK2 and BLK3 one block after
|
|
14
14
|
# another, each with the last address it fills.
|
|
15
15
|
BLOCK_EXPANSIONS = [[0x3fff, :"8k"], [0x5fff, :"16k"], [0x7fff, :"24k"]].freeze
|
|
16
16
|
|
|
17
17
|
# What a VIC-20 doesn't take.
|
|
18
|
-
UNSUPPORTED = %
|
|
18
|
+
UNSUPPORTED = Extensions.of(%i[tune])
|
|
19
19
|
|
|
20
20
|
# The platform byte of a VIC-20 tape (Storage::TAP#platform).
|
|
21
21
|
TAPE_PLATFORM = 1
|
|
22
22
|
|
|
23
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
24
|
# The RAM expansion (Vic20::Bus::RAM_CONFIGURATIONS) a VIC-20 needs
|
|
45
25
|
# for the program at `path`, or for the first program on the disk
|
|
46
26
|
# or in the directory there, by where it loads. A BASIC program
|
|
@@ -60,72 +40,50 @@ module Badline
|
|
|
60
40
|
end
|
|
61
41
|
end
|
|
62
42
|
|
|
63
|
-
|
|
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
|
-
|
|
43
|
+
# The program at `path`, or the first on the disk or in the
|
|
44
|
+
# directory there, or nil.
|
|
70
45
|
def first_program(path)
|
|
71
|
-
|
|
72
|
-
if File.directory?(path) ||
|
|
73
|
-
open_storage(path).read_file("*")
|
|
74
|
-
elsif File.file?(path) && !%
|
|
75
|
-
|
|
46
|
+
kind = Extensions.kind(path)
|
|
47
|
+
if File.directory?(path) || %i[disk archive].include?(kind)
|
|
48
|
+
Media.open_storage(path).read_file("*")
|
|
49
|
+
elsif File.file?(path) && !%i[tune cartridge tape gcr].include?(kind)
|
|
50
|
+
Media.program_bytes(path)
|
|
76
51
|
end
|
|
77
52
|
end
|
|
78
53
|
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
54
|
+
# What loads and runs `data`, the first program on the disk in
|
|
55
|
+
# device 8: a BASIC program loads relocated to the start of BASIC,
|
|
56
|
+
# and anything else where it says.
|
|
116
57
|
def disk_autostart(data)
|
|
117
58
|
basic = data && data.length > 2 && Vic20Basic::BASIC_STARTS.key?(data[0] | (data[1] << 8))
|
|
118
59
|
basic ? %(lO"*",8\rrun\r) : AUTOSTART
|
|
119
60
|
end
|
|
120
61
|
|
|
121
|
-
|
|
122
|
-
|
|
62
|
+
# Loads the program, `bytes` from the file at `path`, as Vic20Basic
|
|
63
|
+
# has it once the KERNAL has booted, or attaches the cartridge it's
|
|
64
|
+
# a part of (Vic20Cartridge), and says which.
|
|
65
|
+
def attach_program(machine, path, bytes, autostart:)
|
|
123
66
|
chips = Vic20Cartridge.chips(path, bytes)
|
|
124
|
-
|
|
67
|
+
if chips
|
|
68
|
+
machine.attach_cartridge(chips)
|
|
69
|
+
return "Attached cartridge #{path}"
|
|
70
|
+
end
|
|
125
71
|
|
|
126
72
|
machine.on_init { Vic20Basic.load_program(machine, bytes, autostart:) }
|
|
127
73
|
"Loading #{path}"
|
|
128
74
|
end
|
|
75
|
+
|
|
76
|
+
# A tape made for another machine plays all the same, and says so.
|
|
77
|
+
def tape_message(tape, message)
|
|
78
|
+
tape.platform == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the VIC-20"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def expansion_reaching(last)
|
|
84
|
+
BLOCK_EXPANSIONS.each { |reach, ram| return ram if last <= reach }
|
|
85
|
+
BLOCK_EXPANSIONS.last.last
|
|
86
|
+
end
|
|
129
87
|
end
|
|
130
88
|
end
|
|
131
89
|
end
|
data/lib/badline/media.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/media/extensions"
|
|
3
4
|
require "badline/media/true_drive"
|
|
4
5
|
require "badline/media/boot_disk"
|
|
5
6
|
require "badline/media/not_disk"
|
|
@@ -9,6 +10,8 @@ require "badline/media/disk_list"
|
|
|
9
10
|
require "badline/media/vic20_basic"
|
|
10
11
|
require "badline/media/vic20_cartridge"
|
|
11
12
|
require "badline/media/vic20_media"
|
|
13
|
+
require "badline/media/tune"
|
|
14
|
+
require "badline/media/program"
|
|
12
15
|
|
|
13
16
|
module Badline
|
|
14
17
|
module Media
|
|
@@ -16,6 +19,7 @@ module Badline
|
|
|
16
19
|
TAPE_AUTOSTART = %(lO\rrun\r)
|
|
17
20
|
BASIC_START = 0x0801
|
|
18
21
|
|
|
22
|
+
# The storage that opens each of Extensions::MOUNTABLE.
|
|
19
23
|
MOUNT_TYPES = {
|
|
20
24
|
".d64" => Storage::D64Image,
|
|
21
25
|
".d71" => Storage::D71Image,
|
|
@@ -38,13 +42,17 @@ module Badline
|
|
|
38
42
|
# KERNAL to boot at power-on, with nothing typed (BootDisk).
|
|
39
43
|
#
|
|
40
44
|
# An .m3u or .vfl list of disks attaches the first disk it lists
|
|
41
|
-
# (DiskList).
|
|
42
|
-
#
|
|
45
|
+
# (DiskList). Every machine takes each kind of medium the same way,
|
|
46
|
+
# except that the VIC-20 (Vic20Media) has cartridges, programs and a
|
|
47
|
+
# disk autostart of its own, and takes no .sid tunes. The C128 in C64
|
|
48
|
+
# mode takes a C64's.
|
|
43
49
|
def attach(computer, path, autostart: true, subtune: nil, **)
|
|
44
50
|
path = DiskList.disk(path)
|
|
45
|
-
|
|
51
|
+
if computer.family == :vic20 && Vic20Media::UNSUPPORTED.include?(File.extname(path).downcase)
|
|
52
|
+
raise ArgumentError, "#{path} doesn't go in a VIC-20"
|
|
53
|
+
end
|
|
46
54
|
|
|
47
|
-
BootDisk.attach(computer, path, **) ||
|
|
55
|
+
BootDisk.attach(computer, path, **) || attach_kind(computer, path, autostart:, subtune:, **)
|
|
48
56
|
end
|
|
49
57
|
|
|
50
58
|
# Swaps the disk in device 8 for a disk image or a host directory,
|
|
@@ -73,109 +81,104 @@ module Badline
|
|
|
73
81
|
# The SID a machine for `path` should be built with. A .sid tune names
|
|
74
82
|
# its own; everything else gets `otherwise`.
|
|
75
83
|
def sid_model(path, otherwise: :mos6581)
|
|
76
|
-
return otherwise unless path &&
|
|
84
|
+
return otherwise unless path && Extensions.kind(path) == :tune
|
|
77
85
|
|
|
78
86
|
Storage::SIDFile.new(path).sid_model
|
|
79
87
|
end
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
# What the medium at `path` is to `computer`, which attach goes by:
|
|
90
|
+
# :true_drive for a disk that goes in a true drive (TrueDrive),
|
|
91
|
+
# :directory, :storage for a disk image or a .t64 that the traps
|
|
92
|
+
# mount, and otherwise its kind by its extension (Extensions).
|
|
93
|
+
def kind(computer, path)
|
|
94
|
+
return :true_drive if TrueDrive.takes?(computer, path)
|
|
95
|
+
return :directory if File.directory?(path)
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
|
|
86
|
-
elsif File.directory?(path)
|
|
87
|
-
computer.mount(Storage::HostDirectory.new(path))
|
|
88
|
-
"Mounted #{path} as device 8"
|
|
89
|
-
elsif File.extname(path).downcase == ".crt"
|
|
90
|
-
attach_cartridge(computer, path, options.fetch(:cartridge, {}))
|
|
91
|
-
elsif File.extname(path).downcase == ".sid"
|
|
92
|
-
attach_sid(computer, path, autostart:, subtune:)
|
|
93
|
-
elsif File.extname(path).downcase == ".tap"
|
|
94
|
-
attach_tape(computer, path, autostart:)
|
|
95
|
-
elsif MOUNT_TYPES.key?(File.extname(path).downcase)
|
|
96
|
-
attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
|
|
97
|
-
else
|
|
98
|
-
attach_prg(computer, path, autostart:)
|
|
99
|
-
end
|
|
97
|
+
kind = Extensions.kind(path)
|
|
98
|
+
%i[disk archive].include?(kind) ? :storage : kind
|
|
100
99
|
end
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Disk images take the `disk` options. A .t64 is read-only whatever
|
|
108
|
-
# it's given.
|
|
109
|
-
def open_storage(path, disk)
|
|
101
|
+
# What device 8 serves through the traps for a host directory, a disk
|
|
102
|
+
# image or a .t64. Disk images take the `disk` options. A .t64 is
|
|
103
|
+
# read-only whatever it's given.
|
|
104
|
+
def open_storage(path, disk = {})
|
|
110
105
|
return Storage::HostDirectory.new(path) if File.directory?(path)
|
|
111
106
|
|
|
112
|
-
storage = MOUNT_TYPES
|
|
107
|
+
storage = MOUNT_TYPES.fetch(File.extname(path).downcase)
|
|
113
108
|
storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
|
|
114
109
|
end
|
|
115
110
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
# A program file's bytes, unwrapped from a .p00.
|
|
112
|
+
def program_bytes(path)
|
|
113
|
+
bytes = File.binread(path).bytes
|
|
114
|
+
Storage::P00.wraps?(bytes) ? Storage::P00.data(bytes) : bytes
|
|
119
115
|
end
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
tune = Storage::SIDFile.new(path)
|
|
123
|
-
if tune.sids > 1
|
|
124
|
-
raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
|
|
128
|
-
computer.on_init { start_tune(computer, tune, autostart:, subtune:) }
|
|
129
|
-
title = tune.name.empty? ? path : tune.name
|
|
130
|
-
title += " (subtune #{subtune})" if tune.subtunes > 1
|
|
131
|
-
autostart ? "Playing #{title}" : "Loaded #{title}"
|
|
132
|
-
end
|
|
117
|
+
private
|
|
133
118
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
computer
|
|
119
|
+
def attach_kind(computer, path, autostart:, subtune:, **options)
|
|
120
|
+
disk = options.fetch(:disk, {})
|
|
121
|
+
case kind(computer, path)
|
|
122
|
+
when :true_drive then attach_true_drive(computer, path, disk, autostart:)
|
|
123
|
+
when :directory then mount(computer, path, disk, autostart: false)
|
|
124
|
+
when :storage then mount(computer, path, disk, autostart:)
|
|
125
|
+
when :cartridge then attach_cartridge(computer, path, options.fetch(:cartridge, {}))
|
|
126
|
+
when :tune then Tune.attach(computer, path, autostart:, subtune:)
|
|
127
|
+
when :tape then attach_tape(computer, path, autostart:)
|
|
128
|
+
else attach_prg(computer, path, autostart:)
|
|
129
|
+
end
|
|
138
130
|
end
|
|
139
131
|
|
|
140
|
-
def
|
|
141
|
-
computer.datasette.insert(Storage::TAP.new(path))
|
|
142
|
-
computer.datasette.play!
|
|
143
|
-
computer.type_text(TAPE_AUTOSTART) if autostart
|
|
144
|
-
"Inserted #{path} in the datasette"
|
|
145
|
-
end
|
|
132
|
+
def disk?(path) = File.directory?(path) || Extensions.kind(path) == :disk
|
|
146
133
|
|
|
147
134
|
def attach_true_drive(computer, path, disk, autostart:)
|
|
148
135
|
message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
|
|
149
|
-
computer.type_text(
|
|
136
|
+
computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
|
|
150
137
|
message
|
|
151
138
|
end
|
|
152
139
|
|
|
153
|
-
def
|
|
154
|
-
|
|
155
|
-
computer.
|
|
140
|
+
def mount(computer, path, disk, autostart:)
|
|
141
|
+
storage = open_storage(path, disk)
|
|
142
|
+
computer.mount(storage)
|
|
143
|
+
computer.type_text(disk_autostart(computer) { storage.read_file("*") }) if autostart
|
|
156
144
|
"Mounted #{path} as device 8"
|
|
157
145
|
end
|
|
158
146
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
147
|
+
# What loads and runs the first program on the disk in device 8. A
|
|
148
|
+
# VIC-20 goes by that program, which the block gives
|
|
149
|
+
# (Vic20Media.disk_autostart).
|
|
150
|
+
def disk_autostart(computer)
|
|
151
|
+
computer.family == :vic20 ? Vic20Media.disk_autostart(yield) : AUTOSTART
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# A VIC-20 cartridge is its ROM chips.
|
|
155
|
+
def attach_cartridge(computer, path, options)
|
|
156
|
+
if computer.family == :vic20
|
|
157
|
+
computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
|
|
158
|
+
else
|
|
159
|
+
computer.attach_cartridge(Cartridge.from_file(path, **options))
|
|
160
|
+
end
|
|
161
|
+
"Attached cartridge #{path}"
|
|
164
162
|
end
|
|
165
163
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
164
|
+
# A VIC-20 says when the tape was made for another machine
|
|
165
|
+
# (Vic20Media.tape_message).
|
|
166
|
+
def attach_tape(computer, path, autostart:)
|
|
167
|
+
tape = Storage::TAP.new(path)
|
|
168
|
+
computer.datasette.insert(tape)
|
|
169
|
+
computer.datasette.play!
|
|
170
|
+
computer.type_text(TAPE_AUTOSTART) if autostart
|
|
171
|
+
message = "Inserted #{path} in the datasette"
|
|
172
|
+
computer.family == :vic20 ? Vic20Media.tape_message(tape, message) : message
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A VIC-20 has programs of its own (Vic20Media.attach_program).
|
|
176
|
+
def attach_prg(computer, path, autostart:)
|
|
177
|
+
bytes = program_bytes(path)
|
|
178
|
+
return Vic20Media.attach_program(computer, path, bytes, autostart:) if computer.family == :vic20
|
|
179
|
+
|
|
180
|
+
computer.on_init { Program.load(computer, bytes, autostart:) }
|
|
181
|
+
"Loading #{path}"
|
|
179
182
|
end
|
|
180
183
|
end
|
|
181
184
|
end
|