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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's prompt for a name, typed with the keys by their
|
|
6
|
+
# place on a US keyboard: letters, with Shift for capitals, digits,
|
|
7
|
+
# space, - and _ and the full stop. Backspace deletes, Return takes the
|
|
8
|
+
# name and Esc leaves it.
|
|
9
|
+
class NameField
|
|
10
|
+
COLUMNS = 30
|
|
11
|
+
LETTERS = "abcdefghijklmnopqrstuvwxyz"
|
|
12
|
+
DIGITS = "1234567890"
|
|
13
|
+
OTHERS = { 44 => [" ", " "], 45 => ["-", "_"], 55 => [".", "."] }.freeze
|
|
14
|
+
|
|
15
|
+
RETURN = 40
|
|
16
|
+
ESCAPE = 41
|
|
17
|
+
BACKSPACE = 42
|
|
18
|
+
|
|
19
|
+
attr_reader :text
|
|
20
|
+
|
|
21
|
+
def initialize(painter)
|
|
22
|
+
@painter = painter
|
|
23
|
+
@open = false
|
|
24
|
+
@title = ""
|
|
25
|
+
@text = ""
|
|
26
|
+
@problem = ""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def open? = @open
|
|
30
|
+
|
|
31
|
+
def open(title, text)
|
|
32
|
+
@open = true
|
|
33
|
+
@title = title
|
|
34
|
+
@text = text
|
|
35
|
+
@problem = ""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def close
|
|
39
|
+
@open = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Says why the name won't do, under the field.
|
|
43
|
+
def refuse(problem)
|
|
44
|
+
@problem = problem
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Handles a key, and returns :done for Return, :cancel for Esc, or
|
|
48
|
+
# nil.
|
|
49
|
+
def key(scancode, shift)
|
|
50
|
+
return :done if scancode == RETURN
|
|
51
|
+
return :cancel if scancode == ESCAPE
|
|
52
|
+
|
|
53
|
+
@problem = ""
|
|
54
|
+
if scancode == BACKSPACE
|
|
55
|
+
@text = @text[0, @text.length - 1].to_s
|
|
56
|
+
else
|
|
57
|
+
char = character(scancode, shift)
|
|
58
|
+
@text += char unless char.empty? || @text.length >= COLUMNS
|
|
59
|
+
end
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def draw(left, top, width)
|
|
64
|
+
painter = @painter
|
|
65
|
+
painter.text(left, top, @title, PauseMenu::BRIGHT)
|
|
66
|
+
painter.box(left, top + 18, width, Buttons::HEIGHT + 4, PauseMenu::FILL)
|
|
67
|
+
painter.text(left + 4, top + 22, "#{@text}_", PauseMenu::TEXT)
|
|
68
|
+
painter.text(left, top + 42, @problem, MenuPages::WARNING) unless @problem.empty?
|
|
69
|
+
painter.text(left, top + 60, "RETURN: SAVE ESC: CANCEL", PauseMenu::DIM)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def character(scancode, shift)
|
|
75
|
+
if scancode.between?(4, 29)
|
|
76
|
+
letter = LETTERS[scancode - 4]
|
|
77
|
+
shift ? letter.upcase : letter
|
|
78
|
+
elsif scancode.between?(30, 39)
|
|
79
|
+
DIGITS[scancode - 30]
|
|
80
|
+
elsif OTHERS.key?(scancode)
|
|
81
|
+
OTHERS[scancode][shift ? 1 : 0]
|
|
82
|
+
else
|
|
83
|
+
""
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
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
|
|
|
@@ -14,9 +14,8 @@ module Badline
|
|
|
14
14
|
# a cartridge going in or out ask first, as each power cycles the
|
|
15
15
|
# machine.
|
|
16
16
|
class PauseMenu
|
|
17
|
-
SECTIONS = ["DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
|
|
18
|
-
PAGES = %i[drive datasette expansion ports sound power].freeze
|
|
19
|
-
DISKS = %w[.d64 .d71 .d81 .g64 .t64].freeze
|
|
17
|
+
SECTIONS = ["SNAPSHOTS", "DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
|
|
18
|
+
PAGES = %i[snapshots drive datasette expansion ports sound power].freeze
|
|
20
19
|
|
|
21
20
|
PANEL = 0x1d2230
|
|
22
21
|
EDGE = 0x3a4256
|
|
@@ -25,40 +24,37 @@ module Badline
|
|
|
25
24
|
DIM = 0x6b7489
|
|
26
25
|
FILL = 0x343c50
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
BODY = 154
|
|
27
|
+
MARGIN = 16
|
|
28
|
+
WIDTH = 352
|
|
29
|
+
HEIGHT = 240
|
|
30
|
+
BODY = 138
|
|
33
31
|
|
|
34
32
|
ESCAPE = 41
|
|
35
33
|
RETURN = 40
|
|
36
34
|
SPACE = 44
|
|
35
|
+
SNAPSHOT_ACTIONS = %i[quicksave_now save_as load_save].freeze
|
|
37
36
|
ARROWS = { 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
|
|
38
37
|
MOUSEWHEEL = 0x403
|
|
39
38
|
|
|
40
39
|
# Takes the media, the REU and whether disks are writable from Options.
|
|
41
|
-
def initialize(painter, options)
|
|
40
|
+
def initialize(painter, options, snapshots)
|
|
42
41
|
media_path = options.media_path.to_s
|
|
43
42
|
@painter = painter
|
|
44
43
|
@buttons = Buttons.new(painter, [TEXT, BRIGHT, PANEL, FILL])
|
|
45
|
-
@pages = MenuPages.new(painter, @buttons,
|
|
46
|
-
@
|
|
44
|
+
@pages = MenuPages.new(painter, @buttons, media_path, options, snapshots)
|
|
45
|
+
@snapshots = snapshots
|
|
46
|
+
@dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options, snapshots)
|
|
47
47
|
@open = false
|
|
48
48
|
@section = 0
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# The path of the disk the command line puts in device 8, or an empty
|
|
52
|
-
# one for other media.
|
|
53
|
-
def self.disk_path(path)
|
|
54
|
-
return "" if path.empty?
|
|
55
|
-
return path if File.directory?(path)
|
|
56
|
-
|
|
57
|
-
DISKS.include?(File.extname(path).downcase) ? path : ""
|
|
49
|
+
@left = MARGIN
|
|
50
|
+
@top = MARGIN
|
|
58
51
|
end
|
|
59
52
|
|
|
60
53
|
def open? = @open
|
|
61
54
|
|
|
55
|
+
# The index in PAGES of the page #show opens on.
|
|
56
|
+
attr_writer :section
|
|
57
|
+
|
|
62
58
|
# The machine Quick open started, which the app runs in place of the
|
|
63
59
|
# one before once #frame returns :swap.
|
|
64
60
|
def computer = @dialogs.computer
|
|
@@ -113,10 +109,10 @@ module Badline
|
|
|
113
109
|
end
|
|
114
110
|
|
|
115
111
|
# Handles the events that came in, draws the menu over the frozen
|
|
116
|
-
# picture, dimmed,
|
|
117
|
-
# any, leaving the events after one for the
|
|
118
|
-
# still reach the machine, so no key stays held
|
|
119
|
-
# still.
|
|
112
|
+
# picture, dimmed, for the app to present, and returns the action the
|
|
113
|
+
# app has to take on, if any, leaving the events after one for the
|
|
114
|
+
# machine. Key releases still reach the machine, so no key stays held
|
|
115
|
+
# down while it stands still.
|
|
120
116
|
def frame(renderer, texture, controls)
|
|
121
117
|
action = nil
|
|
122
118
|
action = event(SDL.event_type(SDL.event), controls) while action.nil? && SDL.SDL_PollEvent(SDL.event) != 0
|
|
@@ -126,26 +122,30 @@ module Badline
|
|
|
126
122
|
SDL.SDL_SetTextureColorMod(texture, 255, 255, 255)
|
|
127
123
|
draw
|
|
128
124
|
SDL.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)
|
|
129
|
-
SDL.SDL_RenderPresent(renderer)
|
|
130
|
-
SDL.SDL_Delay(10)
|
|
131
125
|
action
|
|
132
126
|
end
|
|
133
127
|
|
|
128
|
+
# Centres the menu on a screen `width` by `height`.
|
|
129
|
+
def center(width, height)
|
|
130
|
+
@left = (width - WIDTH) / 2
|
|
131
|
+
@top = (height - HEIGHT) / 2
|
|
132
|
+
end
|
|
133
|
+
|
|
134
134
|
def draw
|
|
135
135
|
painter = @painter
|
|
136
136
|
@buttons.forget
|
|
137
|
-
painter.box(
|
|
138
|
-
painter.box(
|
|
139
|
-
painter.text(
|
|
137
|
+
painter.box(@left, @top, WIDTH, HEIGHT, PANEL)
|
|
138
|
+
painter.box(@left, @top + 24, WIDTH, 1, EDGE)
|
|
139
|
+
painter.text(@left + 8, @top + 9, "PAUSED", BRIGHT)
|
|
140
140
|
resume = "RESUME"
|
|
141
|
-
@buttons.plain(
|
|
142
|
-
return @dialogs.draw(
|
|
141
|
+
@buttons.plain(@left + WIDTH - 8 - Painter.width(resume) - (Buttons::PAD * 2), @top + 7, resume, :resume)
|
|
142
|
+
return @dialogs.draw(@left + 8, @top + 34, WIDTH - 16) if @dialogs.open?
|
|
143
143
|
|
|
144
|
-
painter.box(BODY - 8,
|
|
144
|
+
painter.box(@left + BODY - 8, @top + 32, 1, HEIGHT - 56, EDGE)
|
|
145
145
|
draw_sections
|
|
146
|
-
@buttons.plain(
|
|
147
|
-
painter.text(
|
|
148
|
-
@pages.draw(PAGES[@section], BODY,
|
|
146
|
+
@buttons.plain(@left + 6, @top + HEIGHT - 28, "QUICK OPEN...", :quick_open)
|
|
147
|
+
painter.text(@left + 8, @top + HEIGHT - 12, "F9/ESC: RESUME", DIM)
|
|
148
|
+
@pages.draw(PAGES[@section], @left + BODY, @top + 34)
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
private
|
|
@@ -165,6 +165,8 @@ module Badline
|
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
def menu_action(action)
|
|
168
|
+
return snapshot_action(action) if SNAPSHOT_ACTIONS.include?(action) || MenuPages::RECENT.include?(action)
|
|
169
|
+
|
|
168
170
|
case action
|
|
169
171
|
when :insert_disk then @dialogs.browse(:disk)
|
|
170
172
|
when :insert_tape then @dialogs.browse(:tape)
|
|
@@ -176,6 +178,19 @@ module Badline
|
|
|
176
178
|
nil
|
|
177
179
|
end
|
|
178
180
|
|
|
181
|
+
# Saves now, asks for a name to save under, opens the saves folder,
|
|
182
|
+
# or loads one of the page's quicksaves and autosaves, returning
|
|
183
|
+
# :swap once a save has loaded.
|
|
184
|
+
def snapshot_action(action)
|
|
185
|
+
case action
|
|
186
|
+
when :quicksave_now then @snapshots.quicksave
|
|
187
|
+
when :save_as then @dialogs.ask_name(@snapshots.free_name(@pages.media.game_name))
|
|
188
|
+
when :load_save then @dialogs.browse(:snapshot)
|
|
189
|
+
else return @dialogs.load(@pages.recent(action))
|
|
190
|
+
end
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
179
194
|
# Moves the focus: down from RESUME to the sections, through them,
|
|
180
195
|
# where each opens as it's reached, on to Quick open, right into a
|
|
181
196
|
# page, up and down it, and left out of it again.
|
|
@@ -235,9 +250,9 @@ module Badline
|
|
|
235
250
|
end
|
|
236
251
|
|
|
237
252
|
def draw_sections
|
|
238
|
-
top =
|
|
253
|
+
top = @top + 32
|
|
239
254
|
SECTIONS.each_with_index do |name, index|
|
|
240
|
-
@buttons.plain(
|
|
255
|
+
@buttons.plain(@left + 6, top, name.ljust(14), PAGES[index], on: index == @section)
|
|
241
256
|
top += Buttons::HEIGHT + 2
|
|
242
257
|
end
|
|
243
258
|
end
|
|
@@ -23,7 +23,7 @@ module Badline
|
|
|
23
23
|
UP = 82
|
|
24
24
|
DOWN = 81
|
|
25
25
|
|
|
26
|
-
WARNING =
|
|
26
|
+
WARNING = VIC::PALETTE[10]
|
|
27
27
|
|
|
28
28
|
CHIPS = PlayerHeader::CHIPS
|
|
29
29
|
|
|
@@ -45,7 +45,7 @@ module Badline
|
|
|
45
45
|
@state = PlayerState.new
|
|
46
46
|
@commands = PlayerCommands.new(@state)
|
|
47
47
|
@next_draw = 0.0
|
|
48
|
-
@
|
|
48
|
+
@seek_to = 0.0
|
|
49
49
|
@dropped = []
|
|
50
50
|
@credits = []
|
|
51
51
|
@stil = []
|
|
@@ -55,7 +55,7 @@ module Badline
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# The seconds into the subtune the last click on the bar asked for.
|
|
58
|
-
|
|
58
|
+
attr_reader :seek_to
|
|
59
59
|
|
|
60
60
|
# The files dropped on the window since it last said :drop.
|
|
61
61
|
def dropped
|
|
@@ -175,7 +175,7 @@ module Badline
|
|
|
175
175
|
action = @screen.buttons.action_at(left, SDL.event_y(SDL.event))
|
|
176
176
|
return if action.nil?
|
|
177
177
|
|
|
178
|
-
@
|
|
178
|
+
@seek_to = PlayerFooter.seek(left, @state.length) if action == :seek
|
|
179
179
|
return scroll_along if action == :scroll
|
|
180
180
|
|
|
181
181
|
command(action, actions)
|
|
@@ -2,60 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The visible part of the
|
|
6
|
-
#
|
|
7
|
-
# Array of Integers to C as 64-bit
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
# The visible part of the video chip's display, the crop of the
|
|
6
|
+
# machine's timing, repacked for an SDL texture in XRGB8888 in the
|
|
7
|
+
# chip's palette. Spinel hands an Array of Integers to C as 64-bit
|
|
8
|
+
# words, as badline/ffi does on CRuby, so each word carries two
|
|
9
|
+
# neighbouring pixels, the left one in the low half.
|
|
10
|
+
#
|
|
11
|
+
# The texture is as wide as the crop and at least as high as PAL's
|
|
12
|
+
# C64 crop, 272 lines, so a C64 shows in the same window whatever its
|
|
13
|
+
# region: a crop with fewer lines sits in the middle of the texture,
|
|
14
|
+
# between black bands. The VIC-20's 284 by 284 crop fills its own.
|
|
11
15
|
class Screen
|
|
12
|
-
|
|
13
|
-
HEIGHT = Region::PAL.crop[3]
|
|
14
|
-
ROW_BYTES = WIDTH * 4
|
|
15
|
-
ROW_WORDS = WIDTH / 2
|
|
16
|
+
MIN_HEIGHT = Region::PAL.crop[3]
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
0x000000, 0xffffff, 0x924a40, 0x84c5cc,
|
|
19
|
-
0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
|
|
20
|
-
0x675200, 0xc33d00, 0xc18178, 0x606060,
|
|
21
|
-
0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
|
|
22
|
-
].freeze
|
|
18
|
+
attr_reader :pixels, :width, :height
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
attr_reader :pixels
|
|
27
|
-
|
|
28
|
-
def initialize(vic)
|
|
29
|
-
@vic = vic
|
|
30
|
-
crop = vic.region.crop
|
|
20
|
+
def initialize(video, crop)
|
|
21
|
+
@video = video
|
|
31
22
|
@col_offset = crop[0]
|
|
32
23
|
@row_offset = crop[1]
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
24
|
+
@lines = crop[3]
|
|
25
|
+
@width = crop[2]
|
|
26
|
+
@stride = video.width.to_i
|
|
27
|
+
@height = [@lines, MIN_HEIGHT].max
|
|
28
|
+
@row_words = @width / 2
|
|
29
|
+
@band = (@height - @lines) / 2
|
|
30
|
+
palette = video.palette
|
|
31
|
+
@pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
|
|
32
|
+
@pixels = Array.new(@height * @row_words, 0)
|
|
36
33
|
end
|
|
37
34
|
|
|
38
|
-
#
|
|
35
|
+
# The bytes of one row of the texture.
|
|
36
|
+
def row_bytes = @width * 4
|
|
37
|
+
|
|
38
|
+
# Repacks only the lines the chip has changed since the last frame.
|
|
39
39
|
def update
|
|
40
|
-
|
|
40
|
+
video = @video
|
|
41
|
+
dirty = video.dirty_lines
|
|
42
|
+
display = video.display
|
|
41
43
|
row = 0
|
|
42
|
-
while row < @
|
|
43
|
-
pack_row(row) if dirty[row + @row_offset]
|
|
44
|
+
while row < @lines
|
|
45
|
+
pack_row(display, row) if dirty[row + @row_offset]
|
|
44
46
|
row += 1
|
|
45
47
|
end
|
|
46
|
-
|
|
48
|
+
video.clear_dirty_lines!
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
private
|
|
50
52
|
|
|
51
|
-
def pack_row(row)
|
|
52
|
-
display = @vic.display
|
|
53
|
+
def pack_row(display, row)
|
|
53
54
|
pixels = @pixels
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
pairs = @pairs
|
|
56
|
+
from = ((row + @row_offset) * @stride) + @col_offset
|
|
57
|
+
to = (row + @band) * @row_words
|
|
58
|
+
last = to + @row_words
|
|
57
59
|
while to < last
|
|
58
|
-
pixels[to] =
|
|
60
|
+
pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
|
|
59
61
|
from += 2
|
|
60
62
|
to += 1
|
|
61
63
|
end
|
|
@@ -8,7 +8,7 @@ module Badline
|
|
|
8
8
|
# a tune on more than one gets a row of buttons above the voices that
|
|
9
9
|
# pick it, which makes the view PICKER taller.
|
|
10
10
|
class SIDView
|
|
11
|
-
COLORS =
|
|
11
|
+
COLORS = VIC::PALETTE
|
|
12
12
|
TEXT = COLORS[14]
|
|
13
13
|
BRIGHT = COLORS[1]
|
|
14
14
|
DIM = COLORS[11]
|
|
@@ -2,20 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The window's snapshots: F11 saves the machine to
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# The window's snapshots: F11 saves the machine to the next of five
|
|
6
|
+
# quicksave slots in the data folder, replacing the oldest, and F12
|
|
7
|
+
# restores the newest quicksave or named save in a new machine. A run
|
|
8
|
+
# without a frame limit also autosaves every AUTOSAVE_FRAMES frames to
|
|
9
|
+
# the next of three autosave slots, which only the pause menu loads.
|
|
10
|
+
# --save-snapshot saves one after the last frame.
|
|
9
11
|
class Snapshots
|
|
10
12
|
KEYS = [Keys::F11, Keys::F12].freeze
|
|
13
|
+
SLOTS = 5
|
|
14
|
+
AUTOSAVES = 3
|
|
15
|
+
AUTOSAVE_FRAMES = 6000
|
|
11
16
|
|
|
12
17
|
# The machine running, which F12 replaces with the restored one.
|
|
13
18
|
attr_accessor :computer
|
|
14
19
|
|
|
15
20
|
def initialize(computer, options)
|
|
16
21
|
@computer = computer
|
|
17
|
-
@last = options.snapshot? ? options.media_path.to_s : ""
|
|
18
22
|
@at_finish = options.save_snapshot
|
|
23
|
+
@autosave = options.frames.zero?
|
|
24
|
+
@autosave_at = AUTOSAVE_FRAMES
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
# Saves or restores for F11 or F12, and says whether a restored
|
|
@@ -23,38 +29,160 @@ module Badline
|
|
|
23
29
|
def key(scancode)
|
|
24
30
|
return restore if scancode == Keys::F12
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
quicksave if scancode == Keys::F11
|
|
27
33
|
false
|
|
28
34
|
end
|
|
29
35
|
|
|
30
|
-
# Saves the machine,
|
|
31
|
-
|
|
36
|
+
# Saves the machine to the first empty quicksave slot, or else to the
|
|
37
|
+
# one saved longest ago, and warns when it can't.
|
|
38
|
+
def quicksave
|
|
39
|
+
folder = Badline.data_folder("quicksaves")
|
|
40
|
+
slot = next_slot(folder, "quicksave", SLOTS)
|
|
41
|
+
save(slot_path(folder, "quicksave", slot), "quicksave #{slot}")
|
|
42
|
+
rescue SystemCallError => e
|
|
43
|
+
warn "badline: quicksave: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Saves the machine, and warns when it can't. Says whether it saved.
|
|
47
|
+
def save(path, name = "")
|
|
32
48
|
@computer.save_snapshot(path)
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
puts(name.empty? ? "Saved #{path}" : "Saved #{name} to #{path}")
|
|
50
|
+
true
|
|
35
51
|
rescue Snapshot::FormatError, SystemCallError => e
|
|
36
52
|
warn "badline: #{path}: #{e.message}"
|
|
53
|
+
false
|
|
37
54
|
end
|
|
38
55
|
|
|
39
|
-
# Restores the
|
|
56
|
+
# Restores the newest quicksave or named save into a new machine, and
|
|
40
57
|
# warns when it fails, leaving the machine running as it was.
|
|
41
58
|
def restore
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
path = newest
|
|
60
|
+
if path.empty?
|
|
61
|
+
puts "No quicksave to restore. F11: Quicksave"
|
|
44
62
|
return false
|
|
45
63
|
end
|
|
46
64
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
restore_from(path)
|
|
66
|
+
rescue SystemCallError => e
|
|
67
|
+
warn "badline: quicksave: #{e.message}"
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Autosaves once the machine has run AUTOSAVE_FRAMES frames since the
|
|
72
|
+
# last, quietly. A machine that can't be saved stops autosaving after
|
|
73
|
+
# one warning.
|
|
74
|
+
def tick(frames)
|
|
75
|
+
return unless @autosave && frames >= @autosave_at
|
|
76
|
+
|
|
77
|
+
@autosave_at = frames + AUTOSAVE_FRAMES
|
|
78
|
+
folder = Badline.data_folder("autosaves")
|
|
79
|
+
@computer.save_snapshot(slot_path(folder, "autosave", next_slot(folder, "autosave", AUTOSAVES)))
|
|
50
80
|
rescue Snapshot::FormatError, SystemCallError => e
|
|
51
|
-
|
|
81
|
+
@autosave = false
|
|
82
|
+
warn "badline: autosave: #{e.message}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The snapshots in one of the data folders, `quicksaves`, `autosaves`
|
|
86
|
+
# or `saves`, newest first.
|
|
87
|
+
def list(name)
|
|
88
|
+
folder = Badline.data_folder(name)
|
|
89
|
+
paths = Dir.children(folder).select { |child| File.extname(child).casecmp?(".vsf") }
|
|
90
|
+
paths.map { |child| File.join(folder, child) }.sort_by { |path| -modified(path) }
|
|
91
|
+
rescue SystemCallError
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether a save in the saves folder has the name.
|
|
96
|
+
def named?(name) = File.exist?(File.join(Badline.data_folder("saves"), "#{name}.vsf"))
|
|
97
|
+
|
|
98
|
+
# Saves the machine as `name` in the saves folder, and says whether it
|
|
99
|
+
# did: a name already taken is overwritten only with `replace`.
|
|
100
|
+
def save_named(name, replace: false)
|
|
101
|
+
path = File.join(Badline.data_folder("saves"), "#{name}.vsf")
|
|
102
|
+
return false if File.exist?(path) && !replace
|
|
103
|
+
|
|
104
|
+
save(path, name)
|
|
105
|
+
rescue SystemCallError => e
|
|
106
|
+
warn "badline: #{name}: #{e.message}"
|
|
52
107
|
false
|
|
53
108
|
end
|
|
54
109
|
|
|
110
|
+
# `base` and the lowest number no named save has yet.
|
|
111
|
+
def free_name(base)
|
|
112
|
+
folder = Badline.data_folder("saves")
|
|
113
|
+
number = 1
|
|
114
|
+
number += 1 while File.exist?(File.join(folder, "#{base} #{number}.vsf"))
|
|
115
|
+
"#{base} #{number}"
|
|
116
|
+
rescue SystemCallError
|
|
117
|
+
"#{base} 1"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Restores the snapshot at `path` into a new machine, and says whether
|
|
121
|
+
# it did.
|
|
122
|
+
def load(path) = restore_from(path)
|
|
123
|
+
|
|
124
|
+
# When the snapshot at `path` was saved, as HH:MM:SS.
|
|
125
|
+
def time_of(path)
|
|
126
|
+
time = File.mtime(path)
|
|
127
|
+
format("%<hour>02d:%<minute>02d:%<second>02d", hour: time.hour, minute: time.min, second: time.sec)
|
|
128
|
+
end
|
|
129
|
+
|
|
55
130
|
def finish
|
|
56
131
|
save(@at_finish) unless @at_finish.empty?
|
|
57
132
|
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def restore_from(path)
|
|
137
|
+
@computer = Snapshot.load(path) { |line| puts line }
|
|
138
|
+
puts "Restored #{name_of(path)} from #{path}"
|
|
139
|
+
true
|
|
140
|
+
rescue Snapshot::FormatError, SystemCallError => e
|
|
141
|
+
warn "badline: #{path}: #{e.message}"
|
|
142
|
+
false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def next_slot(folder, kind, count)
|
|
146
|
+
empty = (1..count).find { |slot| !File.exist?(slot_path(folder, kind, slot)) }
|
|
147
|
+
return empty unless empty.nil?
|
|
148
|
+
|
|
149
|
+
(1..count).min_by { |slot| modified(slot_path(folder, kind, slot)) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def slot_path(folder, kind, slot) = File.join(folder, "#{kind}-#{slot}.vsf")
|
|
153
|
+
|
|
154
|
+
# The .vsf in quicksaves/ or saves/ written last, or "" when there's
|
|
155
|
+
# none.
|
|
156
|
+
def newest
|
|
157
|
+
found = ""
|
|
158
|
+
found_at = 0
|
|
159
|
+
%w[quicksaves saves].each do |name|
|
|
160
|
+
folder = Badline.data_folder(name)
|
|
161
|
+
Dir.children(folder).each do |child|
|
|
162
|
+
next unless File.extname(child).casecmp?(".vsf")
|
|
163
|
+
|
|
164
|
+
path = File.join(folder, child)
|
|
165
|
+
at = modified(path)
|
|
166
|
+
next unless found.empty? || at > found_at
|
|
167
|
+
|
|
168
|
+
found = path
|
|
169
|
+
found_at = at
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
found
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def name_of(path)
|
|
176
|
+
folder = Badline.data_folder("quicksaves")
|
|
177
|
+
slot = (1..SLOTS).find { |n| slot_path(folder, "quicksave", n) == path }
|
|
178
|
+
slot.nil? ? File.basename(path, ".vsf") : "quicksave #{slot}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The modification time in nanoseconds.
|
|
182
|
+
def modified(path)
|
|
183
|
+
time = File.mtime(path)
|
|
184
|
+
(time.tv_sec * 1_000_000_000) + time.nsec
|
|
185
|
+
end
|
|
58
186
|
end
|
|
59
187
|
end
|
|
60
188
|
end
|