badline 0.8.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 +58 -0
- data/CLAUDE.md +6 -4
- data/README.md +60 -19
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +199 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +79 -33
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +13 -92
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +52 -12
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +50 -33
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +51 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +63 -10
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- 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 +63 -64
- data/lib/badline/frontend/boot.rb +34 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- 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_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +70 -16
- data/lib/badline/frontend/menu_pages.rb +47 -42
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +52 -37
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +4 -4
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +17 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +3 -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 +24 -25
- data/lib/badline/kernal_trap/routine.rb +17 -5
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +38 -24
- 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/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +13 -3
- 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 +39 -20
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +10 -6
- data/lib/badline/options/help.rb +25 -13
- data/lib/badline/options/table.rb +15 -1
- data/lib/badline/options/validation.rb +63 -3
- data/lib/badline/options.rb +37 -34
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +37 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +15 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- 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 +10 -4
- data/lib/badline/storage/tap.rb +44 -8
- data/lib/badline/storage/unavailable.rb +3 -1
- data/lib/badline/storage.rb +5 -5
- 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/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- 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
- data/lib/badline.rb +2 -38
- metadata +37 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
|
@@ -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
|
|
@@ -3,43 +3,57 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# The pause menu's screens that take over its panel: the file browser
|
|
6
|
-
# INSERT
|
|
7
|
-
# or Quick open power cycles the machine
|
|
8
|
-
# straight in.
|
|
6
|
+
# INSERT, Quick open and LOAD bring up, the question before a cartridge
|
|
7
|
+
# or Quick open power cycles the machine, and the prompt for a save's
|
|
8
|
+
# name. A disk or tape picked goes straight in, and a save loads.
|
|
9
9
|
class MenuDialogs
|
|
10
10
|
EXTENSIONS = {
|
|
11
|
-
disk:
|
|
12
|
-
program: %w[.d64 .d71 .d81 .g64 .t64 .tap .crt .prg .p00 .sid]
|
|
11
|
+
disk: %w[.d64 .d71 .d81 .g64 .t64 .m3u .vfl], tape: %w[.tap], cartridge: %w[.crt],
|
|
12
|
+
program: %w[.d64 .d71 .d81 .g64 .t64 .m3u .vfl .tap .crt .prg .p00 .sid], snapshot: %w[.vsf]
|
|
13
13
|
}.freeze
|
|
14
14
|
TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
|
|
15
|
-
program: "QUICK OPEN" }.freeze
|
|
15
|
+
program: "QUICK OPEN", snapshot: "LOAD A SAVE" }.freeze
|
|
16
16
|
|
|
17
17
|
ESCAPE = 41
|
|
18
18
|
RETURN = 40
|
|
19
19
|
SPACE = 44
|
|
20
20
|
|
|
21
|
-
# The machine Quick open started.
|
|
21
|
+
# The machine Quick open started or a save loaded.
|
|
22
22
|
attr_reader :computer
|
|
23
23
|
|
|
24
|
-
def initialize(painter, buttons, media, options)
|
|
24
|
+
def initialize(painter, buttons, media, options, snapshots)
|
|
25
25
|
media_path = options.media_path.to_s
|
|
26
26
|
@buttons = buttons
|
|
27
27
|
@media = media
|
|
28
28
|
@options = options
|
|
29
29
|
@browser = FileBrowser.new(painter, buttons)
|
|
30
30
|
@confirmation = Confirmation.new(painter, buttons)
|
|
31
|
+
@name_field = NameField.new(painter)
|
|
32
|
+
@snapshots = snapshots
|
|
31
33
|
@browsing = nil
|
|
32
34
|
@browse_from = media_path.empty? ? Dir.pwd : File.dirname(File.expand_path(media_path))
|
|
33
35
|
@computer = nil
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def open? = !@browsing.nil? || @confirmation.open?
|
|
38
|
+
def open? = !@browsing.nil? || @confirmation.open? || @name_field.open?
|
|
37
39
|
|
|
38
|
-
# Opens the browser on the files for `kind`: :disk, :tape, :cartridge
|
|
39
|
-
# or :
|
|
40
|
+
# Opens the browser on the files for `kind`: :disk, :tape, :cartridge,
|
|
41
|
+
# :program, or :snapshot, which starts in the saves folder.
|
|
40
42
|
def browse(kind)
|
|
41
43
|
@browsing = kind
|
|
42
|
-
@browser.open(@browse_from, EXTENSIONS.fetch(kind))
|
|
44
|
+
@browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, EXTENSIONS.fetch(kind))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Asks for a save's name, starting from `name`.
|
|
48
|
+
def ask_name(name) = @name_field.open("SAVE AS", name)
|
|
49
|
+
|
|
50
|
+
# Loads the save at `path` into a new machine, and returns :swap, or
|
|
51
|
+
# nil when it won't load.
|
|
52
|
+
def load(path)
|
|
53
|
+
return nil unless @snapshots.load(path)
|
|
54
|
+
|
|
55
|
+
@computer = @snapshots.computer
|
|
56
|
+
:swap
|
|
43
57
|
end
|
|
44
58
|
|
|
45
59
|
def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
|
|
@@ -61,6 +75,9 @@ module Badline
|
|
|
61
75
|
# answered yes, or nil.
|
|
62
76
|
def key(scancode)
|
|
63
77
|
return browsed(@browser.key(scancode)) unless @browsing.nil?
|
|
78
|
+
if @name_field.open? && !@confirmation.open?
|
|
79
|
+
return named(@name_field.key(scancode, SDL.event_mod(SDL.event).anybits?(SDL::KMOD_SHIFT)))
|
|
80
|
+
end
|
|
64
81
|
return answer(:cancel) if scancode == ESCAPE
|
|
65
82
|
return answer(@buttons.focus) if [RETURN, SPACE].include?(scancode)
|
|
66
83
|
|
|
@@ -77,8 +94,9 @@ module Badline
|
|
|
77
94
|
|
|
78
95
|
def draw(left, top, width)
|
|
79
96
|
return @browser.draw(left, top, width, TITLES.fetch(@browsing)) unless @browsing.nil?
|
|
97
|
+
return @confirmation.draw(left, top) if @confirmation.open?
|
|
80
98
|
|
|
81
|
-
@
|
|
99
|
+
@name_field.draw(left, top, width)
|
|
82
100
|
end
|
|
83
101
|
|
|
84
102
|
private
|
|
@@ -90,9 +108,10 @@ module Badline
|
|
|
90
108
|
return nil if picked.nil?
|
|
91
109
|
|
|
92
110
|
kind = @browsing
|
|
93
|
-
@browse_from = @browser.directory
|
|
111
|
+
@browse_from = @browser.directory unless kind == :snapshot
|
|
94
112
|
@browsing = nil
|
|
95
113
|
return nil if picked == :cancel
|
|
114
|
+
return load(picked) if kind == :snapshot
|
|
96
115
|
|
|
97
116
|
if %i[disk tape].include?(kind)
|
|
98
117
|
@media.insert(kind, picked)
|
|
@@ -102,6 +121,24 @@ module Badline
|
|
|
102
121
|
nil
|
|
103
122
|
end
|
|
104
123
|
|
|
124
|
+
# Saves the machine under the name typed once Return takes it, and
|
|
125
|
+
# asks first when a save has that name already.
|
|
126
|
+
def named(result)
|
|
127
|
+
name = @name_field.text.strip
|
|
128
|
+
if result == :cancel
|
|
129
|
+
@name_field.close
|
|
130
|
+
elsif result == :done && name.empty?
|
|
131
|
+
@name_field.refuse("TYPE A NAME")
|
|
132
|
+
elsif result == :done
|
|
133
|
+
@snapshots.named?(name) ? @confirmation.ask(:overwrite, name) : save_as(name)
|
|
134
|
+
end
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def save_as(name, replace: false)
|
|
139
|
+
@name_field.close if @snapshots.save_named(name, replace:)
|
|
140
|
+
end
|
|
141
|
+
|
|
105
142
|
# Does what the question asked about if `action` is :confirm, and
|
|
106
143
|
# returns :resume, or :swap for a new machine.
|
|
107
144
|
def answer(action)
|
|
@@ -112,6 +149,7 @@ module Badline
|
|
|
112
149
|
case question[0]
|
|
113
150
|
when :cartridge then @media.attach_cartridge(path) ? :resume : nil
|
|
114
151
|
when :remove then @media.remove_cartridge && :resume
|
|
152
|
+
when :overwrite then save_as(path, replace: true) && nil
|
|
115
153
|
else
|
|
116
154
|
@computer = @media.start(@options, path)
|
|
117
155
|
@computer.nil? ? nil : :swap
|
|
@@ -4,14 +4,18 @@ 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
|
-
#
|
|
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.
|
|
9
11
|
class MenuMedia
|
|
10
|
-
attr_reader :
|
|
12
|
+
attr_reader :problem
|
|
11
13
|
attr_writer :computer
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
# Takes the disk set from the command line's `media_path` when it's a
|
|
16
|
+
# list.
|
|
17
|
+
def initialize(media_path, writable)
|
|
18
|
+
@list = Media::DiskList.list?(media_path) ? media_path : ""
|
|
15
19
|
@writable = writable
|
|
16
20
|
@set_path = ""
|
|
17
21
|
@set = []
|
|
@@ -21,24 +25,40 @@ module Badline
|
|
|
21
25
|
|
|
22
26
|
def writable? = @writable
|
|
23
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
|
+
|
|
24
41
|
def inserted?
|
|
25
42
|
drive = @computer.drive1541
|
|
26
43
|
drive.nil? ? @computer.mounted? : !drive.disk.nil?
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
# Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
|
|
30
|
-
# :tape.
|
|
47
|
+
# :tape, and says whether it went in. A list puts its first disk in.
|
|
31
48
|
def insert(kind, path)
|
|
32
49
|
@problem = ""
|
|
33
50
|
if kind == :disk
|
|
34
|
-
Media.
|
|
35
|
-
@
|
|
51
|
+
disk = Media::DiskList.disk(path)
|
|
52
|
+
Media.insert_disk(@computer, disk, read_only: !@writable)
|
|
53
|
+
list(path)
|
|
36
54
|
else
|
|
37
55
|
@computer.datasette.insert(Storage::TAP.new(path))
|
|
38
56
|
end
|
|
57
|
+
true
|
|
39
58
|
rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
|
|
40
59
|
Storage::G64Image::FormatError => e
|
|
41
60
|
@problem = e.message
|
|
61
|
+
false
|
|
42
62
|
end
|
|
43
63
|
|
|
44
64
|
# Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
|
|
@@ -55,23 +75,25 @@ module Badline
|
|
|
55
75
|
def eject_disk
|
|
56
76
|
drive = @computer.drive1541
|
|
57
77
|
drive.nil? ? @computer.unmount : drive.insert(nil)
|
|
58
|
-
@disk_path = ""
|
|
59
78
|
end
|
|
60
79
|
|
|
61
80
|
# Sets whether disks are writable, for the disk in the drive too,
|
|
62
81
|
# which goes in again as the notch now says.
|
|
63
82
|
def writable(writable)
|
|
64
83
|
@writable = writable
|
|
65
|
-
|
|
84
|
+
path = disk_path
|
|
85
|
+
insert(:disk, path) if inserted? && !path.empty?
|
|
66
86
|
end
|
|
67
87
|
|
|
68
88
|
# The disks of the set the disk in the drive belongs to.
|
|
69
89
|
def disk_set
|
|
70
|
-
|
|
90
|
+
path = disk_path
|
|
91
|
+
return [] if path.empty?
|
|
71
92
|
|
|
72
|
-
unless @set_path ==
|
|
73
|
-
|
|
74
|
-
@
|
|
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
|
|
75
97
|
end
|
|
76
98
|
@set
|
|
77
99
|
end
|
|
@@ -80,7 +102,7 @@ module Badline
|
|
|
80
102
|
# drive, if there is one.
|
|
81
103
|
def step_disk(step)
|
|
82
104
|
set = disk_set
|
|
83
|
-
index = set.index(
|
|
105
|
+
index = set.index(disk_path)
|
|
84
106
|
return if index.nil? || !(index + step).between?(0, set.size - 1)
|
|
85
107
|
|
|
86
108
|
insert(:disk, set[index + step])
|
|
@@ -97,6 +119,26 @@ module Badline
|
|
|
97
119
|
false
|
|
98
120
|
end
|
|
99
121
|
|
|
122
|
+
# What a save of the machine is named after: the cartridge, the disk,
|
|
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.
|
|
125
|
+
def game_name
|
|
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
|
|
131
|
+
name = name.sub(/\s*[(\[].*\z/, "").strip
|
|
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))
|
|
140
|
+
end
|
|
141
|
+
|
|
100
142
|
# The name the cartridge in the expansion port gives itself, or an
|
|
101
143
|
# empty one.
|
|
102
144
|
def cartridge_name
|
|
@@ -115,7 +157,7 @@ module Badline
|
|
|
115
157
|
def start(options, path)
|
|
116
158
|
@problem = ""
|
|
117
159
|
computer = Frontend.start(options, path, @writable)
|
|
118
|
-
|
|
160
|
+
list(path)
|
|
119
161
|
computer
|
|
120
162
|
rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
|
|
121
163
|
Storage::T64::FormatError, Storage::TAP::FormatError, Storage::CRTFile::FormatError,
|
|
@@ -123,6 +165,18 @@ module Badline
|
|
|
123
165
|
@problem = e.message
|
|
124
166
|
nil
|
|
125
167
|
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Keeps `path` as the list the set comes from, if it's a list.
|
|
172
|
+
def list(path)
|
|
173
|
+
return unless Media::DiskList.list?(path)
|
|
174
|
+
|
|
175
|
+
@list = path
|
|
176
|
+
@set_path = ""
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def expanded(paths) = paths.map { |path| File.expand_path(path) }
|
|
126
180
|
end
|
|
127
181
|
end
|
|
128
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
|
|
@@ -14,19 +16,23 @@ module Badline
|
|
|
14
16
|
ROW_WIDTH = 200
|
|
15
17
|
|
|
16
18
|
WARNING = 0xff7a6b
|
|
19
|
+
RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
26
|
|
|
23
27
|
# The disks, tapes and cartridges the pages put in and take out.
|
|
24
28
|
attr_reader :media
|
|
25
29
|
|
|
26
|
-
def initialize(painter, buttons,
|
|
30
|
+
def initialize(painter, buttons, media_path, options, snapshots)
|
|
31
|
+
@snapshots = snapshots
|
|
32
|
+
@recent = []
|
|
27
33
|
@painter = painter
|
|
28
34
|
@buttons = buttons
|
|
29
|
-
@media = MenuMedia.new(
|
|
35
|
+
@media = MenuMedia.new(media_path, options.writable?)
|
|
30
36
|
@reu_kb = options.reu
|
|
31
37
|
@computer = nil
|
|
32
38
|
@controls = nil
|
|
@@ -54,10 +60,14 @@ module Badline
|
|
|
54
60
|
when :expansion then draw_expansion
|
|
55
61
|
when :ports then draw_ports
|
|
56
62
|
when :sound then draw_sound
|
|
63
|
+
when :snapshots then draw_snapshots
|
|
57
64
|
else draw_power
|
|
58
65
|
end
|
|
59
66
|
end
|
|
60
67
|
|
|
68
|
+
# The path of the quicksave or autosave a RECENT action stands for.
|
|
69
|
+
def recent(action) = @recent[RECENT.index(action)].to_s
|
|
70
|
+
|
|
61
71
|
def perform(action)
|
|
62
72
|
case action
|
|
63
73
|
when :eject_disk, :previous_disk, :next_disk, :protect, :unprotect then @media.drive(action)
|
|
@@ -68,7 +78,7 @@ module Badline
|
|
|
68
78
|
when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
|
|
69
79
|
when :swap then @controls.swap_ports
|
|
70
80
|
when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
|
|
71
|
-
when :sid6581, :sid8580 then @computer.
|
|
81
|
+
when :sid6581, :sid8580 then @computer.sound_source.model = action == :sid8580 ? :mos8580 : :mos6581
|
|
72
82
|
when :reset then @computer.reset!
|
|
73
83
|
when :power_cycle then @computer.power_cycle!
|
|
74
84
|
else pick_port(action)
|
|
@@ -77,6 +87,14 @@ module Badline
|
|
|
77
87
|
|
|
78
88
|
private
|
|
79
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
|
+
|
|
80
98
|
def draw_drive
|
|
81
99
|
media = @media
|
|
82
100
|
inserted = media.inserted?
|
|
@@ -107,6 +125,8 @@ module Badline
|
|
|
107
125
|
end
|
|
108
126
|
|
|
109
127
|
def draw_expansion
|
|
128
|
+
return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
|
|
129
|
+
|
|
110
130
|
cartridge = @computer.address_bus.cartridge
|
|
111
131
|
info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
|
|
112
132
|
row("INSERT...", :insert_cartridge)
|
|
@@ -118,27 +138,32 @@ module Badline
|
|
|
118
138
|
info("RAM EXPANSION", "REU, #{@reu_kb} KB")
|
|
119
139
|
end
|
|
120
140
|
|
|
121
|
-
|
|
122
|
-
toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
|
|
123
|
-
toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
|
|
124
|
-
joystick = @controls.joystick_mode?
|
|
125
|
-
toggle("KEYS", :keys, [%w[C64 JOYSTICK], %i[keys_c64 keys_joystick], joystick ? 1 : 0])
|
|
126
|
-
return unless joystick
|
|
127
|
-
|
|
128
|
-
row("SWAP JOYSTICKS", :swap)
|
|
129
|
-
skip
|
|
130
|
-
arrows = @controls.arrows_port
|
|
131
|
-
info("ARROWS, SPACE", "PORT #{arrows}")
|
|
132
|
-
info("WASD, LEFT SHIFT", "PORT #{3 - arrows}")
|
|
133
|
-
end
|
|
134
|
-
|
|
141
|
+
# The SID's model is a setting, and other sound chips have none.
|
|
135
142
|
def draw_sound
|
|
136
143
|
if @sound.on?
|
|
137
144
|
toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
|
|
138
145
|
else
|
|
139
146
|
info("OUTPUT", "OFF")
|
|
140
147
|
end
|
|
141
|
-
|
|
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])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Saving now and by name, loading a named save, and the quicksaves
|
|
155
|
+
# and autosaves, newest first, which a press loads.
|
|
156
|
+
def draw_snapshots
|
|
157
|
+
row("QUICKSAVE", :quicksave_now)
|
|
158
|
+
row("SAVE...", :save_as)
|
|
159
|
+
row("LOAD...", :load_save)
|
|
160
|
+
skip
|
|
161
|
+
saves = (@snapshots.list("quicksaves") + @snapshots.list("autosaves")).sort_by { |path| -File.mtime(path).to_f }
|
|
162
|
+
@recent = saves.first(RECENT.size)
|
|
163
|
+
@recent.each_with_index do |path, index|
|
|
164
|
+
kind = File.basename(path).start_with?("autosave") ? "AUTOSAVE" : "QUICKSAVE"
|
|
165
|
+
row(kind, RECENT[index], @snapshots.time_of(path))
|
|
166
|
+
end
|
|
142
167
|
end
|
|
143
168
|
|
|
144
169
|
def draw_power
|
|
@@ -173,26 +198,6 @@ module Badline
|
|
|
173
198
|
def skip
|
|
174
199
|
@top += LINE
|
|
175
200
|
end
|
|
176
|
-
|
|
177
|
-
def keys(mode) = @controls.joystick_mode = mode == :joystick
|
|
178
|
-
|
|
179
|
-
def port_device(port)
|
|
180
|
-
index = POTS[port - 1].index(@controls.pot)
|
|
181
|
-
index.nil? ? :joystick : PORT_DEVICES[index]
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Plugs the device a choice such as :port1_mouse names into its port.
|
|
185
|
-
def pick_port(action)
|
|
186
|
-
index = PORT_ACTIONS[0].include?(action) ? 0 : 1
|
|
187
|
-
choice = PORT_ACTIONS[index].index(action)
|
|
188
|
-
plug_device(index + 1, choice) unless choice.nil? || choice == PORT_DEVICES.index(port_device(index + 1))
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Plugs PORT_DEVICES[choice] into the port. A joystick there takes out
|
|
192
|
-
# the pot device.
|
|
193
|
-
def plug_device(port, choice)
|
|
194
|
-
@controls.plug(POTS[port - 1][choice])
|
|
195
|
-
end
|
|
196
201
|
end
|
|
197
202
|
end
|
|
198
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
|