badline 0.14.0 → 0.15.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 +23 -0
- data/CLAUDE.md +15 -5
- data/README.md +68 -8
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +188 -38
- data/doc/snapshots.md +18 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +2 -12
- data/lib/badline/c128/banks.rb +1 -3
- data/lib/badline/c128/bus.rb +19 -1
- data/lib/badline/c128/mmu_pages.rb +7 -0
- data/lib/badline/c64/bus/extra_sids.rb +80 -0
- data/lib/badline/c64/bus.rb +1 -0
- data/lib/badline/c64/cartridge/action_replay.rb +6 -2
- data/lib/badline/c64/cartridge/comal80.rb +1 -0
- data/lib/badline/c64/cartridge/dinamic.rb +3 -1
- data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
- data/lib/badline/c64/cartridge/fun_play.rb +2 -1
- data/lib/badline/c64/cartridge/game_system.rb +2 -1
- data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
- data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
- data/lib/badline/c64/cartridge/ocean.rb +1 -0
- data/lib/badline/c64/cartridge/pagefox.rb +1 -0
- data/lib/badline/c64/cartridge/rgcd.rb +2 -1
- data/lib/badline/c64/cartridge/super_games.rb +3 -2
- data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
- data/lib/badline/c64/cartridge.rb +3 -3
- data/lib/badline/c64/keyed_modules.rb +6 -2
- data/lib/badline/chips/ted/beam.rb +13 -14
- data/lib/badline/chips/ted/fetches.rb +199 -0
- data/lib/badline/chips/ted/keyed_state.rb +156 -0
- data/lib/badline/chips/ted/modes.rb +134 -0
- data/lib/badline/chips/ted/palette.rb +43 -0
- data/lib/badline/chips/ted/picture.rb +182 -0
- data/lib/badline/chips/ted/sound/output.rb +86 -0
- data/lib/badline/chips/ted/sound.rb +284 -0
- data/lib/badline/chips/ted.rb +53 -6
- data/lib/badline/chips/tia.rb +12 -0
- data/lib/badline/drives/datasette/keyed_state.rb +7 -2
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/frontend/boot.rb +3 -1
- data/lib/badline/frontend/controls.rb +7 -3
- data/lib/badline/frontend/keys.rb +14 -2
- data/lib/badline/frontend/media_slots.rb +11 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
- data/lib/badline/frontend/menu/menu_media.rb +13 -3
- data/lib/badline/frontend/menu/menu_pages.rb +8 -8
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/screen.rb +33 -12
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/sound.rb +4 -4
- data/lib/badline/frontend/timeline.rb +19 -6
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +26 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +30 -6
- data/lib/badline/options/table.rb +15 -2
- data/lib/badline/options/validation.rb +26 -4
- data/lib/badline/options.rb +37 -9
- data/lib/badline/plus4/bus.rb +77 -14
- data/lib/badline/plus4/cassette_sense.rb +21 -0
- data/lib/badline/plus4/keyboard_port.rb +35 -10
- data/lib/badline/plus4/keyed_modules.rb +99 -0
- data/lib/badline/plus4/keyed_parts.rb +143 -0
- data/lib/badline/plus4/keyed_state.rb +41 -0
- data/lib/badline/plus4.rb +92 -16
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
- data/lib/badline/snapshot/field_doc.rb +46 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +39 -0
- data/lib/badline/snapshot.rb +9 -4
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/storage/tap.rb +21 -12
- data/lib/badline/support/checkpoint.rb +3 -2
- data/lib/badline/support/plus4_checkpoint.rb +42 -0
- data/lib/badline/version.rb +1 -1
- metadata +15 -1
data/lib/badline/chips/tia.rb
CHANGED
|
@@ -44,6 +44,18 @@ module Badline
|
|
|
44
44
|
@ports[register] = value
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
# The ports, their directions and what the other side drives, in a
|
|
49
|
+
# keyed snapshot module (Snapshot::Keyed, BL-TIA1 and BL-TIA2).
|
|
50
|
+
def save_fields(out)
|
|
51
|
+
out.ints("ports", @ports).ints("directions", @directions).ints("inputs", @inputs)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def load_fields(input)
|
|
55
|
+
input.ints_into("ports", @ports)
|
|
56
|
+
input.ints_into("directions", @directions)
|
|
57
|
+
input.ints_into("inputs", @inputs)
|
|
58
|
+
end
|
|
47
59
|
end
|
|
48
60
|
end
|
|
49
61
|
end
|
|
@@ -6,11 +6,14 @@ module Badline
|
|
|
6
6
|
# The datasette's fields in a keyed snapshot module (Snapshot::Keyed,
|
|
7
7
|
# BL-TAPE): the keys, the motor, the countdown to the next pulse, and
|
|
8
8
|
# a tape in it under tape.*: its path, its bytes and how far it has
|
|
9
|
-
# played. A tape that isn't in leaves tape.* out.
|
|
10
|
-
#
|
|
9
|
+
# played. A tape that isn't in leaves tape.* out. A datasette that
|
|
10
|
+
# times the read line's level adds when in the countdown the line
|
|
11
|
+
# rises, and whether a recording's low half wave is on the tape. The
|
|
12
|
+
# sense and flag handlers don't fire on load.
|
|
11
13
|
module KeyedState
|
|
12
14
|
def save_fields(out)
|
|
13
15
|
out.bool("playing", @playing).bool("motor", @motor).int("countdown", @countdown)
|
|
16
|
+
out.int("rise_at", @rise_at).bool("low_recorded", @low_recorded) if @level
|
|
14
17
|
tape = @tape
|
|
15
18
|
return out unless tape
|
|
16
19
|
|
|
@@ -23,6 +26,8 @@ module Badline
|
|
|
23
26
|
@playing = input.bool("playing", @playing)
|
|
24
27
|
@motor = input.bool("motor", @motor)
|
|
25
28
|
@countdown = input.int("countdown", @countdown)
|
|
29
|
+
@rise_at = input.int("rise_at", @rise_at)
|
|
30
|
+
@low_recorded = input.bool("low_recorded", @low_recorded)
|
|
26
31
|
bytes = input.bytes("tape.bytes", "".b).bytes
|
|
27
32
|
return @tape = nil if bytes.empty?
|
|
28
33
|
|
|
@@ -27,6 +27,7 @@ module Badline
|
|
|
27
27
|
@motor = false
|
|
28
28
|
@countdown = 0
|
|
29
29
|
@rise_at = 0
|
|
30
|
+
@low_recorded = false
|
|
30
31
|
@flag_handler = nil
|
|
31
32
|
@sense_handler = nil
|
|
32
33
|
end
|
|
@@ -78,6 +79,7 @@ module Badline
|
|
|
78
79
|
press(true)
|
|
79
80
|
@recording = true
|
|
80
81
|
@since = 0
|
|
82
|
+
@low_recorded = false
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
def recording? = @recording
|
|
@@ -85,14 +87,21 @@ module Badline
|
|
|
85
87
|
# The machine's tape write line. While the deck records with its motor
|
|
86
88
|
# running, each rising edge ends a pulse on the tape, as long as the
|
|
87
89
|
# time since the edge before, and plays back as a falling edge on the
|
|
88
|
-
# read line.
|
|
90
|
+
# read line. A datasette built with +level: true+ records a version 2
|
|
91
|
+
# tape's half waves instead: a rising edge ends the low half and a
|
|
92
|
+
# falling edge the high half after it, so the tape plays back the
|
|
93
|
+
# levels it recorded (#read_high?).
|
|
89
94
|
def write_line=(high)
|
|
90
|
-
|
|
95
|
+
edge = high != @write_high
|
|
91
96
|
@write_high = high
|
|
92
|
-
return unless
|
|
97
|
+
return unless edge && @recording && running?
|
|
93
98
|
|
|
94
|
-
@tape.
|
|
95
|
-
|
|
99
|
+
if @level && @tape.half_waves?
|
|
100
|
+
record_half_wave(high)
|
|
101
|
+
elsif high
|
|
102
|
+
@tape.record_pulse(@since)
|
|
103
|
+
@since = 0
|
|
104
|
+
end
|
|
96
105
|
end
|
|
97
106
|
|
|
98
107
|
# Sets the write line's level without recording an edge, as a
|
|
@@ -153,6 +162,14 @@ module Badline
|
|
|
153
162
|
@countdown = first + @rise_at
|
|
154
163
|
end
|
|
155
164
|
|
|
165
|
+
# A high half wave goes on the tape only after the low half before it,
|
|
166
|
+
# so the tape starts with a whole pulse.
|
|
167
|
+
def record_half_wave(rose)
|
|
168
|
+
@tape.record_half_wave(@since) if rose || @low_recorded
|
|
169
|
+
@low_recorded = rose
|
|
170
|
+
@since = 0
|
|
171
|
+
end
|
|
172
|
+
|
|
156
173
|
def press(down)
|
|
157
174
|
return if down == @playing || !@connected
|
|
158
175
|
|
|
@@ -55,11 +55,13 @@ module Badline
|
|
|
55
55
|
|
|
56
56
|
# A new machine built from the options (Machine.for_options), with
|
|
57
57
|
# `media`, if not nil, attached and started as the command line does,
|
|
58
|
-
# its disk writable if `writable
|
|
58
|
+
# its disk writable if `writable`, after the Plus/4's cartridges the
|
|
59
|
+
# options put in its slots. The VIC-20 and the Plus/4 raise
|
|
59
60
|
# Machine::Unsupported for a .sid tune.
|
|
60
61
|
def self.start(options, media, writable)
|
|
61
62
|
computer = build(options, media)
|
|
62
63
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
64
|
+
options.cartridges.each { |slot, path| puts Media.attach(computer, path, cartridge: { slot: }) }
|
|
63
65
|
unless media.nil?
|
|
64
66
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
65
67
|
disk: { read_only: !writable })
|
|
@@ -117,7 +117,11 @@ module Badline
|
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def host_key(scancode)
|
|
120
|
-
@computer.family
|
|
120
|
+
case @computer.family
|
|
121
|
+
when :c128 then Keys.c128_key(scancode, @computer.mode)
|
|
122
|
+
when :plus4 then Keys.plus4_key(scancode)
|
|
123
|
+
else Keys.c64_key(scancode)
|
|
124
|
+
end
|
|
121
125
|
end
|
|
122
126
|
|
|
123
127
|
# RESTORE isn't in the key matrix, so it goes to the machine instead.
|
|
@@ -196,8 +200,8 @@ module Badline
|
|
|
196
200
|
end
|
|
197
201
|
|
|
198
202
|
def release_all
|
|
199
|
-
Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(
|
|
200
|
-
Keys::WASD.each_key { |scancode| @computer.keyboard.release(
|
|
203
|
+
Keys::ARROWS.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
|
|
204
|
+
Keys::WASD.each_key { |scancode| @computer.keyboard.release(host_key(scancode)) }
|
|
201
205
|
Keys::ARROWS.each_value do |direction|
|
|
202
206
|
@computer.joystick1.release(direction)
|
|
203
207
|
@computer.joystick2.release(direction)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
5
|
# SDL scancodes follow key positions on a US layout. They map onto the C64
|
|
6
|
-
# keys, the C128's own keys and, in joystick mode, the
|
|
7
|
-
# directions.
|
|
6
|
+
# keys, the C128's and the 264's own keys and, in joystick mode, the
|
|
7
|
+
# joystick directions.
|
|
8
8
|
module Keys
|
|
9
9
|
RETURN = 40
|
|
10
10
|
ESCAPE = 41
|
|
@@ -56,6 +56,15 @@ module Badline
|
|
|
56
56
|
63 => :forty_eighty, 72 => :forty_eighty, 78 => :run_stop
|
|
57
57
|
}.freeze
|
|
58
58
|
|
|
59
|
+
# The 264's keys where they differ from the C64's: ESC on Esc, with
|
|
60
|
+
# RUN/STOP on Page Down as on the C128, F1-F3 on F1-F3, HELP on F4,
|
|
61
|
+
# Insert or Help, a cursor key on each arrow, and either SHIFT on the
|
|
62
|
+
# one SHIFT. Page Up, RESTORE on the C64, presses nothing.
|
|
63
|
+
PLUS4_OTHERS = {
|
|
64
|
+
41 => :esc, 78 => :run_stop, 58 => :f1, 59 => :f2, 60 => :f3, 61 => :help, 73 => :help, 117 => :help,
|
|
65
|
+
79 => :crsr_right, 80 => :crsr_left, 81 => :crsr_down, 82 => :crsr_up, 229 => :lshift, 75 => :none
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
59
68
|
ARROWS = { 44 => :fire, 228 => :fire, 79 => :right, 80 => :left, 81 => :down, 82 => :up }.freeze
|
|
60
69
|
WASD = { 225 => :fire, 7 => :right, 4 => :left, 22 => :down, 26 => :up }.freeze
|
|
61
70
|
|
|
@@ -77,6 +86,9 @@ module Badline
|
|
|
77
86
|
C128_OTHERS[scancode] || c64_key(scancode)
|
|
78
87
|
end
|
|
79
88
|
|
|
89
|
+
# The 264's key for the scancode: one of PLUS4_OTHERS or the C64's.
|
|
90
|
+
def self.plus4_key(scancode) = PLUS4_OTHERS[scancode] || c64_key(scancode)
|
|
91
|
+
|
|
80
92
|
# The character the key types on a US layout: a letter, a capital
|
|
81
93
|
# with `shift`, or a digit, and "" for any other key.
|
|
82
94
|
def self.character(scancode, shift)
|
|
@@ -31,8 +31,18 @@ module Badline
|
|
|
31
31
|
drive.nil? ? computer.unmount : drive.insert(nil)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# Whether the expansion port has a cartridge in, on the Plus/4 in
|
|
35
|
+
# either cartridge's slots.
|
|
36
|
+
def cartridge?(computer)
|
|
37
|
+
return !computer.cartridge_slots.empty? if computer.family == :plus4
|
|
38
|
+
|
|
39
|
+
!computer.address_bus.cartridge.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Takes the cartridge out with the power off, on the Plus/4 both.
|
|
35
43
|
def remove_cartridge(computer)
|
|
44
|
+
return computer.detach_cartridges if computer.family == :plus4
|
|
45
|
+
|
|
36
46
|
computer.address_bus.detach_cartridge
|
|
37
47
|
computer.power_cycle!
|
|
38
48
|
end
|
|
@@ -37,7 +37,8 @@ module Badline
|
|
|
37
37
|
# :program, or :snapshot, which starts in the saves folder.
|
|
38
38
|
def browse(kind)
|
|
39
39
|
@browsing = kind
|
|
40
|
-
|
|
40
|
+
extensions = kind == :cartridge ? @media.cartridge_extensions : EXTENSIONS.fetch(kind)
|
|
41
|
+
@browser.open(kind == :snapshot ? Badline.data_folder("saves") : @browse_from, extensions)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# Asks for a save's name, starting from `name`.
|
|
@@ -87,6 +87,13 @@ module Badline
|
|
|
87
87
|
insert(:disk, set[index + step])
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# The extensions of the cartridges the machine takes: .crt, and on
|
|
91
|
+
# the Plus/4 raw .bin ROM images too.
|
|
92
|
+
def cartridge_extensions
|
|
93
|
+
extensions = Media::Extensions.of(%i[cartridge])
|
|
94
|
+
@computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
|
|
95
|
+
end
|
|
96
|
+
|
|
90
97
|
# Swaps the cartridge for the one at `path`, which power cycles the
|
|
91
98
|
# machine, and says whether it went in.
|
|
92
99
|
def attach_cartridge(path)
|
|
@@ -95,10 +102,11 @@ module Badline
|
|
|
95
102
|
|
|
96
103
|
# What a save of the machine is named after: the cartridge, the disk,
|
|
97
104
|
# the tape, or BASIC, without the bracketed parts of a file's name. A
|
|
98
|
-
# VIC-20's are named after its disk, or else the machine
|
|
105
|
+
# VIC-20's are named after its disk, or else the machine, and a
|
|
106
|
+
# Plus/4's after its disk or tape.
|
|
99
107
|
def game_name
|
|
100
108
|
vic20 = @computer.family == :vic20
|
|
101
|
-
name = vic20 ? "" : cartridge_name
|
|
109
|
+
name = vic20 || @computer.family == :plus4 ? "" : cartridge_name
|
|
102
110
|
disk = disk_path
|
|
103
111
|
name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
|
|
104
112
|
name = tape_name if name.empty? && !vic20
|
|
@@ -114,8 +122,10 @@ module Badline
|
|
|
114
122
|
end
|
|
115
123
|
|
|
116
124
|
# The name the cartridge in the expansion port gives itself, or an
|
|
117
|
-
# empty one.
|
|
125
|
+
# empty one. A Plus/4's names the slots with a ROM in them.
|
|
118
126
|
def cartridge_name
|
|
127
|
+
return @computer.cartridge_slots.map(&:upcase).join(", ") if @computer.family == :plus4
|
|
128
|
+
|
|
119
129
|
cartridge = @computer.address_bus.cartridge
|
|
120
130
|
cartridge.nil? ? "" : cartridge.name
|
|
121
131
|
end
|
|
@@ -89,11 +89,7 @@ module Badline
|
|
|
89
89
|
|
|
90
90
|
def vic20? = @computer.family == :vic20
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
# shows.
|
|
94
|
-
def note
|
|
95
|
-
@painter.text(@left, @top, "NOT ON THE VIC-20 YET", MenuTheme::DIM)
|
|
96
|
-
end
|
|
92
|
+
def plus4? = @computer.family == :plus4
|
|
97
93
|
|
|
98
94
|
def draw_drive
|
|
99
95
|
media = @media
|
|
@@ -128,10 +124,12 @@ module Badline
|
|
|
128
124
|
def draw_expansion
|
|
129
125
|
return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
|
|
127
|
+
info("CARTRIDGE", @media.cartridge_name)
|
|
133
128
|
row("INSERT...", :insert_cartridge)
|
|
134
129
|
row("REMOVE", :remove_cartridge)
|
|
130
|
+
return if plus4?
|
|
131
|
+
|
|
132
|
+
cartridge = @computer.address_bus.cartridge
|
|
135
133
|
row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer? && @computer.family == :c64
|
|
136
134
|
return if @reu_kb.nil?
|
|
137
135
|
|
|
@@ -167,9 +165,11 @@ module Badline
|
|
|
167
165
|
end
|
|
168
166
|
end
|
|
169
167
|
|
|
170
|
-
# The C128 names its model and the mode it runs in first
|
|
168
|
+
# The C128 names its model and the mode it runs in first, and the
|
|
169
|
+
# Plus/4 its model and RAM.
|
|
171
170
|
def draw_power
|
|
172
171
|
info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
|
|
172
|
+
info("MACHINE", @computer.model_label) if plus4?
|
|
173
173
|
row("RESET", :reset)
|
|
174
174
|
row("POWER CYCLE", :power_cycle)
|
|
175
175
|
skip
|
|
@@ -5,7 +5,7 @@ module Badline
|
|
|
5
5
|
# The pause menu's PORTS page, which MenuPages draws: what each C64
|
|
6
6
|
# control port has plugged in, a joystick or a pot device, and whether
|
|
7
7
|
# the keys type or drive the joysticks. The VIC-20 has one port, a
|
|
8
|
-
# joystick's.
|
|
8
|
+
# joystick's, and the Plus/4 two joysticks and no pot devices.
|
|
9
9
|
module MenuPorts
|
|
10
10
|
PORT_DEVICES = %i[joystick mouse paddles].freeze
|
|
11
11
|
PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
|
|
@@ -16,6 +16,7 @@ module Badline
|
|
|
16
16
|
|
|
17
17
|
def draw_ports
|
|
18
18
|
return draw_vic20_port if @computer.family == :vic20
|
|
19
|
+
return draw_plus4_ports if @computer.family == :plus4
|
|
19
20
|
|
|
20
21
|
toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
|
|
21
22
|
toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
|
|
@@ -37,6 +38,17 @@ module Badline
|
|
|
37
38
|
info("WASD, LEFT SHIFT", "JOYSTICK")
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
# The Plus/4's two joysticks, one for each set of keys.
|
|
42
|
+
def draw_plus4_ports
|
|
43
|
+
return unless draw_keys("PLUS/4")
|
|
44
|
+
|
|
45
|
+
row("SWAP JOYSTICKS", :swap)
|
|
46
|
+
skip
|
|
47
|
+
arrows = @controls.arrows_port
|
|
48
|
+
info("ARROWS, SPACE", "JOYSTICK #{arrows}")
|
|
49
|
+
info("WASD, LEFT SHIFT", "JOYSTICK #{3 - arrows}")
|
|
50
|
+
end
|
|
51
|
+
|
|
40
52
|
# The toggle between the keyboard named and the joysticks, and
|
|
41
53
|
# whether the keys drive the joysticks.
|
|
42
54
|
def draw_keys(keyboard)
|
|
@@ -4,9 +4,10 @@ module Badline
|
|
|
4
4
|
module Frontend
|
|
5
5
|
# The visible part of the video chip's display, the crop of the
|
|
6
6
|
# machine's timing, repacked for an SDL texture in XRGB8888 in the
|
|
7
|
-
# chip's palette
|
|
8
|
-
# words, as badline/ffi does on CRuby, so
|
|
9
|
-
# neighbouring pixels, the left one in the low
|
|
7
|
+
# chip's palette, of 16 colours or the TED's 128. Spinel hands an Array
|
|
8
|
+
# of Integers to C as 64-bit words, as badline/ffi does on CRuby, so
|
|
9
|
+
# each word carries two neighbouring pixels, the left one in the low
|
|
10
|
+
# half.
|
|
10
11
|
#
|
|
11
12
|
# Each pixel shows `pixel_width` of the window's square pixels wide,
|
|
12
13
|
# or with `dots`, that many pixels share one, as the VDC's dots do. The
|
|
@@ -14,18 +15,21 @@ module Badline
|
|
|
14
15
|
# least MIN_WIDTH window pixels wide, and at least as high as PAL's C64
|
|
15
16
|
# crop, 272 lines, so a C64 shows in the same window whatever its
|
|
16
17
|
# region. A crop with fewer lines or pixels sits in the middle of the
|
|
17
|
-
# texture, between black bands. The VIC-20's 284 by 284 crop
|
|
18
|
-
# own.
|
|
18
|
+
# texture, between black bands. The VIC-20's 284 by 284 crop and the
|
|
19
|
+
# TED's 384 by 288 fill their own.
|
|
20
|
+
#
|
|
21
|
+
# With `frame_lines`, the crop's lines run on past the frame's last
|
|
22
|
+
# line to its first, as the TED's do: its top border is the end of
|
|
23
|
+
# the frame.
|
|
19
24
|
class Screen
|
|
20
25
|
MIN_WIDTH = Region::PAL.crop[2]
|
|
21
26
|
MIN_HEIGHT = Region::PAL.crop[3]
|
|
22
27
|
|
|
23
28
|
attr_reader :pixels, :width, :height
|
|
24
29
|
|
|
25
|
-
def initialize(video, crop, pixel_width: 1, dots: 1)
|
|
30
|
+
def initialize(video, crop, pixel_width: 1, dots: 1, frame_lines: 0)
|
|
26
31
|
@video = video
|
|
27
32
|
@col_offset = crop[0]
|
|
28
|
-
@row_offset = crop[1]
|
|
29
33
|
@lines = crop[3]
|
|
30
34
|
@columns = crop[2] & ~1
|
|
31
35
|
@pixel_width = pixel_width
|
|
@@ -37,8 +41,10 @@ module Badline
|
|
|
37
41
|
@pair_count = @columns / 2
|
|
38
42
|
@margin = (@row_words - @pair_count) / 2
|
|
39
43
|
@band = (@height - @lines) / 2
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
top = crop[1]
|
|
45
|
+
wrap = frame_lines.zero? ? top + @lines : frame_lines
|
|
46
|
+
@sources = Array.new(@lines) { |row| (row + top) % wrap }
|
|
47
|
+
pair_up(video.palette)
|
|
42
48
|
@pixels = Array.new(@height * @row_words, 0)
|
|
43
49
|
pack_all
|
|
44
50
|
end
|
|
@@ -54,9 +60,10 @@ module Badline
|
|
|
54
60
|
video = @video
|
|
55
61
|
dirty = video.dirty_lines
|
|
56
62
|
display = video.display
|
|
63
|
+
sources = @sources
|
|
57
64
|
row = 0
|
|
58
65
|
while row < @lines
|
|
59
|
-
pack_row(display, row) if dirty[row
|
|
66
|
+
pack_row(display, row) if dirty[sources[row]]
|
|
60
67
|
row += 1
|
|
61
68
|
end
|
|
62
69
|
video.clear_dirty_lines!
|
|
@@ -64,6 +71,18 @@ module Badline
|
|
|
64
71
|
|
|
65
72
|
private
|
|
66
73
|
|
|
74
|
+
# The packed word for each pair of palette indices, the left one in
|
|
75
|
+
# the low bits.
|
|
76
|
+
def pair_up(palette)
|
|
77
|
+
colors = palette.size
|
|
78
|
+
shift = 4
|
|
79
|
+
shift += 1 while (1 << shift) < colors
|
|
80
|
+
mask = colors - 1
|
|
81
|
+
@shift = shift
|
|
82
|
+
@mask = mask
|
|
83
|
+
@pairs = Array.new(colors << shift) { |pair| palette[pair & mask] | (palette[(pair >> shift) & mask] << 32) }
|
|
84
|
+
end
|
|
85
|
+
|
|
67
86
|
# Repacks every line, as a screen built for a display the chip has
|
|
68
87
|
# already drawn starts with all of it.
|
|
69
88
|
def pack_all
|
|
@@ -78,11 +97,13 @@ module Badline
|
|
|
78
97
|
def pack_row(display, row)
|
|
79
98
|
pixels = @pixels
|
|
80
99
|
pairs = @pairs
|
|
81
|
-
|
|
100
|
+
mask = @mask
|
|
101
|
+
shift = @shift
|
|
102
|
+
from = (@sources[row] * @stride) + @col_offset
|
|
82
103
|
to = ((row + @band) * @row_words) + @margin
|
|
83
104
|
last = to + @pair_count
|
|
84
105
|
while to < last
|
|
85
|
-
pixels[to] = pairs[(display[from] &
|
|
106
|
+
pixels[to] = pairs[(display[from] & mask) | ((display[from + 1] & mask) << shift)]
|
|
86
107
|
from += 2
|
|
87
108
|
to += 1
|
|
88
109
|
end
|
|
@@ -70,7 +70,8 @@ module Badline
|
|
|
70
70
|
@screen = Screen.new(vdc, vdc.crop, dots: 2)
|
|
71
71
|
else
|
|
72
72
|
timing = @computer.timing
|
|
73
|
-
@screen = Screen.new(@computer.video, timing.crop,
|
|
73
|
+
@screen = Screen.new(@computer.video, timing.crop,
|
|
74
|
+
pixel_width: timing.pixel_width, frame_lines: timing.lines_per_frame)
|
|
74
75
|
end
|
|
75
76
|
end
|
|
76
77
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Frontend
|
|
5
|
-
# The machine's sound source, the C64's SID
|
|
6
|
-
# SDL's audio queue, as mono signed 16-bit samples.
|
|
7
|
-
# #record(rate:, clock_hz:) and #drain_samples.
|
|
5
|
+
# The machine's sound source, the C64's SID, the VIC-20's VIC or the
|
|
6
|
+
# Plus/4's TED, on SDL's audio queue, as mono signed 16-bit samples.
|
|
7
|
+
# Each takes #record(rate:, clock_hz:) and #drain_samples.
|
|
8
8
|
# Once the device plays, it is the clock: #wait holds each frame until
|
|
9
9
|
# the queue is down to `AHEAD`, so the machine runs at the device's pace.
|
|
10
10
|
#
|
|
@@ -16,7 +16,7 @@ module Badline
|
|
|
16
16
|
#
|
|
17
17
|
# Each frame's samples go to a mono AudioDevice. The chip's cycles are
|
|
18
18
|
# converted at `clock_hz`, the machine's clock, so an NTSC or Drean
|
|
19
|
-
# machine plays at its own pitch.
|
|
19
|
+
# machine plays at its own pitch, and the TED's at its single clock.
|
|
20
20
|
class Sound
|
|
21
21
|
RATE = 44_100
|
|
22
22
|
AHEAD = 0.08
|
|
@@ -38,17 +38,30 @@ module Badline
|
|
|
38
38
|
# sound. An empty one is its first.
|
|
39
39
|
def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
|
|
40
40
|
|
|
41
|
+
# Whether the family's machine has a key of the name. The Plus/4 has
|
|
42
|
+
# no RESTORE.
|
|
41
43
|
def self.key?(name, family)
|
|
42
|
-
name == "restore" || (family == :c128 && LOCKS.include?(name)) ||
|
|
44
|
+
(name == "restore" && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
|
|
43
45
|
!keyboard_key(name, family).nil? || !joystick_direction(name).nil?
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# The family's Keyboard key for a name such as space, return or a,
|
|
47
|
-
#
|
|
49
|
+
# on the C128 esc or keypad1, or on the Plus/4 help or crsr_up. The
|
|
50
|
+
# shifted cursor keys, such as cursor_up, are the C64's and the
|
|
51
|
+
# C128's.
|
|
48
52
|
def self.keyboard_key(name, family)
|
|
49
|
-
|
|
50
|
-
key
|
|
51
|
-
|
|
53
|
+
key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
|
|
54
|
+
return key unless key.nil? && family != :plus4
|
|
55
|
+
|
|
56
|
+
Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.keyboard_matrix(family)
|
|
60
|
+
case family
|
|
61
|
+
when :c128 then C128::KEYBOARD_MATRIX
|
|
62
|
+
when :plus4 then Plus4::KeyboardPort::MATRIX
|
|
63
|
+
else Input::Keyboard::C64_MATRIX
|
|
64
|
+
end
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
# The direction of joy1-NAME or joy2-NAME, such as up or fire.
|
|
@@ -215,7 +228,7 @@ module Badline
|
|
|
215
228
|
|
|
216
229
|
def inserted?(computer, what)
|
|
217
230
|
return !computer.datasette.tape.nil? if what == "tape"
|
|
218
|
-
return
|
|
231
|
+
return MediaSlots.cartridge?(computer) if what == "cartridge"
|
|
219
232
|
|
|
220
233
|
MediaSlots.disk?(computer)
|
|
221
234
|
end
|
|
@@ -4,7 +4,7 @@ module Badline
|
|
|
4
4
|
module Machine
|
|
5
5
|
# Brings the machine to the point a snapshot file is saved at: the main
|
|
6
6
|
# CPU between instructions, or jammed, with no REU transfer holding the
|
|
7
|
-
# bus. C64, Vic20 and
|
|
7
|
+
# bus. C64, Vic20, C128 and Plus4 each include it, and answer
|
|
8
8
|
# #settled_for_snapshot? and #settle_chips_for_snapshot, which brings
|
|
9
9
|
# what each chip defers up to the cycle without running one.
|
|
10
10
|
module Settling
|
data/lib/badline/machine.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Badline
|
|
|
9
9
|
#
|
|
10
10
|
# The machines badline builds by family, as the command line's
|
|
11
11
|
# subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, and the
|
|
12
|
-
# C128 in C64 mode, C128.
|
|
12
|
+
# C128 in C64 mode, C128, and the 264 series, Plus4.
|
|
13
13
|
#
|
|
14
14
|
# A machine answers what the front end, Media, the runners and the
|
|
15
15
|
# snapshots call on it:
|
|
@@ -49,22 +49,31 @@ module Badline
|
|
|
49
49
|
# +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
|
|
50
50
|
# C128D's 1571, its true drive.
|
|
51
51
|
#
|
|
52
|
+
# The Plus/4 answers the identity, clocking, lifecycle, video, sound and
|
|
53
|
+
# input parts, with nil for +control_ports+ and no RESTORE, and of the
|
|
54
|
+
# rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+, +mounted?+,
|
|
55
|
+
# +datasette+, +attach_cartridge+, +attach_drive1541+, +drive1541+,
|
|
56
|
+
# +true_drive+, +plug_true_drive+, the 1581's, +install_debug_register+
|
|
57
|
+
# and +capture_output+, and besides +model_label+, +cartridge_slots+ and
|
|
58
|
+
# +detach_cartridges+ for its two cartridges.
|
|
59
|
+
#
|
|
52
60
|
# Each machine includes Machine::Clocking for the clocking part.
|
|
53
61
|
module Machine
|
|
54
62
|
# Media the machine named doesn't take.
|
|
55
63
|
class Unsupported < ArgumentError; end
|
|
56
64
|
|
|
57
|
-
# A new machine of `family`, :c64, :vic20 or :
|
|
58
|
-
# `model` names. A C64's model is one of Model::ALL, with its SID
|
|
65
|
+
# A new machine of `family`, :c64, :vic20, :c128 or :plus4, the model
|
|
66
|
+
# of it `model` names. A C64's model is one of Model::ALL, with its SID
|
|
59
67
|
# `sid_model` when not nil and an REU of `reu` K when not nil. A
|
|
60
68
|
# VIC-20's is "pal", with the RAM expansion `ram` names
|
|
61
69
|
# (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
|
|
62
70
|
# one of C128::Model::ALL, with its SID `sid_model` when not nil, built
|
|
63
|
-
# for C64 mode.
|
|
71
|
+
# for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
|
|
72
|
+
# the model's own RAM.
|
|
64
73
|
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
65
74
|
return vic20(model, ram) if family == :vic20
|
|
66
75
|
return c128(model, sid_model, :c64) if family == :c128
|
|
67
|
-
|
|
76
|
+
return Plus4.new(model:) if family == :plus4
|
|
68
77
|
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
69
78
|
|
|
70
79
|
profile = Model.named(model)
|
|
@@ -80,10 +89,11 @@ module Badline
|
|
|
80
89
|
# only C64 mode runs: a .sid tune or a program that loads at the C64's
|
|
81
90
|
# BASIC start. A VIC-20 without RAM named gets the expansion the media
|
|
82
91
|
# needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
|
|
83
|
-
# Unsupported for media a VIC-20 doesn't take.
|
|
92
|
+
# Unsupported for media a VIC-20 or a Plus/4 doesn't take.
|
|
84
93
|
def self.for_options(options, media, &)
|
|
85
94
|
family = options.family
|
|
86
95
|
return vic20_for(media, options.model, options.ram, &) if family == :vic20
|
|
96
|
+
return plus4_for(media, options.model, options.ram_kb) if family == :plus4
|
|
87
97
|
|
|
88
98
|
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
89
99
|
return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
|
|
@@ -117,6 +127,16 @@ module Badline
|
|
|
117
127
|
end
|
|
118
128
|
private_class_method :vic20_for
|
|
119
129
|
|
|
130
|
+
# A Plus/4 with `ram_kb` K of RAM (Plus4::Model::RAM_SIZES), or with
|
|
131
|
+
# the model's own when nil.
|
|
132
|
+
def self.plus4_for(media, model, ram_kb)
|
|
133
|
+
unsupported = !media.nil? && Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
134
|
+
raise Unsupported, "doesn't go in a Plus/4" if unsupported
|
|
135
|
+
|
|
136
|
+
Plus4.new(model:, ram: ram_kb)
|
|
137
|
+
end
|
|
138
|
+
private_class_method :plus4_for
|
|
139
|
+
|
|
120
140
|
def self.c64_media?(media)
|
|
121
141
|
return false if media.nil? || !File.file?(media)
|
|
122
142
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# What the 264 series does with media of its own (Media.attach). A
|
|
6
|
+
# cartridge is a PLUS4 CARTRIDGE .crt, or a raw ROM image (.bin) that
|
|
7
|
+
# goes in the slot it's given, or by its size in cartridge 1's.
|
|
8
|
+
module Plus4Media
|
|
9
|
+
# The platform byte of a C16 or Plus/4 tape (Storage::TAP#platform).
|
|
10
|
+
TAPE_PLATFORM = 2
|
|
11
|
+
|
|
12
|
+
# The extension of a raw cartridge ROM image.
|
|
13
|
+
ROM_IMAGE = ".bin"
|
|
14
|
+
|
|
15
|
+
# The cartridge slots by xplus4's names, each as the bank and address
|
|
16
|
+
# a .crt's CHIP packet gives it (Plus4::Bus#attach_cartridge).
|
|
17
|
+
SLOTS = { "c1lo" => [0, 0x8000], "c1hi" => [0, 0xc000], "c2lo" => [1, 0x8000], "c2hi" => [1, 0xc000] }.freeze
|
|
18
|
+
|
|
19
|
+
SLOT_SIZE = 0x4000
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# A tape made for another machine plays all the same, and says so.
|
|
23
|
+
def tape_message(tape, message)
|
|
24
|
+
tape.platform == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the Plus/4"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Whether the file at `path` is a raw cartridge ROM image.
|
|
28
|
+
def rom_image?(path) = File.extname(path).downcase == ROM_IMAGE
|
|
29
|
+
|
|
30
|
+
# The ROM chips, as Storage::CRTFile::Chips, of the cartridge at
|
|
31
|
+
# `path`: a .crt's, or a raw image's (#rom_chips).
|
|
32
|
+
def cartridge_chips(path, slot: nil)
|
|
33
|
+
return rom_chips(File.binread(path).bytes, slot:) if rom_image?(path)
|
|
34
|
+
|
|
35
|
+
Storage::CRTFile.new(path, machine: :plus4).chips
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A raw ROM image's chips: one in `slot`, one of SLOTS, when it's
|
|
39
|
+
# named. Otherwise up to 16K goes in cartridge 1's low slot, and
|
|
40
|
+
# up to 32K fills its high slot too.
|
|
41
|
+
def rom_chips(bytes, slot: nil)
|
|
42
|
+
return [slot_chip(bytes, slot)] if slot
|
|
43
|
+
|
|
44
|
+
size = bytes.length
|
|
45
|
+
unless size.between?(1, 2 * SLOT_SIZE)
|
|
46
|
+
raise Machine::Unsupported, "a #{size}-byte image is no Plus/4 cartridge"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
chips = [chip(0, 0x8000, bytes[0, SLOT_SIZE])]
|
|
50
|
+
chips << chip(0, 0xc000, bytes[SLOT_SIZE..]) if size > SLOT_SIZE
|
|
51
|
+
chips
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def slot_chip(bytes, slot)
|
|
57
|
+
bank, address = SLOTS.fetch(slot) { raise ArgumentError, "no Plus/4 cartridge slot named #{slot}" }
|
|
58
|
+
size = bytes.length
|
|
59
|
+
raise Machine::Unsupported, "a #{size}-byte image doesn't fit a 16K slot" unless size.between?(1, SLOT_SIZE)
|
|
60
|
+
|
|
61
|
+
chip(bank, address, bytes)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def chip(bank, address, data) = Storage::CRTFile::Chip.new(chip_type: 0, bank:, address:, data:)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|