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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +102 -12
  5. data/doc/media.md +5 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +376 -69
  8. data/doc/snapshots.md +148 -14
  9. data/lib/badline/cbm2/acia.rb +10 -0
  10. data/lib/badline/cbm2/bus.rb +263 -0
  11. data/lib/badline/cbm2/cpu.rb +58 -0
  12. data/lib/badline/cbm2/crtc_video.rb +133 -0
  13. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  14. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  15. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  16. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  17. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  18. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  19. data/lib/badline/cbm2/keyed_state.rb +91 -0
  20. data/lib/badline/cbm2/model.rb +44 -0
  21. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  22. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  23. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  24. data/lib/badline/cbm2/p500/model.rb +39 -0
  25. data/lib/badline/cbm2/p500/pages.rb +57 -0
  26. data/lib/badline/cbm2/p500.rb +262 -0
  27. data/lib/badline/cbm2/sid_page.rb +19 -0
  28. data/lib/badline/cbm2.rb +267 -0
  29. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  30. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  31. data/lib/badline/chips/acia.rb +307 -0
  32. data/lib/badline/chips/crtc.rb +3 -2
  33. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  34. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  35. data/lib/badline/chips/riot/timer.rb +80 -0
  36. data/lib/badline/chips/riot.rb +162 -0
  37. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  38. data/lib/badline/chips/rriot.rb +132 -0
  39. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  40. data/lib/badline/chips/tpi.rb +233 -0
  41. data/lib/badline/chips/vic/bank.rb +60 -5
  42. data/lib/badline/chips/vic/sprite.rb +4 -3
  43. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  44. data/lib/badline/drives/datasette.rb +27 -3
  45. data/lib/badline/drives/model8050/disk.rb +163 -0
  46. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  47. data/lib/badline/drives/model8050/idle.rb +177 -0
  48. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  49. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  50. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  51. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  52. data/lib/badline/drives/model8050/parts.rb +37 -0
  53. data/lib/badline/drives/model8050.rb +243 -0
  54. data/lib/badline/frontend/app.rb +10 -3
  55. data/lib/badline/frontend/boot.rb +1 -1
  56. data/lib/badline/frontend/controls.rb +51 -15
  57. data/lib/badline/frontend/gamepads.rb +3 -3
  58. data/lib/badline/frontend/keys.rb +61 -2
  59. data/lib/badline/frontend/media_slots.rb +1 -1
  60. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  61. data/lib/badline/frontend/menu/menu_pages.rb +18 -4
  62. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  63. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  64. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  65. data/lib/badline/frontend/timeline.rb +19 -10
  66. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  67. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  68. data/lib/badline/kernal_trap/dos.rb +18 -5
  69. data/lib/badline/kernal_trap/ieee.rb +4 -2
  70. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  71. data/lib/badline/machine/ieee_drives.rb +194 -0
  72. data/lib/badline/machine/settling.rb +4 -0
  73. data/lib/badline/machine.rb +23 -6
  74. data/lib/badline/media/cbm2_media.rb +29 -0
  75. data/lib/badline/media/program.rb +6 -3
  76. data/lib/badline/media/true_drive.rb +38 -9
  77. data/lib/badline/media.rb +15 -6
  78. data/lib/badline/options/family.rb +15 -3
  79. data/lib/badline/options/help.rb +16 -2
  80. data/lib/badline/options/table.rb +18 -4
  81. data/lib/badline/options/validation.rb +27 -3
  82. data/lib/badline/options.rb +21 -7
  83. data/lib/badline/pet/cassettes.rb +22 -1
  84. data/lib/badline/pet/ieee_drive.rb +5 -64
  85. data/lib/badline/pet/ieee_ports.rb +7 -0
  86. data/lib/badline/pet.rb +15 -4
  87. data/lib/badline/plus4/acia.rb +4 -298
  88. data/lib/badline/plus4/keyed_parts.rb +0 -44
  89. data/lib/badline/plus4.rb +1 -0
  90. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  91. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  92. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  93. data/lib/badline/roms/README +111 -0
  94. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  95. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  96. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  97. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  98. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  99. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  100. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  101. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  102. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  103. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  104. data/lib/badline/snapshot/container.rb +7 -2
  105. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  106. data/lib/badline/snapshot/field_doc.rb +38 -3
  107. data/lib/badline/snapshot/image.rb +62 -9
  108. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  109. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  110. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  111. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  112. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  113. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  114. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  115. data/lib/badline/snapshot/vice/cias.rb +13 -9
  116. data/lib/badline/snapshot/vice/p500.rb +96 -0
  117. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  118. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  119. data/lib/badline/snapshot/vice/pet.rb +7 -3
  120. data/lib/badline/snapshot/vice/pet_cpu.rb +5 -2
  121. data/lib/badline/snapshot/vice/pet_crtc.rb +10 -15
  122. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  123. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  124. data/lib/badline/snapshot/vice/pet_restore.rb +70 -6
  125. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  126. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  127. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  128. data/lib/badline/snapshot/vice.rb +3 -0
  129. data/lib/badline/snapshot.rb +11 -3
  130. data/lib/badline/storage/crt_file.rb +7 -1
  131. data/lib/badline/storage/d64_image.rb +7 -0
  132. data/lib/badline/storage/d80_image.rb +17 -1
  133. data/lib/badline/storage/d81_image.rb +14 -0
  134. data/lib/badline/storage/disk_image/bam.rb +18 -3
  135. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  136. data/lib/badline/storage/disk_image/writing.rb +35 -0
  137. data/lib/badline/storage/disk_image.rb +2 -0
  138. data/lib/badline/storage.rb +1 -0
  139. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  140. data/lib/badline/support/p500_checkpoint.rb +35 -0
  141. data/lib/badline/version.rb +1 -1
  142. data/lib/badline.rb +1 -0
  143. 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
- computer.save_modules.each { |section| sections << section if KEPT.include?(section.name) }
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(NAME, MAJOR, MINOR)
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
- registers.first(0x19).each_with_index { |value, reg| computer.sid.poke(0xd400 + reg, value) }
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
- vic = computer.vic
18
- computer.address_bus.color_ram.tap do |ram|
19
- @color_ram.each_with_index { |value, offset| ram.poke(0xd800 + offset, value) }
20
- end
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
@@ -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 PET's snapshot is in a container named as xvic, x128,
40
- # xplus4 or xpet names its machine.
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 PET each include it.
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 :plus4, or nil for any.
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 = 6
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 goes the interleave further round
75
- # the same track, or on the next track out once that one is full.
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, candidate == track ? sector + interleave : 0)
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
@@ -24,6 +24,7 @@ module Badline
24
24
  WRITE_PROTECT_ON = 26
25
25
  FILE_NOT_FOUND = 62
26
26
  FILE_EXISTS = 63
27
+ FILE_TYPE_MISMATCH = 64
27
28
  DISK_FULL = 72
28
29
 
29
30
  attr_reader :code
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.16.1"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/badline.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "badline/core"
4
4
  require "badline/c128"
5
5
  require "badline/plus4"
6
+ require "badline/cbm2"
6
7
  require "badline/options"
7
8
  require "badline/data_path"
8
9
  require "badline/snapshot"