badline 0.1.0 → 0.2.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 +167 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +88 -23
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +131 -15
- data/lib/badline/cia/interrupt_register.rb +88 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +109 -74
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +68 -8
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +171 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +108 -167
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +33 -0
- data/lib/badline/kernal_trap/drive.rb +239 -0
- data/lib/badline/kernal_trap/file.rb +4 -24
- data/lib/badline/kernal_trap/load.rb +2 -2
- data/lib/badline/kernal_trap/routine.rb +34 -0
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +139 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +32 -27
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/disk_image.rb +52 -5
- data/lib/badline/storage/host_directory.rb +18 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +373 -49
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +103 -4
data/lib/badline/sid.rb
CHANGED
|
@@ -1,25 +1,292 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "badline/sid/waveform"
|
|
4
|
+
require "badline/sid/envelope"
|
|
5
|
+
require "badline/sid/voice"
|
|
6
|
+
require "badline/sid/filter"
|
|
7
|
+
require "badline/sid/decimator"
|
|
8
|
+
|
|
3
9
|
module Badline
|
|
10
|
+
# SID (Sound Interface Device) chip, in either the 6581 or the 8580
|
|
11
|
+
# revision.
|
|
12
|
+
#
|
|
13
|
+
# $D400-$D418 - Voices, filter and volume - write only
|
|
14
|
+
# $D419-$D41A - POTX/POTY paddle inputs - read only
|
|
15
|
+
# $D41B-$D41C - Voice 3 oscillator/envelope - read only
|
|
16
|
+
# $D41D-$D41F - Unconnected
|
|
17
|
+
#
|
|
18
|
+
# The register file mirrors every 32 bytes up to $D7FF.
|
|
19
|
+
#
|
|
20
|
+
# Reading a write-only or unconnected register returns the last byte the
|
|
21
|
+
# SID saw on the data bus. That value fades to $00 as the capacitance
|
|
22
|
+
# holding it drains. Paddles and the 1351 mouse attach as a pots source
|
|
23
|
+
# responding to #pot_x and #pot_y; with none attached the lines read $FF.
|
|
24
|
+
#
|
|
25
|
+
# Three voices feed the filter and the master volume; #output and #sample
|
|
26
|
+
# read out the mixed result, and #record collects it at an audio rate for
|
|
27
|
+
# #drain_samples. The DSP is clocked lazily and catches up when something
|
|
28
|
+
# asks for its state.
|
|
29
|
+
#
|
|
30
|
+
# The 6581 drives its voices well above ground, so the mix carries a large
|
|
31
|
+
# DC offset that the RC network on the C64 board filters out.
|
|
4
32
|
class SID
|
|
5
33
|
include Addressable
|
|
6
34
|
|
|
7
|
-
|
|
35
|
+
VOICES = 3
|
|
36
|
+
|
|
37
|
+
# Cycles the data bus holds a value, measured through SID/bitfade.
|
|
38
|
+
BUS_TTL = { mos6581: 0x1d00, mos8580: 0xa2000 }.freeze
|
|
39
|
+
|
|
40
|
+
# Scales the filter's 20-bit mix down to a signed 16-bit sample.
|
|
41
|
+
SAMPLE_DIVISOR = ((0xfff * 0xff) >> 7) * 3 * 15 * 2 / (2**16)
|
|
42
|
+
|
|
43
|
+
# Writes the DSP queues between catch-ups. A full queue catches up
|
|
44
|
+
# there and then, so no write loses its timing.
|
|
45
|
+
DEFERRED_WRITES = 0x400
|
|
46
|
+
|
|
47
|
+
# Cycles the filter integrates in one step while synthesizing, after
|
|
48
|
+
# reSID's delta_t_flt. 1 makes the audio exact, at a per-cycle cost.
|
|
49
|
+
FILTER_CHUNK = 4
|
|
50
|
+
|
|
51
|
+
attr_accessor :pots
|
|
52
|
+
attr_reader :model
|
|
53
|
+
|
|
54
|
+
def initialize(model: :mos6581, pots: nil, filter_chunk: FILTER_CHUNK)
|
|
8
55
|
addressable_at(0xd400, length: 2**10)
|
|
56
|
+
@model = model
|
|
57
|
+
@pots = pots
|
|
58
|
+
@bus_ttl_reset = BUS_TTL.fetch(model)
|
|
59
|
+
@filter = Filter.new(model:)
|
|
60
|
+
@synthesizing = false
|
|
61
|
+
@pending_cycles = 0
|
|
62
|
+
@deferred_writes = []
|
|
63
|
+
@decimator = nil
|
|
64
|
+
@samples = []
|
|
65
|
+
@filter_chunk = filter_chunk
|
|
66
|
+
@voices = Voice.linked(VOICES, model:)
|
|
67
|
+
@voice1, @voice2, @voice3 = @voices
|
|
68
|
+
@waveform1, @waveform2, @waveform3 = @voices.map(&:waveform)
|
|
69
|
+
reset!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The RES line clears the registers, the data bus, the filter and the
|
|
73
|
+
# voices, all but their accumulators. Recording carries on across it.
|
|
74
|
+
def reset!
|
|
75
|
+
catch_up
|
|
9
76
|
@registers = Memory.new(length: 2**5)
|
|
77
|
+
@bus_value = 0x00
|
|
78
|
+
@bus_ttl = 0
|
|
79
|
+
@voices.each(&:reset!)
|
|
80
|
+
@filter.reset
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The DSP only counts cycles as they pass. Whatever asks for its state
|
|
84
|
+
# catches it up, a span at a time: each span fast-forwards the voices
|
|
85
|
+
# and ends on one whole cycle, so everything the CPU can read stays
|
|
86
|
+
# exact. The filter only runs once something wants audio.
|
|
87
|
+
def synthesizing? = @synthesizing
|
|
88
|
+
|
|
89
|
+
def synthesize!
|
|
90
|
+
return if @synthesizing
|
|
91
|
+
|
|
92
|
+
@synthesizing = true
|
|
93
|
+
catch_up
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Starts collecting the output, averaged down to `rate` samples a
|
|
97
|
+
# second, for #drain_samples. `filter_chunk` overrides the one the SID
|
|
98
|
+
# was built with, for a machine that built its own.
|
|
99
|
+
def record(rate:, filter_chunk: @filter_chunk)
|
|
100
|
+
synthesize!
|
|
101
|
+
catch_up
|
|
102
|
+
@filter_chunk = filter_chunk
|
|
103
|
+
@decimator = Decimator.new(clock_hz: TimeOfDay::CLOCK_HZ, rate:)
|
|
104
|
+
@samples = []
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The samples recorded since the last drain.
|
|
108
|
+
def drain_samples
|
|
109
|
+
catch_up
|
|
110
|
+
samples = @samples
|
|
111
|
+
@samples = []
|
|
112
|
+
samples
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def voices
|
|
116
|
+
catch_up
|
|
117
|
+
@voices
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def filter
|
|
121
|
+
catch_up
|
|
122
|
+
@filter
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Voice 3's oscillator and envelope, the only synthesis state the CPU
|
|
126
|
+
# can see. Programs poll OSC3 with the noise waveform selected for
|
|
127
|
+
# random numbers, and ENV3 to time hard restarts.
|
|
128
|
+
def osc3
|
|
129
|
+
catch_up
|
|
130
|
+
@waveform3.osc3 >> 4
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def env3
|
|
134
|
+
catch_up
|
|
135
|
+
@voice3.envelope.env3
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def output
|
|
139
|
+
synthesize!
|
|
140
|
+
catch_up
|
|
141
|
+
@filter.output
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def sample
|
|
145
|
+
output
|
|
146
|
+
current_sample
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def cycle!
|
|
150
|
+
age_bus if @bus_ttl != 0
|
|
151
|
+
@pending_cycles += 1
|
|
10
152
|
end
|
|
11
153
|
|
|
12
154
|
def peek(addr)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
when
|
|
16
|
-
|
|
155
|
+
case index(addr) % (2**5)
|
|
156
|
+
when 0x19 then latch(pots ? pots.pot_x : 0xff)
|
|
157
|
+
when 0x1a then latch(pots ? pots.pot_y : 0xff)
|
|
158
|
+
when 0x1b then latch(osc3)
|
|
159
|
+
when 0x1c then latch(env3)
|
|
160
|
+
else @bus_value
|
|
17
161
|
end
|
|
18
162
|
end
|
|
19
163
|
|
|
20
164
|
def poke(addr, value)
|
|
21
|
-
|
|
22
|
-
|
|
165
|
+
reg = index(addr) % (2**5)
|
|
166
|
+
write_register(reg, value) if reg <= 0x18
|
|
167
|
+
latch(value)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Register contents as written, for save states and debugging.
|
|
171
|
+
def register(reg) = @registers.peek(reg)
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
# Runs the cycles that have passed, landing each queued write on the
|
|
176
|
+
# cycle the CPU wrote it on.
|
|
177
|
+
def catch_up
|
|
178
|
+
replayed = 0
|
|
179
|
+
@deferred_writes.each do |cycle, reg, value|
|
|
180
|
+
run(cycle - replayed)
|
|
181
|
+
replayed = cycle
|
|
182
|
+
apply_write(reg, value)
|
|
183
|
+
end
|
|
184
|
+
run(@pending_cycles - replayed)
|
|
185
|
+
@deferred_writes.clear
|
|
186
|
+
@pending_cycles = 0
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def run(cycles)
|
|
190
|
+
while cycles.positive?
|
|
191
|
+
span = span(cycles)
|
|
192
|
+
fast_forward(span - 1) if span > 1
|
|
193
|
+
clock!(span)
|
|
194
|
+
cycles -= span
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The longest stretch that can be fast-forwarded. A combined waveform
|
|
199
|
+
# feeding back into its own oscillator steps cycle by cycle, and a span
|
|
200
|
+
# never runs past an MSB rise that hard-syncs the next voice, so both
|
|
201
|
+
# land on a whole cycle.
|
|
202
|
+
def span(cycles)
|
|
203
|
+
synthesizing = @synthesizing
|
|
204
|
+
return 1 if @waveform1.stepped?(synthesizing) || @waveform2.stepped?(synthesizing) ||
|
|
205
|
+
@waveform3.stepped?(synthesizing)
|
|
206
|
+
|
|
207
|
+
span = synthesizing ? synthesis_span(cycles) : cycles
|
|
208
|
+
span = sync_span(@waveform1, span)
|
|
209
|
+
span = sync_span(@waveform2, span)
|
|
210
|
+
sync_span(@waveform3, span)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The filter steps at most @filter_chunk cycles at a time, and never
|
|
214
|
+
# across the end of a recorded sample.
|
|
215
|
+
def synthesis_span(cycles)
|
|
216
|
+
span = [cycles, @filter_chunk].min
|
|
217
|
+
return span unless @decimator
|
|
218
|
+
|
|
219
|
+
[@decimator.cycles_to_close, span].min
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def sync_span(waveform, span)
|
|
223
|
+
return span unless waveform.sync_dest.sync?
|
|
224
|
+
|
|
225
|
+
rise = waveform.cycles_to_msb_rise
|
|
226
|
+
rise && rise < span ? rise : span
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Unrolled over the three voices: the per-cycle block calls cost
|
|
230
|
+
# measurably on the synthesis path.
|
|
231
|
+
def fast_forward(cycles)
|
|
232
|
+
@voice1.fast_forward(cycles)
|
|
233
|
+
@voice2.fast_forward(cycles)
|
|
234
|
+
@voice3.fast_forward(cycles)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The last cycle of a span, run whole. The filter integrates the span
|
|
238
|
+
# from the voices' output at its end.
|
|
239
|
+
def clock!(span)
|
|
240
|
+
@voice1.cycle!
|
|
241
|
+
@voice2.cycle!
|
|
242
|
+
@voice3.cycle!
|
|
243
|
+
@waveform1.synchronize!
|
|
244
|
+
@waveform2.synchronize!
|
|
245
|
+
@waveform3.synchronize!
|
|
246
|
+
return unless @synthesizing
|
|
247
|
+
|
|
248
|
+
@filter.cycle!(@voices, span)
|
|
249
|
+
record_sample(span) if @decimator
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def record_sample(span)
|
|
253
|
+
sample = @decimator.push(current_sample, span)
|
|
254
|
+
@samples << sample if sample
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def current_sample = (@filter.output / SAMPLE_DIVISOR).clamp(-0x8000, 0x7fff)
|
|
258
|
+
|
|
259
|
+
def latch(value)
|
|
260
|
+
@bus_ttl = @bus_ttl_reset
|
|
261
|
+
@bus_value = value
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def age_bus
|
|
265
|
+
@bus_ttl -= 1
|
|
266
|
+
@bus_value = 0x00 if @bus_ttl.zero?
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def write_register(reg, value)
|
|
270
|
+
@registers.poke(reg, value)
|
|
271
|
+
catch_up if @deferred_writes.length == DEFERRED_WRITES
|
|
272
|
+
@deferred_writes << [@pending_cycles, reg, value]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def apply_write(reg, value)
|
|
276
|
+
reg < 0x15 ? write_voice(reg, value) : @filter.write(reg, value)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def write_voice(reg, value)
|
|
280
|
+
voice = @voices[reg / 7]
|
|
281
|
+
case reg % 7
|
|
282
|
+
when 0 then voice.waveform.frequency_low = value
|
|
283
|
+
when 1 then voice.waveform.frequency_high = value
|
|
284
|
+
when 2 then voice.waveform.pulse_width_low = value
|
|
285
|
+
when 3 then voice.waveform.pulse_width_high = value
|
|
286
|
+
when 4 then voice.control = value
|
|
287
|
+
when 5 then voice.envelope.attack_decay = value
|
|
288
|
+
when 6 then voice.envelope.sustain_release = value
|
|
289
|
+
end
|
|
23
290
|
end
|
|
24
291
|
end
|
|
25
292
|
end
|
data/lib/badline/status.rb
CHANGED
|
@@ -4,6 +4,38 @@ module Badline
|
|
|
4
4
|
class Status
|
|
5
5
|
attr_reader :value, :flags, :bitmask, :low_mask, :high_mask
|
|
6
6
|
|
|
7
|
+
class << self
|
|
8
|
+
# Each flag layout gets a subclass of its own, with the flag accessors
|
|
9
|
+
# compiled as ordinary methods rather than closures.
|
|
10
|
+
def new(flags = [], value: 0x0)
|
|
11
|
+
return super unless equal?(Status)
|
|
12
|
+
|
|
13
|
+
layout(flags).new(flags, value:)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def layout(flags)
|
|
19
|
+
@layouts ||= {}
|
|
20
|
+
@layouts[flags] ||= Class.new(Status) { define_accessors(flags) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def define_accessors(flags)
|
|
24
|
+
flags.each_with_index do |name, i|
|
|
25
|
+
next unless name.is_a?(Symbol)
|
|
26
|
+
|
|
27
|
+
mask = 1 << i
|
|
28
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
|
29
|
+
def #{name}=(enabled) # def carry=(enabled)
|
|
30
|
+
@value = enabled && enabled != 0 ? @value | #{mask} : @value & #{~mask} # @value = ... | 0x01 : ... & ~0x01
|
|
31
|
+
end # end
|
|
32
|
+
def #{name}? = @value & #{mask} != 0 # def carry? = @value & 0x01 != 0
|
|
33
|
+
def #{name} = @value & #{mask} == 0 ? 0 : 1 # def carry = ... ? 0 : 1
|
|
34
|
+
RUBY
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
7
39
|
def initialize(flags = [], value: 0x0)
|
|
8
40
|
@flags = flags
|
|
9
41
|
|
|
@@ -12,8 +44,6 @@ module Badline
|
|
|
12
44
|
@high_mask = create_mask { |f| f.is_a?(Integer) && f == 1 }
|
|
13
45
|
|
|
14
46
|
self.value = value
|
|
15
|
-
|
|
16
|
-
define_accessors!
|
|
17
47
|
end
|
|
18
48
|
|
|
19
49
|
def value=(new_value)
|
|
@@ -27,30 +57,5 @@ module Badline
|
|
|
27
57
|
mask + (predicate.call(flag) ? 1 << i : 0)
|
|
28
58
|
end
|
|
29
59
|
end
|
|
30
|
-
|
|
31
|
-
def define_accessors!
|
|
32
|
-
flags.each_with_index do |name, i|
|
|
33
|
-
next unless name.is_a?(Symbol)
|
|
34
|
-
|
|
35
|
-
mask = 1 << i
|
|
36
|
-
define_singleton_method("#{name}=") do |enabled|
|
|
37
|
-
update(mask, enabled)
|
|
38
|
-
end
|
|
39
|
-
define_singleton_method("#{name}?") do
|
|
40
|
-
!@value.nobits?(mask)
|
|
41
|
-
end
|
|
42
|
-
define_singleton_method(name) do
|
|
43
|
-
@value.nobits?(mask) ? 0 : 1
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def update(mask, enabled)
|
|
49
|
-
self.value = if enabled && enabled != 0
|
|
50
|
-
value | mask
|
|
51
|
-
else
|
|
52
|
-
value & ~mask
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
60
|
end
|
|
56
61
|
end
|
|
@@ -12,7 +12,21 @@ module Badline
|
|
|
12
12
|
|
|
13
13
|
Chip = Data.define(:chip_type, :bank, :address, :data)
|
|
14
14
|
|
|
15
|
-
attr_reader :hardware_type, :exrom, :game, :name, :chips
|
|
15
|
+
attr_reader :hardware_type, :subtype, :exrom, :game, :name, :chips
|
|
16
|
+
|
|
17
|
+
# What .write needs of an image: the fields a parsed CRTFile reads.
|
|
18
|
+
Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
|
|
19
|
+
|
|
20
|
+
def self.write(path, image)
|
|
21
|
+
header = SIGNATURE +
|
|
22
|
+
[0x40, 0x0100, image.hardware_type, image.exrom, image.game, image.subtype].pack("NnnCCCx5") +
|
|
23
|
+
[image.name.to_s.b[0, 32]].pack("a32")
|
|
24
|
+
packets = image.chips.map do |chip|
|
|
25
|
+
["CHIP", CHIP_HEADER_SIZE + chip.data.length, chip.chip_type, chip.bank, chip.address,
|
|
26
|
+
chip.data.length].pack("a4Nn4") + chip.data.pack("C*")
|
|
27
|
+
end
|
|
28
|
+
File.binwrite(path, header + packets.join)
|
|
29
|
+
end
|
|
16
30
|
|
|
17
31
|
def initialize(path)
|
|
18
32
|
parse(File.binread(path))
|
|
@@ -26,13 +40,15 @@ module Badline
|
|
|
26
40
|
@hardware_type = bytes[0x16, 2].unpack1("n")
|
|
27
41
|
@exrom = bytes.getbyte(0x18)
|
|
28
42
|
@game = bytes.getbyte(0x19)
|
|
43
|
+
@subtype = bytes.getbyte(0x1a)
|
|
29
44
|
@name = bytes[0x20, 32].unpack1("Z*")
|
|
30
45
|
@chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
|
|
31
46
|
end
|
|
32
47
|
|
|
48
|
+
# Trailing bytes too short to hold a CHIP header are ignored.
|
|
33
49
|
def parse_chips(bytes, offset)
|
|
34
50
|
chips = []
|
|
35
|
-
while offset
|
|
51
|
+
while offset + CHIP_HEADER_SIZE <= bytes.length
|
|
36
52
|
chip, offset = parse_chip(bytes, offset)
|
|
37
53
|
chips << chip
|
|
38
54
|
end
|
|
@@ -6,21 +6,62 @@ module Badline
|
|
|
6
6
|
SECTOR_SIZE = 256
|
|
7
7
|
ENTRY_SIZE = 32
|
|
8
8
|
ENTRIES_PER_SECTOR = 8
|
|
9
|
-
|
|
9
|
+
FILETYPES = { seq: 0x01, prg: 0x02, usr: 0x03 }.freeze
|
|
10
10
|
NAME_PADDING = 0xa0
|
|
11
11
|
|
|
12
|
+
# Error table codes and the DOS errors they stand for: 20 to 29 are
|
|
13
|
+
# the read, write and ID errors, 74 is DRIVE NOT READY.
|
|
14
|
+
DOS_ERRORS = (2..11).to_h { |code| [code, code + 18] }.merge(15 => 74).freeze
|
|
15
|
+
|
|
12
16
|
def initialize(path)
|
|
13
17
|
@bytes = File.binread(path).bytes
|
|
18
|
+
@errors = split_error_table
|
|
14
19
|
end
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
# A LOAD reads only PRG files. An OPEN names the type it wants, or
|
|
22
|
+
# takes the first file of any type with a nil `type`.
|
|
23
|
+
def read_file(name, type: :prg)
|
|
17
24
|
pattern = Storage.matcher(name)
|
|
18
|
-
entry = entries.find
|
|
25
|
+
entry = entries.find do |e|
|
|
26
|
+
(type.nil? || e[:type] == type) && pattern.match?(e[:name])
|
|
27
|
+
end
|
|
19
28
|
read_chain(entry[:track], entry[:sector]) if entry
|
|
20
29
|
end
|
|
21
30
|
|
|
31
|
+
# Raw block access for the DOS `U1` command. Returns nil for blocks
|
|
32
|
+
# outside the image's geometry.
|
|
33
|
+
def read_block(track, sector)
|
|
34
|
+
return unless block?(track, sector)
|
|
35
|
+
|
|
36
|
+
sector_at(track, sector)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The DOS error a read of the block raises, from the error table an
|
|
40
|
+
# image can carry after its last block, or nil when it reads cleanly.
|
|
41
|
+
def block_error(track, sector)
|
|
42
|
+
return unless @errors && block?(track, sector)
|
|
43
|
+
|
|
44
|
+
DOS_ERRORS[@errors[track_offset(track) + sector]]
|
|
45
|
+
end
|
|
46
|
+
|
|
22
47
|
private
|
|
23
48
|
|
|
49
|
+
# An error table holds one byte per block, so the image is 257 bytes
|
|
50
|
+
# per block rather than 256.
|
|
51
|
+
def split_error_table
|
|
52
|
+
return unless (@bytes.length % (SECTOR_SIZE + 1)).zero? &&
|
|
53
|
+
!(@bytes.length % SECTOR_SIZE).zero?
|
|
54
|
+
|
|
55
|
+
@bytes.pop(@bytes.length / (SECTOR_SIZE + 1))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def block?(track, sector)
|
|
59
|
+
return false unless track.between?(1, 255) &&
|
|
60
|
+
sector.between?(0, sectors_in(track) - 1)
|
|
61
|
+
|
|
62
|
+
sector_offset(track, sector) + SECTOR_SIZE <= @bytes.length
|
|
63
|
+
end
|
|
64
|
+
|
|
24
65
|
def entries
|
|
25
66
|
@entries ||= each_sector(directory_track, directory_sector)
|
|
26
67
|
.flat_map { |data| parse_entries(data) }
|
|
@@ -29,9 +70,11 @@ module Badline
|
|
|
29
70
|
def parse_entries(data)
|
|
30
71
|
(0...ENTRIES_PER_SECTOR).filter_map do |i|
|
|
31
72
|
entry = data[i * ENTRY_SIZE, ENTRY_SIZE]
|
|
32
|
-
|
|
73
|
+
type = FILETYPES.key(entry[2] & 0x07)
|
|
74
|
+
next unless type
|
|
33
75
|
|
|
34
76
|
{ name: decode_name(entry[5, 16]),
|
|
77
|
+
type:,
|
|
35
78
|
track: entry[3],
|
|
36
79
|
sector: entry[4] }
|
|
37
80
|
end
|
|
@@ -64,7 +107,11 @@ module Badline
|
|
|
64
107
|
end
|
|
65
108
|
|
|
66
109
|
def sector_offset(track, sector)
|
|
67
|
-
((
|
|
110
|
+
(track_offset(track) + sector) * SECTOR_SIZE
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def track_offset(track)
|
|
114
|
+
(1...track).sum { |t| sectors_in(t) }
|
|
68
115
|
end
|
|
69
116
|
end
|
|
70
117
|
end
|
|
@@ -7,12 +7,12 @@ module Badline
|
|
|
7
7
|
@path = path
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def read_file(name)
|
|
10
|
+
def read_file(name, **)
|
|
11
11
|
entry = find(name)
|
|
12
12
|
return unless entry
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
archive = entry[:archive]
|
|
15
|
+
archive ? archive.read_file(entry[:name]) : file_bytes(entry[:file])
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def write_file(name, bytes)
|
|
@@ -28,7 +28,7 @@ module Badline
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def entries
|
|
31
|
-
Dir.children(@path).sort.
|
|
31
|
+
Dir.children(@path).sort.flat_map { |f| entry_for(f) }.compact
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def entry_for(file)
|
|
@@ -37,6 +37,8 @@ module Badline
|
|
|
37
37
|
{ name: File.basename(file, ".*").downcase, file: }
|
|
38
38
|
when ".p00"
|
|
39
39
|
p00_entry(file)
|
|
40
|
+
when ".t64"
|
|
41
|
+
t64_entries(file)
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -44,6 +46,18 @@ module Badline
|
|
|
44
46
|
header = File.binread(File.join(@path, file), P00::HEADER_SIZE).bytes
|
|
45
47
|
{ name: P00.name(header), file: } if P00.wraps?(header)
|
|
46
48
|
end
|
|
49
|
+
|
|
50
|
+
def t64_entries(file)
|
|
51
|
+
archive = T64.new(File.join(@path, file))
|
|
52
|
+
archive.names.map { |name| { name:, archive: } }
|
|
53
|
+
rescue T64::FormatError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def file_bytes(file)
|
|
58
|
+
bytes = File.binread(File.join(@path, file)).bytes
|
|
59
|
+
P00.wraps?(bytes) ? P00.data(bytes) : bytes
|
|
60
|
+
end
|
|
47
61
|
end
|
|
48
62
|
end
|
|
49
63
|
end
|