badline 0.8.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 +29 -0
- data/README.md +22 -16
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +53 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +66 -33
- 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/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 +4 -3
- data/lib/badline/frontend/boot.rb +9 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- data/lib/badline/frontend/menu_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +36 -7
- data/lib/badline/frontend/menu_pages.rb +24 -2
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pause_menu.rb +25 -7
- data/lib/badline/frontend/player_window.rb +3 -3
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +11 -5
- data/lib/badline/frontend.rb +1 -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 +13 -3
- data/lib/badline/media.rb +8 -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 +5 -1
- data/lib/badline/options/validation.rb +9 -0
- data/lib/badline/options.rb +7 -4
- 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 +10 -1
|
@@ -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
|
|
@@ -5,13 +5,17 @@ module Badline
|
|
|
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
7
|
# one disk to the next, and the disk's set, which PREVIOUS and NEXT step
|
|
8
|
-
# through.
|
|
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.
|
|
9
11
|
class MenuMedia
|
|
10
12
|
attr_reader :disk_path, :problem
|
|
11
13
|
attr_writer :computer
|
|
12
14
|
|
|
13
|
-
|
|
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 : ""
|
|
15
19
|
@writable = writable
|
|
16
20
|
@set_path = ""
|
|
17
21
|
@set = []
|
|
@@ -27,12 +31,14 @@ module Badline
|
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
# Puts a disk in device 8 or a tape in the datasette, `kind` :disk or
|
|
30
|
-
# :tape.
|
|
34
|
+
# :tape. A list puts its first disk in.
|
|
31
35
|
def insert(kind, path)
|
|
32
36
|
@problem = ""
|
|
33
37
|
if kind == :disk
|
|
34
|
-
Media.
|
|
35
|
-
@
|
|
38
|
+
disk = Media::DiskList.disk(path)
|
|
39
|
+
Media.insert_disk(@computer, disk, read_only: !@writable)
|
|
40
|
+
list(path)
|
|
41
|
+
@disk_path = disk
|
|
36
42
|
else
|
|
37
43
|
@computer.datasette.insert(Storage::TAP.new(path))
|
|
38
44
|
end
|
|
@@ -70,7 +76,8 @@ module Badline
|
|
|
70
76
|
return [] if @disk_path.empty?
|
|
71
77
|
|
|
72
78
|
unless @set_path == @disk_path
|
|
73
|
-
|
|
79
|
+
listed = @list.empty? ? [] : Media::DiskList.disks(@list)
|
|
80
|
+
@set = listed.include?(@disk_path) ? listed : Media::DiskSet.around(@disk_path)
|
|
74
81
|
@set_path = @disk_path
|
|
75
82
|
end
|
|
76
83
|
@set
|
|
@@ -97,6 +104,17 @@ module Badline
|
|
|
97
104
|
false
|
|
98
105
|
end
|
|
99
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
|
+
|
|
100
118
|
# The name the cartridge in the expansion port gives itself, or an
|
|
101
119
|
# empty one.
|
|
102
120
|
def cartridge_name
|
|
@@ -116,6 +134,7 @@ module Badline
|
|
|
116
134
|
@problem = ""
|
|
117
135
|
computer = Frontend.start(options, path, @writable)
|
|
118
136
|
@disk_path = PauseMenu.disk_path(path)
|
|
137
|
+
list(path)
|
|
119
138
|
computer
|
|
120
139
|
rescue ArgumentError, SystemCallError, Media::TrueDrive::Error, Storage::SIDFile::FormatError,
|
|
121
140
|
Storage::T64::FormatError, Storage::TAP::FormatError, Storage::CRTFile::FormatError,
|
|
@@ -123,6 +142,16 @@ module Badline
|
|
|
123
142
|
@problem = e.message
|
|
124
143
|
nil
|
|
125
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
|
|
126
155
|
end
|
|
127
156
|
end
|
|
128
157
|
end
|
|
@@ -14,6 +14,7 @@ module Badline
|
|
|
14
14
|
ROW_WIDTH = 200
|
|
15
15
|
|
|
16
16
|
WARNING = 0xff7a6b
|
|
17
|
+
RECENT = %i[recent0 recent1 recent2 recent3 recent4 recent5 recent6 recent7].freeze
|
|
17
18
|
|
|
18
19
|
PORT_DEVICES = %i[joystick mouse paddles].freeze
|
|
19
20
|
PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
|
|
@@ -23,10 +24,12 @@ module Badline
|
|
|
23
24
|
# The disks, tapes and cartridges the pages put in and take out.
|
|
24
25
|
attr_reader :media
|
|
25
26
|
|
|
26
|
-
def initialize(painter, buttons,
|
|
27
|
+
def initialize(painter, buttons, media_path, options, snapshots)
|
|
28
|
+
@snapshots = snapshots
|
|
29
|
+
@recent = []
|
|
27
30
|
@painter = painter
|
|
28
31
|
@buttons = buttons
|
|
29
|
-
@media = MenuMedia.new(
|
|
32
|
+
@media = MenuMedia.new(media_path, options.writable?)
|
|
30
33
|
@reu_kb = options.reu
|
|
31
34
|
@computer = nil
|
|
32
35
|
@controls = nil
|
|
@@ -54,10 +57,14 @@ module Badline
|
|
|
54
57
|
when :expansion then draw_expansion
|
|
55
58
|
when :ports then draw_ports
|
|
56
59
|
when :sound then draw_sound
|
|
60
|
+
when :snapshots then draw_snapshots
|
|
57
61
|
else draw_power
|
|
58
62
|
end
|
|
59
63
|
end
|
|
60
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
|
+
|
|
61
68
|
def perform(action)
|
|
62
69
|
case action
|
|
63
70
|
when :eject_disk, :previous_disk, :next_disk, :protect, :unprotect then @media.drive(action)
|
|
@@ -141,6 +148,21 @@ module Badline
|
|
|
141
148
|
toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], @computer.sid.model == :mos8580 ? 1 : 0])
|
|
142
149
|
end
|
|
143
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
|
+
|
|
144
166
|
def draw_power
|
|
145
167
|
row("RESET", :reset)
|
|
146
168
|
row("POWER CYCLE", :power_cycle)
|
|
@@ -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
|
|
@@ -14,8 +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
|
|
17
|
+
SECTIONS = ["SNAPSHOTS", "DRIVE 8", "DATASETTE", "EXPANSION PORT", "PORTS", "SOUND", "POWER"].freeze
|
|
18
|
+
PAGES = %i[snapshots drive datasette expansion ports sound power].freeze
|
|
19
19
|
DISKS = %w[.d64 .d71 .d81 .g64 .t64].freeze
|
|
20
20
|
|
|
21
21
|
PANEL = 0x1d2230
|
|
@@ -34,25 +34,28 @@ module Badline
|
|
|
34
34
|
ESCAPE = 41
|
|
35
35
|
RETURN = 40
|
|
36
36
|
SPACE = 44
|
|
37
|
+
SNAPSHOT_ACTIONS = %i[quicksave_now save_as load_save].freeze
|
|
37
38
|
ARROWS = { 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
|
|
38
39
|
MOUSEWHEEL = 0x403
|
|
39
40
|
|
|
40
41
|
# Takes the media, the REU and whether disks are writable from Options.
|
|
41
|
-
def initialize(painter, options)
|
|
42
|
+
def initialize(painter, options, snapshots)
|
|
42
43
|
media_path = options.media_path.to_s
|
|
43
44
|
@painter = painter
|
|
44
45
|
@buttons = Buttons.new(painter, [TEXT, BRIGHT, PANEL, FILL])
|
|
45
|
-
@pages = MenuPages.new(painter, @buttons,
|
|
46
|
-
@
|
|
46
|
+
@pages = MenuPages.new(painter, @buttons, media_path, options, snapshots)
|
|
47
|
+
@snapshots = snapshots
|
|
48
|
+
@dialogs = MenuDialogs.new(painter, @buttons, @pages.media, options, snapshots)
|
|
47
49
|
@open = false
|
|
48
50
|
@section = 0
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
# The path of the disk the command line puts in device 8,
|
|
52
|
-
# one for other media.
|
|
53
|
+
# The path of the disk the command line puts in device 8, a list's
|
|
54
|
+
# first, or an empty one for other media.
|
|
53
55
|
def self.disk_path(path)
|
|
54
56
|
return "" if path.empty?
|
|
55
57
|
return path if File.directory?(path)
|
|
58
|
+
return Media::DiskList.disks(path).first.to_s if Media::DiskList.list?(path)
|
|
56
59
|
|
|
57
60
|
DISKS.include?(File.extname(path).downcase) ? path : ""
|
|
58
61
|
end
|
|
@@ -165,6 +168,8 @@ module Badline
|
|
|
165
168
|
end
|
|
166
169
|
|
|
167
170
|
def menu_action(action)
|
|
171
|
+
return snapshot_action(action) if SNAPSHOT_ACTIONS.include?(action) || MenuPages::RECENT.include?(action)
|
|
172
|
+
|
|
168
173
|
case action
|
|
169
174
|
when :insert_disk then @dialogs.browse(:disk)
|
|
170
175
|
when :insert_tape then @dialogs.browse(:tape)
|
|
@@ -176,6 +181,19 @@ module Badline
|
|
|
176
181
|
nil
|
|
177
182
|
end
|
|
178
183
|
|
|
184
|
+
# Saves now, asks for a name to save under, opens the saves folder,
|
|
185
|
+
# or loads one of the page's quicksaves and autosaves, returning
|
|
186
|
+
# :swap once a save has loaded.
|
|
187
|
+
def snapshot_action(action)
|
|
188
|
+
case action
|
|
189
|
+
when :quicksave_now then @snapshots.quicksave
|
|
190
|
+
when :save_as then @dialogs.ask_name(@snapshots.free_name(@pages.media.game_name))
|
|
191
|
+
when :load_save then @dialogs.browse(:snapshot)
|
|
192
|
+
else return @dialogs.load(@pages.recent(action))
|
|
193
|
+
end
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
179
197
|
# Moves the focus: down from RESUME to the sections, through them,
|
|
180
198
|
# where each opens as it's reached, on to Quick open, right into a
|
|
181
199
|
# page, up and down it, and left out of it again.
|
|
@@ -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,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
|