badline 0.9.0 → 0.10.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/library.md +15 -1
  6. data/doc/pinned-behaviour.md +146 -0
  7. data/doc/snapshots.md +17 -3
  8. data/lib/badline/address_bus/fittings.rb +38 -0
  9. data/lib/badline/address_bus/pla.rb +160 -0
  10. data/lib/badline/address_bus/roms.rb +12 -5
  11. data/lib/badline/address_bus.rb +15 -90
  12. data/lib/badline/chrout_trap.rb +4 -3
  13. data/lib/badline/computer/attachments.rb +47 -7
  14. data/lib/badline/computer/saved_state.rb +1 -0
  15. data/lib/badline/computer.rb +52 -37
  16. data/lib/badline/control_ports.rb +10 -3
  17. data/lib/badline/core.rb +3 -0
  18. data/lib/badline/datasette.rb +39 -1
  19. data/lib/badline/drive1541/serial_port.rb +5 -8
  20. data/lib/badline/drive1541/sleep.rb +5 -6
  21. data/lib/badline/drive1541.rb +3 -3
  22. data/lib/badline/frontend/app.rb +62 -64
  23. data/lib/badline/frontend/boot.rb +33 -8
  24. data/lib/badline/frontend/controls.rb +12 -6
  25. data/lib/badline/frontend/drive_led.rb +9 -10
  26. data/lib/badline/frontend/frame_rate.rb +4 -4
  27. data/lib/badline/frontend/machine_window.rb +87 -0
  28. data/lib/badline/frontend/menu_media.rb +49 -24
  29. data/lib/badline/frontend/menu_pages.rb +23 -40
  30. data/lib/badline/frontend/menu_ports.rb +69 -0
  31. data/lib/badline/frontend/pacer.rb +6 -5
  32. data/lib/badline/frontend/pause_menu.rb +30 -33
  33. data/lib/badline/frontend/player_screen.rb +1 -1
  34. data/lib/badline/frontend/player_window.rb +1 -1
  35. data/lib/badline/frontend/screen.rb +39 -37
  36. data/lib/badline/frontend/sid_view.rb +1 -1
  37. data/lib/badline/frontend/sound.rb +11 -9
  38. data/lib/badline/frontend/timeline.rb +36 -5
  39. data/lib/badline/frontend.rb +2 -0
  40. data/lib/badline/iec_bus.rb +16 -28
  41. data/lib/badline/kernal_trap/drive.rb +3 -0
  42. data/lib/badline/kernal_trap/file.rb +1 -1
  43. data/lib/badline/kernal_trap/layout.rb +112 -0
  44. data/lib/badline/kernal_trap/load.rb +16 -27
  45. data/lib/badline/kernal_trap/routine.rb +11 -27
  46. data/lib/badline/kernal_trap/save.rb +13 -23
  47. data/lib/badline/kernal_trap/serial.rb +21 -19
  48. data/lib/badline/kernal_trap.rb +1 -0
  49. data/lib/badline/keyboard.rb +28 -14
  50. data/lib/badline/machine.rb +60 -0
  51. data/lib/badline/media/vic20_basic.rb +54 -0
  52. data/lib/badline/media/vic20_cartridge.rb +74 -0
  53. data/lib/badline/media/vic20_media.rb +132 -0
  54. data/lib/badline/media.rb +32 -18
  55. data/lib/badline/model.rb +29 -12
  56. data/lib/badline/options/event.rb +5 -3
  57. data/lib/badline/options/help.rb +11 -3
  58. data/lib/badline/options/table.rb +12 -2
  59. data/lib/badline/options/validation.rb +55 -4
  60. data/lib/badline/options.rb +33 -33
  61. data/lib/badline/roms/README +51 -0
  62. data/lib/badline/roms/c64gs.rom +0 -0
  63. data/lib/badline/roms/kernal-pet64.rom +0 -0
  64. data/lib/badline/roms/vic20/basic.rom +0 -0
  65. data/lib/badline/roms/vic20/character.rom +0 -0
  66. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  67. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  68. data/lib/badline/snapshot/container.rb +2 -0
  69. data/lib/badline/snapshot/image.rb +15 -0
  70. data/lib/badline/snapshot/setup.rb +15 -10
  71. data/lib/badline/snapshot/state.rb +5 -3
  72. data/lib/badline/snapshot/vice.rb +2 -1
  73. data/lib/badline/snapshot.rb +10 -0
  74. data/lib/badline/storage/crt_file.rb +16 -5
  75. data/lib/badline/storage/disk_image.rb +3 -0
  76. data/lib/badline/storage/host_directory.rb +3 -0
  77. data/lib/badline/storage/t64.rb +3 -0
  78. data/lib/badline/storage/tap.rb +38 -4
  79. data/lib/badline/storage/unavailable.rb +2 -0
  80. data/lib/badline/timing.rb +12 -0
  81. data/lib/badline/version.rb +1 -1
  82. data/lib/badline/via/shift_register.rb +43 -16
  83. data/lib/badline/vic/bank.rb +55 -17
  84. data/lib/badline/vic/registers.rb +20 -4
  85. data/lib/badline/vic.rb +48 -15
  86. data/lib/badline/vic20/attachments.rb +103 -0
  87. data/lib/badline/vic20/bus.rb +308 -0
  88. data/lib/badline/vic20/cpu.rb +10 -0
  89. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  90. data/lib/badline/vic20/port_wiring.rb +50 -0
  91. data/lib/badline/vic20/saved_state.rb +152 -0
  92. data/lib/badline/vic20/sound/output.rb +83 -0
  93. data/lib/badline/vic20/sound.rb +233 -0
  94. data/lib/badline/vic20/user_via_ports.rb +47 -0
  95. data/lib/badline/vic20/vic/painter.rb +239 -0
  96. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  97. data/lib/badline/vic20/vic/text_window.rb +71 -0
  98. data/lib/badline/vic20/vic.rb +267 -0
  99. data/lib/badline/vic20.rb +220 -0
  100. metadata +31 -2
  101. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A program loading into a VIC-20 once it has booted (Vic20Media).
6
+ module Vic20Basic
7
+ # Where BASIC starts with each RAM expansion it can start in, as
8
+ # TXTTAB holds it.
9
+ BASIC_STARTS = { 0x1001 => :unexpanded, 0x0401 => :"3k", 0x1201 => :"8k" }.freeze
10
+
11
+ class << self
12
+ # A BASIC program, one that loads at a start of BASIC or a byte
13
+ # ahead of it, goes to this machine's start of BASIC. Anything else
14
+ # goes where it says, and doesn't RUN.
15
+ def load_program(machine, data, autostart:)
16
+ load_addr = data[0] | (data[1] << 8)
17
+ basic_start = machine.basic_start
18
+ ahead = [0, 1].find { |offset| BASIC_STARTS.key?(load_addr + offset) || load_addr + offset == basic_start }
19
+ return machine.load_prg(data) unless ahead
20
+
21
+ load_basic(machine, data, basic_start - ahead, load_addr)
22
+ machine.type_text("run\r") if autostart
23
+ end
24
+
25
+ private
26
+
27
+ # Puts a BASIC program at `address`, relinking its lines when that
28
+ # isn't where it was saved from, as BASIC's LOAD does, and sets the
29
+ # end of the program, where BASIC's variables start, and the end
30
+ # address the KERNAL's LOAD leaves.
31
+ def load_basic(machine, data, address, saved_at)
32
+ ram = machine.ram
33
+ ram.write(address, data[2..])
34
+ end_addr = address + data.length - 2
35
+ relink(ram, machine.basic_start, end_addr) unless address == saved_at
36
+ [0x2d, 0xae].each { |pointer| ram.write(pointer, [end_addr & 0xff, end_addr >> 8]) }
37
+ end
38
+
39
+ # Points each line's link at the line after it, as BASIC's LINKPRG
40
+ # does: past the zero that ends the line's text. A link with a zero
41
+ # high byte ends the program.
42
+ def relink(ram, line, end_addr)
43
+ while line + 4 < end_addr && ram.peek(line + 1).positive?
44
+ following = line + 4
45
+ following += 1 while following < end_addr && ram.peek(following).positive?
46
+ following += 1
47
+ ram.write(line, [following & 0xff, following >> 8])
48
+ line = following
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # A VIC-20 cartridge kept as programs, one per ROM chip, each loading
6
+ # at the block the chip goes in. The chip in BLK5 at $A000 starts the
7
+ # cartridge, and a bigger cartridge has more in BLK1 to BLK3. The files
8
+ # of one cartridge share a name, with each block's address in its
9
+ # extension (game.a0, game.60) or its name (game-a000.prg,
10
+ # game-6000.prg).
11
+ module Vic20Cartridge
12
+ BLOCKS = [0xa000, 0x2000, 0x4000, 0x6000].freeze
13
+
14
+ class << self
15
+ # The ROM chips, as CRTFile::Chips, of the cartridge `data`, the
16
+ # program at `path`, is a part of, or nil when it isn't one: a
17
+ # program loading at $A000, or one loading at the start of BLK1 to
18
+ # BLK3 with an $A000 part beside it.
19
+ def chips(path, data)
20
+ own = load_address(data)
21
+ return unless BLOCKS.include?(own)
22
+
23
+ parts = BLOCKS.filter_map { |address| address == own ? data : part(path, own, address) }
24
+ return unless parts.any? { |part| load_address(part) == 0xa000 }
25
+
26
+ parts.map { |part| rom_chip(part) }
27
+ end
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
+ private
36
+
37
+ def load_address(data)
38
+ data[0] | (data[1] << 8) if data.length > 2
39
+ end
40
+
41
+ def part(path, own, address)
42
+ name = part_names(path, own, address).find { |candidate| File.file?(candidate) }
43
+ return unless name
44
+
45
+ part = program_bytes(name)
46
+ part if load_address(part) == address
47
+ end
48
+
49
+ # `path` with the block at `own` in its extension or its name
50
+ # swapped for the one at `address`, its letters in either case.
51
+ def part_names(path, own, address)
52
+ extension = File.extname(path)
53
+ base = path[0, path.length - extension.length]
54
+ own_tag = own.to_s(16)
55
+ tags = [address.to_s(16), address.to_s(16).upcase].uniq
56
+ return tags.map { |tag| "#{base}.#{tag[0, 2]}" } if extension.downcase == ".#{own_tag[0, 2]}"
57
+
58
+ name = File.basename(base)
59
+ at = name.downcase.index(own_tag)
60
+ return [] unless at
61
+
62
+ tags.map { |tag| File.join(File.dirname(base), name[0, at] + tag + name[(at + 4)..]) + extension }
63
+ end
64
+
65
+ # A ROM chip of whole pages, the last padded with $FF.
66
+ def rom_chip(part)
67
+ data = part[2..]
68
+ data += [0xff] * (-data.length % 0x100)
69
+ Storage::CRTFile::Chip.new(chip_type: 0, bank: 0, address: load_address(part), data:)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
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.
12
+ module Vic20Media
13
+ # The expansions that fill BLK1, BLK2 and BLK3 one block after
14
+ # another, each with the last address it fills.
15
+ BLOCK_EXPANSIONS = [[0x3fff, :"8k"], [0x5fff, :"16k"], [0x7fff, :"24k"]].freeze
16
+
17
+ # What a VIC-20 doesn't take.
18
+ UNSUPPORTED = %w[.sid].freeze
19
+
20
+ # The platform byte of a VIC-20 tape (Storage::TAP#platform).
21
+ TAPE_PLATFORM = 1
22
+
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
+ # The RAM expansion (Vic20::Bus::RAM_CONFIGURATIONS) a VIC-20 needs
45
+ # for the program at `path`, or for the first program on the disk
46
+ # or in the directory there, by where it loads. A BASIC program
47
+ # needs the expansion its start of BASIC comes with, and more of
48
+ # BLK1 to BLK3 when it runs past $3FFF. So does a machine-language
49
+ # program loading into those blocks. Anything else, cartridges
50
+ # included, runs unexpanded.
51
+ def ram_for(path)
52
+ data = first_program(path)
53
+ return :unexpanded unless data && data.length > 2 && !Vic20Cartridge.chips(path, data)
54
+
55
+ load_addr = data[0] | (data[1] << 8)
56
+ if load_addr == 0x1201 || load_addr.between?(0x2000, 0x7fff)
57
+ expansion_reaching(load_addr + data.length - 3)
58
+ else
59
+ Vic20Basic::BASIC_STARTS.fetch(load_addr, :unexpanded)
60
+ end
61
+ end
62
+
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
+
70
+ 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)
76
+ end
77
+ end
78
+
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.
116
+ def disk_autostart(data)
117
+ basic = data && data.length > 2 && Vic20Basic::BASIC_STARTS.key?(data[0] | (data[1] << 8))
118
+ basic ? %(lO"*",8\rrun\r) : AUTOSTART
119
+ end
120
+
121
+ def attach_prg(machine, path, autostart:)
122
+ bytes = Vic20Cartridge.program_bytes(path)
123
+ chips = Vic20Cartridge.chips(path, bytes)
124
+ return attach_cartridge(machine, path, chips) if chips
125
+
126
+ machine.on_init { Vic20Basic.load_program(machine, bytes, autostart:) }
127
+ "Loading #{path}"
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
data/lib/badline/media.rb CHANGED
@@ -4,6 +4,9 @@ require "badline/media/true_drive"
4
4
  require "badline/media/queue"
5
5
  require "badline/media/disk_set"
6
6
  require "badline/media/disk_list"
7
+ require "badline/media/vic20_basic"
8
+ require "badline/media/vic20_cartridge"
9
+ require "badline/media/vic20_media"
7
10
 
8
11
  module Badline
9
12
  module Media
@@ -30,25 +33,13 @@ module Badline
30
33
  # autostart then loads through it.
31
34
  #
32
35
  # An .m3u or .vfl list of disks attaches the first disk it lists
33
- # (DiskList).
34
- def attach(computer, path, autostart: true, subtune: nil, **options)
36
+ # (DiskList). A machine other than the C64 takes its own media
37
+ # (Vic20Media).
38
+ def attach(computer, path, autostart: true, subtune: nil, **)
35
39
  path = DiskList.disk(path)
36
- if TrueDrive.takes?(computer, path)
37
- attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
38
- elsif File.directory?(path)
39
- computer.mount(Storage::HostDirectory.new(path))
40
- "Mounted #{path} as device 8"
41
- elsif File.extname(path).downcase == ".crt"
42
- attach_cartridge(computer, path, options.fetch(:cartridge, {}))
43
- elsif File.extname(path).downcase == ".sid"
44
- attach_sid(computer, path, autostart:, subtune:)
45
- elsif File.extname(path).downcase == ".tap"
46
- attach_tape(computer, path, autostart:)
47
- elsif MOUNT_TYPES.key?(File.extname(path).downcase)
48
- attach_storage(computer, path, options.fetch(:disk, {}), autostart:)
49
- else
50
- attach_prg(computer, path, autostart:)
51
- end
40
+ return Vic20Media.attach(computer, path, autostart:, **) unless computer.is_a?(Computer)
41
+
42
+ attach_c64(computer, path, autostart:, subtune:, **)
52
43
  end
53
44
 
54
45
  # Swaps the disk in device 8 for a disk image or a host directory,
@@ -69,6 +60,10 @@ module Badline
69
60
  "Inserted #{path} in device 8"
70
61
  end
71
62
 
63
+ # The RAM expansion a VIC-20 for `path` should be built with
64
+ # (Vic20Media.ram_for), a key of Vic20::Bus::RAM_CONFIGURATIONS.
65
+ def vic20_ram_for(path) = Vic20Media.ram_for(path)
66
+
72
67
  # The SID a machine for `path` should be built with. A .sid tune names
73
68
  # its own; everything else gets `otherwise`.
74
69
  def sid_model(path, otherwise: :mos6581)
@@ -79,6 +74,25 @@ module Badline
79
74
 
80
75
  private
81
76
 
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
94
+ end
95
+
82
96
  def disk?(path)
83
97
  storage = MOUNT_TYPES[File.extname(path).downcase]
84
98
  File.directory?(path) || (!storage.nil? && storage < Storage::DiskImage)
data/lib/badline/model.rb CHANGED
@@ -8,39 +8,56 @@ module Badline
8
8
  # a datasette. The names are x64sc's.
9
9
  module Model
10
10
  # Each field but the name is a keyword Computer.new takes.
11
- Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette)
11
+ Profile = Data.define(:name, :vic_model, :cia_model, :sid_model, :region, :kernal, :datasette, :board)
12
12
 
13
13
  # The breadbin PAL C64: the 6569 VIC-II, 6526 CIAs and the 6581 SID.
14
14
  C64 = Profile.new(name: "c64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
15
- region: Region::PAL, kernal: :c64, datasette: true)
15
+ region: Region::PAL, kernal: :c64, datasette: true, board: :c64)
16
16
 
17
17
  # The PAL C64C: the HMOS 8565 VIC-II, 6526A CIAs and the 8580 SID.
18
18
  C64C = Profile.new(name: "c64c", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
19
- region: Region::PAL, kernal: :c64, datasette: true)
19
+ region: Region::PAL, kernal: :c64, datasette: true, board: :c64)
20
20
 
21
21
  # The NTSC C64 with the 6567R8 VIC-II.
22
22
  NTSC = Profile.new(name: "ntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
23
- region: Region::NTSC, kernal: :c64, datasette: true)
23
+ region: Region::NTSC, kernal: :c64, datasette: true, board: :c64)
24
24
 
25
25
  # The NTSC C64C: the 8562, the 6567R8's HMOS successor, with the
26
26
  # C64C's CIAs and SID.
27
27
  NEW_NTSC = Profile.new(name: "newntsc", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
28
- region: Region::NTSC, kernal: :c64, datasette: true)
28
+ region: Region::NTSC, kernal: :c64, datasette: true, board: :c64)
29
29
 
30
30
  # The first NTSC C64s, with the 6567R56A VIC-II.
31
31
  OLD_NTSC = Profile.new(name: "oldntsc", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
32
- region: Region::NTSC_OLD, kernal: :c64, datasette: true)
32
+ region: Region::NTSC_OLD, kernal: :c64, datasette: true, board: :c64)
33
33
 
34
34
  # The Drean C64 of Argentina, on PAL-N with the 6572 VIC-II.
35
35
  DREAN = Profile.new(name: "drean", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
36
- region: Region::DREAN, kernal: :c64, datasette: true)
36
+ region: Region::DREAN, kernal: :c64, datasette: true, board: :c64)
37
37
 
38
38
  # The portable SX-64: a PAL C64's chips with its own KERNAL, a 1541
39
39
  # built in as device 8 and no cassette port.
40
40
  SX64 = Profile.new(name: "sx64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
41
- region: Region::PAL, kernal: :sx64, datasette: false)
41
+ region: Region::PAL, kernal: :sx64, datasette: false, board: :c64)
42
42
 
43
- ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64].freeze
43
+ # The PET 64, or Educator 64: a PAL C64's board in a PET's case, with
44
+ # its own KERNAL and a green monochrome monitor built in.
45
+ PET64 = Profile.new(name: "pet64", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
46
+ region: Region::PAL, kernal: :pet64, datasette: true, board: :pet64)
47
+
48
+ # The C64GS games console: a PAL C64C's board with its own BASIC and
49
+ # KERNAL, which show an animation asking for a cartridge when it has
50
+ # none, in a case with no keyboard and no cassette port.
51
+ C64GS = Profile.new(name: "c64gs", vic_model: :mos8565, cia_model: :mos6526a, sid_model: :mos8580,
52
+ region: Region::PAL, kernal: :gs, datasette: false, board: :gs)
53
+
54
+ # The MAX Machine, or Ultimax, of Japan: the 6566, an NTSC VIC-II
55
+ # for static RAM, which runs here as the first NTSC C64s' 6567R56A,
56
+ # one CIA, 2K of RAM and no ROMs, so it runs only a cartridge's.
57
+ ULTIMAX = Profile.new(name: "ultimax", vic_model: :mos6569, cia_model: :mos6526, sid_model: :mos6581,
58
+ region: Region::NTSC_OLD, kernal: :c64, datasette: true, board: :max)
59
+
60
+ ALL = [C64, C64C, NTSC, NEW_NTSC, OLD_NTSC, DREAN, SX64, PET64, C64GS, ULTIMAX].freeze
44
61
 
45
62
  def self.named(name)
46
63
  model = ALL.find { |candidate| candidate.name == name }
@@ -49,13 +66,13 @@ module Badline
49
66
  model
50
67
  end
51
68
 
52
- # The model whose chips, region, KERNAL and datasette `machine` has, or
53
- # nil for none.
69
+ # The model whose chips, region, KERNAL, datasette and board `machine`
70
+ # has, or nil for none.
54
71
  def self.of(machine)
55
72
  ALL.find do |model|
56
73
  model.vic_model == machine.vic_model && model.cia_model == machine.cia_model &&
57
74
  model.sid_model == machine.sid_model && model.region == machine.region &&
58
- model.kernal == machine.kernal && model.datasette == machine.datasette
75
+ model.kernal == machine.kernal && model.datasette == machine.datasette && model.board == machine.board
59
76
  end
60
77
  end
61
78
  end
@@ -6,10 +6,11 @@ module Badline
6
6
  # `frame` frames have run. An event given at several frames becomes
7
7
  # one Event per frame.
8
8
  class Event
9
- # The actions, and whether each takes an argument.
9
+ # The actions, and whether each takes an argument. menu's page may be
10
+ # left out.
10
11
  ACTIONS = {
11
- "key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true,
12
- "reset" => false, "freeze" => false, "quit" => false
12
+ "key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true, "menu" => true,
13
+ "reset" => false, "freeze" => false, "resume" => false, "quit" => false
13
14
  }.freeze
14
15
 
15
16
  # The frames a key or the freeze button is held for.
@@ -79,6 +80,7 @@ module Badline
79
80
 
80
81
  def self.valid?(action, argument)
81
82
  return false unless ACTIONS.key?(action)
83
+ return true if action == "menu"
82
84
  return argument.empty? unless ACTIONS[action]
83
85
  return EJECTABLE.include?(argument) if action == "eject"
84
86
  return insertable?(argument) if action == "insert"
@@ -37,11 +37,17 @@ module Badline
37
37
 
38
38
  def banner
39
39
  <<~BANNER
40
- Usage: #{@program} [options] [media]
40
+ Usage: #{@program} [c64] [options] [media]
41
+ #{@program} vic20 [options] [media]
41
42
  #{@program} --headless [options] tune.sid
42
43
  #{@program} [options] tune.sid --audio-out FILE
43
44
  #{@program} sid [options] FILE|DIR...
44
45
 
46
+ Commands:
47
+ c64 Run a C64, the one --model names (the default)
48
+ vic20 Run a PAL VIC-20, with the RAM --ram names
49
+ sid Play .sid tunes and directories of them
50
+
45
51
  Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
46
52
  .g64 disk image for the true 1541, an .m3u or .vfl list of disk
47
53
  images, a .t64 tape archive, a .tap tape, a .crt cartridge, a .sid
@@ -80,8 +86,10 @@ module Badline
80
86
  frames. type=TEXT types it, with \\n for RETURN. insert=FILE swaps
81
87
  in a disk, a list's first disk, a tape or a cartridge, and
82
88
  eject=disk, tape or cartridge takes one out. screenshot=FILE saves
83
- the frame as a .bmp, with the frame's number in place of %d. reset,
84
- freeze and quit take no argument.
89
+ the frame as a .bmp, with the frame's number in place of %d. menu
90
+ opens the pause menu, and menu=PAGE opens it on snapshots, drive,
91
+ datasette, expansion, ports, sound or power. Its frames still count,
92
+ and resume closes it. reset, freeze and quit take no argument.
85
93
  BANNER
86
94
  end
87
95
 
@@ -5,7 +5,15 @@ module Badline
5
5
  DEFAULT_RATE = 44_100
6
6
 
7
7
  # The names Badline::Model builds, in the order the help lists them.
8
- MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64].freeze
8
+ MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64 pet64 c64gs ultimax].freeze
9
+
10
+ # The VIC-20's models, as xvic names its video standards. Only PAL is
11
+ # emulated.
12
+ VIC20_MODELS = %w[pal ntsc].freeze
13
+
14
+ # The VIC-20 RAM expansions --ram takes, in the order the help lists
15
+ # them.
16
+ RAM_NAMES = %w[unexpanded 3k 8k 16k 24k 32k all].freeze
9
17
 
10
18
  SECTIONS = ["Options", "Window options", "Options without the window", "Testing options"].freeze
11
19
 
@@ -28,9 +36,11 @@ module Badline
28
36
  section: "Window options", needs: :window),
29
37
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
30
38
  section: "Window options", needs: :window),
31
- Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first})",
39
+ Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal",
32
40
  section: "Window options", needs: :window),
33
41
  Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
42
+ Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded)",
43
+ section: "Window options", needs: :window),
34
44
  Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
35
45
  section: "Window options", needs: :window),
36
46
  Option.new("--verbose", "Print the display, sound and gamepad setup and the frame timing",
@@ -2,13 +2,45 @@
2
2
 
3
3
  module Badline
4
4
  class Options
5
- # The checks Options#parse makes once every argument has been read:
6
- # the options fit the mode, the numbers are positive and the files
7
- # exist.
5
+ # The checks Options#parse makes: that each value reads as its option
6
+ # takes it, and once every argument has been read, that the options
7
+ # fit the mode, the numbers are positive and the files exist.
8
8
  module Validation
9
9
  private
10
10
 
11
+ def sid_model_for(value)
12
+ return if value == "auto"
13
+ raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
14
+
15
+ SID_MODELS[value]
16
+ end
17
+
18
+ def ram_configuration(value)
19
+ raise Error, "invalid argument: --ram #{value}" unless RAM_CONFIGURATIONS.key?(value)
20
+
21
+ RAM_CONFIGURATIONS[value]
22
+ end
23
+
24
+ def reu_size(value)
25
+ raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
26
+
27
+ value.to_i
28
+ end
29
+
30
+ def number(flag, value)
31
+ raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
32
+
33
+ value.to_i
34
+ end
35
+
36
+ def decimal(flag, value)
37
+ raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
38
+
39
+ value.to_f
40
+ end
41
+
11
42
  def validate
43
+ validate_family
12
44
  validate_mode
13
45
  validate_models
14
46
  validate_numbers
@@ -26,6 +58,24 @@ module Badline
26
58
  end
27
59
  end
28
60
 
61
+ def validate_family
62
+ if @family == :vic20
63
+ validate_vic20
64
+ elsif !@ram.nil?
65
+ raise Error, "--ram needs vic20"
66
+ end
67
+ end
68
+
69
+ # The VIC-20 is a PAL one, in the window, without the C64's SID and
70
+ # REU.
71
+ def validate_vic20
72
+ raise Error, "the NTSC VIC-20 isn't emulated yet" if @models.include?("ntsc")
73
+
74
+ raise Error, "--sid needs the C64" unless @sid_model.nil?
75
+ raise Error, "--reu needs the C64" unless @reu.nil?
76
+ raise Error, "the VIC-20 needs the window" if headless?
77
+ end
78
+
29
79
  # `sid` takes directories as well as tunes.
30
80
  def validate_tune(path)
31
81
  return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
@@ -37,7 +87,8 @@ module Badline
37
87
  # --model and --ntsc may be given more than once, but only for one
38
88
  # model.
39
89
  def validate_models
40
- unknown = @models.find { |name| !MODELS.include?(name) }
90
+ names = @family == :vic20 ? VIC20_MODELS : MODELS
91
+ unknown = @models.find { |name| !names.include?(name) }
41
92
  raise Error, "invalid argument: --model #{unknown}" unless unknown.nil?
42
93
  raise Error, "conflicting models: #{@models.uniq.join(' and ')}" unless @models.uniq.size <= 1
43
94
  end