badline 0.5.1 → 0.6.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 (173) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +82 -10
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +20 -23
  7. data/lib/badline/address_bus/saved_state.rb +72 -0
  8. data/lib/badline/address_bus.rb +11 -1
  9. data/lib/badline/audio/cli.rb +18 -5
  10. data/lib/badline/audio/console.rb +2 -1
  11. data/lib/badline/audio.rb +0 -3
  12. data/lib/badline/cartridge/action_replay.rb +15 -0
  13. data/lib/badline/cartridge/comal80.rb +8 -0
  14. data/lib/badline/cartridge/dinamic.rb +8 -0
  15. data/lib/badline/cartridge/easy_flash.rb +20 -1
  16. data/lib/badline/cartridge/epyx_fastload.rb +8 -0
  17. data/lib/badline/cartridge/final_cartridge3.rb +12 -0
  18. data/lib/badline/cartridge/flash.rb +41 -0
  19. data/lib/badline/cartridge/fun_play.rb +8 -0
  20. data/lib/badline/cartridge/g_mod2.rb +12 -0
  21. data/lib/badline/cartridge/game_system.rb +8 -0
  22. data/lib/badline/cartridge/geo_ram.rb +16 -0
  23. data/lib/badline/cartridge/magic_desk.rb +8 -0
  24. data/lib/badline/cartridge/ocean.rb +8 -0
  25. data/lib/badline/cartridge/pagefox.rb +12 -0
  26. data/lib/badline/cartridge/retro_replay.rb +44 -1
  27. data/lib/badline/cartridge/rgcd.rb +10 -0
  28. data/lib/badline/cartridge/super_games.rb +10 -0
  29. data/lib/badline/cartridge/zaxxon.rb +16 -0
  30. data/lib/badline/cartridge.rb +72 -0
  31. data/lib/badline/cia/interrupt_register.rb +17 -0
  32. data/lib/badline/cia/serial.rb +44 -0
  33. data/lib/badline/cia/state.rb +47 -0
  34. data/lib/badline/cia/timer.rb +33 -0
  35. data/lib/badline/cia.rb +8 -0
  36. data/lib/badline/computer/attachments.rb +132 -0
  37. data/lib/badline/computer/saved_state.rb +116 -0
  38. data/lib/badline/computer.rb +13 -33
  39. data/lib/badline/cpu/boundary.rb +54 -0
  40. data/lib/badline/cpu/core.rb +165 -0
  41. data/lib/badline/cpu/microcode.rb +8 -0
  42. data/lib/badline/cpu/saved_state.rb +115 -0
  43. data/lib/badline/cpu.rb +3 -212
  44. data/lib/badline/datasette.rb +27 -0
  45. data/lib/badline/drive1541/bus.rb +13 -0
  46. data/lib/badline/drive1541/cpu.rb +29 -0
  47. data/lib/badline/drive1541/disk.rb +7 -2
  48. data/lib/badline/drive1541/disk_state.rb +80 -0
  49. data/lib/badline/drive1541/mechanism.rb +2 -0
  50. data/lib/badline/drive1541/mechanism_state.rb +67 -0
  51. data/lib/badline/drive1541/rotation.rb +10 -0
  52. data/lib/badline/drive1541/serial_port.rb +10 -0
  53. data/lib/badline/drive1541.rb +35 -0
  54. data/lib/badline/ffi.rb +157 -0
  55. data/lib/badline/frontend/app.rb +225 -0
  56. data/lib/badline/frontend/audio_sink.rb +72 -0
  57. data/lib/badline/frontend/boot.rb +52 -0
  58. data/lib/badline/frontend/controls.rb +163 -0
  59. data/lib/badline/frontend/drive_led.rb +51 -0
  60. data/lib/badline/frontend/frame_rate.rb +83 -0
  61. data/lib/badline/frontend/gamepads.rb +76 -0
  62. data/lib/badline/frontend/keys.rb +48 -0
  63. data/lib/badline/frontend/pacer.rb +100 -0
  64. data/lib/badline/frontend/pad_port.rb +61 -0
  65. data/lib/badline/frontend/screen.rb +65 -0
  66. data/lib/badline/frontend/screenshot.rb +22 -0
  67. data/lib/badline/frontend/snapshots.rb +60 -0
  68. data/lib/badline/frontend/sound.rb +153 -0
  69. data/lib/badline/frontend/timeline.rb +171 -0
  70. data/lib/badline/frontend.rb +18 -0
  71. data/lib/badline/kernal_trap/channel.rb +27 -0
  72. data/lib/badline/kernal_trap/drive/channels.rb +25 -0
  73. data/lib/badline/kernal_trap/drive/memory.rb +9 -0
  74. data/lib/badline/kernal_trap/drive/status.rb +8 -0
  75. data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
  76. data/lib/badline/kernal_trap/drive.rb +19 -0
  77. data/lib/badline/kernal_trap/save.rb +8 -0
  78. data/lib/badline/kernal_trap/serial.rb +16 -0
  79. data/lib/badline/memory.rb +8 -0
  80. data/lib/badline/options/event.rb +94 -0
  81. data/lib/badline/options/help.rb +78 -0
  82. data/lib/badline/options/option.rb +29 -0
  83. data/lib/badline/options/table.rb +62 -0
  84. data/lib/badline/options.rb +181 -144
  85. data/lib/badline/ram_expansion/plus256k.rb +16 -0
  86. data/lib/badline/ram_expansion/plus60k.rb +12 -0
  87. data/lib/badline/ram_expansion/unexpanded.rb +6 -0
  88. data/lib/badline/reu/dma/saved_state.rb +50 -0
  89. data/lib/badline/reu/dma.rb +56 -11
  90. data/lib/badline/reu/ram.rb +15 -0
  91. data/lib/badline/reu/saved_state.rb +53 -0
  92. data/lib/badline/reu.rb +23 -10
  93. data/lib/badline/sdl.rb +123 -51
  94. data/lib/badline/sid/envelope/state.rb +68 -0
  95. data/lib/badline/sid/envelope.rb +7 -0
  96. data/lib/badline/sid/filter.rb +38 -0
  97. data/lib/badline/sid/voice.rb +10 -0
  98. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  99. data/lib/badline/sid/waveform/state.rb +66 -0
  100. data/lib/badline/sid/waveform.rb +4 -3
  101. data/lib/badline/sid.rb +34 -0
  102. data/lib/badline/snapshot/container.rb +117 -0
  103. data/lib/badline/snapshot/fields.rb +54 -0
  104. data/lib/badline/snapshot/image.rb +70 -0
  105. data/lib/badline/snapshot/machine_state.rb +77 -0
  106. data/lib/badline/snapshot/setup.rb +58 -0
  107. data/lib/badline/snapshot/state.rb +178 -0
  108. data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
  109. data/lib/badline/snapshot/vice/cias.rb +89 -0
  110. data/lib/badline/snapshot/vice/cias_import.rb +70 -0
  111. data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
  112. data/lib/badline/snapshot/vice/peripherals.rb +47 -0
  113. data/lib/badline/snapshot/vice/restore.rb +125 -0
  114. data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
  115. data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
  116. data/lib/badline/snapshot/vice/vicii.rb +119 -0
  117. data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
  118. data/lib/badline/snapshot/vice.rb +74 -0
  119. data/lib/badline/snapshot.rb +52 -0
  120. data/lib/badline/storage/crt_file.rb +9 -3
  121. data/lib/badline/storage/d64_image.rb +2 -0
  122. data/lib/badline/storage/d71_image.rb +2 -0
  123. data/lib/badline/storage/d81_image.rb +2 -0
  124. data/lib/badline/storage/disk_image.rb +11 -2
  125. data/lib/badline/storage/host_directory.rb +5 -0
  126. data/lib/badline/storage/t64.rb +6 -0
  127. data/lib/badline/storage/tap.rb +10 -1
  128. data/lib/badline/storage/unavailable.rb +20 -0
  129. data/lib/badline/storage.rb +34 -0
  130. data/lib/badline/time_of_day.rb +56 -0
  131. data/lib/badline/version.rb +1 -1
  132. data/lib/badline/via/control_lines.rb +15 -0
  133. data/lib/badline/via/interrupt_register.rb +9 -0
  134. data/lib/badline/via/shift_register.rb +13 -0
  135. data/lib/badline/via/state.rb +42 -0
  136. data/lib/badline/via/timer1.rb +13 -0
  137. data/lib/badline/via/timer2.rb +18 -0
  138. data/lib/badline/via.rb +2 -0
  139. data/lib/badline/vic/bank.rb +21 -0
  140. data/lib/badline/vic/border_mask.rb +10 -0
  141. data/lib/badline/vic/collisions.rb +11 -0
  142. data/lib/badline/vic/color_patches.rb +11 -0
  143. data/lib/badline/vic/display_state.rb +29 -1
  144. data/lib/badline/vic/graphics_shifter.rb +26 -0
  145. data/lib/badline/vic/register_log.rb +18 -0
  146. data/lib/badline/vic/registers.rb +13 -2
  147. data/lib/badline/vic/sequencer.rb +12 -0
  148. data/lib/badline/vic/sequencer_state.rb +89 -0
  149. data/lib/badline/vic/sprite/internal_bus.rb +11 -0
  150. data/lib/badline/vic/sprite/saved_state.rb +64 -0
  151. data/lib/badline/vic/sprite.rb +2 -0
  152. data/lib/badline/vic/sprites.rb +3 -0
  153. data/lib/badline/vic/sprites_state.rb +57 -0
  154. data/lib/badline/vic.rb +109 -16
  155. data/lib/badline.rb +1 -0
  156. metadata +59 -18
  157. data/.claude/hooks/session-start.sh +0 -57
  158. data/.claude/settings.json +0 -14
  159. data/lib/badline/audio/sdl_sink.rb +0 -70
  160. data/lib/badline/audio/stream.rb +0 -117
  161. data/lib/badline/gui/application.rb +0 -216
  162. data/lib/badline/gui/drive_led_pane.rb +0 -29
  163. data/lib/badline/gui/gamepads.rb +0 -97
  164. data/lib/badline/gui/joy_map.rb +0 -27
  165. data/lib/badline/gui/key_map.rb +0 -39
  166. data/lib/badline/gui/palette.rb +0 -35
  167. data/lib/badline/gui/pane.rb +0 -36
  168. data/lib/badline/gui/screen_pane.rb +0 -46
  169. data/lib/badline/gui/window.rb +0 -61
  170. data/lib/badline/gui.rb +0 -13
  171. data/lib/badline/sdl/events.rb +0 -47
  172. data/lib/badline/sdl/functions.rb +0 -74
  173. data/vendor/.gitkeep +0 -0
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/address_bus/saved_state"
3
4
  require "badline/address_bus/ultimax_pages"
4
5
 
5
6
  module Badline
@@ -88,6 +89,7 @@ module Badline
88
89
 
89
90
  @color_ram = ColorMemory.new(@vic)
90
91
  @open_bus = OpenBus.new(@vic)
92
+ @cpu_off_bus = false
91
93
 
92
94
  @port_ddr = 0x00
93
95
  @port_out = 0x00
@@ -153,11 +155,19 @@ module Badline
153
155
  @ram.poke(addr, @vic.phi1_data)
154
156
  addr.zero? ? @port_ddr = value : @port_out = value
155
157
  update_port!
156
- else
158
+ elsif !@cpu_off_bus
157
159
  @write_pages[addr >> 8].poke(addr, value)
158
160
  end
159
161
  end
160
162
 
163
+ # Runs a CPU cycle with the CPU off the bus, as an REU's /DMA line
164
+ # holds it: a write it makes reaches nothing but its own port.
165
+ def cycle_cpu_off_bus(cpu)
166
+ @cpu_off_bus = true
167
+ cpu.cycle!
168
+ @cpu_off_bus = false
169
+ end
170
+
161
171
  def inspect
162
172
  "#<#{self.class.name} port=#{format('0x%02x', @io_port.value)} " \
163
173
  "cartridge=#{@cartridge ? @cartridge.class.name : 'none'} " \
@@ -2,18 +2,31 @@
2
2
 
3
3
  module Badline
4
4
  module Audio
5
- # Runs `badline-ruby --headless` or `--audio-out` once its options have
6
- # parsed: picks the song and its length, then plays it or renders it
7
- # to a file. Played on a terminal, the console lets the listener step
5
+ # Runs `--headless` or `--audio-out`, in either build, once its options
6
+ # have parsed: picks the song and its length, then plays it or renders
7
+ # it to a file. Played on a terminal, the console lets the listener step
8
8
  # between songs.
9
9
  class CLI
10
10
  class Error < StandardError; end
11
11
 
12
+ # Plays or renders the tune as #run does, reports an error on stderr
13
+ # under the program's name, and returns the exit status.
14
+ def self.run(options, sink:, console:)
15
+ new(options, sink:, console:).run
16
+ 0
17
+ rescue Error => e
18
+ warn "#{options.program}: #{e.message}"
19
+ 1
20
+ rescue Interrupt
21
+ 130
22
+ end
23
+
12
24
  # Playing needs `sink`, which builds the audio device given the rate
13
25
  # to ask for and whether it has to be exact, and, on a terminal,
14
26
  # `console`, which builds the terminal to play on given the input and
15
- # output. The front end hands them in, so the native badline can hand
16
- # in its own: badline-ruby's are SDLSink and Console.
27
+ # output. Each build's entry point hands them in: both builds' sink is
28
+ # Frontend::AudioSink, and the console is badline-ruby's Console or
29
+ # the native badline's Native::Console.
17
30
  def initialize(options, sink: nil, console: nil, out: $stdout, input: $stdin)
18
31
  @options = options
19
32
  @out = out
@@ -6,7 +6,8 @@ require "io/wait"
6
6
  module Badline
7
7
  module Audio
8
8
  # The terminal of `badline-ruby --headless`, put in raw mode with
9
- # io/console.
9
+ # io/console. It is badline-ruby's side of the console seam, and
10
+ # exe/badline-ruby requires it: the native badline's is Native::Console.
10
11
  class Console < Terminal
11
12
  private
12
13
 
data/lib/badline/audio.rb CHANGED
@@ -7,9 +7,6 @@ require "badline/audio/bare_player"
7
7
  require "badline/audio/machine_player"
8
8
  require "badline/audio/renderer"
9
9
  require "badline/audio/playback"
10
- require "badline/audio/sdl_sink"
11
- require "badline/audio/stream"
12
10
  require "badline/audio/terminal"
13
- require "badline/audio/console"
14
11
  require "badline/audio/jukebox"
15
12
  require "badline/audio/cli"
@@ -64,6 +64,21 @@ module Badline
64
64
 
65
65
  private
66
66
 
67
+ def save_mapper(out)
68
+ save_windows(out, windows)
69
+ out.int(window_index(@rom, windows)).boolean(@active).boolean(@io2_ram).blob(@ram_data)
70
+ end
71
+
72
+ def load_mapper(input)
73
+ load_windows(input, windows)
74
+ @rom = window_at(input.int, windows)
75
+ @active = input.boolean?
76
+ @io2_ram = input.boolean?
77
+ input.blob_into(@ram_data)
78
+ end
79
+
80
+ def windows = [@through, @isolated, @contended, *@rom_banks, EMPTY_BANK]
81
+
67
82
  def io2_peek(addr)
68
83
  @io2_ram ? @ram_data[IO2_PAGE | (addr & 0xff)] : @rom.peek(addr)
69
84
  end
@@ -33,6 +33,14 @@ module Badline
33
33
 
34
34
  private
35
35
 
36
+ def save_mapper(out)
37
+ save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
38
+ end
39
+
40
+ def load_mapper(input)
41
+ load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
42
+ end
43
+
36
44
  def select(number, mode)
37
45
  @roml = bank(@roml_banks, number)
38
46
  @romh = bank(@romh_banks, number)
@@ -24,6 +24,14 @@ module Badline
24
24
 
25
25
  private
26
26
 
27
+ def save_mapper(out)
28
+ save_windows(out, [*@banks, EMPTY_BANK])
29
+ end
30
+
31
+ def load_mapper(input)
32
+ load_windows(input, [*@banks, EMPTY_BANK])
33
+ end
34
+
27
35
  def install_chips(chips)
28
36
  @banks = banks_from(chips).first
29
37
  self.mode = :rom8k
@@ -78,9 +78,28 @@ module Badline
78
78
  private
79
79
 
80
80
  def select_bank
81
+ map_bank
82
+ changed!
83
+ end
84
+
85
+ def map_bank
81
86
  @roml = @low_flash.window(@bank * BANK_SIZE)
82
87
  @romh = @high_flash.window(@bank * BANK_SIZE)
83
- changed!
88
+ end
89
+
90
+ def save_mapper(out)
91
+ out.int(@bank).int(@control).blob(@io_ram)
92
+ @low_flash.save_state(out)
93
+ @high_flash.save_state(out)
94
+ end
95
+
96
+ def load_mapper(input)
97
+ @bank = input.int
98
+ @control = input.int
99
+ input.blob_into(@io_ram)
100
+ @low_flash.load_state(input)
101
+ @high_flash.load_state(input)
102
+ map_bank
84
103
  end
85
104
 
86
105
  def apply_control
@@ -56,6 +56,14 @@ module Badline
56
56
 
57
57
  private
58
58
 
59
+ def save_mapper(out)
60
+ out.int(@discharged_at)
61
+ end
62
+
63
+ def load_mapper(input)
64
+ @discharged_at = input.int
65
+ end
66
+
59
67
  def discharge!
60
68
  @discharged_at = @clock ? @clock.call : 0
61
69
  end
@@ -62,6 +62,18 @@ module Badline
62
62
 
63
63
  private
64
64
 
65
+ def save_mapper(out)
66
+ save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
67
+ out.boolean(@register_visible).boolean(@phi1_ultimax)
68
+ end
69
+
70
+ def load_mapper(input)
71
+ load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
72
+ @io_bank = @roml
73
+ @register_visible = input.boolean?
74
+ @phi1_ultimax = input.boolean?
75
+ end
76
+
65
77
  def select(number)
66
78
  @io_bank = @roml = bank(@roml_banks, number)
67
79
  @romh = bank(@romh_banks, number)
@@ -2,6 +2,37 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # A Flash chip's state, for a snapshot.
6
+ module FlashState
7
+ STATES = %i[read program_setup autoselect programming erase_window erasing suspended chip_erase
8
+ program_error].freeze
9
+
10
+ # The array, the command state, a timed operation's end on the
11
+ # machine's clock, and the windows handed out so far, which load_state
12
+ # builds anew.
13
+ def save_state(out)
14
+ out.blob(@data).int(STATES.index(@state)).int(STATES.index(@base_state)).int(@cycle).int(@toggle)
15
+ out.optional_int(@done_at).optional_int(@programmed).optional_int(@remaining)
16
+ out.boolean(!@erasing_sectors.nil?)
17
+ out.ints(@erasing_sectors) if @erasing_sectors
18
+ out.ints(@windows.keys).ints(@status_windows.keys)
19
+ end
20
+
21
+ def load_state(input)
22
+ input.blob_into(@data)
23
+ @state = STATES.fetch(input.int).to_sym
24
+ @base_state = STATES.fetch(input.int).to_sym
25
+ @cycle = input.int
26
+ @toggle = input.int
27
+ @done_at = input.optional_int
28
+ @programmed = input.optional_int
29
+ @remaining = input.optional_int
30
+ @erasing_sectors = nil
31
+ @erasing_sectors = input.ints if input.boolean?
32
+ rebuild_windows(input.ints, input.ints)
33
+ end
34
+ end
35
+
5
36
  # An AMD 29F0x0 flash chip, driven through the JEDEC command set in the
6
37
  # AMD datasheets. Each command opens with two unlock cycles, $AA to the
7
38
  # first unlock address and $55 to the second, and a third write picks
@@ -66,6 +97,8 @@ module Badline
66
97
  alias [] peek
67
98
  end
68
99
 
100
+ include FlashState
101
+
69
102
  attr_reader :data, :state
70
103
  attr_writer :clock
71
104
 
@@ -95,6 +128,14 @@ module Badline
95
128
  end
96
129
  end
97
130
 
131
+ # Hands out new windows at the bases given, as #window would have.
132
+ def rebuild_windows(bases, status_bases)
133
+ @windows = {}
134
+ @status_windows = {}
135
+ bases.each { |base| @windows[base] = Window.new(self, base) }
136
+ status_bases.each { |base| @status_windows[base] = StatusWindow.new(self, base) }
137
+ end
138
+
98
139
  def array_mode?
99
140
  @state == :read || @state == :program_setup
100
141
  end
@@ -28,6 +28,14 @@ module Badline
28
28
 
29
29
  private
30
30
 
31
+ def save_mapper(out)
32
+ save_windows(out, [*@banks, EMPTY_BANK])
33
+ end
34
+
35
+ def load_mapper(input)
36
+ load_windows(input, [*@banks, EMPTY_BANK])
37
+ end
38
+
31
39
  def install_chips(chips)
32
40
  @banks = []
33
41
  chips.each { |chip| @banks[self.class.bank_number(chip.bank)] = rom_bank(chip.data) }
@@ -47,6 +47,18 @@ module Badline
47
47
  changed!
48
48
  end
49
49
 
50
+ def save_mapper(out)
51
+ out.int(@bank).boolean(@flash_writes)
52
+ @flash.save_state(out)
53
+ end
54
+
55
+ def load_mapper(input)
56
+ @bank = input.int
57
+ @flash_writes = input.boolean?
58
+ @flash.load_state(input)
59
+ @roml = @flash.window(@bank * BANK_SIZE)
60
+ end
61
+
50
62
  def install_chips(chips)
51
63
  data = Array.new(BANKS * BANK_SIZE, 0xff)
52
64
  chips.each do |chip|
@@ -25,6 +25,14 @@ module Badline
25
25
 
26
26
  private
27
27
 
28
+ def save_mapper(out)
29
+ save_windows(out, [*@banks, EMPTY_BANK])
30
+ end
31
+
32
+ def load_mapper(input)
33
+ load_windows(input, [*@banks, EMPTY_BANK])
34
+ end
35
+
28
36
  def select(addr)
29
37
  @roml = bank(@banks, addr & 0x3f)
30
38
  changed!
@@ -52,8 +52,24 @@ module Badline
52
52
  @block = @page = @window = 0
53
53
  end
54
54
 
55
+ # A GEO-RAM is built from its size alone.
56
+ def save_setup(out)
57
+ out.int(GEO_RAM_SETUP).int(@size)
58
+ end
59
+
55
60
  private
56
61
 
62
+ def save_mapper(out)
63
+ out.blob(@ram_data).int(@block).int(@page).int(@window)
64
+ end
65
+
66
+ def load_mapper(input)
67
+ input.blob_into(@ram_data)
68
+ @block = input.int
69
+ @page = input.int
70
+ @window = input.int
71
+ end
72
+
57
73
  def install_chips(_chips)
58
74
  @ram_data = Array.new(@size * 1024, 0)
59
75
  reset
@@ -20,6 +20,14 @@ module Badline
20
20
 
21
21
  private
22
22
 
23
+ def save_mapper(out)
24
+ save_windows(out, [*@banks, EMPTY_BANK])
25
+ end
26
+
27
+ def load_mapper(input)
28
+ load_windows(input, [*@banks, EMPTY_BANK])
29
+ end
30
+
23
31
  def install_chips(chips)
24
32
  @banks = banks_from(chips).first
25
33
  @bank_mask = (bank_mask(@banks) | 0x03) & 0x7f
@@ -19,6 +19,14 @@ module Badline
19
19
 
20
20
  private
21
21
 
22
+ def save_mapper(out)
23
+ save_windows(out, [*@banks, EMPTY_BANK])
24
+ end
25
+
26
+ def load_mapper(input)
27
+ load_windows(input, [*@banks, EMPTY_BANK])
28
+ end
29
+
22
30
  # In 16K mode the selected bank shows through ROMH as well.
23
31
  def select_bank(number)
24
32
  @roml = bank(@banks, number)
@@ -29,6 +29,18 @@ module Badline
29
29
 
30
30
  private
31
31
 
32
+ def save_mapper(out)
33
+ save_windows(out, windows)
34
+ @ram_banks.each { |ram| out.blob(ram.data) }
35
+ end
36
+
37
+ def load_mapper(input)
38
+ load_windows(input, windows)
39
+ @ram_banks.each { |ram| input.blob_into(ram.data) }
40
+ end
41
+
42
+ def windows = [*@roml_banks, *@romh_banks, *@ram_banks, @open_bus || EMPTY_BANK, EMPTY_BANK]
43
+
32
44
  def select(value)
33
45
  half = (value >> 1) & 0x01
34
46
  chip = (value >> 2) & 0x03
@@ -2,6 +2,44 @@
2
2
 
3
3
  module Badline
4
4
  class Cartridge
5
+ # A Retro Replay's registers, RAM and flash, for a snapshot.
6
+ module RetroReplayState
7
+ private
8
+
9
+ def save_jumpers(out)
10
+ out.boolean(true).boolean(@flash_jumper).boolean(@bank_jumper)
11
+ end
12
+
13
+ def save_mapper(out)
14
+ [@active, @frozen, @phi1_ultimax, @ram_enabled, @ram_at_a000, @allow_bank, @no_freeze, @reu_mapping,
15
+ @write_once].each { |flag| out.boolean(flag) }
16
+ out.int(@bank).int(RetroReplay::CONTROL_MODES.index(@config))
17
+ @ram_data.each { |data| out.blob(data) }
18
+ @flash.save_state(out)
19
+ end
20
+
21
+ def load_mapper(input)
22
+ load_flags(input)
23
+ @bank = input.int
24
+ @config = RetroReplay::CONTROL_MODES.fetch(input.int)
25
+ @ram_data.each { |data| input.blob_into(data) }
26
+ @flash.load_state(input)
27
+ map_windows
28
+ end
29
+
30
+ def load_flags(input)
31
+ @active = input.boolean?
32
+ @frozen = input.boolean?
33
+ @phi1_ultimax = input.boolean?
34
+ @ram_enabled = input.boolean?
35
+ @ram_at_a000 = input.boolean?
36
+ @allow_bank = input.boolean?
37
+ @no_freeze = input.boolean?
38
+ @reu_mapping = input.boolean?
39
+ @write_once = input.boolean?
40
+ end
41
+ end
42
+
5
43
  # Retro Replay: an Am29F010 flash chip of two 64K halves, each eight 8K
6
44
  # ROM banks, and four 8K RAM banks, with the Action Replay's register at
7
45
  # $DE00 and an extended one at $DE01. The Nordic Replay (subtype 1) adds
@@ -44,6 +82,7 @@ module Badline
44
82
  # the half in bit 5.
45
83
  class RetroReplay < Cartridge
46
84
  include Freezer
85
+ include RetroReplayState
47
86
 
48
87
  CONTROL_MODES = %i[rom8k rom16k off ultimax].freeze
49
88
  IO1_PAGE = 0x1e00
@@ -212,11 +251,15 @@ module Badline
212
251
  end
213
252
 
214
253
  def configure
254
+ map_windows
255
+ changed!
256
+ end
257
+
258
+ def map_windows
215
259
  self.mode = @config
216
260
  @rom = flash_window(@bank)
217
261
  @roml = roml_window
218
262
  @romh = romh_window
219
- changed!
220
263
  end
221
264
 
222
265
  def roml_window
@@ -28,6 +28,16 @@ module Badline
28
28
 
29
29
  private
30
30
 
31
+ def save_mapper(out)
32
+ save_windows(out, [*@banks, EMPTY_BANK])
33
+ out.boolean(@disabled)
34
+ end
35
+
36
+ def load_mapper(input)
37
+ load_windows(input, [*@banks, EMPTY_BANK])
38
+ @disabled = input.boolean?
39
+ end
40
+
31
41
  def select(value)
32
42
  value ^= 0x07 if @hucky
33
43
  if value.anybits?(0x08)
@@ -20,6 +20,16 @@ module Badline
20
20
 
21
21
  private
22
22
 
23
+ def save_mapper(out)
24
+ save_windows(out, [*@roml_banks, *@romh_banks, EMPTY_BANK])
25
+ out.boolean(@locked)
26
+ end
27
+
28
+ def load_mapper(input)
29
+ load_windows(input, [*@roml_banks, *@romh_banks, EMPTY_BANK])
30
+ @locked = input.boolean?
31
+ end
32
+
23
33
  def select(value)
24
34
  @roml = bank(@roml_banks, value & 0x03)
25
35
  @romh = bank(@romh_banks, value & 0x03)
@@ -31,6 +31,14 @@ module Badline
31
31
  def peek(addr)
32
32
  @bank.peek(addr)
33
33
  end
34
+
35
+ def save_state(out)
36
+ out.int([*@banks, EMPTY_BANK].index { |bank| bank.equal?(@bank) })
37
+ end
38
+
39
+ def load_state(input)
40
+ @bank = [*@banks, EMPTY_BANK].fetch(input.int)
41
+ end
34
42
  end
35
43
 
36
44
  def reset
@@ -39,6 +47,14 @@ module Badline
39
47
 
40
48
  private
41
49
 
50
+ def save_mapper(out)
51
+ @romh.save_state(out)
52
+ end
53
+
54
+ def load_mapper(input)
55
+ @romh.load_state(input)
56
+ end
57
+
42
58
  def install_chips(chips)
43
59
  roml, romh = banks_from(chips)
44
60
  @romh = High.new(romh)
@@ -69,9 +69,28 @@ module Badline
69
69
  end
70
70
  mapper.new(crt, **)
71
71
  end
72
+
73
+ # A cartridge built afresh from what save_setup wrote, before
74
+ # load_state puts its state in.
75
+ def from_setup(input)
76
+ return GeoRAM.new(size: input.int) if input.int == GEO_RAM_SETUP
77
+
78
+ crt = Storage::CRTFile.new(bytes: input.string)
79
+ return RetroReplay.new(crt, flash_jumper: input.boolean?, bank_jumper: input.boolean?) if input.boolean?
80
+
81
+ from_crt(crt)
82
+ end
72
83
  end
73
84
 
85
+ # What save_setup writes first: a cartridge from a CRT image, or a
86
+ # GEO-RAM.
87
+ CRT_SETUP = 0
88
+ GEO_RAM_SETUP = 1
89
+
90
+ # Keeps the image it was built from, so a snapshot's machine can build
91
+ # the same cartridge before the snapshot's state goes into it.
74
92
  def initialize(crt)
93
+ @crt = crt
75
94
  @name = crt.name
76
95
  @exrom = crt.exrom
77
96
  @game = crt.game
@@ -88,6 +107,31 @@ module Badline
88
107
  @on_change = block
89
108
  end
90
109
 
110
+ # What builds the same cartridge afresh (Cartridge.from_setup): the CRT
111
+ # image it was built from, and the jumpers of a mapper that has them.
112
+ def save_setup(out)
113
+ out.int(CRT_SETUP).string(Storage::CRTFile.encode(@crt))
114
+ save_jumpers(out)
115
+ end
116
+
117
+ # The lines, the pull on NMI, the button and the mapper's own state.
118
+ # Loading sets them without calling back into the machine, which maps
119
+ # the cartridge again afterwards.
120
+ def save_state(out)
121
+ out.marker("CARTRIDGE")
122
+ out.int(@exrom).int(@game).boolean(@nmi).boolean(@button == true)
123
+ save_mapper(out)
124
+ end
125
+
126
+ def load_state(input)
127
+ input.marker("CARTRIDGE")
128
+ @exrom = input.int
129
+ @game = input.int
130
+ @nmi = input.boolean?
131
+ @button = input.boolean?
132
+ load_mapper(input)
133
+ end
134
+
91
135
  # Called with the level of the cartridge's pull on the NMI line when it
92
136
  # changes. The line is wired-OR with CIA 2's.
93
137
  def on_nmi_change(&block)
@@ -142,6 +186,34 @@ module Badline
142
186
 
143
187
  private
144
188
 
189
+ def save_jumpers(out)
190
+ out.boolean(false)
191
+ end
192
+
193
+ def save_mapper(_out); end
194
+
195
+ def load_mapper(_input); end
196
+
197
+ # Where the ROML and ROMH windows point, as their places in `windows`,
198
+ # the banks and views the mapper picks them from, or -1 for none.
199
+ def save_windows(out, windows)
200
+ out.int(window_index(@roml, windows)).int(window_index(@romh, windows))
201
+ end
202
+
203
+ def load_windows(input, windows)
204
+ @roml = window_at(input.int, windows)
205
+ @romh = window_at(input.int, windows)
206
+ end
207
+
208
+ def window_index(window, windows)
209
+ return -1 if window.nil?
210
+
211
+ windows.index { |candidate| candidate.equal?(window) } ||
212
+ raise(ArgumentError, "#{self.class.name}: a window that isn't among its banks")
213
+ end
214
+
215
+ def window_at(index, windows) = index.negative? ? nil : windows.fetch(index)
216
+
145
217
  def nmi=(level)
146
218
  return if @nmi == level
147
219
 
@@ -34,6 +34,23 @@ module Badline
34
34
 
35
35
  def interrupted? = status.value >= 0x80
36
36
 
37
+ def save_state(out)
38
+ out.int(@mask.value).int(@status.value).int(@pending)
39
+ [@read, @read_last_cycle, @read_two_cycles_ago].each { |read| out.optional_int(read) }
40
+ out.boolean(@timer_b_bug).boolean(@quiet)
41
+ end
42
+
43
+ def load_state(input)
44
+ @mask.value = input.int
45
+ @status.value = input.int
46
+ @pending = input.int
47
+ @read = input.optional_int
48
+ @read_last_cycle = input.optional_int
49
+ @read_two_cycles_ago = input.optional_int
50
+ @timer_b_bug = input.boolean?
51
+ @quiet = input.boolean?
52
+ end
53
+
37
54
  # Raises IR on this cycle.
38
55
  def assert_now
39
56
  status.interrupt = true