badline 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/CLAUDE.md +6 -4
- data/README.md +39 -4
- data/doc/pinned-behaviour.md +146 -0
- data/doc/snapshots.md +13 -0
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus.rb +9 -88
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +47 -7
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +44 -29
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +3 -0
- data/lib/badline/datasette.rb +39 -1
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +62 -64
- data/lib/badline/frontend/boot.rb +33 -8
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_media.rb +49 -24
- data/lib/badline/frontend/menu_pages.rb +23 -40
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +30 -33
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +1 -1
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/sound.rb +11 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +2 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +16 -27
- data/lib/badline/kernal_trap/routine.rb +11 -27
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +21 -19
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +32 -18
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +11 -3
- data/lib/badline/options/table.rb +11 -1
- data/lib/badline/options/validation.rb +55 -4
- data/lib/badline/options.rb +33 -33
- data/lib/badline/roms/README +27 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/state.rb +4 -3
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +3 -0
- data/lib/badline/storage/tap.rb +38 -4
- data/lib/badline/storage/unavailable.rb +2 -0
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic.rb +34 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- metadata +28 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -21,7 +21,7 @@ module Badline
|
|
|
21
21
|
rescue Media::TrueDrive::Error, Storage::SIDFile::FormatError, Storage::T64::FormatError,
|
|
22
22
|
Storage::TAP::FormatError, Storage::CRTFile::FormatError, Storage::G64Image::FormatError,
|
|
23
23
|
Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
|
|
24
|
-
Datasette::Missing => e
|
|
24
|
+
Datasette::Missing, Unsupported => e
|
|
25
25
|
warn "#{options.program}: #{options.media_path}: #{e.message}"
|
|
26
26
|
return 1
|
|
27
27
|
end
|
|
@@ -45,14 +45,11 @@ module Badline
|
|
|
45
45
|
|
|
46
46
|
# A new machine built from the options, with `media`, if not nil,
|
|
47
47
|
# attached and started as the command line does, its disk writable if
|
|
48
|
-
# `writable`. The model
|
|
49
|
-
# SID.
|
|
48
|
+
# `writable`. The family and model give the machine, and a .sid tune
|
|
49
|
+
# or --sid the SID. The VIC-20 raises Unsupported for a .sid
|
|
50
|
+
# tune.
|
|
50
51
|
def self.start(options, media, writable)
|
|
51
|
-
|
|
52
|
-
computer = Computer.new(vic_model: model.vic_model, cia_model: model.cia_model,
|
|
53
|
-
sid_model: options.sid_model || Media.sid_model(media, otherwise: model.sid_model),
|
|
54
|
-
region: model.region, reu: options.reu, kernal: model.kernal,
|
|
55
|
-
datasette: model.datasette)
|
|
52
|
+
computer = options.family == :vic20 ? vic20(options, media) : c64(options, media)
|
|
56
53
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
57
54
|
unless media.nil?
|
|
58
55
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
@@ -60,5 +57,33 @@ module Badline
|
|
|
60
57
|
end
|
|
61
58
|
computer
|
|
62
59
|
end
|
|
60
|
+
|
|
61
|
+
def self.c64(options, media)
|
|
62
|
+
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
63
|
+
Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A VIC-20 with the RAM --ram names, or else the RAM `media` needs
|
|
67
|
+
# (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
|
|
68
|
+
# media a VIC-20 doesn't take.
|
|
69
|
+
def self.vic20(options, media)
|
|
70
|
+
ram = options.ram
|
|
71
|
+
unless media.nil?
|
|
72
|
+
unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
73
|
+
raise Unsupported, "doesn't go in a VIC-20" if unsupported
|
|
74
|
+
|
|
75
|
+
ram ||= vic20_ram(media)
|
|
76
|
+
end
|
|
77
|
+
Machine.build(:vic20, model: options.model, ram:)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.vic20_ram(media)
|
|
81
|
+
ram = Media::Vic20Media.ram_for(media)
|
|
82
|
+
puts "RAM expansion: #{ram}"
|
|
83
|
+
ram
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Media the machine the options name doesn't take.
|
|
87
|
+
class Unsupported < ArgumentError; end
|
|
63
88
|
end
|
|
64
89
|
end
|
|
@@ -4,11 +4,11 @@ module Badline
|
|
|
4
4
|
module Frontend
|
|
5
5
|
# Routes host input to the machine. Two settings decide where it goes:
|
|
6
6
|
#
|
|
7
|
-
# - The keys: every host key goes to the
|
|
7
|
+
# - The keys: every host key goes to the machine's keyboard, or, as Tab
|
|
8
8
|
# switches, the arrow cluster and space drive one joystick and WASD
|
|
9
9
|
# and left shift the other. The arrows start on joystick 2, and
|
|
10
|
-
# #swap_ports swaps them.
|
|
11
|
-
# - A pot device the pause menu plugs into a control port: a 1351
|
|
10
|
+
# #swap_ports swaps them. The VIC-20's one joystick takes both.
|
|
11
|
+
# - A pot device the pause menu plugs into a C64 control port: a 1351
|
|
12
12
|
# mouse or a pair of paddles, which the host mouse drives. Motion
|
|
13
13
|
# turns the paddles or moves the mouse, and the host's left and right
|
|
14
14
|
# buttons go to whichever lines the device puts them on.
|
|
@@ -65,7 +65,7 @@ module Badline
|
|
|
65
65
|
# The title bar's tag for the settings, empty for the keyboard alone.
|
|
66
66
|
def tag
|
|
67
67
|
tags = []
|
|
68
|
-
tags << "JOY #{@arrows_port}" if @joystick
|
|
68
|
+
tags << (@computer.family == :vic20 ? "JOY" : "JOY #{@arrows_port}") if @joystick
|
|
69
69
|
tags << TAGS[POTS.index(@pot)] if pot_device?
|
|
70
70
|
tags.join(", ")
|
|
71
71
|
end
|
|
@@ -105,7 +105,7 @@ module Badline
|
|
|
105
105
|
# RESTORE isn't in the key matrix, so it goes to the machine instead.
|
|
106
106
|
def c64_key(key, down)
|
|
107
107
|
if key == :restore
|
|
108
|
-
@computer.press_restore
|
|
108
|
+
down ? @computer.press_restore : @computer.release_restore
|
|
109
109
|
else
|
|
110
110
|
down ? @computer.keyboard.press(key) : @computer.keyboard.release(key)
|
|
111
111
|
end
|
|
@@ -113,9 +113,15 @@ module Badline
|
|
|
113
113
|
|
|
114
114
|
def paddles? = POTS.index(@pot) >= 3
|
|
115
115
|
|
|
116
|
-
# A fresh pot device on its port, and none on the other.
|
|
116
|
+
# A fresh pot device on its port, and none on the other. A machine
|
|
117
|
+
# without the ports takes none.
|
|
117
118
|
def attach_pot_device
|
|
118
119
|
ports = @computer.control_ports
|
|
120
|
+
if ports.nil?
|
|
121
|
+
@pot = :none
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
119
125
|
ports.device1 = nil
|
|
120
126
|
ports.device2 = nil
|
|
121
127
|
return unless pot_device?
|
|
@@ -8,8 +8,6 @@ module Badline
|
|
|
8
8
|
class DriveLed
|
|
9
9
|
WIDTH = 12
|
|
10
10
|
HEIGHT = 4
|
|
11
|
-
LEFT = Screen::WIDTH - WIDTH - 8
|
|
12
|
-
TOP = Screen::HEIGHT - HEIGHT - 6
|
|
13
11
|
|
|
14
12
|
LIT = [0xff, 0x20, 0x20].freeze
|
|
15
13
|
DARK = [0x40, 0x00, 0x00].freeze
|
|
@@ -20,11 +18,12 @@ module Badline
|
|
|
20
18
|
drive ? new(drive) : nil
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
# Sets SDL.led_rect to where the LED goes
|
|
24
|
-
|
|
21
|
+
# Sets SDL.led_rect to where the LED goes on a screen `width` by
|
|
22
|
+
# `height`.
|
|
23
|
+
def self.place(width, height)
|
|
25
24
|
rect = SDL.led_rect
|
|
26
|
-
SDL.rect_x(rect,
|
|
27
|
-
SDL.rect_y(rect,
|
|
25
|
+
SDL.rect_x(rect, width - WIDTH - 8)
|
|
26
|
+
SDL.rect_y(rect, height - HEIGHT - 6)
|
|
28
27
|
SDL.rect_w(rect, WIDTH)
|
|
29
28
|
SDL.rect_h(rect, HEIGHT)
|
|
30
29
|
end
|
|
@@ -37,10 +36,10 @@ module Badline
|
|
|
37
36
|
|
|
38
37
|
def color = lit? ? LIT : DARK
|
|
39
38
|
|
|
40
|
-
# Fills the LED's rectangle
|
|
41
|
-
# that RenderClear clears with.
|
|
42
|
-
def draw(renderer)
|
|
43
|
-
DriveLed.place
|
|
39
|
+
# Fills the LED's rectangle on a screen `width` by `height` in its
|
|
40
|
+
# colour, then puts back the black that RenderClear clears with.
|
|
41
|
+
def draw(renderer, width, height)
|
|
42
|
+
DriveLed.place(width, height)
|
|
44
43
|
rgb = color
|
|
45
44
|
SDL.SDL_SetRenderDrawColor(renderer, rgb[0], rgb[1], rgb[2], 255)
|
|
46
45
|
SDL.SDL_RenderFillRect(renderer, SDL.led_rect)
|
|
@@ -31,10 +31,10 @@ module Badline
|
|
|
31
31
|
|
|
32
32
|
attr_reader :base_cycles, :seconds, :refresh, :trim
|
|
33
33
|
|
|
34
|
-
# One frame of the
|
|
35
|
-
def self.machine(
|
|
36
|
-
cycles =
|
|
37
|
-
new(cycles, cycles.to_f /
|
|
34
|
+
# One frame of the machine's raster, from its Timing.
|
|
35
|
+
def self.machine(timing)
|
|
36
|
+
cycles = timing.cycles_per_line * timing.lines_per_frame
|
|
37
|
+
new(cycles, cycles.to_f / timing.clock_hz, 0, timing.clock_hz)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def self.display(refresh, clock_hz)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The emulator's SDL window, its renderer and the streaming texture a
|
|
6
|
+
# Screen's pixels go into. The renderer's logical size is the screen's,
|
|
7
|
+
# with each of its pixels `pixel_width` square pixels wide, so the
|
|
8
|
+
# picture keeps the machine's aspect while the menu and the drive LED
|
|
9
|
+
# draw in square pixels over it. The window opens at SCALE times that.
|
|
10
|
+
class MachineWindow
|
|
11
|
+
SCALE = 2
|
|
12
|
+
TITLE = "Badline"
|
|
13
|
+
|
|
14
|
+
# The logical size, in square pixels.
|
|
15
|
+
attr_reader :width, :height
|
|
16
|
+
|
|
17
|
+
attr_reader :renderer, :texture
|
|
18
|
+
|
|
19
|
+
# Opens the window for `screen`, presenting in step with the display
|
|
20
|
+
# when `vsync`.
|
|
21
|
+
def initialize(screen, pixel_width, vsync:)
|
|
22
|
+
abort "SDL_Init: #{SDL.SDL_GetError}" unless SDL.SDL_Init(SDL::INIT_VIDEO | SDL::INIT_EVENTS).zero?
|
|
23
|
+
|
|
24
|
+
SDL.SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "0")
|
|
25
|
+
SDL.SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0")
|
|
26
|
+
measure(screen, pixel_width)
|
|
27
|
+
@window = SDL.SDL_CreateWindow(
|
|
28
|
+
TITLE, SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED, @width * SCALE, @height * SCALE,
|
|
29
|
+
SDL::WINDOW_RESIZABLE
|
|
30
|
+
)
|
|
31
|
+
flags = SDL::RENDERER_ACCELERATED
|
|
32
|
+
flags |= SDL::RENDERER_PRESENTVSYNC if vsync
|
|
33
|
+
@renderer = SDL.SDL_CreateRenderer(@window, -1, flags)
|
|
34
|
+
create_texture
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The refresh rate of the display the window is on, in Hz.
|
|
38
|
+
def refresh_rate
|
|
39
|
+
SDL.SDL_GetWindowDisplayMode(@window, SDL.display_mode)
|
|
40
|
+
SDL.mode_refresh(SDL.display_mode)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Fits the texture, the logical size and the window to another
|
|
44
|
+
# machine's screen, unless it measures the same.
|
|
45
|
+
def fit(screen, pixel_width)
|
|
46
|
+
before = [@width, @height, @texture_width]
|
|
47
|
+
measure(screen, pixel_width)
|
|
48
|
+
return if before == [@width, @height, @texture_width]
|
|
49
|
+
|
|
50
|
+
SDL.SDL_DestroyTexture(@texture)
|
|
51
|
+
create_texture
|
|
52
|
+
SDL.SDL_SetWindowSize(@window, @width * SCALE, @height * SCALE)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def upload(screen)
|
|
56
|
+
SDL.SDL_UpdateTexture(@texture, nil, screen.pixels, screen.row_bytes)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Titles the window, TITLE followed by `tags` in brackets.
|
|
60
|
+
def title(tags)
|
|
61
|
+
SDL.SDL_SetWindowTitle(@window, ([TITLE] + tags.map { |tag| "[#{tag}]" }).join(" "))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def close
|
|
65
|
+
SDL.SDL_DestroyTexture(@texture)
|
|
66
|
+
SDL.SDL_DestroyRenderer(@renderer)
|
|
67
|
+
SDL.SDL_DestroyWindow(@window)
|
|
68
|
+
SDL.SDL_Quit
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def measure(screen, pixel_width)
|
|
74
|
+
@texture_width = screen.width
|
|
75
|
+
@width = screen.width * pixel_width
|
|
76
|
+
@height = screen.height
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_texture
|
|
80
|
+
SDL.SDL_RenderSetLogicalSize(@renderer, @width, @height)
|
|
81
|
+
@texture = SDL.SDL_CreateTexture(
|
|
82
|
+
@renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, @texture_width, @height
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -4,17 +4,17 @@ module Badline
|
|
|
4
4
|
module Frontend
|
|
5
5
|
# What the pause menu puts in the machine and takes out: disks, tapes
|
|
6
6
|
# and cartridges, whether disks are writable, which carries over from
|
|
7
|
-
# one disk to the next, and the
|
|
8
|
-
# through: the .m3u or .vfl list it came from,
|
|
9
|
-
# the set DiskSet finds. It keeps what last went
|
|
10
|
-
# show.
|
|
7
|
+
# one disk to the next, and the set of the disk in device 8, which
|
|
8
|
+
# PREVIOUS and NEXT step through: the .m3u or .vfl list it came from,
|
|
9
|
+
# while that lists it, or the set DiskSet finds. It keeps what last went
|
|
10
|
+
# wrong for the page to show.
|
|
11
11
|
class MenuMedia
|
|
12
|
-
attr_reader :
|
|
12
|
+
attr_reader :problem
|
|
13
13
|
attr_writer :computer
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Takes the disk set from the command line's `media_path` when it's a
|
|
16
|
+
# list.
|
|
16
17
|
def initialize(media_path, writable)
|
|
17
|
-
@disk_path = PauseMenu.disk_path(media_path)
|
|
18
18
|
@list = Media::DiskList.list?(media_path) ? media_path : ""
|
|
19
19
|
@writable = writable
|
|
20
20
|
@set_path = ""
|
|
@@ -25,26 +25,40 @@ module Badline
|
|
|
25
25
|
|
|
26
26
|
def writable? = @writable
|
|
27
27
|
|
|
28
|
+
# The expanded path of the disk in device 8, however it went in, or
|
|
29
|
+
# an empty one.
|
|
30
|
+
def disk_path
|
|
31
|
+
drive = @computer.drive1541
|
|
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
|
|
40
|
+
|
|
28
41
|
def inserted?
|
|
29
42
|
drive = @computer.drive1541
|
|
30
43
|
drive.nil? ? @computer.mounted? : !drive.disk.nil?
|
|
31
44
|
end
|
|
32
45
|
|
|
33
46
|
# Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
|
|
34
|
-
# :tape. A list puts its first disk in.
|
|
47
|
+
# :tape, and says whether it went in. A list puts its first disk in.
|
|
35
48
|
def insert(kind, path)
|
|
36
49
|
@problem = ""
|
|
37
50
|
if kind == :disk
|
|
38
51
|
disk = Media::DiskList.disk(path)
|
|
39
52
|
Media.insert_disk(@computer, disk, read_only: !@writable)
|
|
40
53
|
list(path)
|
|
41
|
-
@disk_path = disk
|
|
42
54
|
else
|
|
43
55
|
@computer.datasette.insert(Storage::TAP.new(path))
|
|
44
56
|
end
|
|
57
|
+
true
|
|
45
58
|
rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
|
|
46
59
|
Storage::G64Image::FormatError => e
|
|
47
60
|
@problem = e.message
|
|
61
|
+
false
|
|
48
62
|
end
|
|
49
63
|
|
|
50
64
|
# Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
|
|
@@ -61,24 +75,25 @@ module Badline
|
|
|
61
75
|
def eject_disk
|
|
62
76
|
drive = @computer.drive1541
|
|
63
77
|
drive.nil? ? @computer.unmount : drive.insert(nil)
|
|
64
|
-
@disk_path = ""
|
|
65
78
|
end
|
|
66
79
|
|
|
67
80
|
# Sets whether disks are writable, for the disk in the drive too,
|
|
68
81
|
# which goes in again as the notch now says.
|
|
69
82
|
def writable(writable)
|
|
70
83
|
@writable = writable
|
|
71
|
-
|
|
84
|
+
path = disk_path
|
|
85
|
+
insert(:disk, path) if inserted? && !path.empty?
|
|
72
86
|
end
|
|
73
87
|
|
|
74
88
|
# The disks of the set the disk in the drive belongs to.
|
|
75
89
|
def disk_set
|
|
76
|
-
|
|
90
|
+
path = disk_path
|
|
91
|
+
return [] if path.empty?
|
|
77
92
|
|
|
78
|
-
unless @set_path ==
|
|
79
|
-
listed = @list.empty? ? [] : Media::DiskList.disks(@list)
|
|
80
|
-
@set = listed.include?(
|
|
81
|
-
@set_path =
|
|
93
|
+
unless @set_path == path
|
|
94
|
+
listed = @list.empty? ? [] : expanded(Media::DiskList.disks(@list))
|
|
95
|
+
@set = listed.include?(path) ? listed : expanded(Media::DiskSet.around(path))
|
|
96
|
+
@set_path = path
|
|
82
97
|
end
|
|
83
98
|
@set
|
|
84
99
|
end
|
|
@@ -87,7 +102,7 @@ module Badline
|
|
|
87
102
|
# drive, if there is one.
|
|
88
103
|
def step_disk(step)
|
|
89
104
|
set = disk_set
|
|
90
|
-
index = set.index(
|
|
105
|
+
index = set.index(disk_path)
|
|
91
106
|
return if index.nil? || !(index + step).between?(0, set.size - 1)
|
|
92
107
|
|
|
93
108
|
insert(:disk, set[index + step])
|
|
@@ -105,14 +120,23 @@ module Badline
|
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
# What a save of the machine is named after: the cartridge, the disk,
|
|
108
|
-
# the tape, or BASIC, without the bracketed parts of a file's name.
|
|
123
|
+
# the tape, or BASIC, without the bracketed parts of a file's name. A
|
|
124
|
+
# VIC-20's are named after its disk, or else the machine.
|
|
109
125
|
def game_name
|
|
110
|
-
|
|
111
|
-
name =
|
|
112
|
-
|
|
113
|
-
name = File.basename(
|
|
126
|
+
vic20 = @computer.family == :vic20
|
|
127
|
+
name = vic20 ? "" : cartridge_name
|
|
128
|
+
disk = disk_path
|
|
129
|
+
name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
|
|
130
|
+
name = tape_name if name.empty? && !vic20
|
|
114
131
|
name = name.sub(/\s*[(\[].*\z/, "").strip
|
|
115
|
-
name.empty?
|
|
132
|
+
return name unless name.empty?
|
|
133
|
+
|
|
134
|
+
vic20 ? "VIC-20" : "BASIC"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def tape_name
|
|
138
|
+
tape = @computer.datasette.tape
|
|
139
|
+
tape.nil? ? "" : File.basename(tape.path, File.extname(tape.path))
|
|
116
140
|
end
|
|
117
141
|
|
|
118
142
|
# The name the cartridge in the expansion port gives itself, or an
|
|
@@ -133,7 +157,6 @@ module Badline
|
|
|
133
157
|
def start(options, path)
|
|
134
158
|
@problem = ""
|
|
135
159
|
computer = Frontend.start(options, path, @writable)
|
|
136
|
-
@disk_path = PauseMenu.disk_path(path)
|
|
137
160
|
list(path)
|
|
138
161
|
computer
|
|
139
162
|
rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
|
|
@@ -152,6 +175,8 @@ module Badline
|
|
|
152
175
|
@list = path
|
|
153
176
|
@set_path = ""
|
|
154
177
|
end
|
|
178
|
+
|
|
179
|
+
def expanded(paths) = paths.map { |path| File.expand_path(path) }
|
|
155
180
|
end
|
|
156
181
|
end
|
|
157
182
|
end
|
|
@@ -7,6 +7,8 @@ module Badline
|
|
|
7
7
|
# row for each thing to do, a setting showing its value on the right,
|
|
8
8
|
# which a press steps on.
|
|
9
9
|
class MenuPages
|
|
10
|
+
include MenuPorts
|
|
11
|
+
|
|
10
12
|
TEXT = PauseMenu::TEXT
|
|
11
13
|
DIM = PauseMenu::DIM
|
|
12
14
|
LINE = 12
|
|
@@ -16,10 +18,11 @@ module Badline
|
|
|
16
18
|
WARNING = 0xff7a6b
|
|
17
19
|
RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
SID_MODELS = %i[mos6581 mos8580].freeze
|
|
22
|
+
RAM_NAMES = {
|
|
23
|
+
unexpanded: "", "3k": "3K", "8k": "8K", "16k": "16K", "24k": "24K", "32k": "32K",
|
|
24
|
+
all: "35K, ALL BLOCKS"
|
|
25
|
+
}.freeze
|
|
23
26
|
|
|
24
27
|
# The disks, tapes and cartridges the pages put in and take out.
|
|
25
28
|
attr_reader :media
|
|
@@ -75,7 +78,7 @@ module Badline
|
|
|
75
78
|
when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
|
|
76
79
|
when :swap then @controls.swap_ports
|
|
77
80
|
when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
|
|
78
|
-
when :sid6581, :sid8580 then @computer.
|
|
81
|
+
when :sid6581, :sid8580 then @computer.sound_source.model = action == :sid8580 ? :mos8580 : :mos6581
|
|
79
82
|
when :reset then @computer.reset!
|
|
80
83
|
when :power_cycle then @computer.power_cycle!
|
|
81
84
|
else pick_port(action)
|
|
@@ -84,6 +87,14 @@ module Badline
|
|
|
84
87
|
|
|
85
88
|
private
|
|
86
89
|
|
|
90
|
+
def vic20? = @computer.family == :vic20
|
|
91
|
+
|
|
92
|
+
# Says in the dim colour that the VIC-20 hasn't got what the page
|
|
93
|
+
# shows.
|
|
94
|
+
def note
|
|
95
|
+
@painter.text(@left, @top, "NOT ON THE VIC-20 YET", DIM)
|
|
96
|
+
end
|
|
97
|
+
|
|
87
98
|
def draw_drive
|
|
88
99
|
media = @media
|
|
89
100
|
inserted = media.inserted?
|
|
@@ -114,6 +125,8 @@ module Badline
|
|
|
114
125
|
end
|
|
115
126
|
|
|
116
127
|
def draw_expansion
|
|
128
|
+
return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
|
|
129
|
+
|
|
117
130
|
cartridge = @computer.address_bus.cartridge
|
|
118
131
|
info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
|
|
119
132
|
row("INSERT...", :insert_cartridge)
|
|
@@ -125,27 +138,17 @@ module Badline
|
|
|
125
138
|
info("RAM EXPANSION", "REU, #{@reu_kb} KB")
|
|
126
139
|
end
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
|
|
130
|
-
toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
|
|
131
|
-
joystick = @controls.joystick_mode?
|
|
132
|
-
toggle("KEYS", :keys, [%w[C64 JOYSTICK], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
|
|
133
|
-
return unless joystick
|
|
134
|
-
|
|
135
|
-
row("SWAP JOYSTICKS", :swap)
|
|
136
|
-
skip
|
|
137
|
-
arrows = @controls.arrows_port
|
|
138
|
-
info("ARROWS, SPACE", "PORT #{arrows}")
|
|
139
|
-
info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
|
|
140
|
-
end
|
|
141
|
-
|
|
141
|
+
# The SID's model is a setting, and other sound chips have none.
|
|
142
142
|
def draw_sound
|
|
143
143
|
if @sound.on?
|
|
144
144
|
toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
|
|
145
145
|
else
|
|
146
146
|
info("OUTPUT", "OFF")
|
|
147
147
|
end
|
|
148
|
-
|
|
148
|
+
model = @computer.sound_source.model
|
|
149
|
+
return unless SID_MODELS.include?(model)
|
|
150
|
+
|
|
151
|
+
toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
# Saving now and by name, loading a named save, and the quicksaves
|
|
@@ -195,26 +198,6 @@ module Badline
|
|
|
195
198
|
def skip
|
|
196
199
|
@top += LINE
|
|
197
200
|
end
|
|
198
|
-
|
|
199
|
-
def keys(mode) = @controls.joystick_mode = mode == :joystick
|
|
200
|
-
|
|
201
|
-
def port_device(port)
|
|
202
|
-
index = POTS[port - 1].index(@controls.pot)
|
|
203
|
-
index.nil? ? :joystick : PORT_DEVICES[index]
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Plugs the device a choice such as :port1_mouse names into its port.
|
|
207
|
-
def pick_port(action)
|
|
208
|
-
index = PORT_ACTIONS[0].include?(action) ? 0 : 1
|
|
209
|
-
choice = PORT_ACTIONS[index].index(action)
|
|
210
|
-
plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
|
|
214
|
-
# the pot device.
|
|
215
|
-
def plug_device(port, choice)
|
|
216
|
-
@controls.plug(POTS[port - 1][choice])
|
|
217
|
-
end
|
|
218
201
|
end
|
|
219
202
|
end
|
|
220
203
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's PORTS page, which MenuPages draws: what each C64
|
|
6
|
+
# control port has plugged in, a joystick or a pot device, and whether
|
|
7
|
+
# the keys type or drive the joysticks. The VIC-20 has one port, a
|
|
8
|
+
# joystick's.
|
|
9
|
+
module MenuPorts
|
|
10
|
+
PORT_DEVICES = %i[joystick mouse paddles].freeze
|
|
11
|
+
PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
|
|
12
|
+
PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
|
|
13
|
+
POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def draw_ports
|
|
18
|
+
return draw_vic20_port if @computer.family == :vic20
|
|
19
|
+
|
|
20
|
+
toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
|
|
21
|
+
toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
|
|
22
|
+
return unless draw_keys("C64")
|
|
23
|
+
|
|
24
|
+
row("SWAP JOYSTICKS", :swap)
|
|
25
|
+
skip
|
|
26
|
+
arrows = @controls.arrows_port
|
|
27
|
+
info("ARROWS, SPACE", "PORT #{arrows}")
|
|
28
|
+
info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The VIC-20's one joystick, which both sets of keys drive.
|
|
32
|
+
def draw_vic20_port
|
|
33
|
+
return unless draw_keys("VIC-20")
|
|
34
|
+
|
|
35
|
+
skip
|
|
36
|
+
info("ARROWS, SPACE", "JOYSTICK")
|
|
37
|
+
info("WASD, LEFT SHIFT", "JOYSTICK")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The toggle between the keyboard named and the joysticks, and
|
|
41
|
+
# whether the keys drive the joysticks.
|
|
42
|
+
def draw_keys(keyboard)
|
|
43
|
+
joystick = @controls.joystick_mode?
|
|
44
|
+
toggle("KEYS", :keys, [[keyboard, "JOYSTICK"], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
|
|
45
|
+
joystick
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def keys(mode) = @controls.joystick_mode = mode == :joystick
|
|
49
|
+
|
|
50
|
+
def port_device(port)
|
|
51
|
+
index = POTS[port - 1].index(@controls.pot)
|
|
52
|
+
index.nil? ? :joystick : PORT_DEVICES[index]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Plugs the device a choice such as :port1_mouse names into its port.
|
|
56
|
+
def pick_port(action)
|
|
57
|
+
index = PORT_ACTIONS[0].include?(action) ? 0 : 1
|
|
58
|
+
choice = PORT_ACTIONS[index].index(action)
|
|
59
|
+
plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
|
|
63
|
+
# the pot device.
|
|
64
|
+
def plug_device(port, choice)
|
|
65
|
+
@controls.plug(POTS[port - 1][choice])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -13,7 +13,8 @@ module Badline
|
|
|
13
13
|
# SAFETY, so no samples are dropped. #measure fits the frames to the
|
|
14
14
|
# rate the display presents at, measured over the whole run.
|
|
15
15
|
#
|
|
16
|
-
# Without vsync, a frame is one of the machine's frames,
|
|
16
|
+
# Without vsync, a frame is one of the machine's frames, as its Timing
|
|
17
|
+
# gives them, 20 ms on PAL.
|
|
17
18
|
# With the sound playing, #wait lasts until the audio queue is down to
|
|
18
19
|
# Sound::AHEAD; otherwise it waits out the rest of the frame.
|
|
19
20
|
class Pacer
|
|
@@ -23,12 +24,12 @@ module Badline
|
|
|
23
24
|
|
|
24
25
|
attr_reader :rate
|
|
25
26
|
|
|
26
|
-
def initialize(paced:, vsync:, verbose:,
|
|
27
|
+
def initialize(paced:, vsync:, verbose:, timing:)
|
|
27
28
|
@paced = paced
|
|
28
29
|
@verbose = verbose
|
|
29
30
|
@vsync = paced && vsync
|
|
30
|
-
@
|
|
31
|
-
@rate = FrameRate.machine(
|
|
31
|
+
@timing = timing
|
|
32
|
+
@rate = FrameRate.machine(timing)
|
|
32
33
|
@deadline = 0.0
|
|
33
34
|
@measured_frames = 0
|
|
34
35
|
@measured_seconds = 0.0
|
|
@@ -39,7 +40,7 @@ module Badline
|
|
|
39
40
|
# Fits the frames to the display's refresh rate, once the window has
|
|
40
41
|
# been opened with vsync.
|
|
41
42
|
def fit(refresh)
|
|
42
|
-
@rate = FrameRate.display(refresh, @
|
|
43
|
+
@rate = FrameRate.display(refresh, @timing.clock_hz)
|
|
43
44
|
puts "Display #{@rate.refresh} Hz, vsync: #{@rate.base_cycles} cycles a frame" if @verbose
|
|
44
45
|
end
|
|
45
46
|
|