badline 0.9.0 → 0.10.0

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +39 -4
  5. data/doc/pinned-behaviour.md +146 -0
  6. data/doc/snapshots.md +13 -0
  7. data/lib/badline/address_bus/pla.rb +130 -0
  8. data/lib/badline/address_bus.rb +9 -88
  9. data/lib/badline/chrout_trap.rb +4 -3
  10. data/lib/badline/computer/attachments.rb +47 -7
  11. data/lib/badline/computer/saved_state.rb +1 -0
  12. data/lib/badline/computer.rb +44 -29
  13. data/lib/badline/control_ports.rb +10 -3
  14. data/lib/badline/core.rb +3 -0
  15. data/lib/badline/datasette.rb +39 -1
  16. data/lib/badline/drive1541/serial_port.rb +5 -8
  17. data/lib/badline/drive1541/sleep.rb +5 -6
  18. data/lib/badline/drive1541.rb +3 -3
  19. data/lib/badline/frontend/app.rb +62 -64
  20. data/lib/badline/frontend/boot.rb +33 -8
  21. data/lib/badline/frontend/controls.rb +12 -6
  22. data/lib/badline/frontend/drive_led.rb +9 -10
  23. data/lib/badline/frontend/frame_rate.rb +4 -4
  24. data/lib/badline/frontend/machine_window.rb +87 -0
  25. data/lib/badline/frontend/menu_media.rb +49 -24
  26. data/lib/badline/frontend/menu_pages.rb +23 -40
  27. data/lib/badline/frontend/menu_ports.rb +69 -0
  28. data/lib/badline/frontend/pacer.rb +6 -5
  29. data/lib/badline/frontend/pause_menu.rb +30 -33
  30. data/lib/badline/frontend/player_screen.rb +1 -1
  31. data/lib/badline/frontend/player_window.rb +1 -1
  32. data/lib/badline/frontend/screen.rb +39 -37
  33. data/lib/badline/frontend/sid_view.rb +1 -1
  34. data/lib/badline/frontend/sound.rb +11 -9
  35. data/lib/badline/frontend/timeline.rb +36 -5
  36. data/lib/badline/frontend.rb +2 -0
  37. data/lib/badline/iec_bus.rb +16 -28
  38. data/lib/badline/kernal_trap/drive.rb +3 -0
  39. data/lib/badline/kernal_trap/file.rb +1 -1
  40. data/lib/badline/kernal_trap/layout.rb +112 -0
  41. data/lib/badline/kernal_trap/load.rb +16 -27
  42. data/lib/badline/kernal_trap/routine.rb +11 -27
  43. data/lib/badline/kernal_trap/save.rb +13 -23
  44. data/lib/badline/kernal_trap/serial.rb +21 -19
  45. data/lib/badline/kernal_trap.rb +1 -0
  46. data/lib/badline/keyboard.rb +17 -14
  47. data/lib/badline/machine.rb +60 -0
  48. data/lib/badline/media/vic20_basic.rb +54 -0
  49. data/lib/badline/media/vic20_cartridge.rb +74 -0
  50. data/lib/badline/media/vic20_media.rb +132 -0
  51. data/lib/badline/media.rb +32 -18
  52. data/lib/badline/options/event.rb +5 -3
  53. data/lib/badline/options/help.rb +11 -3
  54. data/lib/badline/options/table.rb +11 -1
  55. data/lib/badline/options/validation.rb +55 -4
  56. data/lib/badline/options.rb +33 -33
  57. data/lib/badline/roms/README +27 -0
  58. data/lib/badline/roms/vic20/basic.rom +0 -0
  59. data/lib/badline/roms/vic20/character.rom +0 -0
  60. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  61. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  62. data/lib/badline/snapshot/container.rb +2 -0
  63. data/lib/badline/snapshot/image.rb +15 -0
  64. data/lib/badline/snapshot/state.rb +4 -3
  65. data/lib/badline/snapshot.rb +10 -0
  66. data/lib/badline/storage/crt_file.rb +16 -5
  67. data/lib/badline/storage/disk_image.rb +3 -0
  68. data/lib/badline/storage/host_directory.rb +3 -0
  69. data/lib/badline/storage/t64.rb +3 -0
  70. data/lib/badline/storage/tap.rb +38 -4
  71. data/lib/badline/storage/unavailable.rb +2 -0
  72. data/lib/badline/timing.rb +12 -0
  73. data/lib/badline/version.rb +1 -1
  74. data/lib/badline/via/shift_register.rb +43 -16
  75. data/lib/badline/vic/bank.rb +53 -16
  76. data/lib/badline/vic/registers.rb +20 -4
  77. data/lib/badline/vic.rb +34 -15
  78. data/lib/badline/vic20/attachments.rb +103 -0
  79. data/lib/badline/vic20/bus.rb +308 -0
  80. data/lib/badline/vic20/cpu.rb +10 -0
  81. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  82. data/lib/badline/vic20/port_wiring.rb +50 -0
  83. data/lib/badline/vic20/saved_state.rb +152 -0
  84. data/lib/badline/vic20/sound/output.rb +83 -0
  85. data/lib/badline/vic20/sound.rb +233 -0
  86. data/lib/badline/vic20/user_via_ports.rb +47 -0
  87. data/lib/badline/vic20/vic/painter.rb +239 -0
  88. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  89. data/lib/badline/vic20/vic/text_window.rb +71 -0
  90. data/lib/badline/vic20/vic.rb +267 -0
  91. data/lib/badline/vic20.rb +220 -0
  92. metadata +28 -2
  93. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -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)
@@ -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
 
@@ -7,6 +7,14 @@ module Badline
7
7
  # The names Badline::Model builds, in the order the help lists them.
8
8
  MODELS = %w[c64 c64c ntsc newntsc oldntsc drean sx64].freeze
9
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
17
+
10
18
  SECTIONS = ["Options", "Window options", "Options without the window", "Testing options"].freeze
11
19
 
12
20
  # Every option of both builds, in the order the help lists them.
@@ -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
@@ -10,7 +10,9 @@ module Badline
10
10
  # The command line of both builds, badline-ruby and the native badline.
11
11
  # Either opens the window for any media, or with --headless or
12
12
  # --audio-out plays or renders a .sid tune without one. `sid`, given
13
- # first, plays .sid tunes and directories of them in the terminal. The
13
+ # first, plays .sid tunes and directories of them in the terminal.
14
+ # `c64` or `vic20`, given first, names the machine family, and without
15
+ # either the family is the C64. The
14
16
  # native build adds --no-sound and --version, and badline-ruby adds
15
17
  # --disable-jit.
16
18
  #
@@ -37,6 +39,13 @@ module Badline
37
39
 
38
40
  REU_SIZES = %w[128 256 512 1024 2048 4096 8192 16384].freeze
39
41
 
42
+ # The VIC-20's RAM expansions as --ram names them (RAM_NAMES), and the
43
+ # names Vic20::Bus::RAM_CONFIGURATIONS gives them.
44
+ RAM_CONFIGURATIONS = {
45
+ "unexpanded" => :unexpanded, "3k" => :"3k", "8k" => :"8k", "16k" => :"16k", "24k" => :"24k",
46
+ "32k" => :"32k", "all" => :all
47
+ }.freeze
48
+
40
49
  # A Float as OptionParser took one: digits with an optional sign,
41
50
  # fraction and exponent.
42
51
  DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
@@ -46,6 +55,13 @@ module Badline
46
55
  attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
47
56
  :audio_out, :seconds, :songlengths, :filter_chunk, :timeline
48
57
 
58
+ # The machine family to build (Badline::Machine), :c64 or :vic20.
59
+ attr_reader :family
60
+
61
+ # The VIC-20's RAM expansion --ram names, a key of
62
+ # Vic20::Bus::RAM_CONFIGURATIONS, or nil without --ram.
63
+ attr_reader :ram
64
+
49
65
  def self.parse(argv, native: false) = new(native:).parse(argv)
50
66
 
51
67
  # The native build plays the SID in the window unless --no-sound says
@@ -55,12 +71,14 @@ module Badline
55
71
  @native = native
56
72
  @program = native ? "badline" : "badline-ruby"
57
73
  @media_path = nil
74
+ @family = :c64
58
75
  @sid_command = false
59
76
  @tune_paths = []
60
77
  @subtune = nil
61
78
  @sid_model = nil
62
79
  @models = []
63
80
  @reu = nil
81
+ @ram = nil
64
82
  @autostart = true
65
83
  @writable = false
66
84
  @sound = native
@@ -90,7 +108,7 @@ module Badline
90
108
 
91
109
  def parse(argv)
92
110
  args = argv.dup
93
- sid_command(args) if args.first == "sid"
111
+ subcommand(args)
94
112
  argument(args.shift, args) until args.empty?
95
113
  play_lone_tune
96
114
  validate unless help? || version?
@@ -149,9 +167,9 @@ module Badline
149
167
 
150
168
  def jit? = @jit
151
169
 
152
- # The name of the C64 to build (Badline::Model). --ntsc is short for
153
- # --model ntsc.
154
- def model = @models.empty? ? MODELS.first : @models.first
170
+ # The name of the model to build: a C64 Badline::Model names, or a
171
+ # VIC-20 of VIC20_MODELS. --ntsc is short for --model ntsc.
172
+ def model = @models.first || (@family == :vic20 ? VIC20_MODELS.first : MODELS.first)
155
173
 
156
174
  def help? = @help
157
175
 
@@ -165,15 +183,21 @@ module Badline
165
183
  [:both, @native ? :native : :ruby].include?(option.build)
166
184
  end
167
185
 
168
- def sid_command(args)
186
+ # Takes `sid`, `c64` or `vic20` when given first.
187
+ def subcommand(args)
188
+ case args.first
189
+ when "sid" then @sid_command = true
190
+ when "c64" then @family = :c64
191
+ when "vic20" then @family = :vic20
192
+ else return
193
+ end
169
194
  args.shift
170
- @sid_command = true
171
195
  end
172
196
 
173
197
  # A .sid on its own plays in the SID player, as `sid` would, unless an
174
198
  # option of the emulator's window asks for the machine.
175
199
  def play_lone_tune
176
- return if @sid_command || headless? || !@window_only.empty? || @media_path.nil?
200
+ return if @sid_command || @family == :vic20 || headless? || !@window_only.empty? || @media_path.nil?
177
201
 
178
202
  @sid_command = File.extname(@media_path).casecmp?(".sid")
179
203
  end
@@ -226,6 +250,7 @@ module Badline
226
250
  when "--sid" then @sid_model = sid_model_for(value)
227
251
  when "--model" then @models << value
228
252
  when "--reu" then @reu = reu_size(value)
253
+ when "--ram" then @ram = ram_configuration(value)
229
254
  when "--frames" then @frames = number(flag, value)
230
255
  when "--screenshot" then @screenshot = value
231
256
  when "--save-snapshot" then @save_snapshot = value
@@ -259,31 +284,6 @@ module Badline
259
284
  end
260
285
  end
261
286
 
262
- def sid_model_for(value)
263
- return if value == "auto"
264
- raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
265
-
266
- SID_MODELS[value]
267
- end
268
-
269
- def reu_size(value)
270
- raise Error, "invalid argument: --reu #{value}" unless REU_SIZES.include?(value)
271
-
272
- value.to_i
273
- end
274
-
275
- def number(flag, value)
276
- raise Error, "invalid argument: #{flag} #{value}" unless value.match?(/\A\d+\z/)
277
-
278
- value.to_i
279
- end
280
-
281
- def decimal(flag, value)
282
- raise Error, "invalid argument: #{flag} #{value}" unless value.match?(DECIMAL)
283
-
284
- value.to_f
285
- end
286
-
287
287
  def media_argument(arg)
288
288
  raise Error, "unexpected argument: #{arg}" unless @sid_command || @media_path.nil?
289
289
 
@@ -36,3 +36,30 @@ Both halves downloaded from
36
36
  https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1541/
37
37
  (1541-c000.325302-01.bin and 1541-e000.901229-05.bin) and concatenated.
38
38
  SHA-1 ab16f56989b27d89babe5f89c5a8cb3da71a82f0.
39
+
40
+ Commodore VIC-20 ROMs
41
+ =====================
42
+
43
+ Downloaded from
44
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/vic20/
45
+ Vic20::Bus maps them.
46
+
47
+ vic20/basic.rom
48
+ ---------------
49
+ VIC-20 BASIC V2, 901486-01, for $C000-$DFFF.
50
+ SHA-1 587d1e90950675ab6b12d91248a3f0d640d02e8d.
51
+
52
+ vic20/kernal-pal.rom
53
+ --------------------
54
+ The PAL KERNAL, 901486-07, for $E000-$FFFF.
55
+ SHA-1 ce0137ed69f003a299f43538fa9eee27898e621e.
56
+
57
+ vic20/kernal-ntsc.rom
58
+ ---------------------
59
+ The NTSC KERNAL, 901486-06, for $E000-$FFFF.
60
+ SHA-1 06de7ec017a5e78bd6746d89c2ecebb646efeb19.
61
+
62
+ vic20/character.rom
63
+ -------------------
64
+ The character generator ROM, 901460-03, for $8000-$8FFF.
65
+ SHA-1 4fd85ab6647ee2ac7ba40f729323f2472d35b9b4.
Binary file
@@ -26,6 +26,8 @@ module Badline
26
26
  FORMAT_MINOR = 0
27
27
  # x64sc names its machine C64SC, and refuses a snapshot naming another.
28
28
  MACHINE = "C64SC"
29
+ # xvic names its machine VIC20.
30
+ VIC20 = "VIC20"
29
31
  GZIP_MAGIC = "\x1f\x8b".b
30
32
 
31
33
  # The VICE version block, when VICE wrote the file: four version
@@ -28,11 +28,16 @@ module Badline
28
28
  # modules badline reads. Yields a line for each thing left out, or
29
29
  # warns it without a block.
30
30
  def load(&)
31
+ return load_vic20(&) if vic20?
32
+
31
33
  computer = badline? ? opening { Computer.restored(state) } : Vice.setup(@container).build
32
34
  tell(badline? ? state_report(computer) : Vice::Restore.apply(@container, computer), &)
33
35
  computer
34
36
  end
35
37
 
38
+ # Whether the machine is a VIC-20, as the container names it.
39
+ def vic20? = @container.machine == Container::VIC20
40
+
36
41
  # Restores `computer` from the snapshot, and returns the Report. A
37
42
  # snapshot that fails leaves the machine as it was.
38
43
  def restore(computer, &)
@@ -45,6 +50,16 @@ module Badline
45
50
 
46
51
  def section = @container[MachineState::NAME]
47
52
 
53
+ # A VIC-20 restores from its BADLINE module alone. xvic's own modules
54
+ # aren't read.
55
+ def load_vic20(&)
56
+ raise FormatError, "badline reads only its own VIC-20 snapshots, not xvic's" unless badline?
57
+
58
+ machine = Vic20.restored(state)
59
+ tell(state_report(machine), &)
60
+ machine
61
+ end
62
+
48
63
  def tell(report)
49
64
  report.ignored.each { |line| block_given? ? yield(line) : warn("badline: #{line}") }
50
65
  end
@@ -14,9 +14,10 @@ module Badline
14
14
  # The layout the chips write their fields in. It goes up whenever a
15
15
  # save_state writes something else, so a State in another layout
16
16
  # fails before anything is read into a machine. Layout 3, without the
17
- # KERNAL and datasette in the setup, still reads.
18
- SCHEMA = 4
19
- READABLE = [3, SCHEMA].freeze
17
+ # KERNAL and datasette in the setup, and layout 4, without the VIA
18
+ # shift register's clock delay, still read.
19
+ SCHEMA = 5
20
+ READABLE = [3, 4, SCHEMA].freeze
20
21
 
21
22
  attr_reader :values, :strings
22
23
 
@@ -27,8 +27,12 @@ module Badline
27
27
  path
28
28
  end
29
29
 
30
+ # A VIC-20's snapshot holds the BADLINE module alone, in a container
31
+ # named as xvic names its machine.
30
32
  def dump(computer)
31
33
  state = computer.snapshot
34
+ return Container.new([MachineState.section(state)], machine: Container::VIC20).to_s if computer.family == :vic20
35
+
32
36
  Container.new(Vice.export(state) + [MachineState.section(state)]).to_s
33
37
  end
34
38
 
@@ -49,4 +53,10 @@ module Badline
49
53
 
50
54
  def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
51
55
  end
56
+
57
+ class Vic20
58
+ def save_snapshot(path) = Snapshot.save(self, path)
59
+
60
+ def restore_snapshot(path, &) = Snapshot.restore(self, path, &)
61
+ end
52
62
  end