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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's screens that take over its panel: the file browser
|
|
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
|
+
class MenuDialogs
|
|
10
|
+
EXTENSIONS = {
|
|
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
|
+
}.freeze
|
|
14
|
+
TITLES = { disk: "INSERT A DISK", tape: "INSERT A TAPE", cartridge: "INSERT A CARTRIDGE",
|
|
15
|
+
program: "QUICK OPEN", snapshot: "LOAD A SAVE" }.freeze
|
|
16
|
+
|
|
17
|
+
ESCAPE = 41
|
|
18
|
+
RETURN = 40
|
|
19
|
+
SPACE = 44
|
|
20
|
+
|
|
21
|
+
# The machine Quick open started or a save loaded.
|
|
22
|
+
attr_reader :computer
|
|
23
|
+
|
|
24
|
+
def initialize(painter, buttons, media, options, snapshots)
|
|
25
|
+
media_path = options.media_path.to_s
|
|
26
|
+
@buttons = buttons
|
|
27
|
+
@media = media
|
|
28
|
+
@options = options
|
|
29
|
+
@browser = FileBrowser.new(painter, buttons)
|
|
30
|
+
@confirmation = Confirmation.new(painter, buttons)
|
|
31
|
+
@name_field = NameField.new(painter)
|
|
32
|
+
@snapshots = snapshots
|
|
33
|
+
@browsing = nil
|
|
34
|
+
@browse_from = media_path.empty? ? Dir.pwd : File.dirname(File.expand_path(media_path))
|
|
35
|
+
@computer = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def open? = !@browsing.nil? || @confirmation.open? || @name_field.open?
|
|
39
|
+
|
|
40
|
+
# Opens the browser on the files for `kind`: :disk, :tape, :cartridge,
|
|
41
|
+
# :program, or :snapshot, which starts in the saves folder.
|
|
42
|
+
def browse(kind)
|
|
43
|
+
@browsing = 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
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ask_remove = @confirmation.ask(:remove, @media.cartridge_name)
|
|
60
|
+
|
|
61
|
+
# Puts a dropped disk or tape in its device, or asks before a
|
|
62
|
+
# cartridge or a program starts, and says whether it asks. Other
|
|
63
|
+
# files are left alone.
|
|
64
|
+
def drop(path)
|
|
65
|
+
extension = File.extname(path).downcase
|
|
66
|
+
return @media.insert(:disk, path) && false if EXTENSIONS[:disk].include?(extension)
|
|
67
|
+
return @media.insert(:tape, path) && false if EXTENSIONS[:tape].include?(extension)
|
|
68
|
+
return false unless EXTENSIONS[:program].include?(extension)
|
|
69
|
+
|
|
70
|
+
@confirmation.ask(extension == ".crt" ? :cartridge : :start, path)
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Handles a key, and returns :resume or :swap once a question is
|
|
75
|
+
# answered yes, or nil.
|
|
76
|
+
def key(scancode)
|
|
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
|
|
81
|
+
return answer(:cancel) if scancode == ESCAPE
|
|
82
|
+
return answer(@buttons.focus) if [RETURN, SPACE].include?(scancode)
|
|
83
|
+
|
|
84
|
+
direction = PauseMenu::ARROWS[scancode]
|
|
85
|
+
@buttons.shift(direction == :up ? -1 : 1, 1) if %i[up down].include?(direction)
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Handles a click on the browser's list at `top`, or on a question's
|
|
90
|
+
# answer, `action`.
|
|
91
|
+
def click(action, top) = action == :browse ? browsed(@browser.click(top)) : answer(action)
|
|
92
|
+
|
|
93
|
+
def scroll(rows) = @browser.scroll(rows)
|
|
94
|
+
|
|
95
|
+
def draw(left, top, width)
|
|
96
|
+
return @browser.draw(left, top, width, TITLES.fetch(@browsing)) unless @browsing.nil?
|
|
97
|
+
return @confirmation.draw(left, top) if @confirmation.open?
|
|
98
|
+
|
|
99
|
+
@name_field.draw(left, top, width)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Puts the disk or tape picked in, or asks before a cartridge or
|
|
105
|
+
# Quick open power cycles the machine, and leaves the browser, as Esc
|
|
106
|
+
# does.
|
|
107
|
+
def browsed(picked)
|
|
108
|
+
return nil if picked.nil?
|
|
109
|
+
|
|
110
|
+
kind = @browsing
|
|
111
|
+
@browse_from = @browser.directory unless kind == :snapshot
|
|
112
|
+
@browsing = nil
|
|
113
|
+
return nil if picked == :cancel
|
|
114
|
+
return load(picked) if kind == :snapshot
|
|
115
|
+
|
|
116
|
+
if %i[disk tape].include?(kind)
|
|
117
|
+
@media.insert(kind, picked)
|
|
118
|
+
else
|
|
119
|
+
@confirmation.ask(kind == :cartridge ? :cartridge : :start, picked)
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
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
|
+
|
|
142
|
+
# Does what the question asked about if `action` is :confirm, and
|
|
143
|
+
# returns :resume, or :swap for a new machine.
|
|
144
|
+
def answer(action)
|
|
145
|
+
question = @confirmation.close
|
|
146
|
+
return nil unless action == :confirm
|
|
147
|
+
|
|
148
|
+
path = question[1]
|
|
149
|
+
case question[0]
|
|
150
|
+
when :cartridge then @media.attach_cartridge(path) ? :resume : nil
|
|
151
|
+
when :remove then @media.remove_cartridge && :resume
|
|
152
|
+
when :overwrite then save_as(path, replace: true) && nil
|
|
153
|
+
else
|
|
154
|
+
@computer = @media.start(@options, path)
|
|
155
|
+
@computer.nil? ? nil : :swap
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# What the pause menu puts in the machine and takes out: disks, tapes
|
|
6
|
+
# and cartridges, whether disks are writable, which carries over from
|
|
7
|
+
# one disk to the next, and the disk's set, which PREVIOUS and NEXT step
|
|
8
|
+
# through: the .m3u or .vfl list it came from, while that lists it, or
|
|
9
|
+
# the set DiskSet finds. It keeps what last went wrong for the page to
|
|
10
|
+
# show.
|
|
11
|
+
class MenuMedia
|
|
12
|
+
attr_reader :disk_path, :problem
|
|
13
|
+
attr_writer :computer
|
|
14
|
+
|
|
15
|
+
# Starts on the disk the command line's `media_path` put in.
|
|
16
|
+
def initialize(media_path, writable)
|
|
17
|
+
@disk_path = PauseMenu.disk_path(media_path)
|
|
18
|
+
@list = Media::DiskList.list?(media_path) ? media_path : ""
|
|
19
|
+
@writable = writable
|
|
20
|
+
@set_path = ""
|
|
21
|
+
@set = []
|
|
22
|
+
@problem = ""
|
|
23
|
+
@computer = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def writable? = @writable
|
|
27
|
+
|
|
28
|
+
def inserted?
|
|
29
|
+
drive = @computer.drive1541
|
|
30
|
+
drive.nil? ? @computer.mounted? : !drive.disk.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
|
|
34
|
+
# :tape. A list puts its first disk in.
|
|
35
|
+
def insert(kind, path)
|
|
36
|
+
@problem = ""
|
|
37
|
+
if kind == :disk
|
|
38
|
+
disk = Media::DiskList.disk(path)
|
|
39
|
+
Media.insert_disk(@computer, disk, read_only: !@writable)
|
|
40
|
+
list(path)
|
|
41
|
+
@disk_path = disk
|
|
42
|
+
else
|
|
43
|
+
@computer.datasette.insert(Storage::TAP.new(path))
|
|
44
|
+
end
|
|
45
|
+
rescue ArgumentError, SystemCallError, Storage::TAP::FormatError, Storage::T64::FormatError,
|
|
46
|
+
Storage::G64Image::FormatError => e
|
|
47
|
+
@problem = e.message
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Does what the Drive page's :eject_disk, :previous_disk, :next_disk,
|
|
51
|
+
# :protect or :unprotect asks.
|
|
52
|
+
def drive(action)
|
|
53
|
+
case action
|
|
54
|
+
when :eject_disk then eject_disk
|
|
55
|
+
when :previous_disk then step_disk(-1)
|
|
56
|
+
when :next_disk then step_disk(1)
|
|
57
|
+
else writable(action == :unprotect)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def eject_disk
|
|
62
|
+
drive = @computer.drive1541
|
|
63
|
+
drive.nil? ? @computer.unmount : drive.insert(nil)
|
|
64
|
+
@disk_path = ""
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Sets whether disks are writable, for the disk in the drive too,
|
|
68
|
+
# which goes in again as the notch now says.
|
|
69
|
+
def writable(writable)
|
|
70
|
+
@writable = writable
|
|
71
|
+
insert(:disk, @disk_path) if inserted? && !@disk_path.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The disks of the set the disk in the drive belongs to.
|
|
75
|
+
def disk_set
|
|
76
|
+
return [] if @disk_path.empty?
|
|
77
|
+
|
|
78
|
+
unless @set_path == @disk_path
|
|
79
|
+
listed = @list.empty? ? [] : Media::DiskList.disks(@list)
|
|
80
|
+
@set = listed.include?(@disk_path) ? listed : Media::DiskSet.around(@disk_path)
|
|
81
|
+
@set_path = @disk_path
|
|
82
|
+
end
|
|
83
|
+
@set
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Puts the disk `step` before or after this one in its set in the
|
|
87
|
+
# drive, if there is one.
|
|
88
|
+
def step_disk(step)
|
|
89
|
+
set = disk_set
|
|
90
|
+
index = set.index(@disk_path)
|
|
91
|
+
return if index.nil? || !(index + step).between?(0, set.size - 1)
|
|
92
|
+
|
|
93
|
+
insert(:disk, set[index + step])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Swaps the cartridge for the one at `path`, which power cycles the
|
|
97
|
+
# machine, and says whether it went in.
|
|
98
|
+
def attach_cartridge(path)
|
|
99
|
+
@problem = ""
|
|
100
|
+
Media.attach(@computer, path)
|
|
101
|
+
true
|
|
102
|
+
rescue ArgumentError, SystemCallError, Storage::CRTFile::FormatError, Cartridge::UnsupportedTypeError => e
|
|
103
|
+
@problem = e.message
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 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.
|
|
109
|
+
def game_name
|
|
110
|
+
name = cartridge_name
|
|
111
|
+
name = File.basename(@disk_path, File.extname(@disk_path)) if name.empty? && !@disk_path.empty?
|
|
112
|
+
tape = @computer.datasette.tape
|
|
113
|
+
name = File.basename(tape.path, File.extname(tape.path)) if name.empty? && !tape.nil?
|
|
114
|
+
name = name.sub(/\s*[(\[].*\z/, "").strip
|
|
115
|
+
name.empty? ? "BASIC" : name
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The name the cartridge in the expansion port gives itself, or an
|
|
119
|
+
# empty one.
|
|
120
|
+
def cartridge_name
|
|
121
|
+
cartridge = @computer.address_bus.cartridge
|
|
122
|
+
cartridge.nil? ? "" : cartridge.name
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Takes the cartridge out with the power off.
|
|
126
|
+
def remove_cartridge
|
|
127
|
+
@computer.address_bus.detach_cartridge
|
|
128
|
+
@computer.power_cycle!
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A new machine started on `path`, as the command line would, or nil
|
|
132
|
+
# when it won't start.
|
|
133
|
+
def start(options, path)
|
|
134
|
+
@problem = ""
|
|
135
|
+
computer = Frontend.start(options, path, @writable)
|
|
136
|
+
@disk_path = PauseMenu.disk_path(path)
|
|
137
|
+
list(path)
|
|
138
|
+
computer
|
|
139
|
+
rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
|
|
140
|
+
Storage::T64::FormatError, Storage::TAP::FormatError, Storage::CRTFile::FormatError,
|
|
141
|
+
Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError => e
|
|
142
|
+
@problem = e.message
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Keeps `path` as the list the set comes from, if it's a list.
|
|
149
|
+
def list(path)
|
|
150
|
+
return unless Media::DiskList.list?(path)
|
|
151
|
+
|
|
152
|
+
@list = path
|
|
153
|
+
@set_path = ""
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Frontend
|
|
5
|
+
# The pause menu's sections, each showing a part of the machine's
|
|
6
|
+
# hardware as it stands, in one column: what's there, and under it a
|
|
7
|
+
# row for each thing to do, a setting showing its value on the right,
|
|
8
|
+
# which a press steps on.
|
|
9
|
+
class MenuPages
|
|
10
|
+
TEXT = PauseMenu::TEXT
|
|
11
|
+
DIM = PauseMenu::DIM
|
|
12
|
+
LINE = 12
|
|
13
|
+
COLUMNS = 25
|
|
14
|
+
ROW_WIDTH = 200
|
|
15
|
+
|
|
16
|
+
WARNING = 0xff7a6b
|
|
17
|
+
RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
|
|
18
|
+
|
|
19
|
+
PORT_DEVICES = %i[joystick mouse paddles].freeze
|
|
20
|
+
PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
|
|
21
|
+
PORT_ACTIONS = [%i[port1_joystick port1_mouse port1_paddles], %i[port2_joystick port2_mouse port2_paddles]].freeze
|
|
22
|
+
POTS = [%i[none mouse1 paddles1], %i[none mouse2 paddles2]].freeze
|
|
23
|
+
|
|
24
|
+
# The disks, tapes and cartridges the pages put in and take out.
|
|
25
|
+
attr_reader :media
|
|
26
|
+
|
|
27
|
+
def initialize(painter, buttons, media_path, options, snapshots)
|
|
28
|
+
@snapshots = snapshots
|
|
29
|
+
@recent = []
|
|
30
|
+
@painter = painter
|
|
31
|
+
@buttons = buttons
|
|
32
|
+
@media = MenuMedia.new(media_path, options.writable?)
|
|
33
|
+
@reu_kb = options.reu
|
|
34
|
+
@computer = nil
|
|
35
|
+
@controls = nil
|
|
36
|
+
@sound = nil
|
|
37
|
+
@left = 0
|
|
38
|
+
@top = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The machine the pages show and change.
|
|
42
|
+
def machine(computer, controls, sound)
|
|
43
|
+
@computer = computer
|
|
44
|
+
@media.computer = computer
|
|
45
|
+
@controls = controls
|
|
46
|
+
@sound = sound
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def draw(page, left, top)
|
|
50
|
+
problem = @media.problem
|
|
51
|
+
@painter.text(left, top + 186, problem[0, COLUMNS], WARNING) unless problem.empty?
|
|
52
|
+
@left = left
|
|
53
|
+
@top = top
|
|
54
|
+
case page
|
|
55
|
+
when :drive then draw_drive
|
|
56
|
+
when :datasette then draw_datasette
|
|
57
|
+
when :expansion then draw_expansion
|
|
58
|
+
when :ports then draw_ports
|
|
59
|
+
when :sound then draw_sound
|
|
60
|
+
when :snapshots then draw_snapshots
|
|
61
|
+
else draw_power
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The path of the quicksave or autosave a RECENT action stands for.
|
|
66
|
+
def recent(action) = @recent[RECENT.index(action)].to_s
|
|
67
|
+
|
|
68
|
+
def perform(action)
|
|
69
|
+
case action
|
|
70
|
+
when :eject_disk, :previous_disk, :next_disk, :protect, :unprotect then @media.drive(action)
|
|
71
|
+
when :tape_stop then @computer.datasette.stop!
|
|
72
|
+
when :tape_down then @computer.datasette.play!
|
|
73
|
+
when :tape_rewind then @computer.datasette.rewind
|
|
74
|
+
when :tape_eject then @computer.datasette.eject
|
|
75
|
+
when :keys_c64, :keys_joystick then keys(action == :keys_c64 ? :keyboard : :joystick)
|
|
76
|
+
when :swap then @controls.swap_ports
|
|
77
|
+
when :sound_on, :mute then @sound.toggle_mute unless @sound.muted? == (action == :mute)
|
|
78
|
+
when :sid6581, :sid8580 then @computer.sid.model = action == :sid8580 ? :mos8580 : :mos6581
|
|
79
|
+
when :reset then @computer.reset!
|
|
80
|
+
when :power_cycle then @computer.power_cycle!
|
|
81
|
+
else pick_port(action)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def draw_drive
|
|
88
|
+
media = @media
|
|
89
|
+
inserted = media.inserted?
|
|
90
|
+
set = inserted ? media.disk_set : []
|
|
91
|
+
place = set.size < 2 ? "" : " #{set.index(media.disk_path).to_i + 1} OF #{set.size}"
|
|
92
|
+
info("DISK#{place}", inserted ? File.basename(media.disk_path) : "")
|
|
93
|
+
row("INSERT...", :insert_disk)
|
|
94
|
+
row("EJECT", :eject_disk)
|
|
95
|
+
unless set.size < 2
|
|
96
|
+
row("PREVIOUS DISK", :previous_disk)
|
|
97
|
+
row("NEXT DISK", :next_disk)
|
|
98
|
+
end
|
|
99
|
+
toggle("WRITABLE", :writable, [%w[OFF ON], %i[protect unprotect], media.writable? ? 1 : 0])
|
|
100
|
+
skip
|
|
101
|
+
info("DEVICE", @computer.drive1541.nil? ? "FAST LOADING" : "1541, TRUE DRIVE")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def draw_datasette
|
|
105
|
+
datasette = @computer.datasette
|
|
106
|
+
tape = datasette.tape
|
|
107
|
+
info("TAPE", tape.nil? ? "" : File.basename(tape.path))
|
|
108
|
+
row("INSERT...", :insert_tape)
|
|
109
|
+
row("EJECT", :tape_eject)
|
|
110
|
+
toggle("PLAY", :tape_play, [%w[UP DOWN], %i[tape_stop tape_down], datasette.playing? ? 1 : 0])
|
|
111
|
+
row("REWIND", :tape_rewind)
|
|
112
|
+
skip
|
|
113
|
+
info("MOTOR", datasette.motor? ? "ON" : "OFF")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def draw_expansion
|
|
117
|
+
cartridge = @computer.address_bus.cartridge
|
|
118
|
+
info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
|
|
119
|
+
row("INSERT...", :insert_cartridge)
|
|
120
|
+
row("REMOVE", :remove_cartridge)
|
|
121
|
+
row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer?
|
|
122
|
+
return if @reu_kb.nil?
|
|
123
|
+
|
|
124
|
+
skip
|
|
125
|
+
info("RAM EXPANSION", "REU, #{@reu_kb} KB")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def draw_ports
|
|
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
|
+
|
|
142
|
+
def draw_sound
|
|
143
|
+
if @sound.on?
|
|
144
|
+
toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
|
|
145
|
+
else
|
|
146
|
+
info("OUTPUT", "OFF")
|
|
147
|
+
end
|
|
148
|
+
toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], @computer.sid.model == :mos8580 ? 1 : 0])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Saving now and by name, loading a named save, and the quicksaves
|
|
152
|
+
# and autosaves, newest first, which a press loads.
|
|
153
|
+
def draw_snapshots
|
|
154
|
+
row("QUICKSAVE", :quicksave_now)
|
|
155
|
+
row("SAVE...", :save_as)
|
|
156
|
+
row("LOAD...", :load_save)
|
|
157
|
+
skip
|
|
158
|
+
saves = (@snapshots.list("quicksaves") + @snapshots.list("autosaves")).sort_by { |path| -File.mtime(path).to_f }
|
|
159
|
+
@recent = saves.first(RECENT.size)
|
|
160
|
+
@recent.each_with_index do |path, index|
|
|
161
|
+
kind = File.basename(path).start_with?("autosave") ? "AUTOSAVE" : "QUICKSAVE"
|
|
162
|
+
row(kind, RECENT[index], @snapshots.time_of(path))
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def draw_power
|
|
167
|
+
row("RESET", :reset)
|
|
168
|
+
row("POWER CYCLE", :power_cycle)
|
|
169
|
+
skip
|
|
170
|
+
row("QUIT", :quit)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# A label, and its value under it, or NONE in the dim colour when the
|
|
174
|
+
# value is empty.
|
|
175
|
+
def info(label, value)
|
|
176
|
+
@painter.text(@left, @top, label, DIM)
|
|
177
|
+
if value.empty?
|
|
178
|
+
@painter.text(@left, @top + LINE, "NONE", DIM)
|
|
179
|
+
else
|
|
180
|
+
@painter.text(@left, @top + LINE, Painter.fit(value, COLUMNS), TEXT)
|
|
181
|
+
end
|
|
182
|
+
@top += (LINE * 2) + 6
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def row(label, action, value = "")
|
|
186
|
+
@buttons.row([@left, @top, ROW_WIDTH], label, action, value)
|
|
187
|
+
@top += Buttons::HEIGHT + 2
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def toggle(label, action, choices)
|
|
191
|
+
@buttons.toggle([@left, @top, ROW_WIDTH], label, action, choices)
|
|
192
|
+
@top += Buttons::HEIGHT + 2
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def skip
|
|
196
|
+
@top += LINE
|
|
197
|
+
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
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -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
|
|
@@ -95,6 +95,16 @@ module Badline
|
|
|
95
95
|
|
|
96
96
|
def self.width(string, scale: 1) = string.length * GLYPH * scale
|
|
97
97
|
|
|
98
|
+
# The string cut to `columns` characters in its middle, so both its
|
|
99
|
+
# start and its end show.
|
|
100
|
+
def self.fit(string, columns)
|
|
101
|
+
return string if string.length <= columns
|
|
102
|
+
|
|
103
|
+
head = (columns - 3) / 2
|
|
104
|
+
tail = columns - 3 - head
|
|
105
|
+
"#{string[0, head]}...#{string[string.length - tail, tail]}"
|
|
106
|
+
end
|
|
107
|
+
|
|
98
108
|
def icon(name, left, top, rgb, scale: 1)
|
|
99
109
|
tint(rgb)
|
|
100
110
|
place(top, GLYPH * scale)
|