badline 0.15.1 → 0.16.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 +39 -0
- data/CLAUDE.md +8 -7
- data/README.md +102 -6
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +213 -18
- data/doc/snapshots.md +14 -0
- data/lib/badline/c64/common.rb +1 -1
- data/lib/badline/chips/crtc/keyed_state.rb +46 -0
- data/lib/badline/chips/crtc.rb +163 -0
- data/lib/badline/chips/pia/control_lines.rb +138 -0
- data/lib/badline/chips/pia/keyed_parts.rb +56 -0
- data/lib/badline/chips/pia.rb +138 -0
- data/lib/badline/chips/vic/register_log.rb +1 -4
- data/lib/badline/core.rb +3 -0
- data/lib/badline/drives/ieee_bus.rb +28 -0
- data/lib/badline/drives/model1551/cable.rb +5 -2
- data/lib/badline/drives/model1551/tia.rb +13 -34
- data/lib/badline/drives/model1551.rb +26 -4
- data/lib/badline/drives.rb +1 -0
- data/lib/badline/frontend/app.rb +1 -1
- data/lib/badline/frontend/controls.rb +23 -3
- data/lib/badline/frontend/gamepads.rb +10 -3
- data/lib/badline/frontend/keys.rb +88 -0
- data/lib/badline/frontend/media_slots.rb +1 -0
- data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
- data/lib/badline/frontend/menu/menu_media.rb +27 -28
- data/lib/badline/frontend/menu/menu_pages.rb +15 -2
- data/lib/badline/frontend/menu/menu_ports.rb +22 -1
- data/lib/badline/frontend/menu/pause_menu.rb +1 -0
- data/lib/badline/frontend/pet_keyboard.rb +124 -0
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/timeline.rb +16 -9
- data/lib/badline/frontend.rb +1 -0
- data/lib/badline/kernal_trap/dos/writes.rb +3 -1
- data/lib/badline/kernal_trap/dos.rb +6 -1
- data/lib/badline/kernal_trap/frames.rb +117 -0
- data/lib/badline/kernal_trap/ieee.rb +148 -0
- data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
- data/lib/badline/kernal_trap/keyed_state.rb +4 -4
- data/lib/badline/kernal_trap/routine.rb +3 -53
- data/lib/badline/kernal_trap/serial.rb +4 -102
- data/lib/badline/kernal_trap/steps.rb +64 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/machine/keyboard_buffer.rb +8 -0
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +28 -6
- data/lib/badline/media/extensions.rb +5 -2
- data/lib/badline/media/pet_media.rb +40 -0
- data/lib/badline/media/program.rb +14 -2
- data/lib/badline/media/vic20_media.rb +2 -2
- data/lib/badline/media.rb +36 -16
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +19 -3
- data/lib/badline/options/table.rb +14 -2
- data/lib/badline/options/validation.rb +68 -5
- data/lib/badline/options.rb +31 -19
- data/lib/badline/pet/bus/expansion.rb +106 -0
- data/lib/badline/pet/bus.rb +204 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +26 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +142 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_ports.rb +66 -0
- data/lib/badline/pet/ieee_traps.rb +75 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +85 -0
- data/lib/badline/pet/keyed_parts.rb +123 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +113 -0
- data/lib/badline/pet/rom_sets.rb +65 -0
- data/lib/badline/pet/sound.rb +128 -0
- data/lib/badline/pet/via.rb +10 -0
- data/lib/badline/pet/via_ports.rb +19 -0
- data/lib/badline/pet.rb +226 -0
- data/lib/badline/plus4/keyed_state.rb +5 -3
- data/lib/badline/plus4/tcbm_drive.rb +14 -0
- data/lib/badline/plus4.rb +1 -1
- data/lib/badline/roms/README +137 -0
- data/lib/badline/roms/pet/basic1.rom +0 -0
- data/lib/badline/roms/pet/basic2.rom +0 -0
- data/lib/badline/roms/pet/basic4.rom +0 -0
- data/lib/badline/roms/pet/characters1.rom +0 -0
- data/lib/badline/roms/pet/characters2.rom +0 -0
- data/lib/badline/roms/pet/editor1.rom +0 -0
- data/lib/badline/roms/pet/editor2-b.rom +0 -0
- data/lib/badline/roms/pet/editor2-n.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
- data/lib/badline/roms/pet/editor4-b.rom +0 -0
- data/lib/badline/roms/pet/editor4-n.rom +0 -0
- data/lib/badline/roms/pet/kernal1.rom +0 -0
- data/lib/badline/roms/pet/kernal2.rom +0 -0
- data/lib/badline/roms/pet/kernal4.rom +0 -0
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +16 -3
- data/lib/badline/snapshot/image.rb +14 -7
- data/lib/badline/snapshot/pet_setup.rb +71 -0
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- data/lib/badline/snapshot.rb +9 -5
- data/lib/badline/storage/d80_image.rb +48 -0
- data/lib/badline/storage/d82_image.rb +21 -0
- data/lib/badline/storage/tap.rb +16 -3
- data/lib/badline/storage.rb +2 -0
- data/lib/badline/support/pet_checkpoint.rb +38 -0
- data/lib/badline/timing.rb +5 -4
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic20/attachments.rb +7 -2
- data/lib/badline/vic20/bus/keyed_state.rb +5 -4
- data/lib/badline/vic20/bus.rb +5 -0
- data/lib/badline/vic20.rb +1 -1
- metadata +55 -1
|
@@ -5,23 +5,15 @@ module Badline
|
|
|
5
5
|
# PC traps on the KERNAL's serial bus primitives. Frames addressed to
|
|
6
6
|
# device 8 go to a virtual drive instead of out over the IEC lines, so
|
|
7
7
|
# OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
|
|
8
|
-
# loaders make through them. Other devices fall through to the
|
|
8
|
+
# loaders make through them (Frames). Other devices fall through to the
|
|
9
|
+
# ROM.
|
|
9
10
|
#
|
|
10
11
|
# With +device+ nil the traps answer no device, and every frame goes
|
|
11
12
|
# out over the bus, to a true drive if one is plugged in.
|
|
12
13
|
class Serial < Routine
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Frame type in the high nibble of the secondary address
|
|
16
|
-
OPEN = 0xf0
|
|
17
|
-
CLOSE = 0xe0
|
|
18
|
-
|
|
19
|
-
# ST bits at $90
|
|
20
|
-
DEVICE_NOT_PRESENT = 0x80
|
|
21
|
-
EOI = 0x40
|
|
22
|
-
READ_TIMEOUT = 0x02
|
|
14
|
+
include Frames
|
|
23
15
|
|
|
24
|
-
|
|
16
|
+
attr_accessor :device
|
|
25
17
|
|
|
26
18
|
def initialize(cpu:, bus:, layout:, dos:, device: DEVICE)
|
|
27
19
|
super(cpu:, bus:, layout:)
|
|
@@ -35,46 +27,8 @@ module Badline
|
|
|
35
27
|
@buffer = []
|
|
36
28
|
end
|
|
37
29
|
|
|
38
|
-
def install
|
|
39
|
-
routines.each do |address, routine|
|
|
40
|
-
@cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
|
|
41
|
-
end
|
|
42
|
-
self
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Takes the traps out again, so the KERNAL's own routines run.
|
|
46
|
-
def uninstall
|
|
47
|
-
routines.each_key { |address| @cpu.remove_trap(address) }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
30
|
private
|
|
51
31
|
|
|
52
|
-
def routines
|
|
53
|
-
{
|
|
54
|
-
@layout.talk => :talk,
|
|
55
|
-
@layout.listen => :listen,
|
|
56
|
-
@layout.second => :second,
|
|
57
|
-
@layout.tksa => :tksa,
|
|
58
|
-
@layout.ciout => :ciout,
|
|
59
|
-
@layout.untalk => :untalk,
|
|
60
|
-
@layout.unlisten => :unlisten,
|
|
61
|
-
@layout.acptr => :acptr
|
|
62
|
-
}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def call_routine(routine)
|
|
66
|
-
case routine
|
|
67
|
-
when :talk then talk
|
|
68
|
-
when :listen then listen
|
|
69
|
-
when :second then second
|
|
70
|
-
when :tksa then tksa
|
|
71
|
-
when :ciout then ciout
|
|
72
|
-
when :untalk then untalk
|
|
73
|
-
when :unlisten then unlisten
|
|
74
|
-
when :acptr then acptr
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
32
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
79
33
|
# in the secondary address that follows. Each byte sent under ATN,
|
|
80
34
|
# and each byte of a frame, times its handshake with the serial timer
|
|
@@ -112,25 +66,6 @@ module Badline
|
|
|
112
66
|
sent_under_atn
|
|
113
67
|
end
|
|
114
68
|
|
|
115
|
-
# CIOUT holds each byte back until the next one, or the UNLISTEN,
|
|
116
|
-
# comes, so the first byte of a frame sends nothing.
|
|
117
|
-
def ciout
|
|
118
|
-
return unless @listening
|
|
119
|
-
|
|
120
|
-
time_serial_byte(ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
|
|
121
|
-
@buffer << @cpu.a
|
|
122
|
-
update_status(DEVICE_NOT_PRESENT) unless accepted?
|
|
123
|
-
@cpu.status.carry = false
|
|
124
|
-
return_to_caller
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# The drive acknowledges every byte of an open or close frame, but
|
|
128
|
-
# takes data only on a channel it has open. Otherwise nothing holds
|
|
129
|
-
# the data line when a byte starts, so the KERNAL finds no device.
|
|
130
|
-
def accepted?
|
|
131
|
-
[OPEN, CLOSE].include?(@frame) || @dos.listening?(@listen_channel)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
69
|
def unlisten
|
|
135
70
|
return unless @listening
|
|
136
71
|
|
|
@@ -148,15 +83,6 @@ module Badline
|
|
|
148
83
|
release_bus
|
|
149
84
|
end
|
|
150
85
|
|
|
151
|
-
# UNLSN and UNTLK end by releasing ATN, the clock and the data line,
|
|
152
|
-
# and leave the port's last read in the accumulator.
|
|
153
|
-
def release_bus
|
|
154
|
-
@cpu.a = @layout.release_serial_lines(@bus)
|
|
155
|
-
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
156
|
-
@cpu.status.zero = @cpu.a.zero?
|
|
157
|
-
return_to_caller
|
|
158
|
-
end
|
|
159
|
-
|
|
160
86
|
def acptr
|
|
161
87
|
return unless @talking
|
|
162
88
|
|
|
@@ -167,30 +93,6 @@ module Badline
|
|
|
167
93
|
@cpu.status.carry = false
|
|
168
94
|
return_to_caller
|
|
169
95
|
end
|
|
170
|
-
|
|
171
|
-
def deliver_frame
|
|
172
|
-
return unless @listen_channel
|
|
173
|
-
|
|
174
|
-
case @frame
|
|
175
|
-
when OPEN then @dos.open(@listen_channel, @buffer.pack("C*"))
|
|
176
|
-
when CLOSE then @dos.close(@listen_channel)
|
|
177
|
-
else @dos.write(@listen_channel, @buffer)
|
|
178
|
-
end
|
|
179
|
-
@buffer = []
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def received
|
|
183
|
-
byte, eoi = @dos.read(@talk_channel)
|
|
184
|
-
return NO_DATA unless byte
|
|
185
|
-
|
|
186
|
-
[byte, eoi ? EOI : 0]
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def update_status(bits)
|
|
190
|
-
return if bits.zero?
|
|
191
|
-
|
|
192
|
-
@bus.poke(zero_page.status, @bus.peek(zero_page.status) | bits)
|
|
193
|
-
end
|
|
194
96
|
end
|
|
195
97
|
end
|
|
196
98
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module KernalTrap
|
|
5
|
+
# The steps the PC traps on KERNAL routines share, on the @cpu, @bus
|
|
6
|
+
# and @layout of the trap that includes them, which includes
|
|
7
|
+
# IntegerHelper too: the layout's checks and timer, the zero page's
|
|
8
|
+
# words, and the return that stands in for the trapped routine's RTS.
|
|
9
|
+
# Routine includes them, and so does each trap that isn't a Routine,
|
|
10
|
+
# so every trap class keeps its own layout's type.
|
|
11
|
+
module Steps
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def kernal_mapped?
|
|
15
|
+
@layout.kernal_mapped?(@bus)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def zero_page = @layout.zero_page
|
|
19
|
+
|
|
20
|
+
# The two-byte value whose low byte is at +address+.
|
|
21
|
+
def peek_word(address)
|
|
22
|
+
uint16(@bus.peek(address), @bus.peek(address + 1))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def poke_word(address, value)
|
|
26
|
+
@bus.poke(address, low_byte(value))
|
|
27
|
+
@bus.poke(address + 1, high_byte(value))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The ROM times each byte on the serial bus with a one-shot timer
|
|
31
|
+
# (Layout#time_serial_byte): Routine::ISOUR_TIMEOUT for a byte sent,
|
|
32
|
+
# Routine::ACPTR_TIMEOUT for one received.
|
|
33
|
+
def time_serial_byte(timer_high)
|
|
34
|
+
@layout.time_serial_byte(@bus, timer_high)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def return_to_caller
|
|
38
|
+
@cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Runs the ROM routines in order, each returning into the next, and
|
|
42
|
+
# the last one to the trapped routine's caller
|
|
43
|
+
def continue_with(first, *rest)
|
|
44
|
+
rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
|
|
45
|
+
@cpu.program_counter = first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def push_address(address)
|
|
49
|
+
push_byte(high_byte(address))
|
|
50
|
+
push_byte(low_byte(address))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def push_byte(value)
|
|
54
|
+
@bus.poke(0x0100 + @cpu.stack_pointer, value)
|
|
55
|
+
@cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pull_byte
|
|
59
|
+
@cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
|
|
60
|
+
@bus.peek(0x0100 + @cpu.stack_pointer)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/badline/kernal_trap.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/kernal_trap/layout"
|
|
4
|
+
require "badline/kernal_trap/ieee_layout"
|
|
5
|
+
require "badline/kernal_trap/steps"
|
|
4
6
|
require "badline/kernal_trap/routine"
|
|
5
7
|
require "badline/kernal_trap/dos"
|
|
6
8
|
require "badline/kernal_trap/file_routine"
|
|
7
9
|
require "badline/kernal_trap/load"
|
|
8
10
|
require "badline/kernal_trap/save"
|
|
11
|
+
require "badline/kernal_trap/frames"
|
|
9
12
|
require "badline/kernal_trap/serial"
|
|
13
|
+
require "badline/kernal_trap/ieee"
|
|
10
14
|
require "badline/kernal_trap/chrout"
|
|
11
15
|
require "badline/kernal_trap/keyed_state"
|
|
@@ -20,6 +20,14 @@ module Badline
|
|
|
20
20
|
# ($D86E).
|
|
21
21
|
PLUS4_LAYOUT = Layout.new(address: 0x0527, count: 0xef, capacity: 10)
|
|
22
22
|
|
|
23
|
+
# The PET's, under BASIC 2 and BASIC 4. Every editor's GETIN takes
|
|
24
|
+
# ten keys from it, though the CRTC editors' keyboard scans stop at
|
|
25
|
+
# nine (XMAX).
|
|
26
|
+
PET_LAYOUT = Layout.new(address: 0x026f, count: 0x9e, capacity: 10)
|
|
27
|
+
|
|
28
|
+
# The PET's under BASIC 1, outside the zero page.
|
|
29
|
+
PET_BASIC1_LAYOUT = Layout.new(address: 0x020f, count: 0x020d, capacity: 10)
|
|
30
|
+
|
|
23
31
|
def type_text(text)
|
|
24
32
|
codes = text.bytes.map { |b| ascii_to_petscii(b) }
|
|
25
33
|
on_init { @pending_keys = codes }
|
|
@@ -4,7 +4,7 @@ module Badline
|
|
|
4
4
|
module Machine
|
|
5
5
|
# Brings the machine to the point a snapshot file is saved at: the main
|
|
6
6
|
# CPU between instructions, or jammed, with no REU transfer holding the
|
|
7
|
-
# bus. C64, Vic20, C128 and
|
|
7
|
+
# bus. C64, Vic20, C128, Plus4 and PET each include it, and answer
|
|
8
8
|
# #settled_for_snapshot? and #settle_chips_for_snapshot, which brings
|
|
9
9
|
# what each chip defers up to the cycle without running one.
|
|
10
10
|
module Settling
|
data/lib/badline/machine.rb
CHANGED
|
@@ -8,8 +8,9 @@ module Badline
|
|
|
8
8
|
# = Machine
|
|
9
9
|
#
|
|
10
10
|
# The machines badline builds by family, as the command line's
|
|
11
|
-
# subcommand names them: the C64, Badline::C64, the VIC-20, Vic20,
|
|
12
|
-
# C128 in C64 mode, C128,
|
|
11
|
+
# subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, the
|
|
12
|
+
# C128 in C64 mode, C128, the 264 series, Plus4, and the PET and CBM,
|
|
13
|
+
# PET.
|
|
13
14
|
#
|
|
14
15
|
# A machine answers what the front end, Media, the runners and the
|
|
15
16
|
# snapshots call on it:
|
|
@@ -57,23 +58,31 @@ module Badline
|
|
|
57
58
|
# and +capture_output+, and besides +model_label+, +cartridge_slots+ and
|
|
58
59
|
# +detach_cartridges+ for its two cartridges.
|
|
59
60
|
#
|
|
61
|
+
# The PET answers the identity, clocking, lifecycle, video, sound and
|
|
62
|
+
# keyboard parts, with no joysticks, RESTORE or control ports, and of
|
|
63
|
+
# the rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
|
|
64
|
+
# +mounted?+, +datasette+, +true_drive+, nil, +install_debug_register+
|
|
65
|
+
# and +capture_output+, and besides +model_label+ and +ram_kb+.
|
|
66
|
+
#
|
|
60
67
|
# Each machine includes Machine::Clocking for the clocking part.
|
|
61
68
|
module Machine
|
|
62
69
|
# Media the machine named doesn't take.
|
|
63
70
|
class Unsupported < ArgumentError; end
|
|
64
71
|
|
|
65
|
-
# A new machine of `family`, :c64, :vic20, :c128 or :
|
|
66
|
-
# of it `model` names. A C64's model is one of Model::ALL, with its SID
|
|
72
|
+
# A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
|
|
73
|
+
# model of it `model` names. A C64's model is one of Model::ALL, with its SID
|
|
67
74
|
# `sid_model` when not nil and an REU of `reu` K when not nil. A
|
|
68
75
|
# VIC-20's is "pal", with the RAM expansion `ram` names
|
|
69
76
|
# (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
|
|
70
77
|
# one of C128::Model::ALL, with its SID `sid_model` when not nil, built
|
|
71
78
|
# for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
|
|
72
|
-
# the model's own RAM.
|
|
79
|
+
# the model's own RAM. A PET's is one of PET::Model::ALL, with its own
|
|
80
|
+
# RAM and ROMs, its CRTC's editor the 50 Hz one.
|
|
73
81
|
def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
|
|
74
82
|
return vic20(model, ram) if family == :vic20
|
|
75
83
|
return c128(model, sid_model, :c64) if family == :c128
|
|
76
84
|
return Plus4.new(model:) if family == :plus4
|
|
85
|
+
return PET.new(model:) if family == :pet
|
|
77
86
|
raise ArgumentError, "no machine family named #{family}" unless family == :c64
|
|
78
87
|
|
|
79
88
|
profile = Model.named(model)
|
|
@@ -89,11 +98,12 @@ module Badline
|
|
|
89
98
|
# only C64 mode runs: a .sid tune or a program that loads at the C64's
|
|
90
99
|
# BASIC start. A VIC-20 without RAM named gets the expansion the media
|
|
91
100
|
# needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
|
|
92
|
-
# Unsupported for media a VIC-20
|
|
101
|
+
# Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
|
|
93
102
|
def self.for_options(options, media, &)
|
|
94
103
|
family = options.family
|
|
95
104
|
return vic20_for(media, options.model, options.ram, &) if family == :vic20
|
|
96
105
|
return plus4_for(media, options) if family == :plus4
|
|
106
|
+
return pet_for(media, options) if family == :pet
|
|
97
107
|
|
|
98
108
|
sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
|
|
99
109
|
return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
|
|
@@ -141,6 +151,18 @@ module Badline
|
|
|
141
151
|
end
|
|
142
152
|
private_class_method :plus4_for
|
|
143
153
|
|
|
154
|
+
# A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
|
|
155
|
+
# model's own without it, the editor at the frame rate --60hz or
|
|
156
|
+
# --editor asks for, and the ROMs --basic and --editor fit it with.
|
|
157
|
+
def self.pet_for(media, options)
|
|
158
|
+
unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
|
|
159
|
+
raise Unsupported, "doesn't go in a PET" if unsupported
|
|
160
|
+
|
|
161
|
+
PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
|
|
162
|
+
editor: options.editor)
|
|
163
|
+
end
|
|
164
|
+
private_class_method :pet_for
|
|
165
|
+
|
|
144
166
|
def self.c64_media?(media)
|
|
145
167
|
return false if media.nil? || !File.file?(media)
|
|
146
168
|
|
|
@@ -7,6 +7,8 @@ module Badline
|
|
|
7
7
|
# It loads on its own, for Options.
|
|
8
8
|
#
|
|
9
9
|
# - :disk, a disk image that the traps mount and a true drive reads
|
|
10
|
+
# - :ieee_disk, an 8050's or 8250's disk image, for a PET's IEEE-488
|
|
11
|
+
# drive
|
|
10
12
|
# - :gcr, a disk's raw GCR, which only a true drive reads
|
|
11
13
|
# - :archive, a .t64, which the traps mount read-only
|
|
12
14
|
# - :tape, :cartridge and :tune
|
|
@@ -14,7 +16,8 @@ module Badline
|
|
|
14
16
|
module Extensions
|
|
15
17
|
KINDS = {
|
|
16
18
|
".d64" => :disk, ".d71" => :disk, ".d81" => :disk, ".g64" => :gcr, ".g71" => :gcr, ".t64" => :archive,
|
|
17
|
-
".
|
|
19
|
+
".d80" => :ieee_disk, ".d82" => :ieee_disk, ".tap" => :tape, ".crt" => :cartridge, ".sid" => :tune,
|
|
20
|
+
".m3u" => :disk_list, ".vfl" => :disk_list
|
|
18
21
|
}.freeze
|
|
19
22
|
|
|
20
23
|
# The kind of the medium at `path`, by its extension.
|
|
@@ -27,7 +30,7 @@ module Badline
|
|
|
27
30
|
DISKS = of(%i[disk gcr])
|
|
28
31
|
|
|
29
32
|
# What goes in device 8 through the traps.
|
|
30
|
-
MOUNTABLE = of(%i[disk archive])
|
|
33
|
+
MOUNTABLE = of(%i[disk archive ieee_disk])
|
|
31
34
|
|
|
32
35
|
# What goes in device 8, a true drive's or the traps', and the lists
|
|
33
36
|
# of disks to put there.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# What a PET does with media of its own (Media.attach). Disks of every
|
|
6
|
+
# kind, .d80 and .d82 among them, directories and .t64 archives mount
|
|
7
|
+
# on device 8 through the traps on the KERNAL's IEEE-488 primitives
|
|
8
|
+
# (PET::IEEETraps), programs load at the start of BASIC, $0401
|
|
9
|
+
# (Program), and tapes play on the datasette at cassette port #1. There
|
|
10
|
+
# is no cartridge port and no SID, and the true drives are serial ones.
|
|
11
|
+
module PETMedia
|
|
12
|
+
# What a PET doesn't take.
|
|
13
|
+
UNSUPPORTED = Extensions.of(%i[tune cartridge gcr])
|
|
14
|
+
|
|
15
|
+
# What loads and runs the first program on the disk in device 8.
|
|
16
|
+
AUTOSTART = %(load"*",8\rrun\r)
|
|
17
|
+
|
|
18
|
+
# What loads and runs the first program on the tape in cassette #1.
|
|
19
|
+
TAPE_AUTOSTART = "load\rrun\r"
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# A tape made for another machine plays all the same, and says so.
|
|
23
|
+
def tape_message(tape, message)
|
|
24
|
+
tape.platform == Storage::TAP::PET_PLATFORM ? message : "#{message}, a tape for another machine than the PET"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# BASIC 2 and 4 load the first program off the disk and run it.
|
|
28
|
+
# BASIC 1's LOAD finds no device on the IEEE-488 bus, so the first
|
|
29
|
+
# program goes in as a program file does, once the KERNAL has
|
|
30
|
+
# booted.
|
|
31
|
+
def disk_autostart(machine, storage)
|
|
32
|
+
return machine.type_text(AUTOSTART) unless machine.model.basic1?
|
|
33
|
+
|
|
34
|
+
data = storage.read_file("*")
|
|
35
|
+
machine.on_init { Program.load(machine, data, autostart: true) } if data
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Media
|
|
5
|
-
# A program loading into a C64, a C128
|
|
6
|
-
# has booted (Media.attach). The VIC-20 has its own
|
|
5
|
+
# A program loading into a C64, a C128, a 264 series machine or a PET
|
|
6
|
+
# once it has booted (Media.attach). The VIC-20 has its own
|
|
7
|
+
# (Vic20Basic).
|
|
7
8
|
module Program
|
|
8
9
|
# Where BASIC 3.5 starts on the 264 series.
|
|
9
10
|
PLUS4_BASIC_START = 0x1001
|
|
10
11
|
|
|
12
|
+
# Where BASIC starts on every PET.
|
|
13
|
+
PET_BASIC_START = 0x0401
|
|
14
|
+
|
|
11
15
|
class << self
|
|
12
16
|
# Loads the program where it says, and RUNs one that loads at the
|
|
13
17
|
# start of BASIC.
|
|
@@ -30,12 +34,20 @@ module Badline
|
|
|
30
34
|
# Where BASIC starts, the pointer to the end of its text and the
|
|
31
35
|
# KERNAL's end address (EAL). BASIC 7.0 starts at $1C01 and keeps
|
|
32
36
|
# the end of its text at $1210, and the 264 KERNAL keeps EAL at $9D.
|
|
37
|
+
# PET BASIC 2 and 4 keep the end of their text at $2A and EAL at
|
|
38
|
+
# $C9, and BASIC 1 at $7C and $E5.
|
|
33
39
|
def basic(computer)
|
|
34
40
|
return [PLUS4_BASIC_START, 0x2d, KernalTrap::PLUS4_ZERO_PAGE.end_address] if computer.family == :plus4
|
|
41
|
+
return pet_basic(computer) if computer.family == :pet
|
|
35
42
|
return [0x1c01, 0x1210, 0xae] if computer.family == :c128 && computer.mode == :c128
|
|
36
43
|
|
|
37
44
|
[BASIC_START, 0x2d, 0xae]
|
|
38
45
|
end
|
|
46
|
+
|
|
47
|
+
def pet_basic(computer)
|
|
48
|
+
model = computer.model
|
|
49
|
+
[PET_BASIC_START, model.basic1? ? 0x7c : 0x2a, model.trap_layout.zero_page.end_address]
|
|
50
|
+
end
|
|
39
51
|
end
|
|
40
52
|
end
|
|
41
53
|
end
|
|
@@ -61,11 +61,11 @@ module Badline
|
|
|
61
61
|
|
|
62
62
|
# Loads the program, `bytes` from the file at `path`, as Vic20Basic
|
|
63
63
|
# has it once the KERNAL has booted, or attaches the cartridge it's
|
|
64
|
-
# a part of (Vic20Cartridge), and says which.
|
|
64
|
+
# a part of (Vic20Cartridge), named after the file, and says which.
|
|
65
65
|
def attach_program(machine, path, bytes, autostart:)
|
|
66
66
|
chips = Vic20Cartridge.chips(path, bytes)
|
|
67
67
|
if chips
|
|
68
|
-
machine.attach_cartridge(chips)
|
|
68
|
+
machine.attach_cartridge(chips, File.basename(path, File.extname(path)))
|
|
69
69
|
return "Attached cartridge #{path}"
|
|
70
70
|
end
|
|
71
71
|
|
data/lib/badline/media.rb
CHANGED
|
@@ -11,6 +11,7 @@ require "badline/media/vic20_basic"
|
|
|
11
11
|
require "badline/media/vic20_cartridge"
|
|
12
12
|
require "badline/media/vic20_media"
|
|
13
13
|
require "badline/media/plus4_media"
|
|
14
|
+
require "badline/media/pet_media"
|
|
14
15
|
require "badline/media/tune"
|
|
15
16
|
require "badline/media/program"
|
|
16
17
|
|
|
@@ -25,7 +26,9 @@ module Badline
|
|
|
25
26
|
".d64" => Storage::D64Image,
|
|
26
27
|
".d71" => Storage::D71Image,
|
|
27
28
|
".d81" => Storage::D81Image,
|
|
28
|
-
".t64" => Storage::T64
|
|
29
|
+
".t64" => Storage::T64,
|
|
30
|
+
".d80" => Storage::D80Image,
|
|
31
|
+
".d82" => Storage::D82Image
|
|
29
32
|
}.freeze
|
|
30
33
|
|
|
31
34
|
class << self
|
|
@@ -48,8 +51,10 @@ module Badline
|
|
|
48
51
|
# disk autostart of its own, and takes no .sid tunes, and the 264
|
|
49
52
|
# series loads programs at the start of its BASIC (Program), takes
|
|
50
53
|
# .crt cartridges and raw .bin ROM images in its two cartridges'
|
|
51
|
-
# slots (Plus4Media), and no .sid tunes either. The
|
|
52
|
-
#
|
|
54
|
+
# slots (Plus4Media), and no .sid tunes either. The PET mounts .d80
|
|
55
|
+
# and .d82 disks too, which no other machine takes, and has no
|
|
56
|
+
# cartridges, tunes or true drives (PETMedia). The C128 in C64 mode
|
|
57
|
+
# takes a C64's.
|
|
53
58
|
def attach(computer, path, autostart: true, subtune: nil, **)
|
|
54
59
|
path = DiskList.disk(path)
|
|
55
60
|
refuse_unsupported(computer, path)
|
|
@@ -99,7 +104,7 @@ module Badline
|
|
|
99
104
|
return :cartridge if computer.family == :plus4 && Plus4Media.rom_image?(path)
|
|
100
105
|
|
|
101
106
|
kind = Extensions.kind(path)
|
|
102
|
-
%i[disk archive].include?(kind) ? :storage : kind
|
|
107
|
+
%i[disk archive ieee_disk].include?(kind) ? :storage : kind
|
|
103
108
|
end
|
|
104
109
|
|
|
105
110
|
# What device 8 serves through the traps for a host directory, a disk
|
|
@@ -120,14 +125,21 @@ module Badline
|
|
|
120
125
|
|
|
121
126
|
private
|
|
122
127
|
|
|
123
|
-
# The VIC-20 and the 264 series have no SID to play a .sid tune on
|
|
128
|
+
# The VIC-20 and the 264 series have no SID to play a .sid tune on,
|
|
129
|
+
# and only the PET has the IEEE-488 drive a .d80 or .d82 goes in.
|
|
124
130
|
def refuse_unsupported(computer, path)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
when :vic20 then raise ArgumentError, "#{path} doesn't go in a VIC-20"
|
|
129
|
-
when :plus4 then raise ArgumentError, "#{path} doesn't go in a Plus/4"
|
|
131
|
+
family = computer.family
|
|
132
|
+
if Extensions.kind(path) == :ieee_disk && family != :pet
|
|
133
|
+
raise ArgumentError, "#{path} needs a PET's IEEE-488 drive"
|
|
130
134
|
end
|
|
135
|
+
|
|
136
|
+
refused, name = case family
|
|
137
|
+
when :vic20 then [Vic20Media::UNSUPPORTED, "a VIC-20"]
|
|
138
|
+
when :plus4 then [Vic20Media::UNSUPPORTED, "a Plus/4"]
|
|
139
|
+
when :pet then [PETMedia::UNSUPPORTED, "a PET"]
|
|
140
|
+
else [[], ""]
|
|
141
|
+
end
|
|
142
|
+
raise ArgumentError, "#{path} doesn't go in #{name}" if refused.include?(File.extname(path).downcase)
|
|
131
143
|
end
|
|
132
144
|
|
|
133
145
|
def attach_kind(computer, path, autostart:, subtune:, **options)
|
|
@@ -143,7 +155,7 @@ module Badline
|
|
|
143
155
|
end
|
|
144
156
|
end
|
|
145
157
|
|
|
146
|
-
def disk?(path) = File.directory?(path) || Extensions.kind(path)
|
|
158
|
+
def disk?(path) = File.directory?(path) || %i[disk ieee_disk].include?(Extensions.kind(path))
|
|
147
159
|
|
|
148
160
|
def attach_true_drive(computer, path, disk, autostart:)
|
|
149
161
|
message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
|
|
@@ -154,7 +166,11 @@ module Badline
|
|
|
154
166
|
def mount(computer, path, disk, autostart:)
|
|
155
167
|
storage = open_storage(path, disk)
|
|
156
168
|
computer.mount(storage)
|
|
157
|
-
|
|
169
|
+
if autostart && computer.family == :pet
|
|
170
|
+
PETMedia.disk_autostart(computer, storage)
|
|
171
|
+
elsif autostart
|
|
172
|
+
computer.type_text(disk_autostart(computer) { storage.read_file("*") })
|
|
173
|
+
end
|
|
158
174
|
"Mounted #{path} as device 8"
|
|
159
175
|
end
|
|
160
176
|
|
|
@@ -170,7 +186,9 @@ module Badline
|
|
|
170
186
|
# (Plus4Media.cartridge_chips).
|
|
171
187
|
def attach_cartridge(computer, path, options)
|
|
172
188
|
case computer.family
|
|
173
|
-
when :vic20
|
|
189
|
+
when :vic20
|
|
190
|
+
crt = Storage::CRTFile.new(path, machine: :vic20)
|
|
191
|
+
computer.attach_cartridge(crt.chips, crt.name.empty? ? File.basename(path, File.extname(path)) : crt.name)
|
|
174
192
|
when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
|
|
175
193
|
else
|
|
176
194
|
computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
|
|
@@ -178,17 +196,19 @@ module Badline
|
|
|
178
196
|
"Attached cartridge #{path}"
|
|
179
197
|
end
|
|
180
198
|
|
|
181
|
-
# A VIC-20 or a
|
|
182
|
-
# (Vic20Media.tape_message, Plus4Media.tape_message
|
|
199
|
+
# A VIC-20, a 264 or a PET says when the tape was made for another
|
|
200
|
+
# machine (Vic20Media.tape_message, Plus4Media.tape_message,
|
|
201
|
+
# PETMedia.tape_message).
|
|
183
202
|
def attach_tape(computer, path, autostart:)
|
|
184
203
|
tape = Storage::TAP.new(path)
|
|
185
204
|
computer.datasette.insert(tape)
|
|
186
205
|
computer.datasette.play!
|
|
187
|
-
computer.type_text(TAPE_AUTOSTART) if autostart
|
|
206
|
+
computer.type_text(computer.family == :pet ? PETMedia::TAPE_AUTOSTART : TAPE_AUTOSTART) if autostart
|
|
188
207
|
message = "Inserted #{path} in the datasette"
|
|
189
208
|
case computer.family
|
|
190
209
|
when :vic20 then Vic20Media.tape_message(tape, message)
|
|
191
210
|
when :plus4 then Plus4Media.tape_message(tape, message)
|
|
211
|
+
when :pet then PETMedia.tape_message(tape, message)
|
|
192
212
|
else message
|
|
193
213
|
end
|
|
194
214
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/storage/p00"
|
|
4
|
+
require "badline/storage/tap"
|
|
4
5
|
|
|
5
6
|
module Badline
|
|
6
7
|
class Options
|
|
@@ -12,7 +13,7 @@ module Badline
|
|
|
12
13
|
|
|
13
14
|
private
|
|
14
15
|
|
|
15
|
-
# Takes `sid`, `c64`, `vic20`, `c128` or `
|
|
16
|
+
# Takes `sid`, `c64`, `vic20`, `c128`, `plus4` or `pet` when given first.
|
|
16
17
|
def subcommand(args)
|
|
17
18
|
case args.first
|
|
18
19
|
when "sid" then @sid_command = true
|
|
@@ -20,6 +21,7 @@ module Badline
|
|
|
20
21
|
when "vic20" then @family = :vic20
|
|
21
22
|
when "c128" then @family = :c128
|
|
22
23
|
when "plus4" then @family = :plus4
|
|
24
|
+
when "pet" then @family = :pet
|
|
23
25
|
else return
|
|
24
26
|
end
|
|
25
27
|
@family_named = !@sid_command
|
|
@@ -31,19 +33,28 @@ module Badline
|
|
|
31
33
|
when :vic20 then VIC20_MODELS
|
|
32
34
|
when :c128 then C128_MODELS
|
|
33
35
|
when :plus4 then PLUS4_MODELS
|
|
36
|
+
when :pet then PET_MODELS
|
|
34
37
|
else MODELS
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
# Without a family named, a program that loads at C128_BASIC_START is
|
|
39
|
-
# the C128's.
|
|
42
|
+
# the C128's, and an 8050's or 8250's disk or a PET tape the PET's. A
|
|
43
|
+
# program that loads where PET BASIC starts could be a VIC-20's too.
|
|
40
44
|
def pick_family
|
|
41
|
-
return if @family_named || @sid_command || @media_path.nil?
|
|
45
|
+
return if @family_named || @sid_command || @media_path.nil? || !File.file?(@media_path)
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if pet_media?
|
|
48
|
+
@family = :pet
|
|
49
|
+
elsif Media::Extensions::PROGRAMS.include?(File.extname(@media_path).downcase)
|
|
50
|
+
@family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
|
|
51
|
+
end
|
|
52
|
+
end
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
# Whether the media is an 8050's or 8250's disk or a PET tape.
|
|
55
|
+
def pet_media?
|
|
56
|
+
kind = Media::Extensions.kind(@media_path)
|
|
57
|
+
kind == :ieee_disk || (kind == :tape && Storage::TAP.platform_of(@media_path) == Storage::TAP::PET_PLATFORM)
|
|
47
58
|
end
|
|
48
59
|
end
|
|
49
60
|
end
|