badline 0.16.0 → 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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +130 -17
  5. data/doc/media.md +14 -2
  6. data/doc/pinned-behaviour.md +61 -0
  7. data/doc/snapshot-format.md +379 -70
  8. data/doc/snapshots.md +185 -10
  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/ieee_bus.rb +50 -4
  46. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  47. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  48. data/lib/badline/drives/model2031/parts.rb +25 -0
  49. data/lib/badline/drives/model2031.rb +92 -0
  50. data/lib/badline/drives/model8050/disk.rb +163 -0
  51. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  52. data/lib/badline/drives/model8050/idle.rb +177 -0
  53. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  54. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  55. data/lib/badline/drives/model8050/keyed_state.rb +160 -0
  56. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  57. data/lib/badline/drives/model8050/parts.rb +37 -0
  58. data/lib/badline/drives/model8050.rb +243 -0
  59. data/lib/badline/frontend/app.rb +10 -3
  60. data/lib/badline/frontend/boot.rb +1 -1
  61. data/lib/badline/frontend/controls.rb +51 -15
  62. data/lib/badline/frontend/gamepads.rb +3 -3
  63. data/lib/badline/frontend/keys.rb +61 -2
  64. data/lib/badline/frontend/media_slots.rb +1 -1
  65. data/lib/badline/frontend/menu/menu_media.rb +14 -11
  66. data/lib/badline/frontend/menu/menu_pages.rb +29 -6
  67. data/lib/badline/frontend/menu/menu_ports.rb +40 -4
  68. data/lib/badline/frontend/menu/pause_menu.rb +1 -1
  69. data/lib/badline/frontend/pet_keyboard.rb +23 -7
  70. data/lib/badline/frontend/timeline.rb +19 -10
  71. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  72. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  73. data/lib/badline/kernal_trap/dos.rb +18 -5
  74. data/lib/badline/kernal_trap/ieee.rb +7 -2
  75. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  76. data/lib/badline/machine/ieee_drives.rb +194 -0
  77. data/lib/badline/machine/settling.rb +4 -0
  78. data/lib/badline/machine.rb +26 -8
  79. data/lib/badline/media/cbm2_media.rb +29 -0
  80. data/lib/badline/media/pet_media.rb +5 -5
  81. data/lib/badline/media/program.rb +6 -3
  82. data/lib/badline/media/true_drive.rb +50 -8
  83. data/lib/badline/media.rb +21 -8
  84. data/lib/badline/options/family.rb +15 -3
  85. data/lib/badline/options/help.rb +24 -6
  86. data/lib/badline/options/table.rb +25 -5
  87. data/lib/badline/options/validation.rb +47 -7
  88. data/lib/badline/options.rb +48 -15
  89. data/lib/badline/pet/bus/expansion.rb +99 -5
  90. data/lib/badline/pet/bus.rb +30 -12
  91. data/lib/badline/pet/cassettes.rb +22 -1
  92. data/lib/badline/pet/characters.rb +8 -2
  93. data/lib/badline/pet/crtc_video.rb +20 -7
  94. data/lib/badline/pet/ieee_drive.rb +17 -0
  95. data/lib/badline/pet/ieee_ports.rb +19 -1
  96. data/lib/badline/pet/ieee_traps.rb +10 -5
  97. data/lib/badline/pet/keyed_modules.rb +4 -1
  98. data/lib/badline/pet/keyed_parts.rb +10 -6
  99. data/lib/badline/pet/model.rb +56 -6
  100. data/lib/badline/pet/rom_sets.rb +30 -6
  101. data/lib/badline/pet.rb +45 -16
  102. data/lib/badline/plus4/acia.rb +4 -298
  103. data/lib/badline/plus4/keyed_parts.rb +0 -44
  104. data/lib/badline/plus4.rb +1 -0
  105. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  106. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  107. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  108. data/lib/badline/roms/README +144 -1
  109. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  110. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  111. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  112. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  113. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  114. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  115. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  116. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  117. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  118. data/lib/badline/roms/dos2031.rom +0 -0
  119. data/lib/badline/roms/pet/characters-din.rom +0 -0
  120. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  121. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  122. data/lib/badline/snapshot/container.rb +9 -1
  123. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  124. data/lib/badline/snapshot/field_doc.rb +39 -3
  125. data/lib/badline/snapshot/image.rb +79 -15
  126. data/lib/badline/snapshot/pet_setup.rb +30 -13
  127. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  128. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  129. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  130. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  131. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  132. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  133. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  134. data/lib/badline/snapshot/vice/cias.rb +13 -9
  135. data/lib/badline/snapshot/vice/p500.rb +96 -0
  136. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  137. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  138. data/lib/badline/snapshot/vice/pet.rb +121 -0
  139. data/lib/badline/snapshot/vice/pet_cpu.rb +70 -0
  140. data/lib/badline/snapshot/vice/pet_crtc.rb +241 -0
  141. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  142. data/lib/badline/snapshot/vice/pet_drive8050.rb +216 -0
  143. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  144. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  145. data/lib/badline/snapshot/vice/pet_restore.rb +214 -0
  146. data/lib/badline/snapshot/vice/pet_via.rb +165 -0
  147. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  148. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  149. data/lib/badline/snapshot/vice.rb +4 -0
  150. data/lib/badline/snapshot.rb +11 -3
  151. data/lib/badline/storage/crt_file.rb +7 -1
  152. data/lib/badline/storage/d64_image.rb +7 -0
  153. data/lib/badline/storage/d80_image.rb +17 -1
  154. data/lib/badline/storage/d81_image.rb +14 -0
  155. data/lib/badline/storage/disk_image/bam.rb +18 -3
  156. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  157. data/lib/badline/storage/disk_image/writing.rb +35 -0
  158. data/lib/badline/storage/disk_image.rb +2 -0
  159. data/lib/badline/storage.rb +1 -0
  160. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  161. data/lib/badline/support/p500_checkpoint.rb +35 -0
  162. data/lib/badline/version.rb +1 -1
  163. data/lib/badline.rb +1 -0
  164. metadata +87 -2
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/drives/model2031"
4
+ require "badline/drives/model8050"
5
+
6
+ module Badline
7
+ module Machine
8
+ # A machine's true drive on its IEEE-488 bus: a Drives::Model2031,
9
+ # Model8050 or Model8250, one at a time, clocked after the CPU each
10
+ # cycle at the machine's clock (ieee_host_clock_hz) and reset with it,
11
+ # as the machine's RES line reaches IFC. The traps on the KERNAL's
12
+ # primitives leave device 8 to the drive while it is device 8
13
+ # (trap_device). The PET (PET::IEEEDrive) and the CBM-II
14
+ # (CBM2::IEEEDrive) include it.
15
+ #
16
+ # The machine's @ieee is its side of the bus: #bus, the
17
+ # Drives::IEEEBus, #push_lines, which shows the drives the lines the
18
+ # machine pulls now, and #cycle_lines, which does so once a cycle if
19
+ # the machine doesn't push them as its chips are written.
20
+ module IEEEDrives
21
+ # The 2031 on the bus, or nil.
22
+ attr_reader :drive2031
23
+
24
+ # The 8050 or 8250 on the bus, or nil.
25
+ attr_reader :drive8050
26
+
27
+ # Plugs in a Drives::Model2031, in place of the drive there.
28
+ def attach_drive2031(drive)
29
+ detach_true_drives
30
+ connect_drive(drive)
31
+ @drive2031 = drive
32
+ @ieee_trap&.device = trap_device
33
+ @init_threshold = [@init_threshold, Badline::Drives::Model2031::BOOT_CYCLES].max if @cycles < @init_threshold
34
+ end
35
+
36
+ # Unplugs the 2031, leaving it on a bus of its own.
37
+ def detach_drive2031
38
+ drive = @drive2031
39
+ return unless drive
40
+
41
+ drive.connect(Badline::Drives::IEEEBus.new)
42
+ @drive2031 = nil
43
+ @ieee_trap&.device = trap_device
44
+ end
45
+
46
+ # Plugs in a Drives::Model8050 or Model8250, in place of the drive
47
+ # there.
48
+ def attach_drive8050(drive)
49
+ detach_true_drives
50
+ connect_drive(drive)
51
+ @drive8050 = drive
52
+ @ieee_trap&.device = trap_device
53
+ @init_threshold = [@init_threshold, Badline::Drives::Model8050::BOOT_CYCLES].max if @cycles < @init_threshold
54
+ end
55
+
56
+ # Unplugs the 8050 or 8250, leaving it on a bus of its own.
57
+ def detach_drive8050
58
+ drive = @drive8050
59
+ return unless drive
60
+
61
+ drive.connect(Badline::Drives::IEEEBus.new)
62
+ @drive8050 = nil
63
+ @ieee_trap&.device = trap_device
64
+ end
65
+
66
+ # Plugs in an 8050, or an 8250 for +model+ "8250", as device 8, in
67
+ # place of the drive there, and returns it.
68
+ def plug_drive8050(model = "8050")
69
+ drive = model == "8250" ? Badline::Drives::Model8250.new : Badline::Drives::Model8050.new
70
+ drive.tap { |unit| attach_drive8050(unit) }
71
+ end
72
+
73
+ # The true drive on the bus, or nil.
74
+ def true_drive = @drive8050 || @drive2031
75
+
76
+ # Plugs in a 2031 as device 8, in place of the drive there, and
77
+ # returns it.
78
+ def plug_true_drive
79
+ Badline::Drives::Model2031.new.tap { |drive| attach_drive2031(drive) }
80
+ end
81
+
82
+ # Settles for a snapshot, then runs on until a 2031's CPU, or an
83
+ # 8050's interface processor with its controller at the start of its
84
+ # idle loop, is between instructions with the machine's, as VICE
85
+ # saves them, or for so many cycles, then on to the machine's next
86
+ # instruction. It stands in for Machine::Settling's, which the
87
+ # machines include first.
88
+ def settle_for_vice!
89
+ ran = settle_for_snapshot!
90
+ return ran if @drive2031.nil? && @drive8050.nil?
91
+
92
+ until ran >= Machine::Settling::LIMIT || (drive_settled_for_vice? && settled_for_snapshot?)
93
+ cycle!
94
+ ran += 1
95
+ end
96
+ ran + settle_for_snapshot!
97
+ end
98
+
99
+ # Whether the true drive is where VICE saves it (#settle_for_vice!).
100
+ def drive_settled_for_vice?
101
+ return @drive2031.cpu.boundary? if @drive2031
102
+
103
+ fdc = @drive8050.fdc_cpu
104
+ @drive8050.ip_cpu.boundary? && fdc.boundary? && fdc.program_counter == Badline::Drives::Model8050::Idle::FDC_LOOP
105
+ end
106
+
107
+ private
108
+
109
+ def init_ieee_drives
110
+ @drive2031 = nil
111
+ @drive8050 = nil
112
+ end
113
+
114
+ def detach_true_drives
115
+ detach_drive2031
116
+ detach_drive8050
117
+ end
118
+
119
+ def connect_drive(drive)
120
+ drive.host_clock_hz = ieee_host_clock_hz
121
+ drive.connect(@ieee.bus)
122
+ @ieee.push_lines
123
+ end
124
+
125
+ # The true drive's cycles for one of the machine's, after the lines
126
+ # it pulls.
127
+ def clock_ieee_drives
128
+ drive2031 = @drive2031
129
+ drive8050 = @drive8050
130
+ return unless drive2031 || drive8050
131
+
132
+ @ieee.cycle_lines
133
+ drive2031&.host_cycle!
134
+ drive8050&.host_cycle!
135
+ end
136
+
137
+ # The RES line reaches the true drive as IFC.
138
+ def reset_ieee_drives
139
+ @drive2031&.reset!
140
+ @drive8050&.reset!
141
+ end
142
+
143
+ # The true drive as keyed snapshot modules named from its device
144
+ # number: BL-D8, BL-D8-CPU and so on for device 8.
145
+ def save_drive_modules(sections)
146
+ drive2031 = @drive2031
147
+ drive2031&.save_modules(sections, "BL-D#{drive2031.device}")
148
+ drive8050 = @drive8050
149
+ drive8050&.save_modules(sections, "BL-D#{drive8050.device}")
150
+ sections
151
+ end
152
+
153
+ # The true drive a snapshot's modules hold, a 2031, an 8050 or an
154
+ # 8250 by its model, built afresh unless the same model is on the bus
155
+ # as the same device, or none when they hold no drive. The bus then
156
+ # carries the machine's lines as its chips left them.
157
+ def load_drive_modules(modules)
158
+ device = 8
159
+ input = modules.optional("BL-D8")
160
+ while input.nil? && device < 15
161
+ device += 1
162
+ input = modules.optional("BL-D#{device}")
163
+ end
164
+ model = input&.text("model", "2031")
165
+ if %w[8050 8250].include?(model)
166
+ load_drive8050_modules(modules, input, model, device)
167
+ elsif input
168
+ load_drive2031_modules(modules, input, device)
169
+ else
170
+ detach_true_drives
171
+ end
172
+ @ieee.push_lines
173
+ end
174
+
175
+ def load_drive2031_modules(modules, input, device)
176
+ detach_drive8050
177
+ detach_drive2031 if @drive2031 && @drive2031.device != device
178
+ attach_drive2031(Badline::Drives::Model2031.new(device:)) unless @drive2031
179
+ @drive2031.load_modules(modules, input, "BL-D#{device}")
180
+ end
181
+
182
+ def load_drive8050_modules(modules, input, model, device)
183
+ detach_drive2031
184
+ drive = @drive8050
185
+ detach_drive8050 if drive && (drive.device != device || drive.model_name != model)
186
+ unless @drive8050
187
+ unit = model == "8250" ? Badline::Drives::Model8250.new(device:) : Badline::Drives::Model8050.new(device:)
188
+ attach_drive8050(unit)
189
+ end
190
+ @drive8050.load_modules(modules, input, "BL-D#{device}")
191
+ end
192
+ end
193
+ end
194
+ end
@@ -24,6 +24,10 @@ module Badline
24
24
  settle_chips_for_snapshot
25
25
  ran
26
26
  end
27
+
28
+ # Settles as #settle_for_snapshot! does, for a snapshot VICE opens,
29
+ # which a machine with a drive VICE runs settles further.
30
+ def settle_for_vice! = settle_for_snapshot!
27
31
  end
28
32
  end
29
33
  end
@@ -69,20 +69,24 @@ module Badline
69
69
  # Media the machine named doesn't take.
70
70
  class Unsupported < ArgumentError; end
71
71
 
72
- # A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
73
- # model of it `model` names. A C64's model is one of Model::ALL, with its SID
74
- # `sid_model` when not nil and an REU of `reu` K when not nil. A
75
- # VIC-20's is "pal", with the RAM expansion `ram` names
72
+ # A new machine of `family`, :c64, :vic20, :c128, :plus4, :pet or :cbm2, the
73
+ # model of it `model` names. A C64's model is one of Model::ALL, with
74
+ # its SID `sid_model` when not nil and an REU of `reu` K when not nil.
75
+ # A VIC-20's is "pal", with the RAM expansion `ram` names
76
76
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
77
77
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
78
78
  # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
79
79
  # the model's own RAM. A PET's is one of PET::Model::ALL, with its own
80
- # RAM and ROMs, its CRTC's editor the 50 Hz one.
80
+ # RAM and ROMs, its CRTC's editor the 50 Hz one. A CBM-II's is one of
81
+ # CBM2::Model::ALL, with VICE's KERNAL, 901244-04a, at 50 Hz, or a 510,
82
+ # one of CBM2::P500::Model::ALL, with its region's mains.
81
83
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
82
84
  return vic20(model, ram) if family == :vic20
83
85
  return c128(model, sid_model, :c64) if family == :c128
84
86
  return Plus4.new(model:) if family == :plus4
85
87
  return PET.new(model:) if family == :pet
88
+ return CBM2::P500.new(model:) if family == :cbm2 && model.start_with?("510")
89
+ return CBM2.new(model:) if family == :cbm2
86
90
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
87
91
 
88
92
  profile = Model.named(model)
@@ -98,12 +102,14 @@ module Badline
98
102
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
99
103
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
100
104
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
101
- # Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
105
+ # Unsupported for media a VIC-20, a Plus/4, a PET or a CBM-II doesn't
106
+ # take.
102
107
  def self.for_options(options, media, &)
103
108
  family = options.family
104
109
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
105
110
  return plus4_for(media, options) if family == :plus4
106
111
  return pet_for(media, options) if family == :pet
112
+ return cbm2_for(media, options) if family == :cbm2
107
113
 
108
114
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
109
115
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -153,16 +159,28 @@ module Badline
153
159
 
154
160
  # A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
155
161
  # model's own without it, the editor at the frame rate --60hz or
156
- # --editor asks for, and the ROMs --basic and --editor fit it with.
162
+ # --editor asks for, the ROMs --basic, --editor and --characters fit it
163
+ # with, and the 8296 board's jumpers --jumpers names.
157
164
  def self.pet_for(media, options)
158
165
  unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
159
166
  raise Unsupported, "doesn't go in a PET" if unsupported
160
167
 
161
168
  PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
162
- editor: options.editor)
169
+ editor: options.editor, characters: options.characters, jumpers: options.jumpers)
163
170
  end
164
171
  private_class_method :pet_for
165
172
 
173
+ # A CBM-II with the KERNAL --kernal names, on the mains --60hz asks
174
+ # for: a B series' 50 Hz without it, and a P500's its region's.
175
+ def self.cbm2_for(media, options)
176
+ unsupported = !media.nil? && Media::CBM2Media::UNSUPPORTED.include?(File.extname(media).downcase)
177
+ raise Unsupported, "doesn't go in a CBM-II" if unsupported
178
+ return CBM2::P500.new(model: options.model, mains_hz: options.mains_hz) if options.p500?
179
+
180
+ CBM2.new(model: options.model, kernal: options.kernal, mains_hz: options.mains_hz || 50)
181
+ end
182
+ private_class_method :cbm2_for
183
+
166
184
  def self.c64_media?(media)
167
185
  return false if media.nil? || !File.file?(media)
168
186
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What a CBM-II does with media of its own (Media.attach). Disks of
6
+ # every kind, .d80 and .d82 among them, directories and .t64 archives
7
+ # mount on device 8 through the traps on the KERNAL's IEEE-488
8
+ # primitives (CBM2::IEEETraps), programs load into BASIC's text segment
9
+ # (Program), and a CBM2 .crt's ROMs go in at their load addresses in
10
+ # segment 15. There is no datasette, the SID plays no .sid tunes, and
11
+ # the true drives are serial ones.
12
+ module CBM2Media
13
+ # What a CBM-II doesn't take.
14
+ UNSUPPORTED = Extensions.of(%i[tune tape gcr])
15
+
16
+ # What loads and runs the first program on the disk in device 8.
17
+ AUTOSTART = %(load"*",8\rrun\r)
18
+
19
+ # Where BASIC's text starts in segment 1, and the pointers to its end
20
+ # and to the KERNAL's end address (EAL) in segment 15.
21
+ BASIC_START = 0x0003
22
+ TEXT_END = 0x2f
23
+ EAL = 0x96
24
+
25
+ # The ROM chips of a CBM2 .crt.
26
+ def self.cartridge_chips(path) = Storage::CRTFile.new(path, machine: :cbm2).chips
27
+ end
28
+ end
29
+ end
@@ -10,7 +10,7 @@ module Badline
10
10
  # is no cartridge port and no SID, and the true drives are serial ones.
11
11
  module PETMedia
12
12
  # What a PET doesn't take.
13
- UNSUPPORTED = Extensions.of(%i[tune cartridge gcr])
13
+ UNSUPPORTED = Extensions.of(%i[tune cartridge])
14
14
 
15
15
  # What loads and runs the first program on the disk in device 8.
16
16
  AUTOSTART = %(load"*",8\rrun\r)
@@ -26,12 +26,12 @@ module Badline
26
26
 
27
27
  # BASIC 2 and 4 load the first program off the disk and run it.
28
28
  # BASIC 1's LOAD finds no device on the IEEE-488 bus, so the first
29
- # program goes in as a program file does, once the KERNAL has
30
- # booted.
31
- def disk_autostart(machine, storage)
29
+ # program, which the block gives, goes in as a program file does,
30
+ # once the KERNAL has booted.
31
+ def disk_autostart(machine)
32
32
  return machine.type_text(AUTOSTART) unless machine.model.basic1?
33
33
 
34
- data = storage.read_file("*")
34
+ data = yield
35
35
  machine.on_init { Program.load(machine, data, autostart: true) } if data
36
36
  end
37
37
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # A program loading into a C64, a C128, a 264 series machine or a PET
6
- # once it has booted (Media.attach). The VIC-20 has its own
5
+ # A program loading into a C64, a C128, a 264 series machine, a PET or
6
+ # a CBM-II once it has booted (Media.attach). The VIC-20 has its own
7
7
  # (Vic20Basic).
8
8
  module Program
9
9
  # Where BASIC 3.5 starts on the 264 series.
@@ -35,10 +35,13 @@ module Badline
35
35
  # KERNAL's end address (EAL). BASIC 7.0 starts at $1C01 and keeps
36
36
  # the end of its text at $1210, and the 264 KERNAL keeps EAL at $9D.
37
37
  # PET BASIC 2 and 4 keep the end of their text at $2A and EAL at
38
- # $C9, and BASIC 1 at $7C and $E5.
38
+ # $C9, and BASIC 1 at $7C and $E5. The CBM-II's BASIC starts at
39
+ # $0003 of segment 1 and keeps the end of its text at $2F, and its
40
+ # KERNAL EAL at $96, in segment 15.
39
41
  def basic(computer)
40
42
  return [PLUS4_BASIC_START, 0x2d, KernalTrap::PLUS4_ZERO_PAGE.end_address] if computer.family == :plus4
41
43
  return pet_basic(computer) if computer.family == :pet
44
+ return [CBM2Media::BASIC_START, CBM2Media::TEXT_END, CBM2Media::EAL] if computer.family == :cbm2
42
45
  return [0x1c01, 0x1210, 0xae] if computer.family == :c128 && computer.mode == :c128
43
46
 
44
47
  [BASIC_START, 0x2d, 0xae]
@@ -4,18 +4,23 @@ module Badline
4
4
  module Media
5
5
  # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
6
  # the C128 its 1571, or on the Plus/4 a 1551 if it asks for one, or a
7
- # 1581. Once a true drive is plugged in as device 8, disk media go into
7
+ # 1581, or on the PET or the CBM-II a 2031 on the IEEE-488 bus, or for
8
+ # a .d80 or .d82 an 8050 or 8250. Once a true drive is plugged in as device 8, disk media go into
8
9
  # it rather than into the traps, so a LOAD runs the DOS and every byte
9
10
  # crosses the serial bus or TCBM. A .g64 holds the
10
11
  # disk's raw GCR, which only a true drive reads, so it plugs one in
11
12
  # when there's none yet. The 1541 reads only .d64 and .g64 images, the
12
13
  # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
13
14
  # the drive on device 8 for a 1581 (Drives::Model1581::Slot), and another disk
14
- # image swaps a 1581 back for the machine's own drive. The traps' other
15
- # disk media, a .t64 or a host directory, raise Error.
15
+ # image swaps a 1581 back for the machine's own drive. The PET and the
16
+ # CBM-II have no 1581, and their 2031 reads .d64 and .g64 images only.
17
+ # The traps' other disk media, a .t64 or a host directory, raise Error.
16
18
  module TrueDrive
17
19
  class Error < ArgumentError; end
18
20
 
21
+ # The families whose drives sit on the IEEE-488 bus.
22
+ IEEE_FAMILIES = %i[pet cbm2].freeze
23
+
19
24
  class << self
20
25
  # Plugs the machine's true drive in as device 8, unless one is there
21
26
  # already, and returns it.
@@ -36,7 +41,7 @@ module Badline
36
41
  kind = Extensions.kind(path)
37
42
  return true if kind == :gcr
38
43
 
39
- !drive(computer).nil? && (File.directory?(path) || %i[disk archive].include?(kind))
44
+ !drive(computer).nil? && (File.directory?(path) || %i[disk archive ieee_disk].include?(kind))
40
45
  end
41
46
 
42
47
  # Puts the image in the drive on device 8, plugging one in that
@@ -44,12 +49,16 @@ module Badline
44
49
  # mounted through the traps, so LOAD and SAVE reach the drive too.
45
50
  # `read_only` puts it in write-protected.
46
51
  def insert(computer, path, read_only: false)
47
- return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
52
+ swapped = insert_swapping(computer, path, read_only)
53
+ return swapped if swapped
48
54
 
55
+ pet = IEEE_FAMILIES.include?(computer.family)
49
56
  drive = own_drive(computer)
50
- name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
57
+ name = drive&.model_name || default_model(computer)
51
58
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
52
59
  unless types.include?(File.extname(path).downcase)
60
+ raise Error, "#{path} is not a #{types.join(' or ')} image, which the #{name} reads" if pet
61
+
53
62
  raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
54
63
  end
55
64
 
@@ -61,10 +70,43 @@ module Badline
61
70
 
62
71
  private
63
72
 
64
- # The drive on device 8 unless it's a 1581.
73
+ # The model plug_true_drive plugs in.
74
+ def default_model(computer)
75
+ case computer.family
76
+ when :c128 then "1571"
77
+ when :pet, :cbm2 then "2031"
78
+ else "1541"
79
+ end
80
+ end
81
+
82
+ # The drive on device 8 unless it's a 1581, or an 8050 or 8250,
83
+ # which a .d64 swaps back out.
65
84
  def own_drive(computer)
66
85
  drive = drive(computer)
67
- drive unless drive&.model_name == "1581"
86
+ drive unless %w[1581 8050 8250].include?(drive&.model_name)
87
+ end
88
+
89
+ # A .d81 in a 1581, and on the PET or the CBM-II a .d80 or .d82 in
90
+ # an 8050 or 8250, each swapped in for the drive there, or nil for
91
+ # the rest.
92
+ def insert_swapping(computer, path, read_only)
93
+ extension = File.extname(path).downcase
94
+ if IEEE_FAMILIES.include?(computer.family)
95
+ insert_d80(computer, path, read_only) if %w[.d80 .d82].include?(extension)
96
+ elsif extension == ".d81"
97
+ insert_d81(computer, path, read_only)
98
+ end
99
+ end
100
+
101
+ # An 8050 for a .d80 and an 8250 for a .d82, swapped in for the
102
+ # drive on device 8 unless it's that model already.
103
+ def insert_d80(computer, path, read_only)
104
+ model = File.extname(path).casecmp?(".d82") ? "8250" : "8050"
105
+ drive = drive(computer)
106
+ drive = computer.plug_drive8050(model) unless drive&.model_name == model
107
+ drive.insert_image(path, read_only:)
108
+ computer.unmount
109
+ "Inserted #{path} in the #{model} as device 8"
68
110
  end
69
111
 
70
112
  def insert_d81(computer, path, read_only)
data/lib/badline/media.rb CHANGED
@@ -12,6 +12,7 @@ require "badline/media/vic20_cartridge"
12
12
  require "badline/media/vic20_media"
13
13
  require "badline/media/plus4_media"
14
14
  require "badline/media/pet_media"
15
+ require "badline/media/cbm2_media"
15
16
  require "badline/media/tune"
16
17
  require "badline/media/program"
17
18
 
@@ -53,8 +54,9 @@ module Badline
53
54
  # .crt cartridges and raw .bin ROM images in its two cartridges'
54
55
  # slots (Plus4Media), and no .sid tunes either. The PET mounts .d80
55
56
  # and .d82 disks too, which no other machine takes, and has no
56
- # cartridges, tunes or true drives (PETMedia). The C128 in C64 mode
57
- # takes a C64's.
57
+ # cartridges, tunes or true drives (PETMedia), and so does the CBM-II,
58
+ # which takes its own .crt cartridges and no tapes (CBM2Media). The
59
+ # C128 in C64 mode takes a C64's.
58
60
  def attach(computer, path, autostart: true, subtune: nil, **)
59
61
  path = DiskList.disk(path)
60
62
  refuse_unsupported(computer, path)
@@ -126,17 +128,19 @@ module Badline
126
128
  private
127
129
 
128
130
  # The VIC-20 and the 264 series have no SID to play a .sid tune on,
129
- # and only the PET has the IEEE-488 drive a .d80 or .d82 goes in.
131
+ # and only the PET and the CBM-II have the IEEE-488 drive a .d80 or
132
+ # .d82 goes in.
130
133
  def refuse_unsupported(computer, path)
131
134
  family = computer.family
132
- if Extensions.kind(path) == :ieee_disk && family != :pet
133
- raise ArgumentError, "#{path} needs a PET's IEEE-488 drive"
135
+ if Extensions.kind(path) == :ieee_disk && !%i[pet cbm2].include?(family)
136
+ raise ArgumentError, "#{path} needs a PET's or a CBM-II's IEEE-488 drive"
134
137
  end
135
138
 
136
139
  refused, name = case family
137
140
  when :vic20 then [Vic20Media::UNSUPPORTED, "a VIC-20"]
138
141
  when :plus4 then [Vic20Media::UNSUPPORTED, "a Plus/4"]
139
142
  when :pet then [PETMedia::UNSUPPORTED, "a PET"]
143
+ when :cbm2 then [CBM2Media::UNSUPPORTED, "a CBM-II"]
140
144
  else [[], ""]
141
145
  end
142
146
  raise ArgumentError, "#{path} doesn't go in #{name}" if refused.include?(File.extname(path).downcase)
@@ -159,7 +163,11 @@ module Badline
159
163
 
160
164
  def attach_true_drive(computer, path, disk, autostart:)
161
165
  message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
162
- computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
166
+ if autostart && computer.family == :pet
167
+ PETMedia.disk_autostart(computer) { Vic20Media.first_program(path) }
168
+ elsif autostart
169
+ computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) })
170
+ end
163
171
  message
164
172
  end
165
173
 
@@ -167,7 +175,7 @@ module Badline
167
175
  storage = open_storage(path, disk)
168
176
  computer.mount(storage)
169
177
  if autostart && computer.family == :pet
170
- PETMedia.disk_autostart(computer, storage)
178
+ PETMedia.disk_autostart(computer) { storage.read_file("*") }
171
179
  elsif autostart
172
180
  computer.type_text(disk_autostart(computer) { storage.read_file("*") })
173
181
  end
@@ -178,7 +186,11 @@ module Badline
178
186
  # VIC-20 goes by that program, which the block gives
179
187
  # (Vic20Media.disk_autostart).
180
188
  def disk_autostart(computer)
181
- computer.family == :vic20 ? Vic20Media.disk_autostart(yield) : AUTOSTART
189
+ case computer.family
190
+ when :vic20 then Vic20Media.disk_autostart(yield)
191
+ when :cbm2 then CBM2Media::AUTOSTART
192
+ else AUTOSTART
193
+ end
182
194
  end
183
195
 
184
196
  # A VIC-20 or 264 series cartridge is its ROM chips. On the 264, the
@@ -190,6 +202,7 @@ module Badline
190
202
  crt = Storage::CRTFile.new(path, machine: :vic20)
191
203
  computer.attach_cartridge(crt.chips, crt.name.empty? ? File.basename(path, File.extname(path)) : crt.name)
192
204
  when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
205
+ when :cbm2 then computer.attach_cartridge(CBM2Media.cartridge_chips(path))
193
206
  else
194
207
  computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
195
208
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/storage/crt_file"
3
4
  require "badline/storage/p00"
4
5
  require "badline/storage/tap"
5
6
 
@@ -13,7 +14,8 @@ module Badline
13
14
 
14
15
  private
15
16
 
16
- # Takes `sid`, `c64`, `vic20`, `c128`, `plus4` or `pet` when given first.
17
+ # Takes `sid`, `c64`, `vic20`, `c128`, `plus4`, `pet` or `cbm2` when
18
+ # given first.
17
19
  def subcommand(args)
18
20
  case args.first
19
21
  when "sid" then @sid_command = true
@@ -22,6 +24,7 @@ module Badline
22
24
  when "c128" then @family = :c128
23
25
  when "plus4" then @family = :plus4
24
26
  when "pet" then @family = :pet
27
+ when "cbm2" then @family = :cbm2
25
28
  else return
26
29
  end
27
30
  @family_named = !@sid_command
@@ -34,23 +37,32 @@ module Badline
34
37
  when :c128 then C128_MODELS
35
38
  when :plus4 then PLUS4_MODELS
36
39
  when :pet then PET_MODELS
40
+ when :cbm2 then CBM2_MODELS
37
41
  else MODELS
38
42
  end
39
43
  end
40
44
 
41
45
  # Without a family named, a program that loads at C128_BASIC_START is
42
- # the C128's, and an 8050's or 8250's disk or a PET tape the PET's. A
43
- # program that loads where PET BASIC starts could be a VIC-20's too.
46
+ # the C128's, an 8050's or 8250's disk or a PET tape the PET's, and a
47
+ # cartridge signed as a CBM-II's the CBM-II's. A program that loads
48
+ # where PET BASIC starts could be a VIC-20's too.
44
49
  def pick_family
45
50
  return if @family_named || @sid_command || @media_path.nil? || !File.file?(@media_path)
46
51
 
47
52
  if pet_media?
48
53
  @family = :pet
54
+ elsif cbm2_cartridge?
55
+ @family = :cbm2
49
56
  elsif Media::Extensions::PROGRAMS.include?(File.extname(@media_path).downcase)
50
57
  @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
51
58
  end
52
59
  end
53
60
 
61
+ # Whether the media is a cartridge signed as a CBM-II's.
62
+ def cbm2_cartridge?
63
+ Media::Extensions.kind(@media_path) == :cartridge && Storage::CRTFile.cbm2?(@media_path)
64
+ end
65
+
54
66
  # Whether the media is an 8050's or 8250's disk or a PET tape.
55
67
  def pet_media?
56
68
  kind = Media::Extensions.kind(@media_path)
@@ -86,6 +86,7 @@ module Badline
86
86
  #{@program} c128 [options] [media]
87
87
  #{@program} plus4 [options] [media]
88
88
  #{@program} pet [options] [media]
89
+ #{@program} cbm2 [options] [media]
89
90
  #{@program} --headless [options] tune.sid
90
91
  #{@program} [options] tune.sid --audio-out FILE
91
92
  #{@program} sid [options] FILE|DIR...
@@ -96,6 +97,7 @@ module Badline
96
97
  c128 Run a C128 in C128 mode, the one --model names
97
98
  plus4 Run a Plus/4 or a C16, the one --model names
98
99
  pet Run a PET or a CBM, the one --model names
100
+ cbm2 Run a CBM-II, a B series or a P500, the one --model names
99
101
  sid Play .sid tunes and directories of them
100
102
 
101
103
  #{media_kinds}
@@ -126,10 +128,26 @@ module Badline
126
128
  and the pause menu's KEYBOARD page says where the rest are. --basic
127
129
  and --editor fit the upgrades Commodore sold, as in --model 2001
128
130
  --basic 2. BASIC 1 can't LOAD from a disk drive, though a disk still
129
- starts its first program, and BASIC 2 can. The 8032 and the 8096
130
- have two 50 Hz editors: 4-80-b-50hz, the default, and
131
- 4-80-b-50hz-0283, another chip's, with 64-cycle lines and 313 lines
132
- a frame.
131
+ starts its first program, and BASIC 2 can. Their --true-drive makes
132
+ device 8 an emulated 2031 on the IEEE-488 bus, which reads .d64 and
133
+ .g64 images. The 8032 and the 8096 have two 50 Hz editors:
134
+ 4-80-b-50hz, the default, and 4-80-b-50hz-0283, another chip's,
135
+ with 64-cycle lines and 313 lines a frame. The 8296 runs its own,
136
+ 4-80-b-50hz-8296, and takes the 8032's too. --characters din fits
137
+ it with the DIN character ROM, and --jumpers ram9,rama puts RAM at
138
+ $9000 and $A000, as xpet's -petram9 and -petramA do.
139
+
140
+ The CBM-II takes programs, disks, .d80 and .d82 among them, and
141
+ cartridges, and a cartridge made for it starts one without `cbm2`.
142
+ --kernal 03b fits the 1983 KERNAL in place of 04a, and --60hz runs
143
+ it on 60 Hz mains. Its --true-drive makes device 8 an emulated 2031
144
+ on its IEEE-488 bus, which reads .d64 and .g64 images, and a .d80
145
+ or a .d82 then goes in an emulated 8050 or 8250. Its keys type what
146
+ the host's keys type, F12 is RUN/STOP, Alt with F9 or F10 presses
147
+ its F9 or F10, and the pause menu's KEYBOARD page says where the
148
+ rest are. --model 510 or 510ntsc runs the P500, on the VIC-II with
149
+ KERNAL 02, whose keys are the B series' and whose two joysticks Tab
150
+ switches to.
133
151
 
134
152
  --headless plays a .sid tune on the host's audio device without the
135
153
  window, and --audio-out renders it to 16-bit PCM instead. The
@@ -152,8 +170,8 @@ module Badline
152
170
  in --at 3500:key=space or --at 250,500:screenshot=shot%05d.bmp.
153
171
  key=NAME presses a key (space, return, a, f1, run_stop, restore, and
154
172
  on the C128 esc, help, keypad1 and the rest, on the Plus/4
155
- esc, help and f1 to f3, and on the PET rvs, escape and kp1) or a
156
- joystick's
173
+ esc, help and f1 to f3, on the PET rvs, escape and kp1, and on
174
+ the CBM-II escape, norm_graph and kp00) or a joystick's
157
175
  direction or fire (joy1-up, joy2-fire) for #{Event::HOLD} frames, and
158
176
  pushes the C128's caps_lock or forty_eighty, which lock. type=TEXT
159
177
  types it, with \\n for RETURN. insert=FILE swaps in a disk, a list's