badline 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/lib/badline/address_bus/saved_state.rb +4 -10
  4. data/lib/badline/address_bus.rb +8 -27
  5. data/lib/badline/audio/bare_player.rb +1 -1
  6. data/lib/badline/c128/bus.rb +13 -35
  7. data/lib/badline/c128/bus_state.rb +5 -5
  8. data/lib/badline/c128.rb +5 -64
  9. data/lib/badline/computer/attachments.rb +16 -45
  10. data/lib/badline/computer/common.rb +34 -0
  11. data/lib/badline/computer/kernal_traps.rb +65 -5
  12. data/lib/badline/computer/true_drives.rb +12 -23
  13. data/lib/badline/computer.rb +5 -68
  14. data/lib/badline/core.rb +2 -2
  15. data/lib/badline/cpu_port.rb +112 -0
  16. data/lib/badline/drive1541/disk.rb +14 -17
  17. data/lib/badline/drive1541/disk_state.rb +7 -15
  18. data/lib/badline/drive1581/wd1772.rb +1 -1
  19. data/lib/badline/frontend/boot.rb +8 -59
  20. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  21. data/lib/badline/kernal_trap/load.rb +1 -1
  22. data/lib/badline/kernal_trap/save.rb +1 -1
  23. data/lib/badline/kernal_trap.rb +1 -1
  24. data/lib/badline/machine/clocking.rb +50 -0
  25. data/lib/badline/machine.rb +58 -2
  26. data/lib/badline/media/boot_disk.rb +2 -2
  27. data/lib/badline/media/disk_list.rb +3 -3
  28. data/lib/badline/media/disk_set.rb +1 -1
  29. data/lib/badline/media/extensions.rb +33 -0
  30. data/lib/badline/media/program.rb +28 -0
  31. data/lib/badline/media/true_drive.rb +3 -2
  32. data/lib/badline/media/tune.rb +35 -0
  33. data/lib/badline/media/vic20_cartridge.rb +1 -7
  34. data/lib/badline/media/vic20_media.rb +38 -80
  35. data/lib/badline/media.rb +82 -79
  36. data/lib/badline/options/event.rb +3 -1
  37. data/lib/badline/sid.rb +1 -1
  38. data/lib/badline/snapshot/machine_state.rb +9 -1
  39. data/lib/badline/snapshot/state.rb +5 -4
  40. data/lib/badline/snapshot.rb +11 -9
  41. data/lib/badline/storage/d64_image.rb +15 -7
  42. data/lib/badline/storage/disk_image/writing.rb +2 -2
  43. data/lib/badline/storage/disk_image.rb +1 -1
  44. data/lib/badline/storage/host_directory.rb +1 -1
  45. data/lib/badline/storage/t64.rb +1 -1
  46. data/lib/badline/version.rb +1 -1
  47. data/lib/badline/vic/bank.rb +1 -1
  48. data/lib/badline/vic/border_mask.rb +1 -1
  49. data/lib/badline/vic/collisions.rb +1 -1
  50. data/lib/badline/vic/color_patches.rb +1 -1
  51. data/lib/badline/vic/display_state.rb +1 -1
  52. data/lib/badline/vic/graphics_mode.rb +1 -1
  53. data/lib/badline/vic/graphics_shifter.rb +1 -1
  54. data/lib/badline/vic/register_log.rb +1 -1
  55. data/lib/badline/vic/registers.rb +1 -1
  56. data/lib/badline/vic/sequencer.rb +1 -1
  57. data/lib/badline/vic/sequencer_output.rb +1 -1
  58. data/lib/badline/vic/sequencer_state.rb +1 -1
  59. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  60. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  61. data/lib/badline/vic/sprite/shifter.rb +1 -1
  62. data/lib/badline/vic/sprite/timing.rb +1 -1
  63. data/lib/badline/vic/sprite.rb +1 -1
  64. data/lib/badline/vic/sprites.rb +1 -1
  65. data/lib/badline/vic/sprites_state.rb +1 -1
  66. data/lib/badline/vic.rb +6 -5
  67. data/lib/badline/vic20/attachments.rb +10 -80
  68. data/lib/badline/vic20/saved_state.rb +0 -30
  69. data/lib/badline/vic20.rb +1 -37
  70. metadata +8 -3
  71. data/lib/badline/cycleable.rb +0 -38
@@ -2,23 +2,24 @@
2
2
 
3
3
  module Badline
4
4
  class Computer
5
- # The true drives on the C64's serial bus: a Drive1541, and a Drive1581
6
- # (Drive1581::Slot), each in a field of its own, and their state for a
7
- # snapshot.
5
+ # The true drives on the machine's serial bus: a Drive1541, and a
6
+ # Drive1581 (Drive1581::Slot), each in a field of its own. Computer,
7
+ # Vic20 and C128 each include it.
8
8
  module TrueDrives
9
9
  include Drive1581::Slot
10
10
 
11
- # Plugs in a Drive1541, which then runs alongside the C64 on its own
12
- # clock and talks to it over the serial bus. The serial traps stop
11
+ # Plugs in a Drive1541, which then runs alongside the machine on its
12
+ # own clock and talks to it over the serial bus. The serial traps stop
13
13
  # answering the drive's device number, so the KERNAL's TALK, LISTEN and
14
14
  # byte transfers reach the drive. The LOAD and SAVE traps stay, and
15
15
  # still serve a mounted image.
16
16
  def attach_drive1541(drive)
17
17
  @iec_bus.detach(@drive1541) if @drive1541
18
- drive.host_clock_hz = region.clock_hz
18
+ drive.host_clock_hz = timing.clock_hz
19
19
  @drive1541 = drive
20
- drive.connect(iec_bus)
20
+ drive.connect(@iec_bus)
21
21
  @serial_trap&.device = serial_trap_device
22
+ drive1541_attached
22
23
  end
23
24
 
24
25
  # The true drive on the serial bus: the Drive1581 ahead of the
@@ -50,23 +51,11 @@ module Badline
50
51
  @drive1541&.device == device || drive1581_on_device8? ? nil : device
51
52
  end
52
53
 
53
- # A true 1541 on the serial bus, then a 1581. The leading flag is the
54
- # bus itself, which every machine now has.
55
- def save_serial_bus(out)
56
- out.boolean(true).boolean(!@drive1541.nil?)
57
- if @drive1541
58
- out.int(@drive1541.device)
59
- @drive1541.save_state(out)
60
- end
61
- save_drive1581(out)
62
- end
63
-
64
- def load_serial_bus(input)
65
- input.boolean?
66
- load_drive1541(input)
67
- load_drive1581(input)
68
- end
54
+ # What the machine does as a 1541 goes on its bus. The VIC-20 waits
55
+ # for its DOS to boot before typing.
56
+ def drive1541_attached = nil
69
57
 
58
+ # A true 1541 on the serial bus, with its device number.
70
59
  def load_drive1541(input)
71
60
  return detach_drive1541 unless input.boolean?
72
61
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/computer/common"
3
4
  require "badline/computer/true_drives"
4
5
  require "badline/computer/attachments"
5
6
  require "badline/computer/kernal_traps"
@@ -9,16 +10,14 @@ module Badline
9
10
  class Computer
10
11
  include IntegerHelper
11
12
  include KeyboardBuffer
13
+ include Machine::Clocking
14
+ include Common
12
15
  include Attachments
13
16
  include KernalTraps
14
17
  include SavedState
15
18
 
16
19
  attr_reader :address_bus, :cpu, :cycles, :drive1541
17
20
 
18
- # The path of the disk or directory device 8 serves through the traps
19
- # (Attachments), or an empty one.
20
- def mounted_path = @drive.nil? ? "" : @drive.path
21
-
22
21
  def family = :c64
23
22
 
24
23
  def region = address_bus.region
@@ -108,46 +107,17 @@ module Badline
108
107
  @cycles += 1
109
108
  end
110
109
 
111
- # Runs `count` cycles, one #cycle! after another.
112
- def run_cycles(count)
113
- i = 0
114
- while i < count
115
- cycle!
116
- i += 1
117
- end
118
- end
119
-
120
- # Runs cycles until the block returns true or the cycle count passes
121
- # `limit`, checking before each cycle.
122
- def run_until(limit)
123
- cycle! until yield || @cycles > limit
124
- end
125
-
126
110
  # The cycle at which #on_init's handlers run, once the KERNAL has booted.
127
111
  def init_threshold
128
112
  INIT_THRESHOLD
129
113
  end
130
114
 
131
- # The clock, the raster and the crop of the machine's region, a pixel
132
- # to a window pixel.
133
- def timing
134
- region = address_bus.region
135
- Timing.new(clock_hz: region.clock_hz, cycles_per_line: region.cycles_per_line,
136
- lines_per_frame: region.lines_per_frame, crop: region.crop, pixel_width: 1)
137
- end
138
-
139
115
  # The chip whose #display a front end shows.
140
116
  def video = @vic
141
117
 
142
118
  # The chip a front end records the machine's sound from.
143
119
  def sound_source = @sid
144
120
 
145
- def load_prg(data)
146
- uint16(data[0], data[1]).tap do |load_addr|
147
- ram.write(load_addr, data[2..])
148
- end
149
- end
150
-
151
121
  def attach_cartridge(cartridge)
152
122
  connect_cartridge(cartridge)
153
123
  power_cycle!
@@ -208,37 +178,12 @@ module Badline
208
178
  # The one-shot has pulsed already, so letting go does nothing.
209
179
  def release_restore; end
210
180
 
211
- def capture_output
212
- @capture_output ||= ChroutTrap.new(cpu:, bus: address_bus, layout: KernalTrap::C64_LAYOUT).tap do |trap|
213
- cpu.install_trap(ChroutTrap::ADDRESS) { trap.call }
214
- end
215
- end
216
-
217
- def inspect
218
- "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
219
- end
220
-
221
- def on_init(&block)
222
- if booting?
223
- @init_handlers << block
224
- else
225
- block.call
226
- end
227
- end
228
-
229
181
  private
230
182
 
231
- # The KERNAL the traps stand in for (Attachments#mount).
183
+ # The KERNAL the traps stand in for (KernalTraps#mount).
232
184
  def trap_layout = KernalTrap::C64_LAYOUT
233
185
 
234
- # The NMI line is wired-OR between CIA 2, the cartridge and the RESTORE
235
- # key, and the CPU takes an interrupt on its falling edge.
236
- def drive_nmi
237
- nmi = @cia2.interrupted? || @cartridge_nmi || @restore_pulse
238
- @restore_pulse = false
239
- @cpu.nmi = true if nmi && !@nmi_asserted
240
- @nmi_asserted = nmi
241
- end
186
+ def trap_bus = address_bus
242
187
 
243
188
  def plug_reu(size_kb)
244
189
  reu = REU.new(size_kb, bus: @address_bus, vic: @vic)
@@ -278,13 +223,5 @@ module Badline
278
223
  @freezing = false
279
224
  address_bus.cartridge.freeze!
280
225
  end
281
-
282
- def booting?
283
- @cycles < init_threshold
284
- end
285
-
286
- def handle_init
287
- @init_handlers.each(&:call)
288
- end
289
226
  end
290
227
  end
data/lib/badline/core.rb CHANGED
@@ -15,6 +15,7 @@ require "badline/memory"
15
15
  require "badline/color_memory"
16
16
  require "badline/rom"
17
17
  require "badline/ram_expansion"
18
+ require "badline/cpu_port"
18
19
  require "badline/address_bus"
19
20
  require "badline/instruction"
20
21
  require "badline/instruction_set"
@@ -23,7 +24,6 @@ require "badline/keyboard"
23
24
  require "badline/joystick"
24
25
  require "badline/control_ports"
25
26
  require "badline/input"
26
- require "badline/cycleable"
27
27
  require "badline/datasette"
28
28
  require "badline/time_of_day"
29
29
  require "badline/cia"
@@ -42,6 +42,7 @@ require "badline/drive1581"
42
42
  require "badline/drive1581/slot"
43
43
  require "badline/snapshot/state"
44
44
  require "badline/snapshot/setup"
45
+ require "badline/machine"
45
46
  require "badline/computer"
46
47
  require "badline/vic20"
47
48
  require "badline/checkpoint"
@@ -50,4 +51,3 @@ require "badline/cartridge"
50
51
  require "badline/kernal_trap"
51
52
  require "badline/chrout_trap"
52
53
  require "badline/media"
53
- require "badline/machine"
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # The I/O port of the 6510 and the 8502 at $00 (direction) and $01
5
+ # (data). An input pin reads its pull-up, P4 reads the datasette's sense
6
+ # line, and a pin without a pull-up holds the charge last driven onto
7
+ # it. P5 drives the datasette motor through an inverter: low runs it.
8
+ #
9
+ # The 6510 has pull-ups on P0-P2 and P4 and leaves P3, P6 and P7
10
+ # floating. The 8502 adds a CAPS LOCK pin on P6, high while the key is
11
+ # up.
12
+ class CPUPort
13
+ TAPE_SENSE = 0b0001_0000
14
+
15
+ # The direction and output registers, and the charge on the floating
16
+ # pins.
17
+ attr_reader :ddr, :data, :floating
18
+
19
+ # The port as the banking logic reads it (PortStatus).
20
+ attr_reader :status
21
+
22
+ # Whether CAPS LOCK is down, holding its pin low.
23
+ attr_reader :caps_lock
24
+
25
+ # +pullups+ and +floating+ are the masks of the pins with a pull-up and
26
+ # of the floating ones, and +caps_lock+ the CAPS LOCK pin's, 0 where
27
+ # there is none.
28
+ def initialize(datasette, pullups:, floating:, caps_lock: 0)
29
+ @datasette = datasette
30
+ @pullups = pullups
31
+ @floating_mask = floating
32
+ @caps_lock_pin = caps_lock
33
+ @caps_lock = false
34
+ @ddr = 0x00
35
+ @data = 0x00
36
+ @floating = 0x00
37
+ @change_handler = nil
38
+ @status = PortStatus.new(%i[basic kernal io tape_out tape_switch tape_motor], value: input_value)
39
+ end
40
+
41
+ # Calls the block after every write to the port, for the bus to remap
42
+ # its banks.
43
+ def on_change(&handler)
44
+ @change_handler = handler
45
+ end
46
+
47
+ # What a read of $01 sees.
48
+ def value = @status.value
49
+
50
+ def write_ddr(value)
51
+ @ddr = value
52
+ update
53
+ end
54
+
55
+ def write_data(value)
56
+ @data = value
57
+ update
58
+ end
59
+
60
+ # The RES line clears the direction and output registers. The floating
61
+ # pins keep their charge.
62
+ def reset!
63
+ @ddr = 0x00
64
+ @data = 0x00
65
+ update
66
+ end
67
+
68
+ # Sets the registers and the charge, as a snapshot restores them, and
69
+ # drives the motor and calls the change handler as a write does.
70
+ def restore(ddr, data, floating)
71
+ load(ddr, data, floating)
72
+ update
73
+ end
74
+
75
+ # Sets the registers and the charge, as a snapshot loads them, without
76
+ # driving the motor or calling the change handler.
77
+ def load(ddr, data, floating)
78
+ @ddr = ddr
79
+ @data = data
80
+ @floating = floating
81
+ refresh
82
+ end
83
+
84
+ # Holds CAPS LOCK down, or lets it up.
85
+ def caps_lock=(down)
86
+ @caps_lock = down
87
+ refresh
88
+ end
89
+
90
+ # Reads the inputs again, after the datasette's sense line changes.
91
+ def refresh
92
+ @status.value = input_value
93
+ end
94
+
95
+ private
96
+
97
+ def update
98
+ driven = @ddr & @floating_mask
99
+ @floating = (@floating & ~driven) | (@data & driven)
100
+ refresh
101
+ @datasette.motor = !@status.tape_motor?
102
+ @change_handler&.call
103
+ end
104
+
105
+ def input_value
106
+ input = @pullups | (@floating & @floating_mask)
107
+ input |= @caps_lock_pin unless @caps_lock
108
+ input &= ~TAPE_SENSE if @datasette.sense_low?
109
+ (@data & @ddr) | (input & ~@ddr & 0xff)
110
+ end
111
+ end
112
+ end
@@ -35,20 +35,10 @@ module Badline
35
35
  # The half track entries a side of a .g71 holds.
36
36
  G71_SIDE_ENTRIES = 84
37
37
 
38
- # GCR bytes around a track in each speed zone: 200 ms of rotation at
39
- # 300 rpm, in bytes of 32, 30, 28 and 26 µs.
38
+ # GCR bytes around a track in each speed zone (Storage::D64Image.zone):
39
+ # 200 ms of rotation at 300 rpm, in bytes of 32, 30, 28 and 26 µs.
40
40
  TRACK_LENGTHS = [6250, 6666, 7142, 7692].freeze
41
41
 
42
- # The speed zone each track is written in: 3 for the 21 sectors of
43
- # tracks 1-17, 2 for 18-24, 1 for 25-30 and 0 from 31 on.
44
- def self.zone(track)
45
- if track <= 17 then 3
46
- elsif track <= 24 then 2
47
- elsif track <= 30 then 1
48
- else 0
49
- end
50
- end
51
-
52
42
  # How far round the DOS's N: starts each track from the one before,
53
43
  # in each zone, in 1/10000 of a turn: it steps in and starts writing
54
44
  # the next track that long after it started the last.
@@ -69,10 +59,17 @@ module Badline
69
59
  # a .d71 formatted on both sides, and anything else as a .d64,
70
60
  # formatted. `read_only` opens the image write-protected.
71
61
  def self.open(path, read_only: false)
72
- return from_g64(Storage::G64Image.new(path, read_only:)).opened(path, read_only) if State.g64?(path)
73
- return from_d64(Storage::D71Image.new(path, read_only:)).opened(path, read_only) if State.d71?(path)
62
+ image = image_for(path, read_only)
63
+ (State.g64?(path) ? from_g64(image) : from_d64(image)).opened(path, read_only)
64
+ end
65
+
66
+ # The image at +path+, by its name: a .g64 or .g71, a .d71, and
67
+ # anything else a .d64.
68
+ def self.image_for(path, read_only)
69
+ return Storage::G64Image.new(path, read_only:) if State.g64?(path)
70
+ return Storage::D71Image.new(path, read_only:) if File.extname(path).casecmp?(".d71")
74
71
 
75
- from_d64(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
72
+ Storage::D64Image.new(path, read_only:)
76
73
  end
77
74
 
78
75
  # A disk from a G64 or G71 image, each half track as the image stores
@@ -141,7 +138,7 @@ module Badline
141
138
  angle = 0
142
139
  (1..image.track_count).each do |track|
143
140
  half_track = d64_half_track(image, track)
144
- zone = zone((half_track % SIDE) / 2)
141
+ zone = Storage::D64Image.zone((half_track % SIDE) / 2)
145
142
  angle = half_track == SIDE + 2 ? 0 : (angle + SKEWS[zone]) % SKEW_TURN if track > 1
146
143
  bytes = format_track(image, track, id, zone)
147
144
  disk.write(half_track, Track.new(bytes.rotate(-(bytes.length * angle / SKEW_TURN)), zone))
@@ -149,7 +146,7 @@ module Badline
149
146
  disk
150
147
  end
151
148
 
152
- def self.format_track(image, track, id, zone = zone(track))
149
+ def self.format_track(image, track, id, zone = Storage::D64Image.zone(track))
153
150
  sectors = image.sectors_in(track)
154
151
  length = TRACK_LENGTHS[zone]
155
152
  gap = (length - (sectors * SECTOR_LENGTH)) / sectors
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/media/extensions"
4
+
3
5
  module Badline
4
6
  class Drive1541
5
7
  class Disk
@@ -12,22 +14,12 @@ module Badline
12
14
  # for a disk from Disk.open. Nil for a disk made another way.
13
15
  attr_reader :path, :read_only
14
16
 
15
- # A .g64 or .g71 image by its name, a .d71 by its name, and anything
16
- # else a .d64.
17
- def self.g64?(path) = %w[.g64 .g71].include?(File.extname(path).downcase)
18
-
19
- def self.d71?(path) = File.extname(path).casecmp?(".d71")
17
+ # Whether the image is a .g64 or .g71, by its name.
18
+ def self.g64?(path) = Media::Extensions.kind(path) == :gcr
20
19
 
21
- # A disk for the image at the path, its tracks left for the state.
22
- def self.reopen(path, read_only)
23
- if g64?(path)
24
- Disk.new(Storage::G64Image.new(path, read_only:)).opened(path, read_only)
25
- elsif d71?(path)
26
- Disk.new(Storage::D71Image.new(path, read_only:)).opened(path, read_only)
27
- else
28
- Disk.new(Storage::D64Image.new(path, read_only:)).opened(path, read_only)
29
- end
30
- end
20
+ # A disk for the image at the path (Disk.image_for), its tracks left
21
+ # for the state.
22
+ def self.reopen(path, read_only) = Disk.new(Disk.image_for(path, read_only)).opened(path, read_only)
31
23
 
32
24
  # The disk a state from save_state describes: +current+ when it was
33
25
  # opened from the same image the same way, and otherwise a disk for
@@ -51,7 +51,7 @@ module Badline
51
51
  STEP_RATES = [12_000, 24_000, 4_000, 6_000].freeze
52
52
  SETTLE = 30_000
53
53
 
54
- NEVER = 1 << 62
54
+ NEVER = 1 << 60
55
55
 
56
56
  # Status bits. Bits 1 and 2 report the index pulse and track 0 after
57
57
  # a type I command, and DRQ and lost data after the others; bit 5
@@ -33,7 +33,7 @@ module Badline
33
33
  def self.media_problem
34
34
  yield
35
35
  ""
36
- rescue Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Datasette::Missing,
36
+ rescue Machine::Unsupported, Media::TrueDrive::Error, Media::DiskList::Error, Media::NotDisk, Datasette::Missing,
37
37
  SystemCallError, Storage::SIDFile::FormatError, Storage::T64::FormatError, Storage::TAP::FormatError,
38
38
  Storage::CRTFile::FormatError, Storage::G64Image::FormatError, Cartridge::UnsupportedTypeError,
39
39
  Snapshot::FormatError => e
@@ -53,11 +53,10 @@ module Badline
53
53
  start(options, media, options.writable?)
54
54
  end
55
55
 
56
- # A new machine built from the options, with `media`, if not nil,
57
- # attached and started as the command line does, its disk writable if
58
- # `writable`. The family and model give the machine, and a .sid tune
59
- # or --sid the SID. The VIC-20 raises Unsupported for a .sid
60
- # tune.
56
+ # A new machine built from the options (Machine.for_options), with
57
+ # `media`, if not nil, attached and started as the command line does,
58
+ # its disk writable if `writable`. The VIC-20 raises
59
+ # Machine::Unsupported for a .sid tune.
61
60
  def self.start(options, media, writable)
62
61
  computer = build(options, media)
63
62
  Media::TrueDrive.plug(computer) if options.true_drive?
@@ -68,60 +67,10 @@ module Badline
68
67
  computer
69
68
  end
70
69
 
70
+ # The machine the options name, built for `media`, which says the RAM
71
+ # expansion a VIC-20 gets for it.
71
72
  def self.build(options, media)
72
- case options.family
73
- when :vic20 then vic20(options, media)
74
- when :c128 then c128(options, media)
75
- else c64(options, media)
76
- end
77
- end
78
-
79
- def self.c64(options, media)
80
- sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
81
- Machine.build(:c64, model: options.model, sid_model:, reu: options.reu)
82
- end
83
-
84
- # A C128 in C128 mode. With --c64, or media only C64 mode runs, a .sid
85
- # tune or a program that loads at the C64's BASIC start, C= is held
86
- # through the reset, so the C128 KERNAL starts C64 mode.
87
- def self.c128(options, media)
88
- sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
89
- machine = C128.new(model: options.model, sid_model:, mode: :c128)
90
- machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
91
- machine
92
- end
93
-
94
- def self.c64_media?(media)
95
- return false if media.nil? || !File.file?(media)
96
-
97
- extension = File.extname(media).downcase
98
- return true if extension == ".sid"
99
- return false unless %w[.prg .p00].include?(extension)
100
-
101
- [Media::BASIC_START, Media::BASIC_START - 1].include?(Storage::P00.load_address(media))
73
+ Machine.for_options(options, media) { |line| puts line }
102
74
  end
103
-
104
- # A VIC-20 with the RAM --ram names, or else the RAM `media` needs
105
- # (Media::Vic20Media.ram_for), which it says. Raises Unsupported for
106
- # media a VIC-20 doesn't take.
107
- def self.vic20(options, media)
108
- ram = options.ram
109
- unless media.nil?
110
- unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
111
- raise Unsupported, "doesn't go in a VIC-20" if unsupported
112
-
113
- ram ||= vic20_ram(media)
114
- end
115
- Machine.build(:vic20, model: options.model, ram:)
116
- end
117
-
118
- def self.vic20_ram(media)
119
- ram = Media::Vic20Media.ram_for(media)
120
- puts "RAM expansion: #{ram}"
121
- ram
122
- end
123
-
124
- # Media the machine the options name doesn't take.
125
- class Unsupported < ArgumentError; end
126
75
  end
127
76
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Badline
4
4
  module KernalTrap
5
- class File < Routine
5
+ class FileRoutine < Routine
6
6
  private
7
7
 
8
8
  def active?
@@ -12,7 +12,7 @@ module Badline
12
12
  # below $0334, into the zero page, the stack or the KERNAL vectors: the
13
13
  # ROM's byte loop then loads it through the serial traps, and a loader
14
14
  # that overwrites ISTOP takes over mid-load as it does on a real drive.
15
- class Load < File
15
+ class Load < FileRoutine
16
16
  # ST bits at $90
17
17
  EOI = 0x40
18
18
  VERIFY_MISMATCH = 0x10
@@ -15,7 +15,7 @@ module Badline
15
15
  # write: the drive stops listening, so ST reads DEVICE NOT PRESENT,
16
16
  # and the ROM returns without an error. A "@" before the drive prefix
17
17
  # writes over a file of the same name.
18
- class Save < File
18
+ class Save < FileRoutine
19
19
  SECONDARY = 0x61
20
20
 
21
21
  # ST bit at $90
@@ -4,7 +4,7 @@ require "badline/kernal_trap/layout"
4
4
  require "badline/kernal_trap/routine"
5
5
  require "badline/kernal_trap/channel"
6
6
  require "badline/kernal_trap/drive"
7
- require "badline/kernal_trap/file"
7
+ require "badline/kernal_trap/file_routine"
8
8
  require "badline/kernal_trap/load"
9
9
  require "badline/kernal_trap/save"
10
10
  require "badline/kernal_trap/serial"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Machine
5
+ # Running a machine from outside its own #cycle!, and the handlers that
6
+ # wait for its KERNAL to boot. Computer, Vic20 and C128 each include it
7
+ # and answer #cycle!, #cycles and #init_threshold, the cycle at which
8
+ # #on_init's handlers run, and keep the handlers in @init_handlers.
9
+ module Clocking
10
+ # Runs `count` cycles, one #cycle! after another.
11
+ def run_cycles(count)
12
+ i = 0
13
+ while i < count
14
+ cycle!
15
+ i += 1
16
+ end
17
+ end
18
+
19
+ # Runs cycles until the block returns true or the cycle count passes
20
+ # `limit`, checking before each cycle.
21
+ def run_until(limit)
22
+ cycle! until yield || @cycles > limit
23
+ end
24
+
25
+ # Runs the block once the KERNAL has booted (see init_threshold), or
26
+ # now if it has.
27
+ def on_init(&block)
28
+ if booting?
29
+ @init_handlers << block
30
+ else
31
+ block.call
32
+ end
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class.name} cycles=#{@cycles} cpu=(#{@cpu.inspect})>"
37
+ end
38
+
39
+ private
40
+
41
+ def booting?
42
+ @cycles < init_threshold
43
+ end
44
+
45
+ def handle_init
46
+ @init_handlers.each(&:call)
47
+ end
48
+ end
49
+ end
50
+ end