badline 0.11.1 → 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 +26 -0
- data/CLAUDE.md +8 -7
- data/CONTRIBUTING.md +7 -6
- data/README.md +12 -5
- data/doc/media.md +28 -1
- data/doc/pinned-behaviour.md +31 -1
- data/exe/badline-ruby +1 -1
- 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 +17 -52
- data/lib/badline/c128/bus_state.rb +5 -5
- data/lib/badline/c128/drives.rb +35 -22
- data/lib/badline/c128/fast_serial.rb +1 -0
- data/lib/badline/c128/io_pages.rb +35 -0
- data/lib/badline/c128/mmu.rb +9 -7
- data/lib/badline/c128/modes.rb +17 -4
- data/lib/badline/c128/saved_state.rb +22 -0
- data/lib/badline/c128/z80.rb +49 -0
- data/lib/badline/c128/z80_pages.rb +118 -0
- data/lib/badline/c128/z80_turns.rb +106 -0
- data/lib/badline/c128.rb +21 -83
- data/lib/badline/cartridge/action_replay.rb +2 -2
- data/lib/badline/cartridge/comal80.rb +1 -7
- data/lib/badline/cartridge/dinamic.rb +0 -8
- data/lib/badline/cartridge/final_cartridge3.rb +6 -6
- data/lib/badline/cartridge/fun_play.rb +0 -8
- data/lib/badline/cartridge/game_system.rb +0 -8
- data/lib/badline/cartridge/kcs_power.rb +10 -7
- data/lib/badline/cartridge/mach5.rb +2 -4
- data/lib/badline/cartridge/magic_desk.rb +0 -8
- data/lib/badline/cartridge/ocean.rb +0 -8
- data/lib/badline/cartridge/pagefox.rb +2 -2
- data/lib/badline/cartridge/retro_replay.rb +7 -6
- data/lib/badline/cartridge/rex_utility.rb +2 -6
- data/lib/badline/cartridge/rgcd.rb +2 -2
- data/lib/badline/cartridge/simons_basic.rb +0 -5
- data/lib/badline/cartridge/super_games.rb +4 -2
- data/lib/badline/cartridge/westermann.rb +2 -6
- data/lib/badline/cartridge.rb +37 -9
- data/lib/badline/cia/fast_forward.rb +21 -3
- data/lib/badline/cia.rb +8 -5
- data/lib/badline/computer/attachments.rb +14 -95
- data/lib/badline/computer/common.rb +34 -0
- data/lib/badline/computer/kernal_traps.rb +65 -5
- data/lib/badline/computer/true_drives.rb +69 -0
- data/lib/badline/computer.rb +9 -68
- data/lib/badline/core.rb +4 -2
- data/lib/badline/cpu_port.rb +112 -0
- data/lib/badline/drive/core.rb +8 -33
- data/lib/badline/drive/fast_serial.rb +73 -0
- data/lib/badline/drive/idle.rb +1 -11
- data/lib/badline/drive/orbit.rb +2 -5
- data/lib/badline/drive/sleep.rb +0 -2
- data/lib/badline/drive/vias.rb +84 -0
- data/lib/badline/drive.rb +2 -0
- data/lib/badline/drive1541/disk.rb +14 -17
- data/lib/badline/drive1541/disk_state.rb +7 -15
- data/lib/badline/drive1541.rb +1 -0
- data/lib/badline/drive1571/wd1770.rb +94 -8
- data/lib/badline/drive1571.rb +8 -58
- data/lib/badline/drive1581/bus.rb +137 -0
- data/lib/badline/drive1581/cpu.rb +11 -0
- data/lib/badline/drive1581/disk.rb +135 -0
- data/lib/badline/drive1581/disk_state.rb +87 -0
- data/lib/badline/drive1581/mechanism.rb +117 -0
- data/lib/badline/drive1581/serial_port.rb +52 -0
- data/lib/badline/drive1581/slot.rb +78 -0
- data/lib/badline/drive1581/track.rb +100 -0
- data/lib/badline/drive1581/track_bytes.rb +75 -0
- data/lib/badline/drive1581/wd1772/registers.rb +89 -0
- data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
- data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
- data/lib/badline/drive1581/wd1772/state.rb +66 -0
- data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
- data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
- data/lib/badline/drive1581/wd1772/turn.rb +73 -0
- data/lib/badline/drive1581/wd1772.rb +216 -0
- data/lib/badline/drive1581.rb +233 -0
- data/lib/badline/frontend/app.rb +1 -3
- data/lib/badline/frontend/audio_device.rb +67 -0
- data/lib/badline/frontend/audio_sink.rb +16 -50
- data/lib/badline/frontend/boot.rb +24 -67
- data/lib/badline/frontend/buttons.rb +7 -7
- data/lib/badline/frontend/keys.rb +33 -8
- data/lib/badline/frontend/media_slots.rb +42 -0
- data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
- data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
- data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
- data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
- data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
- data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
- data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
- data/lib/badline/frontend/menu/theme.rb +18 -0
- data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
- data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
- data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
- data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
- data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
- data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
- data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
- data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
- data/lib/badline/frontend/player/theme.rb +18 -0
- data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
- data/lib/badline/frontend/sound.rb +19 -38
- data/lib/badline/frontend/timeline.rb +3 -14
- data/lib/badline/frontend.rb +24 -20
- data/lib/badline/iec_bus.rb +14 -2
- 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 +63 -6
- data/lib/badline/media/boot_disk.rb +50 -0
- 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/not_disk.rb +8 -0
- data/lib/badline/media/program.rb +28 -0
- data/lib/badline/media/true_drive.rb +38 -17
- 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 +90 -81
- data/lib/badline/options/event.rb +4 -2
- data/lib/badline/options/family.rb +3 -9
- data/lib/badline/options/help.rb +2 -1
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1581.rom +0 -0
- data/lib/badline/sdl.rb +37 -20
- data/lib/badline/sid.rb +1 -1
- data/lib/badline/snapshot/machine_state.rb +9 -1
- data/lib/badline/snapshot/state.rb +6 -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/p00.rb +8 -0
- data/lib/badline/storage/t64.rb +1 -1
- data/lib/badline/time_of_day.rb +3 -3
- 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 +15 -71
- data/lib/badline/vic20/saved_state.rb +2 -30
- data/lib/badline/vic20.rb +4 -37
- data/lib/badline/z80/core.rb +2 -0
- data/lib/badline/z80/saved_state.rb +69 -0
- metadata +61 -23
- data/lib/badline/cycleable.rb +0 -38
- /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
- /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
- /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
- /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Media
|
|
5
5
|
# Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
|
|
6
|
-
# the C128 its 1571. Once a true drive is plugged in as
|
|
7
|
-
# media go into it rather than into the traps, so a LOAD
|
|
8
|
-
# and every byte crosses the serial bus. A .g64 holds the
|
|
9
|
-
# GCR, which only a true drive reads, so it plugs one in
|
|
10
|
-
# none yet. The 1541 reads only .d64 and .g64 images,
|
|
11
|
-
# .d71 and .g71 as well,
|
|
12
|
-
#
|
|
6
|
+
# the C128 its 1571, or a 1581. Once a true drive is plugged in as
|
|
7
|
+
# device 8, disk media go into it rather than into the traps, so a LOAD
|
|
8
|
+
# runs the DOS and every byte crosses the serial bus. A .g64 holds the
|
|
9
|
+
# disk's raw GCR, which only a true drive reads, so it plugs one in
|
|
10
|
+
# when there's none yet. The 1541 reads only .d64 and .g64 images, the
|
|
11
|
+
# 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
|
|
12
|
+
# the drive on device 8 for a 1581 (Drive1581::Slot), and another disk
|
|
13
|
+
# image swaps a 1581 back for the machine's own drive. The traps' other
|
|
14
|
+
# disk media, a .t64 or a host directory, raise Error.
|
|
13
15
|
module TrueDrive
|
|
14
16
|
class Error < ArgumentError; end
|
|
15
17
|
|
|
@@ -30,27 +32,46 @@ module Badline
|
|
|
30
32
|
# other disk media, which the traps would mount, when a true drive
|
|
31
33
|
# is device 8.
|
|
32
34
|
def takes?(computer, path)
|
|
33
|
-
|
|
35
|
+
kind = Extensions.kind(path)
|
|
36
|
+
return true if kind == :gcr
|
|
34
37
|
|
|
35
|
-
!drive(computer).nil? && (File.directory?(path) ||
|
|
38
|
+
!drive(computer).nil? && (File.directory?(path) || %i[disk archive].include?(kind))
|
|
36
39
|
end
|
|
37
40
|
|
|
38
|
-
# Puts the image in the drive on device 8, plugging one in
|
|
39
|
-
# there'
|
|
40
|
-
# so LOAD and SAVE reach the drive too.
|
|
41
|
-
# write-protected.
|
|
41
|
+
# Puts the image in the drive on device 8, plugging one in that
|
|
42
|
+
# reads it when the drive there doesn't, and takes out what was
|
|
43
|
+
# mounted through the traps, so LOAD and SAVE reach the drive too.
|
|
44
|
+
# `read_only` puts it in write-protected.
|
|
42
45
|
def insert(computer, path, read_only: false)
|
|
43
|
-
|
|
46
|
+
return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
|
|
47
|
+
|
|
48
|
+
drive = own_drive(computer)
|
|
49
|
+
name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
|
|
44
50
|
types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
|
|
45
51
|
unless types.include?(File.extname(path).downcase)
|
|
46
|
-
raise Error, "#{path} is not a #{types
|
|
52
|
+
raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
|
|
47
53
|
end
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
(drive || computer.plug_true_drive).insert_image(path, read_only:)
|
|
50
56
|
computer.unmount
|
|
51
|
-
plug(computer).insert(disk)
|
|
52
57
|
"Inserted #{path} in the #{name} as device 8"
|
|
53
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The drive on device 8 unless it's a 1581.
|
|
63
|
+
def own_drive(computer)
|
|
64
|
+
drive = drive(computer)
|
|
65
|
+
drive unless drive&.model_name == "1581"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def insert_d81(computer, path, read_only)
|
|
69
|
+
drive = drive(computer)
|
|
70
|
+
drive = computer.plug_drive1581 unless drive&.model_name == "1581"
|
|
71
|
+
drive.insert_image(path, read_only:)
|
|
72
|
+
computer.unmount
|
|
73
|
+
"Inserted #{path} in the 1581 as device 8"
|
|
74
|
+
end
|
|
54
75
|
end
|
|
55
76
|
end
|
|
56
77
|
end
|
|
@@ -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,12 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/media/extensions"
|
|
3
4
|
require "badline/media/true_drive"
|
|
5
|
+
require "badline/media/boot_disk"
|
|
6
|
+
require "badline/media/not_disk"
|
|
4
7
|
require "badline/media/queue"
|
|
5
8
|
require "badline/media/disk_set"
|
|
6
9
|
require "badline/media/disk_list"
|
|
7
10
|
require "badline/media/vic20_basic"
|
|
8
11
|
require "badline/media/vic20_cartridge"
|
|
9
12
|
require "badline/media/vic20_media"
|
|
13
|
+
require "badline/media/tune"
|
|
14
|
+
require "badline/media/program"
|
|
10
15
|
|
|
11
16
|
module Badline
|
|
12
17
|
module Media
|
|
@@ -14,6 +19,7 @@ module Badline
|
|
|
14
19
|
TAPE_AUTOSTART = %(lO\rrun\r)
|
|
15
20
|
BASIC_START = 0x0801
|
|
16
21
|
|
|
22
|
+
# The storage that opens each of Extensions::MOUNTABLE.
|
|
17
23
|
MOUNT_TYPES = {
|
|
18
24
|
".d64" => Storage::D64Image,
|
|
19
25
|
".d71" => Storage::D71Image,
|
|
@@ -32,14 +38,21 @@ module Badline
|
|
|
32
38
|
# there, a .d64 goes into it instead of the KERNAL traps too. The
|
|
33
39
|
# autostart then loads through it.
|
|
34
40
|
#
|
|
41
|
+
# On a C128 in C128 mode, a boot disk goes in a true drive for the
|
|
42
|
+
# KERNAL to boot at power-on, with nothing typed (BootDisk).
|
|
43
|
+
#
|
|
35
44
|
# An .m3u or .vfl list of disks attaches the first disk it lists
|
|
36
|
-
# (DiskList).
|
|
37
|
-
#
|
|
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.
|
|
38
49
|
def attach(computer, path, autostart: true, subtune: nil, **)
|
|
39
50
|
path = DiskList.disk(path)
|
|
40
|
-
|
|
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
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
BootDisk.attach(computer, path, **) || attach_kind(computer, path, autostart:, subtune:, **)
|
|
43
56
|
end
|
|
44
57
|
|
|
45
58
|
# Swaps the disk in device 8 for a disk image or a host directory,
|
|
@@ -50,11 +63,12 @@ module Badline
|
|
|
50
63
|
# A .g64 goes in a true 1541, which is plugged in as device 8 when
|
|
51
64
|
# there's none yet (TrueDrive). Once a true 1541 is device 8, a .d64
|
|
52
65
|
# goes in its drive as well, and other disks, which a 1541 can't
|
|
53
|
-
# read, raise TrueDrive::Error, an ArgumentError.
|
|
66
|
+
# read, raise TrueDrive::Error, an ArgumentError. Anything else
|
|
67
|
+
# raises NotDisk.
|
|
54
68
|
def insert_disk(computer, path, read_only: false)
|
|
55
69
|
return TrueDrive.insert(computer, path, read_only:) if TrueDrive.takes?(computer, path)
|
|
56
70
|
|
|
57
|
-
raise
|
|
71
|
+
raise NotDisk, "#{path} is not a disk image or a directory" unless disk?(path)
|
|
58
72
|
|
|
59
73
|
computer.mount(open_storage(path, { read_only: }))
|
|
60
74
|
"Inserted #{path} in device 8"
|
|
@@ -67,109 +81,104 @@ module Badline
|
|
|
67
81
|
# The SID a machine for `path` should be built with. A .sid tune names
|
|
68
82
|
# its own; everything else gets `otherwise`.
|
|
69
83
|
def sid_model(path, otherwise: :mos6581)
|
|
70
|
-
return otherwise unless path &&
|
|
84
|
+
return otherwise unless path && Extensions.kind(path) == :tune
|
|
71
85
|
|
|
72
86
|
Storage::SIDFile.new(path).sid_model
|
|
73
87
|
end
|
|
74
88
|
|
|
75
|
-
|
|
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)
|
|
76
96
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
97
|
+
kind = Extensions.kind(path)
|
|
98
|
+
%i[disk archive].include?(kind) ? :storage : kind
|
|
94
99
|
end
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Disk images take the `disk` options. A .t64 is read-only whatever
|
|
102
|
-
# it's given.
|
|
103
|
-
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 = {})
|
|
104
105
|
return Storage::HostDirectory.new(path) if File.directory?(path)
|
|
105
106
|
|
|
106
|
-
storage = MOUNT_TYPES
|
|
107
|
+
storage = MOUNT_TYPES.fetch(File.extname(path).downcase)
|
|
107
108
|
storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
|
|
108
109
|
end
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
113
115
|
end
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
tune = Storage::SIDFile.new(path)
|
|
117
|
-
if tune.sids > 1
|
|
118
|
-
raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
|
|
122
|
-
computer.on_init { start_tune(computer, tune, autostart:, subtune:) }
|
|
123
|
-
title = tune.name.empty? ? path : tune.name
|
|
124
|
-
title += " (subtune #{subtune})" if tune.subtunes > 1
|
|
125
|
-
autostart ? "Playing #{title}" : "Loaded #{title}"
|
|
126
|
-
end
|
|
117
|
+
private
|
|
127
118
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
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
|
|
132
130
|
end
|
|
133
131
|
|
|
134
|
-
def
|
|
135
|
-
computer.datasette.insert(Storage::TAP.new(path))
|
|
136
|
-
computer.datasette.play!
|
|
137
|
-
computer.type_text(TAPE_AUTOSTART) if autostart
|
|
138
|
-
"Inserted #{path} in the datasette"
|
|
139
|
-
end
|
|
132
|
+
def disk?(path) = File.directory?(path) || Extensions.kind(path) == :disk
|
|
140
133
|
|
|
141
134
|
def attach_true_drive(computer, path, disk, autostart:)
|
|
142
135
|
message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
|
|
143
|
-
computer.type_text(
|
|
136
|
+
computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
|
|
144
137
|
message
|
|
145
138
|
end
|
|
146
139
|
|
|
147
|
-
def
|
|
148
|
-
|
|
149
|
-
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
|
|
150
144
|
"Mounted #{path} as device 8"
|
|
151
145
|
end
|
|
152
146
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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}"
|
|
158
162
|
end
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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}"
|
|
173
182
|
end
|
|
174
183
|
end
|
|
175
184
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/media/extensions"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
class Options
|
|
5
7
|
# One event of --at or --script: what to do, with its argument, once
|
|
@@ -23,7 +25,7 @@ module Badline
|
|
|
23
25
|
|
|
24
26
|
# What insert takes: disk images, tapes and cartridges, or an .m3u
|
|
25
27
|
# or .vfl list of disks or a directory to mount as device 8.
|
|
26
|
-
INSERTABLE = %
|
|
28
|
+
INSERTABLE = Media::Extensions.of(%i[disk gcr archive tape cartridge disk_list])
|
|
27
29
|
|
|
28
30
|
attr_reader :frame, :action, :argument
|
|
29
31
|
|
|
@@ -95,7 +97,7 @@ module Badline
|
|
|
95
97
|
def self.insertable?(path)
|
|
96
98
|
return true if File.directory?(path)
|
|
97
99
|
|
|
98
|
-
File.exist?(path) &&
|
|
100
|
+
File.exist?(path) && INSERTABLE.include?(File.extname(path).downcase)
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/storage/p00"
|
|
4
|
+
|
|
3
5
|
module Badline
|
|
4
6
|
class Options
|
|
5
7
|
# The machine family: the subcommand that names it, or else the media,
|
|
@@ -37,15 +39,7 @@ module Badline
|
|
|
37
39
|
return if @family_named || @sid_command || @media_path.nil?
|
|
38
40
|
return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
|
|
39
41
|
|
|
40
|
-
@family = :c128 if load_address(@media_path) == C128_BASIC_START
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# The load address of a .prg, or of the program a .p00 wraps after its
|
|
44
|
-
# 26-byte header.
|
|
45
|
-
def load_address(path)
|
|
46
|
-
offset = File.extname(path).casecmp?(".p00") ? 26 : 0
|
|
47
|
-
bytes = File.binread(path).bytes
|
|
48
|
-
bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
|
|
42
|
+
@family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
|
|
49
43
|
end
|
|
50
44
|
end
|
|
51
45
|
end
|
data/lib/badline/options/help.rb
CHANGED
|
@@ -61,7 +61,8 @@ module Badline
|
|
|
61
61
|
pause menu loads. Device 8 answers through traps on the KERNAL's
|
|
62
62
|
disk routines, unless --true-drive puts an emulated 1541 there,
|
|
63
63
|
which runs its own DOS and reads .d64 and .g64 images only, or on
|
|
64
|
-
the C128 the C128D's 1571, which reads .d71 and .g71 too. A .
|
|
64
|
+
the C128 the C128D's 1571, which reads .d71 and .g71 too. A .d81
|
|
65
|
+
then goes in an emulated 1581 in its place. A .sid
|
|
65
66
|
tune opens in the SID player, as `sid` plays it, unless an option
|
|
66
67
|
of the emulator's window asks for the machine.
|
|
67
68
|
|
data/lib/badline/roms/README
CHANGED
|
@@ -70,6 +70,15 @@ Downloaded from
|
|
|
70
70
|
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1571/1571-rom.310654-05.bin
|
|
71
71
|
SHA-1 f1be619c106641a685f6609e4d43d6fc9eac1e70.
|
|
72
72
|
|
|
73
|
+
dos1581.rom
|
|
74
|
+
-----------
|
|
75
|
+
The 1581 disk drive's DOS, 318045-02, 32 KB for $8000-$FFFF.
|
|
76
|
+
Drive1581 loads it.
|
|
77
|
+
|
|
78
|
+
Downloaded from
|
|
79
|
+
https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
|
|
80
|
+
SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
|
|
81
|
+
|
|
73
82
|
Commodore VIC-20 ROMs
|
|
74
83
|
=====================
|
|
75
84
|
|
|
Binary file
|