badline 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +45 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +111 -61
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +275 -33
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +22 -10
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +2 -2
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge.rb +1 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +15 -8
- data/lib/badline/computer.rb +28 -13
- data/lib/badline/gui/application.rb +33 -21
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +3 -0
- data/lib/badline/kernal_trap/drive/block_commands.rb +33 -4
- data/lib/badline/kernal_trap/drive/status.rb +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +51 -51
- data/lib/badline/kernal_trap/file.rb +6 -9
- data/lib/badline/kernal_trap/save.rb +9 -4
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +42 -11
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +1 -1
- data/lib/badline/sid.rb +3 -1
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +27 -4
- data/lib/badline/storage/host_directory.rb +2 -1
- data/lib/badline/storage.rb +15 -0
- data/lib/badline/time_of_day.rb +4 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +42 -7
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +93 -44
- data/lib/badline.rb +4 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +30 -20
- data/exe/badline +0 -78
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
class Drive
|
|
6
|
+
# Opens for writing, the S command and the reports a write makes. A
|
|
7
|
+
# file open for writing takes the type its name asks for, or SEQ on a
|
|
8
|
+
# data channel. On a write-protected disk, or storage that doesn't
|
|
9
|
+
# write files through channels, an open for writing fails.
|
|
10
|
+
module Writes
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# A name already on the disk fails as FILE EXISTS unless @ asks to
|
|
14
|
+
# replace it. A write-protected disk fails anything else and leaves
|
|
15
|
+
# the channel closed.
|
|
16
|
+
def open_write(secondary, name, file, type)
|
|
17
|
+
replace = name.start_with?("@")
|
|
18
|
+
if !replace && @storage.read_file(file, type: nil)
|
|
19
|
+
@channels.close(secondary)
|
|
20
|
+
return report(FILE_EXISTS)
|
|
21
|
+
end
|
|
22
|
+
return refuse_write(secondary) unless writable_disk?
|
|
23
|
+
|
|
24
|
+
open_writer(secondary, WriteFile.new(file, type || :seq, mode: replace ? :replace : :write))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# An A mode open needs the file on the disk.
|
|
28
|
+
def open_append(secondary, file, type)
|
|
29
|
+
unless @storage.read_file(file, type:)
|
|
30
|
+
@channels.close(secondary)
|
|
31
|
+
return report(FILE_NOT_FOUND)
|
|
32
|
+
end
|
|
33
|
+
return refuse_append(secondary, file, type) unless writable_disk?
|
|
34
|
+
|
|
35
|
+
open_writer(secondary, WriteFile.new(file, type, mode: :append))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def open_writer(secondary, channel)
|
|
39
|
+
@channels.open_file(secondary, channel) ? report(OK) : report(NO_CHANNEL)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def writable_disk? = @storage.respond_to?(:writable?) && @storage.writable?
|
|
43
|
+
|
|
44
|
+
# Runs a write to the disk and reports how it went. Returns whether
|
|
45
|
+
# the disk took it.
|
|
46
|
+
def writing
|
|
47
|
+
written = yield != false
|
|
48
|
+
@memory.read_bam if @memory.bam?
|
|
49
|
+
written ? report(OK) : report(WRITE_PROTECT_ON)
|
|
50
|
+
written
|
|
51
|
+
rescue Storage::WriteError => e
|
|
52
|
+
report(e.code)
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# An open for writing on a write-protected disk fails as WRITE
|
|
57
|
+
# PROTECT ON.
|
|
58
|
+
def refuse_write(secondary)
|
|
59
|
+
@channels.close(secondary)
|
|
60
|
+
report(WRITE_PROTECT_ON, *protected_block(secondary))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# An append on a write-protected disk fails at the file's last block,
|
|
64
|
+
# the first one an append writes back.
|
|
65
|
+
def refuse_append(secondary, file, type)
|
|
66
|
+
@channels.close(secondary)
|
|
67
|
+
report(WRITE_PROTECT_ON, *(@storage.last_block(file, type:) if @storage.respond_to?(:last_block)))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# SAVE's channel fails at the directory block its entry would go to,
|
|
71
|
+
# a W mode open at the disk's header block.
|
|
72
|
+
def protected_block(secondary)
|
|
73
|
+
return [] unless @storage.respond_to?(:header_block)
|
|
74
|
+
|
|
75
|
+
secondary == 1 ? @storage.new_entry_block : @storage.header_block
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# S takes names separated by commas, each of which can be a pattern,
|
|
79
|
+
# and reports how many files it scratched.
|
|
80
|
+
def scratch(names)
|
|
81
|
+
return report(WRITE_PROTECT_ON) unless writable_disk?
|
|
82
|
+
|
|
83
|
+
count = 0
|
|
84
|
+
scratched = writing do
|
|
85
|
+
names.split(",").each { |name| count += @storage.scratch(Storage.strip_drive_prefix(name)) }
|
|
86
|
+
end
|
|
87
|
+
report(FILES_SCRATCHED, count) if scratched
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -5,24 +5,31 @@ require "badline/kernal_trap/drive/channels"
|
|
|
5
5
|
require "badline/kernal_trap/drive/memory"
|
|
6
6
|
require "badline/kernal_trap/drive/parameters"
|
|
7
7
|
require "badline/kernal_trap/drive/status"
|
|
8
|
+
require "badline/kernal_trap/drive/writes"
|
|
9
|
+
require "badline/kernal_trap/drive/write_file"
|
|
8
10
|
|
|
9
11
|
module Badline
|
|
10
12
|
module KernalTrap
|
|
11
13
|
# The CBM DOS side of the serial bus. Keeps the open channels, the
|
|
12
14
|
# command channel and its status message. `U1` reads a raw block into a
|
|
13
15
|
# buffer channel, which is how block-access loaders bypass the KERNAL's
|
|
14
|
-
# LOAD.
|
|
16
|
+
# LOAD. A disk image whose host file takes writes takes files, blocks
|
|
17
|
+
# and BAM changes too. Other disks are write-protected, though storage
|
|
18
|
+
# that can write files still takes a SAVE handed over whole.
|
|
15
19
|
class Drive
|
|
16
20
|
include BlockCommands
|
|
21
|
+
include Writes
|
|
17
22
|
|
|
18
23
|
COMMAND_CHANNEL = 15
|
|
19
24
|
BLOCK_SIZE = 256
|
|
20
25
|
|
|
21
26
|
OK = 0
|
|
27
|
+
FILES_SCRATCHED = 1
|
|
22
28
|
WRITE_PROTECT_ON = 26
|
|
23
29
|
SYNTAX_ERROR = 30
|
|
24
30
|
FILE_NOT_FOUND = 62
|
|
25
31
|
FILE_EXISTS = 63
|
|
32
|
+
NO_BLOCK = 65
|
|
26
33
|
ILLEGAL_TRACK_OR_SECTOR = 66
|
|
27
34
|
NO_CHANNEL = 70
|
|
28
35
|
DOS_VERSION = 73
|
|
@@ -50,22 +57,36 @@ module Badline
|
|
|
50
57
|
/\AB-W\s*:?\s*(.*)/i => :counted_block_write,
|
|
51
58
|
/\AI/i => :initialize_disk,
|
|
52
59
|
/\AV/i => :initialized,
|
|
53
|
-
/\AB-
|
|
60
|
+
/\AB-A\s*:?\s*(.*)/i => :block_allocate,
|
|
61
|
+
/\AB-F\s*:?\s*(.*)/i => :block_free,
|
|
62
|
+
/\AS[^:]*:(.*)/im => :scratch
|
|
54
63
|
}.freeze
|
|
55
64
|
|
|
65
|
+
# Commands that take their argument as text rather than numbers.
|
|
66
|
+
TEXT_COMMANDS = %i[scratch].freeze
|
|
67
|
+
|
|
56
68
|
# A drive powers on reporting its DOS version, as a reset does. It
|
|
57
|
-
#
|
|
58
|
-
# initializes the new disk.
|
|
69
|
+
# can take the RAM of a drive it replaces, and initializes its disk.
|
|
59
70
|
def initialize(storage, memory = Memory.new)
|
|
60
|
-
@storage = storage
|
|
61
71
|
@channels = Channels.new
|
|
62
72
|
@status = Status.new
|
|
63
73
|
@memory = memory
|
|
64
|
-
|
|
65
|
-
memory.read_bam
|
|
74
|
+
insert(storage)
|
|
66
75
|
report(DOS_VERSION)
|
|
67
76
|
end
|
|
68
77
|
|
|
78
|
+
# Changes the disk. The drive keeps its RAM and its status, closes
|
|
79
|
+
# its channels, and initializes the new disk.
|
|
80
|
+
def insert(storage)
|
|
81
|
+
@storage = storage
|
|
82
|
+
@channels.clear
|
|
83
|
+
@memory.storage = storage
|
|
84
|
+
@memory.read_bam
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Whether the disk takes a SAVE handed over whole.
|
|
88
|
+
def saves? = @storage.respond_to?(:write_file)
|
|
89
|
+
|
|
69
90
|
# A name on the command channel is a command, "#" opens a block
|
|
70
91
|
# buffer, anything else opens a file for reading. The name comes as
|
|
71
92
|
# the bytes sent, because a memory command's arguments are binary.
|
|
@@ -82,13 +103,19 @@ module Badline
|
|
|
82
103
|
end
|
|
83
104
|
end
|
|
84
105
|
|
|
85
|
-
# Closing the command channel closes every other channel with it
|
|
106
|
+
# Closing the command channel closes every other channel with it,
|
|
107
|
+
# and a file open for writing is lost. Closing a file open for
|
|
108
|
+
# writing writes it to the disk.
|
|
86
109
|
def close(secondary)
|
|
87
|
-
|
|
110
|
+
return @channels.clear if secondary == COMMAND_CHANNEL
|
|
111
|
+
|
|
112
|
+
channel = @channels.close(secondary)
|
|
113
|
+
writing { channel.store(@storage) } if channel.is_a?(WriteFile)
|
|
88
114
|
end
|
|
89
115
|
|
|
90
|
-
# A buffer channel takes the bytes into its block
|
|
91
|
-
#
|
|
116
|
+
# A buffer channel takes the bytes into its block, and a file open for
|
|
117
|
+
# writing gathers them. A file open for reading fails as a
|
|
118
|
+
# write-protected disk does.
|
|
92
119
|
def write(secondary, bytes)
|
|
93
120
|
return command(bytes) if secondary == COMMAND_CHANNEL
|
|
94
121
|
|
|
@@ -99,12 +126,11 @@ module Badline
|
|
|
99
126
|
end
|
|
100
127
|
|
|
101
128
|
# A SAVE the trap hands over whole, to storage that can write files.
|
|
102
|
-
# A
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
saved
|
|
129
|
+
# A disk image refuses a name it already has unless `replace` asks to
|
|
130
|
+
# write over it, and a write the host refuses fails as a
|
|
131
|
+
# write-protected disk does. Returns whether the file was written.
|
|
132
|
+
def save(name, bytes, replace: false)
|
|
133
|
+
writing { @storage.write_file(name, bytes, type: :prg, replace:) }
|
|
108
134
|
end
|
|
109
135
|
|
|
110
136
|
# Whether data sent on the channel reaches the drive. The command
|
|
@@ -139,12 +165,12 @@ module Badline
|
|
|
139
165
|
|
|
140
166
|
# Secondary addresses 0 and 1 are LOAD and SAVE, which look for a PRG
|
|
141
167
|
# file unless the name asks for another type. Other channels take any
|
|
142
|
-
# type the name doesn't pin down.
|
|
168
|
+
# type the name doesn't pin down, and write a SEQ file.
|
|
143
169
|
def open_file(secondary, name)
|
|
144
170
|
file, type = Storage.parse_name(name)
|
|
145
171
|
type ||= :prg if secondary < 2
|
|
146
|
-
return
|
|
147
|
-
return
|
|
172
|
+
return open_write(secondary, name, file, type) if secondary == 1 || mode?(name, "W")
|
|
173
|
+
return open_append(secondary, file, type) if mode?(name, "A")
|
|
148
174
|
|
|
149
175
|
channel = @channels.open_file(secondary, Channel.for_name(@storage, @memory, secondary, name, type))
|
|
150
176
|
return report(NO_CHANNEL) unless channel
|
|
@@ -156,40 +182,14 @@ module Badline
|
|
|
156
182
|
name.split(",").drop(1).any? { |field| field.strip[0]&.upcase == mode }
|
|
157
183
|
end
|
|
158
184
|
|
|
159
|
-
# The disk is write-protected, so an open for writing fails and leaves
|
|
160
|
-
# the channel closed. A name already on the disk fails as FILE EXISTS
|
|
161
|
-
# unless @ asks to replace it, anything else as WRITE PROTECT ON.
|
|
162
|
-
def refuse_write(secondary, name, file)
|
|
163
|
-
@channels.close(secondary)
|
|
164
|
-
return report(FILE_EXISTS) if !name.start_with?("@") && @storage.read_file(file, type: nil)
|
|
165
|
-
|
|
166
|
-
report(WRITE_PROTECT_ON, *protected_block(secondary))
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# An A mode open needs the file on the disk. It fails at the file's
|
|
170
|
-
# last block, the first one an append writes back.
|
|
171
|
-
def refuse_append(secondary, file, type)
|
|
172
|
-
@channels.close(secondary)
|
|
173
|
-
return report(FILE_NOT_FOUND) unless @storage.read_file(file, type:)
|
|
174
|
-
|
|
175
|
-
report(WRITE_PROTECT_ON, *(@storage.last_block(file, type:) if @storage.respond_to?(:last_block)))
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# SAVE's channel fails at the directory block its entry would go to,
|
|
179
|
-
# a W mode open at the disk's header block.
|
|
180
|
-
def protected_block(secondary)
|
|
181
|
-
return [] unless @storage.respond_to?(:header_block)
|
|
182
|
-
|
|
183
|
-
secondary == 1 ? @storage.new_entry_block : @storage.header_block
|
|
184
|
-
end
|
|
185
|
-
|
|
186
185
|
def execute(text)
|
|
187
186
|
command = text.strip
|
|
188
187
|
pattern, action = COMMANDS.find { |candidate, _| candidate.match?(command) }
|
|
189
188
|
return report(SYNTAX_ERROR) unless action
|
|
190
189
|
|
|
191
190
|
action = USER_TABLE[(command.getbyte(1) - 1) & 0x0f] if action == :user
|
|
192
|
-
|
|
191
|
+
argument = pattern.match(command)[1]
|
|
192
|
+
run_command(action, TEXT_COMMANDS.include?(action) ? argument : Parameters.parse(argument))
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
def command(bytes)
|
|
@@ -206,7 +206,9 @@ module Badline
|
|
|
206
206
|
when :buffer_pointer then buffer_pointer(arguments)
|
|
207
207
|
when :initialized then report(OK)
|
|
208
208
|
when :initialize_disk then initialize_disk
|
|
209
|
-
when :
|
|
209
|
+
when :block_allocate then block_allocate(arguments)
|
|
210
|
+
when :block_free then block_free(arguments)
|
|
211
|
+
when :scratch then scratch(arguments)
|
|
210
212
|
when :cold_reset then reset(cold: true)
|
|
211
213
|
when :warm_reset then reset(cold: false)
|
|
212
214
|
when :memory_write then memory_write(arguments)
|
|
@@ -234,8 +236,6 @@ module Badline
|
|
|
234
236
|
arguments[0].to_i | (arguments[1].to_i << 8)
|
|
235
237
|
end
|
|
236
238
|
|
|
237
|
-
def write_protected(_arguments) = report(WRITE_PROTECT_ON)
|
|
238
|
-
|
|
239
239
|
def reset(cold:)
|
|
240
240
|
@memory.clear if cold
|
|
241
241
|
@channels.clear
|
|
@@ -3,26 +3,23 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module KernalTrap
|
|
5
5
|
class File < Routine
|
|
6
|
-
MISSING_FILENAME = 0x08
|
|
7
|
-
|
|
8
6
|
private
|
|
9
7
|
|
|
10
8
|
def active?
|
|
11
9
|
kernal? && @bus.peek(0xba) == DEVICE
|
|
12
10
|
end
|
|
13
11
|
|
|
14
|
-
# Filename pointer at $BB/$BC, length at $B7
|
|
15
12
|
def filename
|
|
13
|
+
Storage.strip_drive_prefix(full_filename)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Filename pointer at $BB/$BC, length at $B7
|
|
17
|
+
def full_filename
|
|
16
18
|
pointer = uint16(@bus.peek(0xbb), @bus.peek(0xbc))
|
|
17
19
|
bytes = Array.new(@bus.peek(0xb7)) do |i|
|
|
18
20
|
@bus.peek((pointer + i) & 0xffff)
|
|
19
21
|
end
|
|
20
|
-
Storage.
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def error(code)
|
|
24
|
-
@cpu.a = code
|
|
25
|
-
@cpu.status.carry = true
|
|
22
|
+
Storage.ascii(bytes)
|
|
26
23
|
end
|
|
27
24
|
end
|
|
28
25
|
end
|
|
@@ -5,12 +5,15 @@ module Badline
|
|
|
5
5
|
# PC trap on the KERNAL serial SAVE routine ($F5ED, the default ISAVE
|
|
6
6
|
# vector target). Hands device 8 saves to the virtual drive as a PRG
|
|
7
7
|
# (load address followed by the memory range); other devices fall
|
|
8
|
-
# through to the ROM
|
|
8
|
+
# through to the ROM, and so do saves to a disk that doesn't take them
|
|
9
|
+
# whole. The ROM prints SAVING in direct mode and returns
|
|
9
10
|
# into the trap, which then writes the file and leaves through the
|
|
10
11
|
# ROM's own tail with the registers its UNLISTEN and return leave. A
|
|
11
|
-
#
|
|
12
|
+
# write that fails, to a full disk, a name already on it or a host file
|
|
13
|
+
# that can't be written, ends the way a 1541 ends a SAVE it can't
|
|
12
14
|
# write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
|
|
13
|
-
# and the ROM returns without an error.
|
|
15
|
+
# and the ROM returns without an error. A "@" before the drive prefix
|
|
16
|
+
# writes over a file of the same name.
|
|
14
17
|
class Save < File
|
|
15
18
|
ADDRESS = 0xf5ed
|
|
16
19
|
|
|
@@ -37,6 +40,7 @@ module Badline
|
|
|
37
40
|
def call
|
|
38
41
|
return unless active?
|
|
39
42
|
return finish if @saving
|
|
43
|
+
return unless @drive.saves?
|
|
40
44
|
|
|
41
45
|
@bus.poke(0xb9, SECONDARY)
|
|
42
46
|
return @cpu.program_counter = MISSING_FILE_NAME_EXIT if name.empty?
|
|
@@ -54,7 +58,8 @@ module Badline
|
|
|
54
58
|
|
|
55
59
|
def finish
|
|
56
60
|
@saving = false
|
|
57
|
-
|
|
61
|
+
saved = @drive.save(name, payload, replace: full_filename.start_with?("@"))
|
|
62
|
+
@bus.poke(0x90, DEVICE_NOT_PRESENT) unless saved
|
|
58
63
|
@bus.poke(0xac, @bus.peek(0xae))
|
|
59
64
|
@bus.poke(0xad, @bus.peek(0xaf))
|
|
60
65
|
@cpu.y = 0
|
data/lib/badline/keyboard.rb
CHANGED
|
@@ -11,6 +11,14 @@ module Badline
|
|
|
11
11
|
class Keyboard
|
|
12
12
|
include IntegerHelper
|
|
13
13
|
|
|
14
|
+
# Keys the C64 types as a combination: cursor up and cursor left are the
|
|
15
|
+
# shifted cursor keys. Pressing one holds both of its matrix keys, while
|
|
16
|
+
# a shift key held on its own stays down when it is released.
|
|
17
|
+
COMBINATIONS = {
|
|
18
|
+
cursor_up: %i[lshift cursor_v],
|
|
19
|
+
cursor_left: %i[lshift cursor_h]
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
14
22
|
attr_reader :keys, :matrix
|
|
15
23
|
|
|
16
24
|
def initialize
|
|
@@ -70,13 +78,21 @@ module Badline
|
|
|
70
78
|
|
|
71
79
|
# Columns held down per row, as a bit mask of port B lines.
|
|
72
80
|
def row_masks
|
|
73
|
-
@row_masks ||=
|
|
74
|
-
|
|
81
|
+
@row_masks ||= begin
|
|
82
|
+
pressed = held
|
|
83
|
+
matrix.map do |row|
|
|
84
|
+
row.each_with_index.sum { |key, column| pressed.include?(key) ? 1 << column : 0 }
|
|
85
|
+
end
|
|
75
86
|
end
|
|
76
87
|
end
|
|
77
88
|
|
|
89
|
+
# The matrix keys held down, with each combination expanded.
|
|
90
|
+
def held
|
|
91
|
+
keys.flat_map { |key| COMBINATIONS[key] || [key] }
|
|
92
|
+
end
|
|
93
|
+
|
|
78
94
|
def valid_key?(key)
|
|
79
|
-
matrix.flatten.include?(key)
|
|
95
|
+
COMBINATIONS.key?(key) || matrix.flatten.include?(key)
|
|
80
96
|
end
|
|
81
97
|
end
|
|
82
98
|
end
|
data/lib/badline/media.rb
CHANGED
|
@@ -14,25 +14,39 @@ module Badline
|
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
class << self
|
|
17
|
-
# `cartridge
|
|
18
|
-
# as `{ flash_jumper: true }` for
|
|
19
|
-
|
|
17
|
+
# The options are the medium's own. `cartridge:` sets the jumpers of a
|
|
18
|
+
# .crt cartridge that has them, such as `{ flash_jumper: true }` for
|
|
19
|
+
# the Retro Replay's flash mode, and `disk: { read_only: true }` mounts
|
|
20
|
+
# a disk image write-protected, so nothing the program does writes to
|
|
21
|
+
# its file. Media they don't apply to ignore them.
|
|
22
|
+
def attach(computer, path, autostart: true, song: nil, **options)
|
|
20
23
|
if File.directory?(path)
|
|
21
24
|
computer.mount(Storage::HostDirectory.new(path))
|
|
22
25
|
"Mounted #{path} as device 8"
|
|
23
26
|
elsif File.extname(path).downcase == ".crt"
|
|
24
|
-
attach_cartridge(computer, path, cartridge)
|
|
27
|
+
attach_cartridge(computer, path, options.fetch(:cartridge, {}))
|
|
25
28
|
elsif File.extname(path).downcase == ".sid"
|
|
26
29
|
attach_sid(computer, path, autostart:, song:)
|
|
27
30
|
elsif File.extname(path).downcase == ".tap"
|
|
28
31
|
attach_tape(computer, path, autostart:)
|
|
29
|
-
elsif (
|
|
30
|
-
attach_storage(computer,
|
|
32
|
+
elsif MOUNT_TYPES.key?(File.extname(path).downcase)
|
|
33
|
+
attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
|
|
31
34
|
else
|
|
32
35
|
attach_prg(computer, path, autostart:)
|
|
33
36
|
end
|
|
34
37
|
end
|
|
35
38
|
|
|
39
|
+
# Swaps the disk in device 8 for a disk image or a host directory,
|
|
40
|
+
# while the machine runs, without loading anything. The drive keeps
|
|
41
|
+
# its RAM and its status. `read_only` inserts a disk image
|
|
42
|
+
# write-protected.
|
|
43
|
+
def insert_disk(computer, path, read_only: false)
|
|
44
|
+
raise ArgumentError, "#{path} is not a disk image or a directory" unless disk?(path)
|
|
45
|
+
|
|
46
|
+
computer.mount(open_storage(path, { read_only: }))
|
|
47
|
+
"Inserted #{path} in device 8"
|
|
48
|
+
end
|
|
49
|
+
|
|
36
50
|
# The SID a machine for `path` should be built with. A .sid tune names
|
|
37
51
|
# its own; everything else gets the 6581.
|
|
38
52
|
def sid_model(path)
|
|
@@ -43,6 +57,20 @@ module Badline
|
|
|
43
57
|
|
|
44
58
|
private
|
|
45
59
|
|
|
60
|
+
def disk?(path)
|
|
61
|
+
storage = MOUNT_TYPES[File.extname(path).downcase]
|
|
62
|
+
File.directory?(path) || (!storage.nil? && storage < Storage::DiskImage)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Disk images take the `disk` options. A .t64 is read-only whatever
|
|
66
|
+
# it's given.
|
|
67
|
+
def open_storage(path, disk)
|
|
68
|
+
return Storage::HostDirectory.new(path) if File.directory?(path)
|
|
69
|
+
|
|
70
|
+
storage = MOUNT_TYPES[File.extname(path).downcase]
|
|
71
|
+
storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
|
|
72
|
+
end
|
|
73
|
+
|
|
46
74
|
def attach_cartridge(computer, path, options)
|
|
47
75
|
computer.attach_cartridge(Cartridge.from_file(path, **options))
|
|
48
76
|
"Attached cartridge #{path}"
|
|
@@ -70,8 +98,8 @@ module Badline
|
|
|
70
98
|
"Inserted #{path} in the datasette"
|
|
71
99
|
end
|
|
72
100
|
|
|
73
|
-
def attach_storage(computer,
|
|
74
|
-
computer.mount(
|
|
101
|
+
def attach_storage(computer, path, disk, autostart:)
|
|
102
|
+
computer.mount(open_storage(path, disk))
|
|
75
103
|
computer.type_text(AUTOSTART) if autostart
|
|
76
104
|
"Mounted #{path} as device 8"
|
|
77
105
|
end
|
|
@@ -85,11 +113,14 @@ module Badline
|
|
|
85
113
|
|
|
86
114
|
def start_prg(computer, data, autostart:)
|
|
87
115
|
load_addr = computer.load_prg(data)
|
|
88
|
-
# Run only makes sense for programs at BASIC start
|
|
89
|
-
|
|
116
|
+
# Run only makes sense for programs at BASIC start, or a byte ahead
|
|
117
|
+
# of it, where BASIC keeps the zero before its first line.
|
|
118
|
+
return unless autostart && (load_addr == BASIC_START || load_addr == BASIC_START - 1)
|
|
90
119
|
|
|
120
|
+
# The end of the program, where BASIC's variables start, and where
|
|
121
|
+
# the KERNAL's LOAD leaves its end address.
|
|
91
122
|
end_addr = load_addr + data.length - 2
|
|
92
|
-
computer.ram.write(
|
|
123
|
+
[0x2d, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
|
|
93
124
|
computer.type_text("run\r")
|
|
94
125
|
end
|
|
95
126
|
end
|