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
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The visible part of the VIC's display, the crop of the machine's
6
+ # region, repacked for an SDL texture in XRGB8888. Spinel hands an
7
+ # Array of Integers to C as 64-bit words, as badline/ffi does on CRuby,
8
+ # so each word carries two neighbouring pixels, the left one in the low
9
+ # half. The texture is PAL's crop, 384x272, and a region with fewer
10
+ # lines to show sits in the middle of it, between black bands.
11
+ class Screen
12
+ WIDTH = Region::PAL.crop[2]
13
+ HEIGHT = Region::PAL.crop[3]
14
+ ROW_BYTES = WIDTH * 4
15
+ ROW_WORDS = WIDTH / 2
16
+
17
+ COLORS = [
18
+ 0x000000, 0xffffff, 0x924a40, 0x84c5cc,
19
+ 0x9351b6, 0x72b14b, 0x483aaa, 0xd5df7c,
20
+ 0x675200, 0xc33d00, 0xc18178, 0x606060,
21
+ 0x8a8a8a, 0xb3ec91, 0x867ade, 0xb3b3b3
22
+ ].freeze
23
+
24
+ PAIRS = Array.new(256) { |pair| COLORS[pair & 0x0f] | (COLORS[pair >> 4] << 32) }.freeze
25
+
26
+ attr_reader :pixels
27
+
28
+ def initialize(vic)
29
+ @vic = vic
30
+ crop = vic.region.crop
31
+ @col_offset = crop[0]
32
+ @row_offset = crop[1]
33
+ @height = crop[3]
34
+ @band = (HEIGHT - @height) / 2
35
+ @pixels = Array.new(HEIGHT * ROW_WORDS, 0)
36
+ end
37
+
38
+ # Repacks only the lines the VIC has changed since the last frame.
39
+ def update
40
+ dirty = @vic.dirty_lines
41
+ row = 0
42
+ while row < @height
43
+ pack_row(row) if dirty[row + @row_offset]
44
+ row += 1
45
+ end
46
+ @vic.clear_dirty_lines!
47
+ end
48
+
49
+ private
50
+
51
+ def pack_row(row)
52
+ display = @vic.display
53
+ pixels = @pixels
54
+ from = ((row + @row_offset) * @vic.width) + @col_offset
55
+ to = (row + @band) * ROW_WORDS
56
+ last = to + ROW_WORDS
57
+ while to < last
58
+ pixels[to] = PAIRS[(display[from] & 0x0f) | ((display[from + 1] & 0x0f) << 4)]
59
+ from += 2
60
+ to += 1
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # --screenshot's frame: what the renderer drew, read back before it is
6
+ # presented and saved as a BMP.
7
+ module Screenshot
8
+ def self.write(renderer, path)
9
+ SDL.SDL_GetRendererOutputSize(renderer, SDL.output_w, SDL.output_h)
10
+ width = SDL.read_i32(SDL.output_w)
11
+ height = SDL.read_i32(SDL.output_h)
12
+ data = LibC.malloc(width * height * 4)
13
+ SDL.SDL_RenderReadPixels(renderer, nil, SDL::PIXELFORMAT_RGB888, data, width * 4)
14
+ surface = SDL.SDL_CreateRGBSurfaceWithFormatFrom(data, width, height, 32, width * 4, SDL::PIXELFORMAT_RGB888)
15
+ SDL.SDL_SaveBMP_RW(surface, SDL.SDL_RWFromFile(path, "wb"), 1)
16
+ SDL.SDL_FreeSurface(surface)
17
+ LibC.free(data)
18
+ puts "wrote #{path}, #{width}x#{height}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The window's snapshots: F11 saves the machine to a new
6
+ # badline-<date>-<time>.vsf in the working directory, and F12 goes back
7
+ # to the snapshot last saved or opened, in a new machine. --save-snapshot
8
+ # saves one after the last frame.
9
+ class Snapshots
10
+ KEYS = [Keys::F11, Keys::F12].freeze
11
+
12
+ # The machine running, which F12 replaces with the restored one.
13
+ attr_reader :computer
14
+
15
+ def initialize(computer, options)
16
+ @computer = computer
17
+ @last = options.snapshot? ? options.media_path.to_s : ""
18
+ @at_finish = options.save_snapshot
19
+ end
20
+
21
+ # Saves or restores for F11 or F12, and says whether a restored
22
+ # machine now runs in place of the one before.
23
+ def key(scancode)
24
+ return restore if scancode == Keys::F12
25
+
26
+ save if scancode == Keys::F11
27
+ false
28
+ end
29
+
30
+ # Saves the machine, and warns when it can't.
31
+ def save(path = "badline-#{Time.now.strftime('%Y%m%d-%H%M%S')}.vsf")
32
+ @computer.save_snapshot(path)
33
+ @last = path
34
+ puts "Saved #{path}"
35
+ rescue Snapshot::FormatError, SystemCallError => e
36
+ warn "badline: #{path}: #{e.message}"
37
+ end
38
+
39
+ # Restores the snapshot last saved or opened into a new machine, and
40
+ # warns when it fails, leaving the machine running as it was.
41
+ def restore
42
+ if @last.empty?
43
+ puts "No snapshot to restore, F11 saves one"
44
+ return false
45
+ end
46
+
47
+ @computer = Snapshot.load(@last) { |line| puts line }
48
+ puts "Restored #{@last}"
49
+ true
50
+ rescue Snapshot::FormatError, SystemCallError => e
51
+ warn "badline: #{@last}: #{e.message}"
52
+ false
53
+ end
54
+
55
+ def finish
56
+ save(@at_finish) unless @at_finish.empty?
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The SID's output on SDL's audio queue, as mono signed 16-bit samples.
6
+ # Once the device plays, it is the clock: #wait holds each frame until
7
+ # the queue is down to `AHEAD`, so the machine runs at the device's pace.
8
+ #
9
+ # The device starts once the queue holds AHEAD. Below real time the
10
+ # queue runs dry, and the device then stops until it holds AHEAD again,
11
+ # so the sound stutters with silent gaps rather than slowing down. A
12
+ # frame that would take the queue past LIMIT is dropped whole, which
13
+ # only happens unpaced.
14
+ #
15
+ # Each frame's samples go to SDL through an IO::Buffer of 16-bit values.
16
+ class Sound
17
+ RATE = 44_100
18
+ AHEAD = 0.08
19
+ LIMIT = 0.25
20
+
21
+ attr_reader :rate, :underruns, :dropped, :queued, :low, :high
22
+
23
+ def initialize(sid, wanted, verbose)
24
+ @sid = sid
25
+ @verbose = verbose
26
+ @device = 0
27
+ @rate = RATE
28
+ @started = false
29
+ @muted = false
30
+ @underruns = 0
31
+ @dropped = 0
32
+ @queued = 0
33
+ @buffer = IO::Buffer.new(4096)
34
+ reset_levels
35
+ open_device if wanted
36
+ end
37
+
38
+ def on? = @device != 0
39
+
40
+ # Plays another machine's SID from here on.
41
+ def sid=(sid)
42
+ @sid = sid
43
+ sid.record(rate: @rate) if on?
44
+ end
45
+
46
+ def muted? = @muted
47
+
48
+ def playing? = @started
49
+
50
+ def feed
51
+ return unless on?
52
+
53
+ samples = @sid.drain_samples
54
+ return if @muted || samples.empty?
55
+
56
+ level = queued_seconds
57
+ if level + (samples.size.to_f / @rate) > LIMIT
58
+ @dropped += samples.size
59
+ else
60
+ play(samples, level)
61
+ end
62
+ end
63
+
64
+ # How much the audio queue holds, in seconds.
65
+ def level = queued_seconds
66
+
67
+ # Waits until the queue is down to `level` seconds.
68
+ def wait(level = AHEAD)
69
+ SDL.SDL_Delay(1) while queued_seconds > level
70
+ end
71
+
72
+ def toggle_mute
73
+ @muted = !@muted
74
+ return unless @muted
75
+
76
+ halt
77
+ SDL.SDL_ClearQueuedAudio(@device)
78
+ end
79
+
80
+ def reset_levels
81
+ @low = LIMIT
82
+ @high = 0.0
83
+ end
84
+
85
+ def close
86
+ SDL.SDL_CloseAudioDevice(@device) if on?
87
+ end
88
+
89
+ private
90
+
91
+ def open_device
92
+ unless SDL.SDL_InitSubSystem(SDL::INIT_AUDIO).zero?
93
+ puts "No sound: #{SDL.SDL_GetError}"
94
+ return
95
+ end
96
+
97
+ SDL.spec_freq(SDL.wanted, RATE)
98
+ SDL.spec_format(SDL.wanted, SDL::AUDIO_S16LSB)
99
+ SDL.spec_channels(SDL.wanted, 1)
100
+ SDL.spec_samples(SDL.wanted, 512)
101
+ @device = SDL.SDL_OpenAudioDevice(nil, 0, SDL.wanted, SDL.obtained, SDL::ALLOW_FREQUENCY_CHANGE)
102
+ return puts "No sound: #{SDL.SDL_GetError}" unless on?
103
+
104
+ @rate = SDL.read_i32(SDL.obtained)
105
+ @sid.record(rate: @rate)
106
+ puts "Sound at #{@rate} Hz" if @verbose
107
+ end
108
+
109
+ def queued_seconds = SDL.SDL_GetQueuedAudioSize(@device) / (2.0 * @rate)
110
+
111
+ def play(samples, level)
112
+ @low = level if level < @low
113
+ underrun! if @started && level.zero?
114
+ queue(samples)
115
+ level = queued_seconds
116
+ @high = level if level > @high
117
+ start if level >= AHEAD
118
+ end
119
+
120
+ def queue(samples)
121
+ count = samples.size
122
+ bytes = count * 2
123
+ @buffer.resize(bytes) if bytes > @buffer.size
124
+ buffer = @buffer
125
+ i = 0
126
+ while i < count
127
+ buffer.set_value(:s16, i * 2, samples[i])
128
+ i += 1
129
+ end
130
+ SDL.SDL_QueueAudio(@device, buffer, bytes)
131
+ @queued += count
132
+ end
133
+
134
+ def start
135
+ return if @started
136
+
137
+ @started = true
138
+ SDL.SDL_PauseAudioDevice(@device, 0)
139
+ end
140
+
141
+ def halt
142
+ @started = false
143
+ SDL.SDL_PauseAudioDevice(@device, 1)
144
+ end
145
+
146
+ def underrun!
147
+ @underruns += 1
148
+ puts "Running below real time, so the sound will stutter." if @underruns == 1
149
+ halt
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Frontend
5
+ # The events of --at and --script. Each runs once its number of frames
6
+ # has run: a screenshot saves that frame, and the rest act on the
7
+ # machine before the next frame is clocked. A key or the freeze button
8
+ # is let go Options::Event::HOLD frames after it is pressed. An event
9
+ # that fails ends the run.
10
+ class Timeline
11
+ # Takes the events, --screenshot's at the last frame and whether to
12
+ # insert disks write-protected from Options.
13
+ def initialize(options)
14
+ @events = options.timeline
15
+ unless options.screenshot.empty? || options.frames.zero?
16
+ @events += [Options::Event.new(options.frames, "screenshot", options.screenshot)]
17
+ end
18
+ @read_only = options.read_only?
19
+ @program = options.program
20
+ @releases = []
21
+ @failed = false
22
+ end
23
+
24
+ # What's wrong with the events, the first key that names nothing,
25
+ # or an empty string.
26
+ def self.error(events)
27
+ event = events.find { |candidate| candidate.action == "key" && !key?(candidate.argument) }
28
+ event.nil? ? "" : "no such key: #{event.argument}"
29
+ end
30
+
31
+ def self.key?(name) = name == "restore" || !keyboard_key(name).nil? || !joystick_direction(name).nil?
32
+
33
+ # The Keyboard's key for a name such as space, return or a.
34
+ def self.keyboard_key(name)
35
+ key = Keyboard.new.matrix.flatten.find { |candidate| candidate.to_s == name }
36
+ key.nil? ? Keyboard::COMBINATIONS.keys.find { |candidate| candidate.to_s == name } : key
37
+ end
38
+
39
+ # The direction of joy1-NAME or joy2-NAME, such as up or fire.
40
+ def self.joystick_direction(name)
41
+ return nil unless name.start_with?("joy1-") || name.start_with?("joy2-")
42
+
43
+ Joystick::DIRECTIONS.keys.find { |direction| direction.to_s == name[5, name.size - 5] }
44
+ end
45
+
46
+ # The screenshot's file name, with the frame's number in place of %d,
47
+ # or of %Nd or %0Nd padded to N digits.
48
+ def self.path(pattern, frame)
49
+ start = pattern.index("%")
50
+ finish = start.nil? ? nil : pattern.index("d", start)
51
+ return pattern if finish.nil?
52
+
53
+ width = pattern[start + 1, finish - start - 1]
54
+ return pattern unless width.match?(/\A\d*\z/)
55
+
56
+ number = frame.to_s.rjust(width.to_i, width.start_with?("0") ? "0" : " ")
57
+ pattern[0, start] + number + pattern[finish + 1, pattern.size - finish - 1]
58
+ end
59
+
60
+ # The files the frame's screenshots go to.
61
+ def screenshots(frame)
62
+ @events.select { |event| event.frame == frame && event.action == "screenshot" }
63
+ .map { |event| Timeline.path(event.argument, frame) }
64
+ end
65
+
66
+ # Whether the run ends at the frame: at a quit, or once an event failed.
67
+ def quit?(frame) = @failed || @events.any? { |event| event.frame == frame && event.action == "quit" }
68
+
69
+ # Whether an event failed, which ends the run.
70
+ def failed? = @failed
71
+
72
+ # Lets go of what was held until this frame, then runs its events, up
73
+ # to one that fails.
74
+ def run(computer, frame)
75
+ @releases.each { |event| release(computer, event) if event.frame == frame }
76
+ @releases.reject! { |event| event.frame == frame }
77
+ @events.each { |event| perform(computer, event) if event.frame == frame && !@failed }
78
+ end
79
+
80
+ private
81
+
82
+ def perform(computer, event)
83
+ case event.action
84
+ when "key" then press(computer, event)
85
+ when "type" then computer.type_text(event.argument.gsub("\\n", "\r").gsub("\\r", "\r"))
86
+ when "insert" then insert(computer, event.argument)
87
+ when "eject" then eject(computer, event.argument)
88
+ when "reset" then computer.reset!
89
+ when "freeze" then freeze(computer, event)
90
+ end
91
+ end
92
+
93
+ def press(computer, event)
94
+ name = event.argument
95
+ if name == "restore"
96
+ computer.press_restore
97
+ return
98
+ end
99
+
100
+ direction = Timeline.joystick_direction(name)
101
+ if direction.nil?
102
+ computer.keyboard.press(Timeline.keyboard_key(name))
103
+ else
104
+ joystick(computer, name).press(direction)
105
+ end
106
+ hold(event)
107
+ end
108
+
109
+ def freeze(computer, event)
110
+ computer.press_cartridge_button
111
+ hold(event)
112
+ end
113
+
114
+ def hold(event)
115
+ @releases << Options::Event.new(event.frame + Options::Event::HOLD, event.action, event.argument)
116
+ end
117
+
118
+ def release(computer, event)
119
+ return computer.release_cartridge_button if event.action == "freeze"
120
+
121
+ name = event.argument
122
+ direction = Timeline.joystick_direction(name)
123
+ if direction.nil?
124
+ computer.keyboard.release(Timeline.keyboard_key(name))
125
+ else
126
+ joystick(computer, name).release(direction)
127
+ end
128
+ end
129
+
130
+ def joystick(computer, name) = name.start_with?("joy1-") ? computer.joystick1 : computer.joystick2
131
+
132
+ def insert(computer, path)
133
+ puts Media.attach(computer, path, autostart: false, disk: { read_only: @read_only })
134
+ rescue StandardError => e
135
+ warn "#{@program}: #{path}: #{e.message}"
136
+ @failed = true
137
+ end
138
+
139
+ def eject(computer, what)
140
+ return puts "No #{what} to eject" unless inserted?(computer, what)
141
+
142
+ if what == "disk"
143
+ eject_disk(computer)
144
+ elsif what == "tape"
145
+ computer.datasette.eject
146
+ else
147
+ computer.address_bus.detach_cartridge
148
+ computer.power_cycle!
149
+ end
150
+ puts "Ejected the #{what}"
151
+ end
152
+
153
+ def inserted?(computer, what)
154
+ return !computer.datasette.tape.nil? if what == "tape"
155
+ return !computer.address_bus.cartridge.nil? if what == "cartridge"
156
+
157
+ drive = Media::TrueDrive.drive(computer)
158
+ drive.nil? ? computer.mounted? : !drive.disk.nil?
159
+ end
160
+
161
+ def eject_disk(computer)
162
+ drive = Media::TrueDrive.drive(computer)
163
+ if drive.nil?
164
+ computer.unmount
165
+ else
166
+ drive.insert(nil)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/sdl"
4
+ require "badline/frontend/boot"
5
+ require "badline/frontend/frame_rate"
6
+ require "badline/frontend/screen"
7
+ require "badline/frontend/drive_led"
8
+ require "badline/frontend/keys"
9
+ require "badline/frontend/snapshots"
10
+ require "badline/frontend/screenshot"
11
+ require "badline/frontend/timeline"
12
+ require "badline/frontend/sound"
13
+ require "badline/frontend/pacer"
14
+ require "badline/frontend/controls"
15
+ require "badline/frontend/pad_port"
16
+ require "badline/frontend/gamepads"
17
+ require "badline/frontend/app"
18
+ require "badline/frontend/audio_sink"
@@ -80,6 +80,33 @@ module Badline
80
80
  data ? new(data) : new([], error: [Drive::ILLEGAL_TRACK_OR_SECTOR, track, sector])
81
81
  end
82
82
 
83
+ # Reads what save_state wrote after the flag that marks a file open
84
+ # for writing (Drive::Channels.channel_from_state). A buffer channel
85
+ # takes its block in `ram`, the drive's RAM, again.
86
+ def self.from_state(input, ram)
87
+ new.tap { |channel| channel.load_state(input, ram) }
88
+ end
89
+
90
+ # A buffer channel's bytes are the drive's RAM, which the drive
91
+ # writes itself.
92
+ def save_state(out)
93
+ out.boolean(false).boolean(@writable).int(@base).int(@length).int(@pointer)
94
+ out.optional_int(@lead).optional_int(@buffer).boolean(!@error.nil?)
95
+ out.ints(@error) if @error
96
+ out.blob(@bytes) unless @writable
97
+ end
98
+
99
+ def load_state(input, ram)
100
+ @writable = input.boolean?
101
+ @base = input.int
102
+ @length = input.int
103
+ @pointer = input.int
104
+ @lead = input.optional_int
105
+ @buffer = input.optional_int
106
+ @error = input.boolean? ? input.ints : nil
107
+ @bytes = @writable ? ram : input.blob
108
+ end
109
+
83
110
  def replace(bytes, length = bytes.length)
84
111
  @bytes = bytes
85
112
  rewind(length)
@@ -22,6 +22,31 @@ module Badline
22
22
 
23
23
  def clear = @channels.clear
24
24
 
25
+ # Each open channel with its secondary address, in the order they
26
+ # opened.
27
+ def save_state(out)
28
+ out.int(@channels.length)
29
+ @channels.each do |secondary, channel|
30
+ out.int(secondary)
31
+ channel.save_state(out)
32
+ end
33
+ end
34
+
35
+ # Buffer channels take their block in the drive's RAM again.
36
+ def load_state(input, memory)
37
+ @channels.clear
38
+ input.int.times do
39
+ secondary = input.int
40
+ @channels[secondary] = Channels.channel_from_state(input, memory.ram)
41
+ end
42
+ end
43
+
44
+ # A channel save_state wrote: a file open for writing, or a Channel
45
+ # whose buffer, if it has one, is in `ram`.
46
+ def self.channel_from_state(input, ram)
47
+ input.boolean? ? WriteFile.from_state(input) : Channel.from_state(input, ram)
48
+ end
49
+
25
50
  # Opens a buffer channel on the buffer a "#n" name asks for, or on
26
51
  # the highest free one. Returns nil when that buffer isn't free,
27
52
  # which leaves a channel asking again for its own buffer open.
@@ -62,6 +62,15 @@ module Badline
62
62
  load_buffer(Channels::BAM_BUFFER, block) if block
63
63
  end
64
64
 
65
+ def save_state(out)
66
+ out.blob(@ram).boolean(@bam)
67
+ end
68
+
69
+ def load_state(input)
70
+ input.blob_into(@ram)
71
+ @bam = input.boolean?
72
+ end
73
+
65
74
  # Whether the BAM has been read since the last reset.
66
75
  def bam? = @bam
67
76
 
@@ -37,6 +37,14 @@ module Badline
37
37
  nil
38
38
  end
39
39
 
40
+ def save_state(out)
41
+ @channel.save_state(out)
42
+ end
43
+
44
+ def load_state(input)
45
+ @channel = Channels.channel_from_state(input, [])
46
+ end
47
+
40
48
  def replace(bytes)
41
49
  @channel.replace(bytes)
42
50
  end
@@ -11,6 +11,17 @@ module Badline
11
11
  attr_accessor :buffer
12
12
  attr_reader :name, :type, :mode
13
13
 
14
+ TYPES = %i[del seq prg usr rel].freeze
15
+ MODES = %i[write replace append].freeze
16
+
17
+ # Reads what save_state wrote after the flag that marks a file
18
+ # open for writing.
19
+ def self.from_state(input)
20
+ name = input.string
21
+ type = TYPES.fetch(input.int).to_sym
22
+ new(name, type, mode: MODES.fetch(input.int).to_sym).tap { |file| file.load_state(input) }
23
+ end
24
+
14
25
  def initialize(name, type, mode: :write)
15
26
  @name = name
16
27
  @type = type
@@ -18,6 +29,16 @@ module Badline
18
29
  @bytes = []
19
30
  end
20
31
 
32
+ def save_state(out)
33
+ out.boolean(true).string(@name).int(TYPES.index(@type)).int(MODES.index(@mode))
34
+ out.blob(@bytes).optional_int(@buffer)
35
+ end
36
+
37
+ def load_state(input)
38
+ @bytes = input.blob
39
+ @buffer = input.optional_int
40
+ end
41
+
21
42
  def write(bytes) = @bytes.concat(bytes)
22
43
 
23
44
  def writable? = true
@@ -143,6 +143,25 @@ module Badline
143
143
  # vector, as U: does.
144
144
  def reset! = reset(cold: true)
145
145
 
146
+ # The mounted storage's setup, for Storage.reopen, then the drive's
147
+ # RAM, its status and its open channels.
148
+ def save_state(out)
149
+ @storage.save_setup(out)
150
+ out.marker("TRAP DRIVE")
151
+ @memory.save_state(out)
152
+ @status.save_state(out)
153
+ @channels.save_state(out)
154
+ end
155
+
156
+ # Takes the state after the storage's setup, into a drive mounted on
157
+ # the reopened storage.
158
+ def load_state(input)
159
+ input.marker("TRAP DRIVE")
160
+ @memory.load_state(input)
161
+ @status.load_state(input)
162
+ @channels.load_state(input, @memory)
163
+ end
164
+
146
165
  # Returns the next byte and whether it is the channel's last, or nil
147
166
  # when there is nothing left to send.
148
167
  def read(secondary)
@@ -37,6 +37,14 @@ module Badline
37
37
  @saving = false
38
38
  end
39
39
 
40
+ def save_state(out)
41
+ out.boolean(@saving)
42
+ end
43
+
44
+ def load_state(input)
45
+ @saving = input.boolean?
46
+ end
47
+
40
48
  def call
41
49
  return unless active?
42
50
  return finish if @saving
@@ -46,6 +46,22 @@ module Badline
46
46
  @buffer = []
47
47
  end
48
48
 
49
+ # The frame under way, with the channels and the bytes it gathered.
50
+ # The device it answers is the machine's to set.
51
+ def save_state(out)
52
+ out.boolean(@listening).boolean(@talking).optional_int(@listen_channel).optional_int(@talk_channel)
53
+ out.int(@frame).blob(@buffer)
54
+ end
55
+
56
+ def load_state(input)
57
+ @listening = input.boolean?
58
+ @talking = input.boolean?
59
+ @listen_channel = input.optional_int
60
+ @talk_channel = input.optional_int
61
+ @frame = input.int
62
+ @buffer = input.blob
63
+ end
64
+
49
65
  def install
50
66
  ROUTINES.each do |address, routine|
51
67
  @cpu.install_trap(address) { call_routine(routine) if kernal? }