badline 0.15.1 → 0.16.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/CLAUDE.md +8 -7
  4. data/README.md +102 -6
  5. data/doc/pinned-behaviour.md +67 -0
  6. data/doc/snapshot-format.md +213 -18
  7. data/doc/snapshots.md +14 -0
  8. data/lib/badline/c64/common.rb +1 -1
  9. data/lib/badline/chips/crtc/keyed_state.rb +46 -0
  10. data/lib/badline/chips/crtc.rb +163 -0
  11. data/lib/badline/chips/pia/control_lines.rb +138 -0
  12. data/lib/badline/chips/pia/keyed_parts.rb +56 -0
  13. data/lib/badline/chips/pia.rb +138 -0
  14. data/lib/badline/chips/vic/register_log.rb +1 -4
  15. data/lib/badline/core.rb +3 -0
  16. data/lib/badline/drives/ieee_bus.rb +28 -0
  17. data/lib/badline/drives/model1551/cable.rb +5 -2
  18. data/lib/badline/drives/model1551/tia.rb +13 -34
  19. data/lib/badline/drives/model1551.rb +26 -4
  20. data/lib/badline/drives.rb +1 -0
  21. data/lib/badline/frontend/app.rb +1 -1
  22. data/lib/badline/frontend/controls.rb +23 -3
  23. data/lib/badline/frontend/gamepads.rb +10 -3
  24. data/lib/badline/frontend/keys.rb +88 -0
  25. data/lib/badline/frontend/media_slots.rb +1 -0
  26. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  27. data/lib/badline/frontend/menu/menu_media.rb +27 -28
  28. data/lib/badline/frontend/menu/menu_pages.rb +15 -2
  29. data/lib/badline/frontend/menu/menu_ports.rb +22 -1
  30. data/lib/badline/frontend/menu/pause_menu.rb +1 -0
  31. data/lib/badline/frontend/pet_keyboard.rb +124 -0
  32. data/lib/badline/frontend/screens.rb +2 -1
  33. data/lib/badline/frontend/timeline.rb +16 -9
  34. data/lib/badline/frontend.rb +1 -0
  35. data/lib/badline/kernal_trap/dos/writes.rb +3 -1
  36. data/lib/badline/kernal_trap/dos.rb +6 -1
  37. data/lib/badline/kernal_trap/frames.rb +117 -0
  38. data/lib/badline/kernal_trap/ieee.rb +148 -0
  39. data/lib/badline/kernal_trap/ieee_layout.rb +113 -0
  40. data/lib/badline/kernal_trap/keyed_state.rb +4 -4
  41. data/lib/badline/kernal_trap/routine.rb +3 -53
  42. data/lib/badline/kernal_trap/serial.rb +4 -102
  43. data/lib/badline/kernal_trap/steps.rb +64 -0
  44. data/lib/badline/kernal_trap.rb +4 -0
  45. data/lib/badline/machine/keyboard_buffer.rb +8 -0
  46. data/lib/badline/machine/settling.rb +1 -1
  47. data/lib/badline/machine.rb +28 -6
  48. data/lib/badline/media/extensions.rb +5 -2
  49. data/lib/badline/media/pet_media.rb +40 -0
  50. data/lib/badline/media/program.rb +14 -2
  51. data/lib/badline/media/vic20_media.rb +2 -2
  52. data/lib/badline/media.rb +36 -16
  53. data/lib/badline/options/family.rb +17 -6
  54. data/lib/badline/options/help.rb +19 -3
  55. data/lib/badline/options/table.rb +14 -2
  56. data/lib/badline/options/validation.rb +68 -5
  57. data/lib/badline/options.rb +31 -19
  58. data/lib/badline/pet/bus/expansion.rb +106 -0
  59. data/lib/badline/pet/bus.rb +204 -0
  60. data/lib/badline/pet/canvas.rb +97 -0
  61. data/lib/badline/pet/cassettes.rb +63 -0
  62. data/lib/badline/pet/characters.rb +26 -0
  63. data/lib/badline/pet/cpu.rb +10 -0
  64. data/lib/badline/pet/crtc_video.rb +142 -0
  65. data/lib/badline/pet/discrete_video.rb +83 -0
  66. data/lib/badline/pet/ieee_ports.rb +66 -0
  67. data/lib/badline/pet/ieee_traps.rb +75 -0
  68. data/lib/badline/pet/keyboard_ports.rb +74 -0
  69. data/lib/badline/pet/keyed_modules.rb +85 -0
  70. data/lib/badline/pet/keyed_parts.rb +123 -0
  71. data/lib/badline/pet/keyed_state.rb +42 -0
  72. data/lib/badline/pet/model.rb +113 -0
  73. data/lib/badline/pet/rom_sets.rb +65 -0
  74. data/lib/badline/pet/sound.rb +128 -0
  75. data/lib/badline/pet/via.rb +10 -0
  76. data/lib/badline/pet/via_ports.rb +19 -0
  77. data/lib/badline/pet.rb +226 -0
  78. data/lib/badline/plus4/keyed_state.rb +5 -3
  79. data/lib/badline/plus4/tcbm_drive.rb +14 -0
  80. data/lib/badline/plus4.rb +1 -1
  81. data/lib/badline/roms/README +137 -0
  82. data/lib/badline/roms/pet/basic1.rom +0 -0
  83. data/lib/badline/roms/pet/basic2.rom +0 -0
  84. data/lib/badline/roms/pet/basic4.rom +0 -0
  85. data/lib/badline/roms/pet/characters1.rom +0 -0
  86. data/lib/badline/roms/pet/characters2.rom +0 -0
  87. data/lib/badline/roms/pet/editor1.rom +0 -0
  88. data/lib/badline/roms/pet/editor2-b.rom +0 -0
  89. data/lib/badline/roms/pet/editor2-n.rom +0 -0
  90. data/lib/badline/roms/pet/editor4-40-n-50hz.rom +0 -0
  91. data/lib/badline/roms/pet/editor4-40-n-60hz.rom +0 -0
  92. data/lib/badline/roms/pet/editor4-80-b-50hz-0283.rom +0 -0
  93. data/lib/badline/roms/pet/editor4-80-b-50hz.rom +0 -0
  94. data/lib/badline/roms/pet/editor4-80-b-60hz.rom +0 -0
  95. data/lib/badline/roms/pet/editor4-b.rom +0 -0
  96. data/lib/badline/roms/pet/editor4-n.rom +0 -0
  97. data/lib/badline/roms/pet/kernal1.rom +0 -0
  98. data/lib/badline/roms/pet/kernal2.rom +0 -0
  99. data/lib/badline/roms/pet/kernal4.rom +0 -0
  100. data/lib/badline/snapshot/container.rb +2 -0
  101. data/lib/badline/snapshot/field_doc/markdown.rb +1 -1
  102. data/lib/badline/snapshot/field_doc.rb +16 -3
  103. data/lib/badline/snapshot/image.rb +14 -7
  104. data/lib/badline/snapshot/pet_setup.rb +71 -0
  105. data/lib/badline/snapshot/storage_setup.rb +7 -1
  106. data/lib/badline/snapshot.rb +9 -5
  107. data/lib/badline/storage/d80_image.rb +48 -0
  108. data/lib/badline/storage/d82_image.rb +21 -0
  109. data/lib/badline/storage/tap.rb +16 -3
  110. data/lib/badline/storage.rb +2 -0
  111. data/lib/badline/support/pet_checkpoint.rb +38 -0
  112. data/lib/badline/timing.rb +5 -4
  113. data/lib/badline/version.rb +1 -1
  114. data/lib/badline/vic20/attachments.rb +7 -2
  115. data/lib/badline/vic20/bus/keyed_state.rb +5 -4
  116. data/lib/badline/vic20/bus.rb +5 -0
  117. data/lib/badline/vic20.rb +1 -1
  118. metadata +55 -1
@@ -5,23 +5,15 @@ module Badline
5
5
  # PC traps on the KERNAL's serial bus primitives. Frames addressed to
6
6
  # device 8 go to a virtual drive instead of out over the IEC lines, so
7
7
  # OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
8
- # loaders make through them. Other devices fall through to the ROM.
8
+ # loaders make through them (Frames). Other devices fall through to the
9
+ # ROM.
9
10
  #
10
11
  # With +device+ nil the traps answer no device, and every frame goes
11
12
  # out over the bus, to a true drive if one is plugged in.
12
13
  class Serial < Routine
13
- attr_accessor :device
14
-
15
- # Frame type in the high nibble of the secondary address
16
- OPEN = 0xf0
17
- CLOSE = 0xe0
18
-
19
- # ST bits at $90
20
- DEVICE_NOT_PRESENT = 0x80
21
- EOI = 0x40
22
- READ_TIMEOUT = 0x02
14
+ include Frames
23
15
 
24
- NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
16
+ attr_accessor :device
25
17
 
26
18
  def initialize(cpu:, bus:, layout:, dos:, device: DEVICE)
27
19
  super(cpu:, bus:, layout:)
@@ -35,46 +27,8 @@ module Badline
35
27
  @buffer = []
36
28
  end
37
29
 
38
- def install
39
- routines.each do |address, routine|
40
- @cpu.install_trap(address) { call_routine(routine) if kernal_mapped? }
41
- end
42
- self
43
- end
44
-
45
- # Takes the traps out again, so the KERNAL's own routines run.
46
- def uninstall
47
- routines.each_key { |address| @cpu.remove_trap(address) }
48
- end
49
-
50
30
  private
51
31
 
52
- def routines
53
- {
54
- @layout.talk => :talk,
55
- @layout.listen => :listen,
56
- @layout.second => :second,
57
- @layout.tksa => :tksa,
58
- @layout.ciout => :ciout,
59
- @layout.untalk => :untalk,
60
- @layout.unlisten => :unlisten,
61
- @layout.acptr => :acptr
62
- }
63
- end
64
-
65
- def call_routine(routine)
66
- case routine
67
- when :talk then talk
68
- when :listen then listen
69
- when :second then second
70
- when :tksa then tksa
71
- when :ciout then ciout
72
- when :untalk then untalk
73
- when :unlisten then unlisten
74
- when :acptr then acptr
75
- end
76
- end
77
-
78
32
  # Addressing the bus starts a new frame. The channel number arrives
79
33
  # in the secondary address that follows. Each byte sent under ATN,
80
34
  # and each byte of a frame, times its handshake with the serial timer
@@ -112,25 +66,6 @@ module Badline
112
66
  sent_under_atn
113
67
  end
114
68
 
115
- # CIOUT holds each byte back until the next one, or the UNLISTEN,
116
- # comes, so the first byte of a frame sends nothing.
117
- def ciout
118
- return unless @listening
119
-
120
- time_serial_byte(ISOUR_TIMEOUT) if !@buffer.empty? && accepted?
121
- @buffer << @cpu.a
122
- update_status(DEVICE_NOT_PRESENT) unless accepted?
123
- @cpu.status.carry = false
124
- return_to_caller
125
- end
126
-
127
- # The drive acknowledges every byte of an open or close frame, but
128
- # takes data only on a channel it has open. Otherwise nothing holds
129
- # the data line when a byte starts, so the KERNAL finds no device.
130
- def accepted?
131
- [OPEN, CLOSE].include?(@frame) || @dos.listening?(@listen_channel)
132
- end
133
-
134
69
  def unlisten
135
70
  return unless @listening
136
71
 
@@ -148,15 +83,6 @@ module Badline
148
83
  release_bus
149
84
  end
150
85
 
151
- # UNLSN and UNTLK end by releasing ATN, the clock and the data line,
152
- # and leave the port's last read in the accumulator.
153
- def release_bus
154
- @cpu.a = @layout.release_serial_lines(@bus)
155
- @cpu.status.negative = @cpu.a.anybits?(0x80)
156
- @cpu.status.zero = @cpu.a.zero?
157
- return_to_caller
158
- end
159
-
160
86
  def acptr
161
87
  return unless @talking
162
88
 
@@ -167,30 +93,6 @@ module Badline
167
93
  @cpu.status.carry = false
168
94
  return_to_caller
169
95
  end
170
-
171
- def deliver_frame
172
- return unless @listen_channel
173
-
174
- case @frame
175
- when OPEN then @dos.open(@listen_channel, @buffer.pack("C*"))
176
- when CLOSE then @dos.close(@listen_channel)
177
- else @dos.write(@listen_channel, @buffer)
178
- end
179
- @buffer = []
180
- end
181
-
182
- def received
183
- byte, eoi = @dos.read(@talk_channel)
184
- return NO_DATA unless byte
185
-
186
- [byte, eoi ? EOI : 0]
187
- end
188
-
189
- def update_status(bits)
190
- return if bits.zero?
191
-
192
- @bus.poke(zero_page.status, @bus.peek(zero_page.status) | bits)
193
- end
194
96
  end
195
97
  end
196
98
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # The steps the PC traps on KERNAL routines share, on the @cpu, @bus
6
+ # and @layout of the trap that includes them, which includes
7
+ # IntegerHelper too: the layout's checks and timer, the zero page's
8
+ # words, and the return that stands in for the trapped routine's RTS.
9
+ # Routine includes them, and so does each trap that isn't a Routine,
10
+ # so every trap class keeps its own layout's type.
11
+ module Steps
12
+ private
13
+
14
+ def kernal_mapped?
15
+ @layout.kernal_mapped?(@bus)
16
+ end
17
+
18
+ def zero_page = @layout.zero_page
19
+
20
+ # The two-byte value whose low byte is at +address+.
21
+ def peek_word(address)
22
+ uint16(@bus.peek(address), @bus.peek(address + 1))
23
+ end
24
+
25
+ def poke_word(address, value)
26
+ @bus.poke(address, low_byte(value))
27
+ @bus.poke(address + 1, high_byte(value))
28
+ end
29
+
30
+ # The ROM times each byte on the serial bus with a one-shot timer
31
+ # (Layout#time_serial_byte): Routine::ISOUR_TIMEOUT for a byte sent,
32
+ # Routine::ACPTR_TIMEOUT for one received.
33
+ def time_serial_byte(timer_high)
34
+ @layout.time_serial_byte(@bus, timer_high)
35
+ end
36
+
37
+ def return_to_caller
38
+ @cpu.program_counter = (uint16(pull_byte, pull_byte) + 1) & 0xffff
39
+ end
40
+
41
+ # Runs the ROM routines in order, each returning into the next, and
42
+ # the last one to the trapped routine's caller
43
+ def continue_with(first, *rest)
44
+ rest.reverse_each { |address| push_address((address - 1) & 0xffff) }
45
+ @cpu.program_counter = first
46
+ end
47
+
48
+ def push_address(address)
49
+ push_byte(high_byte(address))
50
+ push_byte(low_byte(address))
51
+ end
52
+
53
+ def push_byte(value)
54
+ @bus.poke(0x0100 + @cpu.stack_pointer, value)
55
+ @cpu.stack_pointer = (@cpu.stack_pointer - 1) & 0xff
56
+ end
57
+
58
+ def pull_byte
59
+ @cpu.stack_pointer = (@cpu.stack_pointer + 1) & 0xff
60
+ @bus.peek(0x0100 + @cpu.stack_pointer)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "badline/kernal_trap/layout"
4
+ require "badline/kernal_trap/ieee_layout"
5
+ require "badline/kernal_trap/steps"
4
6
  require "badline/kernal_trap/routine"
5
7
  require "badline/kernal_trap/dos"
6
8
  require "badline/kernal_trap/file_routine"
7
9
  require "badline/kernal_trap/load"
8
10
  require "badline/kernal_trap/save"
11
+ require "badline/kernal_trap/frames"
9
12
  require "badline/kernal_trap/serial"
13
+ require "badline/kernal_trap/ieee"
10
14
  require "badline/kernal_trap/chrout"
11
15
  require "badline/kernal_trap/keyed_state"
@@ -20,6 +20,14 @@ module Badline
20
20
  # ($D86E).
21
21
  PLUS4_LAYOUT = Layout.new(address: 0x0527, count: 0xef, capacity: 10)
22
22
 
23
+ # The PET's, under BASIC 2 and BASIC 4. Every editor's GETIN takes
24
+ # ten keys from it, though the CRTC editors' keyboard scans stop at
25
+ # nine (XMAX).
26
+ PET_LAYOUT = Layout.new(address: 0x026f, count: 0x9e, capacity: 10)
27
+
28
+ # The PET's under BASIC 1, outside the zero page.
29
+ PET_BASIC1_LAYOUT = Layout.new(address: 0x020f, count: 0x020d, capacity: 10)
30
+
23
31
  def type_text(text)
24
32
  codes = text.bytes.map { |b| ascii_to_petscii(b) }
25
33
  on_init { @pending_keys = codes }
@@ -4,7 +4,7 @@ module Badline
4
4
  module Machine
5
5
  # Brings the machine to the point a snapshot file is saved at: the main
6
6
  # CPU between instructions, or jammed, with no REU transfer holding the
7
- # bus. C64, Vic20, C128 and Plus4 each include it, and answer
7
+ # bus. C64, Vic20, C128, Plus4 and PET each include it, and answer
8
8
  # #settled_for_snapshot? and #settle_chips_for_snapshot, which brings
9
9
  # what each chip defers up to the cycle without running one.
10
10
  module Settling
@@ -8,8 +8,9 @@ module Badline
8
8
  # = Machine
9
9
  #
10
10
  # The machines badline builds by family, as the command line's
11
- # subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, and the
12
- # C128 in C64 mode, C128, and the 264 series, Plus4.
11
+ # subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, the
12
+ # C128 in C64 mode, C128, the 264 series, Plus4, and the PET and CBM,
13
+ # PET.
13
14
  #
14
15
  # A machine answers what the front end, Media, the runners and the
15
16
  # snapshots call on it:
@@ -57,23 +58,31 @@ module Badline
57
58
  # and +capture_output+, and besides +model_label+, +cartridge_slots+ and
58
59
  # +detach_cartridges+ for its two cartridges.
59
60
  #
61
+ # The PET answers the identity, clocking, lifecycle, video, sound and
62
+ # keyboard parts, with no joysticks, RESTORE or control ports, and of
63
+ # the rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+,
64
+ # +mounted?+, +datasette+, +true_drive+, nil, +install_debug_register+
65
+ # and +capture_output+, and besides +model_label+ and +ram_kb+.
66
+ #
60
67
  # Each machine includes Machine::Clocking for the clocking part.
61
68
  module Machine
62
69
  # Media the machine named doesn't take.
63
70
  class Unsupported < ArgumentError; end
64
71
 
65
- # A new machine of `family`, :c64, :vic20, :c128 or :plus4, the model
66
- # of it `model` names. A C64's model is one of Model::ALL, with its SID
72
+ # A new machine of `family`, :c64, :vic20, :c128, :plus4 or :pet, the
73
+ # model of it `model` names. A C64's model is one of Model::ALL, with its SID
67
74
  # `sid_model` when not nil and an REU of `reu` K when not nil. A
68
75
  # VIC-20's is "pal", with the RAM expansion `ram` names
69
76
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
70
77
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
71
78
  # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
72
- # the model's own RAM.
79
+ # the model's own RAM. A PET's is one of PET::Model::ALL, with its own
80
+ # RAM and ROMs, its CRTC's editor the 50 Hz one.
73
81
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
74
82
  return vic20(model, ram) if family == :vic20
75
83
  return c128(model, sid_model, :c64) if family == :c128
76
84
  return Plus4.new(model:) if family == :plus4
85
+ return PET.new(model:) if family == :pet
77
86
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
78
87
 
79
88
  profile = Model.named(model)
@@ -89,11 +98,12 @@ module Badline
89
98
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
90
99
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
91
100
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
92
- # Unsupported for media a VIC-20 or a Plus/4 doesn't take.
101
+ # Unsupported for media a VIC-20, a Plus/4 or a PET doesn't take.
93
102
  def self.for_options(options, media, &)
94
103
  family = options.family
95
104
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
96
105
  return plus4_for(media, options) if family == :plus4
106
+ return pet_for(media, options) if family == :pet
97
107
 
98
108
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
99
109
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -141,6 +151,18 @@ module Badline
141
151
  end
142
152
  private_class_method :plus4_for
143
153
 
154
+ # A PET with the RAM --ram names (PET::Model::RAM_SIZES), or with the
155
+ # model's own without it, the editor at the frame rate --60hz or
156
+ # --editor asks for, and the ROMs --basic and --editor fit it with.
157
+ def self.pet_for(media, options)
158
+ unsupported = !media.nil? && Media::PETMedia::UNSUPPORTED.include?(File.extname(media).downcase)
159
+ raise Unsupported, "doesn't go in a PET" if unsupported
160
+
161
+ PET.new(model: options.model, ram_kb: options.ram_kb, refresh_hz: options.refresh_hz, basic: options.basic,
162
+ editor: options.editor)
163
+ end
164
+ private_class_method :pet_for
165
+
144
166
  def self.c64_media?(media)
145
167
  return false if media.nil? || !File.file?(media)
146
168
 
@@ -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 gcr])
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 goes in as a program file does, once the KERNAL has
30
+ # booted.
31
+ def disk_autostart(machine, storage)
32
+ return machine.type_text(AUTOSTART) unless machine.model.basic1?
33
+
34
+ data = storage.read_file("*")
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
@@ -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,7 +155,7 @@ 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))
@@ -154,7 +166,11 @@ module Badline
154
166
  def mount(computer, path, disk, autostart:)
155
167
  storage = open_storage(path, disk)
156
168
  computer.mount(storage)
157
- computer.type_text(disk_autostart(computer) { storage.read_file("*") }) if autostart
169
+ if autostart && computer.family == :pet
170
+ PETMedia.disk_autostart(computer, storage)
171
+ elsif autostart
172
+ computer.type_text(disk_autostart(computer) { storage.read_file("*") })
173
+ end
158
174
  "Mounted #{path} as device 8"
159
175
  end
160
176
 
@@ -170,7 +186,9 @@ module Badline
170
186
  # (Plus4Media.cartridge_chips).
171
187
  def attach_cartridge(computer, path, options)
172
188
  case computer.family
173
- when :vic20 then computer.attach_cartridge(Storage::CRTFile.new(path, machine: :vic20).chips)
189
+ when :vic20
190
+ crt = Storage::CRTFile.new(path, machine: :vic20)
191
+ computer.attach_cartridge(crt.chips, crt.name.empty? ? File.basename(path, File.extname(path)) : crt.name)
174
192
  when :plus4 then computer.attach_cartridge(Plus4Media.cartridge_chips(path, slot: options.fetch(:slot, nil)))
175
193
  else
176
194
  computer.attach_cartridge(C64::Cartridge.from_file(path, **options))
@@ -178,17 +196,19 @@ module Badline
178
196
  "Attached cartridge #{path}"
179
197
  end
180
198
 
181
- # A VIC-20 or a 264 says when the tape was made for another machine
182
- # (Vic20Media.tape_message, Plus4Media.tape_message).
199
+ # A VIC-20, a 264 or a PET says when the tape was made for another
200
+ # machine (Vic20Media.tape_message, Plus4Media.tape_message,
201
+ # PETMedia.tape_message).
183
202
  def attach_tape(computer, path, autostart:)
184
203
  tape = Storage::TAP.new(path)
185
204
  computer.datasette.insert(tape)
186
205
  computer.datasette.play!
187
- computer.type_text(TAPE_AUTOSTART) if autostart
206
+ computer.type_text(computer.family == :pet ? PETMedia::TAPE_AUTOSTART : TAPE_AUTOSTART) if autostart
188
207
  message = "Inserted #{path} in the datasette"
189
208
  case computer.family
190
209
  when :vic20 then Vic20Media.tape_message(tape, message)
191
210
  when :plus4 then Plus4Media.tape_message(tape, message)
211
+ when :pet then PETMedia.tape_message(tape, message)
192
212
  else message
193
213
  end
194
214
  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