badline 0.16.1 → 0.17.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 +60 -0
- data/CLAUDE.md +1 -1
- data/README.md +102 -12
- data/doc/media.md +5 -2
- data/doc/pinned-behaviour.md +61 -0
- data/doc/snapshot-format.md +376 -69
- data/doc/snapshots.md +148 -14
- data/lib/badline/cbm2/acia.rb +10 -0
- data/lib/badline/cbm2/bus.rb +263 -0
- data/lib/badline/cbm2/cpu.rb +58 -0
- data/lib/badline/cbm2/crtc_video.rb +133 -0
- data/lib/badline/cbm2/ieee_drive.rb +18 -0
- data/lib/badline/cbm2/ieee_layout.rb +69 -0
- data/lib/badline/cbm2/ieee_ports.rb +104 -0
- data/lib/badline/cbm2/ieee_traps.rb +81 -0
- data/lib/badline/cbm2/keyboard_ports.rb +60 -0
- data/lib/badline/cbm2/keyed_modules.rb +67 -0
- data/lib/badline/cbm2/keyed_state.rb +91 -0
- data/lib/badline/cbm2/model.rb +44 -0
- data/lib/badline/cbm2/p500/control_ports.rb +80 -0
- data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
- data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
- data/lib/badline/cbm2/p500/model.rb +39 -0
- data/lib/badline/cbm2/p500/pages.rb +57 -0
- data/lib/badline/cbm2/p500.rb +262 -0
- data/lib/badline/cbm2/sid_page.rb +19 -0
- data/lib/badline/cbm2.rb +267 -0
- data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
- data/lib/badline/chips/acia/keyed_state.rb +47 -0
- data/lib/badline/chips/acia.rb +307 -0
- data/lib/badline/chips/crtc.rb +3 -2
- data/lib/badline/chips/riot/fast_forward.rb +72 -0
- data/lib/badline/chips/riot/keyed_parts.rb +69 -0
- data/lib/badline/chips/riot/timer.rb +80 -0
- data/lib/badline/chips/riot.rb +162 -0
- data/lib/badline/chips/rriot/keyed_state.rb +30 -0
- data/lib/badline/chips/rriot.rb +132 -0
- data/lib/badline/chips/tpi/keyed_state.rb +38 -0
- data/lib/badline/chips/tpi.rb +233 -0
- data/lib/badline/chips/vic/bank.rb +60 -5
- data/lib/badline/chips/vic/sprite.rb +4 -3
- data/lib/badline/drives/datasette/keyed_state.rb +9 -1
- data/lib/badline/drives/datasette.rb +27 -3
- data/lib/badline/drives/model8050/disk.rb +163 -0
- data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
- data/lib/badline/drives/model8050/idle.rb +177 -0
- data/lib/badline/drives/model8050/ieee_port.rb +147 -0
- data/lib/badline/drives/model8050/ip_bus.rb +93 -0
- data/lib/badline/drives/model8050/keyed_state.rb +160 -0
- data/lib/badline/drives/model8050/mechanism.rb +230 -0
- data/lib/badline/drives/model8050/parts.rb +37 -0
- data/lib/badline/drives/model8050.rb +243 -0
- data/lib/badline/frontend/app.rb +10 -3
- data/lib/badline/frontend/boot.rb +1 -1
- data/lib/badline/frontend/controls.rb +51 -15
- data/lib/badline/frontend/gamepads.rb +3 -3
- data/lib/badline/frontend/keys.rb +61 -2
- data/lib/badline/frontend/media_slots.rb +1 -1
- data/lib/badline/frontend/menu/menu_media.rb +14 -11
- data/lib/badline/frontend/menu/menu_pages.rb +18 -4
- data/lib/badline/frontend/menu/menu_ports.rb +40 -4
- data/lib/badline/frontend/menu/pause_menu.rb +1 -1
- data/lib/badline/frontend/pet_keyboard.rb +23 -7
- data/lib/badline/frontend/timeline.rb +19 -10
- data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
- data/lib/badline/kernal_trap/dos/status.rb +3 -0
- data/lib/badline/kernal_trap/dos.rb +18 -5
- data/lib/badline/kernal_trap/ieee.rb +4 -2
- data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
- data/lib/badline/machine/ieee_drives.rb +194 -0
- data/lib/badline/machine/settling.rb +4 -0
- data/lib/badline/machine.rb +23 -6
- data/lib/badline/media/cbm2_media.rb +29 -0
- data/lib/badline/media/program.rb +6 -3
- data/lib/badline/media/true_drive.rb +38 -9
- data/lib/badline/media.rb +15 -6
- data/lib/badline/options/family.rb +15 -3
- data/lib/badline/options/help.rb +16 -2
- data/lib/badline/options/table.rb +18 -4
- data/lib/badline/options/validation.rb +27 -3
- data/lib/badline/options.rb +21 -7
- data/lib/badline/pet/cassettes.rb +22 -1
- data/lib/badline/pet/ieee_drive.rb +5 -64
- data/lib/badline/pet/ieee_ports.rb +7 -0
- data/lib/badline/pet.rb +15 -4
- data/lib/badline/plus4/acia.rb +4 -298
- data/lib/badline/plus4/keyed_parts.rb +0 -44
- data/lib/badline/plus4.rb +1 -0
- data/lib/badline/roms/8050/dos-2.7.rom +0 -0
- data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
- data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
- data/lib/badline/roms/README +111 -0
- data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
- data/lib/badline/roms/cbm2/basic128.rom +0 -0
- data/lib/badline/roms/cbm2/basic256.rom +0 -0
- data/lib/badline/roms/cbm2/characters-600.rom +0 -0
- data/lib/badline/roms/cbm2/characters-700.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
- data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
- data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
- data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
- data/lib/badline/snapshot/cbm2_setup.rb +53 -0
- data/lib/badline/snapshot/container.rb +7 -2
- data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
- data/lib/badline/snapshot/field_doc.rb +38 -3
- data/lib/badline/snapshot/image.rb +62 -9
- data/lib/badline/snapshot/vice/cbm2.rb +93 -0
- data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
- data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
- data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
- data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
- data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
- data/lib/badline/snapshot/vice/cias.rb +13 -9
- data/lib/badline/snapshot/vice/p500.rb +96 -0
- data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
- data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
- data/lib/badline/snapshot/vice/pet.rb +7 -3
- data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
- data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
- data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
- data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
- data/lib/badline/snapshot/vice/pet_via.rb +6 -5
- data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
- data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
- data/lib/badline/snapshot/vice.rb +3 -0
- data/lib/badline/snapshot.rb +11 -3
- data/lib/badline/storage/crt_file.rb +7 -1
- data/lib/badline/storage/d64_image.rb +7 -0
- data/lib/badline/storage/d80_image.rb +17 -1
- data/lib/badline/storage/d81_image.rb +14 -0
- data/lib/badline/storage/disk_image/bam.rb +18 -3
- data/lib/badline/storage/disk_image/formatting.rb +69 -0
- data/lib/badline/storage/disk_image/writing.rb +35 -0
- data/lib/badline/storage/disk_image.rb +2 -0
- data/lib/badline/storage.rb +1 -0
- data/lib/badline/support/cbm2_checkpoint.rb +48 -0
- data/lib/badline/support/p500_checkpoint.rb +35 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline.rb +1 -0
- metadata +72 -2
|
@@ -8,7 +8,9 @@ module Badline
|
|
|
8
8
|
# reads become the PET's own keyed modules (Keyed), which it loads
|
|
9
9
|
# as it loads its own snapshot. Whatever they don't set stays as the
|
|
10
10
|
# machine has it at power-on, and the datasettes' tapes and the
|
|
11
|
-
# disk device 8 serves stay as they are.
|
|
11
|
+
# disk device 8 serves stay as they are. A snapshot with a true 2031,
|
|
12
|
+
# 8050 or 8250 puts one on the bus, with the disks badline's drive
|
|
13
|
+
# of that model had in it, and one without takes badline's off.
|
|
12
14
|
module Restore
|
|
13
15
|
# MAINCPU and PETMEM hold the machine itself, so a version badline
|
|
14
16
|
# doesn't read fails the restore. The others are left out and
|
|
@@ -16,6 +18,8 @@ module Badline
|
|
|
16
18
|
READ = [MainCPU::NAME, PETMem::NAME, CRTC::NAME, "PIA1", "PIA2", VIA::NAME].freeze
|
|
17
19
|
# The host's media, which a restore keeps.
|
|
18
20
|
KEPT = %w[BL-TAPE1 BL-TAPE2 BL-TRAPDOS].freeze
|
|
21
|
+
# The disks in a true drive's drives, which a restore keeps.
|
|
22
|
+
DISKS = %w[BL-D8-DISK BL-D8-DISK1].freeze
|
|
19
23
|
|
|
20
24
|
module_function
|
|
21
25
|
|
|
@@ -33,9 +37,13 @@ module Badline
|
|
|
33
37
|
raise FormatError, "the snapshot is of a #{setup}, not a #{ours}" unless setup == ours
|
|
34
38
|
|
|
35
39
|
apply(container, setup.build)
|
|
40
|
+
had_drive = computer.true_drive
|
|
36
41
|
computer.power_cycle!
|
|
37
42
|
report = apply(container, computer)
|
|
38
43
|
kept = computer.mounted? ? ["the disk mounted in device 8 stays mounted"] : []
|
|
44
|
+
if had_drive && !computer.true_drive.equal?(had_drive)
|
|
45
|
+
kept << "the #{had_drive.model_name} on device #{had_drive.device} is taken off the bus"
|
|
46
|
+
end
|
|
39
47
|
Report.new(applied: report.applied, ignored: report.ignored + kept)
|
|
40
48
|
end
|
|
41
49
|
|
|
@@ -57,9 +65,65 @@ module Badline
|
|
|
57
65
|
ignored << "#{section}: badline doesn't read this version, left out"
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
|
-
|
|
68
|
+
disks = []
|
|
69
|
+
computer.save_modules.each do |section|
|
|
70
|
+
sections << section if KEPT.include?(section.name)
|
|
71
|
+
disks << section if DISKS.include?(section.name)
|
|
72
|
+
end
|
|
73
|
+
sections += drive(container, applied, ignored, kept_disks(container, computer, disks))
|
|
61
74
|
computer.load_modules(Keyed::Modules.new(modules(container, computer, applied) + sections))
|
|
62
|
-
Report.new(applied:, ignored: ignored + left_out(container))
|
|
75
|
+
Report.new(applied:, ignored: ignored + left_out(container, applied))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The disks badline's true drive has, which stay in the drive the
|
|
79
|
+
# snapshot puts on the bus when it is of the same model.
|
|
80
|
+
def kept_disks(container, computer, disks)
|
|
81
|
+
drive = computer.true_drive
|
|
82
|
+
return [] if drive.nil?
|
|
83
|
+
|
|
84
|
+
if Drive8050.present?(container)
|
|
85
|
+
Drive8050.model(container) == drive.model_name ? disks : []
|
|
86
|
+
else
|
|
87
|
+
drive.model_name == "2031" ? disks.first(1) : []
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The true drive's modules where the snapshot has one badline
|
|
92
|
+
# reads, or none.
|
|
93
|
+
def drive(container, applied, ignored, disks)
|
|
94
|
+
return drive8050(container, applied, ignored, disks) if Drive8050.present?(container)
|
|
95
|
+
return [] unless Drive2031.present?(container)
|
|
96
|
+
|
|
97
|
+
names = [Drive2031::UNITS.first, Drive2031::CPU, Drive2031::VIA1, Drive2031::VIA2]
|
|
98
|
+
return [] unless readable?(container, names, "2031", ignored) { |section| Drive2031.reads?(section) }
|
|
99
|
+
|
|
100
|
+
applied.concat(names)
|
|
101
|
+
Drive2031.import(container, disks.first)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def drive8050(container, applied, ignored, disks)
|
|
105
|
+
model = Drive8050.model(container)
|
|
106
|
+
return [] unless readable?(container, Drive8050::NAMES, model || "8050", ignored) do |section|
|
|
107
|
+
Drive8050.reads?(section)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if model.nil?
|
|
111
|
+
ignored << "the true drive: badline doesn't read its #{Drive8050::UNIT}'s type, left out"
|
|
112
|
+
return []
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
applied.concat(Drive8050::NAMES)
|
|
116
|
+
Drive8050.import(container, Drive8050.reference(model), disks)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Whether the snapshot has every module `names` lists at a version
|
|
120
|
+
# badline reads, saying which it doesn't in `ignored`.
|
|
121
|
+
def readable?(container, names, model, ignored, &reads)
|
|
122
|
+
unread = names.map { |name| container[name] }.reject { |section| section && reads.call(section) }
|
|
123
|
+
return true if unread.empty?
|
|
124
|
+
|
|
125
|
+
ignored << "the true #{model}: badline doesn't read #{unread.compact.join(', ')}, left out"
|
|
126
|
+
false
|
|
63
127
|
end
|
|
64
128
|
|
|
65
129
|
def required?(name) = [MainCPU::NAME, PETMem::NAME].include?(name)
|
|
@@ -135,12 +199,12 @@ module Badline
|
|
|
135
199
|
CRTC.import(crtc, out, CRTC.columns(model), CRTC.attributes(model))
|
|
136
200
|
end
|
|
137
201
|
cycles = (CRTC::CRTC_LEAD + CRTC.column_of(crtc)) % CRTC.frame_cycles(CRTC.registers(crtc))
|
|
138
|
-
fields = CRTC.run_on(computer, Keyed::Reader.parse(counters), cycles)
|
|
202
|
+
fields = CRTC.run_on(CRTC.fresh_crtc(computer), Keyed::Reader.parse(counters), cycles)
|
|
139
203
|
[Keyed.section("BL-CRTC") { |out| CRTC.copy(fields, out) }]
|
|
140
204
|
end
|
|
141
205
|
|
|
142
|
-
def left_out(container)
|
|
143
|
-
container.sections.reject { |section| READ.include?(section.name) }
|
|
206
|
+
def left_out(container, applied)
|
|
207
|
+
container.sections.reject { |section| READ.include?(section.name) || applied.include?(section.name) }
|
|
144
208
|
.map { |section| "#{section}: badline doesn't read this module, left out" }
|
|
145
209
|
end
|
|
146
210
|
end
|
|
@@ -44,8 +44,8 @@ module Badline
|
|
|
44
44
|
|
|
45
45
|
def reads?(section) = section.major == MAJOR && section.minor == MINOR
|
|
46
46
|
|
|
47
|
-
# The VIA's module, from its BL-VIA fields in `input`.
|
|
48
|
-
def export(input)
|
|
47
|
+
# The VIA's module, under `name`, from its BL-VIA fields in `input`.
|
|
48
|
+
def export(input, name = NAME)
|
|
49
49
|
fields = FieldWriter.new
|
|
50
50
|
%w[ora ddra orb ddrb].each { |key| fields.byte(input.int(key, 0)) }
|
|
51
51
|
fields.word(input.int("t1.latch", 0xffff)).word(timer1(input))
|
|
@@ -58,7 +58,7 @@ module Badline
|
|
|
58
58
|
fields.byte(input.bool("t1.pb7", true) ? 0x80 : 0)
|
|
59
59
|
fields.byte(ticks(input))
|
|
60
60
|
fields.byte(LEVELS | (input.bool("ca.c2_output", true) ? 0x80 : 0)).zeros(2)
|
|
61
|
-
fields.byte(input.bool("t2.armed", false) ? 1 : 0).zeros(2).section(
|
|
61
|
+
fields.byte(input.bool("t2.armed", false) ? 1 : 0).zeros(2).section(name, MAJOR, MINOR)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# Timer 1's counter as a read on the next cycle finds it: as it
|
|
@@ -96,13 +96,14 @@ module Badline
|
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
# The shift register from its mode, data and the ticks into its
|
|
99
|
-
# byte (#ticks), with the last bit put out on CB2
|
|
99
|
+
# byte (#ticks), with the last bit put out on CB2, or CB2 high
|
|
100
|
+
# as it is at power-on while the register is off.
|
|
100
101
|
def shift_register(out, mode, data, ticks)
|
|
101
102
|
ticks = [ticks, TICKS].min
|
|
102
103
|
bits = ticks == TICKS ? 0 : 8 - (ticks / 2)
|
|
103
104
|
bits = 8 if mode == 4 && ticks < TICKS
|
|
104
105
|
out.int("sr.mode", mode).int("sr.data", data).int("sr.bits", bits).int("sr.delay", 0)
|
|
105
|
-
out.bool("sr.clock", ticks.even?).bool("sr.cb2", ticks.zero? || data.anybits?(0x01))
|
|
106
|
+
out.bool("sr.clock", ticks.even?).bool("sr.cb2", mode.zero? || ticks.zero? || data.anybits?(0x01))
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
# Whether the shift register clocks off timer 2.
|
|
@@ -38,7 +38,8 @@ module Badline
|
|
|
38
38
|
|
|
39
39
|
def import(section, computer)
|
|
40
40
|
registers = FieldReader.new(section).skip(4).bytes(0x20)
|
|
41
|
-
|
|
41
|
+
sid = computer.sid
|
|
42
|
+
registers.first(0x19).each_with_index { |value, reg| sid.poke(sid.start + reg, value) }
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -4,20 +4,24 @@ module Badline
|
|
|
4
4
|
module Snapshot
|
|
5
5
|
module Vice
|
|
6
6
|
module VICII
|
|
7
|
-
# Reads a VIC-II module and puts it into badline's VIC.
|
|
7
|
+
# Reads a VIC-II module and puts it into badline's VIC. A reader of
|
|
8
|
+
# another layout passes no fields and sets the same state itself.
|
|
8
9
|
class Import
|
|
9
10
|
def initialize(fields)
|
|
10
11
|
@fields = fields
|
|
12
|
+
return if fields.nil?
|
|
13
|
+
|
|
11
14
|
read_beam
|
|
12
15
|
read_counters
|
|
13
16
|
read_sprites
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
def apply(computer)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
def apply(computer) = apply_to(computer.vic, computer.address_bus.color_ram, 0xd800)
|
|
20
|
+
|
|
21
|
+
# Puts it into `vic`, with its colour RAM into `color_ram` from
|
|
22
|
+
# `base`.
|
|
23
|
+
def apply_to(vic, color_ram, base)
|
|
24
|
+
@color_ram.each_with_index { |value, offset| color_ram.poke(base + offset, value) }
|
|
21
25
|
load_registers(vic)
|
|
22
26
|
position(vic)
|
|
23
27
|
load_counters(vic)
|
|
@@ -14,6 +14,9 @@ require "badline/snapshot/vice/vicii"
|
|
|
14
14
|
require "badline/snapshot/vice/vicii_import"
|
|
15
15
|
require "badline/snapshot/vice/restore"
|
|
16
16
|
require "badline/snapshot/vice/pet"
|
|
17
|
+
require "badline/snapshot/vice/cbm2"
|
|
18
|
+
require "badline/snapshot/vice/p500"
|
|
19
|
+
require "badline/snapshot/vice/p500_restore"
|
|
17
20
|
|
|
18
21
|
module Badline
|
|
19
22
|
module Snapshot
|
data/lib/badline/snapshot.rb
CHANGED
|
@@ -36,8 +36,8 @@ module Badline
|
|
|
36
36
|
# end of the instruction its CPU is in, and of an REU transfer under
|
|
37
37
|
# way (Machine::Settling), so a save advances it by a few cycles, and a
|
|
38
38
|
# second save straight after runs none. A VIC-20's, a C128's, a
|
|
39
|
-
# Plus/4's or a
|
|
40
|
-
# xplus4 or
|
|
39
|
+
# Plus/4's, a PET's or a CBM-II's snapshot is in a container named as
|
|
40
|
+
# xvic, x128, xplus4, xpet or xcbm2 names its machine.
|
|
41
41
|
def dump(computer) = Image.of(computer).to_s
|
|
42
42
|
|
|
43
43
|
def read(path) = Image.new(Container.parse(File.binread(path)), name: File.basename(path))
|
|
@@ -52,7 +52,7 @@ module Badline
|
|
|
52
52
|
def restore(computer, path, &) = read(path).restore(computer, &)
|
|
53
53
|
|
|
54
54
|
# A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128,
|
|
55
|
-
# Plus4 and
|
|
55
|
+
# Plus4, PET, CBM2 and CBM2::P500 each include it.
|
|
56
56
|
module Files
|
|
57
57
|
def save_snapshot(path) = Snapshot.save(self, path)
|
|
58
58
|
|
|
@@ -99,4 +99,12 @@ module Badline
|
|
|
99
99
|
class PET
|
|
100
100
|
include Snapshot::Files
|
|
101
101
|
end
|
|
102
|
+
|
|
103
|
+
class CBM2
|
|
104
|
+
include Snapshot::Files
|
|
105
|
+
|
|
106
|
+
class P500
|
|
107
|
+
include Snapshot::Files
|
|
108
|
+
end
|
|
109
|
+
end
|
|
102
110
|
end
|
|
@@ -8,6 +8,7 @@ module Badline
|
|
|
8
8
|
SIGNATURE = "C64 CARTRIDGE ".b
|
|
9
9
|
VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
|
|
10
10
|
PLUS4_SIGNATURE = "PLUS4 CARTRIDGE ".b
|
|
11
|
+
CBM2_SIGNATURE = "CBM2 CARTRIDGE ".b
|
|
11
12
|
CHIP_SIGNATURE = "CHIP".b
|
|
12
13
|
CHIP_HEADER_SIZE = 0x10
|
|
13
14
|
RAM_CHIP = 1
|
|
@@ -19,6 +20,9 @@ module Badline
|
|
|
19
20
|
# What .write needs of an image: the fields a parsed CRTFile reads.
|
|
20
21
|
Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
|
|
21
22
|
|
|
23
|
+
# Whether the file at +path+ is signed as a CBM-II's cartridge.
|
|
24
|
+
def self.cbm2?(path) = File.binread(path, CBM2_SIGNATURE.length) == CBM2_SIGNATURE
|
|
25
|
+
|
|
22
26
|
def self.write(path, image)
|
|
23
27
|
File.binwrite(path, encode(image))
|
|
24
28
|
end
|
|
@@ -36,7 +40,8 @@ module Badline
|
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# `bytes` stands in for the file's contents. `machine` is the one the
|
|
39
|
-
# image has to be for, :c64, :vic20 or :
|
|
43
|
+
# image has to be for, :c64, :vic20, :plus4 or :cbm2, or nil for
|
|
44
|
+
# any.
|
|
40
45
|
def initialize(path = nil, bytes: nil, machine: :c64)
|
|
41
46
|
parse(bytes || File.binread(path))
|
|
42
47
|
return if machine.nil? || @machine == machine
|
|
@@ -60,6 +65,7 @@ module Badline
|
|
|
60
65
|
return :c64 if bytes.start_with?(SIGNATURE)
|
|
61
66
|
return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
|
|
62
67
|
return :plus4 if bytes.start_with?(PLUS4_SIGNATURE)
|
|
68
|
+
return :cbm2 if bytes.start_with?(CBM2_SIGNATURE)
|
|
63
69
|
|
|
64
70
|
raise FormatError, "Missing CRT signature"
|
|
65
71
|
end
|
|
@@ -52,6 +52,13 @@ module Badline
|
|
|
52
52
|
def bam_tracks = 1..35
|
|
53
53
|
def bam_count(track) = [18, 0, 4 * track]
|
|
54
54
|
def bam_bitmap(track) = [18, 0, (4 * track) + 1]
|
|
55
|
+
def bitmap_size = 3
|
|
56
|
+
|
|
57
|
+
# The header links the directory and holds the BAM itself.
|
|
58
|
+
def header_link = [18, 1]
|
|
59
|
+
def dos_type = "2A"
|
|
60
|
+
def header_tail = 4
|
|
61
|
+
def bam_headers(_id) = []
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
end
|
|
@@ -31,7 +31,7 @@ module Badline
|
|
|
31
31
|
def directory_track = 39
|
|
32
32
|
def directory_sector = 1
|
|
33
33
|
def directory_interleave = 3
|
|
34
|
-
def interleave =
|
|
34
|
+
def interleave = 5
|
|
35
35
|
|
|
36
36
|
# Only the directory track is kept from files. The BAM's blocks on
|
|
37
37
|
# track 38 are marked in use, and the rest of that track holds files.
|
|
@@ -43,6 +43,22 @@ module Badline
|
|
|
43
43
|
def bam_tracks = 1..SIDE_TRACKS
|
|
44
44
|
def bam_count(track) = [38, 3 * ((track - 1) / 50), 6 + (5 * ((track - 1) % 50))]
|
|
45
45
|
def bam_bitmap(track) = [38, 3 * ((track - 1) / 50), 7 + (5 * ((track - 1) % 50))]
|
|
46
|
+
def bitmap_size = 4
|
|
47
|
+
|
|
48
|
+
# The header links the BAM's first block, and each BAM block the next,
|
|
49
|
+
# the last one the directory. A BAM block names the format and the
|
|
50
|
+
# first track it covers and the one past its last.
|
|
51
|
+
def header_link = [38, 0]
|
|
52
|
+
def dos_type = "2C"
|
|
53
|
+
def header_tail = 4
|
|
54
|
+
|
|
55
|
+
def bam_headers(_id)
|
|
56
|
+
groups = bam_tracks.each_slice(50).to_a
|
|
57
|
+
groups.each_with_index.map do |tracks, block|
|
|
58
|
+
link = block == groups.length - 1 ? [directory_track, directory_sector] : [38, 3 * (block + 1)]
|
|
59
|
+
[38, 3 * block, [*link, dos_type.getbyte(1), 0, tracks.first, tracks.last + 1]]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
46
62
|
end
|
|
47
63
|
end
|
|
48
64
|
end
|
|
@@ -21,6 +21,20 @@ module Badline
|
|
|
21
21
|
def bam_tracks = 1..80
|
|
22
22
|
def bam_count(track) = [40, track > 40 ? 2 : 1, 0x10 + (((track - 1) % 40) * 6)]
|
|
23
23
|
def bam_bitmap(track) = [40, track > 40 ? 2 : 1, 0x11 + (((track - 1) % 40) * 6)]
|
|
24
|
+
def bitmap_size = 5
|
|
25
|
+
|
|
26
|
+
# The header links the directory's first block, at 40/3. Each BAM
|
|
27
|
+
# block links the next, names the format and its complement, and
|
|
28
|
+
# holds the ID and the I/O byte.
|
|
29
|
+
def header_link = [40, 3]
|
|
30
|
+
def dos_type = "3D"
|
|
31
|
+
def header_tail = 2
|
|
32
|
+
|
|
33
|
+
def bam_headers(id)
|
|
34
|
+
[[40, 1, [40, 2]], [40, 2, [0, 0xff]]].map do |track, sector, link|
|
|
35
|
+
[track, sector, [*link, 0x44, 0xbb, *id, 0xc0, 0]]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
24
38
|
|
|
25
39
|
def sectors_in(_track) = 40
|
|
26
40
|
end
|
|
@@ -71,17 +71,32 @@ module Badline
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# A chain's first block goes on the free track nearest the
|
|
74
|
-
# directory. Each block after it
|
|
75
|
-
#
|
|
74
|
+
# directory, from its sector 0. Each block after it looks from the
|
|
75
|
+
# interleave further round the track (#next_sector), on the same
|
|
76
|
+
# track or, once that one is full, from the same sector on the next
|
|
77
|
+
# track out.
|
|
76
78
|
def next_block(track = nil, sector = nil)
|
|
77
79
|
candidates = track ? [track, *tracks_after(track)] : allocation_order
|
|
80
|
+
start = track ? next_sector(track, sector) : 0
|
|
78
81
|
candidates.each do |candidate|
|
|
79
|
-
free = free_sector(candidate,
|
|
82
|
+
free = free_sector(candidate, start)
|
|
80
83
|
return [candidate, free] if free
|
|
81
84
|
end
|
|
82
85
|
nil
|
|
83
86
|
end
|
|
84
87
|
|
|
88
|
+
# The interleave on from the sector. Past the track's last sector
|
|
89
|
+
# it comes round again one short, but never below sector 0, as
|
|
90
|
+
# the 2031, 8050, 8250 and 1581 DOS do.
|
|
91
|
+
def next_sector(track, sector)
|
|
92
|
+
start = sector + interleave
|
|
93
|
+
count = sectors_in(track)
|
|
94
|
+
return start if start < count
|
|
95
|
+
|
|
96
|
+
start -= count
|
|
97
|
+
start.positive? ? start - 1 : start
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
def free_sector(track, first)
|
|
86
101
|
count = sectors_in(track)
|
|
87
102
|
(0...count).map { |i| (first + i) % count }.find { |s| block_free?(track, s) }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
class DiskImage
|
|
6
|
+
# The DOS's N command. With an ID it formats the disk: every block of
|
|
7
|
+
# the tracks the BAM covers is cleared, and the header, the BAM and an
|
|
8
|
+
# empty directory are written. Without one it only clears the disk:
|
|
9
|
+
# the header takes the new name and keeps its ID and whatever else it
|
|
10
|
+
# held, the BAM marks every block free, and the directory's first
|
|
11
|
+
# block is emptied. Either way the header, the BAM's blocks and the
|
|
12
|
+
# first directory block stay in use. Each format says where these go.
|
|
13
|
+
module Formatting
|
|
14
|
+
# The disk's ID, as the header holds it.
|
|
15
|
+
def disk_id = sector_at(*header_block)[header_name + Catalog::HEADER_ID, 2]
|
|
16
|
+
|
|
17
|
+
# Formats the disk under the name, or with no ID clears it.
|
|
18
|
+
def format(name, id = nil)
|
|
19
|
+
update_image do
|
|
20
|
+
full = !id.nil?
|
|
21
|
+
put_sector(*header_block, []) if full
|
|
22
|
+
id = (id || disk_id).first(2)
|
|
23
|
+
write_header(name, id)
|
|
24
|
+
clear_blocks if full
|
|
25
|
+
headers = bam_headers(id)
|
|
26
|
+
headers.each { |track, sector, bytes| write_at(track, sector, 0, bytes) }
|
|
27
|
+
put_sector(directory_track, directory_sector, [0, 0xff])
|
|
28
|
+
bam_tracks.each { |track| free_track(track) }
|
|
29
|
+
[header_block, *headers.map { |track, sector, _| [track, sector] }, [directory_track, directory_sector]]
|
|
30
|
+
.each { |block| mark(*block, false) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Every block the BAM covers but the header.
|
|
37
|
+
def clear_blocks
|
|
38
|
+
bam_tracks.each do |track|
|
|
39
|
+
(0...sectors_in(track)).each do |sector|
|
|
40
|
+
put_sector(track, sector, []) unless [track, sector] == header_block
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The link to the next block, the format's letter, the name, the ID
|
|
46
|
+
# and the DOS type, each field after the name set off with shifted
|
|
47
|
+
# spaces.
|
|
48
|
+
def write_header(name, id)
|
|
49
|
+
write_at(*header_block, 0, [*header_link, dos_type.getbyte(1), 0])
|
|
50
|
+
write_at(*header_block, header_name,
|
|
51
|
+
encode_name(name) + [NAME_PADDING, NAME_PADDING] + id + [NAME_PADDING] +
|
|
52
|
+
dos_type.bytes + Array.new(header_tail, NAME_PADDING))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A free track counts its sectors and sets a bit for each.
|
|
56
|
+
def free_track(track)
|
|
57
|
+
count = sectors_in(track)
|
|
58
|
+
bits = (1 << count) - 1
|
|
59
|
+
@bytes[count_offset(track)] = count
|
|
60
|
+
bitmap_size.times { |i| @bytes[bitmap_offset(track) + i] = (bits >> (8 * i)) & 0xff }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_at(track, sector, offset, bytes)
|
|
64
|
+
@bytes[sector_offset(track, sector) + offset, bytes.length] = bytes
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -51,6 +51,39 @@ module Badline
|
|
|
51
51
|
files.length
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Gives a file a new name in its directory entry. A missing file
|
|
55
|
+
# fails as FILE NOT FOUND, and a new name already on the disk as
|
|
56
|
+
# FILE EXISTS.
|
|
57
|
+
def rename(old_name, new_name)
|
|
58
|
+
entry = existing_entry(old_name)
|
|
59
|
+
raise WriteError, WriteError::FILE_EXISTS if entry_named(new_name)
|
|
60
|
+
|
|
61
|
+
update_image { @bytes[entry[:offset] + 5, Directory::NAME_LENGTH] = encode_name(new_name) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Writes a new file holding the files named, one after another, as
|
|
65
|
+
# the type of the first. A missing file fails as FILE NOT FOUND, and
|
|
66
|
+
# a new name already on the disk as FILE EXISTS.
|
|
67
|
+
def copy(new_name, names)
|
|
68
|
+
sources = names.map { |name| existing_entry(name) }
|
|
69
|
+
raise WriteError, WriteError::FILE_EXISTS if entry_named(new_name)
|
|
70
|
+
|
|
71
|
+
bytes = sources.flat_map { |entry| read_chain(entry[:track], entry[:sector]) }
|
|
72
|
+
update_image { add_file(new_name, bytes, sources.first[:type]) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Adds the files named to the end of a SEQ file. A missing file fails
|
|
76
|
+
# as FILE NOT FOUND, and a file of another type as FILE TYPE
|
|
77
|
+
# MISMATCH.
|
|
78
|
+
def concat(name, names)
|
|
79
|
+
entry = existing_entry(name)
|
|
80
|
+
sources = names.map { |source| existing_entry(source) }
|
|
81
|
+
raise WriteError, WriteError::FILE_TYPE_MISMATCH unless entry[:type] == :seq
|
|
82
|
+
|
|
83
|
+
bytes = [entry, *sources].flat_map { |source| read_chain(source[:track], source[:sector]) }
|
|
84
|
+
update_image { rewrite(entry, bytes, :seq) }
|
|
85
|
+
end
|
|
86
|
+
|
|
54
87
|
# Raw block access for the DOS `U2` command. A block that the error
|
|
55
88
|
# table marks bad reads cleanly once written.
|
|
56
89
|
def write_block(track, sector, data)
|
|
@@ -81,6 +114,8 @@ module Badline
|
|
|
81
114
|
|
|
82
115
|
private
|
|
83
116
|
|
|
117
|
+
def existing_entry(name) = entry_named(name) || raise(WriteError, WriteError::FILE_NOT_FOUND)
|
|
118
|
+
|
|
84
119
|
def update_image
|
|
85
120
|
raise WriteError, WriteError::WRITE_PROTECT_ON unless writable?
|
|
86
121
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "badline/storage/disk_image/bam"
|
|
4
4
|
require "badline/storage/disk_image/catalog"
|
|
5
5
|
require "badline/storage/disk_image/directory"
|
|
6
|
+
require "badline/storage/disk_image/formatting"
|
|
6
7
|
require "badline/storage/disk_image/writing"
|
|
7
8
|
|
|
8
9
|
module Badline
|
|
@@ -15,6 +16,7 @@ module Badline
|
|
|
15
16
|
include Bam
|
|
16
17
|
include Catalog
|
|
17
18
|
include Directory
|
|
19
|
+
include Formatting
|
|
18
20
|
include Writing
|
|
19
21
|
|
|
20
22
|
SECTOR_SIZE = 256
|
data/lib/badline/storage.rb
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Support
|
|
5
|
+
# A Checkpoint of a CBM-II: its CPU with both segment registers, the
|
|
6
|
+
# 2K of segment 15's RAM, the RAM board's segments, the screen RAM, the
|
|
7
|
+
# picture, the CRTC's registers and counters, the CIA, the ACIA's
|
|
8
|
+
# registers and both TPIs.
|
|
9
|
+
module CBM2Checkpoint
|
|
10
|
+
COMPONENTS = %w[cpu ram segments screen display crtc cia acia tpi1 tpi2].freeze
|
|
11
|
+
|
|
12
|
+
def self.take(machine)
|
|
13
|
+
bus = machine.bus
|
|
14
|
+
Checkpoint.new(machine.cycles, [
|
|
15
|
+
Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu) +
|
|
16
|
+
[bus.execution_segment, bus.indirect_segment]),
|
|
17
|
+
Checkpoint.fnv1a(Checkpoint.ram(bus.ram, 0x800)),
|
|
18
|
+
Checkpoint.fnv1a(segments(bus)),
|
|
19
|
+
Checkpoint.fnv1a(Array.new(0x800) { |offset| bus.screen.peek(0xd000 + offset) }),
|
|
20
|
+
Checkpoint.fnv1a(machine.video.display),
|
|
21
|
+
Checkpoint.fnv1a(crtc_state(machine.crtc)),
|
|
22
|
+
Checkpoint.fnv1a(Checkpoint.cia_state(machine.cia)),
|
|
23
|
+
Checkpoint.fnv1a(acia_state(machine.acia)),
|
|
24
|
+
Checkpoint.fnv1a(tpi_state(machine.tpi1)),
|
|
25
|
+
Checkpoint.fnv1a(tpi_state(machine.tpi2))
|
|
26
|
+
], COMPONENTS)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
|
|
30
|
+
|
|
31
|
+
# Every byte of the segments the RAM board fills, segment by segment.
|
|
32
|
+
def self.segments(bus)
|
|
33
|
+
bytes = []
|
|
34
|
+
bus.segment_ram.each { |memory| bytes.concat(Checkpoint.ram(memory, 0x10000)) if memory }
|
|
35
|
+
bytes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.crtc_state(crtc) = crtc.registers + [crtc.column, crtc.line, crtc.row]
|
|
39
|
+
|
|
40
|
+
def self.acia_state(acia) = [acia.command, acia.control, acia.status]
|
|
41
|
+
|
|
42
|
+
def self.tpi_state(tpi)
|
|
43
|
+
[tpi.port_a_output, tpi.port_b_output, tpi.port_c_output, tpi.latches, tpi.mask, tpi.control,
|
|
44
|
+
tpi.irq? ? 1 : 0, *tpi.stack]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Support
|
|
5
|
+
# A Checkpoint of a P500: its CPU with both segment registers, the 2K
|
|
6
|
+
# of segment 15's RAM, segments 0 and 1, the static video matrix, the
|
|
7
|
+
# colour RAM's nibbles, the picture, the VIC's registers and beam, the
|
|
8
|
+
# SID's registers, the CIA, the ACIA's registers and both TPIs, the
|
|
9
|
+
# last three as CBM2Checkpoint digests them.
|
|
10
|
+
module P500Checkpoint
|
|
11
|
+
COMPONENTS = %w[cpu ram segments screen color_ram display vic sid cia acia tpi1 tpi2].freeze
|
|
12
|
+
|
|
13
|
+
def self.take(machine)
|
|
14
|
+
bus = machine.bus
|
|
15
|
+
Checkpoint.new(machine.cycles, [
|
|
16
|
+
Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu) +
|
|
17
|
+
[bus.execution_segment, bus.indirect_segment]),
|
|
18
|
+
Checkpoint.fnv1a(Checkpoint.ram(bus.ram, 0x800)),
|
|
19
|
+
Checkpoint.fnv1a(CBM2Checkpoint.segments(bus)),
|
|
20
|
+
Checkpoint.fnv1a(Array.new(0x400) { |offset| bus.screen.peek(0xd000 + offset) }),
|
|
21
|
+
Checkpoint.fnv1a(Checkpoint.color_ram(machine.color_ram, 0xd400)),
|
|
22
|
+
Checkpoint.fnv1a(machine.vic.display),
|
|
23
|
+
Checkpoint.fnv1a(Checkpoint.vic_state(machine.vic)),
|
|
24
|
+
Checkpoint.fnv1a(Array.new(0x20) { |reg| machine.sid.register(reg) }),
|
|
25
|
+
Checkpoint.fnv1a(Checkpoint.cia_state(machine.cia)),
|
|
26
|
+
Checkpoint.fnv1a(CBM2Checkpoint.acia_state(machine.acia)),
|
|
27
|
+
Checkpoint.fnv1a(CBM2Checkpoint.tpi_state(machine.tpi1)),
|
|
28
|
+
Checkpoint.fnv1a(CBM2Checkpoint.tpi_state(machine.tpi2))
|
|
29
|
+
], COMPONENTS)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/badline/version.rb
CHANGED