badline 0.14.0 → 0.15.1
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 +86 -0
- data/CLAUDE.md +22 -5
- data/README.md +101 -11
- data/doc/media.md +19 -0
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +244 -46
- data/doc/snapshots.md +18 -1
- data/exe/badline-ruby +1 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +4 -26
- 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/c64/true_drives.rb +13 -1
- data/lib/badline/c64.rb +6 -0
- 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 +28 -0
- data/lib/badline/chips/vic/register_log.rb +4 -1
- data/lib/badline/drives/datasette/keyed_state.rb +14 -4
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/drives/engine/core.rb +6 -14
- data/lib/badline/drives/engine/idle.rb +42 -4
- data/lib/badline/drives/engine/idle_cpu.rb +18 -0
- data/lib/badline/drives/engine/serial_connection.rb +25 -0
- data/lib/badline/drives/engine/sleep.rb +1 -0
- data/lib/badline/drives/engine.rb +1 -0
- data/lib/badline/drives/model1541/byte_ready.rb +19 -3
- data/lib/badline/drives/model1541/mechanism.rb +10 -1
- data/lib/badline/drives/model1541.rb +1 -0
- data/lib/badline/drives/model1551/bus.rb +138 -0
- data/lib/badline/drives/model1551/cable.rb +104 -0
- data/lib/badline/drives/model1551/cpu.rb +14 -0
- data/lib/badline/drives/model1551/keyed_state.rb +65 -0
- data/lib/badline/drives/model1551/port.rb +74 -0
- data/lib/badline/drives/model1551/tia.rb +92 -0
- data/lib/badline/drives/model1551.rb +170 -0
- data/lib/badline/drives/model1571.rb +1 -0
- data/lib/badline/drives/model1581.rb +1 -0
- data/lib/badline/frontend/boot.rb +23 -2
- 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 +14 -5
- data/lib/badline/frontend/menu/menu_media.rb +102 -14
- data/lib/badline/frontend/menu/menu_pages.rb +16 -11
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/player/player_controller.rb +11 -4
- 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 +30 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media/true_drive.rb +7 -5
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +32 -6
- data/lib/badline/options/option.rb +2 -1
- data/lib/badline/options/table.rb +29 -4
- data/lib/badline/options/validation.rb +66 -4
- data/lib/badline/options.rb +71 -11
- data/lib/badline/plus4/acia/frames.rb +55 -0
- data/lib/badline/plus4/acia.rb +276 -16
- data/lib/badline/plus4/bus.rb +245 -46
- 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 +116 -0
- data/lib/badline/plus4/keyed_parts.rb +172 -0
- data/lib/badline/plus4/keyed_state.rb +44 -0
- data/lib/badline/plus4/ram_expansion.rb +145 -0
- data/lib/badline/plus4/sid_card.rb +95 -0
- data/lib/badline/plus4/sound_mix.rb +51 -0
- data/lib/badline/plus4/tcbm_drive.rb +120 -0
- data/lib/badline/plus4/user_port.rb +11 -2
- data/lib/badline/plus4.rb +184 -26
- data/lib/badline/roms/README +9 -0
- data/lib/badline/roms/dos1551.rom +0 -0
- 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 +48 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +81 -0
- data/lib/badline/snapshot.rb +9 -4
- 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 +29 -1
|
@@ -10,17 +10,29 @@ module Badline
|
|
|
10
10
|
# wrong for the page to show.
|
|
11
11
|
class MenuMedia
|
|
12
12
|
attr_reader :problem
|
|
13
|
-
attr_writer :computer
|
|
14
13
|
|
|
15
14
|
# Takes the disk set from the command line's `media_path` when it's a
|
|
16
|
-
# list
|
|
17
|
-
|
|
15
|
+
# list, and the names of the cartridges it and `cartridges`, the
|
|
16
|
+
# Plus/4's ROM images by slot (Options#cartridges), put in the first
|
|
17
|
+
# machine.
|
|
18
|
+
def initialize(media_path, writable, cartridges)
|
|
18
19
|
@list = Media::DiskList.list?(media_path) ? media_path : ""
|
|
19
20
|
@writable = writable
|
|
20
21
|
@set_path = ""
|
|
21
22
|
@set = []
|
|
22
23
|
@problem = ""
|
|
23
24
|
@computer = nil
|
|
25
|
+
@command_line = cartridges.map { |slot, path| [slot, path] } + [["", media_path]]
|
|
26
|
+
@names = {}
|
|
27
|
+
@named = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def computer=(computer)
|
|
31
|
+
@computer = computer
|
|
32
|
+
return unless @named.nil?
|
|
33
|
+
|
|
34
|
+
@command_line.each { |slot, path| note_cartridge(computer, path.dup, slot) }
|
|
35
|
+
@named = computer
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def writable? = @writable
|
|
@@ -87,18 +99,28 @@ module Badline
|
|
|
87
99
|
insert(:disk, set[index + step])
|
|
88
100
|
end
|
|
89
101
|
|
|
102
|
+
# The extensions of the cartridges the machine takes: .crt, and on
|
|
103
|
+
# the Plus/4 raw .bin ROM images too.
|
|
104
|
+
def cartridge_extensions
|
|
105
|
+
extensions = Media::Extensions.of(%i[cartridge])
|
|
106
|
+
@computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
|
|
107
|
+
end
|
|
108
|
+
|
|
90
109
|
# Swaps the cartridge for the one at `path`, which power cycles the
|
|
91
110
|
# machine, and says whether it went in.
|
|
92
111
|
def attach_cartridge(path)
|
|
93
|
-
|
|
112
|
+
attached = cartridge_succeeds? { Media.attach(@computer, path) }
|
|
113
|
+
note_cartridge(@computer, path, "") if attached
|
|
114
|
+
attached
|
|
94
115
|
end
|
|
95
116
|
|
|
96
117
|
# What a save of the machine is named after: the cartridge, the disk,
|
|
97
118
|
# 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
|
|
119
|
+
# VIC-20's are named after its disk, or else the machine, and a
|
|
120
|
+
# Plus/4's after its disk or tape.
|
|
99
121
|
def game_name
|
|
100
122
|
vic20 = @computer.family == :vic20
|
|
101
|
-
name = vic20 ? "" : cartridge_name
|
|
123
|
+
name = vic20 || @computer.family == :plus4 ? "" : cartridge_name
|
|
102
124
|
disk = disk_path
|
|
103
125
|
name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
|
|
104
126
|
name = tape_name if name.empty? && !vic20
|
|
@@ -114,24 +136,36 @@ module Badline
|
|
|
114
136
|
end
|
|
115
137
|
|
|
116
138
|
# The name the cartridge in the expansion port gives itself, or an
|
|
117
|
-
# empty one.
|
|
139
|
+
# empty one. A VIC-20's or a Plus/4's is the name of the file it came
|
|
140
|
+
# from: a .crt's own, or else the file's. A Plus/4 names each of its
|
|
141
|
+
# cartridges with a ROM in, and a slot filled from elsewhere, such as
|
|
142
|
+
# a snapshot, by the slot.
|
|
118
143
|
def cartridge_name
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
case @computer.family
|
|
145
|
+
when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
|
|
146
|
+
when :vic20 then named? ? @names.fetch("", "") : ""
|
|
147
|
+
else
|
|
148
|
+
cartridge = @computer.address_bus.cartridge
|
|
149
|
+
cartridge.nil? ? "" : cartridge.name
|
|
150
|
+
end
|
|
121
151
|
end
|
|
122
152
|
|
|
123
153
|
# Takes the cartridge out with the power off.
|
|
124
|
-
def remove_cartridge
|
|
154
|
+
def remove_cartridge
|
|
155
|
+
@names = {}
|
|
156
|
+
MediaSlots.remove_cartridge(@computer)
|
|
157
|
+
end
|
|
125
158
|
|
|
126
159
|
# A new machine started on `path`, as the command line would, or nil
|
|
127
160
|
# when it won't start.
|
|
128
161
|
def start(options, path)
|
|
129
162
|
computer = nil
|
|
130
|
-
|
|
131
|
-
computer =
|
|
132
|
-
|
|
163
|
+
if Media.kind(@computer, path) == :cartridge
|
|
164
|
+
cartridge_succeeds? { computer = started(options, path) }
|
|
165
|
+
else
|
|
166
|
+
succeeds? { computer = started(options, path) }
|
|
133
167
|
end
|
|
134
|
-
|
|
168
|
+
@problem.empty? ? computer : nil
|
|
135
169
|
end
|
|
136
170
|
|
|
137
171
|
private
|
|
@@ -143,6 +177,60 @@ module Badline
|
|
|
143
177
|
@problem.empty?
|
|
144
178
|
end
|
|
145
179
|
|
|
180
|
+
# Runs the block as #succeeds? does, keeping a cartridge's chip in a
|
|
181
|
+
# bank or slot the machine doesn't map as the problem too.
|
|
182
|
+
def cartridge_succeeds?(&)
|
|
183
|
+
@problem = Frontend.cartridge_problem(&)
|
|
184
|
+
@problem.empty?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The machine Frontend.start started on `path`, with the names of
|
|
188
|
+
# the cartridges in it noted.
|
|
189
|
+
def started(options, path)
|
|
190
|
+
computer = Frontend.start(options, path, @writable)
|
|
191
|
+
list(path)
|
|
192
|
+
@names = {}
|
|
193
|
+
options.cartridges.each { |slot, image| note_cartridge(computer, image.dup, slot) }
|
|
194
|
+
note_cartridge(computer, path, "")
|
|
195
|
+
@named = computer
|
|
196
|
+
computer
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def named? = @computer.equal?(@named)
|
|
200
|
+
|
|
201
|
+
def slot_name(slot) = named? ? @names.fetch(slot, slot.upcase) : slot.upcase
|
|
202
|
+
|
|
203
|
+
# Keeps the name of the cartridge at `path` in `computer` by the
|
|
204
|
+
# slots it fills: a Plus/4's by xplus4's names, a raw image's in
|
|
205
|
+
# `slot` if it names one, and a VIC-20's in the one slot "". Other
|
|
206
|
+
# media, and other machines' cartridges, leave the names alone.
|
|
207
|
+
def note_cartridge(computer, path, slot)
|
|
208
|
+
case computer.family
|
|
209
|
+
when :plus4
|
|
210
|
+
return unless Media.kind(computer, path) == :cartridge
|
|
211
|
+
|
|
212
|
+
name = file_name(path)
|
|
213
|
+
Media::Plus4Media.cartridge_chips(path, slot: slot.empty? ? nil : slot).each do |chip|
|
|
214
|
+
@names["c#{chip.bank + 1}#{chip.address < 0xc000 ? 'lo' : 'hi'}"] = name
|
|
215
|
+
end
|
|
216
|
+
when :vic20 then @names[""] = file_name(path) if vic20_cartridge?(path)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def vic20_cartridge?(path)
|
|
221
|
+
case Media::Extensions.kind(path)
|
|
222
|
+
when :cartridge then true
|
|
223
|
+
when :program then File.file?(path) && !Media::Vic20Cartridge.chips(path, Media.program_bytes(path)).nil?
|
|
224
|
+
else false
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A .crt's own name, or else the file's, without its extension.
|
|
229
|
+
def file_name(path)
|
|
230
|
+
name = Media::Extensions.kind(path) == :cartridge ? Storage::CRTFile.new(path, machine: nil).name : ""
|
|
231
|
+
name.empty? ? File.basename(path, File.extname(path)) : name
|
|
232
|
+
end
|
|
233
|
+
|
|
146
234
|
# Keeps `path` as the list the set comes from, if it's a list.
|
|
147
235
|
def list(path)
|
|
148
236
|
return unless Media::DiskList.list?(path)
|
|
@@ -29,7 +29,7 @@ module Badline
|
|
|
29
29
|
@recent = []
|
|
30
30
|
@painter = painter
|
|
31
31
|
@buttons = buttons
|
|
32
|
-
@media = MenuMedia.new(media_path, options.writable
|
|
32
|
+
@media = MenuMedia.new(media_path, options.writable?, options.cartridges)
|
|
33
33
|
@reu_kb = options.reu
|
|
34
34
|
@computer = nil
|
|
35
35
|
@controls = nil
|
|
@@ -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
|
|
@@ -126,12 +122,14 @@ module Badline
|
|
|
126
122
|
end
|
|
127
123
|
|
|
128
124
|
def draw_expansion
|
|
125
|
+
info("CARTRIDGE", @media.cartridge_name)
|
|
129
126
|
return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
|
|
130
127
|
|
|
131
|
-
cartridge = @computer.address_bus.cartridge
|
|
132
|
-
info("CARTRIDGE", cartridge.nil? ? "" : 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
|
|
|
@@ -139,7 +137,8 @@ module Badline
|
|
|
139
137
|
info("RAM EXPANSION", "REU, #{@reu_kb} KB")
|
|
140
138
|
end
|
|
141
139
|
|
|
142
|
-
# The SID's model is a setting,
|
|
140
|
+
# The SID's model is a setting, the C64's SID's or a Plus/4's SID
|
|
141
|
+
# card's, and other sound chips have none.
|
|
143
142
|
def draw_sound
|
|
144
143
|
if @sound.on?
|
|
145
144
|
toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
|
|
@@ -149,7 +148,7 @@ module Badline
|
|
|
149
148
|
model = @computer.sound_source.model
|
|
150
149
|
return unless SID_MODELS.include?(model)
|
|
151
150
|
|
|
152
|
-
toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
|
|
151
|
+
toggle(plus4? ? "SID CARD" : "SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
|
|
153
152
|
end
|
|
154
153
|
|
|
155
154
|
# Saving now and by name, loading a named save, and the quicksaves
|
|
@@ -167,9 +166,15 @@ module Badline
|
|
|
167
166
|
end
|
|
168
167
|
end
|
|
169
168
|
|
|
170
|
-
# The C128 names its model and the mode it runs in first
|
|
169
|
+
# The C128 names its model and the mode it runs in first, and the
|
|
170
|
+
# Plus/4 its model and RAM, and its RAM expansion if it has one.
|
|
171
171
|
def draw_power
|
|
172
172
|
info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
|
|
173
|
+
if plus4?
|
|
174
|
+
info("MACHINE", @computer.model_label)
|
|
175
|
+
expansion = @computer.ram_expansion_label
|
|
176
|
+
info("RAM EXPANSION", expansion) if expansion
|
|
177
|
+
end
|
|
173
178
|
row("RESET", :reset)
|
|
174
179
|
row("POWER CYCLE", :power_cycle)
|
|
175
180
|
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)
|
|
@@ -15,7 +15,8 @@ module Badline
|
|
|
15
15
|
# way: #wait handles the window's clicks, keys and drops between frames
|
|
16
16
|
# and redraws it at the display's pace. The SID model buttons switch
|
|
17
17
|
# the chips playing on the spot, and each tune after it starts on the
|
|
18
|
-
# same choice. AUTO gives each of a tune's SIDs its own model.
|
|
18
|
+
# same choice. AUTO gives each of a tune's SIDs its own model. It
|
|
19
|
+
# quits once it has drawn `frames` frames (--frames), unless that's 0.
|
|
19
20
|
class PlayerController < Audio::Terminal
|
|
20
21
|
FRAME = 1.0 / 50
|
|
21
22
|
|
|
@@ -30,8 +31,10 @@ module Badline
|
|
|
30
31
|
Keys::TAB => :view, 6 => :chip
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
33
|
-
def initialize(input:, output:)
|
|
34
|
-
super
|
|
34
|
+
def initialize(input:, output:, frames: 0)
|
|
35
|
+
super(input:, output:)
|
|
36
|
+
@frame_limit = frames
|
|
37
|
+
@frames = 0
|
|
35
38
|
@screen = PlayerScreen.new
|
|
36
39
|
@player = nil
|
|
37
40
|
@tune_file = nil
|
|
@@ -73,7 +76,10 @@ module Badline
|
|
|
73
76
|
actions = poll
|
|
74
77
|
deadline = now + seconds
|
|
75
78
|
loop do
|
|
76
|
-
|
|
79
|
+
if now >= @next_draw
|
|
80
|
+
draw
|
|
81
|
+
actions << :quit if @frames == @frame_limit
|
|
82
|
+
end
|
|
77
83
|
left = deadline - now
|
|
78
84
|
break if left <= 0 || !actions.empty?
|
|
79
85
|
|
|
@@ -211,6 +217,7 @@ module Badline
|
|
|
211
217
|
|
|
212
218
|
def draw
|
|
213
219
|
@next_draw = now + FRAME
|
|
220
|
+
@frames += 1
|
|
214
221
|
@screen.draw(@state, @player, @history, @tune_file, SIDTune::STIL::Credit.at(@credits, @state.played))
|
|
215
222
|
end
|
|
216
223
|
|
|
@@ -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) 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,20 @@ module Badline
|
|
|
117
127
|
end
|
|
118
128
|
private_class_method :vic20_for
|
|
119
129
|
|
|
130
|
+
# A Plus/4 with the RAM --ram names (Plus4::Model::RAM_SIZES), or with
|
|
131
|
+
# the model's own without it, the true drive --drive-type names, the
|
|
132
|
+
# SID card --sid-card names, where --sid-card-address puts it, and the
|
|
133
|
+
# RAM expansion --ram-expansion names.
|
|
134
|
+
def self.plus4_for(media, options)
|
|
135
|
+
unsupported = !media.nil? && Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
136
|
+
raise Unsupported, "doesn't go in a Plus/4" if unsupported
|
|
137
|
+
|
|
138
|
+
Plus4.new(model: options.model, ram: options.ram_kb, true_drive_model: options.drive_type,
|
|
139
|
+
sid_card: options.sid_card, sid_card_address: options.sid_card_address || 0xfd40,
|
|
140
|
+
ram_expansion: options.ram_expansion)
|
|
141
|
+
end
|
|
142
|
+
private_class_method :plus4_for
|
|
143
|
+
|
|
120
144
|
def self.c64_media?(media)
|
|
121
145
|
return false if media.nil? || !File.file?(media)
|
|
122
146
|
|
|
@@ -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
|