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
@@ -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
  #
@@ -44,16 +46,22 @@ module Badline
44
46
  # +press_caps_lock+, +release_caps_lock+, +press_display_key+,
45
47
  # +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
46
48
  # C128D's 1571, its true drive.
49
+ #
50
+ # Each machine includes Machine::Clocking for the clocking part.
47
51
  module Machine
52
+ # Media the machine named doesn't take.
53
+ class Unsupported < ArgumentError; end
54
+
48
55
  # A new machine of `family`, :c64, :vic20 or :c128, the model of it
49
56
  # `model` names. A C64's model is one of Model::ALL, with its SID
50
57
  # `sid_model` when not nil and an REU of `reu` K when not nil. A
51
58
  # VIC-20's is "pal", with the RAM expansion `ram` names
52
59
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
53
- # 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.
54
62
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
55
63
  return vic20(model, ram) if family == :vic20
56
- return C128.new(model:, sid_model:) if family == :c128
64
+ return c128(model, sid_model, :c64) if family == :c128
57
65
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
58
66
 
59
67
  profile = Model.named(model)
@@ -62,11 +70,59 @@ module Badline
62
70
  kernal: profile.kernal, datasette: profile.datasette, board: profile.board)
63
71
  end
64
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
+
65
93
  def self.vic20(model, ram)
66
94
  raise ArgumentError, "no VIC-20 model named #{model}" unless model == "pal"
67
95
 
68
96
  Vic20.new(ram: ram || :unexpanded)
69
97
  end
70
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?
71
127
  end
72
128
  end
@@ -15,7 +15,7 @@ module Badline
15
15
  module BootDisk
16
16
  SIGNATURE = "CBM".bytes.freeze
17
17
  AUTO_BOOT = "copyright cbm 86"
18
- TYPES = %w[.d64 .d71 .d81].freeze
18
+ TYPES = Extensions.of(%i[disk])
19
19
 
20
20
  class << self
21
21
  def takes?(computer, path)
@@ -38,7 +38,7 @@ module Badline
38
38
  type = File.extname(path).downcase
39
39
  return false unless TYPES.include?(type) && File.file?(path)
40
40
 
41
- image = MOUNT_TYPES.fetch(type).new(path, read_only: true)
41
+ image = Media.open_storage(path, { read_only: true })
42
42
  block = image.read_block(1, 0)
43
43
  return true if block && block[0, 3] == SIGNATURE
44
44
 
@@ -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,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
@@ -32,9 +32,10 @@ module Badline
32
32
  # other disk media, which the traps would mount, when a true drive
33
33
  # is device 8.
34
34
  def takes?(computer, path)
35
- return true if %w[.g64 .g71].include?(File.extname(path).downcase)
35
+ kind = Extensions.kind(path)
36
+ return true if kind == :gcr
36
37
 
37
- !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))
38
39
  end
39
40
 
40
41
  # Puts the image in the drive on device 8, plugging one in that
@@ -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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/media/extensions"
3
4
  require "badline/media/true_drive"
4
5
  require "badline/media/boot_disk"
5
6
  require "badline/media/not_disk"
@@ -9,6 +10,8 @@ require "badline/media/disk_list"
9
10
  require "badline/media/vic20_basic"
10
11
  require "badline/media/vic20_cartridge"
11
12
  require "badline/media/vic20_media"
13
+ require "badline/media/tune"
14
+ require "badline/media/program"
12
15
 
13
16
  module Badline
14
17
  module Media
@@ -16,6 +19,7 @@ module Badline
16
19
  TAPE_AUTOSTART = %(lO\rrun\r)
17
20
  BASIC_START = 0x0801
18
21
 
22
+ # The storage that opens each of Extensions::MOUNTABLE.
19
23
  MOUNT_TYPES = {
20
24
  ".d64" => Storage::D64Image,
21
25
  ".d71" => Storage::D71Image,
@@ -38,13 +42,17 @@ module Badline
38
42
  # KERNAL to boot at power-on, with nothing typed (BootDisk).
39
43
  #
40
44
  # An .m3u or .vfl list of disks attaches the first disk it lists
41
- # (DiskList). The VIC-20 takes its own media (Vic20Media), and the
42
- # 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.
43
49
  def attach(computer, path, autostart: true, subtune: nil, **)
44
50
  path = DiskList.disk(path)
45
- 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
46
54
 
47
- BootDisk.attach(computer, path, **) || attach_c64(computer, path, autostart:, subtune:, **)
55
+ BootDisk.attach(computer, path, **) || attach_kind(computer, path, autostart:, subtune:, **)
48
56
  end
49
57
 
50
58
  # Swaps the disk in device 8 for a disk image or a host directory,
@@ -73,109 +81,104 @@ module Badline
73
81
  # The SID a machine for `path` should be built with. A .sid tune names
74
82
  # its own; everything else gets `otherwise`.
75
83
  def sid_model(path, otherwise: :mos6581)
76
- return otherwise unless path && File.extname(path).downcase == ".sid"
84
+ return otherwise unless path && Extensions.kind(path) == :tune
77
85
 
78
86
  Storage::SIDFile.new(path).sid_model
79
87
  end
80
88
 
81
- 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)
82
96
 
83
- def attach_c64(computer, path, autostart:, subtune:, **options)
84
- if TrueDrive.takes?(computer, path)
85
- attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
86
- elsif File.directory?(path)
87
- computer.mount(Storage::HostDirectory.new(path))
88
- "Mounted #{path} as device 8"
89
- elsif File.extname(path).downcase == ".crt"
90
- attach_cartridge(computer, path, options.fetch(:cartridge, {}))
91
- elsif File.extname(path).downcase == ".sid"
92
- attach_sid(computer, path, autostart:, subtune:)
93
- elsif File.extname(path).downcase == ".tap"
94
- attach_tape(computer, path, autostart:)
95
- elsif MOUNT_TYPES.key?(File.extname(path).downcase)
96
- attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
97
- else
98
- attach_prg(computer, path, autostart:)
99
- end
97
+ kind = Extensions.kind(path)
98
+ %i[disk archive].include?(kind) ? :storage : kind
100
99
  end
101
100
 
102
- def disk?(path)
103
- storage = MOUNT_TYPES[File.extname(path).downcase]
104
- File.directory?(path) || (!storage.nil? && storage < Storage::DiskImage)
105
- end
106
-
107
- # Disk images take the `disk` options. A .t64 is read-only whatever
108
- # it's given.
109
- 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 = {})
110
105
  return Storage::HostDirectory.new(path) if File.directory?(path)
111
106
 
112
- storage = MOUNT_TYPES[File.extname(path).downcase]
107
+ storage = MOUNT_TYPES.fetch(File.extname(path).downcase)
113
108
  storage < Storage::DiskImage ? storage.new(path, **disk) : storage.new(path)
114
109
  end
115
110
 
116
- def attach_cartridge(computer, path, options)
117
- computer.attach_cartridge(Cartridge.from_file(path, **options))
118
- "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
119
115
  end
120
116
 
121
- def attach_sid(computer, path, autostart:, subtune:)
122
- tune = Storage::SIDFile.new(path)
123
- if tune.sids > 1
124
- raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
125
- end
126
-
127
- subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
128
- computer.on_init { start_tune(computer, tune, autostart:, subtune:) }
129
- title = tune.name.empty? ? path : tune.name
130
- title += " (subtune #{subtune})" if tune.subtunes > 1
131
- autostart ? "Playing #{title}" : "Loaded #{title}"
132
- end
117
+ private
133
118
 
134
- def start_tune(computer, tune, autostart:, subtune:)
135
- computer.ram.write(tune.load_address, tune.data)
136
- tune.boot_memory(subtune:).each { |address, bytes| computer.ram.write(address, bytes) }
137
- 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
138
130
  end
139
131
 
140
- def attach_tape(computer, path, autostart:)
141
- computer.datasette.insert(Storage::TAP.new(path))
142
- computer.datasette.play!
143
- computer.type_text(TAPE_AUTOSTART) if autostart
144
- "Inserted #{path} in the datasette"
145
- end
132
+ def disk?(path) = File.directory?(path) || Extensions.kind(path) == :disk
146
133
 
147
134
  def attach_true_drive(computer, path, disk, autostart:)
148
135
  message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
149
- computer.type_text(AUTOSTART) if autostart
136
+ computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
150
137
  message
151
138
  end
152
139
 
153
- def attach_storage(computer, path, disk, autostart:)
154
- computer.mount(open_storage(path, disk))
155
- 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
156
144
  "Mounted #{path} as device 8"
157
145
  end
158
146
 
159
- def attach_prg(computer, path, autostart:)
160
- bytes = File.binread(path).bytes
161
- bytes = Storage::P00.data(bytes) if Storage::P00.wraps?(bytes)
162
- computer.on_init { start_prg(computer, bytes, autostart:) }
163
- "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}"
164
162
  end
165
163
 
166
- def start_prg(computer, data, autostart:)
167
- load_addr = computer.load_prg(data)
168
- # Run only makes sense for programs at BASIC start, or a byte ahead
169
- # of it, where BASIC keeps the zero before its first line. BASIC 7.0
170
- # starts at $1C01 and keeps the end of its text at $1210.
171
- start, text_end = computer.family == :c128 && computer.mode == :c128 ? [0x1c01, 0x1210] : [BASIC_START, 0x2d]
172
- return unless autostart && (load_addr == start || load_addr == start - 1)
173
-
174
- # The end of the program, where BASIC's variables start, and where
175
- # the KERNAL's LOAD leaves its end address.
176
- end_addr = load_addr + data.length - 2
177
- [text_end, 0xae].each { |pointer| computer.ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
178
- 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}"
179
182
  end
180
183
  end
181
184
  end