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
|
@@ -7,9 +7,11 @@ module Badline
|
|
|
7
7
|
# turn the queue's shuffle, loop and all-subtunes modes on and off, and
|
|
8
8
|
# quit, and the status line follows along. Seeking runs the subtune
|
|
9
9
|
# silently up to where it was asked to go: on from where it is to go
|
|
10
|
-
# forward, and from the
|
|
11
|
-
#
|
|
12
|
-
# the
|
|
10
|
+
# forward, and from the latest of its Checkpoints at or before there to
|
|
11
|
+
# go back, or to go forward when that checkpoint is further on than
|
|
12
|
+
# where the subtune is. A subtune that plays to its end moves on as
|
|
13
|
+
# Media::Queue#advance says. It stops at the end of the queue, on q, or
|
|
14
|
+
# on Ctrl-C.
|
|
13
15
|
#
|
|
14
16
|
# `renderer` builds the renderer for an entry's subtune at the sink's
|
|
15
17
|
# rate, or returns nil for an entry that can't play, which is skipped
|
|
@@ -34,6 +36,9 @@ module Badline
|
|
|
34
36
|
@backward = false
|
|
35
37
|
@below_at = -BELOW_NOTE
|
|
36
38
|
@from = 0.0
|
|
39
|
+
@entry = nil
|
|
40
|
+
@subtune = 0
|
|
41
|
+
@checkpoints = Checkpoints.new
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# Returns the result of the last subtune's Playback#play, or :unplayable
|
|
@@ -77,6 +82,7 @@ module Badline
|
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def play(entry, subtune)
|
|
85
|
+
@checkpoints = Checkpoints.new unless entry.equal?(@entry) && subtune == @subtune
|
|
80
86
|
@entry = entry
|
|
81
87
|
@subtune = subtune
|
|
82
88
|
@elapsed = 0.0
|
|
@@ -84,6 +90,7 @@ module Badline
|
|
|
84
90
|
return :unplayable if renderer.nil?
|
|
85
91
|
|
|
86
92
|
renderer.from = @from
|
|
93
|
+
renderer.checkpoints = @checkpoints
|
|
87
94
|
@streaming = renderer
|
|
88
95
|
@from = 0.0
|
|
89
96
|
@backward = false
|
|
@@ -127,7 +134,7 @@ module Badline
|
|
|
127
134
|
|
|
128
135
|
def seek(seconds)
|
|
129
136
|
target = seconds.clamp(0.0, [@length.call(@entry, @subtune) - 1.0, 0.0].max)
|
|
130
|
-
return @streaming.from = target if target > @elapsed
|
|
137
|
+
return @streaming.from = target if target > @elapsed && !@streaming.checkpoint_ahead?(target)
|
|
131
138
|
|
|
132
139
|
@from = target
|
|
133
140
|
skip
|
|
@@ -39,6 +39,22 @@ module Badline
|
|
|
39
39
|
|
|
40
40
|
def clock_hz = @computer.region.clock_hz
|
|
41
41
|
|
|
42
|
+
# The whole machine, as Computer#snapshot takes it, and the extra SIDs,
|
|
43
|
+
# once it plays.
|
|
44
|
+
def save_state(out)
|
|
45
|
+
@computer.save_state(out)
|
|
46
|
+
@extras.each { |sid| sid.save_state(out) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Takes a machine that hasn't started to where #save_state found one,
|
|
50
|
+
# in place of #start.
|
|
51
|
+
def load_state(input)
|
|
52
|
+
@computer.load_state(input)
|
|
53
|
+
@extras.each { |sid| sid.load_state(input) }
|
|
54
|
+
@stereo.synthesize!
|
|
55
|
+
@started = true
|
|
56
|
+
end
|
|
57
|
+
|
|
42
58
|
# Advances one frame of the machine, or `budget` cycles if that is
|
|
43
59
|
# shorter, then yields whatever the SIDs recorded over it, in stereo.
|
|
44
60
|
# Returns the cycles advanced.
|
|
@@ -35,10 +35,15 @@ module Badline
|
|
|
35
35
|
|
|
36
36
|
# Where #stream starts: the frames before it are rendered as fast as
|
|
37
37
|
# they go and yielded without their samples, so the seconds rendered
|
|
38
|
-
# still count up to it.
|
|
39
|
-
#
|
|
38
|
+
# still count up to it. Rendering picks up from the latest of the
|
|
39
|
+
# checkpoints at or before it, without the frames before that. Set
|
|
40
|
+
# while streaming, it skips ahead from the frame rendered last.
|
|
40
41
|
attr_writer :from
|
|
41
42
|
|
|
43
|
+
# The Checkpoints this subtune's renderers keep as they go and start
|
|
44
|
+
# from, for a player that seeks. Without them nothing is kept.
|
|
45
|
+
attr_writer :checkpoints
|
|
46
|
+
|
|
42
47
|
def initialize(tune, seconds:, subtune: nil, rate: DEFAULT_RATE, sid_models: tune.sid_models)
|
|
43
48
|
@tune = tune
|
|
44
49
|
@seconds = seconds
|
|
@@ -52,6 +57,8 @@ module Badline
|
|
|
52
57
|
@rendered = 0.0
|
|
53
58
|
@observer = ->(_samples, _rendered) {}
|
|
54
59
|
@from = 0.0
|
|
60
|
+
@played = 0
|
|
61
|
+
@checkpoints = nil
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
def silence=(seconds)
|
|
@@ -65,7 +72,6 @@ module Badline
|
|
|
65
72
|
# Yields the seconds rendered so far after every frame.
|
|
66
73
|
def render(path)
|
|
67
74
|
container = container_for(path)
|
|
68
|
-
player.start
|
|
69
75
|
container.open(path, rate: @rate, channels: 2) do |writer|
|
|
70
76
|
each_frame do |samples, seconds|
|
|
71
77
|
samples.each { |sample| writer << sample }
|
|
@@ -76,9 +82,16 @@ module Badline
|
|
|
76
82
|
|
|
77
83
|
# Starts the tune, then yields each frame's samples along with the
|
|
78
84
|
# seconds rendered so far.
|
|
79
|
-
def stream(&)
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
def stream(&) = each_frame(&)
|
|
86
|
+
|
|
87
|
+
# Whether the latest checkpoint at or before `seconds` lies past the
|
|
88
|
+
# frame rendered last, so that a seek there gets there sooner from it
|
|
89
|
+
# than on from here.
|
|
90
|
+
def checkpoint_ahead?(seconds)
|
|
91
|
+
checkpoints = @checkpoints
|
|
92
|
+
return false if checkpoints.nil?
|
|
93
|
+
|
|
94
|
+
checkpoints.reach((seconds * player.clock_hz).round) > @played
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
private
|
|
@@ -101,19 +114,77 @@ module Badline
|
|
|
101
114
|
end
|
|
102
115
|
|
|
103
116
|
def each_frame
|
|
104
|
-
|
|
117
|
+
clock_hz = player.clock_hz
|
|
105
118
|
total = total_cycles
|
|
106
|
-
|
|
119
|
+
@played = start((@from * clock_hz).round)
|
|
120
|
+
remaining = total - @played
|
|
107
121
|
while remaining.positive?
|
|
108
122
|
samples = []
|
|
109
123
|
remaining -= player.frame(remaining) { |sample| samples << sample }
|
|
110
|
-
|
|
124
|
+
played = total - remaining
|
|
125
|
+
@played = played
|
|
126
|
+
@rendered = played.fdiv(clock_hz)
|
|
111
127
|
@observer.call(samples, @rendered)
|
|
112
128
|
yield(@rendered < @from ? [] : samples, @rendered)
|
|
113
129
|
break if fallen_silent?(samples)
|
|
130
|
+
|
|
131
|
+
save_checkpoint(played, clock_hz)
|
|
114
132
|
end
|
|
115
133
|
end
|
|
116
134
|
|
|
135
|
+
# Starts the player and its recording, from the latest checkpoint at
|
|
136
|
+
# or before `cycles` if there is one, and returns the cycles into the
|
|
137
|
+
# subtune it starts at.
|
|
138
|
+
def start(cycles)
|
|
139
|
+
state = @checkpoints&.latest(cycles)
|
|
140
|
+
return start_afresh if state.nil?
|
|
141
|
+
|
|
142
|
+
input = Snapshot::StateReader.new(state)
|
|
143
|
+
played = input.int
|
|
144
|
+
resume(input)
|
|
145
|
+
record
|
|
146
|
+
player.stereo.sids.each { |sid| sid.load_recording(input) }
|
|
147
|
+
@level = input.int
|
|
148
|
+
@still = input.int
|
|
149
|
+
played
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def start_afresh
|
|
153
|
+
player.start
|
|
154
|
+
record
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def record = player.stereo.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
|
|
159
|
+
|
|
160
|
+
# Restores the player as it was, then puts back the SID models chosen
|
|
161
|
+
# for this renderer.
|
|
162
|
+
def resume(input)
|
|
163
|
+
sids = player.stereo.sids
|
|
164
|
+
chosen = sids.map(&:model)
|
|
165
|
+
sids.each { |sid| refit(sid, Snapshot::Setup::SID_MODELS.fetch(input.int)) }
|
|
166
|
+
player.load_state(input)
|
|
167
|
+
sids.each_with_index { |sid, index| refit(sid, chosen[index]) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def refit(sid, model)
|
|
171
|
+
sid.model = model unless sid.model == model
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def save_checkpoint(played, clock_hz)
|
|
175
|
+
checkpoints = @checkpoints
|
|
176
|
+
return if checkpoints.nil? || !checkpoints.due?(played, clock_hz)
|
|
177
|
+
|
|
178
|
+
sids = player.stereo.sids
|
|
179
|
+
out = Snapshot::StateWriter.new
|
|
180
|
+
out.int(played)
|
|
181
|
+
sids.each { |sid| out.int(Snapshot::Setup::SID_MODELS.index(sid.model) || 0) }
|
|
182
|
+
player.save_state(out)
|
|
183
|
+
sids.each { |sid| sid.save_recording(out) }
|
|
184
|
+
out.int(@level).int(@still)
|
|
185
|
+
checkpoints.keep(played, out.state)
|
|
186
|
+
end
|
|
187
|
+
|
|
117
188
|
# Counts the samples since the output last strayed more than QUIET
|
|
118
189
|
# from where it settled.
|
|
119
190
|
def fallen_silent?(samples)
|
data/lib/badline/audio/stereo.rb
CHANGED
data/lib/badline/audio.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "badline/audio/aiff"
|
|
|
6
6
|
require "badline/audio/stereo"
|
|
7
7
|
require "badline/audio/bare_player"
|
|
8
8
|
require "badline/audio/machine_player"
|
|
9
|
+
require "badline/audio/checkpoints"
|
|
9
10
|
require "badline/audio/renderer"
|
|
10
11
|
require "badline/audio/playback"
|
|
11
12
|
require "badline/audio/terminal"
|
|
@@ -50,7 +50,7 @@ module Badline
|
|
|
50
50
|
def detach_drive1541
|
|
51
51
|
return unless @drive1541
|
|
52
52
|
|
|
53
|
-
@iec_bus
|
|
53
|
+
@iec_bus.detach(@drive1541)
|
|
54
54
|
@drive1541 = nil
|
|
55
55
|
@serial_trap&.device = serial_trap_device
|
|
56
56
|
end
|
|
@@ -95,10 +95,10 @@ module Badline
|
|
|
95
95
|
@save_trap.load_state(input)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
98
|
+
# A true 1541 on the serial bus. The leading flag is the bus itself,
|
|
99
|
+
# which every machine now has, kept so older snapshots still load.
|
|
100
100
|
def save_serial_bus(out)
|
|
101
|
-
out.boolean(
|
|
101
|
+
out.boolean(true).boolean(!@drive1541.nil?)
|
|
102
102
|
return unless @drive1541
|
|
103
103
|
|
|
104
104
|
out.int(@drive1541.device)
|
|
@@ -106,7 +106,7 @@ module Badline
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def load_serial_bus(input)
|
|
109
|
-
|
|
109
|
+
input.boolean?
|
|
110
110
|
return detach_drive1541 unless input.boolean?
|
|
111
111
|
|
|
112
112
|
device = input.int
|
data/lib/badline/computer.rb
CHANGED
|
@@ -42,9 +42,13 @@ module Badline
|
|
|
42
42
|
# TOD clocks count. The KERNAL tells PAL from NTSC by the raster, so
|
|
43
43
|
# every region boots the same ROMs. reu plugs an REU of that many K
|
|
44
44
|
# into the expansion port, where it drives the IRQ line and takes the
|
|
45
|
-
# bus for its transfers.
|
|
46
|
-
|
|
45
|
+
# bus for its transfers. kernal: names the KERNAL ROM
|
|
46
|
+
# (AddressBus::ROMs::KERNALS), and datasette: false leaves the cassette port
|
|
47
|
+
# empty, as on the SX-64.
|
|
48
|
+
def initialize(debug: false, reu: nil, kernal: :c64, datasette: true, **machine)
|
|
47
49
|
@address_bus = AddressBus.new(**machine)
|
|
50
|
+
@address_bus.kernal = kernal unless kernal == :c64
|
|
51
|
+
@address_bus.datasette.disconnect! unless datasette
|
|
48
52
|
@cpu = CPU.new(@address_bus, debug:)
|
|
49
53
|
@vic = @address_bus.vic
|
|
50
54
|
@vic.open_bus = -> { @address_bus.ram.peek(@cpu.program_counter) }
|
|
@@ -66,7 +70,7 @@ module Badline
|
|
|
66
70
|
@serial_trap = nil
|
|
67
71
|
@save_trap = nil
|
|
68
72
|
@drive1541 = nil
|
|
69
|
-
@iec_bus =
|
|
73
|
+
@iec_bus = IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
70
74
|
@reu = reu ? plug_reu(reu) : nil
|
|
71
75
|
end
|
|
72
76
|
|
|
@@ -161,19 +165,17 @@ module Badline
|
|
|
161
165
|
# byte transfers reach the drive. The LOAD and SAVE traps stay, and
|
|
162
166
|
# still serve a mounted image.
|
|
163
167
|
def attach_drive1541(drive)
|
|
164
|
-
@iec_bus.detach(@drive1541) if @
|
|
168
|
+
@iec_bus.detach(@drive1541) if @drive1541
|
|
165
169
|
drive.host_clock_hz = region.clock_hz
|
|
166
170
|
@drive1541 = drive
|
|
167
171
|
drive.connect(iec_bus)
|
|
168
172
|
@serial_trap&.device = serial_trap_device
|
|
169
173
|
end
|
|
170
174
|
|
|
171
|
-
# The serial bus
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
@iec_bus ||= IECBus.new(host: @cia2).tap(&:plug_in!)
|
|
176
|
-
end
|
|
175
|
+
# The serial bus, which CIA 2's port A drives through the C64's own
|
|
176
|
+
# inverters and reads back on PA6 and PA7, with or without a drive on
|
|
177
|
+
# it.
|
|
178
|
+
attr_reader :iec_bus
|
|
177
179
|
|
|
178
180
|
def capture_output
|
|
179
181
|
@capture_output ||= ChroutTrap.new(cpu:, bus: address_bus).tap do |trap|
|
data/lib/badline/core.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The emulator core: the machine, its chips, media and traps, and the
|
|
4
|
+
# snapshot state Computer saves and restores with, without the options,
|
|
5
|
+
# the rest of the snapshot code, audio or front end. lib/badline.rb and
|
|
6
|
+
# the Spinel harnesses in spinel/ load it, so a file the core needs is
|
|
7
|
+
# added here.
|
|
8
|
+
require "badline/version"
|
|
9
|
+
require "badline/integer_helper"
|
|
10
|
+
require "badline/region"
|
|
11
|
+
require "badline/model"
|
|
12
|
+
require "badline/addressable"
|
|
13
|
+
require "badline/memory"
|
|
14
|
+
require "badline/color_memory"
|
|
15
|
+
require "badline/rom"
|
|
16
|
+
require "badline/ram_expansion"
|
|
17
|
+
require "badline/address_bus"
|
|
18
|
+
require "badline/instruction"
|
|
19
|
+
require "badline/instruction_set"
|
|
20
|
+
require "badline/status"
|
|
21
|
+
require "badline/keyboard"
|
|
22
|
+
require "badline/joystick"
|
|
23
|
+
require "badline/control_ports"
|
|
24
|
+
require "badline/input"
|
|
25
|
+
require "badline/cycleable"
|
|
26
|
+
require "badline/datasette"
|
|
27
|
+
require "badline/time_of_day"
|
|
28
|
+
require "badline/cia"
|
|
29
|
+
require "badline/via"
|
|
30
|
+
require "badline/sid"
|
|
31
|
+
require "badline/debug_register"
|
|
32
|
+
require "badline/reu"
|
|
33
|
+
require "badline/interrupts"
|
|
34
|
+
require "badline/traps"
|
|
35
|
+
require "badline/cpu"
|
|
36
|
+
require "badline/vic"
|
|
37
|
+
require "badline/keyboard_buffer"
|
|
38
|
+
require "badline/iec_bus"
|
|
39
|
+
require "badline/drive1541"
|
|
40
|
+
require "badline/snapshot/state"
|
|
41
|
+
require "badline/snapshot/setup"
|
|
42
|
+
require "badline/computer"
|
|
43
|
+
require "badline/checkpoint"
|
|
44
|
+
require "badline/storage"
|
|
45
|
+
require "badline/cartridge"
|
|
46
|
+
require "badline/kernal_trap"
|
|
47
|
+
require "badline/chrout_trap"
|
|
48
|
+
require "badline/media"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :data_path
|
|
8
|
+
|
|
9
|
+
# The per-user folder badline keeps its own files in. Defaults to
|
|
10
|
+
# $BADLINE_DATA_PATH, then to the platform's folder for application
|
|
11
|
+
# data. Assigning nil restores the default. Nothing is created until
|
|
12
|
+
# data_folder is asked for a subfolder.
|
|
13
|
+
def data_path
|
|
14
|
+
@data_path ||= ENV.fetch("BADLINE_DATA_PATH", nil) || default_data_path(RUBY_PLATFORM)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ~/Library/Application Support/badline on macOS, and
|
|
18
|
+
# $XDG_DATA_HOME/badline or ~/.local/share/badline elsewhere.
|
|
19
|
+
def default_data_path(platform)
|
|
20
|
+
return File.join(Dir.home, "Library", "Application Support", "badline") if platform.include?("darwin")
|
|
21
|
+
|
|
22
|
+
xdg = ENV.fetch("XDG_DATA_HOME", nil)
|
|
23
|
+
xdg = File.join(Dir.home, ".local", "share") if xdg.nil? || !xdg.start_with?("/")
|
|
24
|
+
File.join(xdg, "badline")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The quicksaves, autosaves or saves subfolder of data_path, created
|
|
28
|
+
# along with data_path if it doesn't exist yet.
|
|
29
|
+
def data_folder(name)
|
|
30
|
+
raise ArgumentError, "unknown data folder: #{name}" unless %w[quicksaves autosaves saves].include?(name)
|
|
31
|
+
|
|
32
|
+
path = File.join(data_path, name)
|
|
33
|
+
FileUtils.mkdir_p(path)
|
|
34
|
+
path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/badline/datasette.rb
CHANGED
|
@@ -5,11 +5,15 @@ module Badline
|
|
|
5
5
|
# motor runs with a key pressed, and reports the keys through the cassette
|
|
6
6
|
# sense line on the $01 port.
|
|
7
7
|
class Datasette
|
|
8
|
+
# What inserting a tape raises on a machine without a cassette port.
|
|
9
|
+
class Missing < ArgumentError; end
|
|
10
|
+
|
|
8
11
|
attr_reader :tape
|
|
9
12
|
attr_writer :motor
|
|
10
13
|
|
|
11
14
|
def initialize(tape = nil)
|
|
12
15
|
@tape = tape
|
|
16
|
+
@connected = true
|
|
13
17
|
@playing = false
|
|
14
18
|
@motor = false
|
|
15
19
|
@countdown = 0
|
|
@@ -27,7 +31,18 @@ module Badline
|
|
|
27
31
|
@sense_handler = handler
|
|
28
32
|
end
|
|
29
33
|
|
|
34
|
+
# Whether the machine has a datasette at its cassette port. The SX-64
|
|
35
|
+
# has none: its keys stay up and it takes no tape.
|
|
36
|
+
def connected? = @connected
|
|
37
|
+
|
|
38
|
+
def disconnect!
|
|
39
|
+
eject
|
|
40
|
+
@connected = false
|
|
41
|
+
end
|
|
42
|
+
|
|
30
43
|
def insert(tape)
|
|
44
|
+
raise Missing, "this machine has no datasette" unless @connected
|
|
45
|
+
|
|
31
46
|
@tape = tape
|
|
32
47
|
rewind
|
|
33
48
|
end
|
|
@@ -54,18 +69,19 @@ module Badline
|
|
|
54
69
|
|
|
55
70
|
def running? = @playing && @motor && !@tape.nil?
|
|
56
71
|
|
|
57
|
-
# The keys, the motor, the countdown to the next pulse and the tape:
|
|
58
|
-
# path and how far it has played.
|
|
72
|
+
# The keys, the motor, the countdown to the next pulse and the tape:
|
|
73
|
+
# its path, its bytes and how far it has played.
|
|
59
74
|
def save_state(out)
|
|
60
75
|
out.marker("DATASETTE")
|
|
61
76
|
out.boolean(@playing).boolean(@motor).int(@countdown).boolean(!@tape.nil?)
|
|
62
77
|
return unless @tape
|
|
63
78
|
|
|
64
|
-
out.string(@tape.path).int(@tape.position)
|
|
79
|
+
out.string(@tape.path).blob(@tape.bytes).int(@tape.position)
|
|
65
80
|
end
|
|
66
81
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
82
|
+
# The tape goes back in from the bytes the state holds, without its
|
|
83
|
+
# host file, unless the tape in is the same one. The sense and flag
|
|
84
|
+
# handlers don't fire.
|
|
69
85
|
def load_state(input)
|
|
70
86
|
input.marker("DATASETTE")
|
|
71
87
|
@playing = input.boolean?
|
|
@@ -74,10 +90,9 @@ module Badline
|
|
|
74
90
|
return @tape = nil unless input.boolean?
|
|
75
91
|
|
|
76
92
|
path = input.string
|
|
93
|
+
bytes = input.blob
|
|
77
94
|
position = input.int
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@tape = Storage::TAP.new(path) unless @tape&.path&.b == path
|
|
95
|
+
@tape = Storage::TAP.new(path, bytes:) unless @tape&.path&.b == path && @tape.bytes == bytes
|
|
81
96
|
@tape.position = position
|
|
82
97
|
end
|
|
83
98
|
|
|
@@ -99,7 +114,7 @@ module Badline
|
|
|
99
114
|
private
|
|
100
115
|
|
|
101
116
|
def press(down)
|
|
102
|
-
return if down == @playing
|
|
117
|
+
return if down == @playing || !@connected
|
|
103
118
|
|
|
104
119
|
@playing = down
|
|
105
120
|
@sense_handler&.call
|
|
@@ -160,9 +160,9 @@ module Badline
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Whether the write-protect notch is covered: the disk's image won't
|
|
163
|
-
# take writes
|
|
164
|
-
# only in its tracks.
|
|
165
|
-
def write_protected? =
|
|
163
|
+
# take writes, or a disk without an image was opened read-only. A
|
|
164
|
+
# disk without an image keeps what it takes only in its tracks.
|
|
165
|
+
def write_protected? = @image.nil? ? @read_only : !@image.writable?
|
|
166
166
|
|
|
167
167
|
# A half track the head writes to, blank to begin with: no flux, and
|
|
168
168
|
# as many bytes as a turn holds at the bit rate of the +zone+ it's
|
|
@@ -5,7 +5,8 @@ module Badline
|
|
|
5
5
|
class Disk
|
|
6
6
|
# Saving and restoring a Disk for a snapshot: the image by its path,
|
|
7
7
|
# and every half track as the head sees it, with those written since
|
|
8
|
-
# the last flush. The image itself is read again from its host file
|
|
8
|
+
# the last flush. The image itself is read again from its host file,
|
|
9
|
+
# where there still is one.
|
|
9
10
|
module State
|
|
10
11
|
# The image's path, expanded, and whether it was opened read-only,
|
|
11
12
|
# for a disk from Disk.open. Nil for a disk made another way.
|
|
@@ -25,18 +26,26 @@ module Badline
|
|
|
25
26
|
|
|
26
27
|
# The disk a state from save_state describes: +current+ when it was
|
|
27
28
|
# opened from the same image the same way, and otherwise a disk for
|
|
28
|
-
# the image at the path the state names
|
|
29
|
-
#
|
|
30
|
-
#
|
|
29
|
+
# the image at the path the state names. Where no file is at that
|
|
30
|
+
# path, the disk has no image and is write-protected, and a
|
|
31
|
+
# detached reader always gets one without an image. Either way its
|
|
32
|
+
# tracks come from the state.
|
|
31
33
|
def self.load(input, current)
|
|
32
34
|
path = input.optional_string
|
|
33
35
|
read_only = input.boolean?
|
|
34
36
|
disk = current if current && !path.nil? && current.path == path && current.read_only == read_only
|
|
35
|
-
disk ||= path
|
|
37
|
+
disk ||= fresh(path, read_only, input.detached?)
|
|
36
38
|
disk.load_tracks(input)
|
|
37
39
|
disk
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def self.fresh(path, read_only, detached)
|
|
43
|
+
return Disk.new if path.nil? || detached
|
|
44
|
+
return Disk.new.opened(path, true) unless File.file?(path)
|
|
45
|
+
|
|
46
|
+
reopen(path, read_only)
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
# Notes the image's path and how it was opened.
|
|
41
50
|
def opened(path, read_only)
|
|
42
51
|
@path = File.expand_path(path)
|
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -23,10 +23,10 @@ module Badline
|
|
|
23
23
|
@led = DriveLed.for(computer)
|
|
24
24
|
@controls = Controls.new(computer)
|
|
25
25
|
open_window
|
|
26
|
-
@sound = Sound.new(computer.sid, options.sound?, @verbose)
|
|
26
|
+
@sound = Sound.new(computer.sid, computer.region.clock_hz, options.sound?, @verbose)
|
|
27
27
|
@gamepads = Gamepads.new(computer, @verbose)
|
|
28
28
|
@frame_report = FrameReport.new(@sound)
|
|
29
|
-
@menu = PauseMenu.new(Painter.new(@renderer), options)
|
|
29
|
+
@menu = PauseMenu.new(Painter.new(@renderer), options, @snapshots)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def run
|
|
@@ -66,6 +66,7 @@ module Badline
|
|
|
66
66
|
@frame_report.add(stamps) if @verbose
|
|
67
67
|
@frames += 1
|
|
68
68
|
@timeline.run(@computer, @frames)
|
|
69
|
+
@snapshots.tick(@frames)
|
|
69
70
|
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
70
71
|
@pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
|
|
71
72
|
report(stamps.last) if (@frames % 50).zero?
|
|
@@ -140,7 +141,7 @@ module Badline
|
|
|
140
141
|
@led = DriveLed.for(computer)
|
|
141
142
|
@controls.computer = computer
|
|
142
143
|
@gamepads.computer = computer
|
|
143
|
-
@sound.sid
|
|
144
|
+
@sound.switch(computer.sid, computer.region.clock_hz)
|
|
144
145
|
@snapshots.computer = computer
|
|
145
146
|
end
|
|
146
147
|
|
|
@@ -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
|
|
@@ -44,10 +45,14 @@ module Badline
|
|
|
44
45
|
|
|
45
46
|
# A new machine built from the options, with `media`, if not nil,
|
|
46
47
|
# attached and started as the command line does, its disk writable if
|
|
47
|
-
# `writable`.
|
|
48
|
+
# `writable`. The model gives the chips, and a .sid tune or --sid the
|
|
49
|
+
# SID.
|
|
48
50
|
def self.start(options, media, writable)
|
|
49
|
-
|
|
50
|
-
|
|
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)
|
|
51
56
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
52
57
|
unless media.nil?
|
|
53
58
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The pause menu's question before something power cycles the machine
|
|
6
|
-
# a cartridge going in or out,
|
|
7
|
-
#
|
|
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.
|
|
8
9
|
class Confirmation
|
|
9
|
-
TITLES = { cartridge: "INSERT A CARTRIDGE", remove: "REMOVE THE CARTRIDGE", start: "QUICK OPEN"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
18
|
|
|
14
19
|
def initialize(painter, buttons)
|
|
15
20
|
@painter = painter
|
|
@@ -21,8 +26,8 @@ module Badline
|
|
|
21
26
|
|
|
22
27
|
def open? = @open
|
|
23
28
|
|
|
24
|
-
# Asks about `kind`, :cartridge, :remove or :
|
|
25
|
-
# `path
|
|
29
|
+
# Asks about `kind`, :cartridge, :remove, :start or :overwrite, for the
|
|
30
|
+
# file at `path`, or the save named so.
|
|
26
31
|
def ask(kind, path)
|
|
27
32
|
@kind = kind
|
|
28
33
|
@path = path
|
|
@@ -40,8 +45,9 @@ module Badline
|
|
|
40
45
|
painter = @painter
|
|
41
46
|
painter.text(left, top, TITLES.fetch(@kind), PauseMenu::BRIGHT)
|
|
42
47
|
painter.text(left, top + 20, Painter.fit(File.basename(@path), 40), PauseMenu::TEXT) unless @path.empty?
|
|
43
|
-
|
|
44
|
-
painter.text(left, top +
|
|
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?
|
|
45
51
|
@buttons.row([left, top + 80, MenuPages::ROW_WIDTH], "CANCEL", :cancel)
|
|
46
52
|
@buttons.row([left, top + 80 + Buttons::HEIGHT + 2, MenuPages::ROW_WIDTH], ANSWERS.fetch(@kind), :confirm)
|
|
47
53
|
end
|