badline 0.15.1 → 0.16.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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +128 -9
  5. data/doc/media.md +9 -0
  6. data/doc/pinned-behaviour.md +67 -0
  7. data/doc/snapshot-format.md +215 -18
  8. data/doc/snapshots.md +55 -0
  9. data/lib/badline/c64/common.rb +1 -1
  10. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  11. data/lib/badline/chips/crtc.rb +163 -0
  12. data/lib/badline/chips/pia/control_lines.rb +138 -0
  13. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  14. data/lib/badline/chips/pia.rb +138 -0
  15. data/lib/badline/chips/vic/register_log.rb +1 -4
  16. data/lib/badline/core.rb +3 -0
  17. data/lib/badline/drives/ieee_bus.rb +74 -0
  18. data/lib/badline/drives/model1551/cable.rb +5 -2
  19. data/lib/badline/drives/model1551/tia.rb +13 -34
  20. data/lib/badline/drives/model1551.rb +26 -4
  21. data/lib/badline/drives/model2031/ieee_port.rb +133 -0
  22. data/lib/badline/drives/model2031/keyed_state.rb +66 -0
  23. data/lib/badline/drives/model2031/parts.rb +25 -0
  24. data/lib/badline/drives/model2031.rb +92 -0
  25. data/lib/badline/drives.rb +1 -0
  26. data/lib/badline/frontend/app.rb +1 -1
  27. data/lib/badline/frontend/controls.rb +23 -3
  28. data/lib/badline/frontend/gamepads.rb +10 -3
  29. data/lib/badline/frontend/keys.rb +88 -0
  30. data/lib/badline/frontend/media_slots.rb +1 -0
  31. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  32. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  33. data/lib/badline/frontend/menu/menu_pages.rb +24 -2
  34. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  35. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  36. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  37. data/lib/badline/frontend/screens.rb +2 -1
  38. data/lib/badline/frontend/timeline.rb +16 -9
  39. data/lib/badline/frontend.rb +1 -0
  40. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  41. data/lib/badline/kernal_trap/dos.rb +6 -1
  42. data/lib/badline/kernal_trap/frames.rb +117 -0
  43. data/lib/badline/kernal_trap/ieee.rb +151 -0
  44. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  45. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  46. data/lib/badline/kernal_trap/routine.rb +3 -53
  47. data/lib/badline/kernal_trap/serial.rb +4 -102
  48. data/lib/badline/kernal_trap/steps.rb +64 -0
  49. data/lib/badline/kernal_trap.rb +4 -0
  50. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  51. data/lib/badline/machine/settling.rb +1 -1
  52. data/lib/badline/machine.rb +29 -6
  53. data/lib/badline/media/extensions.rb +5 -2
  54. data/lib/badline/media/pet_media.rb +40 -0
  55. data/lib/badline/media/program.rb +14 -2
  56. data/lib/badline/media/true_drive.rb +19 -6
  57. data/lib/badline/media/vic20_media.rb +2 -2
  58. data/lib/badline/media.rb +41 -17
  59. data/lib/badline/options/family.rb +17 -6
  60. data/lib/badline/options/help.rb +23 -3
  61. data/lib/badline/options/table.rb +22 -4
  62. data/lib/badline/options/validation.rb +84 -5
  63. data/lib/badline/options.rb +47 -16
  64. data/lib/badline/pet/bus/expansion.rb +200 -0
  65. data/lib/badline/pet/bus.rb +222 -0
  66. data/lib/badline/pet/canvas.rb +97 -0
  67. data/lib/badline/pet/cassettes.rb +63 -0
  68. data/lib/badline/pet/characters.rb +32 -0
  69. data/lib/badline/pet/cpu.rb +10 -0
  70. data/lib/badline/pet/crtc_video.rb +155 -0
  71. data/lib/badline/pet/discrete_video.rb +83 -0
  72. data/lib/badline/pet/ieee_drive.rb +76 -0
  73. data/lib/badline/pet/ieee_ports.rb +77 -0
  74. data/lib/badline/pet/ieee_traps.rb +80 -0
  75. data/lib/badline/pet/keyboard_ports.rb +74 -0
  76. data/lib/badline/pet/keyed_modules.rb +88 -0
  77. data/lib/badline/pet/keyed_parts.rb +127 -0
  78. data/lib/badline/pet/keyed_state.rb +42 -0
  79. data/lib/badline/pet/model.rb +163 -0
  80. data/lib/badline/pet/rom_sets.rb +89 -0
  81. data/lib/badline/pet/sound.rb +128 -0
  82. data/lib/badline/pet/via.rb +10 -0
  83. data/lib/badline/pet/via_ports.rb +19 -0
  84. data/lib/badline/pet.rb +244 -0
  85. data/lib/badline/plus4/keyed_state.rb +5 -3
  86. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  87. data/lib/badline/plus4.rb +1 -1
  88. data/lib/badline/roms/README +169 -0
  89. data/lib/badline/roms/dos2031.rom +0 -0
  90. data/lib/badline/roms/pet/basic1.rom +0 -0
  91. data/lib/badline/roms/pet/basic2.rom +0 -0
  92. data/lib/badline/roms/pet/basic4.rom +0 -0
  93. data/lib/badline/roms/pet/characters-din.rom +0 -0
  94. data/lib/badline/roms/pet/characters1.rom +0 -0
  95. data/lib/badline/roms/pet/characters2.rom +0 -0
  96. data/lib/badline/roms/pet/editor1.rom +0 -0
  97. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  98. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  99. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  100. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  101. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  102. data/lib/badline/roms/pet/editor4-80-b-50hz-8296.rom +0 -0
  103. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  104. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  105. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  106. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  107. data/lib/badline/roms/pet/kernal1.rom +0 -0
  108. data/lib/badline/roms/pet/kernal2.rom +0 -0
  109. data/lib/badline/roms/pet/kernal4.rom +0 -0
  110. data/lib/badline/snapshot/container.rb +6 -1
  111. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  112. data/lib/badline/snapshot/field_doc.rb +17 -3
  113. data/lib/badline/snapshot/image.rb +28 -10
  114. data/lib/badline/snapshot/pet_setup.rb +88 -0
  115. data/lib/badline/snapshot/storage_setup.rb +7 -1
  116. data/lib/badline/snapshot/vice/pet.rb +117 -0
  117. data/lib/badline/snapshot/vice/pet_cpu.rb +67 -0
  118. data/lib/badline/snapshot/vice/pet_crtc.rb +246 -0
  119. data/lib/badline/snapshot/vice/pet_mem.rb +93 -0
  120. data/lib/badline/snapshot/vice/pet_pias.rb +56 -0
  121. data/lib/badline/snapshot/vice/pet_restore.rb +150 -0
  122. data/lib/badline/snapshot/vice/pet_via.rb +164 -0
  123. data/lib/badline/snapshot/vice.rb +1 -0
  124. data/lib/badline/snapshot.rb +9 -5
  125. data/lib/badline/storage/d80_image.rb +48 -0
  126. data/lib/badline/storage/d82_image.rb +21 -0
  127. data/lib/badline/storage/tap.rb +16 -3
  128. data/lib/badline/storage.rb +2 -0
  129. data/lib/badline/support/pet_checkpoint.rb +38 -0
  130. data/lib/badline/timing.rb +5 -4
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/vic20/attachments.rb +7 -2
  133. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  134. data/lib/badline/vic20/bus.rb +5 -0
  135. data/lib/badline/vic20.rb +1 -1
  136. metadata +70 -1
@@ -7,6 +7,8 @@ module Badline
7
7
  # It loads on its own, for Options.
8
8
  #
9
9
  # - :disk, a disk image that the traps mount and a true drive reads
10
+ # - :ieee_disk, an 8050's or 8250's disk image, for a PET's IEEE-488
11
+ # drive
10
12
  # - :gcr, a disk's raw GCR, which only a true drive reads
11
13
  # - :archive, a .t64, which the traps mount read-only
12
14
  # - :tape, :cartridge and :tune
@@ -14,7 +16,8 @@ module Badline
14
16
  module Extensions
15
17
  KINDS = {
16
18
  ".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
19
+ ".d80" => :ieee_disk, ".d82" => :ieee_disk, ".tap" => :tape, ".crt" => :cartridge, ".sid" => :tune,
20
+ ".m3u" => :disk_list, ".vfl" => :disk_list
18
21
  }.freeze
19
22
 
20
23
  # The kind of the medium at `path`, by its extension.
@@ -27,7 +30,7 @@ module Badline
27
30
  DISKS = of(%i[disk gcr])
28
31
 
29
32
  # What goes in device 8 through the traps.
30
- MOUNTABLE = of(%i[disk archive])
33
+ MOUNTABLE = of(%i[disk archive ieee_disk])
31
34
 
32
35
  # What goes in device 8, a true drive's or the traps', and the lists
33
36
  # of disks to put there.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What a PET does with media of its own (Media.attach). Disks of every
6
+ # kind, .d80 and .d82 among them, directories and .t64 archives mount
7
+ # on device 8 through the traps on the KERNAL's IEEE-488 primitives
8
+ # (PET::IEEETraps), programs load at the start of BASIC, $0401
9
+ # (Program), and tapes play on the datasette at cassette port #1. There
10
+ # is no cartridge port and no SID, and the true drives are serial ones.
11
+ module PETMedia
12
+ # What a PET doesn't take.
13
+ UNSUPPORTED = Extensions.of(%i[tune cartridge])
14
+
15
+ # What loads and runs the first program on the disk in device 8.
16
+ AUTOSTART = %(load"*",8\rrun\r)
17
+
18
+ # What loads and runs the first program on the tape in cassette #1.
19
+ TAPE_AUTOSTART = "load\rrun\r"
20
+
21
+ class << self
22
+ # A tape made for another machine plays all the same, and says so.
23
+ def tape_message(tape, message)
24
+ tape.platform == Storage::TAP::PET_PLATFORM ? message : "#{message}, a tape for another machine than the PET"
25
+ end
26
+
27
+ # BASIC 2 and 4 load the first program off the disk and run it.
28
+ # BASIC 1's LOAD finds no device on the IEEE-488 bus, so the first
29
+ # program, which the block gives, goes in as a program file does,
30
+ # once the KERNAL has booted.
31
+ def disk_autostart(machine)
32
+ return machine.type_text(AUTOSTART) unless machine.model.basic1?
33
+
34
+ data = yield
35
+ machine.on_init { Program.load(machine, data, autostart: true) } if data
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,12 +2,16 @@
2
2
 
3
3
  module Badline
4
4
  module Media
5
- # A program loading into a C64, a C128 or a 264 series machine once it
6
- # has booted (Media.attach). The VIC-20 has its own (Vic20Basic).
5
+ # A program loading into a C64, a C128, a 264 series machine or a PET
6
+ # once it has booted (Media.attach). The VIC-20 has its own
7
+ # (Vic20Basic).
7
8
  module Program
8
9
  # Where BASIC 3.5 starts on the 264 series.
9
10
  PLUS4_BASIC_START = 0x1001
10
11
 
12
+ # Where BASIC starts on every PET.
13
+ PET_BASIC_START = 0x0401
14
+
11
15
  class << self
12
16
  # Loads the program where it says, and RUNs one that loads at the
13
17
  # start of BASIC.
@@ -30,12 +34,20 @@ module Badline
30
34
  # Where BASIC starts, the pointer to the end of its text and the
31
35
  # KERNAL's end address (EAL). BASIC 7.0 starts at $1C01 and keeps
32
36
  # the end of its text at $1210, and the 264 KERNAL keeps EAL at $9D.
37
+ # PET BASIC 2 and 4 keep the end of their text at $2A and EAL at
38
+ # $C9, and BASIC 1 at $7C and $E5.
33
39
  def basic(computer)
34
40
  return [PLUS4_BASIC_START, 0x2d, KernalTrap::PLUS4_ZERO_PAGE.end_address] if computer.family == :plus4
41
+ return pet_basic(computer) if computer.family == :pet
35
42
  return [0x1c01, 0x1210, 0xae] if computer.family == :c128 && computer.mode == :c128
36
43
 
37
44
  [BASIC_START, 0x2d, 0xae]
38
45
  end
46
+
47
+ def pet_basic(computer)
48
+ model = computer.model
49
+ [PET_BASIC_START, model.basic1? ? 0x7c : 0x2a, model.trap_layout.zero_page.end_address]
50
+ end
39
51
  end
40
52
  end
41
53
  end
@@ -4,15 +4,16 @@ module Badline
4
4
  module Media
5
5
  # Device 8 as a true drive instead of the KERNAL traps: a 1541, or on
6
6
  # the C128 its 1571, or on the Plus/4 a 1551 if it asks for one, or a
7
- # 1581. Once a true drive is plugged in as device 8, disk media go into
7
+ # 1581, or on the PET a 2031 on the IEEE-488 bus. Once a true drive is plugged in as device 8, disk media go into
8
8
  # it rather than into the traps, so a LOAD runs the DOS and every byte
9
9
  # crosses the serial bus or TCBM. A .g64 holds the
10
10
  # disk's raw GCR, which only a true drive reads, so it plugs one in
11
11
  # when there's none yet. The 1541 reads only .d64 and .g64 images, the
12
12
  # 1571 .d71 and .g71 as well, and the 1581 only .d81, so a .d81 swaps
13
13
  # the drive on device 8 for a 1581 (Drives::Model1581::Slot), and another disk
14
- # image swaps a 1581 back for the machine's own drive. The traps' other
15
- # disk media, a .t64 or a host directory, raise Error.
14
+ # image swaps a 1581 back for the machine's own drive. The PET has no
15
+ # 1581, and its 2031 reads .d64 and .g64 images only. The traps' other
16
+ # disk media, a .t64, a host directory, or a .d80 or .d82, raise Error.
16
17
  module TrueDrive
17
18
  class Error < ArgumentError; end
18
19
 
@@ -36,7 +37,7 @@ module Badline
36
37
  kind = Extensions.kind(path)
37
38
  return true if kind == :gcr
38
39
 
39
- !drive(computer).nil? && (File.directory?(path) || %i[disk archive].include?(kind))
40
+ !drive(computer).nil? && (File.directory?(path) || %i[disk archive ieee_disk].include?(kind))
40
41
  end
41
42
 
42
43
  # Puts the image in the drive on device 8, plugging one in that
@@ -44,12 +45,15 @@ module Badline
44
45
  # mounted through the traps, so LOAD and SAVE reach the drive too.
45
46
  # `read_only` puts it in write-protected.
46
47
  def insert(computer, path, read_only: false)
47
- return insert_d81(computer, path, read_only) if File.extname(path).casecmp?(".d81")
48
+ pet = computer.family == :pet
49
+ return insert_d81(computer, path, read_only) if !pet && File.extname(path).casecmp?(".d81")
48
50
 
49
51
  drive = own_drive(computer)
50
- name = drive&.model_name || (computer.family == :c128 ? "1571" : "1541")
52
+ name = drive&.model_name || default_model(computer)
51
53
  types = name == "1571" ? %w[.d64 .g64 .d71 .g71] : %w[.d64 .g64]
52
54
  unless types.include?(File.extname(path).downcase)
55
+ raise Error, "#{path} is not a #{types.join(' or ')} image, which the #{name} reads" if pet
56
+
53
57
  raise Error, "#{path} is not a #{types.join(', ')} or .d81 image, which the #{name} and the 1581 read"
54
58
  end
55
59
 
@@ -61,6 +65,15 @@ module Badline
61
65
 
62
66
  private
63
67
 
68
+ # The model plug_true_drive plugs in.
69
+ def default_model(computer)
70
+ case computer.family
71
+ when :c128 then "1571"
72
+ when :pet then "2031"
73
+ else "1541"
74
+ end
75
+ end
76
+
64
77
  # The drive on device 8 unless it's a 1581.
65
78
  def own_drive(computer)
66
79
  drive = drive(computer)
@@ -61,11 +61,11 @@ module Badline
61
61
 
62
62
  # Loads the program, `bytes` from the file at `path`, as Vic20Basic
63
63
  # has it once the KERNAL has booted, or attaches the cartridge it's
64
- # a part of (Vic20Cartridge), and says which.
64
+ # a part of (Vic20Cartridge), named after the file, and says which.
65
65
  def attach_program(machine, path, bytes, autostart:)
66
66
  chips = Vic20Cartridge.chips(path, bytes)
67
67
  if chips
68
- machine.attach_cartridge(chips)
68
+ machine.attach_cartridge(chips, File.basename(path, File.extname(path)))
69
69
  return "Attached cartridge #{path}"
70
70
  end
71
71
 
data/lib/badline/media.rb CHANGED
@@ -11,6 +11,7 @@ require "badline/media/vic20_basic"
11
11
  require "badline/media/vic20_cartridge"
12
12
  require "badline/media/vic20_media"
13
13
  require "badline/media/plus4_media"
14
+ require "badline/media/pet_media"
14
15
  require "badline/media/tune"
15
16
  require "badline/media/program"
16
17
 
@@ -25,7 +26,9 @@ module Badline
25
26
  ".d64" => Storage::D64Image,
26
27
  ".d71" => Storage::D71Image,
27
28
  ".d81" => Storage::D81Image,
28
- ".t64" => Storage::T64
29
+ ".t64" => Storage::T64,
30
+ ".d80" => Storage::D80Image,
31
+ ".d82" => Storage::D82Image
29
32
  }.freeze
30
33
 
31
34
  class << self
@@ -48,8 +51,10 @@ module Badline
48
51
  # disk autostart of its own, and takes no .sid tunes, and the 264
49
52
  # series loads programs at the start of its BASIC (Program), takes
50
53
  # .crt cartridges and raw .bin ROM images in its two cartridges'
51
- # slots (Plus4Media), and no .sid tunes either. The
52
- # C128 in C64 mode takes a C64's.
54
+ # slots (Plus4Media), and no .sid tunes either. The PET mounts .d80
55
+ # and .d82 disks too, which no other machine takes, and has no
56
+ # cartridges, tunes or true drives (PETMedia). The C128 in C64 mode
57
+ # takes a C64's.
53
58
  def attach(computer, path, autostart: true, subtune: nil, **)
54
59
  path = DiskList.disk(path)
55
60
  refuse_unsupported(computer, path)
@@ -99,7 +104,7 @@ module Badline
99
104
  return :cartridge if computer.family == :plus4 && Plus4Media.rom_image?(path)
100
105
 
101
106
  kind = Extensions.kind(path)
102
- %i[disk archive].include?(kind) ? :storage : kind
107
+ %i[disk archive ieee_disk].include?(kind) ? :storage : kind
103
108
  end
104
109
 
105
110
  # What device 8 serves through the traps for a host directory, a disk
@@ -120,14 +125,21 @@ module Badline
120
125
 
121
126
  private
122
127
 
123
- # The VIC-20 and the 264 series have no SID to play a .sid tune on.
128
+ # The VIC-20 and the 264 series have no SID to play a .sid tune on,
129
+ # and only the PET has the IEEE-488 drive a .d80 or .d82 goes in.
124
130
  def refuse_unsupported(computer, path)
125
- return unless Vic20Media::UNSUPPORTED.include?(File.extname(path).downcase)
126
-
127
- case computer.family
128
- when :vic20 then raise ArgumentError, "#{path} doesn't go in a VIC-20"
129
- when :plus4 then raise ArgumentError, "#{path} doesn't go in a Plus/4"
131
+ family = computer.family
132
+ if Extensions.kind(path) == :ieee_disk && family != :pet
133
+ raise ArgumentError, "#{path} needs a PET's IEEE-488 drive"
130
134
  end
135
+
136
+ refused, name = case family
137
+ when :vic20 then [Vic20Media::UNSUPPORTED, "a VIC-20"]
138
+ when :plus4 then [Vic20Media::UNSUPPORTED, "a Plus/4"]
139
+ when :pet then [PETMedia::UNSUPPORTED, "a PET"]
140
+ else [[], ""]
141
+ end
142
+ raise ArgumentError, "#{path} doesn't go in #{name}" if refused.include?(File.extname(path).downcase)
131
143
  end
132
144
 
133
145
  def attach_kind(computer, path, autostart:, subtune:, **options)
@@ -143,18 +155,26 @@ module Badline
143
155
  end
144
156
  end
145
157
 
146
- def disk?(path) = File.directory?(path) || Extensions.kind(path) == :disk
158
+ def disk?(path) = File.directory?(path) || %i[disk ieee_disk].include?(Extensions.kind(path))
147
159
 
148
160
  def attach_true_drive(computer, path, disk, autostart:)
149
161
  message = TrueDrive.insert(computer, path, read_only: disk.fetch(:read_only, false))
150
- computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) }) if autostart
162
+ if autostart && computer.family == :pet
163
+ PETMedia.disk_autostart(computer) { Vic20Media.first_program(path) }
164
+ elsif autostart
165
+ computer.type_text(disk_autostart(computer) { Vic20Media.first_program(path) })
166
+ end
151
167
  message
152
168
  end
153
169
 
154
170
  def mount(computer, path, disk, autostart:)
155
171
  storage = open_storage(path, disk)
156
172
  computer.mount(storage)
157
- computer.type_text(disk_autostart(computer) { storage.read_file("*") }) if autostart
173
+ if autostart && computer.family == :pet
174
+ PETMedia.disk_autostart(computer) { storage.read_file("*") }
175
+ elsif autostart
176
+ computer.type_text(disk_autostart(computer) { storage.read_file("*") })
177
+ end
158
178
  "Mounted #{path} as device 8"
159
179
  end
160
180
 
@@ -170,7 +190,9 @@ module Badline
170
190
  # (Plus4Media.cartridge_chips).
171
191
  def attach_cartridge(computer, path, options)
172
192
  case computer.family
173
- when :vic20 then computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
193
+ when :vic20
194
+ crt = Storage::CRTFile.new(path, machine: :vic20)
195
+ computer.attach_cartridge(crt.chips, crt.name.empty? ? File.basename(path, File.extname(path)) : crt.name)
174
196
  when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
175
197
  else
176
198
  computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
@@ -178,17 +200,19 @@ module Badline
178
200
  "Attached cartridge #{path}"
179
201
  end
180
202
 
181
- # A VIC-20 or a 264 says when the tape was made for another machine
182
- # (Vic20Media.tape_message, Plus4Media.tape_message).
203
+ # A VIC-20, a 264 or a PET says when the tape was made for another
204
+ # machine (Vic20Media.tape_message, Plus4Media.tape_message,
205
+ # PETMedia.tape_message).
183
206
  def attach_tape(computer, path, autostart:)
184
207
  tape = Storage::TAP.new(path)
185
208
  computer.datasette.insert(tape)
186
209
  computer.datasette.play!
187
- computer.type_text(TAPE_AUTOSTART) if autostart
210
+ computer.type_text(computer.family == :pet ? PETMedia::TAPE_AUTOSTART : TAPE_AUTOSTART) if autostart
188
211
  message = "Inserted #{path} in the datasette"
189
212
  case computer.family
190
213
  when :vic20 then Vic20Media.tape_message(tape, message)
191
214
  when :plus4 then Plus4Media.tape_message(tape, message)
215
+ when :pet then PETMedia.tape_message(tape, message)
192
216
  else message
193
217
  end
194
218
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/storage/p00"
4
+ require "badline/storage/tap"
4
5
 
5
6
  module Badline
6
7
  class Options
@@ -12,7 +13,7 @@ module Badline
12
13
 
13
14
  private
14
15
 
15
- # Takes `sid`, `c64`, `vic20`, `c128` or `plus4` when given first.
16
+ # Takes `sid`, `c64`, `vic20`, `c128`, `plus4` or `pet` when given first.
16
17
  def subcommand(args)
17
18
  case args.first
18
19
  when "sid" then @sid_command = true
@@ -20,6 +21,7 @@ module Badline
20
21
  when "vic20" then @family = :vic20
21
22
  when "c128" then @family = :c128
22
23
  when "plus4" then @family = :plus4
24
+ when "pet" then @family = :pet
23
25
  else return
24
26
  end
25
27
  @family_named = !@sid_command
@@ -31,19 +33,28 @@ module Badline
31
33
  when :vic20 then VIC20_MODELS
32
34
  when :c128 then C128_MODELS
33
35
  when :plus4 then PLUS4_MODELS
36
+ when :pet then PET_MODELS
34
37
  else MODELS
35
38
  end
36
39
  end
37
40
 
38
41
  # Without a family named, a program that loads at C128_BASIC_START is
39
- # the C128's.
42
+ # the C128's, and an 8050's or 8250's disk or a PET tape the PET's. A
43
+ # program that loads where PET BASIC starts could be a VIC-20's too.
40
44
  def pick_family
41
- return if @family_named || @sid_command || @media_path.nil?
45
+ return if @family_named || @sid_command || @media_path.nil? || !File.file?(@media_path)
42
46
 
43
- program = Media::Extensions::PROGRAMS.include?(File.extname(@media_path).downcase)
44
- return unless program && File.file?(@media_path)
47
+ if pet_media?
48
+ @family = :pet
49
+ elsif Media::Extensions::PROGRAMS.include?(File.extname(@media_path).downcase)
50
+ @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
51
+ end
52
+ end
45
53
 
46
- @family = :c128 if Storage::P00.load_address(@media_path) == C128_BASIC_START
54
+ # Whether the media is an 8050's or 8250's disk or a PET tape.
55
+ def pet_media?
56
+ kind = Media::Extensions.kind(@media_path)
57
+ kind == :ieee_disk || (kind == :tape && Storage::TAP.platform_of(@media_path) == Storage::TAP::PET_PLATFORM)
47
58
  end
48
59
  end
49
60
  end
@@ -49,7 +49,8 @@ module Badline
49
49
  # wrapped as the banner is.
50
50
  def media_kinds
51
51
  text = "Media can be #{listed(Media::Extensions::PROGRAMS)} program, #{listed(%i[disk])} disk image, " \
52
- "#{listed(%i[gcr])} disk image for a true drive, #{listed(%i[disk_list])} list of disk images, " \
52
+ "#{listed(%i[ieee_disk])} disk image for a PET, #{listed(%i[gcr])} disk image for a true drive, " \
53
+ "#{listed(%i[disk_list])} list of disk images, " \
53
54
  "#{listed(%i[archive])} tape archive, #{listed(%i[tape])} tape, #{listed(%i[cartridge])} " \
54
55
  "cartridge, #{listed(%i[tune])} tune, #{listed(%w[.vsf])} snapshot, or a directory to mount as device 8."
55
56
  wrap(text)
@@ -84,6 +85,7 @@ module Badline
84
85
  #{@program} vic20 [options] [media]
85
86
  #{@program} c128 [options] [media]
86
87
  #{@program} plus4 [options] [media]
88
+ #{@program} pet [options] [media]
87
89
  #{@program} --headless [options] tune.sid
88
90
  #{@program} [options] tune.sid --audio-out FILE
89
91
  #{@program} sid [options] FILE|DIR...
@@ -93,6 +95,7 @@ module Badline
93
95
  vic20 Run a PAL VIC-20, with the RAM --ram names
94
96
  c128 Run a C128 in C128 mode, the one --model names
95
97
  plus4 Run a Plus/4 or a C16, the one --model names
98
+ pet Run a PET or a CBM, the one --model names
96
99
  sid Play .sid tunes and directories of them
97
100
 
98
101
  #{media_kinds}
@@ -116,6 +119,22 @@ module Badline
116
119
  --drive-type 1541 a 1541 on the serial bus instead. There Escape is
117
120
  ESC, Page Down RUN/STOP and F4 HELP.
118
121
 
122
+ The PET and the CBM take programs, disks, .d80 and .d82 among them,
123
+ and tapes, and a .d80, a .d82 or a PET tape starts one without
124
+ `pet`. Their keys type what the host's keys type, Alt with a key
125
+ presses the PET's SHIFT and the key its unshifted character is on,
126
+ and the pause menu's KEYBOARD page says where the rest are. --basic
127
+ and --editor fit the upgrades Commodore sold, as in --model 2001
128
+ --basic 2. BASIC 1 can't LOAD from a disk drive, though a disk still
129
+ starts its first program, and BASIC 2 can. Their --true-drive makes
130
+ device 8 an emulated 2031 on the IEEE-488 bus, which reads .d64 and
131
+ .g64 images. The 8032 and the 8096 have two 50 Hz editors:
132
+ 4-80-b-50hz, the default, and 4-80-b-50hz-0283, another chip's,
133
+ with 64-cycle lines and 313 lines a frame. The 8296 runs its own,
134
+ 4-80-b-50hz-8296, and takes the 8032's too. --characters din fits
135
+ it with the DIN character ROM, and --jumpers ram9,rama puts RAM at
136
+ $9000 and $A000, as xpet's -petram9 and -petramA do.
137
+
119
138
  --headless plays a .sid tune on the host's audio device without the
120
139
  window, and --audio-out renders it to 16-bit PCM instead. The
121
140
  container follows the file's extension, .wav or .aiff. Played on a
@@ -136,8 +155,9 @@ module Badline
136
155
  --at and --script run events once a number of frames has run, as
137
156
  in --at 3500:key=space or --at 250,500:screenshot=shot%05d.bmp.
138
157
  key=NAME presses a key (space, return, a, f1, run_stop, restore, and
139
- on the C128 esc, help, keypad1 and the rest, and on the Plus/4
140
- esc, help and f1 to f3) or a joystick's
158
+ on the C128 esc, help, keypad1 and the rest, on the Plus/4
159
+ esc, help and f1 to f3, and on the PET rvs, escape and kp1) or a
160
+ joystick's
141
161
  direction or fire (joy1-up, joy2-fire) for #{Event::HOLD} frames, and
142
162
  pushes the C128's caps_lock or forty_eighty, which lock. type=TEXT
143
163
  types it, with \\n for RETURN. insert=FILE swaps in a disk, a list's
@@ -18,6 +18,10 @@ module Badline
18
18
  # another name for the C16.
19
19
  PLUS4_MODELS = %w[plus4 plus4ntsc c16 c16ntsc c116].freeze
20
20
 
21
+ # The PETs and CBMs, as PET::Model names them, the 8032 first, the
22
+ # default.
23
+ PET_MODELS = %w[8032 8096 8296 4032 3032 3032b 2001].freeze
24
+
21
25
  # The true drives --drive-type names for the Plus/4.
22
26
  PLUS4_DRIVE_TYPES = %w[1541 1551].freeze
23
27
 
@@ -43,20 +47,34 @@ module Badline
43
47
  section: "Window options", needs: :window),
44
48
  Option.new("--no-sound", "Don't play the SID", section: "Window options", needs: :window, build: :native),
45
49
  Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps, a 1571 on the C128, " \
46
- "a 1551 on the Plus/4",
50
+ "a 1551 on the Plus/4, a 2031 on the PET",
47
51
  section: "Window options", needs: :window),
48
52
  Option.new("--drive-type MODEL", "The Plus/4's true drive: 1541 or 1551 (default: 1551); implies --true-drive",
49
53
  section: "Window options", needs: :window),
50
54
  Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
51
55
  section: "Window options", needs: :window),
52
56
  Option.new("--model NAME", "C64 to run: #{MODELS.join(', ')} (default: #{MODELS.first}); VIC-20: pal; " \
53
- "C128: #{C128_MODELS.join(', ')}; Plus/4: #{PLUS4_MODELS.join(', ')}",
57
+ "C128: #{C128_MODELS.join(', ')}; Plus/4: #{PLUS4_MODELS.join(', ')}; " \
58
+ "PET: #{PET_MODELS.join(', ')}",
54
59
  section: "Window options", needs: :window),
55
60
  Option.new("--ntsc", "Run an NTSC C64, as --model ntsc does", section: "Window options", needs: :window),
56
61
  Option.new("--c64", "Start the C128 in C64 mode, holding C= at power-on",
57
62
  section: "Window options", needs: :window),
58
63
  Option.new("--ram SIZE", "VIC-20 RAM expansion: #{RAM_NAMES.join(', ')} (default: unexpanded); " \
59
- "Plus/4 RAM: 16k, 32k or 64k (default: the model's own)",
64
+ "Plus/4 RAM: 16k, 32k or 64k; PET RAM: 4k, 8k, 16k or 32k (default: the model's own)",
65
+ section: "Window options", needs: :window),
66
+ Option.new("--60hz", "Run a CRTC PET's 60 Hz editor instead of its 50 Hz one",
67
+ section: "Window options", needs: :window),
68
+ Option.new("--basic VERSION", "Fit a PET with BASIC 1, 2 or 4, as its board takes (default: its own)",
69
+ section: "Window options", needs: :window),
70
+ Option.new("--editor NAME", "Fit a PET with an editor ROM its board and BASIC take: " \
71
+ "#{PET::ROMSets::EDITORS.keys.join(', ')}",
72
+ section: "Window options", needs: :window),
73
+ Option.new("--characters NAME", "Fit an 8296 with a character ROM: ascii or din (default: ascii)",
74
+ section: "Window options", needs: :window),
75
+ Option.new("--jumpers LIST", "Fit an 8296's board with jumpers, comma-separated: ram9 and rama for RAM at " \
76
+ "$9000 and $A000, userport for the user port's PA0-PA2 to switch in " \
77
+ "the RAM under the ROMs",
60
78
  section: "Window options", needs: :window),
61
79
  Option.new("--c1lo FILE", "Put a .bin ROM image in the Plus/4's cartridge 1, low slot ($8000)",
62
80
  section: "Window options", needs: :window),
@@ -100,7 +118,7 @@ module Badline
100
118
  section: "Testing options", needs: :window),
101
119
  Option.new("--save-snapshot FILE", "Save the machine as a .vsf snapshot after the last frame",
102
120
  section: "Testing options", needs: :window),
103
- Option.new("--vice", "Add VICE's modules to --save-snapshot's C64 snapshot, for x64sc",
121
+ Option.new("--vice", "Add VICE's modules to --save-snapshot's C64 or PET snapshot, for x64sc or xpet",
104
122
  section: "Testing options", needs: :window),
105
123
  Option.new("--at FRAME:EVENT", "Run EVENT once FRAME frames have run (see above)",
106
124
  section: "Testing options", needs: :window),
@@ -39,6 +39,12 @@ module Badline
39
39
  PLUS4_RAM[value]
40
40
  end
41
41
 
42
+ def pet_ram(value)
43
+ raise Error, "invalid argument: --ram #{value}" unless PET_RAM.key?(value)
44
+
45
+ PET_RAM[value]
46
+ end
47
+
42
48
  def plus4_ram_expansion(value)
43
49
  raise Error, "invalid argument: --ram-expansion #{value}" unless PLUS4_RAM_EXPANSIONS.key?(value)
44
50
 
@@ -92,12 +98,10 @@ module Badline
92
98
  end
93
99
 
94
100
  def validate_family
95
- if @family == :vic20
96
- validate_vic20
97
- elsif !@ram.nil?
98
- raise Error, "--ram needs vic20 or plus4"
99
- end
101
+ ram_option
102
+ validate_vic20 if @family == :vic20
100
103
  @family == :plus4 ? validate_plus4 : refuse_plus4_options
104
+ @family == :pet ? validate_pet : refuse_pet_options
101
105
  if @family == :c128
102
106
  validate_c128
103
107
  elsif @c64_mode
@@ -105,6 +109,19 @@ module Badline
105
109
  end
106
110
  end
107
111
 
112
+ # --ram names the VIC-20's RAM expansion, or the Plus/4's or the PET's
113
+ # RAM.
114
+ def ram_option
115
+ return if @ram_name.nil?
116
+
117
+ case @family
118
+ when :vic20 then @ram = ram_configuration(@ram_name)
119
+ when :plus4 then @ram_kb = plus4_ram(@ram_name)
120
+ when :pet then @ram_kb = pet_ram(@ram_name)
121
+ else raise Error, "--ram needs vic20, plus4 or pet"
122
+ end
123
+ end
124
+
108
125
  # The C128 runs in the window, without an REU, and its NTSC models
109
126
  # have names of their own.
110
127
  def validate_c128
@@ -153,6 +170,68 @@ module Badline
153
170
  raise Error, "--sid-card-address needs --sid-card" unless @sid_card_address.nil? || @sid_card
154
171
  end
155
172
 
173
+ # Only the PET has a frame rate to pick and BASIC and editor ROMs to
174
+ # fit.
175
+ def refuse_pet_options
176
+ raise Error, "--60hz needs pet" unless @refresh_hz.nil?
177
+ raise Error, "--basic needs pet" unless @basic.nil?
178
+ raise Error, "--editor needs pet" unless @editor.nil?
179
+ raise Error, "--characters needs pet" unless @characters.nil?
180
+ raise Error, "--jumpers needs pet" unless @jumpers.empty?
181
+ end
182
+
183
+ # The PET runs in the window, without the C64's SID and REU, at the
184
+ # frame rate its editor sets.
185
+ def validate_pet
186
+ raise Error, "--ntsc needs the C64; a CRTC PET's 60 Hz editor is --60hz" if @models.include?("ntsc")
187
+ raise Error, "--sid needs the C64" unless @sid_model.nil?
188
+ raise Error, "--reu needs the C64" unless @reu.nil?
189
+ raise Error, "the PET needs the window" if headless?
190
+
191
+ return unless PET_MODELS.include?(model)
192
+
193
+ validate_pet_roms
194
+ validate_pet_board
195
+ end
196
+
197
+ # --characters names a character ROM the board takes, --jumpers the
198
+ # 8296 board's jumpers, and the 8296 has all of its RAM soldered on.
199
+ def validate_pet_board
200
+ unless @characters.nil? || PET::ROMSets.characters(model).include?(@characters)
201
+ raise Error, "the #{model} takes no character ROM #{@characters}"
202
+ end
203
+
204
+ unknown = @jumpers.find { |jumper| !PET::ROMSets.jumpers(model).include?(jumper) }
205
+ raise Error, "the #{model} has no jumper #{unknown}" unless unknown.nil?
206
+ raise Error, "the 8296 has 32k of RAM" if model == "8296" && ![nil, 32].include?(@ram_kb)
207
+ end
208
+
209
+ # --basic and --editor name ROMs the model's board takes
210
+ # (PET::ROMSets), and an editor named sets the frame rate.
211
+ def validate_pet_roms
212
+ raise Error, "invalid argument: --basic #{@basic}" unless @basic.nil? || PET::ROMSets::BASICS.key?(@basic)
213
+ raise Error, "invalid argument: --editor #{@editor}" unless @editor.nil? || PET::ROMSets::EDITORS.key?(@editor)
214
+
215
+ basic = @basic || PET::ROMSets.stock_basic(model)
216
+ fitting = PET::ROMSets.editors(model, basic)
217
+ raise Error, "the #{model} takes no BASIC #{basic}" if fitting.empty?
218
+ return if @editor.nil?
219
+ raise Error, "the #{model} with BASIC #{basic} takes no editor #{@editor}" unless fitting.include?(@editor)
220
+
221
+ editor_rate(fitting)
222
+ end
223
+
224
+ # The frame rate of the editor named, on a CRTC board, which has a 50
225
+ # Hz and a 60 Hz one.
226
+ def editor_rate(fitting)
227
+ return if fitting.size == 1
228
+
229
+ editor_hz = @editor == fitting.last ? 60 : 50
230
+ raise Error, "--60hz takes a 60 Hz editor, not #{@editor}" if @refresh_hz == 60 && editor_hz == 50
231
+
232
+ @refresh_hz = editor_hz
233
+ end
234
+
156
235
  # `sid` takes directories as well as tunes.
157
236
  def validate_tune(path)
158
237
  return if !File.exist?(path) || File.extname(path).casecmp?(".sid")