badline 0.2.0 → 0.3.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 +67 -0
- data/CLAUDE.md +6 -0
- data/README.md +24 -6
- data/doc/pinned-behaviour.md +18 -6
- data/exe/badline +5 -2
- data/lib/badline/address_bus.rb +14 -6
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- data/lib/badline/cartridge/mach5.rb +1 -1
- data/lib/badline/cartridge/magic_desk.rb +5 -1
- data/lib/badline/cartridge/ocean.rb +5 -2
- data/lib/badline/cartridge/rex_utility.rb +1 -1
- data/lib/badline/cartridge/rgcd.rb +1 -1
- data/lib/badline/cartridge/simons_basic.rb +2 -2
- data/lib/badline/cartridge/westermann.rb +2 -2
- data/lib/badline/cartridge.rb +16 -8
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia/interrupt_register.rb +20 -6
- data/lib/badline/cia.rb +36 -19
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +41 -11
- data/lib/badline/cpu/microcode.rb +41 -43
- data/lib/badline/cpu.rb +3 -3
- data/lib/badline/gui/application.rb +41 -9
- data/lib/badline/kernal_trap/channel.rb +118 -8
- data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
- data/lib/badline/kernal_trap/drive/channels.rb +53 -0
- data/lib/badline/kernal_trap/drive/memory.rb +109 -0
- data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +148 -132
- data/lib/badline/kernal_trap/file.rb +0 -5
- data/lib/badline/kernal_trap/load.rb +93 -25
- data/lib/badline/kernal_trap/routine.rb +17 -0
- data/lib/badline/kernal_trap/save.rb +48 -15
- data/lib/badline/kernal_trap/serial.rb +34 -2
- data/lib/badline/media.rb +2 -2
- data/lib/badline/memory.rb +7 -2
- data/lib/badline/rom.rb +14 -5
- data/lib/badline/sid/catch_up.rb +110 -0
- data/lib/badline/sid/decimator.rb +6 -1
- data/lib/badline/sid/filter.rb +5 -5
- data/lib/badline/sid/waveform/fast_forward.rb +10 -10
- data/lib/badline/sid/waveform.rb +7 -7
- data/lib/badline/sid.rb +16 -92
- data/lib/badline/status.rb +255 -32
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +78 -17
- data/lib/badline/storage/host_directory.rb +26 -2
- data/lib/badline/storage/sid_file.rb +37 -3
- data/lib/badline/storage.rb +3 -1
- data/lib/badline/time_of_day.rb +7 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/graphics_shifter.rb +8 -2
- data/lib/badline/vic/registers.rb +16 -27
- data/lib/badline/vic.rb +23 -10
- data/lib/badline.rb +1 -0
- metadata +9 -1
data/lib/badline/status.rb
CHANGED
|
@@ -1,41 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
+
# A register of flag bits. +flags+ names each bit from bit 0 up: a
|
|
5
|
+
# symbol for a flag, or 0 or 1 for a bit that always reads as that. The
|
|
6
|
+
# subclasses below carry the accessors for each register layout.
|
|
4
7
|
class Status
|
|
5
8
|
attr_reader :value, :flags, :bitmask, :low_mask, :high_mask
|
|
6
9
|
|
|
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
|
-
|
|
39
10
|
def initialize(flags = [], value: 0x0)
|
|
40
11
|
@flags = flags
|
|
41
12
|
|
|
@@ -58,4 +29,256 @@ module Badline
|
|
|
58
29
|
end
|
|
59
30
|
end
|
|
60
31
|
end
|
|
32
|
+
|
|
33
|
+
# The 6510 processor status register.
|
|
34
|
+
class CPUStatus < Status
|
|
35
|
+
def carry=(enabled)
|
|
36
|
+
@value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def carry? = @value & 0x01 != 0
|
|
40
|
+
|
|
41
|
+
def carry = @value.nobits?(0x01) ? 0 : 1
|
|
42
|
+
|
|
43
|
+
def zero=(enabled)
|
|
44
|
+
@value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def zero? = @value & 0x02 != 0
|
|
48
|
+
|
|
49
|
+
def zero = @value.nobits?(0x02) ? 0 : 1
|
|
50
|
+
|
|
51
|
+
def interrupt=(enabled)
|
|
52
|
+
@value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def interrupt? = @value & 0x04 != 0
|
|
56
|
+
|
|
57
|
+
def interrupt = @value.nobits?(0x04) ? 0 : 1
|
|
58
|
+
|
|
59
|
+
def decimal=(enabled)
|
|
60
|
+
@value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def decimal? = @value & 0x08 != 0
|
|
64
|
+
|
|
65
|
+
def decimal = @value.nobits?(0x08) ? 0 : 1
|
|
66
|
+
|
|
67
|
+
def break=(enabled)
|
|
68
|
+
@value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def break? = @value & 0x10 != 0
|
|
72
|
+
|
|
73
|
+
def break = @value.nobits?(0x10) ? 0 : 1
|
|
74
|
+
|
|
75
|
+
def overflow=(enabled)
|
|
76
|
+
@value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def overflow? = @value & 0x40 != 0
|
|
80
|
+
|
|
81
|
+
def overflow = @value.nobits?(0x40) ? 0 : 1
|
|
82
|
+
|
|
83
|
+
def negative=(enabled)
|
|
84
|
+
@value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def negative? = @value & 0x80 != 0
|
|
88
|
+
|
|
89
|
+
def negative = @value.nobits?(0x80) ? 0 : 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The 6510 I/O port at $01.
|
|
93
|
+
class PortStatus < Status
|
|
94
|
+
def basic=(enabled)
|
|
95
|
+
@value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def basic? = @value & 0x01 != 0
|
|
99
|
+
|
|
100
|
+
def basic = @value.nobits?(0x01) ? 0 : 1
|
|
101
|
+
|
|
102
|
+
def kernal=(enabled)
|
|
103
|
+
@value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def kernal? = @value & 0x02 != 0
|
|
107
|
+
|
|
108
|
+
def kernal = @value.nobits?(0x02) ? 0 : 1
|
|
109
|
+
|
|
110
|
+
def io=(enabled)
|
|
111
|
+
@value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def io? = @value & 0x04 != 0
|
|
115
|
+
|
|
116
|
+
def io = @value.nobits?(0x04) ? 0 : 1
|
|
117
|
+
|
|
118
|
+
def tape_out=(enabled)
|
|
119
|
+
@value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def tape_out? = @value & 0x08 != 0
|
|
123
|
+
|
|
124
|
+
def tape_out = @value.nobits?(0x08) ? 0 : 1
|
|
125
|
+
|
|
126
|
+
def tape_switch=(enabled)
|
|
127
|
+
@value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def tape_switch? = @value & 0x10 != 0
|
|
131
|
+
|
|
132
|
+
def tape_switch = @value.nobits?(0x10) ? 0 : 1
|
|
133
|
+
|
|
134
|
+
def tape_motor=(enabled)
|
|
135
|
+
@value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def tape_motor? = @value & 0x20 != 0
|
|
139
|
+
|
|
140
|
+
def tape_motor = @value.nobits?(0x20) ? 0 : 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A CIA timer control register, CRA or CRB. Bits 5-7 differ between the two.
|
|
144
|
+
class ControlRegister < Status
|
|
145
|
+
def start=(enabled)
|
|
146
|
+
@value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def start? = @value & 0x01 != 0
|
|
150
|
+
|
|
151
|
+
def start = @value.nobits?(0x01) ? 0 : 1
|
|
152
|
+
|
|
153
|
+
def output=(enabled)
|
|
154
|
+
@value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def output? = @value & 0x02 != 0
|
|
158
|
+
|
|
159
|
+
def output = @value.nobits?(0x02) ? 0 : 1
|
|
160
|
+
|
|
161
|
+
def out_mode=(enabled)
|
|
162
|
+
@value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def out_mode? = @value & 0x04 != 0
|
|
166
|
+
|
|
167
|
+
def out_mode = @value.nobits?(0x04) ? 0 : 1
|
|
168
|
+
|
|
169
|
+
def run_mode=(enabled)
|
|
170
|
+
@value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def run_mode? = @value & 0x08 != 0
|
|
174
|
+
|
|
175
|
+
def run_mode = @value.nobits?(0x08) ? 0 : 1
|
|
176
|
+
|
|
177
|
+
def load=(enabled)
|
|
178
|
+
@value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def load? = @value & 0x10 != 0
|
|
182
|
+
|
|
183
|
+
def load = @value.nobits?(0x10) ? 0 : 1
|
|
184
|
+
|
|
185
|
+
def in_mode=(enabled)
|
|
186
|
+
@value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def in_mode? = @value & 0x20 != 0
|
|
190
|
+
|
|
191
|
+
def in_mode = @value.nobits?(0x20) ? 0 : 1
|
|
192
|
+
|
|
193
|
+
def serial_mode=(enabled)
|
|
194
|
+
@value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def serial_mode? = @value & 0x40 != 0
|
|
198
|
+
|
|
199
|
+
def serial_mode = @value.nobits?(0x40) ? 0 : 1
|
|
200
|
+
|
|
201
|
+
def clock_frequency=(enabled)
|
|
202
|
+
@value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def clock_frequency? = @value & 0x80 != 0
|
|
206
|
+
|
|
207
|
+
def clock_frequency = @value.nobits?(0x80) ? 0 : 1
|
|
208
|
+
|
|
209
|
+
def in_cnt=(enabled)
|
|
210
|
+
@value = enabled && enabled != 0 ? @value | 0x20 : @value & ~0x20
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def in_cnt? = @value & 0x20 != 0
|
|
214
|
+
|
|
215
|
+
def in_cnt = @value.nobits?(0x20) ? 0 : 1
|
|
216
|
+
|
|
217
|
+
def in_timer_a=(enabled)
|
|
218
|
+
@value = enabled && enabled != 0 ? @value | 0x40 : @value & ~0x40
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def in_timer_a? = @value & 0x40 != 0
|
|
222
|
+
|
|
223
|
+
def in_timer_a = @value.nobits?(0x40) ? 0 : 1
|
|
224
|
+
|
|
225
|
+
def alarm=(enabled)
|
|
226
|
+
@value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def alarm? = @value & 0x80 != 0
|
|
230
|
+
|
|
231
|
+
def alarm = @value.nobits?(0x80) ? 0 : 1
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# The CIA interrupt sources, as flags or as their mask.
|
|
235
|
+
class InterruptFlags < Status
|
|
236
|
+
def timer_a=(enabled)
|
|
237
|
+
@value = enabled && enabled != 0 ? @value | 0x01 : @value & ~0x01
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def timer_a? = @value & 0x01 != 0
|
|
241
|
+
|
|
242
|
+
def timer_a = @value.nobits?(0x01) ? 0 : 1
|
|
243
|
+
|
|
244
|
+
def timer_b=(enabled)
|
|
245
|
+
@value = enabled && enabled != 0 ? @value | 0x02 : @value & ~0x02
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def timer_b? = @value & 0x02 != 0
|
|
249
|
+
|
|
250
|
+
def timer_b = @value.nobits?(0x02) ? 0 : 1
|
|
251
|
+
|
|
252
|
+
def alarm=(enabled)
|
|
253
|
+
@value = enabled && enabled != 0 ? @value | 0x04 : @value & ~0x04
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def alarm? = @value & 0x04 != 0
|
|
257
|
+
|
|
258
|
+
def alarm = @value.nobits?(0x04) ? 0 : 1
|
|
259
|
+
|
|
260
|
+
def serial=(enabled)
|
|
261
|
+
@value = enabled && enabled != 0 ? @value | 0x08 : @value & ~0x08
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def serial? = @value & 0x08 != 0
|
|
265
|
+
|
|
266
|
+
def serial = @value.nobits?(0x08) ? 0 : 1
|
|
267
|
+
|
|
268
|
+
def flag=(enabled)
|
|
269
|
+
@value = enabled && enabled != 0 ? @value | 0x10 : @value & ~0x10
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def flag? = @value & 0x10 != 0
|
|
273
|
+
|
|
274
|
+
def flag = @value.nobits?(0x10) ? 0 : 1
|
|
275
|
+
|
|
276
|
+
def interrupt=(enabled)
|
|
277
|
+
@value = enabled && enabled != 0 ? @value | 0x80 : @value & ~0x80
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def interrupt? = @value & 0x80 != 0
|
|
281
|
+
|
|
282
|
+
def interrupt = @value.nobits?(0x80) ? 0 : 1
|
|
283
|
+
end
|
|
61
284
|
end
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Storage
|
|
5
5
|
class D64Image < DiskImage
|
|
6
|
+
# Image sizes with an error table, mapped to its length: 35 tracks
|
|
7
|
+
# (174848 bytes without one), 40 tracks (196608) and 42 tracks (205312)
|
|
8
|
+
ERROR_TABLES = { 175_531 => 683, 197_376 => 768, 206_114 => 802 }.freeze
|
|
9
|
+
|
|
6
10
|
private
|
|
7
11
|
|
|
12
|
+
def error_tables = ERROR_TABLES
|
|
13
|
+
|
|
8
14
|
def directory_track = 18
|
|
9
15
|
def directory_sector = 1
|
|
10
16
|
|
|
@@ -21,13 +21,49 @@ module Badline
|
|
|
21
21
|
# A LOAD reads only PRG files. An OPEN names the type it wants, or
|
|
22
22
|
# takes the first file of any type with a nil `type`.
|
|
23
23
|
def read_file(name, type: :prg)
|
|
24
|
-
|
|
25
|
-
entry = entries.find do |e|
|
|
26
|
-
(type.nil? || e[:type] == type) && pattern.match?(e[:name])
|
|
27
|
-
end
|
|
24
|
+
entry = find_entry(name, type)
|
|
28
25
|
read_chain(entry[:track], entry[:sector]) if entry
|
|
29
26
|
end
|
|
30
27
|
|
|
28
|
+
# The first block in a file's chain that the error table marks bad:
|
|
29
|
+
# its DOS error, its track and sector, and how many of the file's
|
|
30
|
+
# bytes come before it. Nil when the whole chain reads cleanly.
|
|
31
|
+
def read_error(name, type: :prg)
|
|
32
|
+
entry = find_entry(name, type)
|
|
33
|
+
return unless @errors && entry
|
|
34
|
+
|
|
35
|
+
offset = 0
|
|
36
|
+
each_sector(entry[:track], entry[:sector]) do |data, track, sector|
|
|
37
|
+
error = block_error(track, sector)
|
|
38
|
+
return { error:, track:, sector:, offset: } if error
|
|
39
|
+
|
|
40
|
+
offset += data[0].zero? ? data[1] - 1 : SECTOR_SIZE - 2
|
|
41
|
+
end
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The track and sector a file's chain starts at.
|
|
46
|
+
def first_block(name, type: :prg)
|
|
47
|
+
entry = find_entry(name, type)
|
|
48
|
+
[entry[:track], entry[:sector]] if entry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The bytes of the chain that starts at the block, or nil when the
|
|
52
|
+
# block is outside the image's geometry.
|
|
53
|
+
def read_file_at(track, sector)
|
|
54
|
+
read_chain(track, sector) if block?(track, sector)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The track and sector of the last block in a file's chain.
|
|
58
|
+
def last_block(name, type: :prg)
|
|
59
|
+
entry = find_entry(name, type)
|
|
60
|
+
return unless entry
|
|
61
|
+
|
|
62
|
+
last = nil
|
|
63
|
+
each_sector(entry[:track], entry[:sector]) { |_data, track, sector| last = [track, sector] }
|
|
64
|
+
last
|
|
65
|
+
end
|
|
66
|
+
|
|
31
67
|
# Raw block access for the DOS `U1` command. Returns nil for blocks
|
|
32
68
|
# outside the image's geometry.
|
|
33
69
|
def read_block(track, sector)
|
|
@@ -44,15 +80,30 @@ module Badline
|
|
|
44
80
|
DOS_ERRORS[@errors[track_offset(track) + sector]]
|
|
45
81
|
end
|
|
46
82
|
|
|
83
|
+
# The block holding the disk name and ID, at the start of the
|
|
84
|
+
# directory track.
|
|
85
|
+
def header_block = [directory_track, 0]
|
|
86
|
+
|
|
87
|
+
# The directory block a new file's entry goes into: the first with a
|
|
88
|
+
# free slot, or the last one when every slot is taken.
|
|
89
|
+
def new_entry_block
|
|
90
|
+
last = nil
|
|
91
|
+
each_sector(directory_track, directory_sector) do |data, track, sector|
|
|
92
|
+
return [track, sector] if (0...ENTRIES_PER_SECTOR).any? { |i| data[(i * ENTRY_SIZE) + 2].zero? }
|
|
93
|
+
|
|
94
|
+
last = [track, sector]
|
|
95
|
+
end
|
|
96
|
+
last
|
|
97
|
+
end
|
|
98
|
+
|
|
47
99
|
private
|
|
48
100
|
|
|
49
|
-
# An error table holds one byte per block
|
|
50
|
-
#
|
|
101
|
+
# An error table holds one byte per block after the last one. The image
|
|
102
|
+
# sizes that carry one are listed exactly, since a 40-track D64 with a
|
|
103
|
+
# table is also a whole number of 256-byte blocks.
|
|
51
104
|
def split_error_table
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@bytes.pop(@bytes.length / (SECTOR_SIZE + 1))
|
|
105
|
+
blocks = error_tables[@bytes.length]
|
|
106
|
+
@bytes.pop(blocks) if blocks
|
|
56
107
|
end
|
|
57
108
|
|
|
58
109
|
def block?(track, sector)
|
|
@@ -62,9 +113,19 @@ module Badline
|
|
|
62
113
|
sector_offset(track, sector) + SECTOR_SIZE <= @bytes.length
|
|
63
114
|
end
|
|
64
115
|
|
|
116
|
+
def find_entry(name, type)
|
|
117
|
+
pattern = Storage.matcher(name)
|
|
118
|
+
entries.find do |e|
|
|
119
|
+
(type.nil? || e[:type] == type) && pattern.match?(e[:name])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
65
123
|
def entries
|
|
66
|
-
@entries ||=
|
|
67
|
-
|
|
124
|
+
@entries ||= begin
|
|
125
|
+
list = []
|
|
126
|
+
each_sector(directory_track, directory_sector) { |data| list.concat(parse_entries(data)) }
|
|
127
|
+
list
|
|
128
|
+
end
|
|
68
129
|
end
|
|
69
130
|
|
|
70
131
|
def parse_entries(data)
|
|
@@ -85,19 +146,19 @@ module Badline
|
|
|
85
146
|
end
|
|
86
147
|
|
|
87
148
|
def read_chain(track, sector)
|
|
88
|
-
|
|
89
|
-
|
|
149
|
+
bytes = []
|
|
150
|
+
each_sector(track, sector) do |data|
|
|
151
|
+
bytes.concat(data[0].zero? ? data[2..data[1]] : data[2..])
|
|
90
152
|
end
|
|
153
|
+
bytes
|
|
91
154
|
end
|
|
92
155
|
|
|
93
156
|
def each_sector(track, sector)
|
|
94
|
-
return to_enum(:each_sector, track, sector) unless block_given?
|
|
95
|
-
|
|
96
157
|
visited = {}
|
|
97
158
|
while track != 0 && !visited[[track, sector]]
|
|
98
159
|
visited[[track, sector]] = true
|
|
99
160
|
data = sector_at(track, sector)
|
|
100
|
-
yield data
|
|
161
|
+
yield data, track, sector
|
|
101
162
|
track, sector = data[0, 2]
|
|
102
163
|
end
|
|
103
164
|
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Storage
|
|
5
|
+
# A host directory served as a drive: .prg files by their host name,
|
|
6
|
+
# .p00 and .t64 entries by their embedded names. A .prg the host can't
|
|
7
|
+
# read is still listed but fails to read, a .p00 or .t64 whose names
|
|
8
|
+
# can't be read is left out, and a failed write reports false.
|
|
5
9
|
class HostDirectory
|
|
10
|
+
NO_SYNC = 21
|
|
11
|
+
|
|
6
12
|
def initialize(path)
|
|
7
13
|
@path = path
|
|
8
14
|
end
|
|
@@ -12,12 +18,24 @@ module Badline
|
|
|
12
18
|
return unless entry
|
|
13
19
|
|
|
14
20
|
archive = entry[:archive]
|
|
15
|
-
archive ? archive.read_file(entry[:name]) : file_bytes(entry[:file])
|
|
21
|
+
archive ? archive.read_file(entry[:name]) : file_bytes(entry[:file]) || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A listed file the host can't read fails before its first byte, as
|
|
25
|
+
# a 1541 does on a disk it finds no sync on.
|
|
26
|
+
def read_error(name, **)
|
|
27
|
+
entry = find(name)
|
|
28
|
+
return unless entry && entry[:file] && !file_bytes(entry[:file])
|
|
29
|
+
|
|
30
|
+
{ error: NO_SYNC, track: 0, sector: 0, offset: 0 }
|
|
16
31
|
end
|
|
17
32
|
|
|
18
33
|
def write_file(name, bytes)
|
|
19
34
|
host_name = "#{name.downcase.tr('/', '_')}.prg"
|
|
20
35
|
File.binwrite(File.join(@path, host_name), bytes.pack("C*"))
|
|
36
|
+
true
|
|
37
|
+
rescue SystemCallError
|
|
38
|
+
false
|
|
21
39
|
end
|
|
22
40
|
|
|
23
41
|
private
|
|
@@ -29,6 +47,8 @@ module Badline
|
|
|
29
47
|
|
|
30
48
|
def entries
|
|
31
49
|
Dir.children(@path).sort.flat_map { |f| entry_for(f) }.compact
|
|
50
|
+
rescue SystemCallError
|
|
51
|
+
[]
|
|
32
52
|
end
|
|
33
53
|
|
|
34
54
|
def entry_for(file)
|
|
@@ -45,18 +65,22 @@ module Badline
|
|
|
45
65
|
def p00_entry(file)
|
|
46
66
|
header = File.binread(File.join(@path, file), P00::HEADER_SIZE).bytes
|
|
47
67
|
{ name: P00.name(header), file: } if P00.wraps?(header)
|
|
68
|
+
rescue SystemCallError
|
|
69
|
+
nil
|
|
48
70
|
end
|
|
49
71
|
|
|
50
72
|
def t64_entries(file)
|
|
51
73
|
archive = T64.new(File.join(@path, file))
|
|
52
74
|
archive.names.map { |name| { name:, archive: } }
|
|
53
|
-
rescue T64::FormatError
|
|
75
|
+
rescue T64::FormatError, SystemCallError
|
|
54
76
|
nil
|
|
55
77
|
end
|
|
56
78
|
|
|
57
79
|
def file_bytes(file)
|
|
58
80
|
bytes = File.binread(File.join(@path, file)).bytes
|
|
59
81
|
P00.wraps?(bytes) ? P00.data(bytes) : bytes
|
|
82
|
+
rescue SystemCallError
|
|
83
|
+
nil
|
|
60
84
|
end
|
|
61
85
|
end
|
|
62
86
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest
|
|
3
|
+
require "digest"
|
|
4
4
|
|
|
5
5
|
module Badline
|
|
6
6
|
module Storage
|
|
@@ -13,6 +13,9 @@ module Badline
|
|
|
13
13
|
|
|
14
14
|
V1_HEADER_SIZE = 0x76
|
|
15
15
|
|
|
16
|
+
# BASIC's RUN handler, where a BASIC tune starts.
|
|
17
|
+
BASIC_RUN = 0xa871
|
|
18
|
+
|
|
16
19
|
# The 6502 stub that starts a tune: call init with the song number,
|
|
17
20
|
# point the KERNAL's IRQ vector at a raster handler that calls play,
|
|
18
21
|
# and spin. Both calls are wrapped in the `$01` save, bank and restore
|
|
@@ -171,6 +174,10 @@ module Badline
|
|
|
171
174
|
# code.
|
|
172
175
|
def mus? = psid? && flags[0] == 1
|
|
173
176
|
|
|
177
|
+
# Flag bit 1 marks an RSID body as a BASIC program, which RUN starts
|
|
178
|
+
# instead of a call to init.
|
|
179
|
+
def basic? = !psid? && flags[1] == 1
|
|
180
|
+
|
|
174
181
|
# A v3 header can place a second SID at `$Dxx0` and a v4 header a
|
|
175
182
|
# third, each given by its middle byte: even, and in `$d420-$d7e0` or
|
|
176
183
|
# `$de00-$dfe0`. Anything else means the SID isn't there.
|
|
@@ -185,16 +192,22 @@ module Badline
|
|
|
185
192
|
# 10 the 8580, 11 either and 00 unknown. Only an 8580-only tune gets one.
|
|
186
193
|
def sid_model = flags[4, 2] == 0b10 ? :mos8580 : :mos6581
|
|
187
194
|
|
|
195
|
+
# Flag bits 2-3 name the video standard the same way: 01 PAL, 10 NTSC,
|
|
196
|
+
# 11 either and 00 unknown. Only an NTSC-only tune gets NTSC.
|
|
197
|
+
def ntsc? = flags[2, 2] == 0b10
|
|
198
|
+
|
|
188
199
|
# HVSC keys its song length database on the whole file, header and all.
|
|
189
200
|
def md5 = @md5 ||= Digest::MD5.hexdigest(@bytes.pack("C*"))
|
|
190
201
|
|
|
191
202
|
# The `$01` value a routine at `address` has to run under, following
|
|
192
203
|
# libsidplayfp's iomap: RAM under BASIC from `$a000`, RAM under both
|
|
193
204
|
# ROMs and no I/O for a routine in the I/O window itself, RAM under the
|
|
194
|
-
# KERNAL from `$e000`.
|
|
205
|
+
# KERNAL from `$e000`. A routine below `$a000` still gets BASIC banked
|
|
206
|
+
# out when the image reaches into it, as VSID does, since it may call
|
|
207
|
+
# into the part that lies there. An RSID tune banks itself and gets nil.
|
|
195
208
|
def bank_for(address)
|
|
196
209
|
return unless psid?
|
|
197
|
-
return 0x37 if address < 0xa000
|
|
210
|
+
return (under_basic? ? 0x36 : 0x37) if address < 0xa000
|
|
198
211
|
return 0x36 if address < 0xd000
|
|
199
212
|
return 0x34 if address < 0xe000
|
|
200
213
|
|
|
@@ -227,6 +240,23 @@ module Badline
|
|
|
227
240
|
Driver.new(self, song: song.clamp(1, songs) - 1, base: driver_address).bytes
|
|
228
241
|
end
|
|
229
242
|
|
|
243
|
+
# What a booted machine needs in RAM besides the image before
|
|
244
|
+
# `boot_command` starts the tune, as address => bytes. A BASIC tune
|
|
245
|
+
# gets the end of its program in VARTAB, as LOAD leaves it, and the
|
|
246
|
+
# song in the A, X and Y that SYS loads from `$030c-$030e`, where the
|
|
247
|
+
# tune reads it with PEEK(780).
|
|
248
|
+
def boot_memory(song: start_song)
|
|
249
|
+
return { driver_address => driver(song:) } unless basic?
|
|
250
|
+
|
|
251
|
+
{ 0x2d => [low_byte(end_address), high_byte(end_address)],
|
|
252
|
+
0x030c => [song.clamp(1, songs) - 1] * 3 }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def boot_command = basic? ? "run\r" : "sys#{driver_address}\r"
|
|
256
|
+
|
|
257
|
+
# Where the CPU first lands once `boot_command` has started the tune.
|
|
258
|
+
def entry_address = basic? ? BASIC_RUN : driver_address
|
|
259
|
+
|
|
230
260
|
private
|
|
231
261
|
|
|
232
262
|
def truncated?
|
|
@@ -237,6 +267,10 @@ module Badline
|
|
|
237
267
|
@body ||= @bytes[data_offset..] || []
|
|
238
268
|
end
|
|
239
269
|
|
|
270
|
+
def under_basic?
|
|
271
|
+
load_address < 0xc000 && end_address > 0xa000
|
|
272
|
+
end
|
|
273
|
+
|
|
240
274
|
# The tape buffer and the free block below the stack page, neither
|
|
241
275
|
# of which BASIC touches.
|
|
242
276
|
def candidates
|
data/lib/badline/storage.rb
CHANGED
|
@@ -36,8 +36,10 @@ module Badline
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
|
|
39
|
+
# The DOS stops comparing at the first "*", so whatever follows it
|
|
40
|
+
# is ignored.
|
|
39
41
|
def matcher(name)
|
|
40
|
-
escaped = Regexp.escape(name.downcase)
|
|
42
|
+
escaped = Regexp.escape(name.downcase.sub(/\*.*/m, "*"))
|
|
41
43
|
.gsub('\*', ".*")
|
|
42
44
|
.gsub('\?', ".")
|
|
43
45
|
Regexp.new("\\A#{escaped}\\z")
|
data/lib/badline/time_of_day.rb
CHANGED
|
@@ -49,6 +49,13 @@ module Badline
|
|
|
49
49
|
@latch[:hours]
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# The clock and alarm as stored, read without latching the clock.
|
|
53
|
+
def registers
|
|
54
|
+
[@clock[:tenths], @clock[:seconds], @clock[:minutes], @clock[:hours],
|
|
55
|
+
@alarm[:tenths], @alarm[:seconds], @alarm[:minutes], @alarm[:hours],
|
|
56
|
+
@stopped ? 1 : 0, @latch ? 1 : 0]
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def write(field, value, alarm:)
|
|
53
60
|
# The frequency counter is held clear while the clock is stopped, and
|
|
54
61
|
# starts counting again from the write to tenths that restarts it.
|
data/lib/badline/version.rb
CHANGED