badline 0.7.0 → 0.9.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 +48 -0
- data/CLAUDE.md +3 -1
- data/README.md +104 -292
- data/doc/library.md +69 -0
- data/doc/media.md +78 -0
- data/doc/pinned-behaviour.md +53 -9
- data/doc/sid-player.md +136 -0
- data/doc/snapshots.md +102 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +4 -4
- 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/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/computer/attachments.rb +5 -5
- data/lib/badline/computer.rb +12 -10
- data/lib/badline/core.rb +48 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +24 -9
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/frontend/app.rb +41 -37
- data/lib/badline/frontend/boot.rb +16 -4
- data/lib/badline/frontend/buttons.rb +101 -7
- data/lib/badline/frontend/confirmation.rb +56 -0
- data/lib/badline/frontend/controls.rb +44 -32
- data/lib/badline/frontend/file_browser.rb +225 -0
- data/lib/badline/frontend/frame_report.rb +52 -0
- data/lib/badline/frontend/menu_dialogs.rb +160 -0
- data/lib/badline/frontend/menu_media.rb +157 -0
- data/lib/badline/frontend/menu_pages.rb +220 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/painter.rb +10 -0
- data/lib/badline/frontend/pause_menu.rb +264 -0
- data/lib/badline/frontend/player_window.rb +3 -3
- data/lib/badline/frontend/snapshots.rb +146 -18
- data/lib/badline/frontend/sound.rb +11 -6
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend.rb +8 -0
- data/lib/badline/kernal_trap/load.rb +10 -0
- data/lib/badline/kernal_trap/routine.rb +28 -0
- data/lib/badline/kernal_trap/serial.rb +19 -7
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +118 -0
- data/lib/badline/media.rb +9 -3
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +16 -12
- data/lib/badline/options/table.rb +6 -2
- data/lib/badline/options/validation.rb +9 -0
- data/lib/badline/options.rb +10 -7
- 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 +10 -0
- data/lib/badline/roms/kernal-sx64.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/fields.rb +1 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +14 -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/storage/t64.rb +7 -4
- data/lib/badline/storage/tap.rb +7 -5
- data/lib/badline/storage/unavailable.rb +1 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline.rb +2 -38
- metadata +22 -1
|
@@ -20,7 +20,8 @@ module Badline
|
|
|
20
20
|
computer = boot(options)
|
|
21
21
|
rescue Media::TrueDrive::Error, Storage::SIDFile::FormatError, Storage::T64::FormatError,
|
|
22
22
|
Storage::TAP::FormatError, Storage::CRTFile::FormatError, Storage::G64Image::FormatError,
|
|
23
|
-
Cartridge::UnsupportedTypeError, Snapshot::FormatError
|
|
23
|
+
Cartridge::UnsupportedTypeError, Snapshot::FormatError, Media::DiskList::Error,
|
|
24
|
+
Datasette::Missing => e
|
|
24
25
|
warn "#{options.program}: #{options.media_path}: #{e.message}"
|
|
25
26
|
return 1
|
|
26
27
|
end
|
|
@@ -39,12 +40,23 @@ module Badline
|
|
|
39
40
|
return computer
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
start(options, media, options.writable?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A new machine built from the options, with `media`, if not nil,
|
|
47
|
+
# attached and started as the command line does, its disk writable if
|
|
48
|
+
# `writable`. The model gives the chips, and a .sid tune or --sid the
|
|
49
|
+
# SID.
|
|
50
|
+
def self.start(options, media, writable)
|
|
51
|
+
model = Model.named(options.model)
|
|
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)
|
|
44
56
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
45
57
|
unless media.nil?
|
|
46
58
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
47
|
-
disk: { read_only:
|
|
59
|
+
disk: { read_only: !writable })
|
|
48
60
|
end
|
|
49
61
|
computer
|
|
50
62
|
end
|
|
@@ -5,27 +5,43 @@ module Badline
|
|
|
5
5
|
# The SID player's buttons: each is drawn where it goes on every frame,
|
|
6
6
|
# and remembers its place, so a click finds the action under it. The
|
|
7
7
|
# one under the pointer lights up, and one that's on is drawn reversed.
|
|
8
|
+
# They take the player's colours unless given others: the text, the text
|
|
9
|
+
# under the pointer, the background and, if any, a fill behind each
|
|
10
|
+
# button.
|
|
8
11
|
class Buttons
|
|
9
12
|
PAD = 2
|
|
10
13
|
HEIGHT = Painter::GLYPH + (PAD * 2)
|
|
11
14
|
|
|
12
|
-
def initialize(painter)
|
|
15
|
+
def initialize(painter, colors = [SIDView::TEXT, SIDView::BRIGHT, PlayerScreen::BACKGROUND])
|
|
13
16
|
@painter = painter
|
|
17
|
+
@text = colors[0]
|
|
18
|
+
@bright = colors[1]
|
|
19
|
+
@back = colors[2]
|
|
20
|
+
@fill = colors.size > 3 ? colors[3] : -1
|
|
14
21
|
@lefts = []
|
|
15
22
|
@tops = []
|
|
16
23
|
@widths = []
|
|
17
24
|
@heights = []
|
|
18
25
|
@actions = []
|
|
26
|
+
@focusable = []
|
|
27
|
+
@toggles = {}
|
|
19
28
|
@pointer_x = -1
|
|
20
29
|
@pointer_y = -1
|
|
30
|
+
@focus = nil
|
|
21
31
|
end
|
|
22
32
|
|
|
33
|
+
# The action of the button the keys have moved to, which lights up as
|
|
34
|
+
# the one under the pointer does, or nil.
|
|
35
|
+
attr_accessor :focus
|
|
36
|
+
|
|
23
37
|
def forget
|
|
24
38
|
@lefts.clear
|
|
25
39
|
@tops.clear
|
|
26
40
|
@widths.clear
|
|
27
41
|
@heights.clear
|
|
28
42
|
@actions.clear
|
|
43
|
+
@focusable.clear
|
|
44
|
+
@toggles.clear
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
def point(left, top)
|
|
@@ -38,11 +54,54 @@ module Badline
|
|
|
38
54
|
def text(left, top, label, action, on: false)
|
|
39
55
|
width = Painter.width(label) + (PAD * 2)
|
|
40
56
|
place(left, top, width, HEIGHT, action)
|
|
41
|
-
@painter.box(left, top, width, HEIGHT,
|
|
42
|
-
@painter.
|
|
57
|
+
@painter.box(left, top, width, HEIGHT, @fill) if !on && @fill >= 0
|
|
58
|
+
@painter.box(left, top, width, HEIGHT, action == @focus ? @bright : @text) if on
|
|
59
|
+
@painter.text(left + PAD, top + PAD, label, on ? @back : color(@actions.size - 1))
|
|
43
60
|
width
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
# Draws a button as #text does, without the fill.
|
|
64
|
+
def plain(left, top, label, action, on: false)
|
|
65
|
+
fill = @fill
|
|
66
|
+
@fill = -1
|
|
67
|
+
width = text(left, top, label, action, on:)
|
|
68
|
+
@fill = fill
|
|
69
|
+
width
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Draws a button as wide as `box`, its left and top and width, with
|
|
73
|
+
# `label` at its left and `value`, if any, at its right, as a menu's
|
|
74
|
+
# row.
|
|
75
|
+
def row(box, label, action, value = "")
|
|
76
|
+
left = box[0]
|
|
77
|
+
top = box[1]
|
|
78
|
+
width = box[2]
|
|
79
|
+
place(left, top, width, HEIGHT, action)
|
|
80
|
+
@painter.box(left, top, width, HEIGHT, @fill) if @fill >= 0
|
|
81
|
+
color = color(@actions.size - 1)
|
|
82
|
+
@painter.text(left + PAD, top + PAD, label, color)
|
|
83
|
+
@painter.text(left + width - PAD - Painter.width(value), top + PAD, value, color) unless value.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Draws a row as #row does, with its choices at its right, `choices`
|
|
87
|
+
# holding their labels, their actions and the index of the one on.
|
|
88
|
+
# A click picks a choice, and pressing the row, `action`, picks the
|
|
89
|
+
# next, which #resolve gives.
|
|
90
|
+
def toggle(box, label, action, choices)
|
|
91
|
+
row(box, label, action)
|
|
92
|
+
labels = choices[0]
|
|
93
|
+
@toggles[action] = choices[1][(choices[2] + 1) % labels.size]
|
|
94
|
+
right = box[0] + box[2] - PAD
|
|
95
|
+
index = labels.size - 1
|
|
96
|
+
while index >= 0
|
|
97
|
+
width = Painter.width(labels[index]) + (PAD * 2)
|
|
98
|
+
right -= width
|
|
99
|
+
choice(right, box[1], labels[index], choices[1][index], index == choices[2])
|
|
100
|
+
right -= 2
|
|
101
|
+
index -= 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
46
105
|
# Draws an icon button `scale` times the size of a glyph, and returns
|
|
47
106
|
# its width.
|
|
48
107
|
def icon(left, top, name, action, scale: 1)
|
|
@@ -58,17 +117,42 @@ module Badline
|
|
|
58
117
|
place(box[0], box[1], box[2], box[3], action)
|
|
59
118
|
end
|
|
60
119
|
|
|
61
|
-
# The action of the button at `left`, `top`, or nil
|
|
120
|
+
# The action of the button at `left`, `top`, or nil: a toggle's
|
|
121
|
+
# choice before the row it sits on.
|
|
62
122
|
def action_at(left, top)
|
|
123
|
+
found = nil
|
|
63
124
|
index = 0
|
|
64
125
|
while index < @actions.size
|
|
65
|
-
|
|
126
|
+
if inside?(index, left, top)
|
|
127
|
+
return @actions[index] unless @focusable[index]
|
|
66
128
|
|
|
129
|
+
found ||= @actions[index]
|
|
130
|
+
end
|
|
67
131
|
index += 1
|
|
68
132
|
end
|
|
69
|
-
|
|
133
|
+
found
|
|
70
134
|
end
|
|
71
135
|
|
|
136
|
+
# Moves the focus `delta` buttons on, among those drawn from the
|
|
137
|
+
# `first` on, stopping at either end.
|
|
138
|
+
def shift(delta, first)
|
|
139
|
+
return if @actions.size <= first
|
|
140
|
+
|
|
141
|
+
focusable = []
|
|
142
|
+
(first...@actions.size).each { |index| focusable << @actions[index] if @focusable[index] }
|
|
143
|
+
return if focusable.empty?
|
|
144
|
+
|
|
145
|
+
from = focusable.index(@focus)
|
|
146
|
+
@focus = focusable[from.nil? ? 0 : (from + delta).clamp(0, focusable.size - 1)]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The action pressing a button stands for: for a toggle's row, its
|
|
150
|
+
# next choice's.
|
|
151
|
+
def resolve(action) = @toggles.fetch(action, action)
|
|
152
|
+
|
|
153
|
+
# The action of the button drawn `index`th, or nil.
|
|
154
|
+
def action(index) = @actions[index]
|
|
155
|
+
|
|
72
156
|
private
|
|
73
157
|
|
|
74
158
|
def place(left, top, width, height, action)
|
|
@@ -77,9 +161,19 @@ module Badline
|
|
|
77
161
|
@widths << width
|
|
78
162
|
@heights << height
|
|
79
163
|
@actions << action
|
|
164
|
+
@focusable << true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A choice of a toggle, which a click picks but the keys pass by.
|
|
168
|
+
def choice(left, top, label, action, on)
|
|
169
|
+
width = Painter.width(label) + (PAD * 2)
|
|
170
|
+
place(left, top, width, HEIGHT, action)
|
|
171
|
+
@focusable[-1] = false
|
|
172
|
+
@painter.box(left, top, width, HEIGHT, @text) if on
|
|
173
|
+
@painter.text(left + PAD, top + PAD, label, on ? @back : color(@actions.size - 1))
|
|
80
174
|
end
|
|
81
175
|
|
|
82
|
-
def color(index) = inside?(index, @pointer_x, @pointer_y) ?
|
|
176
|
+
def color(index) = inside?(index, @pointer_x, @pointer_y) || @actions[index] == @focus ? @bright : @text
|
|
83
177
|
|
|
84
178
|
def inside?(index, left, top)
|
|
85
179
|
left >= @lefts[index] && left < @lefts[index] + @widths[index] &&
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's question before something power cycles the machine
|
|
6
|
+
# or replaces a file: a cartridge going in or out, Quick open starting a
|
|
7
|
+
# file, or a save overwriting one of the same name. CANCEL has the
|
|
8
|
+
# focus, so Return alone changes nothing.
|
|
9
|
+
class Confirmation
|
|
10
|
+
TITLES = { cartridge: "INSERT A CARTRIDGE", remove: "REMOVE THE CARTRIDGE", start: "QUICK OPEN",
|
|
11
|
+
overwrite: "SAVE AS" }.freeze
|
|
12
|
+
LOST = "ANYTHING NOT SAVED IS LOST."
|
|
13
|
+
WARNINGS = { cartridge: ["INSERTING IT POWER CYCLES THE MACHINE.", LOST],
|
|
14
|
+
remove: ["REMOVING IT POWER CYCLES THE MACHINE.", LOST],
|
|
15
|
+
start: ["THIS STARTS A NEW MACHINE.", LOST],
|
|
16
|
+
overwrite: ["OVERWRITE EXISTING SAVE?", ""] }.freeze
|
|
17
|
+
ANSWERS = { cartridge: "INSERT", remove: "REMOVE", start: "START", overwrite: "OVERWRITE" }.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(painter, buttons)
|
|
20
|
+
@painter = painter
|
|
21
|
+
@buttons = buttons
|
|
22
|
+
@kind = :start
|
|
23
|
+
@path = ""
|
|
24
|
+
@open = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def open? = @open
|
|
28
|
+
|
|
29
|
+
# Asks about `kind`, :cartridge, :remove, :start or :overwrite, for the
|
|
30
|
+
# file at `path`, or the save named so.
|
|
31
|
+
def ask(kind, path)
|
|
32
|
+
@kind = kind
|
|
33
|
+
@path = path
|
|
34
|
+
@open = true
|
|
35
|
+
@buttons.focus = :cancel
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Puts the question away, and returns its kind and path.
|
|
39
|
+
def close
|
|
40
|
+
@open = false
|
|
41
|
+
[@kind, @path]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def draw(left, top)
|
|
45
|
+
painter = @painter
|
|
46
|
+
painter.text(left, top, TITLES.fetch(@kind), PauseMenu::BRIGHT)
|
|
47
|
+
painter.text(left, top + 20, Painter.fit(File.basename(@path), 40), PauseMenu::TEXT) unless @path.empty?
|
|
48
|
+
warning = WARNINGS.fetch(@kind)
|
|
49
|
+
painter.text(left, top + 44, warning[0], PauseMenu::TEXT)
|
|
50
|
+
painter.text(left, top + 56, warning[1], PauseMenu::TEXT) unless warning[1].empty?
|
|
51
|
+
@buttons.row([left, top + 80, MenuPages::ROW_WIDTH], "CANCEL", :cancel)
|
|
52
|
+
@buttons.row([left, top + 80 + Buttons::HEIGHT + 2, MenuPages::ROW_WIDTH], ANSWERS.fetch(@kind), :confirm)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -2,53 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# Routes host input to the machine
|
|
6
|
-
# steps through and shift-Tab steps back through:
|
|
5
|
+
# Routes host input to the machine. Two settings decide where it goes:
|
|
7
6
|
#
|
|
8
|
-
# -
|
|
9
|
-
#
|
|
10
|
-
# and left shift the other. The arrows start on joystick 2, and
|
|
11
|
-
# swaps them.
|
|
12
|
-
# -
|
|
13
|
-
#
|
|
14
|
-
# paddles or moves the mouse, and the host's left and right
|
|
15
|
-
# to whichever lines the device puts them on.
|
|
7
|
+
# - The keys: every host key goes to the C64 keyboard, or, as Tab
|
|
8
|
+
# switches, the arrow cluster and space drive one joystick and WASD
|
|
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
|
|
12
|
+
# mouse or a pair of paddles, which the host mouse drives. Motion
|
|
13
|
+
# turns the paddles or moves the mouse, and the host's left and right
|
|
14
|
+
# buttons go to whichever lines the device puts them on.
|
|
16
15
|
class Controls
|
|
17
|
-
|
|
18
|
-
TAGS = ["", "
|
|
16
|
+
POTS = %i[none mouse1 mouse2 paddles1 paddles2].freeze
|
|
17
|
+
TAGS = ["", "MOUSE 1", "MOUSE 2", "PADDLE 1", "PADDLE 2"].freeze
|
|
19
18
|
|
|
20
19
|
# SDL mouse button numbers.
|
|
21
20
|
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
# The pot device plugged in, one of POTS.
|
|
23
|
+
attr_reader :pot, :arrows_port
|
|
24
24
|
|
|
25
25
|
def initialize(computer)
|
|
26
26
|
@computer = computer
|
|
27
|
-
@
|
|
27
|
+
@joystick = false
|
|
28
|
+
@pot = :none
|
|
28
29
|
@arrows_port = 2
|
|
29
30
|
@mouse = Input::Mouse1351.new
|
|
30
31
|
@paddles = Input::Paddles.new
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
# Routes the input to another machine, the
|
|
34
|
-
#
|
|
34
|
+
# Routes the input to another machine, the pot device going into its
|
|
35
|
+
# port.
|
|
35
36
|
def computer=(computer)
|
|
36
37
|
release_all
|
|
37
38
|
@computer = computer
|
|
38
39
|
attach_pot_device
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
def joystick_mode? = @
|
|
42
|
+
def joystick_mode? = @joystick
|
|
43
|
+
|
|
44
|
+
# Sends the keys to the joysticks, or with false to the keyboard.
|
|
45
|
+
def joystick_mode=(joystick)
|
|
46
|
+
@joystick = joystick
|
|
47
|
+
release_all
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def toggle_keys
|
|
51
|
+
self.joystick_mode = !@joystick
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Plugs in one of POTS, in place of the one before.
|
|
55
|
+
def plug(pot)
|
|
56
|
+
@pot = pot
|
|
57
|
+
release_all
|
|
58
|
+
attach_pot_device
|
|
59
|
+
end
|
|
42
60
|
|
|
43
61
|
# Whether the host mouse drives a pot device, and so should be held
|
|
44
62
|
# in relative mode.
|
|
45
|
-
def pot_device? =
|
|
63
|
+
def pot_device? = @pot != :none
|
|
46
64
|
|
|
47
|
-
# The title bar's tag for the
|
|
65
|
+
# The title bar's tag for the settings, empty for the keyboard alone.
|
|
48
66
|
def tag
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
TAGS[
|
|
67
|
+
tags = []
|
|
68
|
+
tags << "JOY #{@arrows_port}" if @joystick
|
|
69
|
+
tags << TAGS[POTS.index(@pot)] if pot_device?
|
|
70
|
+
tags.join(", ")
|
|
52
71
|
end
|
|
53
72
|
|
|
54
73
|
def key(scancode, down)
|
|
@@ -59,13 +78,6 @@ module Badline
|
|
|
59
78
|
end
|
|
60
79
|
end
|
|
61
80
|
|
|
62
|
-
# Steps to the next mode, or back with a negative step.
|
|
63
|
-
def cycle_mode(step)
|
|
64
|
-
@mode = MODES[(MODES.index(@mode) + step) % MODES.size]
|
|
65
|
-
release_all
|
|
66
|
-
attach_pot_device
|
|
67
|
-
end
|
|
68
|
-
|
|
69
81
|
def swap_ports
|
|
70
82
|
@arrows_port = 3 - @arrows_port
|
|
71
83
|
release_all
|
|
@@ -99,9 +111,9 @@ module Badline
|
|
|
99
111
|
end
|
|
100
112
|
end
|
|
101
113
|
|
|
102
|
-
def paddles? =
|
|
114
|
+
def paddles? = POTS.index(@pot) >= 3
|
|
103
115
|
|
|
104
|
-
# A fresh device on
|
|
116
|
+
# A fresh pot device on its port, and none on the other.
|
|
105
117
|
def attach_pot_device
|
|
106
118
|
ports = @computer.control_ports
|
|
107
119
|
ports.device1 = nil
|
|
@@ -133,7 +145,7 @@ module Badline
|
|
|
133
145
|
end
|
|
134
146
|
end
|
|
135
147
|
|
|
136
|
-
def port_one? = %i[mouse1 paddles1].include?(@
|
|
148
|
+
def port_one? = %i[mouse1 paddles1].include?(@pot)
|
|
137
149
|
|
|
138
150
|
def joystick_key(scancode, down)
|
|
139
151
|
direction = Keys.arrows(scancode)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's file browser, which lists a folder's subfolders and
|
|
6
|
+
# the files a device takes, folders first, each by name. The keys move
|
|
7
|
+
# through the list, open a folder or pick a file, go up a folder, and
|
|
8
|
+
# jump to the next name starting with a letter or digit. A click picks
|
|
9
|
+
# the row under it, and the wheel scrolls.
|
|
10
|
+
class FileBrowser
|
|
11
|
+
ROW = 12
|
|
12
|
+
ROWS = 12
|
|
13
|
+
COLUMNS = 40
|
|
14
|
+
|
|
15
|
+
RETURN = 40
|
|
16
|
+
ESCAPE = 41
|
|
17
|
+
BACKSPACE = 42
|
|
18
|
+
HOME = 74
|
|
19
|
+
PAGE_UP = 75
|
|
20
|
+
END_KEY = 77
|
|
21
|
+
PAGE_DOWN = 78
|
|
22
|
+
RIGHT = 79
|
|
23
|
+
LEFT = 80
|
|
24
|
+
DOWN = 81
|
|
25
|
+
UP = 82
|
|
26
|
+
DIGITS = "1234567890"
|
|
27
|
+
|
|
28
|
+
attr_reader :directory
|
|
29
|
+
|
|
30
|
+
def initialize(painter, buttons)
|
|
31
|
+
@painter = painter
|
|
32
|
+
@buttons = buttons
|
|
33
|
+
@open = false
|
|
34
|
+
@directory = Dir.pwd
|
|
35
|
+
@extensions = []
|
|
36
|
+
@names = []
|
|
37
|
+
@folders = 0
|
|
38
|
+
@selected = 0
|
|
39
|
+
@top = 0
|
|
40
|
+
@list_top = 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def open? = @open
|
|
44
|
+
|
|
45
|
+
# What a name sorts by: lower case, with its numbers padded, so disk 2
|
|
46
|
+
# comes before disk 10.
|
|
47
|
+
def self.order(name) = name.downcase.gsub(/\d+/) { |digits| digits.rjust(10, "0") }
|
|
48
|
+
|
|
49
|
+
# Lists `directory` for the files with one of `extensions`.
|
|
50
|
+
def open(directory, extensions)
|
|
51
|
+
@open = true
|
|
52
|
+
@extensions = extensions
|
|
53
|
+
enter(directory)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def close
|
|
57
|
+
@open = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Handles a key, and returns the path of a file picked, :cancel when
|
|
61
|
+
# Esc leaves the browser, or nil.
|
|
62
|
+
def key(scancode)
|
|
63
|
+
return :cancel if scancode == ESCAPE
|
|
64
|
+
return pick if [RETURN, RIGHT].include?(scancode)
|
|
65
|
+
return up if [LEFT, BACKSPACE].include?(scancode)
|
|
66
|
+
|
|
67
|
+
select(@selected + step(scancode)) unless step(scancode).zero?
|
|
68
|
+
jump(scancode)
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Picks the row at `top`, as Return would.
|
|
73
|
+
def click(top)
|
|
74
|
+
row = (top - @list_top) / ROW
|
|
75
|
+
return nil if row.negative? || @top + row >= @names.size
|
|
76
|
+
|
|
77
|
+
@selected = @top + row
|
|
78
|
+
pick
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def scroll(rows)
|
|
82
|
+
@top = (@top + rows).clamp(0, [@names.size - ROWS, 0].max)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def draw(left, top, width, title)
|
|
86
|
+
@painter.text(left, top, title, PauseMenu::BRIGHT)
|
|
87
|
+
@painter.text(left, top + ROW + 2, shown_directory, PauseMenu::DIM)
|
|
88
|
+
@list_top = top + (ROW * 2) + 8
|
|
89
|
+
@buttons.area([left, @list_top, width, ROW * ROWS], :browse)
|
|
90
|
+
draw_rows(left, width)
|
|
91
|
+
draw_bar(left + width - 2) if @names.size > ROWS
|
|
92
|
+
@painter.text(left, @list_top + (ROW * ROWS) + 6, "RETURN: OPEN LEFT: BACK ESC: CANCEL",
|
|
93
|
+
PauseMenu::DIM)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# The folder's path, cut at the start to fit.
|
|
99
|
+
def shown_directory
|
|
100
|
+
length = @directory.length
|
|
101
|
+
length > COLUMNS ? "...#{@directory[length - COLUMNS + 3, COLUMNS - 3]}" : @directory
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def draw_rows(left, width)
|
|
105
|
+
ROWS.times do |row|
|
|
106
|
+
index = @top + row
|
|
107
|
+
break if index >= @names.size
|
|
108
|
+
|
|
109
|
+
name = label(index)
|
|
110
|
+
top = @list_top + (row * ROW)
|
|
111
|
+
if index == @selected
|
|
112
|
+
@painter.box(left - 2, top - 2, width - 4, ROW, PauseMenu::TEXT)
|
|
113
|
+
@painter.text(left, top, name, PauseMenu::PANEL)
|
|
114
|
+
else
|
|
115
|
+
@painter.text(left, top, name, index < @folders ? PauseMenu::BRIGHT : PauseMenu::TEXT)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def draw_bar(left)
|
|
121
|
+
height = ROW * ROWS
|
|
122
|
+
size = [(height * ROWS) / @names.size, 4].max
|
|
123
|
+
offset = ((height - size) * @top) / [@names.size - ROWS, 1].max
|
|
124
|
+
@painter.box(left, @list_top - 2, 2, height, PauseMenu::EDGE)
|
|
125
|
+
@painter.box(left, @list_top - 2 + offset, 2, size, PauseMenu::TEXT)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def label(index)
|
|
129
|
+
name = @names[index]
|
|
130
|
+
name += "/" if index < @folders
|
|
131
|
+
Painter.fit(name, COLUMNS)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def step(scancode)
|
|
135
|
+
case scancode
|
|
136
|
+
when UP then -1
|
|
137
|
+
when DOWN then 1
|
|
138
|
+
when PAGE_UP then -ROWS
|
|
139
|
+
when PAGE_DOWN then ROWS
|
|
140
|
+
when HOME then -@names.size
|
|
141
|
+
when END_KEY then @names.size
|
|
142
|
+
else 0
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def select(index)
|
|
147
|
+
return if @names.empty?
|
|
148
|
+
|
|
149
|
+
@selected = index.clamp(0, @names.size - 1)
|
|
150
|
+
@top = @selected if @selected < @top
|
|
151
|
+
@top = @selected - ROWS + 1 if @selected >= @top + ROWS
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Selects the next name after the one selected that starts with the
|
|
155
|
+
# key's letter or digit.
|
|
156
|
+
def jump(scancode)
|
|
157
|
+
char = if scancode.between?(4, 29) then (97 + scancode - 4).chr
|
|
158
|
+
elsif scancode.between?(30, 39) then DIGITS[scancode - 30]
|
|
159
|
+
else ""
|
|
160
|
+
end
|
|
161
|
+
return if char.empty? || @names.empty?
|
|
162
|
+
|
|
163
|
+
@names.size.times do |offset|
|
|
164
|
+
index = (@selected + 1 + offset) % @names.size
|
|
165
|
+
next unless @names[index].downcase.start_with?(char)
|
|
166
|
+
|
|
167
|
+
select(index)
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def pick
|
|
173
|
+
return nil if @names.empty?
|
|
174
|
+
|
|
175
|
+
path = File.join(@directory, @names[@selected])
|
|
176
|
+
return path unless @selected < @folders
|
|
177
|
+
|
|
178
|
+
@names[@selected] == ".." ? up : enter(path)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def up
|
|
182
|
+
parent = File.dirname(@directory)
|
|
183
|
+
return nil if parent == @directory
|
|
184
|
+
|
|
185
|
+
from = File.basename(@directory)
|
|
186
|
+
enter(parent)
|
|
187
|
+
index = @names.index(from)
|
|
188
|
+
select(index) unless index.nil?
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def enter(directory)
|
|
193
|
+
@directory = File.expand_path(directory)
|
|
194
|
+
folders = []
|
|
195
|
+
files = []
|
|
196
|
+
children(@directory).each do |name|
|
|
197
|
+
next if name.start_with?(".")
|
|
198
|
+
|
|
199
|
+
if File.directory?(File.join(@directory, name))
|
|
200
|
+
folders << name
|
|
201
|
+
elsif @extensions.include?(File.extname(name).downcase)
|
|
202
|
+
files << name
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
listing(folders, files)
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def listing(folders, files)
|
|
210
|
+
folders = folders.sort_by { |name| FileBrowser.order(name) }
|
|
211
|
+
folders.unshift("..") unless File.dirname(@directory) == @directory
|
|
212
|
+
@folders = folders.size
|
|
213
|
+
@names = folders + files.sort_by { |name| FileBrowser.order(name) }
|
|
214
|
+
@selected = 0
|
|
215
|
+
@top = 0
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def children(directory)
|
|
219
|
+
Dir.children(directory)
|
|
220
|
+
rescue SystemCallError
|
|
221
|
+
[]
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The --verbose report every 50 frames: the frame rate, the time each
|
|
6
|
+
# stage of a frame took on average, the slowest frame's work, and how
|
|
7
|
+
# the sound's queue fared.
|
|
8
|
+
class FrameReport
|
|
9
|
+
STAGES = %w[events emulate audio blit present wait].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(sound)
|
|
12
|
+
@sound = sound
|
|
13
|
+
@reported_samples = 0
|
|
14
|
+
clear
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Adds a frame's stamps, one as each stage starts and one after the
|
|
18
|
+
# last.
|
|
19
|
+
def add(stamps)
|
|
20
|
+
STAGES.size.times { |stage| @spent[stage] += stamps[stage + 1] - stamps[stage] }
|
|
21
|
+
took = stamps[-2] - stamps.first
|
|
22
|
+
@slowest = took if took > @slowest
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Reports the 50 frames added over the last `seconds`, and starts
|
|
26
|
+
# over.
|
|
27
|
+
def show(seconds)
|
|
28
|
+
stages = STAGES.each_with_index.map { |name, stage| "#{name} #{(@spent[stage] * 20).round(2)}" }
|
|
29
|
+
puts "#{(50 / seconds).round(1)} fps, per frame ms: #{stages.join(' ')}, " \
|
|
30
|
+
"slowest work #{(@slowest * 1000).round(2)}"
|
|
31
|
+
show_sound(seconds) if @sound.on?
|
|
32
|
+
clear
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def clear
|
|
38
|
+
@spent = Array.new(STAGES.size, 0.0)
|
|
39
|
+
@slowest = 0.0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_sound(seconds)
|
|
43
|
+
sound = @sound
|
|
44
|
+
rate = (sound.queued - @reported_samples) / seconds
|
|
45
|
+
queue = sound.high.zero? ? "empty" : "#{(sound.low * 1000).round(1)}-#{(sound.high * 1000).round(1)} ms"
|
|
46
|
+
puts " sound #{rate.round} samples/s, queue #{queue}, #{sound.underruns} underruns, #{sound.dropped} dropped"
|
|
47
|
+
sound.reset_levels
|
|
48
|
+
@reported_samples = sound.queued
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|