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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# An SDL audio device on SDL's queue: signed 16-bit samples, `channels`
|
|
6
|
+
# of them interleaved per frame, go in at the device's rate and SDL
|
|
7
|
+
# plays them out behind us. When #open fails the device stays closed,
|
|
8
|
+
# with the reason in SDL_GetError for the caller to report.
|
|
9
|
+
class AudioDevice
|
|
10
|
+
attr_reader :rate
|
|
11
|
+
|
|
12
|
+
def initialize(channels:, rate:, samples:)
|
|
13
|
+
@channels = channels
|
|
14
|
+
@rate = rate
|
|
15
|
+
@samples = samples
|
|
16
|
+
@device = 0
|
|
17
|
+
@buffer = IO::Buffer.new(4096)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def open? = @device != 0
|
|
21
|
+
|
|
22
|
+
# With `exact_rate` false SDL may pick the device's own rate instead,
|
|
23
|
+
# which #rate then reports.
|
|
24
|
+
def open(exact_rate)
|
|
25
|
+
return unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
|
|
26
|
+
|
|
27
|
+
SDL.spec_freq(SDL.wanted, @rate)
|
|
28
|
+
SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
|
|
29
|
+
SDL.spec_channels(SDL.wanted, @channels)
|
|
30
|
+
SDL.spec_samples(SDL.wanted, @samples)
|
|
31
|
+
allowed_changes = exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE
|
|
32
|
+
@device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
|
|
33
|
+
@rate = SDL.read_i32(SDL.obtained) if open?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns SDL_QueueAudio's result, negative on failure.
|
|
37
|
+
def queue(samples)
|
|
38
|
+
count = samples.size
|
|
39
|
+
bytes = count * 2
|
|
40
|
+
@buffer.resize(bytes) if bytes > @buffer.size
|
|
41
|
+
buffer = @buffer
|
|
42
|
+
i = 0
|
|
43
|
+
while i < count
|
|
44
|
+
buffer.set_value(:s16, i * 2, samples[i])
|
|
45
|
+
i += 1
|
|
46
|
+
end
|
|
47
|
+
SDL.SDL_QueueAudio(@device, buffer, bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @channels * @rate)
|
|
51
|
+
|
|
52
|
+
def start = SDL.SDL_PauseAudioDevice(@device, 0)
|
|
53
|
+
|
|
54
|
+
def pause = SDL.SDL_PauseAudioDevice(@device, 1)
|
|
55
|
+
|
|
56
|
+
def clear = SDL.SDL_ClearQueuedAudio(@device)
|
|
57
|
+
|
|
58
|
+
def close
|
|
59
|
+
return unless open?
|
|
60
|
+
|
|
61
|
+
SDL.SDL_CloseAudioDevice(@device)
|
|
62
|
+
SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
|
|
63
|
+
@device = 0
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -2,72 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The host's audio device for --headless and the SID player
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# us. It answers what Audio::Playback asks of a sink.
|
|
5
|
+
# The host's audio device for --headless and the SID player: a stereo
|
|
6
|
+
# AudioDevice, the left and right samples interleaved, that raises on
|
|
7
|
+
# SDL's failures. It answers what Audio::Playback asks of a sink.
|
|
9
8
|
class AudioSink
|
|
10
9
|
class Error < Audio::Playback::DeviceError; end
|
|
11
10
|
|
|
12
|
-
attr_reader :rate
|
|
13
|
-
|
|
14
11
|
# With `exact_rate` false SDL may pick the device's own rate instead,
|
|
15
12
|
# which #rate then reports.
|
|
16
13
|
def initialize(rate:, exact_rate: false, buffer: 1024)
|
|
17
|
-
@device =
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
check(SDL.SDL_InitSubSystem(SDL::INIT_AUDIO))
|
|
21
|
-
open_device(rate, exact_rate ? 0 : SDL::ALLOW_FREQUENCY_CHANGE, buffer)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def queue(samples)
|
|
25
|
-
count = samples.size
|
|
26
|
-
bytes = count * 2
|
|
27
|
-
@buffer.resize(bytes) if bytes > @buffer.size
|
|
28
|
-
buffer = @buffer
|
|
29
|
-
i = 0
|
|
30
|
-
while i < count
|
|
31
|
-
buffer.set_value(:s16, i * 2, samples[i])
|
|
32
|
-
i += 1
|
|
33
|
-
end
|
|
34
|
-
check(SDL.SDL_QueueAudio(@device, buffer, bytes))
|
|
14
|
+
@device = AudioDevice.new(channels: 2, rate:, samples: buffer)
|
|
15
|
+
@device.open(exact_rate)
|
|
16
|
+
raise Error, SDL.SDL_GetError unless @device.open?
|
|
35
17
|
end
|
|
36
18
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
def start = SDL.SDL_PauseAudioDevice(@device, 0)
|
|
40
|
-
|
|
41
|
-
def pause = SDL.SDL_PauseAudioDevice(@device, 1)
|
|
42
|
-
|
|
43
|
-
def clear = SDL.SDL_ClearQueuedAudio(@device)
|
|
19
|
+
def rate = @device.rate
|
|
44
20
|
|
|
45
|
-
def
|
|
46
|
-
|
|
21
|
+
def queue(samples)
|
|
22
|
+
result = @device.queue(samples)
|
|
23
|
+
raise Error, SDL.SDL_GetError if result.negative?
|
|
47
24
|
|
|
48
|
-
|
|
49
|
-
SDL.SDL_QuitSubSystem(SDL::INIT_AUDIO)
|
|
50
|
-
@device = 0
|
|
25
|
+
result
|
|
51
26
|
end
|
|
52
27
|
|
|
53
|
-
|
|
28
|
+
def queued_seconds = @device.queued_seconds
|
|
54
29
|
|
|
55
|
-
def
|
|
56
|
-
SDL.spec_freq(SDL.wanted, rate)
|
|
57
|
-
SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
|
|
58
|
-
SDL.spec_channels(SDL.wanted, 2)
|
|
59
|
-
SDL.spec_samples(SDL.wanted, buffer)
|
|
60
|
-
@device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, allowed_changes)
|
|
61
|
-
raise Error, SDL.SDL_GetError if @device.zero?
|
|
30
|
+
def start = @device.start
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
end
|
|
32
|
+
def pause = @device.pause
|
|
65
33
|
|
|
66
|
-
def
|
|
67
|
-
raise Error, SDL.SDL_GetError if result.negative?
|
|
34
|
+
def clear = @device.clear
|
|
68
35
|
|
|
69
|
-
|
|
70
|
-
end
|
|
36
|
+
def close = @device.close
|
|
71
37
|
end
|
|
72
38
|
end
|
|
73
39
|
end
|
|
@@ -16,13 +16,10 @@ module Badline
|
|
|
16
16
|
return 1
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
|
|
24
|
-
Datasette::Missing, Unsupported => e
|
|
25
|
-
warn "#{options.program}: #{options.media_path}: #{e.message}"
|
|
19
|
+
computer = nil
|
|
20
|
+
problem = media_problem { computer = boot(options) }
|
|
21
|
+
unless problem.empty?
|
|
22
|
+
warn "#{options.program}: #{options.media_path}: #{problem}"
|
|
26
23
|
return 1
|
|
27
24
|
end
|
|
28
25
|
timeline = Timeline.new(options)
|
|
@@ -30,6 +27,19 @@ module Badline
|
|
|
30
27
|
timeline.failed? ? 1 : 0
|
|
31
28
|
end
|
|
32
29
|
|
|
30
|
+
# Runs the block and returns the message of the error it raised over
|
|
31
|
+
# media that won't go in or start, or an empty string. Other errors,
|
|
32
|
+
# a bare ArgumentError among them, go through.
|
|
33
|
+
def self.media_problem
|
|
34
|
+
yield
|
|
35
|
+
""
|
|
36
|
+
rescue Machine::Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Datasette::Missing,
|
|
37
|
+
SystemCallError, Storage::SIDFile::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
|
|
38
|
+
Storage::CRTFile::FormatError, Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError,
|
|
39
|
+
Snapshot::FormatError => e
|
|
40
|
+
e.message
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
# The machine a .vsf snapshot holds, built as the saved one was, or a
|
|
34
44
|
# new one built from the options with the media attached.
|
|
35
45
|
def self.boot(options)
|
|
@@ -43,11 +53,10 @@ module Badline
|
|
|
43
53
|
start(options, media, options.writable?)
|
|
44
54
|
end
|
|
45
55
|
|
|
46
|
-
# A new machine built from the options, with
|
|
47
|
-
# attached and started as the command line does,
|
|
48
|
-
# `writable`. The
|
|
49
|
-
#
|
|
50
|
-
# tune.
|
|
56
|
+
# A new machine built from the options (Machine.for_options), with
|
|
57
|
+
# `media`, if not nil, attached and started as the command line does,
|
|
58
|
+
# its disk writable if `writable`. The VIC-20 raises
|
|
59
|
+
# Machine::Unsupported for a .sid tune.
|
|
51
60
|
def self.start(options, media, writable)
|
|
52
61
|
computer = build(options, media)
|
|
53
62
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
@@ -58,62 +67,10 @@ module Badline
|
|
|
58
67
|
computer
|
|
59
68
|
end
|
|
60
69
|
|
|
70
|
+
# The machine the options name, built for `media`, which says the RAM
|
|
71
|
+
# expansion a VIC-20 gets for it.
|
|
61
72
|
def self.build(options, media)
|
|
62
|
-
|
|
63
|
-
when :vic20 then vic20(options, media)
|
|
64
|
-
when :c128 then c128(options, media)
|
|
65
|
-
else c64(options, media)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def self.c64(options, media)
|
|
70
|
-
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
71
|
-
Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
|
|
73
|
+
Machine.for_options(options, media) { |line| puts line }
|
|
72
74
|
end
|
|
73
|
-
|
|
74
|
-
# A C128 in C128 mode. With --c64, or media only C64 mode runs, a .sid
|
|
75
|
-
# tune or a program that loads at the C64's BASIC start, C= is held
|
|
76
|
-
# through the reset, so the C128 KERNAL starts C64 mode.
|
|
77
|
-
def self.c128(options, media)
|
|
78
|
-
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
79
|
-
machine = C128.new(model: options.model, sid_model:, mode: :c128)
|
|
80
|
-
machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
|
|
81
|
-
machine
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def self.c64_media?(media)
|
|
85
|
-
return false if media.nil? || !File.file?(media)
|
|
86
|
-
|
|
87
|
-
extension = File.extname(media).downcase
|
|
88
|
-
return true if extension == ".sid"
|
|
89
|
-
return false unless %w[.prg .p00].include?(extension)
|
|
90
|
-
|
|
91
|
-
bytes = File.binread(media).bytes
|
|
92
|
-
bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
|
|
93
|
-
bytes.length >= 2 && [Media::BASIC_START, Media::BASIC_START - 1].include?(bytes[0] | (bytes[1] << 8))
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# A VIC-20 with the RAM --ram names, or else the RAM `media` needs
|
|
97
|
-
# (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
|
|
98
|
-
# media a VIC-20 doesn't take.
|
|
99
|
-
def self.vic20(options, media)
|
|
100
|
-
ram = options.ram
|
|
101
|
-
unless media.nil?
|
|
102
|
-
unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
103
|
-
raise Unsupported, "doesn't go in a VIC-20" if unsupported
|
|
104
|
-
|
|
105
|
-
ram ||= vic20_ram(media)
|
|
106
|
-
end
|
|
107
|
-
Machine.build(:vic20, model: options.model, ram:)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def self.vic20_ram(media)
|
|
111
|
-
ram = Media::Vic20Media.ram_for(media)
|
|
112
|
-
puts "RAM expansion: #{ram}"
|
|
113
|
-
ram
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Media the machine the options name doesn't take.
|
|
117
|
-
class Unsupported < ArgumentError; end
|
|
118
75
|
end
|
|
119
76
|
end
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The SID player
|
|
6
|
-
# and remembers its place, so a click finds the
|
|
7
|
-
# one under the pointer lights up, and one that's
|
|
8
|
-
# They take
|
|
9
|
-
# under the pointer, the background and, if any, a fill
|
|
10
|
-
# button.
|
|
5
|
+
# The buttons of the SID player and the pause menu: each is drawn where
|
|
6
|
+
# it goes on every frame, and remembers its place, so a click finds the
|
|
7
|
+
# action under it. The one under the pointer lights up, and one that's
|
|
8
|
+
# on is drawn reversed. They take their colours from the caller: the
|
|
9
|
+
# text, the text under the pointer, the background and, if any, a fill
|
|
10
|
+
# behind each button.
|
|
11
11
|
class Buttons
|
|
12
12
|
PAD = 2
|
|
13
13
|
HEIGHT = Painter::GLYPH + (PAD * 2)
|
|
14
14
|
|
|
15
|
-
def initialize(painter, colors
|
|
15
|
+
def initialize(painter, colors)
|
|
16
16
|
@painter = painter
|
|
17
17
|
@text = colors[0]
|
|
18
18
|
@bright = colors[1]
|
|
@@ -6,7 +6,27 @@ module Badline
|
|
|
6
6
|
# keys, the C128's own keys and, in joystick mode, the joystick
|
|
7
7
|
# directions.
|
|
8
8
|
module Keys
|
|
9
|
+
RETURN = 40
|
|
10
|
+
ESCAPE = 41
|
|
11
|
+
BACKSPACE = 42
|
|
9
12
|
TAB = 43
|
|
13
|
+
SPACE = 44
|
|
14
|
+
F8 = 65
|
|
15
|
+
F9 = 66
|
|
16
|
+
F10 = 67
|
|
17
|
+
F11 = 68
|
|
18
|
+
F12 = 69
|
|
19
|
+
HOME = 74
|
|
20
|
+
PAGE_UP = 75
|
|
21
|
+
END_KEY = 77
|
|
22
|
+
PAGE_DOWN = 78
|
|
23
|
+
RIGHT = 79
|
|
24
|
+
LEFT = 80
|
|
25
|
+
DOWN = 81
|
|
26
|
+
UP = 82
|
|
27
|
+
|
|
28
|
+
# The arrow keys, which move through the menus.
|
|
29
|
+
DIRECTIONS = { RIGHT => :right, LEFT => :left, DOWN => :down, UP => :up }.freeze
|
|
10
30
|
|
|
11
31
|
LETTERS = %i[a b c d e f g h i j k l m n o p q r s t u v w x y z].freeze
|
|
12
32
|
DIGITS = %i[1 2 3 4 5 6 7 8 9 0].freeze
|
|
@@ -36,14 +56,6 @@ module Badline
|
|
|
36
56
|
63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
|
|
37
57
|
}.freeze
|
|
38
58
|
|
|
39
|
-
ESCAPE = 41
|
|
40
|
-
|
|
41
|
-
F8 = 65
|
|
42
|
-
F9 = 66
|
|
43
|
-
F10 = 67
|
|
44
|
-
F11 = 68
|
|
45
|
-
F12 = 69
|
|
46
|
-
|
|
47
59
|
ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
|
|
48
60
|
WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
|
|
49
61
|
|
|
@@ -65,6 +77,19 @@ module Badline
|
|
|
65
77
|
C128_OTHERS[scancode] || c64_key(scancode)
|
|
66
78
|
end
|
|
67
79
|
|
|
80
|
+
# The character the key types on a US layout: a letter, a capital
|
|
81
|
+
# with `shift`, or a digit, and "" for any other key.
|
|
82
|
+
def self.character(scancode, shift)
|
|
83
|
+
if scancode.between?(4, 29)
|
|
84
|
+
letter = (97 + scancode - 4).chr
|
|
85
|
+
shift ? letter.upcase : letter
|
|
86
|
+
elsif scancode.between?(30, 39)
|
|
87
|
+
"1234567890"[scancode - 30]
|
|
88
|
+
else
|
|
89
|
+
""
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
68
93
|
def self.arrows(scancode) = ARROWS[scancode] || :none
|
|
69
94
|
|
|
70
95
|
def self.wasd(scancode) = WASD[scancode] || :none
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The machine's media slots as the pause menu and the --at events
|
|
6
|
+
# both take media out of them: device 8, a true drive there or else
|
|
7
|
+
# the KERNAL traps, and the expansion port.
|
|
8
|
+
module MediaSlots
|
|
9
|
+
class << self
|
|
10
|
+
# Whether device 8 has a disk in.
|
|
11
|
+
def disk?(computer)
|
|
12
|
+
drive = Media::TrueDrive.drive(computer)
|
|
13
|
+
drive.nil? ? computer.mounted? : !drive.disk.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The expanded path of the disk in device 8, however it went in, or
|
|
17
|
+
# an empty one.
|
|
18
|
+
def disk_path(computer)
|
|
19
|
+
drive = Media::TrueDrive.drive(computer)
|
|
20
|
+
unless drive.nil?
|
|
21
|
+
disk = drive.disk
|
|
22
|
+
return disk.nil? ? "" : disk.path.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
path = computer.mounted_path
|
|
26
|
+
path.empty? ? path : File.expand_path(path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def eject_disk(computer)
|
|
30
|
+
drive = Media::TrueDrive.drive(computer)
|
|
31
|
+
drive.nil? ? computer.unmount : drive.insert(nil)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Takes the cartridge out with the power off.
|
|
35
|
+
def remove_cartridge(computer)
|
|
36
|
+
computer.address_bus.detach_cartridge
|
|
37
|
+
computer.power_cycle!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -43,11 +43,11 @@ module Badline
|
|
|
43
43
|
|
|
44
44
|
def draw(left, top)
|
|
45
45
|
painter = @painter
|
|
46
|
-
painter.text(left, top, TITLES.fetch(@kind),
|
|
47
|
-
painter.text(left, top + 20, Painter.fit(File.basename(@path), 40),
|
|
46
|
+
painter.text(left, top, TITLES.fetch(@kind), MenuTheme::BRIGHT)
|
|
47
|
+
painter.text(left, top + 20, Painter.fit(File.basename(@path), 40), MenuTheme::TEXT) unless @path.empty?
|
|
48
48
|
warning = WARNINGS.fetch(@kind)
|
|
49
|
-
painter.text(left, top + 44, warning[0],
|
|
50
|
-
painter.text(left, top + 56, warning[1],
|
|
49
|
+
painter.text(left, top + 44, warning[0], MenuTheme::TEXT)
|
|
50
|
+
painter.text(left, top + 56, warning[1], MenuTheme::TEXT) unless warning[1].empty?
|
|
51
51
|
@buttons.row([left, top + 80, MenuPages::ROW_WIDTH], "CANCEL", :cancel)
|
|
52
52
|
@buttons.row([left, top + 80 + Buttons::HEIGHT + 2, MenuPages::ROW_WIDTH], ANSWERS.fetch(@kind), :confirm)
|
|
53
53
|
end
|
|
@@ -12,19 +12,6 @@ module Badline
|
|
|
12
12
|
ROWS = 12
|
|
13
13
|
COLUMNS = 40
|
|
14
14
|
|
|
15
|
-
RETURN = 40
|
|
16
|
-
ESCAPE = 41
|
|
17
|
-
BACKSPACE = 42
|
|
18
|
-
HOME = 74
|
|
19
|
-
PAGE_UP = 75
|
|
20
|
-
END_KEY = 77
|
|
21
|
-
PAGE_DOWN = 78
|
|
22
|
-
RIGHT = 79
|
|
23
|
-
LEFT = 80
|
|
24
|
-
DOWN = 81
|
|
25
|
-
UP = 82
|
|
26
|
-
DIGITS = "1234567890"
|
|
27
|
-
|
|
28
15
|
attr_reader :directory
|
|
29
16
|
|
|
30
17
|
def initialize(painter, buttons)
|
|
@@ -60,9 +47,9 @@ module Badline
|
|
|
60
47
|
# Handles a key, and returns the path of a file picked, :cancel when
|
|
61
48
|
# Esc leaves the browser, or nil.
|
|
62
49
|
def key(scancode)
|
|
63
|
-
return :cancel if scancode == ESCAPE
|
|
64
|
-
return pick if [RETURN, RIGHT].include?(scancode)
|
|
65
|
-
return up if [LEFT, BACKSPACE].include?(scancode)
|
|
50
|
+
return :cancel if scancode == Keys::ESCAPE
|
|
51
|
+
return pick if [Keys::RETURN, Keys::RIGHT].include?(scancode)
|
|
52
|
+
return up if [Keys::LEFT, Keys::BACKSPACE].include?(scancode)
|
|
66
53
|
|
|
67
54
|
select(@selected + step(scancode)) unless step(scancode).zero?
|
|
68
55
|
jump(scancode)
|
|
@@ -83,14 +70,14 @@ module Badline
|
|
|
83
70
|
end
|
|
84
71
|
|
|
85
72
|
def draw(left, top, width, title)
|
|
86
|
-
@painter.text(left, top, title,
|
|
87
|
-
@painter.text(left, top + ROW + 2, shown_directory,
|
|
73
|
+
@painter.text(left, top, title, MenuTheme::BRIGHT)
|
|
74
|
+
@painter.text(left, top + ROW + 2, shown_directory, MenuTheme::DIM)
|
|
88
75
|
@list_top = top + (ROW * 2) + 8
|
|
89
76
|
@buttons.area([left, @list_top, width, ROW * ROWS], :browse)
|
|
90
77
|
draw_rows(left, width)
|
|
91
78
|
draw_bar(left + width - 2) if @names.size > ROWS
|
|
92
79
|
@painter.text(left, @list_top + (ROW * ROWS) + 6, "RETURN: OPEN LEFT: BACK ESC: CANCEL",
|
|
93
|
-
|
|
80
|
+
MenuTheme::DIM)
|
|
94
81
|
end
|
|
95
82
|
|
|
96
83
|
private
|
|
@@ -109,10 +96,10 @@ module Badline
|
|
|
109
96
|
name = label(index)
|
|
110
97
|
top = @list_top + (row * ROW)
|
|
111
98
|
if index == @selected
|
|
112
|
-
@painter.box(left - 2, top - 2, width - 4, ROW,
|
|
113
|
-
@painter.text(left, top, name,
|
|
99
|
+
@painter.box(left - 2, top - 2, width - 4, ROW, MenuTheme::TEXT)
|
|
100
|
+
@painter.text(left, top, name, MenuTheme::PANEL)
|
|
114
101
|
else
|
|
115
|
-
@painter.text(left, top, name, index < @folders ?
|
|
102
|
+
@painter.text(left, top, name, index < @folders ? MenuTheme::BRIGHT : MenuTheme::TEXT)
|
|
116
103
|
end
|
|
117
104
|
end
|
|
118
105
|
end
|
|
@@ -121,8 +108,8 @@ module Badline
|
|
|
121
108
|
height = ROW * ROWS
|
|
122
109
|
size = [(height * ROWS) / @names.size, 4].max
|
|
123
110
|
offset = ((height - size) * @top) / [@names.size - ROWS, 1].max
|
|
124
|
-
@painter.box(left, @list_top - 2, 2, height,
|
|
125
|
-
@painter.box(left, @list_top - 2 + offset, 2, size,
|
|
111
|
+
@painter.box(left, @list_top - 2, 2, height, MenuTheme::EDGE)
|
|
112
|
+
@painter.box(left, @list_top - 2 + offset, 2, size, MenuTheme::TEXT)
|
|
126
113
|
end
|
|
127
114
|
|
|
128
115
|
def label(index)
|
|
@@ -133,12 +120,12 @@ module Badline
|
|
|
133
120
|
|
|
134
121
|
def step(scancode)
|
|
135
122
|
case scancode
|
|
136
|
-
when UP then -1
|
|
137
|
-
when DOWN then 1
|
|
138
|
-
when PAGE_UP then -ROWS
|
|
139
|
-
when PAGE_DOWN then ROWS
|
|
140
|
-
when HOME then -@names.size
|
|
141
|
-
when END_KEY then @names.size
|
|
123
|
+
when Keys::UP then -1
|
|
124
|
+
when Keys::DOWN then 1
|
|
125
|
+
when Keys::PAGE_UP then -ROWS
|
|
126
|
+
when Keys::PAGE_DOWN then ROWS
|
|
127
|
+
when Keys::HOME then -@names.size
|
|
128
|
+
when Keys::END_KEY then @names.size
|
|
142
129
|
else 0
|
|
143
130
|
end
|
|
144
131
|
end
|
|
@@ -154,10 +141,7 @@ module Badline
|
|
|
154
141
|
# Selects the next name after the one selected that starts with the
|
|
155
142
|
# key's letter or digit.
|
|
156
143
|
def jump(scancode)
|
|
157
|
-
char =
|
|
158
|
-
elsif scancode.between?(30, 39) then DIGITS[scancode - 30]
|
|
159
|
-
else ""
|
|
160
|
-
end
|
|
144
|
+
char = Keys.character(scancode, false)
|
|
161
145
|
return if char.empty? || @names.empty?
|
|
162
146
|
|
|
163
147
|
@names.size.times do |offset|
|
|
@@ -14,10 +14,6 @@ module Badline
|
|
|
14
14
|
TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
|
|
15
15
|
program: "QUICK OPEN", snapshot: "LOAD A SAVE" }.freeze
|
|
16
16
|
|
|
17
|
-
ESCAPE = 41
|
|
18
|
-
RETURN = 40
|
|
19
|
-
SPACE = 44
|
|
20
|
-
|
|
21
17
|
# The machine Quick open started or a save loaded.
|
|
22
18
|
attr_reader :computer
|
|
23
19
|
|
|
@@ -78,10 +74,10 @@ module Badline
|
|
|
78
74
|
if @name_field.open? && !@confirmation.open?
|
|
79
75
|
return named(@name_field.key(scancode, SDL.event_mod(SDL.event).anybits?(SDL::KMOD_SHIFT)))
|
|
80
76
|
end
|
|
81
|
-
return answer(:cancel) if scancode == ESCAPE
|
|
82
|
-
return answer(@buttons.focus) if [RETURN, SPACE].include?(scancode)
|
|
77
|
+
return answer(:cancel) if scancode == Keys::ESCAPE
|
|
78
|
+
return answer(@buttons.focus) if [Keys::RETURN, Keys::SPACE].include?(scancode)
|
|
83
79
|
|
|
84
|
-
direction =
|
|
80
|
+
direction = Keys::DIRECTIONS[scancode]
|
|
85
81
|
@buttons.shift(direction == :up ? -1 : 1, 1) if %i[up down].include?(direction)
|
|
86
82
|
nil
|
|
87
83
|
end
|
|
@@ -25,40 +25,22 @@ module Badline
|
|
|
25
25
|
|
|
26
26
|
def writable? = @writable
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
# an empty one.
|
|
30
|
-
def disk_path
|
|
31
|
-
drive = @computer.true_drive
|
|
32
|
-
unless drive.nil?
|
|
33
|
-
disk = drive.disk
|
|
34
|
-
return disk.nil? ? "" : disk.path.to_s
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
path = @computer.mounted_path
|
|
38
|
-
path.empty? ? path : File.expand_path(path)
|
|
39
|
-
end
|
|
28
|
+
def disk_path = MediaSlots.disk_path(@computer)
|
|
40
29
|
|
|
41
|
-
def inserted?
|
|
42
|
-
drive = @computer.true_drive
|
|
43
|
-
drive.nil? ? @computer.mounted? : !drive.disk.nil?
|
|
44
|
-
end
|
|
30
|
+
def inserted? = MediaSlots.disk?(@computer)
|
|
45
31
|
|
|
46
32
|
# Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
|
|
47
33
|
# :tape, and says whether it went in. A list puts its first disk in.
|
|
48
34
|
def insert(kind, path)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
succeeds? do
|
|
36
|
+
if kind == :disk
|
|
37
|
+
disk = Media::DiskList.disk(path)
|
|
38
|
+
Media.insert_disk(@computer, disk, read_only: !@writable)
|
|
39
|
+
list(path)
|
|
40
|
+
else
|
|
41
|
+
@computer.datasette.insert(Storage::TAP.new(path))
|
|
42
|
+
end
|
|
56
43
|
end
|
|
57
|
-
true
|
|
58
|
-
rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
|
|
59
|
-
Storage::G64Image::FormatError => e
|
|
60
|
-
@problem = e.message
|
|
61
|
-
false
|
|
62
44
|
end
|
|
63
45
|
|
|
64
46
|
# Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
|
|
@@ -72,10 +54,7 @@ module Badline
|
|
|
72
54
|
end
|
|
73
55
|
end
|
|
74
56
|
|
|
75
|
-
def eject_disk
|
|
76
|
-
drive = @computer.true_drive
|
|
77
|
-
drive.nil? ? @computer.unmount : drive.insert(nil)
|
|
78
|
-
end
|
|
57
|
+
def eject_disk = MediaSlots.eject_disk(@computer)
|
|
79
58
|
|
|
80
59
|
# Sets whether disks are writable, for the disk in the drive too,
|
|
81
60
|
# which goes in again as the notch now says.
|
|
@@ -111,12 +90,7 @@ module Badline
|
|
|
111
90
|
# Swaps the cartridge for the one at `path`, which power cycles the
|
|
112
91
|
# machine, and says whether it went in.
|
|
113
92
|
def attach_cartridge(path)
|
|
114
|
-
@
|
|
115
|
-
Media.attach(@computer, path)
|
|
116
|
-
true
|
|
117
|
-
rescue ArgumentError, SystemCallError, Storage::CRTFile::FormatError, Cartridge::UnsupportedTypeError => e
|
|
118
|
-
@problem = e.message
|
|
119
|
-
false
|
|
93
|
+
succeeds? { Media.attach(@computer, path) }
|
|
120
94
|
end
|
|
121
95
|
|
|
122
96
|
# What a save of the machine is named after: the cartridge, the disk,
|
|
@@ -147,27 +121,28 @@ module Badline
|
|
|
147
121
|
end
|
|
148
122
|
|
|
149
123
|
# Takes the cartridge out with the power off.
|
|
150
|
-
def remove_cartridge
|
|
151
|
-
@computer.address_bus.detach_cartridge
|
|
152
|
-
@computer.power_cycle!
|
|
153
|
-
end
|
|
124
|
+
def remove_cartridge = MediaSlots.remove_cartridge(@computer)
|
|
154
125
|
|
|
155
126
|
# A new machine started on `path`, as the command line would, or nil
|
|
156
127
|
# when it won't start.
|
|
157
128
|
def start(options, path)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError => e
|
|
165
|
-
@problem = e.message
|
|
166
|
-
nil
|
|
129
|
+
computer = nil
|
|
130
|
+
started = succeeds? do
|
|
131
|
+
computer = Frontend.start(options, path, @writable)
|
|
132
|
+
list(path)
|
|
133
|
+
end
|
|
134
|
+
started ? computer : nil
|
|
167
135
|
end
|
|
168
136
|
|
|
169
137
|
private
|
|
170
138
|
|
|
139
|
+
# Runs the block, keeps the media error it raised as the problem, and
|
|
140
|
+
# says whether there was none.
|
|
141
|
+
def succeeds?(&)
|
|
142
|
+
@problem = Frontend.media_problem(&)
|
|
143
|
+
@problem.empty?
|
|
144
|
+
end
|
|
145
|
+
|
|
171
146
|
# Keeps `path` as the list the set comes from, if it's a list.
|
|
172
147
|
def list(path)
|
|
173
148
|
return unless Media::DiskList.list?(path)
|