badline 0.11.1 → 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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CLAUDE.md +8 -7
  4. data/CONTRIBUTING.md +7 -6
  5. data/README.md +12 -5
  6. data/doc/media.md +28 -1
  7. data/doc/pinned-behaviour.md +31 -1
  8. data/exe/badline-ruby +1 -1
  9. data/lib/badline/address_bus/saved_state.rb +4 -10
  10. data/lib/badline/address_bus.rb +8 -27
  11. data/lib/badline/audio/bare_player.rb +1 -1
  12. data/lib/badline/c128/bus.rb +17 -52
  13. data/lib/badline/c128/bus_state.rb +5 -5
  14. data/lib/badline/c128/drives.rb +35 -22
  15. data/lib/badline/c128/fast_serial.rb +1 -0
  16. data/lib/badline/c128/io_pages.rb +35 -0
  17. data/lib/badline/c128/mmu.rb +9 -7
  18. data/lib/badline/c128/modes.rb +17 -4
  19. data/lib/badline/c128/saved_state.rb +22 -0
  20. data/lib/badline/c128/z80.rb +49 -0
  21. data/lib/badline/c128/z80_pages.rb +118 -0
  22. data/lib/badline/c128/z80_turns.rb +106 -0
  23. data/lib/badline/c128.rb +21 -83
  24. data/lib/badline/cartridge/action_replay.rb +2 -2
  25. data/lib/badline/cartridge/comal80.rb +1 -7
  26. data/lib/badline/cartridge/dinamic.rb +0 -8
  27. data/lib/badline/cartridge/final_cartridge3.rb +6 -6
  28. data/lib/badline/cartridge/fun_play.rb +0 -8
  29. data/lib/badline/cartridge/game_system.rb +0 -8
  30. data/lib/badline/cartridge/kcs_power.rb +10 -7
  31. data/lib/badline/cartridge/mach5.rb +2 -4
  32. data/lib/badline/cartridge/magic_desk.rb +0 -8
  33. data/lib/badline/cartridge/ocean.rb +0 -8
  34. data/lib/badline/cartridge/pagefox.rb +2 -2
  35. data/lib/badline/cartridge/retro_replay.rb +7 -6
  36. data/lib/badline/cartridge/rex_utility.rb +2 -6
  37. data/lib/badline/cartridge/rgcd.rb +2 -2
  38. data/lib/badline/cartridge/simons_basic.rb +0 -5
  39. data/lib/badline/cartridge/super_games.rb +4 -2
  40. data/lib/badline/cartridge/westermann.rb +2 -6
  41. data/lib/badline/cartridge.rb +37 -9
  42. data/lib/badline/cia/fast_forward.rb +21 -3
  43. data/lib/badline/cia.rb +8 -5
  44. data/lib/badline/computer/attachments.rb +14 -95
  45. data/lib/badline/computer/common.rb +34 -0
  46. data/lib/badline/computer/kernal_traps.rb +65 -5
  47. data/lib/badline/computer/true_drives.rb +69 -0
  48. data/lib/badline/computer.rb +9 -68
  49. data/lib/badline/core.rb +4 -2
  50. data/lib/badline/cpu_port.rb +112 -0
  51. data/lib/badline/drive/core.rb +8 -33
  52. data/lib/badline/drive/fast_serial.rb +73 -0
  53. data/lib/badline/drive/idle.rb +1 -11
  54. data/lib/badline/drive/orbit.rb +2 -5
  55. data/lib/badline/drive/sleep.rb +0 -2
  56. data/lib/badline/drive/vias.rb +84 -0
  57. data/lib/badline/drive.rb +2 -0
  58. data/lib/badline/drive1541/disk.rb +14 -17
  59. data/lib/badline/drive1541/disk_state.rb +7 -15
  60. data/lib/badline/drive1541.rb +1 -0
  61. data/lib/badline/drive1571/wd1770.rb +94 -8
  62. data/lib/badline/drive1571.rb +8 -58
  63. data/lib/badline/drive1581/bus.rb +137 -0
  64. data/lib/badline/drive1581/cpu.rb +11 -0
  65. data/lib/badline/drive1581/disk.rb +135 -0
  66. data/lib/badline/drive1581/disk_state.rb +87 -0
  67. data/lib/badline/drive1581/mechanism.rb +117 -0
  68. data/lib/badline/drive1581/serial_port.rb +52 -0
  69. data/lib/badline/drive1581/slot.rb +78 -0
  70. data/lib/badline/drive1581/track.rb +100 -0
  71. data/lib/badline/drive1581/track_bytes.rb +75 -0
  72. data/lib/badline/drive1581/wd1772/registers.rb +89 -0
  73. data/lib/badline/drive1581/wd1772/sector_writes.rb +73 -0
  74. data/lib/badline/drive1581/wd1772/sectors.rb +150 -0
  75. data/lib/badline/drive1581/wd1772/state.rb +66 -0
  76. data/lib/badline/drive1581/wd1772/stepping.rb +109 -0
  77. data/lib/badline/drive1581/wd1772/tracks.rb +125 -0
  78. data/lib/badline/drive1581/wd1772/turn.rb +73 -0
  79. data/lib/badline/drive1581/wd1772.rb +216 -0
  80. data/lib/badline/drive1581.rb +233 -0
  81. data/lib/badline/frontend/app.rb +1 -3
  82. data/lib/badline/frontend/audio_device.rb +67 -0
  83. data/lib/badline/frontend/audio_sink.rb +16 -50
  84. data/lib/badline/frontend/boot.rb +24 -67
  85. data/lib/badline/frontend/buttons.rb +7 -7
  86. data/lib/badline/frontend/keys.rb +33 -8
  87. data/lib/badline/frontend/media_slots.rb +42 -0
  88. data/lib/badline/frontend/{confirmation.rb → menu/confirmation.rb} +4 -4
  89. data/lib/badline/frontend/{file_browser.rb → menu/file_browser.rb} +18 -34
  90. data/lib/badline/frontend/{menu_dialogs.rb → menu/menu_dialogs.rb} +3 -7
  91. data/lib/badline/frontend/{menu_media.rb → menu/menu_media.rb} +26 -51
  92. data/lib/badline/frontend/{menu_pages.rb → menu/menu_pages.rb} +5 -8
  93. data/lib/badline/frontend/{name_field.rb → menu/name_field.rb} +11 -24
  94. data/lib/badline/frontend/{pause_menu.rb → menu/pause_menu.rb} +13 -28
  95. data/lib/badline/frontend/menu/theme.rb +18 -0
  96. data/lib/badline/frontend/{filter_view.rb → player/filter_view.rb} +4 -4
  97. data/lib/badline/frontend/{info_view.rb → player/info_view.rb} +3 -3
  98. data/lib/badline/frontend/{player_window.rb → player/player_controller.rb} +17 -25
  99. data/lib/badline/frontend/{player_footer.rb → player/player_footer.rb} +8 -8
  100. data/lib/badline/frontend/{player_header.rb → player/player_header.rb} +5 -5
  101. data/lib/badline/frontend/{player_screen.rb → player/player_screen.rb} +4 -5
  102. data/lib/badline/frontend/{scope.rb → player/scope.rb} +1 -1
  103. data/lib/badline/frontend/{sid_view.rb → player/sid_view.rb} +16 -20
  104. data/lib/badline/frontend/player/theme.rb +18 -0
  105. data/lib/badline/frontend/{visualizer_view.rb → player/visualizer_view.rb} +8 -8
  106. data/lib/badline/frontend/sound.rb +19 -38
  107. data/lib/badline/frontend/timeline.rb +3 -14
  108. data/lib/badline/frontend.rb +24 -20
  109. data/lib/badline/iec_bus.rb +14 -2
  110. data/lib/badline/kernal_trap/{file.rb → file_routine.rb} +1 -1
  111. data/lib/badline/kernal_trap/load.rb +1 -1
  112. data/lib/badline/kernal_trap/save.rb +1 -1
  113. data/lib/badline/kernal_trap.rb +1 -1
  114. data/lib/badline/machine/clocking.rb +50 -0
  115. data/lib/badline/machine.rb +63 -6
  116. data/lib/badline/media/boot_disk.rb +50 -0
  117. data/lib/badline/media/disk_list.rb +3 -3
  118. data/lib/badline/media/disk_set.rb +1 -1
  119. data/lib/badline/media/extensions.rb +33 -0
  120. data/lib/badline/media/not_disk.rb +8 -0
  121. data/lib/badline/media/program.rb +28 -0
  122. data/lib/badline/media/true_drive.rb +38 -17
  123. data/lib/badline/media/tune.rb +35 -0
  124. data/lib/badline/media/vic20_cartridge.rb +1 -7
  125. data/lib/badline/media/vic20_media.rb +38 -80
  126. data/lib/badline/media.rb +90 -81
  127. data/lib/badline/options/event.rb +4 -2
  128. data/lib/badline/options/family.rb +3 -9
  129. data/lib/badline/options/help.rb +2 -1
  130. data/lib/badline/roms/README +9 -0
  131. data/lib/badline/roms/dos1581.rom +0 -0
  132. data/lib/badline/sdl.rb +37 -20
  133. data/lib/badline/sid.rb +1 -1
  134. data/lib/badline/snapshot/machine_state.rb +9 -1
  135. data/lib/badline/snapshot/state.rb +6 -4
  136. data/lib/badline/snapshot.rb +11 -9
  137. data/lib/badline/storage/d64_image.rb +15 -7
  138. data/lib/badline/storage/disk_image/writing.rb +2 -2
  139. data/lib/badline/storage/disk_image.rb +1 -1
  140. data/lib/badline/storage/host_directory.rb +1 -1
  141. data/lib/badline/storage/p00.rb +8 -0
  142. data/lib/badline/storage/t64.rb +1 -1
  143. data/lib/badline/time_of_day.rb +3 -3
  144. data/lib/badline/version.rb +1 -1
  145. data/lib/badline/vic/bank.rb +1 -1
  146. data/lib/badline/vic/border_mask.rb +1 -1
  147. data/lib/badline/vic/collisions.rb +1 -1
  148. data/lib/badline/vic/color_patches.rb +1 -1
  149. data/lib/badline/vic/display_state.rb +1 -1
  150. data/lib/badline/vic/graphics_mode.rb +1 -1
  151. data/lib/badline/vic/graphics_shifter.rb +1 -1
  152. data/lib/badline/vic/register_log.rb +1 -1
  153. data/lib/badline/vic/registers.rb +1 -1
  154. data/lib/badline/vic/sequencer.rb +1 -1
  155. data/lib/badline/vic/sequencer_output.rb +1 -1
  156. data/lib/badline/vic/sequencer_state.rb +1 -1
  157. data/lib/badline/vic/sprite/internal_bus.rb +1 -1
  158. data/lib/badline/vic/sprite/saved_state.rb +1 -1
  159. data/lib/badline/vic/sprite/shifter.rb +1 -1
  160. data/lib/badline/vic/sprite/timing.rb +1 -1
  161. data/lib/badline/vic/sprite.rb +1 -1
  162. data/lib/badline/vic/sprites.rb +1 -1
  163. data/lib/badline/vic/sprites_state.rb +1 -1
  164. data/lib/badline/vic.rb +6 -5
  165. data/lib/badline/vic20/attachments.rb +15 -71
  166. data/lib/badline/vic20/saved_state.rb +2 -30
  167. data/lib/badline/vic20.rb +4 -37
  168. data/lib/badline/z80/core.rb +2 -0
  169. data/lib/badline/z80/saved_state.rb +69 -0
  170. metadata +61 -23
  171. data/lib/badline/cycleable.rb +0 -38
  172. /data/lib/badline/frontend/{menu_ports.rb → menu/menu_ports.rb} +0 -0
  173. /data/lib/badline/frontend/{player_commands.rb → player/player_commands.rb} +0 -0
  174. /data/lib/badline/frontend/{player_state.rb → player/player_state.rb} +0 -0
  175. /data/lib/badline/frontend/{sid_history.rb → player/sid_history.rb} +0 -0
@@ -14,33 +14,33 @@ module Badline
14
14
  # frame that would take the queue past LIMIT is dropped whole, which
15
15
  # only happens unpaced.
16
16
  #
17
- # Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
18
- # The chip's cycles are converted at `clock_hz`, the machine's clock, so
19
- # an NTSC or Drean machine plays at its own pitch.
17
+ # Each frame's samples go to a mono AudioDevice. The chip's cycles are
18
+ # converted at `clock_hz`, the machine's clock, so an NTSC or Drean
19
+ # machine plays at its own pitch.
20
20
  class Sound
21
21
  RATE = 44_100
22
22
  AHEAD = 0.08
23
23
  LIMIT = 0.25
24
24
 
25
- attr_reader :rate, :underruns, :dropped, :queued, :low, :high
25
+ attr_reader :underruns, :dropped, :queued, :low, :high
26
26
 
27
27
  def initialize(source, clock_hz, wanted, verbose)
28
28
  @source = source
29
29
  @clock_hz = clock_hz
30
30
  @verbose = verbose
31
- @device = 0
32
- @rate = RATE
31
+ @device = AudioDevice.new(channels: 1, rate: RATE, samples: 512)
33
32
  @started = false
34
33
  @muted = false
35
34
  @underruns = 0
36
35
  @dropped = 0
37
36
  @queued = 0
38
- @buffer = IO::Buffer.new(4096)
39
37
  reset_levels
40
38
  open_device if wanted
41
39
  end
42
40
 
43
- def on? = @device != 0
41
+ def rate = @device.rate
42
+
43
+ def on? = @device.open?
44
44
 
45
45
  # Plays another machine's sound from here on, at that machine's clock.
46
46
  def switch(source, clock_hz)
@@ -60,7 +60,7 @@ module Badline
60
60
  return if @muted || samples.empty?
61
61
 
62
62
  level = queued_seconds
63
- if level + (samples.size.to_f / @rate) > LIMIT
63
+ if level + (samples.size.to_f / rate) > LIMIT
64
64
  @dropped += samples.size
65
65
  else
66
66
  play(samples, level)
@@ -80,7 +80,7 @@ module Badline
80
80
  return unless @muted
81
81
 
82
82
  halt
83
- SDL.SDL_ClearQueuedAudio(@device)
83
+ @device.clear
84
84
  end
85
85
 
86
86
  def reset_levels
@@ -89,32 +89,22 @@ module Badline
89
89
  end
90
90
 
91
91
  def close
92
- SDL.SDL_CloseAudioDevice(@device) if on?
92
+ @device.close
93
93
  end
94
94
 
95
95
  private
96
96
 
97
97
  def open_device
98
- unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
99
- puts "No sound: #{SDL.SDL_GetError}"
100
- return
101
- end
102
-
103
- SDL.spec_freq(SDL.wanted, RATE)
104
- SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
105
- SDL.spec_channels(SDL.wanted, 1)
106
- SDL.spec_samples(SDL.wanted, 512)
107
- @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, SDL::ALLOW_FREQUENCY_CHANGE)
98
+ @device.open(false)
108
99
  return puts "No sound: #{SDL.SDL_GetError}" unless on?
109
100
 
110
- @rate = SDL.read_i32(SDL.obtained)
111
101
  record
112
- puts "Sound at #{@rate} Hz" if @verbose
102
+ puts "Sound at #{rate} Hz" if @verbose
113
103
  end
114
104
 
115
- def record = @source.record(rate: @rate, clock_hz: @clock_hz)
105
+ def record = @source.record(rate: @device.rate, clock_hz: @clock_hz)
116
106
 
117
- def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
107
+ def queued_seconds = @device.queued_seconds
118
108
 
119
109
  def play(samples, level)
120
110
  @low = level if level < @low
@@ -126,29 +116,20 @@ module Badline
126
116
  end
127
117
 
128
118
  def queue(samples)
129
- count = samples.size
130
- bytes = count * 2
131
- @buffer.resize(bytes) if bytes > @buffer.size
132
- buffer = @buffer
133
- i = 0
134
- while i < count
135
- buffer.set_value(:s16, i * 2, samples[i])
136
- i += 1
137
- end
138
- SDL.SDL_QueueAudio(@device, buffer, bytes)
139
- @queued += count
119
+ @device.queue(samples)
120
+ @queued += samples.size
140
121
  end
141
122
 
142
123
  def start
143
124
  return if @started
144
125
 
145
126
  @started = true
146
- SDL.SDL_PauseAudioDevice(@device, 0)
127
+ @device.start
147
128
  end
148
129
 
149
130
  def halt
150
131
  @started = false
151
- SDL.SDL_PauseAudioDevice(@device, 1)
132
+ @device.pause
152
133
  end
153
134
 
154
135
  def underrun!
@@ -183,12 +183,11 @@ module Badline
183
183
  return puts "No #{what} to eject" unless ejectable?(computer, what)
184
184
 
185
185
  if what == "disk"
186
- eject_disk(computer)
186
+ MediaSlots.eject_disk(computer)
187
187
  elsif what == "tape"
188
188
  computer.datasette.eject
189
189
  else
190
- computer.address_bus.detach_cartridge
191
- computer.power_cycle!
190
+ MediaSlots.remove_cartridge(computer)
192
191
  end
193
192
  puts "Ejected the #{what}"
194
193
  end
@@ -200,17 +199,7 @@ module Badline
200
199
  return !computer.datasette.tape.nil? if what == "tape"
201
200
  return !computer.address_bus.cartridge.nil? if what == "cartridge"
202
201
 
203
- drive = Media::TrueDrive.drive(computer)
204
- drive.nil? ? computer.mounted? : !drive.disk.nil?
205
- end
206
-
207
- def eject_disk(computer)
208
- drive = Media::TrueDrive.drive(computer)
209
- if drive.nil?
210
- computer.unmount
211
- else
212
- drive.insert(nil)
213
- end
202
+ MediaSlots.disk?(computer)
214
203
  end
215
204
  end
216
205
  end
@@ -9,7 +9,9 @@ require "badline/frontend/drive_led"
9
9
  require "badline/frontend/keys"
10
10
  require "badline/frontend/snapshots"
11
11
  require "badline/frontend/screenshot"
12
+ require "badline/frontend/media_slots"
12
13
  require "badline/frontend/timeline"
14
+ require "badline/frontend/audio_device"
13
15
  require "badline/frontend/sound"
14
16
  require "badline/frontend/pacer"
15
17
  require "badline/frontend/frame_report"
@@ -20,24 +22,26 @@ require "badline/frontend/machine_window"
20
22
  require "badline/frontend/app"
21
23
  require "badline/frontend/audio_sink"
22
24
  require "badline/frontend/painter"
23
- require "badline/frontend/sid_history"
24
- require "badline/frontend/sid_view"
25
- require "badline/frontend/filter_view"
26
- require "badline/frontend/scope"
27
- require "badline/frontend/visualizer_view"
25
+ require "badline/frontend/player/theme"
26
+ require "badline/frontend/player/sid_history"
27
+ require "badline/frontend/player/sid_view"
28
+ require "badline/frontend/player/filter_view"
29
+ require "badline/frontend/player/scope"
30
+ require "badline/frontend/player/visualizer_view"
28
31
  require "badline/frontend/buttons"
29
- require "badline/frontend/player_state"
30
- require "badline/frontend/player_header"
31
- require "badline/frontend/player_footer"
32
- require "badline/frontend/info_view"
33
- require "badline/frontend/player_commands"
34
- require "badline/frontend/player_screen"
35
- require "badline/frontend/player_window"
36
- require "badline/frontend/pause_menu"
37
- require "badline/frontend/file_browser"
38
- require "badline/frontend/confirmation"
39
- require "badline/frontend/name_field"
40
- require "badline/frontend/menu_dialogs"
41
- require "badline/frontend/menu_media"
42
- require "badline/frontend/menu_ports"
43
- require "badline/frontend/menu_pages"
32
+ require "badline/frontend/player/player_state"
33
+ require "badline/frontend/player/player_header"
34
+ require "badline/frontend/player/player_footer"
35
+ require "badline/frontend/player/info_view"
36
+ require "badline/frontend/player/player_commands"
37
+ require "badline/frontend/player/player_screen"
38
+ require "badline/frontend/player/player_controller"
39
+ require "badline/frontend/menu/theme"
40
+ require "badline/frontend/menu/pause_menu"
41
+ require "badline/frontend/menu/file_browser"
42
+ require "badline/frontend/menu/confirmation"
43
+ require "badline/frontend/menu/name_field"
44
+ require "badline/frontend/menu/menu_dialogs"
45
+ require "badline/frontend/menu/menu_media"
46
+ require "badline/frontend/menu/menu_ports"
47
+ require "badline/frontend/menu/menu_pages"
@@ -15,7 +15,9 @@ module Badline
15
15
  # A drive drives CLK and DATA from VIA 1's PB3 and PB1, the same way. It
16
16
  # also pulls DATA while ATN IN differs from its ATN acknowledge on PB4
17
17
  # (an XOR gate), which answers ATN in hardware before the DOS gets to it.
18
- # SerialPort reads the lines back into VIA 1.
18
+ # SerialPort reads the lines back into VIA 1. A drive whose
19
+ # serial_output sets DRIVE_ATN_GATED pulls DATA that way only while
20
+ # ATN is low, as the 1581's NAND does.
19
21
  #
20
22
  # Fast serial adds SRQ, which the C64 leaves alone. A C128 and a 1571
21
23
  # clock bytes over it from a CIA's CNT, with the bits on DATA from its
@@ -48,6 +50,10 @@ module Badline
48
50
  DRIVE_FAST_DATA = 0x100
49
51
  DRIVE_FAST_SRQ = 0x200
50
52
 
53
+ # A drive's serial_output bit whose ATN acknowledge pulls DATA only
54
+ # while ATN is low.
55
+ DRIVE_ATN_GATED = 0x400
56
+
51
57
  attr_reader :drives, :host_lines
52
58
 
53
59
  # The lines the host's fast serial pins pull, as DATA and SRQ bits.
@@ -119,7 +125,7 @@ module Badline
119
125
  while i < @drives.length
120
126
  drive = @drives[i].serial_output
121
127
  low |= CLK if drive.anybits?(DRIVE_CLK_OUT)
122
- low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || atn != drive.anybits?(DRIVE_ATNA)
128
+ low |= DATA if drive.anybits?(DRIVE_DATA_OUT) || acknowledging?(drive, atn)
123
129
  i += 1
124
130
  end
125
131
  low
@@ -127,6 +133,12 @@ module Badline
127
133
 
128
134
  def atn_low?(host = @host_lines) = host.anybits?(HOST_ATN_OUT)
129
135
 
136
+ # Whether a drive's ATN acknowledge pulls DATA, with its serial_output
137
+ # +drive+, while ATN is +atn+.
138
+ def acknowledging?(drive, atn)
139
+ atn != drive.anybits?(DRIVE_ATNA) && (atn || drive.nobits?(DRIVE_ATN_GATED))
140
+ end
141
+
130
142
  def clk_low? = low_lines.anybits?(CLK)
131
143
 
132
144
  def data_low? = low_lines.anybits?(DATA)
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/machine/clocking"
4
+
3
5
  module Badline
4
6
  # = Machine
5
7
  #
@@ -25,8 +27,9 @@ module Badline
25
27
  # +mounted?+, +datasette+, +attach_cartridge+,
26
28
  # +press_cartridge_button+, +release_cartridge_button+,
27
29
  # +attach_drive1541+, +drive1541+, +true_drive+ and +plug_true_drive+
28
- # for the drive Media::TrueDrive plugs in, and +address_bus+ for its
29
- # +cartridge+ and +detach_cartridge+.
30
+ # for the drive Media::TrueDrive plugs in, +attach_drive1581+,
31
+ # +drive1581+ and +plug_drive1581+ for a 1581 (Drive1581::Slot), and
32
+ # +address_bus+ for its +cartridge+ and +detach_cartridge+.
30
33
  # - Snapshots and tests: +snapshot+, +restore+, +save_snapshot+,
31
34
  # +install_debug_register+ and +capture_output+.
32
35
  #
@@ -34,8 +37,8 @@ module Badline
34
37
  # input parts, with its one joystick as both +joystick1+ and +joystick2+
35
38
  # and nil for +control_ports+, and of the rest +ram+, +load_prg+,
36
39
  # +type_text+, +mount+, +unmount+, +mounted?+, +datasette+, +attach_cartridge+,
37
- # +attach_drive1541+, +drive1541+, +true_drive+, +plug_true_drive+,
38
- # +install_debug_register+ and +capture_output+.
40
+ # +attach_drive1541+, +drive1541+, +true_drive+, +plug_true_drive+, the
41
+ # 1581's, +install_debug_register+ and +capture_output+.
39
42
  #
40
43
  # The C128 answers all but the cartridge button, and besides +mode+,
41
44
  # :c64, as Machine.build builds it, or :c128, +vdc+, the VDC a front
@@ -43,16 +46,22 @@ module Badline
43
46
  # +press_caps_lock+, +release_caps_lock+, +press_display_key+,
44
47
  # +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
45
48
  # C128D's 1571, its true drive.
49
+ #
50
+ # Each machine includes Machine::Clocking for the clocking part.
46
51
  module Machine
52
+ # Media the machine named doesn't take.
53
+ class Unsupported < ArgumentError; end
54
+
47
55
  # A new machine of `family`, :c64, :vic20 or :c128, the model of it
48
56
  # `model` names. A C64's model is one of Model::ALL, with its SID
49
57
  # `sid_model` when not nil and an REU of `reu` K when not nil. A
50
58
  # VIC-20's is "pal", with the RAM expansion `ram` names
51
59
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
52
- # one of C128::Model::ALL, with its SID `sid_model` when not nil.
60
+ # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
61
+ # for C64 mode.
53
62
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
54
63
  return vic20(model, ram) if family == :vic20
55
- return C128.new(model:, sid_model:) if family == :c128
64
+ return c128(model, sid_model, :c64) if family == :c128
56
65
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
57
66
 
58
67
  profile = Model.named(model)
@@ -61,11 +70,59 @@ module Badline
61
70
  kernal: profile.kernal, datasette: profile.datasette, board: profile.board)
62
71
  end
63
72
 
73
+ # The machine the command line's `options` name, to start `media`, a
74
+ # path or nil. A C64 or a C128 without a SID named takes the one a .sid
75
+ # tune names. The C128 is built for C128 mode, with C= held through the
76
+ # reset, so its KERNAL starts C64 mode, for --c64 (+c64_mode?+) or media
77
+ # only C64 mode runs: a .sid tune or a program that loads at the C64's
78
+ # BASIC start. A VIC-20 without RAM named gets the expansion the media
79
+ # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
80
+ # Unsupported for media a VIC-20 doesn't take.
81
+ def self.for_options(options, media, &)
82
+ family = options.family
83
+ return vic20_for(media, options.model, options.ram, &) if family == :vic20
84
+
85
+ sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
86
+ return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
87
+
88
+ machine = c128(options.model, sid_model, :c128)
89
+ machine.hold_commodore_key if options.c64_mode? || c64_media?(media)
90
+ machine
91
+ end
92
+
64
93
  def self.vic20(model, ram)
65
94
  raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
66
95
 
67
96
  Vic20.new(ram: ram || :unexpanded)
68
97
  end
69
98
  private_class_method :vic20
99
+
100
+ def self.c128(model, sid_model, mode) = C128.new(model:, sid_model:, mode:)
101
+ private_class_method :c128
102
+
103
+ def self.vic20_for(media, model, ram)
104
+ unless media.nil?
105
+ unsupported = Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
106
+ raise Unsupported, "doesn't go in a VIC-20" if unsupported
107
+
108
+ unless ram
109
+ ram = Media::Vic20Media.ram_for(media)
110
+ yield "RAM expansion: #{ram}"
111
+ end
112
+ end
113
+ build(:vic20, model:, ram:)
114
+ end
115
+ private_class_method :vic20_for
116
+
117
+ def self.c64_media?(media)
118
+ return false if media.nil? || !File.file?(media)
119
+
120
+ extension = File.extname(media).downcase
121
+ return true if extension == ".sid"
122
+ return false unless %w[.prg .p00].include?(extension)
123
+
124
+ [Media::BASIC_START, Media::BASIC_START - 1].include?(Storage::P00.load_address(media))
125
+ end
126
+ private_class_method :c64_media?
70
127
  end
71
128
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A C128 boot disk. At power-on the C128 KERNAL reads track 1, sector 0
6
+ # of the disk in device 8 and runs it when it starts with "CBM", as boot
7
+ # games have it. The 1581's DOS first loads and runs a USR file named
8
+ # COPYRIGHT CBM 86 when the disk has one, which is how CP/M's 1581 disks
9
+ # hand the KERNAL their boot sector, so a .d81 with that file boots too.
10
+ #
11
+ # On a C128 in C128 mode a boot disk goes in a true drive, a 1581 for a
12
+ # .d81 and the C128D's 1571 otherwise, and nothing is typed, so the
13
+ # KERNAL boots it. Other disks, and other machines, keep the traps and
14
+ # the autostart.
15
+ module BootDisk
16
+ SIGNATURE = "CBM".bytes.freeze
17
+ AUTO_BOOT = "copyright cbm 86"
18
+ TYPES = Extensions.of(%i[disk])
19
+
20
+ class << self
21
+ def takes?(computer, path)
22
+ computer.family == :c128 && computer.mode == :c128 && boot?(path)
23
+ end
24
+
25
+ # Puts a disk that boots on the computer in a true drive as device
26
+ # 8, write-protected with `disk: { read_only: true }`, for the KERNAL
27
+ # to boot at power-on, and says so. Nil for any other disk.
28
+ def attach(computer, path, disk: {}, **)
29
+ return unless takes?(computer, path)
30
+
31
+ read_only = disk.fetch(:read_only, false)
32
+ "#{TrueDrive.insert(computer, path, read_only:)}, to boot at power-on"
33
+ end
34
+
35
+ # Whether the disk image at path boots: its boot sector, or on a
36
+ # .d81 the 1581's auto-boot file.
37
+ def boot?(path)
38
+ type = File.extname(path).downcase
39
+ return false unless TYPES.include?(type) && File.file?(path)
40
+
41
+ image = Media.open_storage(path, { read_only: true })
42
+ block = image.read_block(1, 0)
43
+ return true if block && block[0, 3] == SIGNATURE
44
+
45
+ type == ".d81" && !image.read_file(AUTO_BOOT, type: :usr).nil?
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -11,9 +11,9 @@ module Badline
11
11
  # A list naming no disk image that's there.
12
12
  class Error < ArgumentError; end
13
13
 
14
- EXTENSIONS = %w[.m3u .vfl].freeze
14
+ EXTENSIONS = Extensions.of(%i[disk_list])
15
15
 
16
- def self.list?(path) = EXTENSIONS.include?(File.extname(path).downcase)
16
+ def self.list?(path) = Extensions.kind(path) == :disk_list
17
17
 
18
18
  # The disk `path` puts in the drive: a list's first disk, or the
19
19
  # path itself.
@@ -32,7 +32,7 @@ module Badline
32
32
  found = []
33
33
  entries(list).each do |entry|
34
34
  disk = entry.start_with?("/") ? entry : File.join(directory, entry)
35
- found << disk if DiskSet::EXTENSIONS.include?(File.extname(disk).downcase) && File.file?(disk)
35
+ found << disk if Extensions::DISKS.include?(File.extname(disk).downcase) && File.file?(disk)
36
36
  end
37
37
  found
38
38
  end
@@ -13,7 +13,7 @@ module Badline
13
13
  # ahead of the names.
14
14
  module DiskSet
15
15
  KINDS = %w[disk disc side part].freeze
16
- EXTENSIONS = %w[.d64 .d71 .d81 .g64].freeze
16
+ EXTENSIONS = Extensions::DISKS
17
17
 
18
18
  # The paths of the set's disks in order, or just `path` when it
19
19
  # belongs to none. A list's set is the disks it lists.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # The kind of medium each file extension names, which every check of a
6
+ # path's extension reads. A file with any other extension is a program.
7
+ # It loads on its own, for Options.
8
+ #
9
+ # - :disk, a disk image that the traps mount and a true drive reads
10
+ # - :gcr, a disk's raw GCR, which only a true drive reads
11
+ # - :archive, a .t64, which the traps mount read-only
12
+ # - :tape, :cartridge and :tune
13
+ # - :disk_list, a list of the disks of a set (DiskList)
14
+ module Extensions
15
+ KINDS = {
16
+ ".d64" => :disk, ".d71" => :disk, ".d81" => :disk, ".g64" => :gcr, ".g71" => :gcr, ".t64" => :archive,
17
+ ".tap" => :tape, ".crt" => :cartridge, ".sid" => :tune, ".m3u" => :disk_list, ".vfl" => :disk_list
18
+ }.freeze
19
+
20
+ # The kind of the medium at `path`, by its extension.
21
+ def self.kind(path) = KINDS.fetch(File.extname(path).downcase, :program)
22
+
23
+ # The extensions of the kinds given.
24
+ def self.of(kinds) = KINDS.keys.select { |extension| kinds.include?(KINDS[extension]) }.freeze
25
+
26
+ # Disk images, which a drive reads.
27
+ DISKS = of(%i[disk gcr])
28
+
29
+ # What goes in device 8 through the traps.
30
+ MOUNTABLE = of(%i[disk archive])
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A file that goes in device 8 but isn't a disk image or a directory.
6
+ class NotDisk < ArgumentError; end
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A program loading into a C64 or a C128 once it has booted
6
+ # (Media.attach). The VIC-20 has its own (Vic20Basic).
7
+ module Program
8
+ class << self
9
+ # Loads the program where it says, and RUNs one that loads at the
10
+ # start of BASIC.
11
+ def load(computer, data, autostart:)
12
+ load_addr = computer.load_prg(data)
13
+ # Run only makes sense for programs at BASIC start, or a byte ahead
14
+ # of it, where BASIC keeps the zero before its first line. BASIC 7.0
15
+ # starts at $1C01 and keeps the end of its text at $1210.
16
+ start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
17
+ return unless autostart && (load_addr == start || load_addr == start - 1)
18
+
19
+ # The end of the program, where BASIC's variables start, and where
20
+ # the KERNAL's LOAD leaves its end address.
21
+ end_addr = load_addr + data.length - 2
22
+ [text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
23
+ computer.type_text("run\r")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end