badline 0.14.0 → 0.15.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +22 -5
  4. data/README.md +101 -11
  5. data/doc/media.md +19 -0
  6. data/doc/pinned-behaviour.md +145 -0
  7. data/doc/snapshot-format.md +244 -46
  8. data/doc/snapshots.md +18 -1
  9. data/exe/badline-ruby +1 -1
  10. data/lib/badline/audio/bare_player.rb +4 -8
  11. data/lib/badline/audio/machine_player.rb +4 -26
  12. data/lib/badline/c128/banks.rb +1 -3
  13. data/lib/badline/c128/bus.rb +19 -1
  14. data/lib/badline/c128/mmu_pages.rb +7 -0
  15. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  16. data/lib/badline/c64/bus.rb +1 -0
  17. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  18. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  19. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  20. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  21. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  22. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  23. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  24. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  25. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  26. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  27. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  28. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  29. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  30. data/lib/badline/c64/cartridge.rb +3 -3
  31. data/lib/badline/c64/keyed_modules.rb +6 -2
  32. data/lib/badline/c64/true_drives.rb +13 -1
  33. data/lib/badline/c64.rb +6 -0
  34. data/lib/badline/chips/ted/beam.rb +13 -14
  35. data/lib/badline/chips/ted/fetches.rb +199 -0
  36. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  37. data/lib/badline/chips/ted/modes.rb +134 -0
  38. data/lib/badline/chips/ted/palette.rb +43 -0
  39. data/lib/badline/chips/ted/picture.rb +182 -0
  40. data/lib/badline/chips/ted/sound/output.rb +86 -0
  41. data/lib/badline/chips/ted/sound.rb +284 -0
  42. data/lib/badline/chips/ted.rb +53 -6
  43. data/lib/badline/chips/tia.rb +28 -0
  44. data/lib/badline/chips/vic/register_log.rb +4 -1
  45. data/lib/badline/drives/datasette/keyed_state.rb +14 -4
  46. data/lib/badline/drives/datasette.rb +22 -5
  47. data/lib/badline/drives/engine/core.rb +6 -14
  48. data/lib/badline/drives/engine/idle.rb +42 -4
  49. data/lib/badline/drives/engine/idle_cpu.rb +18 -0
  50. data/lib/badline/drives/engine/serial_connection.rb +25 -0
  51. data/lib/badline/drives/engine/sleep.rb +1 -0
  52. data/lib/badline/drives/engine.rb +1 -0
  53. data/lib/badline/drives/model1541/byte_ready.rb +19 -3
  54. data/lib/badline/drives/model1541/mechanism.rb +10 -1
  55. data/lib/badline/drives/model1541.rb +1 -0
  56. data/lib/badline/drives/model1551/bus.rb +138 -0
  57. data/lib/badline/drives/model1551/cable.rb +104 -0
  58. data/lib/badline/drives/model1551/cpu.rb +14 -0
  59. data/lib/badline/drives/model1551/keyed_state.rb +65 -0
  60. data/lib/badline/drives/model1551/port.rb +74 -0
  61. data/lib/badline/drives/model1551/tia.rb +92 -0
  62. data/lib/badline/drives/model1551.rb +170 -0
  63. data/lib/badline/drives/model1571.rb +1 -0
  64. data/lib/badline/drives/model1581.rb +1 -0
  65. data/lib/badline/frontend/boot.rb +23 -2
  66. data/lib/badline/frontend/controls.rb +7 -3
  67. data/lib/badline/frontend/keys.rb +14 -2
  68. data/lib/badline/frontend/media_slots.rb +11 -1
  69. data/lib/badline/frontend/menu/menu_dialogs.rb +14 -5
  70. data/lib/badline/frontend/menu/menu_media.rb +102 -14
  71. data/lib/badline/frontend/menu/menu_pages.rb +16 -11
  72. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  73. data/lib/badline/frontend/player/player_controller.rb +11 -4
  74. data/lib/badline/frontend/screen.rb +33 -12
  75. data/lib/badline/frontend/screens.rb +2 -1
  76. data/lib/badline/frontend/sound.rb +4 -4
  77. data/lib/badline/frontend/timeline.rb +19 -6
  78. data/lib/badline/machine/settling.rb +1 -1
  79. data/lib/badline/machine.rb +30 -6
  80. data/lib/badline/media/plus4_media.rb +68 -0
  81. data/lib/badline/media/program.rb +20 -6
  82. data/lib/badline/media/true_drive.rb +7 -5
  83. data/lib/badline/media.rb +33 -12
  84. data/lib/badline/options/family.rb +1 -0
  85. data/lib/badline/options/help.rb +32 -6
  86. data/lib/badline/options/option.rb +2 -1
  87. data/lib/badline/options/table.rb +29 -4
  88. data/lib/badline/options/validation.rb +66 -4
  89. data/lib/badline/options.rb +71 -11
  90. data/lib/badline/plus4/acia/frames.rb +55 -0
  91. data/lib/badline/plus4/acia.rb +276 -16
  92. data/lib/badline/plus4/bus.rb +245 -46
  93. data/lib/badline/plus4/cassette_sense.rb +21 -0
  94. data/lib/badline/plus4/keyboard_port.rb +35 -10
  95. data/lib/badline/plus4/keyed_modules.rb +116 -0
  96. data/lib/badline/plus4/keyed_parts.rb +172 -0
  97. data/lib/badline/plus4/keyed_state.rb +44 -0
  98. data/lib/badline/plus4/ram_expansion.rb +145 -0
  99. data/lib/badline/plus4/sid_card.rb +95 -0
  100. data/lib/badline/plus4/sound_mix.rb +51 -0
  101. data/lib/badline/plus4/tcbm_drive.rb +120 -0
  102. data/lib/badline/plus4/user_port.rb +11 -2
  103. data/lib/badline/plus4.rb +184 -26
  104. data/lib/badline/roms/README +9 -0
  105. data/lib/badline/roms/dos1551.rom +0 -0
  106. data/lib/badline/snapshot/container.rb +2 -0
  107. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  108. data/lib/badline/snapshot/field_doc.rb +48 -7
  109. data/lib/badline/snapshot/image.rb +12 -3
  110. data/lib/badline/snapshot/plus4_setup.rb +81 -0
  111. data/lib/badline/snapshot.rb +9 -4
  112. data/lib/badline/storage/tap.rb +21 -12
  113. data/lib/badline/support/checkpoint.rb +3 -2
  114. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  115. data/lib/badline/version.rb +1 -1
  116. metadata +29 -1
@@ -10,17 +10,29 @@ module Badline
10
10
  # wrong for the page to show.
11
11
  class MenuMedia
12
12
  attr_reader :problem
13
- attr_writer :computer
14
13
 
15
14
  # Takes the disk set from the command line's `media_path` when it's a
16
- # list.
17
- def initialize(media_path, writable)
15
+ # list, and the names of the cartridges it and `cartridges`, the
16
+ # Plus/4's ROM images by slot (Options#cartridges), put in the first
17
+ # machine.
18
+ def initialize(media_path, writable, cartridges)
18
19
  @list = Media::DiskList.list?(media_path) ? media_path : ""
19
20
  @writable = writable
20
21
  @set_path = ""
21
22
  @set = []
22
23
  @problem = ""
23
24
  @computer = nil
25
+ @command_line = cartridges.map { |slot, path| [slot, path] } + [["", media_path]]
26
+ @names = {}
27
+ @named = nil
28
+ end
29
+
30
+ def computer=(computer)
31
+ @computer = computer
32
+ return unless @named.nil?
33
+
34
+ @command_line.each { |slot, path| note_cartridge(computer, path.dup, slot) }
35
+ @named = computer
24
36
  end
25
37
 
26
38
  def writable? = @writable
@@ -87,18 +99,28 @@ module Badline
87
99
  insert(:disk, set[index + step])
88
100
  end
89
101
 
102
+ # The extensions of the cartridges the machine takes: .crt, and on
103
+ # the Plus/4 raw .bin ROM images too.
104
+ def cartridge_extensions
105
+ extensions = Media::Extensions.of(%i[cartridge])
106
+ @computer.family == :plus4 ? extensions + [Media::Plus4Media::ROM_IMAGE] : extensions
107
+ end
108
+
90
109
  # Swaps the cartridge for the one at `path`, which power cycles the
91
110
  # machine, and says whether it went in.
92
111
  def attach_cartridge(path)
93
- succeeds? { Media.attach(@computer, path) }
112
+ attached = cartridge_succeeds? { Media.attach(@computer, path) }
113
+ note_cartridge(@computer, path, "") if attached
114
+ attached
94
115
  end
95
116
 
96
117
  # What a save of the machine is named after: the cartridge, the disk,
97
118
  # the tape, or BASIC, without the bracketed parts of a file's name. A
98
- # VIC-20's are named after its disk, or else the machine.
119
+ # VIC-20's are named after its disk, or else the machine, and a
120
+ # Plus/4's after its disk or tape.
99
121
  def game_name
100
122
  vic20 = @computer.family == :vic20
101
- name = vic20 ? "" : cartridge_name
123
+ name = vic20 || @computer.family == :plus4 ? "" : cartridge_name
102
124
  disk = disk_path
103
125
  name = File.basename(disk, File.extname(disk)) if name.empty? && !disk.empty?
104
126
  name = tape_name if name.empty? && !vic20
@@ -114,24 +136,36 @@ module Badline
114
136
  end
115
137
 
116
138
  # The name the cartridge in the expansion port gives itself, or an
117
- # empty one.
139
+ # empty one. A VIC-20's or a Plus/4's is the name of the file it came
140
+ # from: a .crt's own, or else the file's. A Plus/4 names each of its
141
+ # cartridges with a ROM in, and a slot filled from elsewhere, such as
142
+ # a snapshot, by the slot.
118
143
  def cartridge_name
119
- cartridge = @computer.address_bus.cartridge
120
- cartridge.nil? ? "" : cartridge.name
144
+ case @computer.family
145
+ when :plus4 then @computer.cartridge_slots.map { |slot| slot_name(slot) }.uniq.join(", ")
146
+ when :vic20 then named? ? @names.fetch("", "") : ""
147
+ else
148
+ cartridge = @computer.address_bus.cartridge
149
+ cartridge.nil? ? "" : cartridge.name
150
+ end
121
151
  end
122
152
 
123
153
  # Takes the cartridge out with the power off.
124
- def remove_cartridge = MediaSlots.remove_cartridge(@computer)
154
+ def remove_cartridge
155
+ @names = {}
156
+ MediaSlots.remove_cartridge(@computer)
157
+ end
125
158
 
126
159
  # A new machine started on `path`, as the command line would, or nil
127
160
  # when it won't start.
128
161
  def start(options, path)
129
162
  computer = nil
130
- started = succeeds? do
131
- computer = Frontend.start(options, path, @writable)
132
- list(path)
163
+ if Media.kind(@computer, path) == :cartridge
164
+ cartridge_succeeds? { computer = started(options, path) }
165
+ else
166
+ succeeds? { computer = started(options, path) }
133
167
  end
134
- started ? computer : nil
168
+ @problem.empty? ? computer : nil
135
169
  end
136
170
 
137
171
  private
@@ -143,6 +177,60 @@ module Badline
143
177
  @problem.empty?
144
178
  end
145
179
 
180
+ # Runs the block as #succeeds? does, keeping a cartridge's chip in a
181
+ # bank or slot the machine doesn't map as the problem too.
182
+ def cartridge_succeeds?(&)
183
+ @problem = Frontend.cartridge_problem(&)
184
+ @problem.empty?
185
+ end
186
+
187
+ # The machine Frontend.start started on `path`, with the names of
188
+ # the cartridges in it noted.
189
+ def started(options, path)
190
+ computer = Frontend.start(options, path, @writable)
191
+ list(path)
192
+ @names = {}
193
+ options.cartridges.each { |slot, image| note_cartridge(computer, image.dup, slot) }
194
+ note_cartridge(computer, path, "")
195
+ @named = computer
196
+ computer
197
+ end
198
+
199
+ def named? = @computer.equal?(@named)
200
+
201
+ def slot_name(slot) = named? ? @names.fetch(slot, slot.upcase) : slot.upcase
202
+
203
+ # Keeps the name of the cartridge at `path` in `computer` by the
204
+ # slots it fills: a Plus/4's by xplus4's names, a raw image's in
205
+ # `slot` if it names one, and a VIC-20's in the one slot "". Other
206
+ # media, and other machines' cartridges, leave the names alone.
207
+ def note_cartridge(computer, path, slot)
208
+ case computer.family
209
+ when :plus4
210
+ return unless Media.kind(computer, path) == :cartridge
211
+
212
+ name = file_name(path)
213
+ Media::Plus4Media.cartridge_chips(path, slot: slot.empty? ? nil : slot).each do |chip|
214
+ @names["c#{chip.bank + 1}#{chip.address < 0xc000 ? 'lo' : 'hi'}"] = name
215
+ end
216
+ when :vic20 then @names[""] = file_name(path) if vic20_cartridge?(path)
217
+ end
218
+ end
219
+
220
+ def vic20_cartridge?(path)
221
+ case Media::Extensions.kind(path)
222
+ when :cartridge then true
223
+ when :program then File.file?(path) && !Media::Vic20Cartridge.chips(path, Media.program_bytes(path)).nil?
224
+ else false
225
+ end
226
+ end
227
+
228
+ # A .crt's own name, or else the file's, without its extension.
229
+ def file_name(path)
230
+ name = Media::Extensions.kind(path) == :cartridge ? Storage::CRTFile.new(path, machine: nil).name : ""
231
+ name.empty? ? File.basename(path, File.extname(path)) : name
232
+ end
233
+
146
234
  # Keeps `path` as the list the set comes from, if it's a list.
147
235
  def list(path)
148
236
  return unless Media::DiskList.list?(path)
@@ -29,7 +29,7 @@ module Badline
29
29
  @recent = []
30
30
  @painter = painter
31
31
  @buttons = buttons
32
- @media = MenuMedia.new(media_path, options.writable?)
32
+ @media = MenuMedia.new(media_path, options.writable?, options.cartridges)
33
33
  @reu_kb = options.reu
34
34
  @computer = nil
35
35
  @controls = nil
@@ -89,11 +89,7 @@ module Badline
89
89
 
90
90
  def vic20? = @computer.family == :vic20
91
91
 
92
- # Says in the dim colour that the VIC-20 hasn't got what the page
93
- # shows.
94
- def note
95
- @painter.text(@left, @top, "NOT ON THE VIC-20 YET", MenuTheme::DIM)
96
- end
92
+ def plus4? = @computer.family == :plus4
97
93
 
98
94
  def draw_drive
99
95
  media = @media
@@ -126,12 +122,14 @@ module Badline
126
122
  end
127
123
 
128
124
  def draw_expansion
125
+ info("CARTRIDGE", @media.cartridge_name)
129
126
  return info("RAM EXPANSION", RAM_NAMES.fetch(@computer.ram_configuration)) if vic20?
130
127
 
131
- cartridge = @computer.address_bus.cartridge
132
- info("CARTRIDGE", cartridge.nil? ? "" : cartridge.name)
133
128
  row("INSERT...", :insert_cartridge)
134
129
  row("REMOVE", :remove_cartridge)
130
+ return if plus4?
131
+
132
+ cartridge = @computer.address_bus.cartridge
135
133
  row("FREEZE", :freeze) if !cartridge.nil? && cartridge.freezer? && @computer.family == :c64
136
134
  return if @reu_kb.nil?
137
135
 
@@ -139,7 +137,8 @@ module Badline
139
137
  info("RAM EXPANSION", "REU, #{@reu_kb} KB")
140
138
  end
141
139
 
142
- # The SID's model is a setting, and other sound chips have none.
140
+ # The SID's model is a setting, the C64's SID's or a Plus/4's SID
141
+ # card's, and other sound chips have none.
143
142
  def draw_sound
144
143
  if @sound.on?
145
144
  toggle("OUTPUT", :output, [%w[ON MUTED], %i[sound_on mute], @sound.muted? ? 1 : 0])
@@ -149,7 +148,7 @@ module Badline
149
148
  model = @computer.sound_source.model
150
149
  return unless SID_MODELS.include?(model)
151
150
 
152
- toggle("SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
151
+ toggle(plus4? ? "SID CARD" : "SID", :sid, [%w[6581 8580], %i[sid6581 sid8580], model == :mos8580 ? 1 : 0])
153
152
  end
154
153
 
155
154
  # Saving now and by name, loading a named save, and the quicksaves
@@ -167,9 +166,15 @@ module Badline
167
166
  end
168
167
  end
169
168
 
170
- # The C128 names its model and the mode it runs in first.
169
+ # The C128 names its model and the mode it runs in first, and the
170
+ # Plus/4 its model and RAM, and its RAM expansion if it has one.
171
171
  def draw_power
172
172
  info("MACHINE", "#{@computer.model.name.upcase}, #{@computer.mode.to_s.upcase} MODE") if c128?
173
+ if plus4?
174
+ info("MACHINE", @computer.model_label)
175
+ expansion = @computer.ram_expansion_label
176
+ info("RAM EXPANSION", expansion) if expansion
177
+ end
173
178
  row("RESET", :reset)
174
179
  row("POWER CYCLE", :power_cycle)
175
180
  skip
@@ -5,7 +5,7 @@ module Badline
5
5
  # The pause menu's PORTS page, which MenuPages draws: what each C64
6
6
  # control port has plugged in, a joystick or a pot device, and whether
7
7
  # the keys type or drive the joysticks. The VIC-20 has one port, a
8
- # joystick's.
8
+ # joystick's, and the Plus/4 two joysticks and no pot devices.
9
9
  module MenuPorts
10
10
  PORT_DEVICES = %i[joystick mouse paddles].freeze
11
11
  PORT_NAMES = %w[JOY MOUSE PADDLES].freeze
@@ -16,6 +16,7 @@ module Badline
16
16
 
17
17
  def draw_ports
18
18
  return draw_vic20_port if @computer.family == :vic20
19
+ return draw_plus4_ports if @computer.family == :plus4
19
20
 
20
21
  toggle("PORT 1", :port1, [PORT_NAMES, PORT_ACTIONS[0], PORT_DEVICES.index(port_device(1))])
21
22
  toggle("PORT 2", :port2, [PORT_NAMES, PORT_ACTIONS[1], PORT_DEVICES.index(port_device(2))])
@@ -37,6 +38,17 @@ module Badline
37
38
  info("WASD, LEFT SHIFT", "JOYSTICK")
38
39
  end
39
40
 
41
+ # The Plus/4's two joysticks, one for each set of keys.
42
+ def draw_plus4_ports
43
+ return unless draw_keys("PLUS/4")
44
+
45
+ row("SWAP JOYSTICKS", :swap)
46
+ skip
47
+ arrows = @controls.arrows_port
48
+ info("ARROWS, SPACE", "JOYSTICK #{arrows}")
49
+ info("WASD, LEFT SHIFT", "JOYSTICK #{3 - arrows}")
50
+ end
51
+
40
52
  # The toggle between the keyboard named and the joysticks, and
41
53
  # whether the keys drive the joysticks.
42
54
  def draw_keys(keyboard)
@@ -15,7 +15,8 @@ module Badline
15
15
  # way: #wait handles the window's clicks, keys and drops between frames
16
16
  # and redraws it at the display's pace. The SID model buttons switch
17
17
  # the chips playing on the spot, and each tune after it starts on the
18
- # same choice. AUTO gives each of a tune's SIDs its own model.
18
+ # same choice. AUTO gives each of a tune's SIDs its own model. It
19
+ # quits once it has drawn `frames` frames (--frames), unless that's 0.
19
20
  class PlayerController < Audio::Terminal
20
21
  FRAME = 1.0 / 50
21
22
 
@@ -30,8 +31,10 @@ module Badline
30
31
  Keys::TAB => :view, 6 => :chip
31
32
  }.freeze
32
33
 
33
- def initialize(input:, output:)
34
- super
34
+ def initialize(input:, output:, frames: 0)
35
+ super(input:, output:)
36
+ @frame_limit = frames
37
+ @frames = 0
35
38
  @screen = PlayerScreen.new
36
39
  @player = nil
37
40
  @tune_file = nil
@@ -73,7 +76,10 @@ module Badline
73
76
  actions = poll
74
77
  deadline = now + seconds
75
78
  loop do
76
- draw if now >= @next_draw
79
+ if now >= @next_draw
80
+ draw
81
+ actions << :quit if @frames == @frame_limit
82
+ end
77
83
  left = deadline - now
78
84
  break if left <= 0 || !actions.empty?
79
85
 
@@ -211,6 +217,7 @@ module Badline
211
217
 
212
218
  def draw
213
219
  @next_draw = now + FRAME
220
+ @frames += 1
214
221
  @screen.draw(@state, @player, @history, @tune_file, SIDTune::STIL::Credit.at(@credits, @state.played))
215
222
  end
216
223
 
@@ -4,9 +4,10 @@ module Badline
4
4
  module Frontend
5
5
  # The visible part of the video chip's display, the crop of the
6
6
  # machine's timing, repacked for an SDL texture in XRGB8888 in the
7
- # chip's palette. Spinel hands an Array of Integers to C as 64-bit
8
- # words, as badline/ffi does on CRuby, so each word carries two
9
- # neighbouring pixels, the left one in the low half.
7
+ # chip's palette, of 16 colours or the TED's 128. Spinel hands an Array
8
+ # of Integers to C as 64-bit words, as badline/ffi does on CRuby, so
9
+ # each word carries two neighbouring pixels, the left one in the low
10
+ # half.
10
11
  #
11
12
  # Each pixel shows `pixel_width` of the window's square pixels wide,
12
13
  # or with `dots`, that many pixels share one, as the VDC's dots do. The
@@ -14,18 +15,21 @@ module Badline
14
15
  # least MIN_WIDTH window pixels wide, and at least as high as PAL's C64
15
16
  # crop, 272 lines, so a C64 shows in the same window whatever its
16
17
  # region. A crop with fewer lines or pixels sits in the middle of the
17
- # texture, between black bands. The VIC-20's 284 by 284 crop fills its
18
- # own.
18
+ # texture, between black bands. The VIC-20's 284 by 284 crop and the
19
+ # TED's 384 by 288 fill their own.
20
+ #
21
+ # With `frame_lines`, the crop's lines run on past the frame's last
22
+ # line to its first, as the TED's do: its top border is the end of
23
+ # the frame.
19
24
  class Screen
20
25
  MIN_WIDTH = Region::PAL.crop[2]
21
26
  MIN_HEIGHT = Region::PAL.crop[3]
22
27
 
23
28
  attr_reader :pixels, :width, :height
24
29
 
25
- def initialize(video, crop, pixel_width: 1, dots: 1)
30
+ def initialize(video, crop, pixel_width: 1, dots: 1, frame_lines: 0)
26
31
  @video = video
27
32
  @col_offset = crop[0]
28
- @row_offset = crop[1]
29
33
  @lines = crop[3]
30
34
  @columns = crop[2] & ~1
31
35
  @pixel_width = pixel_width
@@ -37,8 +41,10 @@ module Badline
37
41
  @pair_count = @columns / 2
38
42
  @margin = (@row_words - @pair_count) / 2
39
43
  @band = (@height - @lines) / 2
40
- palette = video.palette
41
- @pairs = Array.new(256) { |pair| palette[pair & 0x0f] | (palette[pair >> 4] << 32) }
44
+ top = crop[1]
45
+ wrap = frame_lines.zero? ? top + @lines : frame_lines
46
+ @sources = Array.new(@lines) { |row| (row + top) % wrap }
47
+ pair_up(video.palette)
42
48
  @pixels = Array.new(@height * @row_words, 0)
43
49
  pack_all
44
50
  end
@@ -54,9 +60,10 @@ module Badline
54
60
  video = @video
55
61
  dirty = video.dirty_lines
56
62
  display = video.display
63
+ sources = @sources
57
64
  row = 0
58
65
  while row < @lines
59
- pack_row(display, row) if dirty[row + @row_offset]
66
+ pack_row(display, row) if dirty[sources[row]]
60
67
  row += 1
61
68
  end
62
69
  video.clear_dirty_lines!
@@ -64,6 +71,18 @@ module Badline
64
71
 
65
72
  private
66
73
 
74
+ # The packed word for each pair of palette indices, the left one in
75
+ # the low bits.
76
+ def pair_up(palette)
77
+ colors = palette.size
78
+ shift = 4
79
+ shift += 1 while (1 << shift) < colors
80
+ mask = colors - 1
81
+ @shift = shift
82
+ @mask = mask
83
+ @pairs = Array.new(colors << shift) { |pair| palette[pair & mask] | (palette[(pair >> shift) & mask] << 32) }
84
+ end
85
+
67
86
  # Repacks every line, as a screen built for a display the chip has
68
87
  # already drawn starts with all of it.
69
88
  def pack_all
@@ -78,11 +97,13 @@ module Badline
78
97
  def pack_row(display, row)
79
98
  pixels = @pixels
80
99
  pairs = @pairs
81
- from = ((row + @row_offset) * @stride) + @col_offset
100
+ mask = @mask
101
+ shift = @shift
102
+ from = (@sources[row] * @stride) + @col_offset
82
103
  to = ((row + @band) * @row_words) + @margin
83
104
  last = to + @pair_count
84
105
  while to < last
85
- pixels[to] = pairs[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
106
+ pixels[to] = pairs[(display[from] & mask) | ((display[from + 1] & mask) << shift)]
86
107
  from += 2
87
108
  to += 1
88
109
  end
@@ -70,7 +70,8 @@ module Badline
70
70
  @screen = Screen.new(vdc, vdc.crop, dots: 2)
71
71
  else
72
72
  timing = @computer.timing
73
- @screen = Screen.new(@computer.video, timing.crop, pixel_width: timing.pixel_width)
73
+ @screen = Screen.new(@computer.video, timing.crop,
74
+ pixel_width: timing.pixel_width, frame_lines: timing.lines_per_frame)
74
75
  end
75
76
  end
76
77
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Badline
4
4
  module Frontend
5
- # The machine's sound source, the C64's SID or the VIC-20's VIC, on
6
- # SDL's audio queue, as mono signed 16-bit samples. Both take
7
- # #record(rate:, clock_hz:) and #drain_samples.
5
+ # The machine's sound source, the C64's SID, the VIC-20's VIC or the
6
+ # Plus/4's TED, on SDL's audio queue, as mono signed 16-bit samples.
7
+ # Each takes #record(rate:, clock_hz:) and #drain_samples.
8
8
  # Once the device plays, it is the clock: #wait holds each frame until
9
9
  # the queue is down to `AHEAD`, so the machine runs at the device's pace.
10
10
  #
@@ -16,7 +16,7 @@ module Badline
16
16
  #
17
17
  # Each frame's samples go to a mono AudioDevice. The chip's cycles are
18
18
  # converted at `clock_hz`, the machine's clock, so an NTSC or Drean
19
- # machine plays at its own pitch.
19
+ # machine plays at its own pitch, and the TED's at its single clock.
20
20
  class Sound
21
21
  RATE = 44_100
22
22
  AHEAD = 0.08
@@ -38,17 +38,30 @@ module Badline
38
38
  # sound. An empty one is its first.
39
39
  def self.page?(name) = name.empty? || PauseMenu::PAGES.any? { |page| page.to_s == name }
40
40
 
41
+ # Whether the family's machine has a key of the name. The Plus/4 has
42
+ # no RESTORE.
41
43
  def self.key?(name, family)
42
- name == "restore" || (family == :c128 && LOCKS.include?(name)) ||
44
+ (name == "restore" && family != :plus4) || (family == :c128 && LOCKS.include?(name)) ||
43
45
  !keyboard_key(name, family).nil? || !joystick_direction(name).nil?
44
46
  end
45
47
 
46
48
  # The family's Keyboard key for a name such as space, return or a,
47
- # or on the C128 esc or keypad1.
49
+ # on the C128 esc or keypad1, or on the Plus/4 help or crsr_up. The
50
+ # shifted cursor keys, such as cursor_up, are the C64's and the
51
+ # C128's.
48
52
  def self.keyboard_key(name, family)
49
- matrix = family == :c128 ? C128::KEYBOARD_MATRIX : Input::Keyboard::C64_MATRIX
50
- key = matrix.flatten.find { |candidate| candidate.to_s == name }
51
- key.nil? ? Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name } : key
53
+ key = keyboard_matrix(family).flatten.find { |candidate| candidate.to_s == name }
54
+ return key unless key.nil? && family != :plus4
55
+
56
+ Input::Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name }
57
+ end
58
+
59
+ def self.keyboard_matrix(family)
60
+ case family
61
+ when :c128 then C128::KEYBOARD_MATRIX
62
+ when :plus4 then Plus4::KeyboardPort::MATRIX
63
+ else Input::Keyboard::C64_MATRIX
64
+ end
52
65
  end
53
66
 
54
67
  # The direction of joy1-NAME or joy2-NAME, such as up or fire.
@@ -215,7 +228,7 @@ module Badline
215
228
 
216
229
  def inserted?(computer, what)
217
230
  return !computer.datasette.tape.nil? if what == "tape"
218
- return !computer.address_bus.cartridge.nil? if what == "cartridge"
231
+ return MediaSlots.cartridge?(computer) if what == "cartridge"
219
232
 
220
233
  MediaSlots.disk?(computer)
221
234
  end
@@ -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 and C128 each include it, and answer
7
+ # bus. C64, Vic20, C128 and Plus4 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
@@ -9,7 +9,7 @@ module Badline
9
9
  #
10
10
  # The machines badline builds by family, as the command line's
11
11
  # subcommand names them: the C64, Badline::C64, the VIC-20, Vic20, and the
12
- # C128 in C64 mode, C128.
12
+ # C128 in C64 mode, C128, and the 264 series, Plus4.
13
13
  #
14
14
  # A machine answers what the front end, Media, the runners and the
15
15
  # snapshots call on it:
@@ -49,22 +49,31 @@ module Badline
49
49
  # +release_display_key+, and +attach_drive1571+ and +drive1571+ for the
50
50
  # C128D's 1571, its true drive.
51
51
  #
52
+ # The Plus/4 answers the identity, clocking, lifecycle, video, sound and
53
+ # input parts, with nil for +control_ports+ and no RESTORE, and of the
54
+ # rest +ram+, +load_prg+, +type_text+, +mount+, +unmount+, +mounted?+,
55
+ # +datasette+, +attach_cartridge+, +attach_drive1541+, +drive1541+,
56
+ # +true_drive+, +plug_true_drive+, the 1581's, +install_debug_register+
57
+ # and +capture_output+, and besides +model_label+, +cartridge_slots+ and
58
+ # +detach_cartridges+ for its two cartridges.
59
+ #
52
60
  # Each machine includes Machine::Clocking for the clocking part.
53
61
  module Machine
54
62
  # Media the machine named doesn't take.
55
63
  class Unsupported < ArgumentError; end
56
64
 
57
- # A new machine of `family`, :c64, :vic20 or :c128, the model of it
58
- # `model` names. A C64's model is one of Model::ALL, with its SID
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
59
67
  # `sid_model` when not nil and an REU of `reu` K when not nil. A
60
68
  # VIC-20's is "pal", with the RAM expansion `ram` names
61
69
  # (Vic20::Bus::RAM_CONFIGURATIONS), unexpanded when nil. A C128's is
62
70
  # one of C128::Model::ALL, with its SID `sid_model` when not nil, built
63
- # for C64 mode. :plus4 is refused until the Plus/4 runs.
71
+ # for C64 mode. A Plus/4's is one of Plus4::Model::ALL or c116, with
72
+ # the model's own RAM.
64
73
  def self.build(family, model:, sid_model: nil, reu: nil, ram: nil)
65
74
  return vic20(model, ram) if family == :vic20
66
75
  return c128(model, sid_model, :c64) if family == :c128
67
- raise ArgumentError, "the Plus/4 isn't emulated yet" if family == :plus4
76
+ return Plus4.new(model:) if family == :plus4
68
77
  raise ArgumentError, "no machine family named #{family}" unless family == :c64
69
78
 
70
79
  profile = Model.named(model)
@@ -80,10 +89,11 @@ module Badline
80
89
  # only C64 mode runs: a .sid tune or a program that loads at the C64's
81
90
  # BASIC start. A VIC-20 without RAM named gets the expansion the media
82
91
  # needs (Media::Vic20Media.ram_for) and yields a line saying so. Raises
83
- # Unsupported for media a VIC-20 doesn't take.
92
+ # Unsupported for media a VIC-20 or a Plus/4 doesn't take.
84
93
  def self.for_options(options, media, &)
85
94
  family = options.family
86
95
  return vic20_for(media, options.model, options.ram, &) if family == :vic20
96
+ return plus4_for(media, options) if family == :plus4
87
97
 
88
98
  sid_model = options.sid_model || Media.sid_model(media, otherwise: nil)
89
99
  return build(:c64, model: options.model, sid_model:, reu: options.reu) unless family == :c128
@@ -117,6 +127,20 @@ module Badline
117
127
  end
118
128
  private_class_method :vic20_for
119
129
 
130
+ # A Plus/4 with the RAM --ram names (Plus4::Model::RAM_SIZES), or with
131
+ # the model's own without it, the true drive --drive-type names, the
132
+ # SID card --sid-card names, where --sid-card-address puts it, and the
133
+ # RAM expansion --ram-expansion names.
134
+ def self.plus4_for(media, options)
135
+ unsupported = !media.nil? && Media::Vic20Media::UNSUPPORTED.include?(File.extname(media).downcase)
136
+ raise Unsupported, "doesn't go in a Plus/4" if unsupported
137
+
138
+ Plus4.new(model: options.model, ram: options.ram_kb, true_drive_model: options.drive_type,
139
+ sid_card: options.sid_card, sid_card_address: options.sid_card_address || 0xfd40,
140
+ ram_expansion: options.ram_expansion)
141
+ end
142
+ private_class_method :plus4_for
143
+
120
144
  def self.c64_media?(media)
121
145
  return false if media.nil? || !File.file?(media)
122
146
 
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Media
5
+ # What the 264 series does with media of its own (Media.attach). A
6
+ # cartridge is a PLUS4 CARTRIDGE .crt, or a raw ROM image (.bin) that
7
+ # goes in the slot it's given, or by its size in cartridge 1's.
8
+ module Plus4Media
9
+ # The platform byte of a C16 or Plus/4 tape (Storage::TAP#platform).
10
+ TAPE_PLATFORM = 2
11
+
12
+ # The extension of a raw cartridge ROM image.
13
+ ROM_IMAGE = ".bin"
14
+
15
+ # The cartridge slots by xplus4's names, each as the bank and address
16
+ # a .crt's CHIP packet gives it (Plus4::Bus#attach_cartridge).
17
+ SLOTS = { "c1lo" => [0, 0x8000], "c1hi" => [0, 0xc000], "c2lo" => [1, 0x8000], "c2hi" => [1, 0xc000] }.freeze
18
+
19
+ SLOT_SIZE = 0x4000
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 == TAPE_PLATFORM ? message : "#{message}, a tape for another machine than the Plus/4"
25
+ end
26
+
27
+ # Whether the file at `path` is a raw cartridge ROM image.
28
+ def rom_image?(path) = File.extname(path).downcase == ROM_IMAGE
29
+
30
+ # The ROM chips, as Storage::CRTFile::Chips, of the cartridge at
31
+ # `path`: a .crt's, or a raw image's (#rom_chips).
32
+ def cartridge_chips(path, slot: nil)
33
+ return rom_chips(File.binread(path).bytes, slot:) if rom_image?(path)
34
+
35
+ Storage::CRTFile.new(path, machine: :plus4).chips
36
+ end
37
+
38
+ # A raw ROM image's chips: one in `slot`, one of SLOTS, when it's
39
+ # named. Otherwise up to 16K goes in cartridge 1's low slot, and
40
+ # up to 32K fills its high slot too.
41
+ def rom_chips(bytes, slot: nil)
42
+ return [slot_chip(bytes, slot)] if slot
43
+
44
+ size = bytes.length
45
+ unless size.between?(1, 2 * SLOT_SIZE)
46
+ raise Machine::Unsupported, "a #{size}-byte image is no Plus/4 cartridge"
47
+ end
48
+
49
+ chips = [chip(0, 0x8000, bytes[0, SLOT_SIZE])]
50
+ chips << chip(0, 0xc000, bytes[SLOT_SIZE..]) if size > SLOT_SIZE
51
+ chips
52
+ end
53
+
54
+ private
55
+
56
+ def slot_chip(bytes, slot)
57
+ bank, address = SLOTS.fetch(slot) { raise ArgumentError, "no Plus/4 cartridge slot named #{slot}" }
58
+ size = bytes.length
59
+ raise Machine::Unsupported, "a #{size}-byte image doesn't fit a 16K slot" unless size.between?(1, SLOT_SIZE)
60
+
61
+ chip(bank, address, bytes)
62
+ end
63
+
64
+ def chip(bank, address, data) = Storage::CRTFile::Chip.new(chip_type: 0, bank:, address:, data:)
65
+ end
66
+ end
67
+ end
68
+ end