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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/CLAUDE.md +6 -0
  4. data/README.md +24 -6
  5. data/doc/pinned-behaviour.md +18 -6
  6. data/exe/badline +5 -2
  7. data/lib/badline/address_bus.rb +14 -6
  8. data/lib/badline/addressable.rb +1 -1
  9. data/lib/badline/audio/bare_player.rb +20 -3
  10. data/lib/badline/audio/machine_player.rb +9 -6
  11. data/lib/badline/audio/renderer.rb +3 -3
  12. data/lib/badline/audio/stream.rb +117 -0
  13. data/lib/badline/audio.rb +1 -0
  14. data/lib/badline/cartridge/easy_flash.rb +6 -2
  15. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  16. data/lib/badline/cartridge/mach5.rb +1 -1
  17. data/lib/badline/cartridge/magic_desk.rb +5 -1
  18. data/lib/badline/cartridge/ocean.rb +5 -2
  19. data/lib/badline/cartridge/rex_utility.rb +1 -1
  20. data/lib/badline/cartridge/rgcd.rb +1 -1
  21. data/lib/badline/cartridge/simons_basic.rb +2 -2
  22. data/lib/badline/cartridge/westermann.rb +2 -2
  23. data/lib/badline/cartridge.rb +16 -8
  24. data/lib/badline/checkpoint.rb +98 -0
  25. data/lib/badline/cia/interrupt_register.rb +20 -6
  26. data/lib/badline/cia.rb +36 -19
  27. data/lib/badline/color_memory.rb +2 -2
  28. data/lib/badline/computer.rb +41 -11
  29. data/lib/badline/cpu/microcode.rb +41 -43
  30. data/lib/badline/cpu.rb +3 -3
  31. data/lib/badline/gui/application.rb +41 -9
  32. data/lib/badline/kernal_trap/channel.rb +118 -8
  33. data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
  34. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  35. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  36. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  37. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  38. data/lib/badline/kernal_trap/drive.rb +148 -132
  39. data/lib/badline/kernal_trap/file.rb +0 -5
  40. data/lib/badline/kernal_trap/load.rb +93 -25
  41. data/lib/badline/kernal_trap/routine.rb +17 -0
  42. data/lib/badline/kernal_trap/save.rb +48 -15
  43. data/lib/badline/kernal_trap/serial.rb +34 -2
  44. data/lib/badline/media.rb +2 -2
  45. data/lib/badline/memory.rb +7 -2
  46. data/lib/badline/rom.rb +14 -5
  47. data/lib/badline/sid/catch_up.rb +110 -0
  48. data/lib/badline/sid/decimator.rb +6 -1
  49. data/lib/badline/sid/filter.rb +5 -5
  50. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  51. data/lib/badline/sid/waveform.rb +7 -7
  52. data/lib/badline/sid.rb +16 -92
  53. data/lib/badline/status.rb +255 -32
  54. data/lib/badline/storage/d64_image.rb +6 -0
  55. data/lib/badline/storage/d71_image.rb +4 -0
  56. data/lib/badline/storage/d81_image.rb +4 -0
  57. data/lib/badline/storage/disk_image.rb +78 -17
  58. data/lib/badline/storage/host_directory.rb +26 -2
  59. data/lib/badline/storage/sid_file.rb +37 -3
  60. data/lib/badline/storage.rb +3 -1
  61. data/lib/badline/time_of_day.rb +7 -0
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/graphics_shifter.rb +8 -2
  64. data/lib/badline/vic/registers.rb +16 -27
  65. data/lib/badline/vic.rb +23 -10
  66. data/lib/badline.rb +1 -0
  67. metadata +9 -1
@@ -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
 
@@ -3,8 +3,12 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D71Image < D64Image
6
+ ERROR_TABLES = { 351_062 => 1366 }.freeze
7
+
6
8
  private
7
9
 
10
+ def error_tables = ERROR_TABLES
11
+
8
12
  def sectors_in(track)
9
13
  track > 35 ? super(track - 35) : super
10
14
  end
@@ -3,8 +3,12 @@
3
3
  module Badline
4
4
  module Storage
5
5
  class D81Image < DiskImage
6
+ ERROR_TABLES = { 822_400 => 3200 }.freeze
7
+
6
8
  private
7
9
 
10
+ def error_tables = ERROR_TABLES
11
+
8
12
  def directory_track = 40
9
13
  def directory_sector = 3
10
14
 
@@ -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
- pattern = Storage.matcher(name)
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, so the image is 257 bytes
50
- # per block rather than 256.
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
- return unless (@bytes.length % (SECTOR_SIZE + 1)).zero? &&
53
- !(@bytes.length % SECTOR_SIZE).zero?
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 ||= each_sector(directory_track, directory_sector)
67
- .flat_map { |data| parse_entries(data) }
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
- each_sector(track, sector).flat_map do |data|
89
- data[0].zero? ? data[2..data[1]] : data[2..]
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/md5"
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`. An RSID tune banks itself and gets nil.
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
@@ -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")
@@ -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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end