badline 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/CLAUDE.md +6 -4
- data/README.md +60 -19
- data/doc/library.md +10 -3
- data/doc/media.md +11 -2
- data/doc/pinned-behaviour.md +199 -9
- data/doc/sid-player.md +8 -4
- data/doc/snapshots.md +79 -33
- data/lib/badline/address_bus/pla.rb +130 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +13 -92
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/chrout_trap.rb +4 -3
- data/lib/badline/computer/attachments.rb +52 -12
- data/lib/badline/computer/saved_state.rb +1 -0
- data/lib/badline/computer.rb +50 -33
- data/lib/badline/control_ports.rb +10 -3
- data/lib/badline/core.rb +51 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +63 -10
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/drive1541/serial_port.rb +5 -8
- data/lib/badline/drive1541/sleep.rb +5 -6
- data/lib/badline/drive1541.rb +3 -3
- data/lib/badline/frontend/app.rb +63 -64
- data/lib/badline/frontend/boot.rb +34 -4
- data/lib/badline/frontend/confirmation.rb +17 -11
- data/lib/badline/frontend/controls.rb +12 -6
- data/lib/badline/frontend/drive_led.rb +9 -10
- data/lib/badline/frontend/frame_rate.rb +4 -4
- data/lib/badline/frontend/machine_window.rb +87 -0
- data/lib/badline/frontend/menu_dialogs.rb +52 -14
- data/lib/badline/frontend/menu_media.rb +70 -16
- data/lib/badline/frontend/menu_pages.rb +47 -42
- data/lib/badline/frontend/menu_ports.rb +69 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/pacer.rb +6 -5
- data/lib/badline/frontend/pause_menu.rb +52 -37
- data/lib/badline/frontend/player_screen.rb +1 -1
- data/lib/badline/frontend/player_window.rb +4 -4
- data/lib/badline/frontend/screen.rb +39 -37
- data/lib/badline/frontend/sid_view.rb +1 -1
- data/lib/badline/frontend/snapshots.rb +145 -17
- data/lib/badline/frontend/sound.rb +17 -9
- data/lib/badline/frontend/timeline.rb +36 -5
- data/lib/badline/frontend.rb +3 -0
- data/lib/badline/iec_bus.rb +16 -28
- data/lib/badline/kernal_trap/drive.rb +3 -0
- data/lib/badline/kernal_trap/file.rb +1 -1
- data/lib/badline/kernal_trap/layout.rb +112 -0
- data/lib/badline/kernal_trap/load.rb +24 -25
- data/lib/badline/kernal_trap/routine.rb +17 -5
- data/lib/badline/kernal_trap/save.rb +13 -23
- data/lib/badline/kernal_trap/serial.rb +38 -24
- data/lib/badline/kernal_trap.rb +1 -0
- data/lib/badline/keyboard.rb +17 -14
- data/lib/badline/machine.rb +60 -0
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +13 -3
- data/lib/badline/media/vic20_basic.rb +54 -0
- data/lib/badline/media/vic20_cartridge.rb +74 -0
- data/lib/badline/media/vic20_media.rb +132 -0
- data/lib/badline/media.rb +39 -20
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +10 -6
- data/lib/badline/options/help.rb +25 -13
- data/lib/badline/options/table.rb +15 -1
- data/lib/badline/options/validation.rb +63 -3
- data/lib/badline/options.rb +37 -34
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +37 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/roms/vic20/basic.rom +0 -0
- data/lib/badline/roms/vic20/character.rom +0 -0
- data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
- data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/image.rb +15 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +15 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- data/lib/badline/snapshot.rb +10 -0
- data/lib/badline/storage/crt_file.rb +16 -5
- data/lib/badline/storage/disk_image.rb +3 -0
- data/lib/badline/storage/host_directory.rb +3 -0
- data/lib/badline/storage/t64.rb +10 -4
- data/lib/badline/storage/tap.rb +44 -8
- data/lib/badline/storage/unavailable.rb +3 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/timing.rb +12 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/shift_register.rb +43 -16
- data/lib/badline/vic/bank.rb +53 -16
- data/lib/badline/vic/registers.rb +20 -4
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline/vic.rb +34 -15
- data/lib/badline/vic20/attachments.rb +103 -0
- data/lib/badline/vic20/bus.rb +308 -0
- data/lib/badline/vic20/cpu.rb +10 -0
- data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
- data/lib/badline/vic20/port_wiring.rb +50 -0
- data/lib/badline/vic20/saved_state.rb +152 -0
- data/lib/badline/vic20/sound/output.rb +83 -0
- data/lib/badline/vic20/sound.rb +233 -0
- data/lib/badline/vic20/user_via_ports.rb +47 -0
- data/lib/badline/vic20/vic/painter.rb +239 -0
- data/lib/badline/vic20/vic/saved_state.rb +55 -0
- data/lib/badline/vic20/vic/text_window.rb +71 -0
- data/lib/badline/vic20/vic.rb +267 -0
- data/lib/badline/vic20.rb +220 -0
- data/lib/badline.rb +2 -38
- metadata +37 -2
- data/lib/badline/address_bus/ultimax_pages.rb +0 -38
data/lib/badline/datasette.rb
CHANGED
|
@@ -3,14 +3,22 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
# The 1530 datasette. Plays a pulse stream into CIA 1's FLAG pin while the
|
|
5
5
|
# motor runs with a key pressed, and reports the keys through the cassette
|
|
6
|
-
# sense line on the $01 port.
|
|
6
|
+
# sense line on the $01 port. With RECORD down too, it records the
|
|
7
|
+
# machine's write line onto the tape instead.
|
|
7
8
|
class Datasette
|
|
9
|
+
# What inserting a tape raises on a machine without a cassette port.
|
|
10
|
+
class Missing < ArgumentError; end
|
|
11
|
+
|
|
8
12
|
attr_reader :tape
|
|
9
13
|
attr_writer :motor
|
|
10
14
|
|
|
11
15
|
def initialize(tape = nil)
|
|
12
16
|
@tape = tape
|
|
17
|
+
@connected = true
|
|
13
18
|
@playing = false
|
|
19
|
+
@recording = false
|
|
20
|
+
@write_high = true
|
|
21
|
+
@since = 0
|
|
14
22
|
@motor = false
|
|
15
23
|
@countdown = 0
|
|
16
24
|
@flag_handler = nil
|
|
@@ -27,7 +35,18 @@ module Badline
|
|
|
27
35
|
@sense_handler = handler
|
|
28
36
|
end
|
|
29
37
|
|
|
38
|
+
# Whether the machine has a datasette at its cassette port. The SX-64
|
|
39
|
+
# has none: its keys stay up and it takes no tape.
|
|
40
|
+
def connected? = @connected
|
|
41
|
+
|
|
42
|
+
def disconnect!
|
|
43
|
+
eject
|
|
44
|
+
@connected = false
|
|
45
|
+
end
|
|
46
|
+
|
|
30
47
|
def insert(tape)
|
|
48
|
+
raise Missing, "this machine has no datasette" unless @connected
|
|
49
|
+
|
|
31
50
|
@tape = tape
|
|
32
51
|
rewind
|
|
33
52
|
end
|
|
@@ -45,6 +64,30 @@ module Badline
|
|
|
45
64
|
def play! = press(true)
|
|
46
65
|
def stop! = press(false)
|
|
47
66
|
|
|
67
|
+
# Presses RECORD and PLAY together.
|
|
68
|
+
def record!
|
|
69
|
+
return if !@connected || @tape.nil?
|
|
70
|
+
|
|
71
|
+
press(true)
|
|
72
|
+
@recording = true
|
|
73
|
+
@since = 0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def recording? = @recording
|
|
77
|
+
|
|
78
|
+
# The machine's tape write line. While the deck records with its motor
|
|
79
|
+
# running, each rising edge ends a pulse on the tape, as long as the
|
|
80
|
+
# time since the edge before, and plays back as a falling edge on the
|
|
81
|
+
# read line.
|
|
82
|
+
def write_line=(high)
|
|
83
|
+
rose = high && !@write_high
|
|
84
|
+
@write_high = high
|
|
85
|
+
return unless rose && @recording && running?
|
|
86
|
+
|
|
87
|
+
@tape.record_pulse(@since)
|
|
88
|
+
@since = 0
|
|
89
|
+
end
|
|
90
|
+
|
|
48
91
|
def playing? = @playing
|
|
49
92
|
|
|
50
93
|
# Sense reads low while a key is down on the deck.
|
|
@@ -54,18 +97,19 @@ module Badline
|
|
|
54
97
|
|
|
55
98
|
def running? = @playing && @motor && !@tape.nil?
|
|
56
99
|
|
|
57
|
-
# The keys, the motor, the countdown to the next pulse and the tape:
|
|
58
|
-
# path and how far it has played.
|
|
100
|
+
# The keys, the motor, the countdown to the next pulse and the tape:
|
|
101
|
+
# its path, its bytes and how far it has played.
|
|
59
102
|
def save_state(out)
|
|
60
103
|
out.marker("DATASETTE")
|
|
61
104
|
out.boolean(@playing).boolean(@motor).int(@countdown).boolean(!@tape.nil?)
|
|
62
105
|
return unless @tape
|
|
63
106
|
|
|
64
|
-
out.string(@tape.path).int(@tape.position)
|
|
107
|
+
out.string(@tape.path).blob(@tape.bytes).int(@tape.position)
|
|
65
108
|
end
|
|
66
109
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
110
|
+
# The tape goes back in from the bytes the state holds, without its
|
|
111
|
+
# host file, unless the tape in is the same one. The sense and flag
|
|
112
|
+
# handlers don't fire.
|
|
69
113
|
def load_state(input)
|
|
70
114
|
input.marker("DATASETTE")
|
|
71
115
|
@playing = input.boolean?
|
|
@@ -74,15 +118,15 @@ module Badline
|
|
|
74
118
|
return @tape = nil unless input.boolean?
|
|
75
119
|
|
|
76
120
|
path = input.string
|
|
121
|
+
bytes = input.blob
|
|
77
122
|
position = input.int
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@tape = Storage::TAP.new(path) unless @tape&.path&.b == path
|
|
123
|
+
@tape = Storage::TAP.new(path, bytes:) unless @tape&.path&.b == path && @tape.bytes == bytes
|
|
81
124
|
@tape.position = position
|
|
82
125
|
end
|
|
83
126
|
|
|
84
127
|
def cycle!
|
|
85
128
|
return unless running?
|
|
129
|
+
return @since += 1 if @recording
|
|
86
130
|
|
|
87
131
|
@countdown = @tape.next_pulse.to_i if @countdown.zero?
|
|
88
132
|
return if @countdown.zero?
|
|
@@ -99,10 +143,19 @@ module Badline
|
|
|
99
143
|
private
|
|
100
144
|
|
|
101
145
|
def press(down)
|
|
102
|
-
return if down == @playing
|
|
146
|
+
return if down == @playing || !@connected
|
|
103
147
|
|
|
148
|
+
finish_recording unless down
|
|
104
149
|
@playing = down
|
|
105
150
|
@sense_handler&.call
|
|
106
151
|
end
|
|
152
|
+
|
|
153
|
+
# Releasing the keys ends a recording, and writes the tape out.
|
|
154
|
+
def finish_recording
|
|
155
|
+
return unless @recording
|
|
156
|
+
|
|
157
|
+
@recording = false
|
|
158
|
+
@tape.save
|
|
159
|
+
end
|
|
107
160
|
end
|
|
108
161
|
end
|
|
@@ -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)
|
|
@@ -27,7 +27,7 @@ module Badline
|
|
|
27
27
|
|
|
28
28
|
attr_reader :bus
|
|
29
29
|
|
|
30
|
-
def initialize(device: 8, bus:
|
|
30
|
+
def initialize(device: 8, bus: IECBus.new)
|
|
31
31
|
@port_b = (0xff & ~(SERIAL_INPUTS | JUMPERS)) | (((device - 8) & 0x03) << 5)
|
|
32
32
|
@host = 0
|
|
33
33
|
self.bus = bus
|
|
@@ -38,10 +38,10 @@ module Badline
|
|
|
38
38
|
latch_host
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# Takes the C64's lines as
|
|
42
|
-
# next host cycle.
|
|
41
|
+
# Takes the C64's lines as the machine last pushed them into the bus,
|
|
42
|
+
# for the drive cycles of the next host cycle.
|
|
43
43
|
def latch_host
|
|
44
|
-
@host = @bus
|
|
44
|
+
@host = @bus.host_lines
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# The C64's lines as last latched. The jumpers and the bus are the
|
|
@@ -55,10 +55,7 @@ module Badline
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Whether the C64 has moved ATN since the last latch_host.
|
|
58
|
-
def atn_moved?
|
|
59
|
-
lines = @bus ? @bus.host_lines : 0
|
|
60
|
-
(lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
|
|
61
|
-
end
|
|
58
|
+
def atn_moved? = (@bus.host_lines ^ @host).anybits?(IECBus::HOST_ATN_OUT)
|
|
62
59
|
|
|
63
60
|
def atn_low? = @bus.atn_low?(@host)
|
|
64
61
|
|
|
@@ -8,9 +8,8 @@ module Badline
|
|
|
8
8
|
# passes go in bulk, and the rest cycle by cycle, which may find the
|
|
9
9
|
# idle loop and put the drive back to sleep.
|
|
10
10
|
#
|
|
11
|
-
# The drive looks at ATN only when the
|
|
12
|
-
#
|
|
13
|
-
# that doesn't.
|
|
11
|
+
# The drive looks at ATN only when the machine may have moved it, as
|
|
12
|
+
# it pushes its lines into the bus (IECBus#host_lines=).
|
|
14
13
|
module Sleep
|
|
15
14
|
# Whether the drive is skipping its idle loop right now.
|
|
16
15
|
def asleep? = @asleep
|
|
@@ -40,10 +39,10 @@ module Badline
|
|
|
40
39
|
@wake_at = 0
|
|
41
40
|
end
|
|
42
41
|
|
|
43
|
-
# A host cycle asleep that may wake the drive. The
|
|
44
|
-
#
|
|
42
|
+
# A host cycle asleep that may wake the drive. The next ones need no
|
|
43
|
+
# look at ATN until the machine pushes its lines again.
|
|
45
44
|
def doze
|
|
46
|
-
@host_still =
|
|
45
|
+
@host_still = true
|
|
47
46
|
if @serial_port.atn_moved?
|
|
48
47
|
settle!
|
|
49
48
|
elsif @slept == @wake_at
|
data/lib/badline/drive1541.rb
CHANGED
|
@@ -149,8 +149,8 @@ module Badline
|
|
|
149
149
|
# The host has run this cycle already, and the drive sees what it did
|
|
150
150
|
# from the next one on (see SerialPort). Asleep, the drive owes the
|
|
151
151
|
# cycles instead, until ATN moves or its counters are due (see Idle).
|
|
152
|
-
# It looks at ATN only after the
|
|
153
|
-
#
|
|
152
|
+
# It looks at ATN only after the machine pushed its lines into the bus
|
|
153
|
+
# (see host_written!).
|
|
154
154
|
def host_cycle!
|
|
155
155
|
if @asleep
|
|
156
156
|
return if (@slept += 1) != @wake_at && @host_still
|
|
@@ -169,7 +169,7 @@ module Badline
|
|
|
169
169
|
plan_wake if @asleep
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
# The
|
|
172
|
+
# The machine pushed its lines into the bus, so ATN may have moved.
|
|
173
173
|
def host_written!
|
|
174
174
|
@host_still = false
|
|
175
175
|
end
|
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# Opens the window, then runs the machine a frame at a time: poll
|
|
6
|
-
# events, clock the frame's cycles, queue the
|
|
6
|
+
# events, clock the frame's cycles, queue the sound's samples, upload the
|
|
7
7
|
# changed lines, present and wait. Pacer decides the cycles and the wait.
|
|
8
8
|
class App
|
|
9
|
-
SCALE = 2
|
|
10
9
|
DROPFILE = 0x1000
|
|
11
|
-
TITLE = "Badline"
|
|
12
10
|
|
|
13
11
|
# Takes the frame limit, the pacing, the snapshot path, the sound and
|
|
14
12
|
# the verbosity from Options, and runs the timeline's events.
|
|
@@ -16,17 +14,21 @@ module Badline
|
|
|
16
14
|
@computer = computer
|
|
17
15
|
@frame_limit = options.frames
|
|
18
16
|
@verbose = options.verbose?
|
|
19
|
-
|
|
17
|
+
timing = computer.timing
|
|
18
|
+
@pacer = Pacer.new(paced: options.paced?, vsync: options.vsync?, verbose: @verbose, timing:)
|
|
20
19
|
@timeline = timeline
|
|
21
20
|
@snapshots = Snapshots.new(computer, options)
|
|
22
|
-
@screen = Screen.new(computer.
|
|
21
|
+
@screen = Screen.new(computer.video, timing.crop)
|
|
23
22
|
@led = DriveLed.for(computer)
|
|
24
23
|
@controls = Controls.new(computer)
|
|
25
|
-
|
|
26
|
-
@
|
|
24
|
+
@window = MachineWindow.new(@screen, timing.pixel_width, vsync: @pacer.vsync?)
|
|
25
|
+
@pacer.fit(@window.refresh_rate) if @pacer.vsync?
|
|
26
|
+
@sound = Sound.new(computer.sound_source, timing.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(@window.renderer), options, @snapshots)
|
|
30
|
+
@menu.center(@window.width, @window.height)
|
|
31
|
+
@scripted = false
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def run
|
|
@@ -39,7 +41,7 @@ module Badline
|
|
|
39
41
|
@computer.drive1541&.flush
|
|
40
42
|
@gamepads.close
|
|
41
43
|
@sound.close
|
|
42
|
-
|
|
44
|
+
@window.close
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
private
|
|
@@ -66,39 +68,24 @@ module Badline
|
|
|
66
68
|
@frame_report.add(stamps) if @verbose
|
|
67
69
|
@frames += 1
|
|
68
70
|
@timeline.run(@computer, @frames)
|
|
71
|
+
@snapshots.tick(@frames)
|
|
69
72
|
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
70
73
|
@pacer.check(Pacer::EARLY_CHECK, stamps.last - @started, stamps.last) if @frames == Pacer::EARLY_CHECK
|
|
71
74
|
report(stamps.last) if (@frames % 50).zero?
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
fit_display if @pacer.vsync?
|
|
87
|
-
SDL.SDL_RenderSetLogicalSize(@renderer, Screen::WIDTH, Screen::HEIGHT)
|
|
88
|
-
create_texture
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def create_texture
|
|
92
|
-
@texture = SDL.SDL_CreateTexture(
|
|
93
|
-
@renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, Screen::WIDTH, Screen::HEIGHT
|
|
94
|
-
)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def close_window
|
|
98
|
-
SDL.SDL_DestroyTexture(@texture)
|
|
99
|
-
SDL.SDL_DestroyRenderer(@renderer)
|
|
100
|
-
SDL.SDL_DestroyWindow(@window)
|
|
101
|
-
SDL.SDL_Quit
|
|
75
|
+
menu_events
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Opens the menu at a menu event, on the page it names, and closes it
|
|
79
|
+
# at a resume. While the timeline holds it open, its frames count
|
|
80
|
+
# towards the events after it and --frames.
|
|
81
|
+
def menu_events
|
|
82
|
+
if @timeline.menu?(@frames)
|
|
83
|
+
@menu.section = @timeline.menu_section(@frames)
|
|
84
|
+
open_menu
|
|
85
|
+
@scripted = true
|
|
86
|
+
elsif @menu.open? && @timeline.resume?(@frames)
|
|
87
|
+
resume(false)
|
|
88
|
+
end
|
|
102
89
|
end
|
|
103
90
|
|
|
104
91
|
def handle_events
|
|
@@ -136,11 +123,14 @@ module Badline
|
|
|
136
123
|
# it.
|
|
137
124
|
def swap(computer)
|
|
138
125
|
@computer = computer
|
|
139
|
-
|
|
126
|
+
timing = computer.timing
|
|
127
|
+
@screen = Screen.new(computer.video, timing.crop)
|
|
128
|
+
@window.fit(@screen, timing.pixel_width)
|
|
129
|
+
@menu.center(@window.width, @window.height)
|
|
140
130
|
@led = DriveLed.for(computer)
|
|
141
131
|
@controls.computer = computer
|
|
142
132
|
@gamepads.computer = computer
|
|
143
|
-
@sound.
|
|
133
|
+
@sound.switch(computer.sound_source, timing.clock_hz)
|
|
144
134
|
@snapshots.computer = computer
|
|
145
135
|
end
|
|
146
136
|
|
|
@@ -161,6 +151,7 @@ module Badline
|
|
|
161
151
|
# pressing the cartridge's freeze button if asked.
|
|
162
152
|
def resume(freeze)
|
|
163
153
|
@menu.close
|
|
154
|
+
@scripted = false
|
|
164
155
|
SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
|
|
165
156
|
@timeline.press_freeze(@computer, @frames) if freeze
|
|
166
157
|
@reported = now
|
|
@@ -169,50 +160,58 @@ module Badline
|
|
|
169
160
|
end
|
|
170
161
|
|
|
171
162
|
# Runs a frame of the menu, which has the keys and the mouse while the
|
|
172
|
-
# machine stands still.
|
|
163
|
+
# machine stands still. A menu the timeline opened counts its frames
|
|
164
|
+
# and takes their screenshots.
|
|
173
165
|
def paused_frame
|
|
174
|
-
|
|
166
|
+
scripted = @scripted
|
|
167
|
+
action = @menu.frame(@window.renderer, @window.texture, @controls)
|
|
168
|
+
present_menu(scripted)
|
|
175
169
|
if action == :quit
|
|
176
170
|
@running = false
|
|
177
171
|
elsif !action.nil?
|
|
178
172
|
swap(@menu.computer) if action == :swap
|
|
179
173
|
resume(action == :freeze)
|
|
180
174
|
end
|
|
175
|
+
finish_paused_frame if scripted && @running
|
|
181
176
|
end
|
|
182
177
|
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
SDL.
|
|
178
|
+
def present_menu(scripted)
|
|
179
|
+
renderer = @window.renderer
|
|
180
|
+
@timeline.screenshots(@frames + 1).each { |path| Screenshot.write(renderer, path) } if scripted
|
|
181
|
+
SDL.SDL_RenderPresent(renderer)
|
|
182
|
+
SDL.SDL_Delay(10)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def finish_paused_frame
|
|
186
|
+
@frames += 1
|
|
187
|
+
@timeline.run(@computer, @frames)
|
|
188
|
+
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
189
|
+
menu_events
|
|
188
190
|
end
|
|
189
191
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
@
|
|
192
|
+
def update_title
|
|
193
|
+
tags = []
|
|
194
|
+
tags << @controls.tag unless @controls.tag.empty?
|
|
195
|
+
tags << "MUTED" if @sound.muted?
|
|
196
|
+
@window.title(tags)
|
|
193
197
|
end
|
|
194
198
|
|
|
195
199
|
def emulate
|
|
196
|
-
computer
|
|
197
|
-
cycles = @pacer.cycles(@sound)
|
|
198
|
-
i = 0
|
|
199
|
-
while i < cycles
|
|
200
|
-
computer.cycle!
|
|
201
|
-
i += 1
|
|
202
|
-
end
|
|
200
|
+
@computer.run_cycles(@pacer.cycles(@sound))
|
|
203
201
|
end
|
|
204
202
|
|
|
205
203
|
def upload
|
|
206
204
|
@screen.update
|
|
207
|
-
|
|
205
|
+
@window.upload(@screen)
|
|
208
206
|
end
|
|
209
207
|
|
|
210
208
|
def draw
|
|
211
|
-
|
|
212
|
-
SDL.
|
|
213
|
-
|
|
214
|
-
@
|
|
215
|
-
|
|
209
|
+
renderer = @window.renderer
|
|
210
|
+
SDL.SDL_RenderClear(renderer)
|
|
211
|
+
SDL.SDL_RenderCopy(renderer, @window.texture, nil, nil)
|
|
212
|
+
@led&.draw(renderer, @window.width, @window.height)
|
|
213
|
+
@timeline.screenshots(@frames + 1).each { |path| Screenshot.write(renderer, path) }
|
|
214
|
+
SDL.SDL_RenderPresent(renderer)
|
|
216
215
|
end
|
|
217
216
|
|
|
218
217
|
def report(at)
|
|
@@ -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, Unsupported => e
|
|
24
25
|
warn "#{options.program}: #{options.media_path}: #{e.message}"
|
|
25
26
|
return 1
|
|
26
27
|
end
|
|
@@ -44,10 +45,11 @@ 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 family and model give the machine, and a .sid tune
|
|
49
|
+
# or --sid the SID. The VIC-20 raises Unsupported for a .sid
|
|
50
|
+
# tune.
|
|
48
51
|
def self.start(options, media, writable)
|
|
49
|
-
computer =
|
|
50
|
-
region: options.ntsc? ? Region::NTSC : Region::PAL)
|
|
52
|
+
computer = options.family == :vic20 ? vic20(options, media) : c64(options, media)
|
|
51
53
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
52
54
|
unless media.nil?
|
|
53
55
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
@@ -55,5 +57,33 @@ module Badline
|
|
|
55
57
|
end
|
|
56
58
|
computer
|
|
57
59
|
end
|
|
60
|
+
|
|
61
|
+
def self.c64(options, media)
|
|
62
|
+
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
63
|
+
Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A VIC-20 with the RAM --ram names, or else the RAM `media` needs
|
|
67
|
+
# (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
|
|
68
|
+
# media a VIC-20 doesn't take.
|
|
69
|
+
def self.vic20(options, media)
|
|
70
|
+
ram = options.ram
|
|
71
|
+
unless media.nil?
|
|
72
|
+
unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
73
|
+
raise Unsupported, "doesn't go in a VIC-20" if unsupported
|
|
74
|
+
|
|
75
|
+
ram ||= vic20_ram(media)
|
|
76
|
+
end
|
|
77
|
+
Machine.build(:vic20, model: options.model, ram:)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.vic20_ram(media)
|
|
81
|
+
ram = Media::Vic20Media.ram_for(media)
|
|
82
|
+
puts "RAM expansion: #{ram}"
|
|
83
|
+
ram
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Media the machine the options name doesn't take.
|
|
87
|
+
class Unsupported < ArgumentError; end
|
|
58
88
|
end
|
|
59
89
|
end
|
|
@@ -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
|
|
@@ -4,11 +4,11 @@ module Badline
|
|
|
4
4
|
module Frontend
|
|
5
5
|
# Routes host input to the machine. Two settings decide where it goes:
|
|
6
6
|
#
|
|
7
|
-
# - The keys: every host key goes to the
|
|
7
|
+
# - The keys: every host key goes to the machine's keyboard, or, as Tab
|
|
8
8
|
# switches, the arrow cluster and space drive one joystick and WASD
|
|
9
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
|
|
10
|
+
# #swap_ports swaps them. The VIC-20's one joystick takes both.
|
|
11
|
+
# - A pot device the pause menu plugs into a C64 control port: a 1351
|
|
12
12
|
# mouse or a pair of paddles, which the host mouse drives. Motion
|
|
13
13
|
# turns the paddles or moves the mouse, and the host's left and right
|
|
14
14
|
# buttons go to whichever lines the device puts them on.
|
|
@@ -65,7 +65,7 @@ module Badline
|
|
|
65
65
|
# The title bar's tag for the settings, empty for the keyboard alone.
|
|
66
66
|
def tag
|
|
67
67
|
tags = []
|
|
68
|
-
tags << "JOY #{@arrows_port}" if @joystick
|
|
68
|
+
tags << (@computer.family == :vic20 ? "JOY" : "JOY #{@arrows_port}") if @joystick
|
|
69
69
|
tags << TAGS[POTS.index(@pot)] if pot_device?
|
|
70
70
|
tags.join(", ")
|
|
71
71
|
end
|
|
@@ -105,7 +105,7 @@ module Badline
|
|
|
105
105
|
# RESTORE isn't in the key matrix, so it goes to the machine instead.
|
|
106
106
|
def c64_key(key, down)
|
|
107
107
|
if key == :restore
|
|
108
|
-
@computer.press_restore
|
|
108
|
+
down ? @computer.press_restore : @computer.release_restore
|
|
109
109
|
else
|
|
110
110
|
down ? @computer.keyboard.press(key) : @computer.keyboard.release(key)
|
|
111
111
|
end
|
|
@@ -113,9 +113,15 @@ module Badline
|
|
|
113
113
|
|
|
114
114
|
def paddles? = POTS.index(@pot) >= 3
|
|
115
115
|
|
|
116
|
-
# A fresh pot device on its port, and none on the other.
|
|
116
|
+
# A fresh pot device on its port, and none on the other. A machine
|
|
117
|
+
# without the ports takes none.
|
|
117
118
|
def attach_pot_device
|
|
118
119
|
ports = @computer.control_ports
|
|
120
|
+
if ports.nil?
|
|
121
|
+
@pot = :none
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
119
125
|
ports.device1 = nil
|
|
120
126
|
ports.device2 = nil
|
|
121
127
|
return unless pot_device?
|