badline 0.14.0 → 0.15.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/CLAUDE.md +15 -5
  4. data/README.md +68 -8
  5. data/doc/pinned-behaviour.md +145 -0
  6. data/doc/snapshot-format.md +188 -38
  7. data/doc/snapshots.md +18 -1
  8. data/lib/badline/audio/bare_player.rb +4 -8
  9. data/lib/badline/audio/machine_player.rb +2 -12
  10. data/lib/badline/c128/banks.rb +1 -3
  11. data/lib/badline/c128/bus.rb +19 -1
  12. data/lib/badline/c128/mmu_pages.rb +7 -0
  13. data/lib/badline/c64/bus/extra_sids.rb +80 -0
  14. data/lib/badline/c64/bus.rb +1 -0
  15. data/lib/badline/c64/cartridge/action_replay.rb +6 -2
  16. data/lib/badline/c64/cartridge/comal80.rb +1 -0
  17. data/lib/badline/c64/cartridge/dinamic.rb +3 -1
  18. data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
  19. data/lib/badline/c64/cartridge/fun_play.rb +2 -1
  20. data/lib/badline/c64/cartridge/game_system.rb +2 -1
  21. data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
  22. data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
  23. data/lib/badline/c64/cartridge/ocean.rb +1 -0
  24. data/lib/badline/c64/cartridge/pagefox.rb +1 -0
  25. data/lib/badline/c64/cartridge/rgcd.rb +2 -1
  26. data/lib/badline/c64/cartridge/super_games.rb +3 -2
  27. data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
  28. data/lib/badline/c64/cartridge.rb +3 -3
  29. data/lib/badline/c64/keyed_modules.rb +6 -2
  30. data/lib/badline/chips/ted/beam.rb +13 -14
  31. data/lib/badline/chips/ted/fetches.rb +199 -0
  32. data/lib/badline/chips/ted/keyed_state.rb +156 -0
  33. data/lib/badline/chips/ted/modes.rb +134 -0
  34. data/lib/badline/chips/ted/palette.rb +43 -0
  35. data/lib/badline/chips/ted/picture.rb +182 -0
  36. data/lib/badline/chips/ted/sound/output.rb +86 -0
  37. data/lib/badline/chips/ted/sound.rb +284 -0
  38. data/lib/badline/chips/ted.rb +53 -6
  39. data/lib/badline/chips/tia.rb +12 -0
  40. data/lib/badline/drives/datasette/keyed_state.rb +7 -2
  41. data/lib/badline/drives/datasette.rb +22 -5
  42. data/lib/badline/frontend/boot.rb +3 -1
  43. data/lib/badline/frontend/controls.rb +7 -3
  44. data/lib/badline/frontend/keys.rb +14 -2
  45. data/lib/badline/frontend/media_slots.rb +11 -1
  46. data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
  47. data/lib/badline/frontend/menu/menu_media.rb +13 -3
  48. data/lib/badline/frontend/menu/menu_pages.rb +8 -8
  49. data/lib/badline/frontend/menu/menu_ports.rb +13 -1
  50. data/lib/badline/frontend/screen.rb +33 -12
  51. data/lib/badline/frontend/screens.rb +2 -1
  52. data/lib/badline/frontend/sound.rb +4 -4
  53. data/lib/badline/frontend/timeline.rb +19 -6
  54. data/lib/badline/machine/settling.rb +1 -1
  55. data/lib/badline/machine.rb +26 -6
  56. data/lib/badline/media/plus4_media.rb +68 -0
  57. data/lib/badline/media/program.rb +20 -6
  58. data/lib/badline/media.rb +33 -12
  59. data/lib/badline/options/family.rb +1 -0
  60. data/lib/badline/options/help.rb +30 -6
  61. data/lib/badline/options/table.rb +15 -2
  62. data/lib/badline/options/validation.rb +26 -4
  63. data/lib/badline/options.rb +37 -9
  64. data/lib/badline/plus4/bus.rb +77 -14
  65. data/lib/badline/plus4/cassette_sense.rb +21 -0
  66. data/lib/badline/plus4/keyboard_port.rb +35 -10
  67. data/lib/badline/plus4/keyed_modules.rb +99 -0
  68. data/lib/badline/plus4/keyed_parts.rb +143 -0
  69. data/lib/badline/plus4/keyed_state.rb +41 -0
  70. data/lib/badline/plus4.rb +92 -16
  71. data/lib/badline/snapshot/container.rb +2 -0
  72. data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
  73. data/lib/badline/snapshot/field_doc.rb +46 -7
  74. data/lib/badline/snapshot/image.rb +12 -3
  75. data/lib/badline/snapshot/plus4_setup.rb +39 -0
  76. data/lib/badline/snapshot.rb +9 -4
  77. data/lib/badline/storage/g64_image.rb +1 -1
  78. data/lib/badline/storage/tap.rb +21 -12
  79. data/lib/badline/support/checkpoint.rb +3 -2
  80. data/lib/badline/support/plus4_checkpoint.rb +42 -0
  81. data/lib/badline/version.rb +1 -1
  82. metadata +15 -1
@@ -12,13 +12,15 @@ module Badline
12
12
 
13
13
  def peek(addr)
14
14
  if addr < 0xde10
15
- @roml = bank(@banks, addr & 0x0f)
15
+ @bank = addr & 0x0f
16
+ @roml = bank(@banks, @bank)
16
17
  changed!
17
18
  end
18
19
  open_bus(addr)
19
20
  end
20
21
 
21
22
  def reset
23
+ @bank = 0
22
24
  @roml = bank(@banks, 0)
23
25
  changed!
24
26
  end
@@ -64,6 +64,7 @@ module Badline
64
64
  def windows = [*@roml_banks, *@romh_banks, EMPTY_BANK]
65
65
 
66
66
  def select(number)
67
+ @bank = number
67
68
  @io_bank = @roml = bank(@roml_banks, number)
68
69
  @romh = bank(@romh_banks, number)
69
70
  end
@@ -10,7 +10,8 @@ module Badline
10
10
  def poke(addr, value)
11
11
  return if addr > 0xdeff
12
12
 
13
- @roml = bank(@banks, self.class.bank_number(value))
13
+ @bank = self.class.bank_number(value)
14
+ @roml = bank(@banks, @bank)
14
15
  case value & 0xc6
15
16
  when 0x00 then self.mode = :rom8k
16
17
  when 0x86 then self.mode = :off
@@ -27,7 +27,8 @@ module Badline
27
27
  private
28
28
 
29
29
  def select(addr)
30
- @roml = bank(@banks, addr & 0x3f)
30
+ @bank = addr & 0x3f
31
+ @roml = bank(@banks, @bank)
31
32
  changed!
32
33
  end
33
34
 
@@ -6,9 +6,13 @@ module Badline
6
6
  # A cartridge's fields in a keyed snapshot module (Snapshot::Keyed,
7
7
  # BL-CART): the CRT image it was built from and its jumpers, which
8
8
  # build it again (Cartridge.from_fields), the EXROM and GAME lines,
9
- # its pull on NMI, the button, and where ROML and ROMH point, each by
10
- # the name of the bank or view it shows (window_labels), and whatever
11
- # each mapper adds.
9
+ # its pull on NMI, the button, and each mapper's own registers, from
10
+ # which a load works out where ROML and ROMH point again.
11
+ #
12
+ # Snapshots from 0.14.0 name the bank or view each window shows
13
+ # instead (window_labels), under roml and romh. A load reads those
14
+ # when the mapper's register isn't there, and works the register out
15
+ # from the windows.
12
16
  module KeyedState
13
17
  def save_fields(out)
14
18
  save_setup_fields(out)
@@ -34,12 +38,28 @@ module Badline
34
38
 
35
39
  def save_jumper_fields(_out); end
36
40
 
37
- def save_mapper_fields(out)
38
- save_window_fields(out)
39
- end
41
+ def save_mapper_fields(_out); end
42
+
43
+ def load_mapper_fields(_input); end
44
+
45
+ # The register at `key`, cut to `mask`, or nil for a snapshot that
46
+ # names the windows instead, with the windows put back from their
47
+ # names.
48
+ def latch_or_windows(input, key, default, mask)
49
+ return input.masked(key, default, mask) if input.tags.key?(key)
40
50
 
41
- def load_mapper_fields(input)
42
51
  load_window_fields(input)
52
+ nil
53
+ end
54
+
55
+ # The number of the bank among `banks` that `window` shows, or of the
56
+ # first the image leaves out for erased ROM.
57
+ def number_showing(banks, window)
58
+ number = banks.index { |each| each.equal?(window) }
59
+ return number if number
60
+ return 0 unless window.equal?(EMPTY_BANK)
61
+
62
+ banks.index(nil) || banks.length
43
63
  end
44
64
 
45
65
  # The name of each of `windows`, in the same order: bank<N> for the
@@ -51,21 +71,6 @@ module Badline
51
71
  [*Array.new(banks.length) { |number| "bank#{number}" }, "empty"]
52
72
  end
53
73
 
54
- def save_window_fields(out)
55
- choices = windows
56
- return unless choices
57
-
58
- labels = window_labels
59
- save_window(out, "roml", @roml, choices, labels)
60
- save_window(out, "romh", @romh, choices, labels)
61
- end
62
-
63
- def save_window(out, key, window, choices, labels)
64
- return if window.nil?
65
-
66
- out.text(key, labels.fetch(window_index(window, choices)))
67
- end
68
-
69
74
  def load_window_fields(input)
70
75
  choices = windows
71
76
  return unless choices
@@ -97,21 +102,104 @@ module Badline
97
102
  def from_jumper_fields(crt, _input) = new(crt)
98
103
  end
99
104
 
100
- # Two banks of ROM, one behind each window: roml<N> and romh<N>.
105
+ # One bank register, under bank, selecting the bank ROML shows.
106
+ module BankLatch
107
+ private
108
+
109
+ def save_mapper_fields(out)
110
+ out.int("bank", @bank)
111
+ end
112
+
113
+ def load_mapper_fields(input)
114
+ latch = latch_or_windows(input, "bank", @bank, bank_latch_mask)
115
+ if latch.nil?
116
+ @bank = number_showing(@banks, @roml)
117
+ else
118
+ @bank = latch
119
+ show_bank
120
+ end
121
+ end
122
+
123
+ def show_bank
124
+ @roml = bank(@banks, @bank)
125
+ end
126
+ end
127
+
128
+ # Two banks of ROM, one behind each window: roml<N> and romh<N>, both
129
+ # selected by one bank register, under bank.
101
130
  module SplitBanks
102
131
  private
103
132
 
133
+ def save_mapper_fields(out)
134
+ out.int("bank", @bank)
135
+ end
136
+
137
+ def load_mapper_fields(input)
138
+ latch = latch_or_windows(input, "bank", @bank, bank_latch_mask)
139
+ if latch.nil?
140
+ @bank = number_showing(@roml_banks, @roml)
141
+ else
142
+ @bank = latch
143
+ @roml = bank(@roml_banks, @bank)
144
+ @romh = bank(@romh_banks, @bank)
145
+ end
146
+ end
147
+
104
148
  def window_labels
105
149
  [*Array.new(@roml_banks.length) { |number| "roml#{number}" },
106
150
  *Array.new(@romh_banks.length) { |number| "romh#{number}" }, "empty"]
107
151
  end
108
152
  end
109
153
 
154
+ class Ocean
155
+ include BankLatch
156
+
157
+ private
158
+
159
+ def bank_latch_mask = @bank_mask
160
+
161
+ def show_bank = select_bank(@bank)
162
+ end
163
+
164
+ class FunPlay
165
+ include BankLatch
166
+
167
+ private
168
+
169
+ def bank_latch_mask = 0x0f
170
+ end
171
+
172
+ class GameSystem
173
+ include BankLatch
174
+
175
+ private
176
+
177
+ def bank_latch_mask = 0x3f
178
+ end
179
+
180
+ class Dinamic
181
+ include BankLatch
182
+
183
+ private
184
+
185
+ def bank_latch_mask = 0x0f
186
+ end
187
+
188
+ class MagicDesk
189
+ include BankLatch
190
+
191
+ private
192
+
193
+ def bank_latch_mask = @bank_mask
194
+ end
195
+
110
196
  class SuperGames
111
197
  include SplitBanks
112
198
 
113
199
  private
114
200
 
201
+ def bank_latch_mask = 0x03
202
+
115
203
  def save_mapper_fields(out)
116
204
  super
117
205
  out.bool("locked", @locked)
@@ -125,6 +213,10 @@ module Badline
125
213
 
126
214
  class Comal80
127
215
  include SplitBanks
216
+
217
+ private
218
+
219
+ def bank_latch_mask = 0x07
128
220
  end
129
221
 
130
222
  class FinalCartridge3
@@ -132,6 +224,8 @@ module Badline
132
224
 
133
225
  private
134
226
 
227
+ def bank_latch_mask = @bank_mask
228
+
135
229
  def save_mapper_fields(out)
136
230
  super
137
231
  out.bool("register_visible", @register_visible).bool("phi1_ultimax", @phi1_ultimax)
@@ -146,8 +240,12 @@ module Badline
146
240
  end
147
241
 
148
242
  class RGCD
243
+ include BankLatch
244
+
149
245
  private
150
246
 
247
+ def bank_latch_mask = @bank_mask
248
+
151
249
  def save_mapper_fields(out)
152
250
  super
153
251
  out.bool("disabled", @disabled)
@@ -162,12 +260,11 @@ module Badline
162
260
  class Zaxxon
163
261
  class High
164
262
  def save_fields(out)
165
- out.int("high_bank", [*@banks, EMPTY_BANK].index { |bank| bank.equal?(@bank) }.to_i)
263
+ out.int("high_bank", @number)
166
264
  end
167
265
 
168
266
  def load_fields(input)
169
- choices = [*@banks, EMPTY_BANK]
170
- @bank = choices.fetch(input.int("high_bank", 0).clamp(0, choices.length - 1))
267
+ select(input.int("high_bank", 0).clamp(0, @banks.length))
171
268
  end
172
269
  end
173
270
 
@@ -204,18 +301,35 @@ module Badline
204
301
  end
205
302
 
206
303
  def save_mapper_fields(out)
207
- super
208
- out.text("rom", window_labels.fetch(window_index(@rom, windows))).bool("active", @active)
209
- out.bool("io2_ram", @io2_ram).bytes("ram", @ram_data)
304
+ out.int("control", @control).bool("active", @active).bytes("ram", @ram_data)
210
305
  end
211
306
 
212
307
  def load_mapper_fields(input)
213
- super
308
+ latch = latch_or_windows(input, "control", @control, 0xff)
309
+ if latch.nil?
310
+ load_legacy_fields(input)
311
+ else
312
+ @control = latch
313
+ @rom = bank(@rom_banks, (@control >> 3) & 0x03)
314
+ configure(@control)
315
+ end
316
+ @active = input.bool("active", @active)
317
+ input.bytes_into("ram", @ram_data)
318
+ end
319
+
320
+ # The ROM bank by name, and whether I/O 2 shows the RAM, from which
321
+ # the register is worked out.
322
+ def load_legacy_fields(input)
214
323
  labels = window_labels
215
324
  @rom = window_named(input.choice("rom", labels, "rom0"), windows, labels)
216
- @active = input.bool("active", @active)
217
325
  @io2_ram = input.bool("io2_ram", @io2_ram)
218
- input.bytes_into("ram", @ram_data)
326
+ number = number_showing(@rom_banks, @rom) << 3
327
+ @control = if @roml.equal?(@contended) || @romh.equal?(@isolated)
328
+ 0x22 | number
329
+ else
330
+ mode = @exrom.zero? ? 1 - @game : 3 - @game
331
+ mode | (@io2_ram ? 0x20 : 0) | number
332
+ end
219
333
  end
220
334
  end
221
335
 
@@ -237,12 +351,19 @@ module Badline
237
351
  end
238
352
 
239
353
  def save_mapper_fields(out)
240
- super
354
+ out.int("control", @control)
241
355
  @ram_banks.each_with_index { |ram, number| out.bytes("ram#{number}.data", ram.data) }
242
356
  end
243
357
 
244
358
  def load_mapper_fields(input)
245
- super
359
+ latch = latch_or_windows(input, "control", @control, 0xff)
360
+ if latch.nil?
361
+ selected = (0...8).find { |select| window(select >> 1, select & 0x01) == [@roml, @romh] } || 0
362
+ @control = (selected << 1) | (@exrom == 1 && @game == 1 ? 0x10 : 0)
363
+ else
364
+ @control = latch
365
+ @roml, @romh = window((@control >> 2) & 0x03, (@control >> 1) & 0x01)
366
+ end
246
367
  @ram_banks.each_with_index { |ram, number| input.bytes_into("ram#{number}.data", ram.data) }
247
368
  end
248
369
  end
@@ -11,7 +11,8 @@ module Badline
11
11
  return if addr > 0xdeff
12
12
 
13
13
  @exrom = value.anybits?(0x80) ? 1 : 0
14
- @roml = bank(@banks, value & @bank_mask)
14
+ @bank = value & @bank_mask
15
+ @roml = bank(@banks, @bank)
15
16
  changed!
16
17
  end
17
18
 
@@ -22,6 +22,7 @@ module Badline
22
22
 
23
23
  # In 16K mode the selected bank shows through ROMH as well.
24
24
  def select_bank(number)
25
+ @bank = number
25
26
  @roml = bank(@banks, number)
26
27
  @romh = @roml if game.zero?
27
28
  end
@@ -33,6 +33,7 @@ module Badline
33
33
  def windows = [*@roml_banks, *@romh_banks, *@ram_banks, @open_bus || EMPTY_BANK, EMPTY_BANK]
34
34
 
35
35
  def select(value)
36
+ @control = value
36
37
  half = (value >> 1) & 0x01
37
38
  chip = (value >> 2) & 0x03
38
39
  @roml, @romh = window(chip, half)
@@ -35,7 +35,8 @@ module Badline
35
35
  @disabled = true
36
36
  self.mode = :off
37
37
  else
38
- @roml = bank(@banks, value & @bank_mask)
38
+ @bank = value & @bank_mask
39
+ @roml = bank(@banks, @bank)
39
40
  end
40
41
  end
41
42
 
@@ -24,8 +24,9 @@ module Badline
24
24
  def windows = [*@roml_banks, *@romh_banks, EMPTY_BANK]
25
25
 
26
26
  def select(value)
27
- @roml = bank(@roml_banks, value & 0x03)
28
- @romh = bank(@romh_banks, value & 0x03)
27
+ @bank = value & 0x03
28
+ @roml = bank(@roml_banks, @bank)
29
+ @romh = bank(@romh_banks, @bank)
29
30
  self.mode = value.anybits?(0x04) ? :off : :rom16k
30
31
  @locked = value.anybits?(0x08)
31
32
  end
@@ -26,6 +26,7 @@ module Badline
26
26
  end
27
27
 
28
28
  def select(number)
29
+ @number = number
29
30
  @bank = @banks[number] || EMPTY_BANK
30
31
  end
31
32
 
@@ -152,9 +152,9 @@ module Badline
152
152
 
153
153
  private
154
154
 
155
- # The banks and views ROML and ROMH can point at, which a snapshot
156
- # names them by: the mapper's banks, or nothing for a mapper whose
157
- # windows stay where install_chips put them.
155
+ # The banks and views ROML and ROMH can point at, which a 0.14.0
156
+ # snapshot names them by: the mapper's banks, or nothing for a mapper
157
+ # whose windows stay where install_chips put them.
158
158
  def windows
159
159
  [*@banks, EMPTY_BANK] if @banks
160
160
  end
@@ -4,8 +4,9 @@ module Badline
4
4
  class C64
5
5
  # The whole machine as keyed snapshot modules (Snapshot::Keyed), in the
6
6
  # order a load applies them: BL-MACHINE with the setup, the memories,
7
- # the cartridge, the CPU with its port, the chips, an REU, the
8
- # datasette, device 8 through the traps and the true drives. A load
7
+ # the cartridge, the CPU with its port, the chips with any extra SIDs
8
+ # (Bus::ExtraSIDs), an REU, the datasette, device 8 through the traps
9
+ # and the true drives. A load
9
10
  # reads the CPU's port before the chips, so the banks are mapped as the
10
11
  # chips load, and the CPU's own fields after them. What each unit works
11
12
  # out again from its fields comes last, once every unit is loaded.
@@ -60,6 +61,7 @@ module Badline
60
61
  sections << Snapshot::Keyed.section("BL-CIA1") { |out| @cia1.save_fields(out) }
61
62
  sections << Snapshot::Keyed.section("BL-CIA2") { |out| @cia2.save_fields(out) }
62
63
  sections << Snapshot::Keyed.section("BL-SID") { |out| @sid.save_fields(out) }
64
+ bus.save_extra_sid_modules(sections)
63
65
  end
64
66
 
65
67
  def load_chip_modules(modules, cpu)
@@ -67,6 +69,7 @@ module Badline
67
69
  @cia1.load_fields(modules.required("BL-CIA1"))
68
70
  @cia2.load_fields(modules.required("BL-CIA2"))
69
71
  @sid.load_fields(modules.required("BL-SID"))
72
+ address_bus.load_extra_sid_modules(modules)
70
73
  @reu&.load_fields(modules.required("BL-REU"))
71
74
  @cpu.load_fields(cpu)
72
75
  end
@@ -94,6 +97,7 @@ module Badline
94
97
  @cia1.restored!
95
98
  @cia2.restored!
96
99
  @sid.restored!
100
+ address_bus.extra_sids.each(&:restored!)
97
101
  @reu&.restored!
98
102
  machine_restored!
99
103
  end
@@ -46,6 +46,11 @@ module Badline
46
46
  DOUBLE_COLUMNS = (43..49)
47
47
  LAST_DISPLAY_LINE = 203
48
48
 
49
+ # The DMA a line makes: none, the attributes or the pointers.
50
+ NO_DMA = 0
51
+ ATTRIBUTE_DMA = 1
52
+ POINTER_DMA = 2
53
+
49
54
  # The single clock cycle in the line, 0-56.
50
55
  attr_reader :column
51
56
 
@@ -67,7 +72,6 @@ module Badline
67
72
  def reset_beam
68
73
  @column = 0
69
74
  @line = 0
70
- @dma_line = false
71
75
  @ba_low = false
72
76
  @cpu_slots = 1
73
77
  end
@@ -79,14 +83,19 @@ module Badline
79
83
  return unless column == LINE_STEP_COLUMN
80
84
 
81
85
  line = @line + 1
82
- line = 0 if line >= lines_per_frame
86
+ if line >= lines_per_frame
87
+ line = 0
88
+ end_frame
89
+ end
90
+ step_row_line(line)
91
+ step_scanline(line)
83
92
  self.line = line
84
93
  end
85
94
 
95
+ # The clock for the cycle. Freeze leaves single clock, without DMA.
86
96
  def decide_clock
87
97
  column = @column
88
- @dma_line = dma_line? if column == BA_COLUMN
89
- if @dma_line && (column >= BA_COLUMN || column <= DMA_END_COLUMN)
98
+ if @dma_kind != NO_DMA && (column >= BA_COLUMN || column <= DMA_END_COLUMN) && !frozen?
90
99
  @ba_low = true
91
100
  @cpu_slots = column >= DMA_COLUMN || column <= DMA_END_COLUMN ? 0 : 1
92
101
  return
@@ -103,16 +112,6 @@ module Badline
103
112
  end
104
113
 
105
114
  def display_window? = @registers[0x06].anybits?(0x10) && @line <= LAST_DISPLAY_LINE
106
-
107
- def dma_line?
108
- return false unless @registers[0x06].anybits?(0x10)
109
-
110
- phase = (@line - (@registers[0x06] & 0x07)) & 0x07
111
- return false if phase > 1
112
-
113
- row = phase.zero? ? @line + 1 : @line
114
- row <= LAST_DISPLAY_LINE
115
- end
116
115
  end
117
116
  end
118
117
  end