badline 0.17.0 → 0.18.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +169 -20
  6. data/doc/pinned-behaviour.md +128 -0
  7. data/doc/snapshot-format.md +253 -18
  8. data/doc/snapshots.md +57 -23
  9. data/exe/badline-ruby +5 -0
  10. data/lib/badline/audio/terminal.rb +34 -2
  11. data/lib/badline/c128/keyed_state.rb +2 -0
  12. data/lib/badline/c128.rb +1 -0
  13. data/lib/badline/c64/keyed_state.rb +2 -0
  14. data/lib/badline/c64.rb +1 -0
  15. data/lib/badline/cbm2/keyed_state.rb +4 -0
  16. data/lib/badline/cbm2/p500.rb +5 -3
  17. data/lib/badline/cbm2.rb +6 -3
  18. data/lib/badline/chips/cia/timer.rb +2 -2
  19. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  20. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  21. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  22. data/lib/badline/chips/m6809/core.rb +149 -0
  23. data/lib/badline/chips/m6809/indexed.rb +82 -0
  24. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  25. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  26. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  27. data/lib/badline/chips/m6809/operations.rb +258 -0
  28. data/lib/badline/chips/m6809/plans.rb +143 -0
  29. data/lib/badline/chips/m6809/registers.rb +82 -0
  30. data/lib/badline/chips/m6809/stack.rb +140 -0
  31. data/lib/badline/chips/m6809.rb +13 -0
  32. data/lib/badline/chips/riot/timer.rb +14 -3
  33. data/lib/badline/chips/rriot.rb +4 -2
  34. data/lib/badline/chips/sid/filter.rb +4 -4
  35. data/lib/badline/chips/sid/waveform.rb +6 -6
  36. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  37. data/lib/badline/chips/vic_i.rb +84 -21
  38. data/lib/badline/data_path.rb +3 -3
  39. data/lib/badline/drives/ieee_models.rb +54 -0
  40. data/lib/badline/drives/model1001.rb +37 -0
  41. data/lib/badline/drives/model2031.rb +6 -0
  42. data/lib/badline/drives/model4040/disk.rb +62 -0
  43. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  44. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  45. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  46. data/lib/badline/drives/model4040/parts.rb +20 -0
  47. data/lib/badline/drives/model4040.rb +179 -0
  48. data/lib/badline/drives/model8050/board.rb +77 -0
  49. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  50. data/lib/badline/drives/model8050/idle.rb +11 -7
  51. data/lib/badline/drives/model8050/keyed_state.rb +5 -2
  52. data/lib/badline/drives/model8050.rb +18 -130
  53. data/lib/badline/drives/model9060/idle.rb +149 -0
  54. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  55. data/lib/badline/drives/model9060/parts.rb +98 -0
  56. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  57. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  58. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  59. data/lib/badline/drives/model9060.rb +195 -0
  60. data/lib/badline/frontend/app.rb +8 -0
  61. data/lib/badline/frontend/boot.rb +3 -2
  62. data/lib/badline/frontend/controls.rb +62 -6
  63. data/lib/badline/frontend/keys.rb +36 -0
  64. data/lib/badline/frontend/kim1_panel.rb +208 -0
  65. data/lib/badline/frontend/media_slots.rb +8 -1
  66. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  67. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  68. data/lib/badline/frontend/menu/menu_media.rb +42 -12
  69. data/lib/badline/frontend/menu/menu_pages.rb +97 -2
  70. data/lib/badline/frontend/menu/menu_ports.rb +38 -1
  71. data/lib/badline/frontend/menu/pause_menu.rb +5 -1
  72. data/lib/badline/frontend/pet_keyboard.rb +23 -1
  73. data/lib/badline/frontend/screen.rb +1 -1
  74. data/lib/badline/frontend/screens.rb +40 -2
  75. data/lib/badline/frontend/teletype_pane.rb +152 -0
  76. data/lib/badline/frontend/timeline.rb +31 -2
  77. data/lib/badline/frontend.rb +3 -0
  78. data/lib/badline/kim1/bus.rb +117 -0
  79. data/lib/badline/kim1/cpu.rb +25 -0
  80. data/lib/badline/kim1/host_terminal.rb +74 -0
  81. data/lib/badline/kim1/key_entry.rb +74 -0
  82. data/lib/badline/kim1/keyed_modules.rb +53 -0
  83. data/lib/badline/kim1/keyed_state.rb +133 -0
  84. data/lib/badline/kim1/panel.rb +176 -0
  85. data/lib/badline/kim1/rriots.rb +28 -0
  86. data/lib/badline/kim1/sound.rb +29 -0
  87. data/lib/badline/kim1/tape.rb +116 -0
  88. data/lib/badline/kim1/teletype.rb +193 -0
  89. data/lib/badline/kim1.rb +190 -0
  90. data/lib/badline/machine/ieee_drives.rb +94 -117
  91. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  92. data/lib/badline/machine/line_sound.rb +122 -0
  93. data/lib/badline/machine.rb +46 -15
  94. data/lib/badline/media/extensions.rb +8 -4
  95. data/lib/badline/media/kim1_media.rb +150 -0
  96. data/lib/badline/media/pet_media.rb +3 -1
  97. data/lib/badline/media/true_drive.rb +42 -45
  98. data/lib/badline/media.rb +12 -6
  99. data/lib/badline/options/family.rb +98 -8
  100. data/lib/badline/options/help.rb +44 -12
  101. data/lib/badline/options/table.rb +43 -11
  102. data/lib/badline/options/validation.rb +44 -8
  103. data/lib/badline/options.rb +27 -8
  104. data/lib/badline/pet/bus.rb +16 -2
  105. data/lib/badline/pet/keyboard_ports.rb +9 -2
  106. data/lib/badline/pet/keyed_state.rb +4 -0
  107. data/lib/badline/pet/model.rb +11 -2
  108. data/lib/badline/pet/rom_sets.rb +1 -0
  109. data/lib/badline/pet/sound.rb +5 -105
  110. data/lib/badline/pet/superpet/acia.rb +12 -0
  111. data/lib/badline/pet/superpet/board.rb +221 -0
  112. data/lib/badline/pet/superpet/dongle.rb +81 -0
  113. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  114. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  115. data/lib/badline/pet/superpet/m6809.rb +32 -0
  116. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  117. data/lib/badline/pet/superpet.rb +216 -0
  118. data/lib/badline/pet.rb +19 -3
  119. data/lib/badline/plus4/keyed_state.rb +2 -0
  120. data/lib/badline/plus4.rb +1 -0
  121. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  122. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  123. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  124. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  125. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  126. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  127. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  128. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  129. data/lib/badline/roms/README +166 -0
  130. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  131. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  132. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  133. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  134. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  135. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  136. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  137. data/lib/badline/snapshot/container.rb +2 -0
  138. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  139. data/lib/badline/snapshot/field_doc.rb +27 -6
  140. data/lib/badline/snapshot/image.rb +4 -1
  141. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  142. data/lib/badline/snapshot/pet_setup.rb +27 -11
  143. data/lib/badline/snapshot/storage_setup.rb +4 -1
  144. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  145. data/lib/badline/snapshot/vice/pet.rb +13 -7
  146. data/lib/badline/snapshot/vice/pet_cpu.rb +39 -5
  147. data/lib/badline/snapshot/vice/pet_crtc.rb +15 -2
  148. data/lib/badline/snapshot/vice/pet_drive8050.rb +188 -68
  149. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  150. data/lib/badline/snapshot/vice/pet_restore.rb +40 -11
  151. data/lib/badline/snapshot.rb +4 -0
  152. data/lib/badline/status.rb +30 -30
  153. data/lib/badline/storage/d67_image.rb +55 -0
  154. data/lib/badline/storage/d90_image.rb +180 -0
  155. data/lib/badline/storage/disk_image/writing.rb +2 -2
  156. data/lib/badline/storage/kim_tape.rb +254 -0
  157. data/lib/badline/storage/papertape.rb +90 -0
  158. data/lib/badline/storage.rb +4 -0
  159. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  160. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  161. data/lib/badline/version.rb +1 -1
  162. data/lib/badline/vic20/keyed_modules.rb +6 -3
  163. data/lib/badline/vic20/keyed_state.rb +6 -4
  164. data/lib/badline/vic20.rb +26 -12
  165. data/lib/badline.rb +1 -0
  166. metadata +77 -1
@@ -1,16 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/drives/ieee_models"
3
4
  require "badline/drives/model2031"
4
5
  require "badline/drives/model8050"
6
+ require "badline/drives/model9060"
7
+ require "badline/drives/model1001"
8
+ require "badline/drives/model4040"
5
9
 
6
10
  module Badline
7
11
  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
12
+ # A machine's true drive on its IEEE-488 bus, one at a time, of a model
13
+ # Drives::IEEEModels names: a Drives::Model2031, Model8050, Model8250,
14
+ # Model1001, Model4040, Model3040, Model2040, Model9060 or Model9090. It
15
+ # is clocked after the CPU each cycle at the machine's clock
16
+ # (ieee_host_clock_hz) and reset with it, as the machine's RES line
17
+ # reaches IFC. The traps on the KERNAL's primitives leave device 8 to
18
+ # the drive while it is device 8 (trap_device). The PET (PET::IEEEDrive) and the CBM-II
14
19
  # (CBM2::IEEEDrive) include it.
15
20
  #
16
21
  # The machine's @ieee is its side of the bus: #bus, the
@@ -18,142 +23,126 @@ module Badline
18
23
  # machine pulls now, and #cycle_lines, which does so once a cycle if
19
24
  # the machine doesn't push them as its chips are written.
20
25
  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
26
+ # The true drive on the bus, or nil.
27
+ attr_reader :true_drive
40
28
 
41
- drive.connect(Badline::Drives::IEEEBus.new)
42
- @drive2031 = nil
43
- @ieee_trap&.device = trap_device
44
- end
29
+ # The model plug_true_drive plugs in, one Drives::IEEEModels names.
30
+ attr_reader :true_drive_model
45
31
 
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
32
+ # Plugs in the drive, in place of the drive there.
33
+ def attach_ieee_drive(drive)
34
+ detach_ieee_drive
35
+ drive.host_clock_hz = ieee_host_clock_hz
36
+ drive.connect(@ieee.bus)
37
+ @ieee.push_lines
38
+ @true_drive = drive
52
39
  @ieee_trap&.device = trap_device
53
- @init_threshold = [@init_threshold, Badline::Drives::Model8050::BOOT_CYCLES].max if @cycles < @init_threshold
40
+ @init_threshold = [@init_threshold, drive.boot_cycles].max if @cycles < @init_threshold
54
41
  end
55
42
 
56
- # Unplugs the 8050 or 8250, leaving it on a bus of its own.
57
- def detach_drive8050
58
- drive = @drive8050
43
+ # Unplugs the true drive, leaving it on a bus of its own.
44
+ def detach_ieee_drive
45
+ drive = @true_drive
59
46
  return unless drive
60
47
 
61
48
  drive.connect(Badline::Drives::IEEEBus.new)
62
- @drive8050 = nil
49
+ @true_drive = nil
63
50
  @ieee_trap&.device = trap_device
64
51
  end
65
52
 
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) }
53
+ # Plugs in the model as +device+, in place of the drive there, and
54
+ # returns it.
55
+ def plug_ieee_drive(model, device: 8)
56
+ build_ieee_drive(model, device).tap { |drive| attach_ieee_drive(drive) }
71
57
  end
72
58
 
73
- # The true drive on the bus, or nil.
74
- def true_drive = @drive8050 || @drive2031
59
+ # The drive of a dual unit a disk image given on its own goes in
60
+ # (Media::TrueDrive): drive 0.
61
+ def disk_drive_number = 0
75
62
 
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
63
+ # Plugs in the model true_drive_model names as device 8, in place of
64
+ # the drive there, and returns it.
65
+ def plug_true_drive = plug_ieee_drive(@true_drive_model)
81
66
 
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
67
+ # Settles for a snapshot, then runs on until the true drive is where
68
+ # VICE saves it (the drive's settled_for_vice?) between instructions
69
+ # with the machine's, or for so many cycles, then on to the machine's
70
+ # next instruction. It stands in for Machine::Settling's, which the
87
71
  # machines include first.
88
72
  def settle_for_vice!
89
73
  ran = settle_for_snapshot!
90
- return ran if @drive2031.nil? && @drive8050.nil?
74
+ drive = @true_drive
75
+ return ran if drive.nil?
91
76
 
92
- until ran >= Machine::Settling::LIMIT || (drive_settled_for_vice? && settled_for_snapshot?)
77
+ until ran >= Machine::Settling::LIMIT || (drive.settled_for_vice? && settled_for_snapshot?)
93
78
  cycle!
94
79
  ran += 1
95
80
  end
96
81
  ran + settle_for_snapshot!
97
82
  end
98
83
 
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
84
  private
108
85
 
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
86
+ def init_ieee_drives(true_drive_model)
87
+ unless Badline::Drives::IEEEModels::NAMES.include?(true_drive_model)
88
+ raise ArgumentError, "no true drive model #{true_drive_model} for the IEEE-488 bus"
89
+ end
118
90
 
119
- def connect_drive(drive)
120
- drive.host_clock_hz = ieee_host_clock_hz
121
- drive.connect(@ieee.bus)
122
- @ieee.push_lines
91
+ @true_drive_model = true_drive_model
92
+ @true_drive = nil
93
+ end
94
+
95
+ # A new drive of the model as +device+.
96
+ def build_ieee_drive(model, device)
97
+ case model
98
+ when "8050" then Badline::Drives::Model8050.new(device:)
99
+ when "8250" then Badline::Drives::Model8250.new(device:)
100
+ when "1001" then Badline::Drives::Model1001.new(device:)
101
+ when "4040" then Badline::Drives::Model4040.new(device:)
102
+ when "3040" then Badline::Drives::Model3040.new(device:)
103
+ when "2040" then Badline::Drives::Model2040.new(device:)
104
+ when "9060" then Badline::Drives::Model9060.new(device:)
105
+ when "9090" then Badline::Drives::Model9090.new(device:)
106
+ else Badline::Drives::Model2031.new(device:)
107
+ end
123
108
  end
124
109
 
125
110
  # The true drive's cycles for one of the machine's, after the lines
126
111
  # it pulls.
127
112
  def clock_ieee_drives
128
- drive2031 = @drive2031
129
- drive8050 = @drive8050
130
- return unless drive2031 || drive8050
113
+ drive = @true_drive
114
+ return unless drive
131
115
 
132
116
  @ieee.cycle_lines
133
- drive2031&.host_cycle!
134
- drive8050&.host_cycle!
117
+ drive.host_cycle!
135
118
  end
136
119
 
137
120
  # The RES line reaches the true drive as IFC.
138
121
  def reset_ieee_drives
139
- @drive2031&.reset!
140
- @drive8050&.reset!
122
+ @true_drive&.reset!
123
+ end
124
+
125
+ def save_ieee_drive_fields(out)
126
+ out.text("host.ieee_drive_model", @true_drive_model)
127
+ end
128
+
129
+ def load_ieee_drive_fields(input)
130
+ @true_drive_model = input.choice("host.ieee_drive_model", Badline::Drives::IEEEModels::NAMES,
131
+ @true_drive_model)
141
132
  end
142
133
 
143
134
  # The true drive as keyed snapshot modules named from its device
144
135
  # number: BL-D8, BL-D8-CPU and so on for device 8.
145
136
  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}")
137
+ drive = @true_drive
138
+ drive&.save_modules(sections, "BL-D#{drive.device}")
150
139
  sections
151
140
  end
152
141
 
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.
142
+ # The true drive a snapshot's modules hold, of the model they name,
143
+ # built afresh unless the same model is on the bus as the same
144
+ # device, or none when they hold no drive. The bus then carries the
145
+ # machine's lines as its chips left them.
157
146
  def load_drive_modules(modules)
158
147
  device = 8
159
148
  input = modules.optional("BL-D8")
@@ -161,33 +150,21 @@ module Badline
161
150
  device += 1
162
151
  input = modules.optional("BL-D#{device}")
163
152
  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)
153
+ if input
154
+ load_ieee_drive_modules(modules, input, device)
169
155
  else
170
- detach_true_drives
156
+ detach_ieee_drive
171
157
  end
172
158
  @ieee.push_lines
173
159
  end
174
160
 
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}")
161
+ def load_ieee_drive_modules(modules, input, device)
162
+ model = input.text("model", Badline::Drives::IEEEModels::DEFAULT)
163
+ model = Badline::Drives::IEEEModels::DEFAULT unless Badline::Drives::IEEEModels::NAMES.include?(model)
164
+ drive = @true_drive
165
+ detach_ieee_drive if drive && (drive.device != device || drive.model_name != model)
166
+ drive = @true_drive || plug_ieee_drive(model, device:)
167
+ drive.load_modules(modules, input, "BL-D#{device}")
191
168
  end
192
169
  end
193
170
  end
@@ -28,13 +28,30 @@ module Badline
28
28
  # The PET's under BASIC 1, outside the zero page.
29
29
  PET_BASIC1_LAYOUT = Layout.new(address: 0x020f, count: 0x020d, capacity: 10)
30
30
 
31
- def type_text(text)
32
- codes = text.bytes.map { |b| ascii_to_petscii(b) }
33
- on_init { @pending_keys = codes }
31
+ def type_text(text) = type_on_init(text.bytes.map { |b| ascii_to_petscii(b) })
32
+
33
+ # The keys type_text left for the KERNAL to boot first, under
34
+ # host.init_keys, so that a machine restored before then types them.
35
+ def save_init_keys(out)
36
+ keys = @init_keys
37
+ out.ints("host.init_keys", keys) if keys
38
+ end
39
+
40
+ def load_init_keys(input)
41
+ keys = input.ints("host.init_keys", [])
42
+ type_on_init(keys.dup) unless keys.empty?
34
43
  end
35
44
 
36
45
  private
37
46
 
47
+ def type_on_init(codes)
48
+ @init_keys = codes if booting?
49
+ on_init do
50
+ @init_keys = nil
51
+ @pending_keys = codes
52
+ end
53
+ end
54
+
38
55
  def feed_keyboard
39
56
  buffer = keyboard_buffer
40
57
  return unless ram.peek(buffer.count).zero?
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Machine
5
+ # The sound of one logic line a machine drives into its speaker or
6
+ # audio output, such as the PET's CB2 or the KIM-1's PB7. The class
7
+ # that includes it answers #line_high?, the line's level this cycle.
8
+ #
9
+ # The machine calls #cycle! once a cycle while #recording?, from #record
10
+ # on, so a sound nobody records costs only that check. Each sample is
11
+ # the share of its cycles the line spent high, through the output
12
+ # stage: a one-pole low-pass at 5 kHz and a one-pole high-pass at
13
+ # 100 Hz, so a line left high or low settles to silence.
14
+ module LineSound
15
+ LOW_PASS_HZ = 5_000
16
+ HIGH_PASS_HZ = 100
17
+
18
+ # A swing from the line low to the line high, as a signed 16-bit
19
+ # sample.
20
+ SCALE = 24_000.0
21
+
22
+ attr_reader :recording
23
+ alias recording? recording
24
+
25
+ # Starts collecting the output, averaged down to `rate` samples a
26
+ # second at `clock_hz` cycles a second, for #drain_samples. The
27
+ # output stage takes the line's level now as settled.
28
+ def record(rate:, clock_hz: 1_000_000)
29
+ @rate = rate
30
+ @clock_hz = clock_hz
31
+ @remainder = 0
32
+ @window = next_window
33
+ @left = @window
34
+ @sum = 0
35
+ @high = line_high?
36
+ @run = 0
37
+ @low = @drift = @high ? 1.0 : 0.0
38
+ @low_pass = 1.0 - Math.exp(-2.0 * Math::PI * LOW_PASS_HZ / rate)
39
+ @high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
40
+ @samples = []
41
+ @recording = true
42
+ end
43
+
44
+ # Takes the line's level for this cycle, while recording.
45
+ def cycle!
46
+ high = line_high?
47
+ if high == @high
48
+ @run += 1
49
+ else
50
+ hold(@high, @run)
51
+ @high = high
52
+ @run = 1
53
+ end
54
+ end
55
+
56
+ # The samples closed since the last drain.
57
+ def drain_samples
58
+ return [] unless @recording
59
+
60
+ hold(@high, @run)
61
+ @run = 0
62
+ samples = @samples
63
+ @samples = []
64
+ samples
65
+ end
66
+
67
+ private
68
+
69
+ def init_line_sound
70
+ @recording = false
71
+ @high = true
72
+ @run = 0
73
+ @rate = 0
74
+ @clock_hz = 0
75
+ @remainder = 0
76
+ @window = 0
77
+ @left = 0
78
+ @sum = 0
79
+ @low = 0.0
80
+ @drift = 0.0
81
+ @low_pass = 0.0
82
+ @high_pass = 0.0
83
+ @samples = []
84
+ end
85
+
86
+ # Adds `cycles` cycles with the line high or low, closing every
87
+ # sample they finish.
88
+ def hold(high, cycles)
89
+ left = @left
90
+ while cycles >= left
91
+ @sum += left if high
92
+ cycles -= left
93
+ close
94
+ left = @left
95
+ end
96
+ @sum += cycles if high
97
+ @left = left - cycles
98
+ end
99
+
100
+ def close
101
+ @low += @low_pass * (@sum.fdiv(@window) - @low)
102
+ @drift += @high_pass * (@low - @drift)
103
+ sample = ((@low - @drift) * SCALE).round
104
+ sample = 32_767 if sample > 32_767
105
+ sample = -32_768 if sample < -32_768
106
+ @samples << sample
107
+ @sum = 0
108
+ @window = next_window
109
+ @left = @window
110
+ end
111
+
112
+ # Each window is a whole number of cycles, and they add up to the
113
+ # clock's cycles over a second.
114
+ def next_window
115
+ @remainder += @clock_hz
116
+ cycles = @remainder / @rate
117
+ @remainder -= cycles * @rate
118
+ cycles
119
+ end
120
+ end
121
+ end
122
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "badline/machine/clocking"
4
4
  require "badline/machine/keyboard_buffer"
5
+ require "badline/machine/line_sound"
5
6
  require "badline/machine/settling"
6
7
 
7
8
  module Badline
@@ -58,6 +59,11 @@ module Badline
58
59
  # and +capture_output+, and besides +model_label+, +cartridge_slots+ and
59
60
  # +detach_cartridges+ for its two cartridges.
60
61
  #
62
+ # The KIM-1 answers the identity, clocking, lifecycle and sound parts,
63
+ # with no video chip: a front end draws its panel (KIM1::Panel), and
64
+ # #true_drive is nil. Its keys are the panel's, through +press_key+,
65
+ # +release_key+, +type_keys+, +press_stop+ and +press_reset+.
66
+ #
61
67
  # The PET answers the identity, clocking, lifecycle, video, sound and
62
68
  # keyboard parts, with no joysticks, RESTORE or control ports, and of
63
69
  # the rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
@@ -69,22 +75,26 @@ module Badline
69
75
  # Media the machine named doesn't take.
70
76
  class Unsupported < ArgumentError; end
71
77
 
72
- # A new machine of `family`, :c64, :vic20, :c128, :plus4, :pet or :cbm2, the
78
+ # A new machine of `family`, :c64, :vic20, :c128, :plus4, :pet, :cbm2 or :kim1, the
73
79
  # model of it `model` names. A C64's model is one of Model::ALL, with
74
80
  # 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
81
+ # A VIC-20's is "pal" or "ntsc", with the RAM expansion `ram` names
76
82
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
77
83
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
78
84
  # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
79
85
  # 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. A CBM-II's is one of
86
+ # RAM and ROMs, its CRTC's editor the 50 Hz one, the superpet a
87
+ # PET::SuperPET with its CPU switch at 6502. A CBM-II's is one of
81
88
  # 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.
89
+ # one of CBM2::P500::Model::ALL, with its region's mains. A KIM-1 is
90
+ # the one model, with the RAM `ram` names
91
+ # (KIM1::Bus::RAM_CONFIGURATIONS), 1K when nil.
83
92
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
93
+ return KIM1.new(ram:) if family == :kim1
84
94
  return vic20(model, ram) if family == :vic20
85
95
  return c128(model, sid_model, :c64) if family == :c128
86
96
  return Plus4.new(model:) if family == :plus4
87
- return PET.new(model:) if family == :pet
97
+ return model == "superpet" ? PET::SuperPET.new : PET.new(model:) if family == :pet
88
98
  return CBM2::P500.new(model:) if family == :cbm2 && model.start_with?("510")
89
99
  return CBM2.new(model:) if family == :cbm2
90
100
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
@@ -110,6 +120,7 @@ module Badline
110
120
  return plus4_for(media, options) if family == :plus4
111
121
  return pet_for(media, options) if family == :pet
112
122
  return cbm2_for(media, options) if family == :cbm2
123
+ return kim1_for(media, options) if family == :kim1
113
124
 
114
125
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
115
126
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -119,11 +130,7 @@ module Badline
119
130
  machine
120
131
  end
121
132
 
122
- def self.vic20(model, ram)
123
- raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
124
-
125
- Vic20.new(ram: ram || :unexpanded)
126
- end
133
+ def self.vic20(model, ram) = Vic20.new(ram: ram || :unexpanded, model:)
127
134
  private_class_method :vic20
128
135
 
129
136
  def self.c128(model, sid_model, mode) = C128.new(model:, sid_model:, mode:)
@@ -160,27 +167,51 @@ module Badline
160
167
  # A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
161
168
  # model's own without it, the editor at the frame rate --60hz or
162
169
  # --editor asks for, the ROMs --basic, --editor and --characters fit it
163
- # with, and the 8296 board's jumpers --jumpers names.
170
+ # with, the 8296 board's jumpers --jumpers names, and the true drive
171
+ # --drive-type names. The SuperPET takes the frame rate, the 8032's
172
+ # BASIC and editors, the true drive, the CPU --cpu names and the bank
173
+ # RAM switch --bank-ram sets, and with the 6809 disks alone.
164
174
  def self.pet_for(media, options)
165
175
  unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
166
176
  raise Unsupported, "doesn't go in a PET" if unsupported
167
177
 
178
+ if options.model == "superpet"
179
+ disk = media.nil? || File.directory?(media) || %i[disk ieee_disk].include?(Media::Extensions.kind(media))
180
+ raise Unsupported, "doesn't go in the SuperPET's 6809 side" unless disk || options.cpu == "6502"
181
+
182
+ return PET::SuperPET.new(refresh_hz: options.refresh_hz, basic: options.basic, editor: options.editor,
183
+ cpu: options.cpu, bank_ram: options.bank_ram, true_drive_model: options.drive_type)
184
+ end
185
+
168
186
  PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
169
- editor: options.editor, characters: options.characters, jumpers: options.jumpers)
187
+ editor: options.editor, characters: options.characters, jumpers: options.jumpers,
188
+ true_drive_model: options.drive_type)
170
189
  end
171
190
  private_class_method :pet_for
172
191
 
173
192
  # 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.
193
+ # for: a B series' 50 Hz without it, and a P500's its region's, and
194
+ # the true drive --drive-type names.
175
195
  def self.cbm2_for(media, options)
176
196
  unsupported = !media.nil? && Media::CBM2Media::UNSUPPORTED.include?(File.extname(media).downcase)
177
197
  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
198
 
180
- CBM2.new(model: options.model, kernal: options.kernal, mains_hz: options.mains_hz || 50)
199
+ drive = options.drive_type
200
+ return CBM2::P500.new(model: options.model, mains_hz: options.mains_hz, true_drive_model: drive) if options.p500?
201
+
202
+ CBM2.new(model: options.model, kernal: options.kernal, mains_hz: options.mains_hz || 50, true_drive_model: drive)
181
203
  end
182
204
  private_class_method :cbm2_for
183
205
 
206
+ # A KIM-1 with the RAM --ram names, on a teletype at --baud with
207
+ # --tty, for media it takes (Media::KIM1Media).
208
+ def self.kim1_for(media, options)
209
+ raise Unsupported, "doesn't go in a KIM-1" unless media.nil? || Media::KIM1Media.takes?(media)
210
+
211
+ KIM1.new(tty: options.tty?, baud: options.baud, ram: options.ram)
212
+ end
213
+ private_class_method :kim1_for
214
+
184
215
  def self.c64_media?(media)
185
216
  return false if media.nil? || !File.file?(media)
186
217
 
@@ -7,17 +7,21 @@ module Badline
7
7
  # It loads on its own, for Options.
8
8
  #
9
9
  # - :disk, a disk image that the traps mount and a true drive reads
10
- # - :ieee_disk, an 8050's or 8250's disk image, for a PET's IEEE-488
11
- # drive
10
+ # - :ieee_disk, an 8050's, 8250's or 2040's disk image, for a PET's
11
+ # IEEE-488 drive
12
+ # - :hard_disk, a D9060's or D9090's hard disk, which only a PET's or a
13
+ # CBM-II's true drive reads
12
14
  # - :gcr, a disk's raw GCR, which only a true drive reads
13
15
  # - :archive, a .t64, which the traps mount read-only
14
16
  # - :tape, :cartridge and :tune
17
+ # - :papertape, a MOS papertape, which only the KIM-1 loads
15
18
  # - :disk_list, a list of the disks of a set (DiskList)
16
19
  module Extensions
17
20
  KINDS = {
18
21
  ".d64" => :disk, ".d71" => :disk, ".d81" => :disk, ".g64" => :gcr, ".g71" => :gcr, ".t64" => :archive,
19
- ".d80" => :ieee_disk, ".d82" => :ieee_disk, ".tap" => :tape, ".crt" => :cartridge, ".sid" => :tune,
20
- ".m3u" => :disk_list, ".vfl" => :disk_list
22
+ ".d80" => :ieee_disk, ".d82" => :ieee_disk, ".d67" => :ieee_disk, ".d90" => :hard_disk, ".tap" => :tape,
23
+ ".crt" => :cartridge, ".sid" => :tune, ".m3u" => :disk_list, ".vfl" => :disk_list, ".ptp" => :papertape,
24
+ ".pap" => :papertape
21
25
  }.freeze
22
26
 
23
27
  # The kind of the medium at `path`, by its extension.