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
|
@@ -4,29 +4,139 @@ module Badline
|
|
|
4
4
|
module KernalTrap
|
|
5
5
|
# One open channel on the virtual drive, holding a file, a block buffer
|
|
6
6
|
# or the command channel's status message. It gives out one byte at a
|
|
7
|
-
# time and flags the last one, so the bus can raise EOI with it.
|
|
7
|
+
# time and flags the last one, so the bus can raise EOI with it. A
|
|
8
|
+
# channel cut short by a read error never flags one, and holds the
|
|
9
|
+
# error for the drive to report once its bytes run out.
|
|
8
10
|
class Channel
|
|
9
|
-
attr_accessor :
|
|
11
|
+
attr_accessor :buffer
|
|
12
|
+
attr_reader :error, :pointer
|
|
13
|
+
attr_writer :lead
|
|
10
14
|
|
|
11
|
-
def initialize(bytes = [])
|
|
15
|
+
def initialize(bytes = [], error: nil, writable: false, base: 0)
|
|
16
|
+
@base = base
|
|
12
17
|
replace(bytes)
|
|
18
|
+
@error = error
|
|
19
|
+
@writable = writable
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
# A block buffer opened with "#", which reads and writes one of the
|
|
23
|
+
# drive's buffers in its RAM, so M-R and M-W reach the same bytes. It
|
|
24
|
+
# hands out the whole block unless a B-R sets a shorter end, and
|
|
25
|
+
# flags that byte as the last. The DOS opens it with the pointer at 1
|
|
26
|
+
# and the buffer's number as the byte to send, so the first read
|
|
27
|
+
# answers with the number and the next one moves on to the third
|
|
28
|
+
# byte.
|
|
29
|
+
def self.buffer(memory, number)
|
|
30
|
+
new(memory.ram, writable: true, base: Drive::Memory.buffer_address(number)).tap do |channel|
|
|
31
|
+
channel.buffer = number
|
|
32
|
+
channel.rewind(Drive::BLOCK_SIZE)
|
|
33
|
+
channel.pointer = 1
|
|
34
|
+
channel.lead = number
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A file's bytes, up to the first block its chain can't read. The
|
|
39
|
+
# drive reads a block ahead before sending the last byte of the one
|
|
40
|
+
# before, so a bad block holds that byte back too, and a bad first
|
|
41
|
+
# block or a missing file leaves nothing to send.
|
|
42
|
+
def self.for_file(storage, name, type)
|
|
43
|
+
data = storage.read_file(name, type:)
|
|
44
|
+
return new([], error: [Drive::FILE_NOT_FOUND]) unless data
|
|
45
|
+
|
|
46
|
+
failure = storage.respond_to?(:read_error) && storage.read_error(name, type:)
|
|
47
|
+
return new(data) unless failure
|
|
48
|
+
|
|
49
|
+
new(data[0, failure[:offset] - 1] || [], error: failure.values_at(:error, :track, :sector))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The file an open names. The drive keeps the first block of the file
|
|
53
|
+
# a LOAD (secondary address 0) opens at $7E and $026F of its memory,
|
|
54
|
+
# and a LOAD of a name starting with "*" reopens that file rather than
|
|
55
|
+
# the first one on the disk. Loaders write the block there with M-W
|
|
56
|
+
# to load a file the directory doesn't list.
|
|
57
|
+
def self.for_name(storage, memory, secondary, name, type)
|
|
58
|
+
file = Storage.parse_name(name).first
|
|
59
|
+
return for_file(storage, file, type) unless secondary.zero? && storage.respond_to?(:first_block)
|
|
60
|
+
|
|
61
|
+
last = memory.last_program if name.start_with?("*")
|
|
62
|
+
return at_block(storage, *last) if last
|
|
63
|
+
|
|
64
|
+
block = storage.first_block(file, type:)
|
|
65
|
+
memory.last_program = block if block
|
|
66
|
+
for_file(storage, file, type)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The file whose chain starts at the block.
|
|
70
|
+
def self.at_block(storage, track, sector)
|
|
71
|
+
data = storage.read_file_at(track, sector)
|
|
72
|
+
data ? new(data) : new([], error: [Drive::ILLEGAL_TRACK_OR_SECTOR, track, sector])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def replace(bytes, length = bytes.length)
|
|
16
76
|
@bytes = bytes
|
|
17
|
-
|
|
77
|
+
rewind(length)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rewind(length)
|
|
81
|
+
@length = length
|
|
82
|
+
self.pointer = 0
|
|
18
83
|
end
|
|
19
84
|
|
|
85
|
+
def pointer=(position)
|
|
86
|
+
@pointer = position
|
|
87
|
+
@lead = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A block read into the channel. A buffer channel takes it into its
|
|
91
|
+
# buffer, a file channel hands it out in place of the file.
|
|
92
|
+
def load(block, length)
|
|
93
|
+
return replace(block.dup, length) unless @writable
|
|
94
|
+
|
|
95
|
+
@bytes[@base, block.length] = block
|
|
96
|
+
rewind(length)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def writable? = @writable
|
|
100
|
+
|
|
20
101
|
def exhausted?
|
|
21
|
-
@pointer >= @
|
|
102
|
+
@pointer >= @length
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A file that holds nothing but its error never opened.
|
|
106
|
+
def failed?
|
|
107
|
+
exhausted? && !@error.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Each byte lands at the pointer, which wraps within the block.
|
|
111
|
+
def write(bytes)
|
|
112
|
+
@lead = nil
|
|
113
|
+
bytes.each do |byte|
|
|
114
|
+
@bytes[@base + @pointer] = byte
|
|
115
|
+
@pointer = (@pointer + 1) & 0xff
|
|
116
|
+
end
|
|
22
117
|
end
|
|
23
118
|
|
|
24
119
|
def read
|
|
120
|
+
return read_buffer if @writable
|
|
25
121
|
return if exhausted?
|
|
26
122
|
|
|
27
|
-
byte = @bytes[@pointer]
|
|
123
|
+
byte = @lead || @bytes[@base + @pointer]
|
|
124
|
+
@lead = nil
|
|
28
125
|
@pointer += 1
|
|
29
|
-
[byte, exhausted?]
|
|
126
|
+
[byte, exhausted? && !@error]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# A buffer never runs out. Past its last byte, which goes out with
|
|
132
|
+
# EOI, the pointer wraps back to the second, and a pointer set past
|
|
133
|
+
# the end runs on around the block to it.
|
|
134
|
+
def read_buffer
|
|
135
|
+
byte = @lead || @bytes[@base + @pointer]
|
|
136
|
+
@lead = nil
|
|
137
|
+
last = @pointer == @length - 1
|
|
138
|
+
@pointer = last ? 1 : (@pointer + 1) & 0xff
|
|
139
|
+
[byte, last]
|
|
30
140
|
end
|
|
31
141
|
end
|
|
32
142
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# The commands that read and write a block through a buffer channel,
|
|
7
|
+
# and B-P, which moves its pointer.
|
|
8
|
+
module BlockCommands
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def block_read(arguments, counted: false)
|
|
12
|
+
channel, _drive, track, sector = arguments
|
|
13
|
+
data = fetch_block(channel, track, sector)
|
|
14
|
+
return unless data
|
|
15
|
+
|
|
16
|
+
buffer = @channels[channel]
|
|
17
|
+
buffer.load(data, counted ? data[0] + 1 : data.length)
|
|
18
|
+
buffer.pointer = 1 if counted
|
|
19
|
+
report_block_error(track, sector)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The disk is write-protected, so a block write that gets as far as
|
|
23
|
+
# the disk fails at the block it names.
|
|
24
|
+
def block_write(arguments, counted: false)
|
|
25
|
+
channel, _drive, track, sector = arguments
|
|
26
|
+
return unless fetch_block(channel, track, sector)
|
|
27
|
+
|
|
28
|
+
store_count(@channels[channel]) if counted
|
|
29
|
+
report(WRITE_PROTECT_ON, track, sector)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# B-W first stores the index of the last byte written, the pointer
|
|
33
|
+
# less one but at least 1, as the block's first byte, which leaves
|
|
34
|
+
# the pointer at 1. B-R reads that count back.
|
|
35
|
+
def counted_block_write(arguments)
|
|
36
|
+
block_write(arguments, counted: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def store_count(buffer)
|
|
40
|
+
count = [buffer.pointer - 1, 1].max
|
|
41
|
+
buffer.pointer = 0
|
|
42
|
+
buffer.write([count])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Block access needs an open buffer channel, a disk and a block on it.
|
|
46
|
+
# Returns the block, or nil once it has reported why there is none.
|
|
47
|
+
def fetch_block(channel, track, sector)
|
|
48
|
+
return report(NO_CHANNEL) unless @channels[channel]
|
|
49
|
+
return report(DRIVE_NOT_READY) unless @storage.respond_to?(:read_block)
|
|
50
|
+
|
|
51
|
+
data = sector && @storage.read_block(track, sector)
|
|
52
|
+
data || report(ILLEGAL_TRACK_OR_SECTOR, track, sector)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A block the image's error table marks bad still fills the buffer,
|
|
56
|
+
# but the read reports its error, which copy protection checks for.
|
|
57
|
+
def report_block_error(track, sector)
|
|
58
|
+
error = @storage.block_error(track, sector)
|
|
59
|
+
error ? report(error, track, sector) : report(OK)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# B-R takes the block's first byte as the index of its last one, and
|
|
63
|
+
# hands out the bytes from the second up to there.
|
|
64
|
+
def counted_block_read(arguments)
|
|
65
|
+
block_read(arguments, counted: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def buffer_pointer(arguments)
|
|
69
|
+
channel, position = arguments
|
|
70
|
+
buffer = @channels[channel]
|
|
71
|
+
return report(NO_CHANNEL) unless buffer
|
|
72
|
+
|
|
73
|
+
buffer.pointer = position.to_i & 0xff
|
|
74
|
+
report(OK)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# The drive's open data channels by secondary address, and the five
|
|
7
|
+
# buffers they hold. The DOS hands out the highest free buffer.
|
|
8
|
+
# Reading the BAM when it initializes the disk takes buffer 4, so
|
|
9
|
+
# channels get 3 down to 0, and the fifth data channel finds none.
|
|
10
|
+
class Channels
|
|
11
|
+
BAM_BUFFER = 4
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@channels = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def [](secondary) = @channels[secondary]
|
|
18
|
+
|
|
19
|
+
def open?(secondary) = @channels.key?(secondary)
|
|
20
|
+
|
|
21
|
+
def close(secondary) = @channels.delete(secondary)
|
|
22
|
+
|
|
23
|
+
def clear = @channels.clear
|
|
24
|
+
|
|
25
|
+
# Opens a buffer channel on the buffer a "#n" name asks for, or on
|
|
26
|
+
# the highest free one. Returns nil when that buffer isn't free,
|
|
27
|
+
# which leaves a channel asking again for its own buffer open.
|
|
28
|
+
def open_buffer(secondary, number, memory)
|
|
29
|
+
return if number && !free_buffers.include?(number)
|
|
30
|
+
|
|
31
|
+
close(secondary)
|
|
32
|
+
number ||= free_buffers.max
|
|
33
|
+
@channels[secondary] = Channel.buffer(memory, number) if number
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A file that opens takes a buffer, though its bytes don't pass
|
|
37
|
+
# through drive RAM here. One that fails to open takes none. Returns
|
|
38
|
+
# nil, leaving the channel closed, when no buffer is free.
|
|
39
|
+
def open_file(secondary, channel)
|
|
40
|
+
close(secondary)
|
|
41
|
+
channel.buffer = free_buffers.max unless channel.failed?
|
|
42
|
+
@channels[secondary] = channel if channel.failed? || channel.buffer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def free_buffers
|
|
48
|
+
(0...BAM_BUFFER).to_a - @channels.values.map(&:buffer)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# The drive's 2K of RAM, as M-W and M-R reach it, with the job queue
|
|
7
|
+
# that runs a read job written into it: a job code for each of five
|
|
8
|
+
# buffers at $00, their track and sector pairs from $06, and the
|
|
9
|
+
# buffers from $0300. A job code is replaced by its result, 1 for
|
|
10
|
+
# success or an error table code: the DOS error less 18, or 15 for
|
|
11
|
+
# DRIVE NOT READY.
|
|
12
|
+
class Memory
|
|
13
|
+
SIZE = 0x800
|
|
14
|
+
JOBS = 5
|
|
15
|
+
READ_JOB = 0x80
|
|
16
|
+
JOB_OK = 1
|
|
17
|
+
HEADER_NOT_FOUND = 20
|
|
18
|
+
|
|
19
|
+
# The RAM itself, which buffer channels read and write in place.
|
|
20
|
+
attr_reader :ram
|
|
21
|
+
|
|
22
|
+
# The disk that read jobs read from.
|
|
23
|
+
attr_writer :storage
|
|
24
|
+
|
|
25
|
+
# Buffer n takes the page $0300 + n * $100.
|
|
26
|
+
def self.buffer_address(number)
|
|
27
|
+
0x300 + (number * BLOCK_SIZE)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@storage = nil
|
|
32
|
+
@ram = Array.new(SIZE, 0)
|
|
33
|
+
@bam = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Writes past the RAM are dropped, since the rest of the address
|
|
37
|
+
# space is I/O and ROM.
|
|
38
|
+
def write(address, bytes)
|
|
39
|
+
bytes.each.with_index(address) do |byte, target|
|
|
40
|
+
@ram[target] = byte if target < SIZE
|
|
41
|
+
end
|
|
42
|
+
run_jobs
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Only the RAM reads back.
|
|
46
|
+
def read(address, count)
|
|
47
|
+
Array.new(count) { |i| @ram.fetch(address + i, 0) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A reset through the reset vector clears the RAM, and the BAM
|
|
51
|
+
# with it, until the next open initializes the disk again.
|
|
52
|
+
def clear
|
|
53
|
+
@ram.fill(0)
|
|
54
|
+
@bam = false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Initializing the disk reads its BAM, the header block, into buffer
|
|
58
|
+
# 4, where M-R finds it. Storage without blocks has none.
|
|
59
|
+
def read_bam
|
|
60
|
+
@bam = true
|
|
61
|
+
block = @storage.respond_to?(:header_block) && @storage.read_block(*@storage.header_block)
|
|
62
|
+
load_buffer(Channels::BAM_BUFFER, block) if block
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Whether the BAM has been read since the last reset.
|
|
66
|
+
def bam? = @bam
|
|
67
|
+
|
|
68
|
+
def load_buffer(number, block)
|
|
69
|
+
@ram[Memory.buffer_address(number), block.length] = block
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The first block of the file the last LOAD read, which the DOS
|
|
73
|
+
# keeps at $7E and $026F, or nil while the track there is 0.
|
|
74
|
+
def last_program
|
|
75
|
+
[@ram[0x7e], @ram[0x26f]] unless @ram[0x7e].zero?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def last_program=(block)
|
|
79
|
+
@ram[0x7e], @ram[0x26f] = block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def run_jobs
|
|
85
|
+
JOBS.times do |job|
|
|
86
|
+
next unless @ram[job] == READ_JOB
|
|
87
|
+
|
|
88
|
+
track, sector = @ram[6 + (job * 2), 2]
|
|
89
|
+
@ram[job] = read_job(job, track, sector)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read_job(number, track, sector)
|
|
94
|
+
data = @storage.respond_to?(:read_block) && @storage.read_block(track, sector)
|
|
95
|
+
return job_result(HEADER_NOT_FOUND) unless data
|
|
96
|
+
|
|
97
|
+
load_buffer(number, data)
|
|
98
|
+
job_result(@storage.block_error(track, sector))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def job_result(error)
|
|
102
|
+
return JOB_OK unless error
|
|
103
|
+
|
|
104
|
+
error == DRIVE_NOT_READY ? 15 : error - 18
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# The numbers a DOS command takes. The DOS skips spaces, commas and
|
|
7
|
+
# cursor-rights before a parameter, reads it from the characters $30
|
|
8
|
+
# to $3F that follow, the low nibble of each, and skips whatever
|
|
9
|
+
# character ends it. So "18,\x02" reads as 18 and 0, ":" to "?" are
|
|
10
|
+
# digits 10 to 15, and a run of digits keeps its last three.
|
|
11
|
+
module Parameters
|
|
12
|
+
# Space, comma and cursor-right
|
|
13
|
+
GAPS = [0x20, 0x2c, 0x1d].freeze
|
|
14
|
+
|
|
15
|
+
def self.parse(text)
|
|
16
|
+
rest = text.to_s.bytes
|
|
17
|
+
values = []
|
|
18
|
+
until (rest = rest.drop_while { |byte| GAPS.include?(byte) }).empty?
|
|
19
|
+
digits = rest.take_while { |byte| byte.between?(0x30, 0x3f) }
|
|
20
|
+
values << digits.last(3).reduce(0) { |value, digit| (value * 10) + (digit & 0x0f) }
|
|
21
|
+
rest = rest.drop(digits.length + 1)
|
|
22
|
+
end
|
|
23
|
+
values
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# The command channel's side of the drive: the last DOS error with the
|
|
7
|
+
# track and sector it happened at, or the bytes an M-R read. Once its
|
|
8
|
+
# last byte has been read, it holds OK again.
|
|
9
|
+
class Status
|
|
10
|
+
MESSAGES = [20, 21, 22, 23, 24, 27].to_h { |code| [code, "READ ERROR"] }.merge(
|
|
11
|
+
0 => " OK",
|
|
12
|
+
25 => "WRITE ERROR",
|
|
13
|
+
26 => "WRITE PROTECT ON",
|
|
14
|
+
28 => "WRITE ERROR",
|
|
15
|
+
29 => "DISK ID MISMATCH",
|
|
16
|
+
30 => "SYNTAX ERROR",
|
|
17
|
+
62 => "FILE NOT FOUND",
|
|
18
|
+
63 => "FILE EXISTS",
|
|
19
|
+
66 => "ILLEGAL TRACK OR SECTOR",
|
|
20
|
+
70 => "NO CHANNEL",
|
|
21
|
+
73 => "CBM DOS V2.6 1541",
|
|
22
|
+
74 => "DRIVE NOT READY"
|
|
23
|
+
).freeze
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@channel = Channel.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def report(code, track = 0, sector = 0)
|
|
30
|
+
message = format("%<code>02d,%<message>s,%<track>02d,%<sector>02d\r",
|
|
31
|
+
code:, message: MESSAGES.fetch(code),
|
|
32
|
+
track: track.to_i, sector: sector.to_i)
|
|
33
|
+
@channel.replace(message.bytes)
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def replace(bytes)
|
|
38
|
+
@channel.replace(bytes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def read
|
|
42
|
+
result = @channel.read
|
|
43
|
+
report(0) if result&.last
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|