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
@@ -3,13 +3,15 @@
3
3
  module Badline
4
4
  module Media
5
5
  # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
- # the C128 its 1571. Once a true drive is plugged in as device 8, disk
7
- # media go into it rather than into the traps, so a LOAD runs the DOS
8
- # and every byte crosses the serial bus. A .g64 holds the disk's raw
9
- # GCR, which only a true drive reads, so it plugs one in when there's
10
- # none yet. The 1541 reads only .d64 and .g64 images, and the 1571
11
- # .d71 and .g71 as well, so the traps' other disk media, a .d81 or
12
- # .t64 or a host directory, raise Error.
6
+ # the C128 its 1571, or a 1581. Once a true drive is plugged in as
7
+ # device 8, disk media go into it rather than into the traps, so a LOAD
8
+ # runs the DOS and every byte crosses the serial bus. A .g64 holds the
9
+ # disk's raw GCR, which only a true drive reads, so it plugs one in
10
+ # when there's none yet. The 1541 reads only .d64 and .g64 images, the
11
+ # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
12
+ # the drive on device 8 for a 1581 (Drive1581::Slot), and another disk
13
+ # image swaps a 1581 back for the machine's own drive. The traps' other
14
+ # disk media, a .t64 or a host directory, raise Error.
13
15
  module TrueDrive
14
16
  class Error < ArgumentError; end
15
17
 
@@ -30,27 +32,46 @@ module Badline
30
32
  # other disk media, which the traps would mount, when a true drive
31
33
  # is device 8.
32
34
  def takes?(computer, path)
33
- return true if %w[.g64 .g71].include?(File.extname(path).downcase)
35
+ kind = Extensions.kind(path)
36
+ return true if kind == :gcr
34
37
 
35
- !drive(computer).nil? && (File.directory?(path) || MOUNT_TYPES.key?(File.extname(path).downcase))
38
+ !drive(computer).nil? && (File.directory?(path) || %i[disk archive].include?(kind))
36
39
  end
37
40
 
38
- # Puts the image in the drive on device 8, plugging one in when
39
- # there's none, and takes out what was mounted through the traps,
40
- # so LOAD and SAVE reach the drive too. `read_only` puts it in
41
- # write-protected.
41
+ # Puts the image in the drive on device 8, plugging one in that
42
+ # reads it when the drive there doesn't, and takes out what was
43
+ # mounted through the traps, so LOAD and SAVE reach the drive too.
44
+ # `read_only` puts it in write-protected.
42
45
  def insert(computer, path, read_only: false)
43
- name = drive(computer)&.model_name || (computer.family == :c128 ? "1571" : "1541")
46
+ return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
47
+
48
+ drive = own_drive(computer)
49
+ name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
44
50
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
45
51
  unless types.include?(File.extname(path).downcase)
46
- raise Error, "#{path} is not a #{types[0..-2].join(', ')} or #{types.last} image, which the #{name} reads"
52
+ raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
47
53
  end
48
54
 
49
- disk = Drive1541::Disk.open(path, read_only:)
55
+ (drive || computer.plug_true_drive).insert_image(path, read_only:)
50
56
  computer.unmount
51
- plug(computer).insert(disk)
52
57
  "Inserted #{path} in the #{name} as device 8"
53
58
  end
59
+
60
+ private
61
+
62
+ # The drive on device 8 unless it's a 1581.
63
+ def own_drive(computer)
64
+ drive = drive(computer)
65
+ drive unless drive&.model_name == "1581"
66
+ end
67
+
68
+ def insert_d81(computer, path, read_only)
69
+ drive = drive(computer)
70
+ drive = computer.plug_drive1581 unless drive&.model_name == "1581"
71
+ drive.insert_image(path, read_only:)
72
+ computer.unmount
73
+ "Inserted #{path} in the 1581 as device 8"
74
+ end
54
75
  end
55
76
  end
56
77
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A .sid tune on a machine with a SID (Media.attach): its data and the
6
+ # player the tune boots with go in RAM once the KERNAL has booted, and
7
+ # the autostart types what starts it.
8
+ module Tune
9
+ class << self
10
+ # Plays `subtune`, or the tune's own start subtune, and says what
11
+ # plays. A tune for more than one SID raises FormatError.
12
+ def attach(computer, path, autostart:, subtune:)
13
+ tune = Storage::SIDFile.new(path)
14
+ if tune.sids > 1
15
+ raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
16
+ end
17
+
18
+ subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
19
+ computer.on_init { start(computer, tune, autostart:, subtune:) }
20
+ title = tune.name.empty? ? path : tune.name
21
+ title += " (subtune #{subtune})" if tune.subtunes > 1
22
+ autostart ? "Playing #{title}" : "Loaded #{title}"
23
+ end
24
+
25
+ private
26
+
27
+ def start(computer, tune, autostart:, subtune:)
28
+ computer.ram.write(tune.load_address, tune.data)
29
+ tune.boot_memory(subtune:).each { |address, bytes| computer.ram.write(address, bytes) }
30
+ computer.type_text(tune.boot_command) if autostart
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -26,12 +26,6 @@ module Badline
26
26
  parts.map { |part| rom_chip(part) }
27
27
  end
28
28
 
29
- # A program's bytes, unwrapped from a .p00.
30
- def program_bytes(path)
31
- bytes = File.binread(path).bytes
32
- Storage::P00.wraps?(bytes) ? Storage::P00.data(bytes) : bytes
33
- end
34
-
35
29
  private
36
30
 
37
31
  def load_address(data)
@@ -42,7 +36,7 @@ module Badline
42
36
  name = part_names(path, own, address).find { |candidate| File.file?(candidate) }
43
37
  return unless name
44
38
 
45
- part = program_bytes(name)
39
+ part = Media.program_bytes(name)
46
40
  part if load_address(part) == address
47
41
  end
48
42
 
@@ -2,45 +2,25 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # Media on a VIC-20. A program loads once the KERNAL has booted: a
6
- # BASIC program at the start of BASIC, wherever the RAM puts it, and
7
- # RUNs. A program that loads into BLK5 at $A000 is a cartridge's ROM
8
- # instead, as are the parts of a set that includes one
9
- # (Vic20Cartridge), and goes in with a power cycle, as a .crt does.
10
- # Disks mount on device 8 through the KERNAL traps, or go in a true
11
- # 1541 (TrueDrive), and tapes play on the datasette.
5
+ # What a VIC-20 does with media of its own (Media.attach). A program
6
+ # loads once the KERNAL has booted: a BASIC program at the start of
7
+ # BASIC, wherever the RAM puts it, and RUNs. A program that loads into
8
+ # BLK5 at $A000 is a cartridge's ROM instead, as are the parts of a set
9
+ # that includes one (Vic20Cartridge), and goes in with a power cycle,
10
+ # as a .crt does. Disks mount on device 8 through the KERNAL traps, or
11
+ # go in a true 1541 (TrueDrive), and tapes play on the datasette.
12
12
  module Vic20Media
13
13
  # The expansions that fill BLK1, BLK2 and BLK3 one block after
14
14
  # another, each with the last address it fills.
15
15
  BLOCK_EXPANSIONS = [[0x3fff, :"8k"], [0x5fff, :"16k"], [0x7fff, :"24k"]].freeze
16
16
 
17
17
  # What a VIC-20 doesn't take.
18
- UNSUPPORTED = %w[.sid].freeze
18
+ UNSUPPORTED = Extensions.of(%i[tune])
19
19
 
20
20
  # The platform byte of a VIC-20 tape (Storage::TAP#platform).
21
21
  TAPE_PLATFORM = 1
22
22
 
23
23
  class << self
24
- def attach(machine, path, autostart:, **options)
25
- extension = File.extname(path).downcase
26
- raise ArgumentError, "#{path} doesn't go in a VIC-20" if UNSUPPORTED.include?(extension)
27
-
28
- if TrueDrive.takes?(machine, path)
29
- attach_true_drive(machine, path, options.fetch(:disk, {}), autostart:)
30
- elsif File.directory?(path)
31
- machine.mount(Storage::HostDirectory.new(path))
32
- "Mounted #{path} as device 8"
33
- elsif extension == ".crt"
34
- attach_cartridge(machine, path, Storage::CRTFile.new(path, machine: :vic20).chips)
35
- elsif extension == ".tap"
36
- attach_tape(machine, path, autostart:)
37
- elsif MOUNT_TYPES.key?(extension)
38
- attach_storage(machine, path, options.fetch(:disk, {}), autostart:)
39
- else
40
- attach_prg(machine, path, autostart:)
41
- end
42
- end
43
-
44
24
  # The RAM expansion (Vic20::Bus::RAM_CONFIGURATIONS) a VIC-20 needs
45
25
  # for the program at `path`, or for the first program on the disk
46
26
  # or in the directory there, by where it loads. A BASIC program
@@ -60,72 +40,50 @@ module Badline
60
40
  end
61
41
  end
62
42
 
63
- private
64
-
65
- def expansion_reaching(last)
66
- BLOCK_EXPANSIONS.each { |reach, ram| return ram if last <= reach }
67
- BLOCK_EXPANSIONS.last.last
68
- end
69
-
43
+ # The program at `path`, or the first on the disk or in the
44
+ # directory there, or nil.
70
45
  def first_program(path)
71
- extension = File.extname(path).downcase
72
- if File.directory?(path) || MOUNT_TYPES.key?(extension)
73
- open_storage(path).read_file("*")
74
- elsif File.file?(path) && !%w[.sid .crt .tap .g64].include?(extension)
75
- Vic20Cartridge.program_bytes(path)
46
+ kind = Extensions.kind(path)
47
+ if File.directory?(path) || %i[disk archive].include?(kind)
48
+ Media.open_storage(path).read_file("*")
49
+ elsif File.file?(path) && !%i[tune cartridge tape gcr].include?(kind)
50
+ Media.program_bytes(path)
76
51
  end
77
52
  end
78
53
 
79
- def open_storage(path, disk = {})
80
- return Storage::HostDirectory.new(path) if File.directory?(path)
81
-
82
- storage = MOUNT_TYPES.fetch(File.extname(path).downcase)
83
- storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
84
- end
85
-
86
- def attach_cartridge(machine, path, chips)
87
- machine.attach_cartridge(chips)
88
- "Attached cartridge #{path}"
89
- end
90
-
91
- # A tape made for another machine plays all the same, and says so.
92
- def attach_tape(machine, path, autostart:)
93
- tape = Storage::TAP.new(path)
94
- machine.datasette.insert(tape)
95
- machine.datasette.play!
96
- machine.type_text(TAPE_AUTOSTART) if autostart
97
- message = "Inserted #{path} in the datasette"
98
- tape.platform == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the VIC-20"
99
- end
100
-
101
- def attach_true_drive(machine, path, disk, autostart:)
102
- message = TrueDrive.insert(machine, path, read_only: disk.fetch(:read_only, false))
103
- machine.type_text(disk_autostart(first_program(path))) if autostart
104
- message
105
- end
106
-
107
- def attach_storage(machine, path, disk, autostart:)
108
- storage = open_storage(path, disk)
109
- machine.mount(storage)
110
- machine.type_text(disk_autostart(storage.read_file("*"))) if autostart
111
- "Mounted #{path} as device 8"
112
- end
113
-
114
- # A BASIC program loads relocated to the start of BASIC, and anything
115
- # else where it says.
54
+ # What loads and runs `data`, the first program on the disk in
55
+ # device 8: a BASIC program loads relocated to the start of BASIC,
56
+ # and anything else where it says.
116
57
  def disk_autostart(data)
117
58
  basic = data && data.length > 2 && Vic20Basic::BASIC_STARTS.key?(data[0] | (data[1] << 8))
118
59
  basic ? %(lO"*",8\rrun\r) : AUTOSTART
119
60
  end
120
61
 
121
- def attach_prg(machine, path, autostart:)
122
- bytes = Vic20Cartridge.program_bytes(path)
62
+ # Loads the program, `bytes` from the file at `path`, as Vic20Basic
63
+ # has it once the KERNAL has booted, or attaches the cartridge it's
64
+ # a part of (Vic20Cartridge), and says which.
65
+ def attach_program(machine, path, bytes, autostart:)
123
66
  chips = Vic20Cartridge.chips(path, bytes)
124
- return attach_cartridge(machine, path, chips) if chips
67
+ if chips
68
+ machine.attach_cartridge(chips)
69
+ return "Attached cartridge #{path}"
70
+ end
125
71
 
126
72
  machine.on_init { Vic20Basic.load_program(machine, bytes, autostart:) }
127
73
  "Loading #{path}"
128
74
  end
75
+
76
+ # A tape made for another machine plays all the same, and says so.
77
+ def tape_message(tape, message)
78
+ tape.platform == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the VIC-20"
79
+ end
80
+
81
+ private
82
+
83
+ def expansion_reaching(last)
84
+ BLOCK_EXPANSIONS.each { |reach, ram| return ram if last <= reach }
85
+ BLOCK_EXPANSIONS.last.last
86
+ end
129
87
  end
130
88
  end
131
89
  end
data/lib/badline/media.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/media/extensions"
3
4
  require "badline/media/true_drive"
5
+ require "badline/media/boot_disk"
6
+ require "badline/media/not_disk"
4
7
  require "badline/media/queue"
5
8
  require "badline/media/disk_set"
6
9
  require "badline/media/disk_list"
7
10
  require "badline/media/vic20_basic"
8
11
  require "badline/media/vic20_cartridge"
9
12
  require "badline/media/vic20_media"
13
+ require "badline/media/tune"
14
+ require "badline/media/program"
10
15
 
11
16
  module Badline
12
17
  module Media
@@ -14,6 +19,7 @@ module Badline
14
19
  TAPE_AUTOSTART = %(lO\rrun\r)
15
20
  BASIC_START = 0x0801
16
21
 
22
+ # The storage that opens each of Extensions::MOUNTABLE.
17
23
  MOUNT_TYPES = {
18
24
  ".d64" => Storage::D64Image,
19
25
  ".d71" => Storage::D71Image,
@@ -32,14 +38,21 @@ module Badline
32
38
  # there, a .d64 goes into it instead of the KERNAL traps too. The
33
39
  # autostart then loads through it.
34
40
  #
41
+ # On a C128 in C128 mode, a boot disk goes in a true drive for the
42
+ # KERNAL to boot at power-on, with nothing typed (BootDisk).
43
+ #
35
44
  # An .m3u or .vfl list of disks attaches the first disk it lists
36
- # (DiskList). The VIC-20 takes its own media (Vic20Media), and the
37
- # C128 in C64 mode a C64's.
45
+ # (DiskList). Every machine takes each kind of medium the same way,
46
+ # except that the VIC-20 (Vic20Media) has cartridges, programs and a
47
+ # disk autostart of its own, and takes no .sid tunes. The C128 in C64
48
+ # mode takes a C64's.
38
49
  def attach(computer, path, autostart: true, subtune: nil, **)
39
50
  path = DiskList.disk(path)
40
- return Vic20Media.attach(computer, path, autostart:, **) if computer.family == :vic20
51
+ if computer.family == :vic20 && Vic20Media::UNSUPPORTED.include?(File.extname(path).downcase)
52
+ raise ArgumentError, "#{path} doesn't go in a VIC-20"
53
+ end
41
54
 
42
- attach_c64(computer, path, autostart:, subtune:, **)
55
+ BootDisk.attach(computer, path, **) || attach_kind(computer, path, autostart:, subtune:, **)
43
56
  end
44
57
 
45
58
  # Swaps the disk in device 8 for a disk image or a host directory,
@@ -50,11 +63,12 @@ module Badline
50
63
  # A .g64 goes in a true 1541, which is plugged in as device 8 when
51
64
  # there's none yet (TrueDrive). Once a true 1541 is device 8, a .d64
52
65
  # goes in its drive as well, and other disks, which a 1541 can't
53
- # read, raise TrueDrive::Error, an ArgumentError.
66
+ # read, raise TrueDrive::Error, an ArgumentError. Anything else
67
+ # raises NotDisk.
54
68
  def insert_disk(computer, path, read_only: false)
55
69
  return TrueDrive.insert(computer, path, read_only:) if TrueDrive.takes?(computer, path)
56
70
 
57
- raise ArgumentError, "#{path} is not a disk image or a directory" unless disk?(path)
71
+ raise NotDisk, "#{path} is not a disk image or a directory" unless disk?(path)
58
72
 
59
73
  computer.mount(open_storage(path, { read_only: }))
60
74
  "Inserted #{path} in device 8"
@@ -67,109 +81,104 @@ module Badline
67
81
  # The SID a machine for `path` should be built with. A .sid tune names
68
82
  # its own; everything else gets `otherwise`.
69
83
  def sid_model(path, otherwise: :mos6581)
70
- return otherwise unless path && File.extname(path).downcase == ".sid"
84
+ return otherwise unless path && Extensions.kind(path) == :tune
71
85
 
72
86
  Storage::SIDFile.new(path).sid_model
73
87
  end
74
88
 
75
- private
89
+ # What the medium at `path` is to `computer`, which attach goes by:
90
+ # :true_drive for a disk that goes in a true drive (TrueDrive),
91
+ # :directory, :storage for a disk image or a .t64 that the traps
92
+ # mount, and otherwise its kind by its extension (Extensions).
93
+ def kind(computer, path)
94
+ return :true_drive if TrueDrive.takes?(computer, path)
95
+ return :directory if File.directory?(path)
76
96
 
77
- def attach_c64(computer, path, autostart:, subtune:, **options)
78
- if TrueDrive.takes?(computer, path)
79
- attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
80
- elsif File.directory?(path)
81
- computer.mount(Storage::HostDirectory.new(path))
82
- "Mounted #{path} as device 8"
83
- elsif File.extname(path).downcase == ".crt"
84
- attach_cartridge(computer, path, options.fetch(:cartridge, {}))
85
- elsif File.extname(path).downcase == ".sid"
86
- attach_sid(computer, path, autostart:, subtune:)
87
- elsif File.extname(path).downcase == ".tap"
88
- attach_tape(computer, path, autostart:)
89
- elsif MOUNT_TYPES.key?(File.extname(path).downcase)
90
- attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
91
- else
92
- attach_prg(computer, path, autostart:)
93
- end
97
+ kind = Extensions.kind(path)
98
+ %i[disk archive].include?(kind) ? :storage : kind
94
99
  end
95
100
 
96
- def disk?(path)
97
- storage = MOUNT_TYPES[File.extname(path).downcase]
98
- File.directory?(path) || (!storage.nil? && storage < Storage::DiskImage)
99
- end
100
-
101
- # Disk images take the `disk` options. A .t64 is read-only whatever
102
- # it's given.
103
- def open_storage(path, disk)
101
+ # What device 8 serves through the traps for a host directory, a disk
102
+ # image or a .t64. Disk images take the `disk` options. A .t64 is
103
+ # read-only whatever it's given.
104
+ def open_storage(path, disk = {})
104
105
  return Storage::HostDirectory.new(path) if File.directory?(path)
105
106
 
106
- storage = MOUNT_TYPES[File.extname(path).downcase]
107
+ storage = MOUNT_TYPES.fetch(File.extname(path).downcase)
107
108
  storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
108
109
  end
109
110
 
110
- def attach_cartridge(computer, path, options)
111
- computer.attach_cartridge(Cartridge.from_file(path, **options))
112
- "Attached cartridge #{path}"
111
+ # A program file's bytes, unwrapped from a .p00.
112
+ def program_bytes(path)
113
+ bytes = File.binread(path).bytes
114
+ Storage::P00.wraps?(bytes) ? Storage::P00.data(bytes) : bytes
113
115
  end
114
116
 
115
- def attach_sid(computer, path, autostart:, subtune:)
116
- tune = Storage::SIDFile.new(path)
117
- if tune.sids > 1
118
- raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
119
- end
120
-
121
- subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
122
- computer.on_init { start_tune(computer, tune, autostart:, subtune:) }
123
- title = tune.name.empty? ? path : tune.name
124
- title += " (subtune #{subtune})" if tune.subtunes > 1
125
- autostart ? "Playing #{title}" : "Loaded #{title}"
126
- end
117
+ private
127
118
 
128
- def start_tune(computer, tune, autostart:, subtune:)
129
- computer.ram.write(tune.load_address, tune.data)
130
- tune.boot_memory(subtune:).each { |address, bytes| computer.ram.write(address, bytes) }
131
- computer.type_text(tune.boot_command) if autostart
119
+ def attach_kind(computer, path, autostart:, subtune:, **options)
120
+ disk = options.fetch(:disk, {})
121
+ case kind(computer, path)
122
+ when :true_drive then attach_true_drive(computer, path, disk, autostart:)
123
+ when :directory then mount(computer, path, disk, autostart: false)
124
+ when :storage then mount(computer, path, disk, autostart:)
125
+ when :cartridge then attach_cartridge(computer, path, options.fetch(:cartridge, {}))
126
+ when :tune then Tune.attach(computer, path, autostart:, subtune:)
127
+ when :tape then attach_tape(computer, path, autostart:)
128
+ else attach_prg(computer, path, autostart:)
129
+ end
132
130
  end
133
131
 
134
- def attach_tape(computer, path, autostart:)
135
- computer.datasette.insert(Storage::TAP.new(path))
136
- computer.datasette.play!
137
- computer.type_text(TAPE_AUTOSTART) if autostart
138
- "Inserted #{path} in the datasette"
139
- end
132
+ def disk?(path) = File.directory?(path) || Extensions.kind(path) == :disk
140
133
 
141
134
  def attach_true_drive(computer, path, disk, autostart:)
142
135
  message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
143
- computer.type_text(AUTOSTART) if autostart
136
+ computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
144
137
  message
145
138
  end
146
139
 
147
- def attach_storage(computer, path, disk, autostart:)
148
- computer.mount(open_storage(path, disk))
149
- computer.type_text(AUTOSTART) if autostart
140
+ def mount(computer, path, disk, autostart:)
141
+ storage = open_storage(path, disk)
142
+ computer.mount(storage)
143
+ computer.type_text(disk_autostart(computer) { storage.read_file("*") }) if autostart
150
144
  "Mounted #{path} as device 8"
151
145
  end
152
146
 
153
- def attach_prg(computer, path, autostart:)
154
- bytes = File.binread(path).bytes
155
- bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
156
- computer.on_init { start_prg(computer, bytes, autostart:) }
157
- "Loading #{path}"
147
+ # What loads and runs the first program on the disk in device 8. A
148
+ # VIC-20 goes by that program, which the block gives
149
+ # (Vic20Media.disk_autostart).
150
+ def disk_autostart(computer)
151
+ computer.family == :vic20 ? Vic20Media.disk_autostart(yield) : AUTOSTART
152
+ end
153
+
154
+ # A VIC-20 cartridge is its ROM chips.
155
+ def attach_cartridge(computer, path, options)
156
+ if computer.family == :vic20
157
+ computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
158
+ else
159
+ computer.attach_cartridge(Cartridge.from_file(path, **options))
160
+ end
161
+ "Attached cartridge #{path}"
158
162
  end
159
163
 
160
- def start_prg(computer, data, autostart:)
161
- load_addr = computer.load_prg(data)
162
- # Run only makes sense for programs at BASIC start, or a byte ahead
163
- # of it, where BASIC keeps the zero before its first line. BASIC 7.0
164
- # starts at $1C01 and keeps the end of its text at $1210.
165
- start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
166
- return unless autostart && (load_addr == start || load_addr == start - 1)
167
-
168
- # The end of the program, where BASIC's variables start, and where
169
- # the KERNAL's LOAD leaves its end address.
170
- end_addr = load_addr + data.length - 2
171
- [text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
172
- computer.type_text("run\r")
164
+ # A VIC-20 says when the tape was made for another machine
165
+ # (Vic20Media.tape_message).
166
+ def attach_tape(computer, path, autostart:)
167
+ tape = Storage::TAP.new(path)
168
+ computer.datasette.insert(tape)
169
+ computer.datasette.play!
170
+ computer.type_text(TAPE_AUTOSTART) if autostart
171
+ message = "Inserted #{path} in the datasette"
172
+ computer.family == :vic20 ? Vic20Media.tape_message(tape, message) : message
173
+ end
174
+
175
+ # A VIC-20 has programs of its own (Vic20Media.attach_program).
176
+ def attach_prg(computer, path, autostart:)
177
+ bytes = program_bytes(path)
178
+ return Vic20Media.attach_program(computer, path, bytes, autostart:) if computer.family == :vic20
179
+
180
+ computer.on_init { Program.load(computer, bytes, autostart:) }
181
+ "Loading #{path}"
173
182
  end
174
183
  end
175
184
  end
@@ -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 Options
5
7
  # One event of --at or --script: what to do, with its argument, once
@@ -23,7 +25,7 @@ module Badline
23
25
 
24
26
  # What insert takes: disk images, tapes and cartridges, or an .m3u
25
27
  # or .vfl list of disks or a directory to mount as device 8.
26
- INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
28
+ INSERTABLE = Media::Extensions.of(%i[disk gcr archive tape cartridge disk_list])
27
29
 
28
30
  attr_reader :frame, :action, :argument
29
31
 
@@ -95,7 +97,7 @@ module Badline
95
97
  def self.insertable?(path)
96
98
  return true if File.directory?(path)
97
99
 
98
- File.exist?(path) && (INSERTABLE.include?(File.extname(path).downcase) || Media::DiskList.list?(path))
100
+ File.exist?(path) && INSERTABLE.include?(File.extname(path).downcase)
99
101
  end
100
102
  end
101
103
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/storage/p00"
4
+
3
5
  module Badline
4
6
  class Options
5
7
  # The machine family: the subcommand that names it, or else the media,
@@ -37,15 +39,7 @@ module Badline
37
39
  return if @family_named || @sid_command || @media_path.nil?
38
40
  return unless %w[.prg .p00].include?(File.extname(@media_path).downcase) && File.file?(@media_path)
39
41
 
40
- @family = :c128 if load_address(@media_path) == C128_BASIC_START
41
- end
42
-
43
- # The load address of a .prg, or of the program a .p00 wraps after its
44
- # 26-byte header.
45
- def load_address(path)
46
- offset = File.extname(path).casecmp?(".p00") ? 26 : 0
47
- bytes = File.binread(path).bytes
48
- bytes.size >= offset + 2 ? bytes[offset] | (bytes[offset + 1] << 8) : -1
42
+ @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
49
43
  end
50
44
  end
51
45
  end
@@ -61,7 +61,8 @@ module Badline
61
61
  pause menu loads. Device 8 answers through traps on the KERNAL's
62
62
  disk routines, unless --true-drive puts an emulated 1541 there,
63
63
  which runs its own DOS and reads .d64 and .g64 images only, or on
64
- the C128 the C128D's 1571, which reads .d71 and .g71 too. A .sid
64
+ the C128 the C128D's 1571, which reads .d71 and .g71 too. A .d81
65
+ then goes in an emulated 1581 in its place. A .sid
65
66
  tune opens in the SID player, as `sid` plays it, unless an option
66
67
  of the emulator's window asks for the machine.
67
68
 
@@ -70,6 +70,15 @@ Downloaded from
70
70
  https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1571/1571-rom.310654-05.bin
71
71
  SHA-1 f1be619c106641a685f6609e4d43d6fc9eac1e70.
72
72
 
73
+ dos1581.rom
74
+ -----------
75
+ The 1581 disk drive's DOS, 318045-02, 32 KB for $8000-$FFFF.
76
+ Drive1581 loads it.
77
+
78
+ Downloaded from
79
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1581/1581-rom.318045-02.bin
80
+ SHA-1 01228eae6f066bd9b7b2b6a7fa3f667e41dad393.
81
+
73
82
  Commodore VIC-20 ROMs
74
83
  =====================
75
84
 
Binary file