badline 0.6.0 → 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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +24 -10
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +20 -24
  7. data/lib/badline/address_bus.rb +10 -1
  8. data/lib/badline/audio/cli.rb +18 -5
  9. data/lib/badline/audio/console.rb +2 -1
  10. data/lib/badline/audio.rb +0 -3
  11. data/lib/badline/computer.rb +6 -3
  12. data/lib/badline/cpu/boundary.rb +54 -0
  13. data/lib/badline/cpu/core.rb +165 -0
  14. data/lib/badline/cpu.rb +3 -214
  15. data/lib/badline/drive1541/cpu.rb +29 -0
  16. data/lib/badline/drive1541.rb +1 -0
  17. data/lib/badline/ffi.rb +157 -0
  18. data/lib/badline/frontend/app.rb +225 -0
  19. data/lib/badline/frontend/audio_sink.rb +72 -0
  20. data/lib/badline/frontend/boot.rb +52 -0
  21. data/lib/badline/frontend/controls.rb +163 -0
  22. data/lib/badline/frontend/drive_led.rb +51 -0
  23. data/lib/badline/frontend/frame_rate.rb +83 -0
  24. data/lib/badline/frontend/gamepads.rb +76 -0
  25. data/lib/badline/frontend/keys.rb +48 -0
  26. data/lib/badline/frontend/pacer.rb +100 -0
  27. data/lib/badline/frontend/pad_port.rb +61 -0
  28. data/lib/badline/frontend/screen.rb +65 -0
  29. data/lib/badline/frontend/screenshot.rb +22 -0
  30. data/lib/badline/frontend/snapshots.rb +60 -0
  31. data/lib/badline/frontend/sound.rb +153 -0
  32. data/lib/badline/frontend/timeline.rb +171 -0
  33. data/lib/badline/frontend.rb +18 -0
  34. data/lib/badline/options/event.rb +94 -0
  35. data/lib/badline/options/help.rb +78 -0
  36. data/lib/badline/options/option.rb +29 -0
  37. data/lib/badline/options/table.rb +62 -0
  38. data/lib/badline/options.rb +181 -145
  39. data/lib/badline/reu/dma/saved_state.rb +50 -0
  40. data/lib/badline/reu/dma.rb +56 -47
  41. data/lib/badline/reu/saved_state.rb +2 -1
  42. data/lib/badline/reu.rb +19 -10
  43. data/lib/badline/sdl.rb +123 -51
  44. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  45. data/lib/badline/sid/waveform.rb +2 -3
  46. data/lib/badline/snapshot/state.rb +1 -1
  47. data/lib/badline/version.rb +1 -1
  48. data/lib/badline/vic/bank.rb +21 -0
  49. data/lib/badline/vic.rb +15 -6
  50. metadata +26 -19
  51. data/.claude/hooks/session-start.sh +0 -57
  52. data/.claude/settings.json +0 -14
  53. data/lib/badline/audio/sdl_sink.rb +0 -70
  54. data/lib/badline/audio/stream.rb +0 -123
  55. data/lib/badline/gui/application.rb +0 -243
  56. data/lib/badline/gui/drive_led_pane.rb +0 -29
  57. data/lib/badline/gui/gamepads.rb +0 -104
  58. data/lib/badline/gui/joy_map.rb +0 -27
  59. data/lib/badline/gui/key_map.rb +0 -39
  60. data/lib/badline/gui/palette.rb +0 -35
  61. data/lib/badline/gui/pane.rb +0 -36
  62. data/lib/badline/gui/screen_pane.rb +0 -46
  63. data/lib/badline/gui/snapshots.rb +0 -67
  64. data/lib/badline/gui/window.rb +0 -61
  65. data/lib/badline/gui.rb +0 -14
  66. data/lib/badline/sdl/events.rb +0 -49
  67. data/lib/badline/sdl/functions.rb +0 -74
  68. data/vendor/.gitkeep +0 -0
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module Audio
5
- # The host's audio device, fed through SDL's queue: mono signed 16-bit
6
- # samples go in at the device's rate and SDL plays them out behind us.
7
- class SDLSink
8
- class Error < Playback::DeviceError; end
9
-
10
- # int freq; Uint16 format; Uint8 channels, silence; Uint16 samples,
11
- # padding; Uint32 size; then the callback and userdata pointers.
12
- SPEC_LAYOUT = "lSCCSSLQQ"
13
-
14
- attr_reader :rate
15
-
16
- # With `exact_rate` false SDL may pick the device's own rate instead,
17
- # which #rate then reports.
18
- def initialize(rate:, exact_rate: false, buffer: 1024)
19
- load_sdl
20
- check(SDL::InitSubSystem.call(SDL::INIT_AUDIO))
21
- @device = open_device(rate, exact_rate ? 0 : SDL::AUDIO_ALLOW_FREQUENCY_CHANGE, buffer)
22
- end
23
-
24
- def queue(samples)
25
- data = samples.pack("s*")
26
- check(SDL::QueueAudio.call(@device, data, data.bytesize))
27
- end
28
-
29
- def queued_seconds = SDL::GetQueuedAudioSize.call(@device).fdiv(2 * rate)
30
-
31
- def start = SDL::PauseAudioDevice.call(@device, 0)
32
-
33
- def pause = SDL::PauseAudioDevice.call(@device, 1)
34
-
35
- def clear = SDL::ClearQueuedAudio.call(@device)
36
-
37
- def close
38
- return unless @device
39
-
40
- SDL::CloseAudioDevice.call(@device)
41
- SDL::QuitSubSystem.call(SDL::INIT_AUDIO)
42
- @device = nil
43
- end
44
-
45
- private
46
-
47
- def open_device(rate, allowed_changes, buffer)
48
- wanted = [rate, SDL::AUDIO_S16SYS, 1, 0, buffer, 0, 0, 0, 0].pack(SPEC_LAYOUT)
49
- obtained = "\0".b * 32
50
- device = SDL::OpenAudioDevice.call(nil, 0, wanted, obtained, allowed_changes)
51
- raise Error, SDL.error if device.zero?
52
-
53
- @rate = obtained.unpack1("l")
54
- device
55
- end
56
-
57
- def load_sdl
58
- require "badline/sdl"
59
- rescue SDL::Error => e
60
- raise Error, e.message
61
- end
62
-
63
- def check(result)
64
- raise Error, SDL.error if result.negative?
65
-
66
- result
67
- end
68
- end
69
- end
70
- end
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module Audio
5
- # Plays a running machine's SID through a sink, for the window: #feed
6
- # queues whatever the SID recorded since the last call, once a frame,
7
- # and #pace holds the machine to the device's clock.
8
- #
9
- # The device starts once the queue holds `ahead` seconds. Below real time
10
- # the queue runs dry, and the device then stops until it holds `ahead`
11
- # again, so the sound stutters with silent gaps rather than slowing
12
- # down; `on_underrun` hears about the first time. A frame that would
13
- # take the queue past `limit` seconds, as an unpaced machine above real
14
- # time makes, is dropped whole.
15
- class Stream
16
- AHEAD = 0.08
17
- LIMIT = 0.25
18
-
19
- attr_reader :underruns, :dropped
20
-
21
- # What #pace reads the time from and sleeps on: #now and #sleep.
22
- attr_writer :clock
23
-
24
- def initialize(sink, sid, ahead: AHEAD, limit: LIMIT, on_underrun: nil)
25
- @sink = sink
26
- @sid = sid
27
- @ahead = ahead
28
- @limit = limit
29
- @on_underrun = on_underrun
30
- @clock = Clock.new
31
- @next_frame = nil
32
- @started = false
33
- @muted = false
34
- @underruns = 0
35
- @dropped = 0
36
- sid.record(rate: sink.rate)
37
- end
38
-
39
- def rate = @sink.rate
40
-
41
- # Plays another machine's SID from here on.
42
- def sid=(sid)
43
- @sid = sid
44
- sid.record(rate: rate)
45
- end
46
-
47
- def muted? = @muted
48
-
49
- def playing? = @started
50
-
51
- # Muting drops the queue; unmuting fills it to `ahead` again before
52
- # the device restarts.
53
- def toggle_mute
54
- @muted = !@muted
55
- return unless @muted
56
-
57
- halt
58
- @sink.clear
59
- end
60
-
61
- def feed
62
- samples = @sid.drain_samples
63
- return if @muted || samples.empty?
64
-
65
- queued = @sink.queued_seconds
66
- return @dropped += samples.size if queued + samples.size.fdiv(rate) > @limit
67
-
68
- underrun! if @started && queued.zero?
69
- @sink.queue(samples)
70
- start if @sink.queued_seconds >= @ahead
71
- end
72
-
73
- # Waits out the frame just run. While the device plays, that is until
74
- # it has played the queue down to `ahead`, so the machine runs at the
75
- # device's rate and the queue neither drifts dry nor grows. Muted or
76
- # filling up, the clock paces frames `seconds` apart instead.
77
- def pace(seconds)
78
- return wait_for_device if playing?
79
-
80
- now = @clock.now
81
- @next_frame = now if @next_frame.nil? || @next_frame < now
82
- @clock.sleep(@next_frame - now)
83
- @next_frame += seconds
84
- end
85
-
86
- def close = @sink.close
87
-
88
- private
89
-
90
- def start
91
- return if @started
92
-
93
- @started = true
94
- @sink.start
95
- end
96
-
97
- def wait_for_device
98
- @next_frame = nil
99
- while (excess = @sink.queued_seconds - @ahead).positive?
100
- @clock.sleep([excess, 0.001].max)
101
- end
102
- end
103
-
104
- def halt
105
- @started = false
106
- @sink.pause
107
- end
108
-
109
- def underrun!
110
- @underruns += 1
111
- @on_underrun&.call if @underruns == 1
112
- halt
113
- end
114
-
115
- # The host's monotonic clock.
116
- class Clock
117
- def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
118
-
119
- def sleep(seconds) = Kernel.sleep(seconds)
120
- end
121
- end
122
- end
123
- end
@@ -1,243 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- class Application
6
- TITLE = "Badline"
7
- TOGGLE_SYM = SDL::KEY_TAB
8
- MUTE_SYM = SDL::KEY_F10
9
- SAVE_SYM = SDL::KEY_F11
10
- RESTORE_SYM = SDL::KEY_F12
11
- HOST_SYMS = [TOGGLE_SYM, MUTE_SYM, SAVE_SYM, RESTORE_SYM].freeze
12
- REVERSE_MOD = SDL::KMOD_SHIFT
13
-
14
- SHARED_KEYS = %i[cursor_up cursor_left cursor_h cursor_v space w a s d lshift].freeze
15
-
16
- # Tab steps through the input modes and shift-Tab back; the title bar
17
- # names the live one. The pot devices appear once per control port, since
18
- # games disagree on which one they read.
19
- MODES = {
20
- keyboard: nil, joystick: "JOY",
21
- mouse1: "MOUSE 1", mouse2: "MOUSE 2",
22
- paddles1: "PADDLE 1", paddles2: "PADDLE 2"
23
- }.freeze
24
-
25
- # Both pot devices take their input from the host mouse. Motion turns the
26
- # paddle knobs or steps the 1351's counters, and the host buttons go to
27
- # whichever lines the device puts them on.
28
- POT_DEVICES = {
29
- mouse1: [Input::Mouse1351, 1], mouse2: [Input::Mouse1351, 2],
30
- paddles1: [Input::Paddles, 1], paddles2: [Input::Paddles, 2]
31
- }.freeze
32
- MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
33
-
34
- # The machine options (sid_model:, reu:, true_drive:, ntsc:) build
35
- # the machine: reu plugs in an REU of that many K, true_drive puts a
36
- # true 1541 on device 8, and ntsc makes it an NTSC C64. The media
37
- # options (autostart:, song:, disk:) go to Media.attach.
38
- def initialize(media_path: nil, machine: {}, sound: false, verbose: false, **media)
39
- @verbose = verbose
40
- @snapshots = Snapshots.new
41
- @computer = @snapshots.boot(media_path, machine, media)
42
-
43
- @mode = :keyboard
44
- @pot_device = nil
45
- @panes = panes
46
- @stream = open_stream if sound
47
- @paced = ENV["NOVSYNC"].nil?
48
- @window = Window.new(
49
- title: TITLE,
50
- width: canvas_width, height: canvas_height,
51
- vsync: @paced && !@stream
52
- )
53
- @gamepads = Gamepads.new(@computer)
54
- @gamepads.names.each { |name| report "Gamepad: #{name}" }
55
- fit_frame
56
- end
57
-
58
- def run
59
- @running = true
60
- while @running
61
- handle_events
62
- @gamepads.poll
63
- @cycles_per_frame.times { @computer.cycle! }
64
- @stream&.feed
65
- @window.draw(@panes)
66
- @stream.pace(@frame_seconds) if @stream && @paced
67
- end
68
- ensure
69
- @computer.drive1541&.flush
70
- @stream&.close
71
- @gamepads.close
72
- @window.close
73
- report @computer.cpu.inspect
74
- end
75
-
76
- private
77
-
78
- def handle_events
79
- while (event = SDL.poll_event)
80
- case event
81
- when SDL::Quit
82
- @running = false
83
- when SDL::KeyDown
84
- handle_key_down(event)
85
- when SDL::KeyUp
86
- handle_key_up(event)
87
- when SDL::MouseMotion
88
- @pot_device&.move(event.xrel, event.yrel)
89
- when SDL::MouseButton
90
- handle_mouse_button(event)
91
- when SDL::ControllerDevice
92
- @gamepads.rescan
93
- end
94
- end
95
- end
96
-
97
- def handle_key_down(event)
98
- return host_key(event) if HOST_SYMS.include?(event.sym)
99
-
100
- port, dir = JoyMap.parse(event) if @mode == :joystick
101
- if port
102
- joystick(port).press(dir)
103
- else
104
- press_key(KeyMap.parse(event), event.repeat)
105
- end
106
- end
107
-
108
- # Tab, F10, F11 and F12 drive the front end, not the machine.
109
- def host_key(event)
110
- case event.sym
111
- when TOGGLE_SYM then cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1)
112
- when MUTE_SYM then toggle_mute
113
- when SAVE_SYM then @snapshots.save(@computer) unless event.repeat
114
- else restore_snapshot unless event.repeat
115
- end
116
- end
117
-
118
- # RESTORE isn't in the key matrix, so it goes to the machine instead,
119
- # once per press as the real key's one-shot fires.
120
- def press_key(key, repeat)
121
- return @computer.press_restore if key == :restore && !repeat
122
- return if key == :restore
123
-
124
- @computer.keyboard.press(key)
125
- end
126
-
127
- def handle_key_up(event)
128
- return if HOST_SYMS.include?(event.sym)
129
-
130
- port, dir = JoyMap.parse(event) if @mode == :joystick
131
- if port
132
- joystick(port).release(dir)
133
- else
134
- @computer.keyboard.release(KeyMap.parse(event))
135
- end
136
- end
137
-
138
- def handle_mouse_button(event)
139
- button = MOUSE_BUTTONS[event.button]
140
- return unless button && @pot_device
141
-
142
- if event.pressed
143
- @pot_device.press(button)
144
- else
145
- @pot_device.release(button)
146
- end
147
- end
148
-
149
- def joystick(port)
150
- port == 1 ? @computer.joystick1 : @computer.joystick2
151
- end
152
-
153
- def cycle_mode(step)
154
- modes = MODES.keys
155
- @mode = modes[(modes.index(@mode) + step) % modes.size]
156
- release_inputs
157
- attach_pot_device
158
- update_title
159
- end
160
-
161
- def toggle_mute
162
- return unless @stream
163
-
164
- @stream.toggle_mute
165
- update_title
166
- end
167
-
168
- # Runs the restored machine in place of the one before: the screen,
169
- # the drive LED, the gamepads and the sound go over to it. The input
170
- # mode stays the host's, so a mouse or paddles go back in their port.
171
- def restore_snapshot
172
- computer = @snapshots.restore
173
- return unless computer
174
-
175
- release_inputs
176
- @computer = computer
177
- @panes = panes
178
- @gamepads.computer = computer
179
- @stream&.sid = computer.sid
180
- attach_pot_device if POT_DEVICES.key?(@mode)
181
- end
182
-
183
- # The screen, and the drive LED in its border when a true drive is
184
- # plugged in.
185
- def panes
186
- screen = ScreenPane.new(@computer)
187
- drive = @computer.drive1541
188
- drive ? [screen, DriveLedPane.new(drive, screen)] : [screen]
189
- end
190
-
191
- def update_title
192
- tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
193
- @window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
194
- end
195
-
196
- def open_stream
197
- sink = Audio::SDLSink.new(rate: Audio::Renderer::DEFAULT_RATE)
198
- report "Sound at #{sink.rate} Hz, F10 mutes"
199
- Audio::Stream.new(sink, @computer.sid,
200
- on_underrun: -> { puts "Running below real time, so the sound will stutter." })
201
- rescue Audio::SDLSink::Error => e
202
- warn "badline: no sound, can't open the audio device: #{e.message}"
203
- end
204
-
205
- def fit_frame
206
- rate = @window.refresh_rate
207
- clock_hz = @computer.region.clock_hz
208
- @cycles_per_frame = clock_hz / rate
209
- @frame_seconds = @cycles_per_frame.fdiv(clock_hz)
210
- report "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
211
- end
212
-
213
- def report(line)
214
- puts line if @verbose
215
- end
216
-
217
- def attach_pot_device
218
- device_class, port = POT_DEVICES[@mode]
219
- @pot_device = device_class&.new
220
- ports = @computer.control_ports
221
- ports.device1 = port == 1 ? @pot_device : nil
222
- ports.device2 = port == 2 ? @pot_device : nil
223
- SDL::SetRelativeMouseMode.call(@pot_device ? 1 : 0)
224
- end
225
-
226
- def release_inputs
227
- SHARED_KEYS.each { |key| @computer.keyboard.release(key) }
228
- Joystick::DIRECTIONS.each_key do |dir|
229
- @computer.joystick1.release(dir)
230
- @computer.joystick2.release(dir)
231
- end
232
- end
233
-
234
- def canvas_width
235
- @panes.map { |pane| pane.left + pane.width }.max
236
- end
237
-
238
- def canvas_height
239
- @panes.map { |pane| pane.top + pane.height }.max
240
- end
241
- end
242
- end
243
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # The true drive's red LED, in the bottom right corner of the screen
6
- # pane's border: bright while the DOS lights it, as it does while a file
7
- # is open or after an error, and dim otherwise.
8
- class DriveLedPane < Pane
9
- WIDTH = 12
10
- HEIGHT = 4
11
-
12
- LIT = [0xff, 0x20, 0x20].freeze
13
- DARK = [0x40, 0x00, 0x00].freeze
14
-
15
- def initialize(drive, screen)
16
- super(width: WIDTH, height: HEIGHT,
17
- left: screen.left + screen.width - WIDTH - 8, top: screen.top + screen.height - HEIGHT - 6)
18
- @drive = drive
19
- end
20
-
21
- def color = @drive.led_on? ? LIT : DARK
22
-
23
- def render(renderer)
24
- SDL::SetRenderDrawColor.call(renderer, *color, 0xff)
25
- SDL::RenderFillRect.call(renderer, @rect)
26
- end
27
- end
28
- end
29
- end
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # Host game controllers wired onto the control ports.
6
- #
7
- # The first controller found drives joystick 2, the port most games read;
8
- # a second one drives joystick 1. D-pad and left stick both steer, every
9
- # face and shoulder button fires.
10
- #
11
- # SDL announces arrivals by device index but reports button presses by
12
- # instance id, so rather than line the two up, this class polls the
13
- # controllers every frame. It only releases directions it
14
- # pressed itself, so the keyboard mapping keeps working with a pad
15
- # connected.
16
- class Gamepads
17
- DEADZONE = 8_000
18
- PORTS = [2, 1].freeze
19
-
20
- BUTTONS = {
21
- up: [SDL::CONTROLLER_BUTTON_DPAD_UP],
22
- down: [SDL::CONTROLLER_BUTTON_DPAD_DOWN],
23
- left: [SDL::CONTROLLER_BUTTON_DPAD_LEFT],
24
- right: [SDL::CONTROLLER_BUTTON_DPAD_RIGHT],
25
- fire: [SDL::CONTROLLER_BUTTON_A, SDL::CONTROLLER_BUTTON_B,
26
- SDL::CONTROLLER_BUTTON_X, SDL::CONTROLLER_BUTTON_Y,
27
- SDL::CONTROLLER_BUTTON_LEFTSHOULDER, SDL::CONTROLLER_BUTTON_RIGHTSHOULDER]
28
- }.freeze
29
-
30
- AXES = {
31
- up: [SDL::CONTROLLER_AXIS_LEFTY, -1],
32
- down: [SDL::CONTROLLER_AXIS_LEFTY, 1],
33
- left: [SDL::CONTROLLER_AXIS_LEFTX, -1],
34
- right: [SDL::CONTROLLER_AXIS_LEFTX, 1]
35
- }.freeze
36
-
37
- def initialize(computer)
38
- SDL.check(SDL::InitSubSystem.call(SDL::INIT_GAMECONTROLLER))
39
- @computer = computer
40
- @controllers = []
41
- @pressed = Array.new(PORTS.size) { [] }
42
- rescan
43
- end
44
-
45
- def names = @controllers.map { |controller| SDL::GameControllerName.call(controller) }
46
-
47
- def rescan
48
- close
49
- @controllers = (0...SDL::NumJoysticks.call)
50
- .select { |index| SDL::IsGameController.call(index) == 1 }
51
- .first(PORTS.size)
52
- .map { |index| SDL::GameControllerOpen.call(index) }
53
- .reject(&:null?)
54
- end
55
-
56
- # Wires the controllers to another machine's joysticks, which take
57
- # what they hold on the next poll.
58
- def computer=(computer)
59
- @computer = computer
60
- @pressed.map! { [] }
61
- end
62
-
63
- def poll
64
- @controllers.each_with_index { |controller, index| apply(controller, index) }
65
- end
66
-
67
- def close
68
- @controllers.each { |controller| SDL::GameControllerClose.call(controller) }
69
- @controllers = []
70
- @pressed.size.times { |index| release(index) }
71
- end
72
-
73
- private
74
-
75
- def apply(controller, index)
76
- active = Joystick::DIRECTIONS.keys.select { |dir| active?(controller, dir) }
77
- (@pressed[index] - active).each { |dir| joystick(index).release(dir) }
78
- active.each { |dir| joystick(index).press(dir) }
79
- @pressed[index] = active
80
- end
81
-
82
- def release(index)
83
- @pressed[index].each { |dir| joystick(index).release(dir) }
84
- @pressed[index] = []
85
- end
86
-
87
- def active?(controller, direction)
88
- BUTTONS.fetch(direction).any? { |button| SDL::GameControllerGetButton.call(controller, button) == 1 } ||
89
- tilted?(controller, direction)
90
- end
91
-
92
- def tilted?(controller, direction)
93
- axis, sign = AXES[direction]
94
- return false unless axis
95
-
96
- SDL::GameControllerGetAxis.call(controller, axis) * sign > DEADZONE
97
- end
98
-
99
- def joystick(index)
100
- PORTS[index] == 1 ? @computer.joystick1 : @computer.joystick2
101
- end
102
- end
103
- end
104
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # Maps host keys onto the two control ports while joystick mode is on.
6
- #
7
- # The arrow cluster plus space drives joystick 2, WASD plus left shift
8
- # joystick 1, so both ports can be played at once.
9
- class JoyMap
10
- PORTS = {
11
- 2 => { "Up" => :up, "Down" => :down, "Left" => :left, "Right" => :right, "Space" => :fire }.freeze,
12
- 1 => { "W" => :up, "S" => :down, "A" => :left, "D" => :right, "Left Shift" => :fire }.freeze
13
- }.freeze
14
-
15
- def self.parse(event)
16
- name = SDL.key_name(event.sym)
17
-
18
- PORTS.each do |port, map|
19
- direction = map[name]
20
- return [port, direction] if direction
21
- end
22
-
23
- nil
24
- end
25
- end
26
- end
27
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- # Translates SDL key events into the emulator's key symbols.
6
- #
7
- # MAP is just the overrides, anything not covered simply falls though.
8
- class KeyMap
9
- MAP = {
10
- "Backspace" => :delete,
11
- "Return" => :return,
12
- "Right" => :cursor_h,
13
- "Down" => :cursor_v,
14
- "Left" => :cursor_left,
15
- "Up" => :cursor_up,
16
- "`" => :left,
17
- "]" => :up,
18
- "PageUp" => :restore,
19
- "Space" => :space,
20
- "Left Ctrl" => :control,
21
- "Left Alt" => :cbm,
22
- "Left Shift" => :lshift,
23
- "Right Shift" => :rshift,
24
- "Home" => :clr_home,
25
- "Escape" => :run_stop,
26
- "Backslash" => :"@",
27
- "'" => :":",
28
- "End" => :£,
29
- "Keypad +" => :+,
30
- "Keypad *" => :*
31
- }.freeze
32
-
33
- def self.parse(event)
34
- name = SDL.key_name(event.sym)
35
- MAP[name] || name.downcase.to_sym
36
- end
37
- end
38
- end
39
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Badline
4
- module GUI
5
- class Palette
6
- COLORS = [
7
- "#000000", "#FFFFFF", "#924A40", "#84C5CC",
8
- "#9351B6", "#72B14B", "#483AAA", "#D5DF7C",
9
- "#675200", "#C33D00", "#C18178", "#606060",
10
- "#8A8A8A", "#B3EC91", "#867ADE", "#B3B3B3"
11
- ].freeze
12
-
13
- attr_reader :dwords
14
-
15
- def initialize
16
- @dwords = COLORS.map { |hex| dword(hex) }.freeze
17
- @entries = @dwords.map { |rgba| [rgba].pack("V") }.freeze
18
- end
19
-
20
- def [](color)
21
- @entries[color]
22
- end
23
-
24
- private
25
-
26
- # Color as a little-endian RGBA dword.
27
- def dword(hex)
28
- r = hex[1, 2].to_i(16)
29
- g = hex[3, 2].to_i(16)
30
- b = hex[5, 2].to_i(16)
31
- (255 << 24) | (b << 16) | (g << 8) | r
32
- end
33
- end
34
- end
35
- end