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
@@ -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
 
@@ -27,6 +27,30 @@ Downloaded from
27
27
  https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/kernal.sx.251104-04.bin
28
28
  SHA-1 aa136e91ecf3c5ac64f696b3dbcbfc5ba0871c98.
29
29
 
30
+ kernal-pet64.rom
31
+ ----------------
32
+ The KERNAL of the PET 64, 4064 and Educator 64, 901246-01. It boots
33
+ with the "**** COMMODORE 4064 BASIC V2.0 ****" banner and a white
34
+ cursor, and its interrupt routine keeps the border, background and
35
+ sprite colours black unless bit 7 of $0286 is set. Computer.new(kernal:
36
+ :pet64) fits it, as --model pet64 does.
37
+
38
+ Downloaded from
39
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/kernal.4064.901246-01.bin
40
+ MD5 da92801e3a03b005b746a4dd0b639c7c.
41
+
42
+ c64gs.rom
43
+ ---------
44
+ The C64GS's BASIC and KERNAL, 390852-01, 16 KB: BASIC for $A000-$BFFF
45
+ followed by the KERNAL for $E000-$FFFF. Its BASIC is 901226-01's but
46
+ for 30 unused bytes. Its KERNAL is 901227-03's with most changes in
47
+ $F72C-$FC92, the cassette routines' place, and many jumps redirected to
48
+ I/O error 5. Computer.new(kernal: :gs) fits both, as --model c64gs does.
49
+
50
+ Downloaded from
51
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/c64/64gs.390852-01.bin
52
+ MD5 049e657afafbdd802e8c6a4f88f3d730.
53
+
30
54
  dos1541.rom
31
55
  -----------
32
56
  The 1541 disk drive's DOS 2.6, 16 KB for $C000-$FFFF: 325302-01
@@ -36,3 +60,30 @@ Both halves downloaded from
36
60
  https://www.zimmers.net/anonftp/pub/cbm/firmware/drives/new/1541/
37
61
  (1541-c000.325302-01.bin and 1541-e000.901229-05.bin) and concatenated.
38
62
  SHA-1 ab16f56989b27d89babe5f89c5a8cb3da71a82f0.
63
+
64
+ Commodore VIC-20 ROMs
65
+ =====================
66
+
67
+ Downloaded from
68
+ https://www.zimmers.net/anonftp/pub/cbm/firmware/computers/vic20/
69
+ Vic20::Bus maps them.
70
+
71
+ vic20/basic.rom
72
+ ---------------
73
+ VIC-20 BASIC V2, 901486-01, for $C000-$DFFF.
74
+ SHA-1 587d1e90950675ab6b12d91248a3f0d640d02e8d.
75
+
76
+ vic20/kernal-pal.rom
77
+ --------------------
78
+ The PAL KERNAL, 901486-07, for $E000-$FFFF.
79
+ SHA-1 ce0137ed69f003a299f43538fa9eee27898e621e.
80
+
81
+ vic20/kernal-ntsc.rom
82
+ ---------------------
83
+ The NTSC KERNAL, 901486-06, for $E000-$FFFF.
84
+ SHA-1 06de7ec017a5e78bd6746d89c2ecebb646efeb19.
85
+
86
+ vic20/character.rom
87
+ -------------------
88
+ The character generator ROM, 901460-03, for $8000-$8FFF.
89
+ SHA-1 4fd85ab6647ee2ac7ba40f729323f2472d35b9b4.
Binary file
Binary file
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
@@ -3,32 +3,36 @@
3
3
  module Badline
4
4
  module Snapshot
5
5
  # How a machine was built: its VIC, CIA and SID models, its region, its
6
- # RAM expansion, the size of its REU, its KERNAL and whether it has a
7
- # datasette, the arguments Computer.new takes. A State starts with them, so a machine to restore it into can
8
- # be built first.
9
- Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu, :kernal, :datasette)
6
+ # RAM expansion, the size of its REU, its KERNAL, whether it has a
7
+ # datasette and its board, the arguments Computer.new takes. A State
8
+ # starts with them, so a machine to restore it into can be built first.
9
+ Setup = Data.define(:vic_model, :cia_model, :sid_model, :region, :ram_expansion, :reu, :kernal, :datasette,
10
+ :board)
10
11
 
11
12
  class Setup
12
13
  SID_MODELS = %i[mos6581 mos8580].freeze
13
14
  REGIONS = [Region::PAL, Region::NTSC, Region::NTSC_OLD, Region::DREAN].freeze
14
15
  RAM_EXPANSIONS = [:none, *RAMExpansion::TYPES.keys].freeze
15
16
  KERNALS = AddressBus::ROMs::KERNALS.keys.freeze
17
+ BOARDS = AddressBus::Fittings::BOARDS
16
18
 
17
19
  def self.of(address_bus)
18
20
  new(vic_model: address_bus.vic.model, cia_model: address_bus.cia1.model, sid_model: address_bus.sid.model,
19
21
  region: address_bus.region, ram_expansion: address_bus.ram_expansion.type, reu: address_bus.reu&.size_kb,
20
- kernal: address_bus.kernal, datasette: address_bus.datasette.connected?)
22
+ kernal: address_bus.kernal, datasette: address_bus.datasette.connected?, board: address_bus.board)
21
23
  end
22
24
 
23
- # Layout 3 has no KERNAL or datasette, and stands for the C64's.
25
+ # Layout 3 has no KERNAL or datasette, and stands for the C64's, and
26
+ # layouts before 6 have no board, and stand for the C64's.
24
27
  def self.read(input)
25
28
  new(vic_model: VIC::MODELS.fetch(input.int), cia_model: CIA::MODELS.fetch(input.int),
26
29
  sid_model: SID_MODELS.fetch(input.int), region: REGIONS.fetch(input.int),
27
30
  ram_expansion: ram_expansion(RAM_EXPANSIONS.fetch(input.int)), reu: reu(input.int),
28
31
  kernal: input.schema > 3 ? KERNALS.fetch(input.int) : :c64,
29
- datasette: input.schema > 3 ? input.boolean? : true)
32
+ datasette: input.schema > 3 ? input.boolean? : true,
33
+ board: input.schema > 5 ? BOARDS.fetch(input.int) : :c64)
30
34
  rescue IndexError
31
- raise FormatError, "the state names a chip model, region, RAM expansion or KERNAL badline doesn't know"
35
+ raise FormatError, "the state names a chip model, region, RAM expansion, KERNAL or board badline doesn't know"
32
36
  end
33
37
 
34
38
  def self.ram_expansion(name) = name == :none ? nil : name
@@ -45,12 +49,12 @@ module Badline
45
49
  out.int(VIC::MODELS.index(vic_model)).int(CIA::MODELS.index(cia_model))
46
50
  out.int(SID_MODELS.index(sid_model)).int(REGIONS.index(region))
47
51
  out.int(RAM_EXPANSIONS.index(ram_expansion || :none)).int(reu || 0)
48
- out.int(KERNALS.index(kernal)).boolean(datasette)
52
+ out.int(KERNALS.index(kernal)).boolean(datasette).int(BOARDS.index(board))
49
53
  end
50
54
 
51
55
  # A machine built this way, at power-on.
52
56
  def build
53
- Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:, kernal:, datasette:)
57
+ Computer.new(vic_model:, cia_model:, sid_model:, region:, ram_expansion:, reu:, kernal:, datasette:, board:)
54
58
  end
55
59
 
56
60
  # The chips, led by the model's name when they make one of
@@ -63,6 +67,7 @@ module Badline
63
67
  parts << "a #{reu}K REU" if reu
64
68
  parts << "the #{kernal} KERNAL" unless kernal == :c64
65
69
  parts << "no datasette" unless datasette
70
+ parts << "the #{board} board" unless board == :c64
66
71
  parts.join(", ")
67
72
  end
68
73
  end
@@ -14,9 +14,11 @@ 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, layout 4, without the VIA shift
18
+ # register's clock delay, and layout 5, without the board in the
19
+ # setup, still read.
20
+ SCHEMA = 6
21
+ READABLE = [3, 4, 5, SCHEMA].freeze
20
22
 
21
23
  attr_reader :values, :strings
22
24
 
@@ -69,7 +69,8 @@ module Badline
69
69
  reu = nil unless reu && REU1764.reads?(reu)
70
70
  Setup.new(vic_model: model, cia_model: model == :mos8565 ? :mos6526a : :mos6526, sid_model: sid,
71
71
  region: vic ? VICII.region(vic) : Region::PAL, ram_expansion: nil,
72
- reu: reu ? REU1764.size_kb(reu) : nil, kernal: :c64, datasette: true)
72
+ reu: reu ? REU1764.size_kb(reu) : nil, kernal: :c64, datasette: true,
73
+ board: :c64)
73
74
  end
74
75
  end
75
76
  end
@@ -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
@@ -6,13 +6,14 @@ module Badline
6
6
  class FormatError < StandardError; end
7
7
 
8
8
  SIGNATURE = "C64 CARTRIDGE ".b
9
+ VIC20_SIGNATURE = "VIC20 CARTRIDGE ".b
9
10
  CHIP_SIGNATURE = "CHIP".b
10
11
  CHIP_HEADER_SIZE = 0x10
11
12
  RAM_CHIP = 1
12
13
 
13
14
  Chip = Data.define(:chip_type, :bank, :address, :data)
14
15
 
15
- attr_reader :hardware_type, :subtype, :exrom, :game, :name, :chips
16
+ attr_reader :machine, :hardware_type, :subtype, :exrom, :game, :name, :chips
16
17
 
17
18
  # What .write needs of an image: the fields a parsed CRTFile reads.
18
19
  Image = Data.define(:hardware_type, :subtype, :exrom, :game, :name, :chips)
@@ -33,16 +34,19 @@ module Badline
33
34
  header + packets.join
34
35
  end
35
36
 
36
- # `bytes` stands in for the file's contents.
37
- def initialize(path = nil, bytes: nil)
37
+ # `bytes` stands in for the file's contents. `machine` is the one the
38
+ # image has to be for, :c64 or :vic20, or nil for either.
39
+ def initialize(path = nil, bytes: nil, machine: :c64)
38
40
  parse(bytes || File.binread(path))
41
+ return if machine.nil? || @machine == machine
42
+
43
+ raise FormatError, "Cartridge image is for the #{@machine}, not the #{machine}"
39
44
  end
40
45
 
41
46
  private
42
47
 
43
48
  def parse(bytes)
44
- raise FormatError, "Missing CRT signature" unless bytes.start_with?(SIGNATURE)
45
-
49
+ @machine = signed_machine(bytes)
46
50
  @hardware_type = bytes[0x16, 2].unpack1("n")
47
51
  @exrom = bytes.getbyte(0x18)
48
52
  @game = bytes.getbyte(0x19)
@@ -51,6 +55,13 @@ module Badline
51
55
  @chips = parse_chips(bytes, bytes[0x10, 4].unpack1("N"))
52
56
  end
53
57
 
58
+ def signed_machine(bytes)
59
+ return :c64 if bytes.start_with?(SIGNATURE)
60
+ return :vic20 if bytes.start_with?(VIC20_SIGNATURE)
61
+
62
+ raise FormatError, "Missing CRT signature"
63
+ end
64
+
54
65
  # Trailing bytes too short to hold a CHIP header are ignored.
55
66
  def parse_chips(bytes, offset)
56
67
  chips = []
@@ -36,6 +36,9 @@ module Badline
36
36
  @errors = split_error_table
37
37
  end
38
38
 
39
+ # The host file's path, as it was opened.
40
+ attr_reader :path
41
+
39
42
  # Whether the image was opened write-protected.
40
43
  def read_only? = @read_only
41
44
 
@@ -10,6 +10,9 @@ module Badline
10
10
  NO_SYNC = 21
11
11
  ID = "DIR".bytes.freeze
12
12
 
13
+ # The directory's path, as it was opened.
14
+ attr_reader :path
15
+
13
16
  def initialize(path)
14
17
  @path = path
15
18
  end
@@ -20,6 +20,9 @@ module Badline
20
20
  raise FormatError, "Missing T64 signature" unless @bytes[0, 3] == SIGNATURE
21
21
  end
22
22
 
23
+ # The host file's path, as it was opened.
24
+ attr_reader :path
25
+
23
26
  # What Storage.reopen needs to open the archive again: its path and
24
27
  # its bytes.
25
28
  def save_setup(out)
@@ -3,9 +3,9 @@
3
3
  module Badline
4
4
  module Storage
5
5
  # Raw datasette pulse stream. The header holds the signature, the
6
- # version at $0c and the data size at $10; the data runs from $14, every
7
- # byte the time to the next falling edge on the tape read line, in units
8
- # of 8 cycles.
6
+ # version at $0c, the platform at $0d (0 C64, 1 VIC-20, 2 C16) and the
7
+ # data size at $10; the data runs from $14, every byte the time to the
8
+ # next falling edge on the tape read line, in units of 8 cycles.
9
9
  class TAP
10
10
  class FormatError < StandardError; end
11
11
 
@@ -14,7 +14,14 @@ module Badline
14
14
  OVERFLOW = 0x100 * 8
15
15
 
16
16
  # The file it was read from, expanded, and the file's bytes.
17
- attr_reader :version, :path, :bytes
17
+ attr_reader :version, :platform, :path, :bytes
18
+
19
+ # A blank version 1 tape for `platform` (0 C64, 1 VIC-20) at `path`,
20
+ # written to the host file.
21
+ def self.create(path, platform:)
22
+ File.binwrite(path, (SIGNATURE + [1, platform, 0, 0, 0, 0, 0, 0]).pack("C*"))
23
+ new(path)
24
+ end
18
25
 
19
26
  # `bytes` stands in for the host file's contents, as a snapshot
20
27
  # restores the tape it holds.
@@ -26,6 +33,8 @@ module Badline
26
33
  @version = @bytes[0x0c].to_i
27
34
  raise FormatError, "Unsupported TAP version #{@version}" if @version > 1
28
35
 
36
+ @platform = @bytes[0x0d].to_i
37
+
29
38
  @end = tape_end
30
39
  rewind
31
40
  end
@@ -43,6 +52,31 @@ module Badline
43
52
 
44
53
  def end? = @pos >= @end
45
54
 
55
+ # Records a pulse `cycles` long at the position, in place of the rest
56
+ # of the tape: a byte of units of 8 cycles, or a zero and the 24-bit
57
+ # cycle count for a long one (a lone zero, an overflow, on a version 0
58
+ # tape).
59
+ def record_pulse(cycles)
60
+ units = (cycles + 4) / 8
61
+ encoded = if units.between?(1, 0xff) then [units]
62
+ elsif version.zero? then [0]
63
+ else
64
+ count = [cycles, 0xffffff].min
65
+ [0, count & 0xff, (count >> 8) & 0xff, count >> 16]
66
+ end
67
+ @bytes.slice!(@pos..)
68
+ @bytes.concat(encoded)
69
+ @pos = @end = @bytes.length
70
+ end
71
+
72
+ # Writes the tape back to its host file, with the data size the
73
+ # header states.
74
+ def save
75
+ size = @bytes.length - HEADER_SIZE
76
+ @bytes[0x10, 4] = [size & 0xff, (size >> 8) & 0xff, (size >> 16) & 0xff, size >> 24]
77
+ File.binwrite(@path, @bytes.pack("C*"))
78
+ end
79
+
46
80
  # Cycles until the next falling edge, or nil once the tape has run out.
47
81
  def next_pulse
48
82
  return if end?
@@ -6,6 +6,8 @@ module Badline
6
6
  # (Snapshot::StateReader): it finds no files and refuses every write
7
7
  # as a write-protected disk does.
8
8
  class Unavailable
9
+ def path = ""
10
+
9
11
  def read_file(_name, **) = nil
10
12
 
11
13
  def write_file(_name, _bytes, **)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ # = Timing
5
+ #
6
+ # What a front end needs to run a machine's frames and show them: the CPU
7
+ # clock, the raster's cycles per line and lines per frame, and the
8
+ # [left, top, width, height] of the raster it shows, and how many of the
9
+ # window's square pixels wide each of its pixels shows. Each machine
10
+ # derives it from its region.
11
+ Timing = Data.define(:clock_hz, :cycles_per_line, :lines_per_frame, :crop, :pixel_width)
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.1"
5
5
  end