badline 0.15.1 → 0.16.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/CHANGELOG.md +55 -0
- data/CLAUDE.md +8 -7
- data/README.md +128 -9
- data/doc/media.md +9 -0
- data/doc/pinned-behaviour.md +67 -0
- data/doc/snapshot-format.md +215 -18
- data/doc/snapshots.md +55 -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 +74 -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/model2031/ieee_port.rb +133 -0
- data/lib/badline/drives/model2031/keyed_state.rb +66 -0
- data/lib/badline/drives/model2031/parts.rb +25 -0
- data/lib/badline/drives/model2031.rb +92 -0
- 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 +24 -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 +151 -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 +29 -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/true_drive.rb +19 -6
- data/lib/badline/media/vic20_media.rb +2 -2
- data/lib/badline/media.rb +41 -17
- data/lib/badline/options/family.rb +17 -6
- data/lib/badline/options/help.rb +23 -3
- data/lib/badline/options/table.rb +22 -4
- data/lib/badline/options/validation.rb +84 -5
- data/lib/badline/options.rb +47 -16
- data/lib/badline/pet/bus/expansion.rb +200 -0
- data/lib/badline/pet/bus.rb +222 -0
- data/lib/badline/pet/canvas.rb +97 -0
- data/lib/badline/pet/cassettes.rb +63 -0
- data/lib/badline/pet/characters.rb +32 -0
- data/lib/badline/pet/cpu.rb +10 -0
- data/lib/badline/pet/crtc_video.rb +155 -0
- data/lib/badline/pet/discrete_video.rb +83 -0
- data/lib/badline/pet/ieee_drive.rb +76 -0
- data/lib/badline/pet/ieee_ports.rb +77 -0
- data/lib/badline/pet/ieee_traps.rb +80 -0
- data/lib/badline/pet/keyboard_ports.rb +74 -0
- data/lib/badline/pet/keyed_modules.rb +88 -0
- data/lib/badline/pet/keyed_parts.rb +127 -0
- data/lib/badline/pet/keyed_state.rb +42 -0
- data/lib/badline/pet/model.rb +163 -0
- data/lib/badline/pet/rom_sets.rb +89 -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 +244 -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 +169 -0
- data/lib/badline/roms/dos2031.rom +0 -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/characters-din.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-8296.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 +6 -1
- data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
- data/lib/badline/snapshot/field_doc.rb +17 -3
- data/lib/badline/snapshot/image.rb +28 -10
- data/lib/badline/snapshot/pet_setup.rb +88 -0
- data/lib/badline/snapshot/storage_setup.rb +7 -1
- data/lib/badline/snapshot/vice/pet.rb +117 -0
- data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
- data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
- data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
- data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
- data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
- data/lib/badline/snapshot/vice/pet_via.rb +164 -0
- data/lib/badline/snapshot/vice.rb +1 -0
- 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 +70 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# PETMEM 1.3: the board, the RAM below $8000 in K and the 8096's
|
|
8
|
+
# control register at $FFF0, then the RAM, the screen RAM, 1K or 2K
|
|
9
|
+
# as the board has, or on the 8296 its 32K from $8000, an 8096's or
|
|
10
|
+
# 8296's 64K board, and 24 bytes of xpet's own settings, which name
|
|
11
|
+
# the 2001.
|
|
12
|
+
#
|
|
13
|
+
# The board is 0 for the discrete video, 1 for a 40-column CRTC, 2
|
|
14
|
+
# for an 80-column one, 3 for the SuperPET, 4 for the 8096 and 5 for
|
|
15
|
+
# the 8296. xpet's ROMs aren't in the snapshot.
|
|
16
|
+
module PETMem
|
|
17
|
+
NAME = "PETMEM"
|
|
18
|
+
MAJOR = 1
|
|
19
|
+
MINOR = 3
|
|
20
|
+
DISCRETE = 0
|
|
21
|
+
CRTC40 = 1
|
|
22
|
+
CRTC80 = 2
|
|
23
|
+
SUPERPET = 3
|
|
24
|
+
CBM8096 = 4
|
|
25
|
+
CBM8296 = 5
|
|
26
|
+
# The byte after the header's control register, as xpet writes it.
|
|
27
|
+
HEADER_END = 0x05
|
|
28
|
+
# xpet's settings at the end, but for the second byte, which is
|
|
29
|
+
# SETTINGS_2001 on a 2001.
|
|
30
|
+
SETTINGS = [0x00, 0x00, 0x00, 0x00, 0xd6, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00,
|
|
31
|
+
0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00].freeze
|
|
32
|
+
SETTINGS_2001 = 0x03
|
|
33
|
+
EXPANSION = 0x10000
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def reads?(section) = section.major == MAJOR && section.minor == MINOR
|
|
38
|
+
|
|
39
|
+
def board(model)
|
|
40
|
+
return CBM8296 if model.cbm8296?
|
|
41
|
+
return CBM8096 if model.expansion
|
|
42
|
+
|
|
43
|
+
case model.video
|
|
44
|
+
when :crtc40 then CRTC40
|
|
45
|
+
when :crtc80 then CRTC80
|
|
46
|
+
else DISCRETE
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def export(machine)
|
|
51
|
+
bus = machine.bus
|
|
52
|
+
expansion = bus.expansion
|
|
53
|
+
fields = FieldWriter.new.byte(board(machine.model)).byte(0).byte(machine.ram_kb)
|
|
54
|
+
fields.byte(expansion ? expansion.control : 0).byte(HEADER_END)
|
|
55
|
+
fields.bytes(machine.ram.snapshot).bytes(bus.screen.cells)
|
|
56
|
+
fields.bytes(expansion.cells) if expansion
|
|
57
|
+
settings = SETTINGS.dup
|
|
58
|
+
settings[1] = SETTINGS_2001 if machine.model.basic1?
|
|
59
|
+
fields.bytes(settings).section(NAME, MAJOR, MINOR)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The board's number in the header.
|
|
63
|
+
def board_of(section) = FieldReader.new(section).byte
|
|
64
|
+
|
|
65
|
+
def ram_kb(section) = FieldReader.new(section).skip(2).byte
|
|
66
|
+
|
|
67
|
+
# Whether xpet's settings at the end name a 2001.
|
|
68
|
+
def pet2001?(section)
|
|
69
|
+
data = section.data
|
|
70
|
+
data.bytesize >= SETTINGS.length && data.getbyte(data.bytesize - SETTINGS.length + 1).to_i.anybits?(0x01)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The RAM, the screen RAM and an 8096's or 8296's board and control
|
|
74
|
+
# register
|
|
75
|
+
# by their BL-RAM keys, in a keyed module's writer, for a machine
|
|
76
|
+
# with `screen_bytes` of screen RAM.
|
|
77
|
+
def import(section, out, screen_bytes)
|
|
78
|
+
fields = FieldReader.new(section)
|
|
79
|
+
board = fields.byte
|
|
80
|
+
fields.skip(1)
|
|
81
|
+
ram_kb = fields.byte
|
|
82
|
+
control = fields.byte
|
|
83
|
+
fields.skip(1)
|
|
84
|
+
out.bytes("ram", fields.string(ram_kb * 1024)).bytes("screen", fields.string(screen_bytes))
|
|
85
|
+
return unless [CBM8096, CBM8296].include?(board)
|
|
86
|
+
|
|
87
|
+
out.int("expansion.control", control).bytes("expansion.ram", fields.string(EXPANSION))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# PIA1 and PIA2 1.0: port A's output and direction registers and
|
|
8
|
+
# its control register as read, with the CA1 and CA2 flags in bits 7
|
|
9
|
+
# and 6, the same for port B, and the levels on CA2 and CB2 in bits
|
|
10
|
+
# 7 and 6 of the last byte, high while a line is an input.
|
|
11
|
+
#
|
|
12
|
+
# Read: each side's flags come from its control register, and a
|
|
13
|
+
# handshake under way from the level its C2 output is at. The levels
|
|
14
|
+
# outside on CA1 and CB1 aren't saved: they stay as the machine has
|
|
15
|
+
# them, with PIA 1's CB1 on the vertical drive line.
|
|
16
|
+
module PIAs
|
|
17
|
+
NAMES = %w[PIA1 PIA2].freeze
|
|
18
|
+
MAJOR = 1
|
|
19
|
+
MINOR = 0
|
|
20
|
+
CA2_HIGH = 0x80
|
|
21
|
+
CB2_HIGH = 0x40
|
|
22
|
+
HANDSHAKE = 0x20
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def reads?(section) = section.major == MAJOR && section.minor == MINOR
|
|
27
|
+
|
|
28
|
+
# The PIA's module, from its BL-PIA1 or BL-PIA2 fields in `input`.
|
|
29
|
+
def export(name, pia, input)
|
|
30
|
+
fields = FieldWriter.new.byte(input.int("ora", 0)).byte(input.int("ddra", 0)).byte(pia.cra)
|
|
31
|
+
fields.byte(input.int("orb", 0)).byte(input.int("ddrb", 0)).byte(pia.crb)
|
|
32
|
+
fields.byte((pia.ca2_output ? CA2_HIGH : 0) | (pia.cb2_output ? CB2_HIGH : 0))
|
|
33
|
+
fields.section(name, MAJOR, MINOR)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The registers and the control lines by their BL-PIA keys, in a
|
|
37
|
+
# keyed module's writer.
|
|
38
|
+
def import(section, out)
|
|
39
|
+
ora, ddra, cra, orb, ddrb, crb, lines = FieldReader.new(section).bytes(7)
|
|
40
|
+
out.int("ora", ora).int("ddra", ddra).int("orb", orb).int("ddrb", ddrb)
|
|
41
|
+
control_lines(out, "ca.", cra, lines.anybits?(CA2_HIGH))
|
|
42
|
+
control_lines(out, "cb.", crb, lines.anybits?(CB2_HIGH))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def control_lines(out, prefix, register, c2_high)
|
|
46
|
+
control = register & 0x3f
|
|
47
|
+
out.int("#{prefix}control", control).bool("#{prefix}c1_flag", register.anybits?(0x80))
|
|
48
|
+
out.bool("#{prefix}c2_flag", register.anybits?(0x40)).int("#{prefix}pulse", 0)
|
|
49
|
+
out.bool("#{prefix}handshake_low", control & 0x38 == HANDSHAKE && !c2_high)
|
|
50
|
+
out.bool("#{prefix}c2_high", c2_high) if control.nobits?(0x20)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# Restores a PET from a snapshot xpet wrote. The modules badline
|
|
8
|
+
# reads become the PET's own keyed modules (Keyed), which it loads
|
|
9
|
+
# as it loads its own snapshot. Whatever they don't set stays as the
|
|
10
|
+
# machine has it at power-on, and the datasettes' tapes and the
|
|
11
|
+
# disk device 8 serves stay as they are.
|
|
12
|
+
module Restore
|
|
13
|
+
# MAINCPU and PETMEM hold the machine itself, so a version badline
|
|
14
|
+
# doesn't read fails the restore. The others are left out and
|
|
15
|
+
# reported.
|
|
16
|
+
READ = [MainCPU::NAME, PETMem::NAME, CRTC::NAME, "PIA1", "PIA2", VIA::NAME].freeze
|
|
17
|
+
# The host's media, which a restore keeps.
|
|
18
|
+
KEPT = %w[BL-TAPE1 BL-TAPE2 BL-TRAPDOS].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Applies the modules badline reads to `computer`, and returns a
|
|
23
|
+
# Report. The snapshot goes into a new machine first, so one that
|
|
24
|
+
# fails leaves `computer` as it was, and one of a machine built
|
|
25
|
+
# another way fails too.
|
|
26
|
+
def import(container, computer)
|
|
27
|
+
unless computer.family == :pet
|
|
28
|
+
raise FormatError, "the snapshot is of a PET, not a #{Image::FAMILIES.fetch(computer.family.to_s)}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
setup = PET.setup(container)
|
|
32
|
+
ours = PETSetup.of(computer)
|
|
33
|
+
raise FormatError, "the snapshot is of a #{setup}, not a #{ours}" unless setup == ours
|
|
34
|
+
|
|
35
|
+
apply(container, setup.build)
|
|
36
|
+
computer.power_cycle!
|
|
37
|
+
report = apply(container, computer)
|
|
38
|
+
kept = computer.mounted? ? ["the disk mounted in device 8 stays mounted"] : []
|
|
39
|
+
Report.new(applied: report.applied, ignored: report.ignored + kept)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Applies the modules badline reads to `computer`, a machine built
|
|
43
|
+
# as the snapshot's was, and returns the Report.
|
|
44
|
+
def apply(container, computer)
|
|
45
|
+
applied = []
|
|
46
|
+
ignored = []
|
|
47
|
+
sections = []
|
|
48
|
+
READ.each do |name|
|
|
49
|
+
section = container[name]
|
|
50
|
+
next if section.nil?
|
|
51
|
+
|
|
52
|
+
if reads?(section)
|
|
53
|
+
applied << name
|
|
54
|
+
else
|
|
55
|
+
raise FormatError, "#{section}: badline doesn't read this version" if required?(name)
|
|
56
|
+
|
|
57
|
+
ignored << "#{section}: badline doesn't read this version, left out"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
computer.save_modules.each { |section| sections << section if KEPT.include?(section.name) }
|
|
61
|
+
computer.load_modules(Keyed::Modules.new(modules(container, computer, applied) + sections))
|
|
62
|
+
Report.new(applied:, ignored: ignored + left_out(container))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def required?(name) = [MainCPU::NAME, PETMem::NAME].include?(name)
|
|
66
|
+
|
|
67
|
+
def reads?(section)
|
|
68
|
+
case section.name
|
|
69
|
+
when MainCPU::NAME then MainCPU.reads?(section)
|
|
70
|
+
when PETMem::NAME then PETMem.reads?(section)
|
|
71
|
+
when CRTC::NAME then CRTC.reads?(section)
|
|
72
|
+
when VIA::NAME then VIA.reads?(section)
|
|
73
|
+
else PIAs.reads?(section)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The PET's keyed modules from the ones `applied` names.
|
|
78
|
+
def modules(container, computer, applied)
|
|
79
|
+
model = computer.model
|
|
80
|
+
crtc = applied.include?(CRTC::NAME) ? container[CRTC::NAME] : nil
|
|
81
|
+
video = video(crtc, computer)
|
|
82
|
+
drive = driving?(crtc, video, model)
|
|
83
|
+
sections = [Keyed.section("BL-MACHINE") { |out| machine(out, container, computer, drive) },
|
|
84
|
+
Keyed.section("BL-RAM") do |out|
|
|
85
|
+
PETMem.import(container[PETMem::NAME], out, model.screen_bytes)
|
|
86
|
+
end]
|
|
87
|
+
sections << Keyed.section("BL-CPU") { |out| MainCPU.import(container[MainCPU::NAME], out) }
|
|
88
|
+
%w[PIA1 PIA2].each do |name|
|
|
89
|
+
next unless applied.include?(name)
|
|
90
|
+
|
|
91
|
+
sections << Keyed.section("BL-#{name}") do |out|
|
|
92
|
+
PIAs.import(container[name], out)
|
|
93
|
+
out.bool("cb.c1_high", drive) if name == "PIA1"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
if applied.include?(VIA::NAME)
|
|
97
|
+
sections << Keyed.section("BL-VIA") { |out| VIA.import(container[VIA::NAME], out) }
|
|
98
|
+
end
|
|
99
|
+
sections + video
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def machine(out, container, computer, drive)
|
|
103
|
+
out.text("family", "pet")
|
|
104
|
+
PETSetup.of(computer).write_fields(out)
|
|
105
|
+
out.int("cycles", FieldReader.new(container[MainCPU::NAME]).qword).bool("vertical_drive", drive)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The vertical drive line, low in the CRTC's vertical sync or past
|
|
109
|
+
# the discrete video's rows, as the video's module, `video`, has it.
|
|
110
|
+
def driving?(crtc, video, model)
|
|
111
|
+
return true if crtc.nil?
|
|
112
|
+
|
|
113
|
+
fields = Keyed::Reader.parse(video.first)
|
|
114
|
+
if model.video == :discrete
|
|
115
|
+
fields.int("line", 0) < Badline::PET::DiscreteVideo::DISPLAYED_LINES
|
|
116
|
+
else
|
|
117
|
+
fields.int("vsync_lines", 0).zero?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The discrete video's module, or the CRTC's, run on from xpet's
|
|
122
|
+
# counters to where badline's drops the vertical drive line on the
|
|
123
|
+
# cycle xpet's does (CRTC::CRTC_LEAD). The CRTC runs on from the
|
|
124
|
+
# start of the beam's line, or for a beam in the first cycles of
|
|
125
|
+
# its line, a frame less those cycles on, which lands on the same
|
|
126
|
+
# counters while the registers stood through the frame as they
|
|
127
|
+
# stand.
|
|
128
|
+
def video(crtc, computer)
|
|
129
|
+
return [] if crtc.nil?
|
|
130
|
+
|
|
131
|
+
model = computer.model
|
|
132
|
+
return [Keyed.section("BL-VIDEO") { |out| CRTC.import_discrete(crtc, out) }] if model.video == :discrete
|
|
133
|
+
|
|
134
|
+
counters = Keyed.section("BL-CRTC") do |out|
|
|
135
|
+
CRTC.import(crtc, out, CRTC.columns(model), CRTC.attributes(model))
|
|
136
|
+
end
|
|
137
|
+
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)
|
|
139
|
+
[Keyed.section("BL-CRTC") { |out| CRTC.copy(fields, out) }]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def left_out(container)
|
|
143
|
+
container.sections.reject { |section| READ.include?(section.name) }
|
|
144
|
+
.map { |section| "#{section}: badline doesn't read this module, left out" }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Snapshot
|
|
5
|
+
module Vice
|
|
6
|
+
module PET
|
|
7
|
+
# VIA 2.2, the 6522 at $E840: port A's and port B's output and
|
|
8
|
+
# direction registers, timer 1's latch and counter, timer 2's low
|
|
9
|
+
# and high latch, the value it was last loaded with and its counter,
|
|
10
|
+
# which timers will set their interrupt flag (timer 1 in bit 7,
|
|
11
|
+
# timer 2 in bit 6 while it counts cycles), the shift register, the
|
|
12
|
+
# ACR, the PCR, the interrupt flags and enable mask, timer 1's level
|
|
13
|
+
# on PB7 in bit 7, the shift register's clock ticks into its byte,
|
|
14
|
+
# CA2's level in bit 7, and whether timer 2's next underflow sets
|
|
15
|
+
# its flag. xpet saves each counter as a read on the next cycle
|
|
16
|
+
# finds it, where badline's holds it before that cycle counts.
|
|
17
|
+
#
|
|
18
|
+
# Written: timer 2's high latch and the value it was loaded with,
|
|
19
|
+
# which badline doesn't keep, as xpet has them before a program
|
|
20
|
+
# loads it, or while the shift register clocks off it, as its low
|
|
21
|
+
# byte reloads. Free-running, the shift register's ticks are only
|
|
22
|
+
# whether CB1 is low.
|
|
23
|
+
#
|
|
24
|
+
# Read: the registers and both timers with their latches, as they
|
|
25
|
+
# count from the next cycle, and the shift register where its clock
|
|
26
|
+
# ticks are. A load or reload under way, a shift clock tick due off
|
|
27
|
+
# timer 2 and the latched ports start afresh, and CB2's level
|
|
28
|
+
# follows from the PCR.
|
|
29
|
+
module VIA
|
|
30
|
+
NAME = "VIA"
|
|
31
|
+
MAJOR = 2
|
|
32
|
+
MINOR = 2
|
|
33
|
+
T1_ARMED = 0x80
|
|
34
|
+
T2_TIMED = 0x40
|
|
35
|
+
LEVELS = 0x60
|
|
36
|
+
# The shift register's clock ticks a byte takes, which an idle
|
|
37
|
+
# register's count stands at.
|
|
38
|
+
TICKS = 16
|
|
39
|
+
SIZE = 28
|
|
40
|
+
# The shift register's modes that clock off timer 2.
|
|
41
|
+
T2_SHIFTS = [1, 4, 5].freeze
|
|
42
|
+
|
|
43
|
+
module_function
|
|
44
|
+
|
|
45
|
+
def reads?(section) = section.major == MAJOR && section.minor == MINOR
|
|
46
|
+
|
|
47
|
+
# The VIA's module, from its BL-VIA fields in `input`.
|
|
48
|
+
def export(input)
|
|
49
|
+
fields = FieldWriter.new
|
|
50
|
+
%w[ora ddra orb ddrb].each { |key| fields.byte(input.int(key, 0)) }
|
|
51
|
+
fields.word(input.int("t1.latch", 0xffff)).word(timer1(input))
|
|
52
|
+
t2 = timer2(input)
|
|
53
|
+
latch_low = input.int("t2.latch_low", 0xff)
|
|
54
|
+
fields.byte(latch_low).byte(0xff).word(shifting?(input) ? (t2 & 0xff00) | latch_low : 0xffff).word(t2)
|
|
55
|
+
fields.byte(armed(input)).byte(input.int("sr.data", 0))
|
|
56
|
+
fields.byte(input.int("acr", 0)).byte(input.int("pcr", 0))
|
|
57
|
+
fields.byte(input.int("ifr.flags", 0)).byte(input.int("ifr.enable", 0))
|
|
58
|
+
fields.byte(input.bool("t1.pb7", true) ? 0x80 : 0)
|
|
59
|
+
fields.byte(ticks(input))
|
|
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(NAME, MAJOR, MINOR)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Timer 1's counter as a read on the next cycle finds it: as it
|
|
65
|
+
# stands while a load holds it, the latch while it reloads, and one
|
|
66
|
+
# less otherwise.
|
|
67
|
+
def timer1(input)
|
|
68
|
+
counter = input.int("t1.counter", 0xffff)
|
|
69
|
+
return counter if input.bool("t1.hold", false)
|
|
70
|
+
return input.int("t1.latch", 0xffff) if input.bool("t1.reload", false)
|
|
71
|
+
|
|
72
|
+
(counter - 1) & 0xffff
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Timer 2's counter as a read on the next cycle finds it: as it
|
|
76
|
+
# stands while it counts pulses or a load holds it, and one less
|
|
77
|
+
# otherwise.
|
|
78
|
+
def timer2(input)
|
|
79
|
+
counter = input.int("t2.counter", 0xffff)
|
|
80
|
+
return counter if input.bool("t2.hold", false) || input.bool("t2.counting_pulses", false)
|
|
81
|
+
|
|
82
|
+
(counter - 1) & 0xffff
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The shift register's clock ticks into its byte: two a bit, the
|
|
86
|
+
# first one dropping CB1, TICKS when idle. Free-running, its bits
|
|
87
|
+
# never run out, and only whether CB1 is low counts.
|
|
88
|
+
def ticks(input)
|
|
89
|
+
bits = input.int("sr.bits", 0)
|
|
90
|
+
return TICKS if bits.zero?
|
|
91
|
+
|
|
92
|
+
low = input.bool("sr.clock", true) ? 0 : 1
|
|
93
|
+
return low if input.int("sr.mode", 0) == 4
|
|
94
|
+
|
|
95
|
+
((8 - bits) * 2) + low
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The shift register from its mode, data and the ticks into its
|
|
99
|
+
# byte (#ticks), with the last bit put out on CB2.
|
|
100
|
+
def shift_register(out, mode, data, ticks)
|
|
101
|
+
ticks = [ticks, TICKS].min
|
|
102
|
+
bits = ticks == TICKS ? 0 : 8 - (ticks / 2)
|
|
103
|
+
bits = 8 if mode == 4 && ticks < TICKS
|
|
104
|
+
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
|
+
end
|
|
107
|
+
|
|
108
|
+
# Whether the shift register clocks off timer 2.
|
|
109
|
+
def shifting?(input) = T2_SHIFTS.include?((input.int("acr", 0) >> 2) & 0x07)
|
|
110
|
+
|
|
111
|
+
# Timer 2 sets its flag, or clocks the shift register, while it
|
|
112
|
+
# counts cycles.
|
|
113
|
+
def armed(input)
|
|
114
|
+
pulses = input.bool("t2.count_pulses", false)
|
|
115
|
+
timed = !pulses && (input.bool("t2.armed", false) || shifting?(input))
|
|
116
|
+
(input.bool("t1.armed", false) ? T1_ARMED : 0) | (timed ? T2_TIMED : 0)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The registers, the timers and the control lines by their BL-VIA
|
|
120
|
+
# keys, in a keyed module's writer.
|
|
121
|
+
def import(section, out)
|
|
122
|
+
bytes = FieldReader.new(section).bytes(SIZE)
|
|
123
|
+
acr = bytes[0x10]
|
|
124
|
+
pcr = bytes[0x11]
|
|
125
|
+
out.int("ora", bytes[0]).int("ddra", bytes[1]).int("orb", bytes[2]).int("ddrb", bytes[3])
|
|
126
|
+
out.int("acr", acr).int("pcr", pcr)
|
|
127
|
+
timers(out, bytes, acr)
|
|
128
|
+
shift_register(out, (acr >> 2) & 0x07, bytes[0x0f], bytes[0x15])
|
|
129
|
+
out.int("ifr.flags", bytes[0x12] & 0x7f).int("ifr.enable", bytes[0x13] & 0x7f)
|
|
130
|
+
control_lines(out, "ca.", pcr & 0x0f, bytes[0x16].anybits?(0x80))
|
|
131
|
+
control_lines(out, "cb.", pcr >> 4, true)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def timers(out, bytes, acr)
|
|
135
|
+
out.int("t1.latch", bytes[4] | (bytes[5] << 8)).int("t1.counter",
|
|
136
|
+
((bytes[6] | (bytes[7] << 8)) + 1) & 0xffff)
|
|
137
|
+
out.bool("t1.hold", false)
|
|
138
|
+
out.bool("t1.reload", false).bool("t1.armed", bytes[0x0e].anybits?(T1_ARMED))
|
|
139
|
+
out.bool("t1.pb7", bytes[0x14].anybits?(0x80))
|
|
140
|
+
pulses = acr.anybits?(0x20)
|
|
141
|
+
t2 = bytes[0x0c] | (bytes[0x0d] << 8)
|
|
142
|
+
out.int("t2.latch_low", bytes[8]).int("t2.counter", pulses ? t2 : (t2 + 1) & 0xffff).bool("t2.hold", false)
|
|
143
|
+
out.bool("t2.low_reload", false).bool("t2.low_underflowed", false)
|
|
144
|
+
out.bool("t2.armed", bytes[0x19].anybits?(0x01))
|
|
145
|
+
out.bool("t2.count_pulses", pulses).bool("t2.counting_pulses", pulses)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A handshake under way holds C2 low.
|
|
149
|
+
def control_lines(out, prefix, control, c2_high)
|
|
150
|
+
mode = control >> 1
|
|
151
|
+
handshake_low = mode == 4 && !c2_high
|
|
152
|
+
output = case mode
|
|
153
|
+
when 4 then !handshake_low
|
|
154
|
+
when 6 then false
|
|
155
|
+
else true
|
|
156
|
+
end
|
|
157
|
+
out.int("#{prefix}control", control).bool("#{prefix}handshake_low", handshake_low)
|
|
158
|
+
out.int("#{prefix}pulse", 0).bool("#{prefix}c2_output", output)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/badline/snapshot.rb
CHANGED
|
@@ -35,9 +35,9 @@ module Badline
|
|
|
35
35
|
# The machine's snapshot file. Saving first runs the machine on to the
|
|
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
|
-
# second save straight after runs none. A VIC-20's, a C128's
|
|
39
|
-
# Plus/4's snapshot is in a container named as xvic, x128
|
|
40
|
-
# names its machine.
|
|
38
|
+
# second save straight after runs none. A VIC-20's, a C128's, a
|
|
39
|
+
# Plus/4's or a PET's snapshot is in a container named as xvic, x128,
|
|
40
|
+
# xplus4 or xpet 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))
|
|
@@ -51,8 +51,8 @@ module Badline
|
|
|
51
51
|
# snapshot that fails leaves the machine as it was.
|
|
52
52
|
def restore(computer, path, &) = read(path).restore(computer, &)
|
|
53
53
|
|
|
54
|
-
# A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128
|
|
55
|
-
# and
|
|
54
|
+
# A machine's snapshots, as .vsf files and in memory. C64, Vic20, C128,
|
|
55
|
+
# Plus4 and PET each include it.
|
|
56
56
|
module Files
|
|
57
57
|
def save_snapshot(path) = Snapshot.save(self, path)
|
|
58
58
|
|
|
@@ -95,4 +95,8 @@ module Badline
|
|
|
95
95
|
class Plus4
|
|
96
96
|
include Snapshot::Files
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
class PET
|
|
100
|
+
include Snapshot::Files
|
|
101
|
+
end
|
|
98
102
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# An 8050 disk: 77 tracks on one side, in four speed zones.
|
|
6
|
+
class D80Image < DiskImage
|
|
7
|
+
ERROR_TABLES = { 535_331 => 2083 }.freeze
|
|
8
|
+
|
|
9
|
+
# The last track of each zone but the innermost, from the outermost
|
|
10
|
+
# in: tracks 1-39, 40-53 and 54-64, and the last zone from 65 on.
|
|
11
|
+
ZONE_LAST_TRACKS = [39, 53, 64].freeze
|
|
12
|
+
|
|
13
|
+
# The sectors a track holds in each zone, from the outermost in.
|
|
14
|
+
ZONE_SECTORS = [29, 27, 25, 23].freeze
|
|
15
|
+
|
|
16
|
+
# The tracks a side holds.
|
|
17
|
+
SIDE_TRACKS = 77
|
|
18
|
+
|
|
19
|
+
def sectors_in(track)
|
|
20
|
+
zone = ZONE_LAST_TRACKS.count { |last| track > last }
|
|
21
|
+
ZONE_SECTORS[zone]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def error_tables = ERROR_TABLES
|
|
27
|
+
|
|
28
|
+
# The header block holds the disk name at $06 and the ID at $18,
|
|
29
|
+
# after the link to the BAM's first block and the format, "C".
|
|
30
|
+
def header_name = 0x06
|
|
31
|
+
def directory_track = 39
|
|
32
|
+
def directory_sector = 1
|
|
33
|
+
def directory_interleave = 3
|
|
34
|
+
def interleave = 6
|
|
35
|
+
|
|
36
|
+
# Only the directory track is kept from files. The BAM's blocks on
|
|
37
|
+
# track 38 are marked in use, and the rest of that track holds files.
|
|
38
|
+
def reserved_tracks = [39]
|
|
39
|
+
|
|
40
|
+
# The BAM takes blocks 0, 3, 6 and 9 of track 38 as it needs them,
|
|
41
|
+
# each covering 50 tracks with a free count and a four-byte bitmap
|
|
42
|
+
# each from offset 6.
|
|
43
|
+
def bam_tracks = 1..SIDE_TRACKS
|
|
44
|
+
def bam_count(track) = [38, 3 * ((track - 1) / 50), 6 + (5 * ((track - 1) % 50))]
|
|
45
|
+
def bam_bitmap(track) = [38, 3 * ((track - 1) / 50), 7 + (5 * ((track - 1) % 50))]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# An 8250 disk: an 8050's 77 tracks on each side, tracks 78-154 on the
|
|
6
|
+
# second, one directory and one BAM for both.
|
|
7
|
+
class D82Image < D80Image
|
|
8
|
+
ERROR_TABLES = { 1_070_662 => 4166 }.freeze
|
|
9
|
+
|
|
10
|
+
def sectors_in(track)
|
|
11
|
+
track > SIDE_TRACKS ? super(track - SIDE_TRACKS) : super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def error_tables = ERROR_TABLES
|
|
17
|
+
|
|
18
|
+
def bam_tracks = 1..(2 * SIDE_TRACKS)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/badline/storage/tap.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Storage
|
|
5
5
|
# Raw datasette pulse stream. The header holds the signature, the
|
|
6
|
-
# version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
|
|
6
|
+
# version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16, 3 PET) and the
|
|
7
7
|
# data size at $10; the data runs from $14, every byte the time to the
|
|
8
8
|
# next falling edge on the tape read line, in units of 8 cycles. A
|
|
9
9
|
# version 2 tape, the C16's and the Plus/4's, times half waves instead,
|
|
@@ -16,18 +16,31 @@ module Badline
|
|
|
16
16
|
HEADER_SIZE = 0x14
|
|
17
17
|
OVERFLOW = 0x100 * 8
|
|
18
18
|
|
|
19
|
+
# The platform byte of a PET tape, a version 1 tape timed in PET
|
|
20
|
+
# cycles.
|
|
21
|
+
PET_PLATFORM = 3
|
|
22
|
+
|
|
19
23
|
# The file it was read from, expanded, and the file's bytes.
|
|
20
24
|
attr_reader :version, :platform, :path, :bytes
|
|
21
25
|
|
|
22
26
|
# A blank tape for `platform` at `path`, written to the host file: a
|
|
23
|
-
# version 1 tape for the C64 (0)
|
|
24
|
-
# C16 tape for the 264 series (2).
|
|
27
|
+
# version 1 tape for the C64 (0), the VIC-20 (1) or the PET (3), and
|
|
28
|
+
# a version 2 C16 tape for the 264 series (2).
|
|
25
29
|
def self.create(path, platform:)
|
|
26
30
|
header = platform == 2 ? C16_SIGNATURE + [2] : SIGNATURE + [1]
|
|
27
31
|
File.binwrite(path, (header + [platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
|
|
28
32
|
new(path)
|
|
29
33
|
end
|
|
30
34
|
|
|
35
|
+
# The platform byte of the tape at `path`, or nil when it isn't a
|
|
36
|
+
# TAP file, read from its header alone.
|
|
37
|
+
def self.platform_of(path)
|
|
38
|
+
header = File.binread(path, HEADER_SIZE)&.bytes || []
|
|
39
|
+
return unless [SIGNATURE, C16_SIGNATURE].include?(header[0, SIGNATURE.length]) && header.length > 0x0d
|
|
40
|
+
|
|
41
|
+
header[0x0d]
|
|
42
|
+
end
|
|
43
|
+
|
|
31
44
|
# `bytes` stands in for the host file's contents, as a snapshot
|
|
32
45
|
# restores the tape it holds.
|
|
33
46
|
def initialize(path, bytes: nil)
|
data/lib/badline/storage.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "badline/storage/disk_image"
|
|
|
7
7
|
require "badline/storage/d64_image"
|
|
8
8
|
require "badline/storage/d71_image"
|
|
9
9
|
require "badline/storage/d81_image"
|
|
10
|
+
require "badline/storage/d80_image"
|
|
11
|
+
require "badline/storage/d82_image"
|
|
10
12
|
require "badline/storage/g64_image"
|
|
11
13
|
require "badline/storage/t64"
|
|
12
14
|
require "badline/storage/tap"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Support
|
|
5
|
+
# A Checkpoint of a PET: its CPU, the RAM below $8000, the screen RAM,
|
|
6
|
+
# the picture the video paints, both PIAs, the VIA, the CRTC's
|
|
7
|
+
# registers and counters, and the 8096's control register and 64K
|
|
8
|
+
# board, or nothing on another model.
|
|
9
|
+
module PETCheckpoint
|
|
10
|
+
COMPONENTS = %w[cpu ram screen display pia1 pia2 via crtc expansion].freeze
|
|
11
|
+
|
|
12
|
+
def self.take(machine)
|
|
13
|
+
ram = machine.ram
|
|
14
|
+
Checkpoint.new(machine.cycles, [
|
|
15
|
+
Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu)),
|
|
16
|
+
Checkpoint.fnv1a(Checkpoint.ram(ram, ram.length)),
|
|
17
|
+
Checkpoint.fnv1a(machine.bus.screen.cells),
|
|
18
|
+
Checkpoint.fnv1a(machine.video.display),
|
|
19
|
+
Checkpoint.fnv1a(pia_state(machine.pia1)),
|
|
20
|
+
Checkpoint.fnv1a(pia_state(machine.pia2)),
|
|
21
|
+
Checkpoint.fnv1a(Vic20Checkpoint.via_state(machine.via)),
|
|
22
|
+
Checkpoint.fnv1a(crtc_state(machine.crtc)),
|
|
23
|
+
Checkpoint.fnv1a(expansion_state(machine.bus.expansion))
|
|
24
|
+
], COMPONENTS)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
|
|
28
|
+
|
|
29
|
+
def self.pia_state(pia) = [pia.port_a_output, pia.port_b_output, pia.cra, pia.crb]
|
|
30
|
+
|
|
31
|
+
def self.crtc_state(crtc) = crtc.registers + [crtc.column, crtc.line, crtc.row]
|
|
32
|
+
|
|
33
|
+
def self.expansion_state(expansion)
|
|
34
|
+
expansion ? [expansion.control, *expansion.cells] : []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|