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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +10 -9
- data/README.md +82 -10
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +20 -23
- data/lib/badline/address_bus/saved_state.rb +72 -0
- data/lib/badline/address_bus.rb +11 -1
- data/lib/badline/audio/cli.rb +18 -5
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio.rb +0 -3
- data/lib/badline/cartridge/action_replay.rb +15 -0
- data/lib/badline/cartridge/comal80.rb +8 -0
- data/lib/badline/cartridge/dinamic.rb +8 -0
- data/lib/badline/cartridge/easy_flash.rb +20 -1
- data/lib/badline/cartridge/epyx_fastload.rb +8 -0
- data/lib/badline/cartridge/final_cartridge3.rb +12 -0
- data/lib/badline/cartridge/flash.rb +41 -0
- data/lib/badline/cartridge/fun_play.rb +8 -0
- data/lib/badline/cartridge/g_mod2.rb +12 -0
- data/lib/badline/cartridge/game_system.rb +8 -0
- data/lib/badline/cartridge/geo_ram.rb +16 -0
- data/lib/badline/cartridge/magic_desk.rb +8 -0
- data/lib/badline/cartridge/ocean.rb +8 -0
- data/lib/badline/cartridge/pagefox.rb +12 -0
- data/lib/badline/cartridge/retro_replay.rb +44 -1
- data/lib/badline/cartridge/rgcd.rb +10 -0
- data/lib/badline/cartridge/super_games.rb +10 -0
- data/lib/badline/cartridge/zaxxon.rb +16 -0
- data/lib/badline/cartridge.rb +72 -0
- data/lib/badline/cia/interrupt_register.rb +17 -0
- data/lib/badline/cia/serial.rb +44 -0
- data/lib/badline/cia/state.rb +47 -0
- data/lib/badline/cia/timer.rb +33 -0
- data/lib/badline/cia.rb +8 -0
- data/lib/badline/computer/attachments.rb +132 -0
- data/lib/badline/computer/saved_state.rb +116 -0
- data/lib/badline/computer.rb +13 -33
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu/microcode.rb +8 -0
- data/lib/badline/cpu/saved_state.rb +115 -0
- data/lib/badline/cpu.rb +3 -212
- data/lib/badline/datasette.rb +27 -0
- data/lib/badline/drive1541/bus.rb +13 -0
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541/disk.rb +7 -2
- data/lib/badline/drive1541/disk_state.rb +80 -0
- data/lib/badline/drive1541/mechanism.rb +2 -0
- data/lib/badline/drive1541/mechanism_state.rb +67 -0
- data/lib/badline/drive1541/rotation.rb +10 -0
- data/lib/badline/drive1541/serial_port.rb +10 -0
- data/lib/badline/drive1541.rb +35 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +72 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend.rb +18 -0
- data/lib/badline/kernal_trap/channel.rb +27 -0
- data/lib/badline/kernal_trap/drive/channels.rb +25 -0
- data/lib/badline/kernal_trap/drive/memory.rb +9 -0
- data/lib/badline/kernal_trap/drive/status.rb +8 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +21 -0
- data/lib/badline/kernal_trap/drive.rb +19 -0
- data/lib/badline/kernal_trap/save.rb +8 -0
- data/lib/badline/kernal_trap/serial.rb +16 -0
- data/lib/badline/memory.rb +8 -0
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +78 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +62 -0
- data/lib/badline/options.rb +181 -144
- data/lib/badline/ram_expansion/plus256k.rb +16 -0
- data/lib/badline/ram_expansion/plus60k.rb +12 -0
- data/lib/badline/ram_expansion/unexpanded.rb +6 -0
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -11
- data/lib/badline/reu/ram.rb +15 -0
- data/lib/badline/reu/saved_state.rb +53 -0
- data/lib/badline/reu.rb +23 -10
- data/lib/badline/sdl.rb +123 -51
- data/lib/badline/sid/envelope/state.rb +68 -0
- data/lib/badline/sid/envelope.rb +7 -0
- data/lib/badline/sid/filter.rb +38 -0
- data/lib/badline/sid/voice.rb +10 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform/state.rb +66 -0
- data/lib/badline/sid/waveform.rb +4 -3
- data/lib/badline/sid.rb +34 -0
- data/lib/badline/snapshot/container.rb +117 -0
- data/lib/badline/snapshot/fields.rb +54 -0
- data/lib/badline/snapshot/image.rb +70 -0
- data/lib/badline/snapshot/machine_state.rb +77 -0
- data/lib/badline/snapshot/setup.rb +58 -0
- data/lib/badline/snapshot/state.rb +178 -0
- data/lib/badline/snapshot/vice/c64_mem.rb +53 -0
- data/lib/badline/snapshot/vice/cias.rb +89 -0
- data/lib/badline/snapshot/vice/cias_import.rb +70 -0
- data/lib/badline/snapshot/vice/main_cpu.rb +76 -0
- data/lib/badline/snapshot/vice/peripherals.rb +47 -0
- data/lib/badline/snapshot/vice/restore.rb +125 -0
- data/lib/badline/snapshot/vice/sid_extended.rb +96 -0
- data/lib/badline/snapshot/vice/sid_registers.rb +46 -0
- data/lib/badline/snapshot/vice/vicii.rb +119 -0
- data/lib/badline/snapshot/vice/vicii_import.rb +131 -0
- data/lib/badline/snapshot/vice.rb +74 -0
- data/lib/badline/snapshot.rb +52 -0
- data/lib/badline/storage/crt_file.rb +9 -3
- data/lib/badline/storage/d64_image.rb +2 -0
- data/lib/badline/storage/d71_image.rb +2 -0
- data/lib/badline/storage/d81_image.rb +2 -0
- data/lib/badline/storage/disk_image.rb +11 -2
- data/lib/badline/storage/host_directory.rb +5 -0
- data/lib/badline/storage/t64.rb +6 -0
- data/lib/badline/storage/tap.rb +10 -1
- data/lib/badline/storage/unavailable.rb +20 -0
- data/lib/badline/storage.rb +34 -0
- data/lib/badline/time_of_day.rb +56 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +15 -0
- data/lib/badline/via/interrupt_register.rb +9 -0
- data/lib/badline/via/shift_register.rb +13 -0
- data/lib/badline/via/state.rb +42 -0
- data/lib/badline/via/timer1.rb +13 -0
- data/lib/badline/via/timer2.rb +18 -0
- data/lib/badline/via.rb +2 -0
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic/border_mask.rb +10 -0
- data/lib/badline/vic/collisions.rb +11 -0
- data/lib/badline/vic/color_patches.rb +11 -0
- data/lib/badline/vic/display_state.rb +29 -1
- data/lib/badline/vic/graphics_shifter.rb +26 -0
- data/lib/badline/vic/register_log.rb +18 -0
- data/lib/badline/vic/registers.rb +13 -2
- data/lib/badline/vic/sequencer.rb +12 -0
- data/lib/badline/vic/sequencer_state.rb +89 -0
- data/lib/badline/vic/sprite/internal_bus.rb +11 -0
- data/lib/badline/vic/sprite/saved_state.rb +64 -0
- data/lib/badline/vic/sprite.rb +2 -0
- data/lib/badline/vic/sprites.rb +3 -0
- data/lib/badline/vic/sprites_state.rb +57 -0
- data/lib/badline/vic.rb +109 -16
- data/lib/badline.rb +1 -0
- metadata +59 -18
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -117
- data/lib/badline/gui/application.rb +0 -216
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -97
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -13
- data/lib/badline/sdl/events.rb +0 -47
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
|
@@ -1,216 +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
|
-
REVERSE_MOD = SDL::KMOD_SHIFT
|
|
10
|
-
|
|
11
|
-
SHARED_KEYS = %i[cursor_up cursor_left cursor_h cursor_v space w a s d lshift].freeze
|
|
12
|
-
|
|
13
|
-
# Tab steps through the input modes and shift-Tab back; the title bar
|
|
14
|
-
# names the live one. The pot devices appear once per control port, since
|
|
15
|
-
# games disagree on which one they read.
|
|
16
|
-
MODES = {
|
|
17
|
-
keyboard: nil, joystick: "JOY",
|
|
18
|
-
mouse1: "MOUSE 1", mouse2: "MOUSE 2",
|
|
19
|
-
paddles1: "PADDLE 1", paddles2: "PADDLE 2"
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
# Both pot devices take their input from the host mouse. Motion turns the
|
|
23
|
-
# paddle knobs or steps the 1351's counters, and the host buttons go to
|
|
24
|
-
# whichever lines the device puts them on.
|
|
25
|
-
POT_DEVICES = {
|
|
26
|
-
mouse1: [Input::Mouse1351, 1], mouse2: [Input::Mouse1351, 2],
|
|
27
|
-
paddles1: [Input::Paddles, 1], paddles2: [Input::Paddles, 2]
|
|
28
|
-
}.freeze
|
|
29
|
-
MOUSE_BUTTONS = { 1 => :left, 3 => :right }.freeze
|
|
30
|
-
|
|
31
|
-
# The machine options (sid_model:, reu:, true_drive:, ntsc:) build
|
|
32
|
-
# the machine: reu plugs in an REU of that many K, true_drive puts a
|
|
33
|
-
# true 1541 on device 8, and ntsc makes it an NTSC C64. The media
|
|
34
|
-
# options (autostart:, song:, disk:) go to Media.attach.
|
|
35
|
-
def initialize(media_path: nil, machine: {}, sound: false, verbose: false, **media)
|
|
36
|
-
@verbose = verbose
|
|
37
|
-
@computer = Computer.new(**machine_options(machine, media_path))
|
|
38
|
-
Media::TrueDrive.plug(@computer) if machine[:true_drive]
|
|
39
|
-
puts Media.attach(@computer, media_path, **media) if media_path
|
|
40
|
-
|
|
41
|
-
@mode = :keyboard
|
|
42
|
-
@pot_device = nil
|
|
43
|
-
@panes = [ScreenPane.new(@computer)]
|
|
44
|
-
@panes << DriveLedPane.new(@computer.drive1541, @panes.first) if @computer.drive1541
|
|
45
|
-
@stream = open_stream if sound
|
|
46
|
-
@paced = ENV["NOVSYNC"].nil?
|
|
47
|
-
@window = Window.new(
|
|
48
|
-
title: TITLE,
|
|
49
|
-
width: canvas_width, height: canvas_height,
|
|
50
|
-
vsync: @paced && !@stream
|
|
51
|
-
)
|
|
52
|
-
@gamepads = Gamepads.new(@computer)
|
|
53
|
-
@gamepads.names.each { |name| report "Gamepad: #{name}" }
|
|
54
|
-
fit_frame
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def run
|
|
58
|
-
@running = true
|
|
59
|
-
while @running
|
|
60
|
-
handle_events
|
|
61
|
-
@gamepads.poll
|
|
62
|
-
@cycles_per_frame.times { @computer.cycle! }
|
|
63
|
-
@stream&.feed
|
|
64
|
-
@window.draw(@panes)
|
|
65
|
-
@stream.pace(@frame_seconds) if @stream && @paced
|
|
66
|
-
end
|
|
67
|
-
ensure
|
|
68
|
-
@computer.drive1541&.flush
|
|
69
|
-
@stream&.close
|
|
70
|
-
@gamepads.close
|
|
71
|
-
@window.close
|
|
72
|
-
report @computer.cpu.inspect
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
def handle_events
|
|
78
|
-
while (event = SDL.poll_event)
|
|
79
|
-
case event
|
|
80
|
-
when SDL::Quit
|
|
81
|
-
@running = false
|
|
82
|
-
when SDL::KeyDown
|
|
83
|
-
handle_key_down(event)
|
|
84
|
-
when SDL::KeyUp
|
|
85
|
-
handle_key_up(event)
|
|
86
|
-
when SDL::MouseMotion
|
|
87
|
-
@pot_device&.move(event.xrel, event.yrel)
|
|
88
|
-
when SDL::MouseButton
|
|
89
|
-
handle_mouse_button(event)
|
|
90
|
-
when SDL::ControllerDevice
|
|
91
|
-
@gamepads.rescan
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def handle_key_down(event)
|
|
97
|
-
return cycle_mode(event.mod.anybits?(REVERSE_MOD) ? -1 : 1) if event.sym == TOGGLE_SYM
|
|
98
|
-
return toggle_mute if event.sym == MUTE_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
|
-
# RESTORE isn't in the key matrix, so it goes to the machine instead,
|
|
109
|
-
# once per press as the real key's one-shot fires.
|
|
110
|
-
def press_key(key, repeat)
|
|
111
|
-
return @computer.press_restore if key == :restore && !repeat
|
|
112
|
-
return if key == :restore
|
|
113
|
-
|
|
114
|
-
@computer.keyboard.press(key)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def handle_key_up(event)
|
|
118
|
-
return if [TOGGLE_SYM, MUTE_SYM].include?(event.sym)
|
|
119
|
-
|
|
120
|
-
port, dir = JoyMap.parse(event) if @mode == :joystick
|
|
121
|
-
if port
|
|
122
|
-
joystick(port).release(dir)
|
|
123
|
-
else
|
|
124
|
-
@computer.keyboard.release(KeyMap.parse(event))
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def handle_mouse_button(event)
|
|
129
|
-
button = MOUSE_BUTTONS[event.button]
|
|
130
|
-
return unless button && @pot_device
|
|
131
|
-
|
|
132
|
-
if event.pressed
|
|
133
|
-
@pot_device.press(button)
|
|
134
|
-
else
|
|
135
|
-
@pot_device.release(button)
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def joystick(port)
|
|
140
|
-
port == 1 ? @computer.joystick1 : @computer.joystick2
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def cycle_mode(step)
|
|
144
|
-
modes = MODES.keys
|
|
145
|
-
@mode = modes[(modes.index(@mode) + step) % modes.size]
|
|
146
|
-
release_inputs
|
|
147
|
-
attach_pot_device
|
|
148
|
-
update_title
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def toggle_mute
|
|
152
|
-
return unless @stream
|
|
153
|
-
|
|
154
|
-
@stream.toggle_mute
|
|
155
|
-
update_title
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def update_title
|
|
159
|
-
tags = [MODES[@mode], @stream&.muted? && "MUTED"].select(&:itself)
|
|
160
|
-
@window.title = [TITLE, *tags.map { |tag| "[#{tag}]" }].join(" ")
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def open_stream
|
|
164
|
-
sink = Audio::SDLSink.new(rate: Audio::Renderer::DEFAULT_RATE)
|
|
165
|
-
report "Sound at #{sink.rate} Hz, F10 mutes"
|
|
166
|
-
Audio::Stream.new(sink, @computer.sid,
|
|
167
|
-
on_underrun: -> { puts "Running below real time, so the sound will stutter." })
|
|
168
|
-
rescue Audio::SDLSink::Error => e
|
|
169
|
-
warn "badline: no sound, can't open the audio device: #{e.message}"
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def machine_options(machine, media_path)
|
|
173
|
-
options = { sid_model: machine[:sid_model] || Media.sid_model(media_path), **machine.slice(:reu) }
|
|
174
|
-
options[:region] = Region::NTSC if machine[:ntsc]
|
|
175
|
-
options
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def fit_frame
|
|
179
|
-
rate = @window.refresh_rate
|
|
180
|
-
clock_hz = @computer.region.clock_hz
|
|
181
|
-
@cycles_per_frame = clock_hz / rate
|
|
182
|
-
@frame_seconds = @cycles_per_frame.fdiv(clock_hz)
|
|
183
|
-
report "Display #{rate} Hz -> #{@cycles_per_frame} cycles/frame"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def report(line)
|
|
187
|
-
puts line if @verbose
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def attach_pot_device
|
|
191
|
-
device_class, port = POT_DEVICES[@mode]
|
|
192
|
-
@pot_device = device_class&.new
|
|
193
|
-
ports = @computer.control_ports
|
|
194
|
-
ports.device1 = port == 1 ? @pot_device : nil
|
|
195
|
-
ports.device2 = port == 2 ? @pot_device : nil
|
|
196
|
-
SDL::SetRelativeMouseMode.call(@pot_device ? 1 : 0)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def release_inputs
|
|
200
|
-
SHARED_KEYS.each { |key| @computer.keyboard.release(key) }
|
|
201
|
-
Joystick::DIRECTIONS.each_key do |dir|
|
|
202
|
-
@computer.joystick1.release(dir)
|
|
203
|
-
@computer.joystick2.release(dir)
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def canvas_width
|
|
208
|
-
@panes.map { |pane| pane.left + pane.width }.max
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def canvas_height
|
|
212
|
-
@panes.map { |pane| pane.top + pane.height }.max
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
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
|
data/lib/badline/gui/gamepads.rb
DELETED
|
@@ -1,97 +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
|
-
def poll
|
|
57
|
-
@controllers.each_with_index { |controller, index| apply(controller, index) }
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def close
|
|
61
|
-
@controllers.each { |controller| SDL::GameControllerClose.call(controller) }
|
|
62
|
-
@controllers = []
|
|
63
|
-
@pressed.size.times { |index| release(index) }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def apply(controller, index)
|
|
69
|
-
active = Joystick::DIRECTIONS.keys.select { |dir| active?(controller, dir) }
|
|
70
|
-
(@pressed[index] - active).each { |dir| joystick(index).release(dir) }
|
|
71
|
-
active.each { |dir| joystick(index).press(dir) }
|
|
72
|
-
@pressed[index] = active
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def release(index)
|
|
76
|
-
@pressed[index].each { |dir| joystick(index).release(dir) }
|
|
77
|
-
@pressed[index] = []
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def active?(controller, direction)
|
|
81
|
-
BUTTONS.fetch(direction).any? { |button| SDL::GameControllerGetButton.call(controller, button) == 1 } ||
|
|
82
|
-
tilted?(controller, direction)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def tilted?(controller, direction)
|
|
86
|
-
axis, sign = AXES[direction]
|
|
87
|
-
return false unless axis
|
|
88
|
-
|
|
89
|
-
SDL::GameControllerGetAxis.call(controller, axis) * sign > DEADZONE
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def joystick(index)
|
|
93
|
-
PORTS[index] == 1 ? @computer.joystick1 : @computer.joystick2
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
data/lib/badline/gui/joy_map.rb
DELETED
|
@@ -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
|
data/lib/badline/gui/key_map.rb
DELETED
|
@@ -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
|
data/lib/badline/gui/palette.rb
DELETED
|
@@ -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
|
data/lib/badline/gui/pane.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Badline
|
|
4
|
-
module GUI
|
|
5
|
-
class Pane
|
|
6
|
-
attr_reader :left, :top, :width, :height
|
|
7
|
-
|
|
8
|
-
def initialize(width:, height:, left: 0, top: 0)
|
|
9
|
-
@width = width
|
|
10
|
-
@height = height
|
|
11
|
-
@left = left
|
|
12
|
-
@top = top
|
|
13
|
-
@rect = [left, top, width, height].pack("l4")
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def render(_renderer)
|
|
17
|
-
raise NotImplementedError, "#{self.class} must implement #render"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
# The pixels are little-endian RGBA dwords, red in the low byte.
|
|
23
|
-
def blit(renderer, pixels)
|
|
24
|
-
surface = SDL::CreateRGBSurfaceFrom.call(
|
|
25
|
-
pixels, width, height, 32, width * 4,
|
|
26
|
-
0x0000_00ff, 0x0000_ff00, 0x00ff_0000, 0xff00_0000
|
|
27
|
-
)
|
|
28
|
-
texture = SDL::CreateTextureFromSurface.call(renderer, surface)
|
|
29
|
-
SDL::RenderCopy.call(renderer, texture, nil, @rect)
|
|
30
|
-
ensure
|
|
31
|
-
SDL::DestroyTexture.call(texture) if texture
|
|
32
|
-
SDL::FreeSurface.call(surface) if surface
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Badline
|
|
4
|
-
module GUI
|
|
5
|
-
# The part of the VIC's raster the machine's region crops out.
|
|
6
|
-
class ScreenPane < Pane
|
|
7
|
-
def initialize(computer, left: 0, top: 0, palette: Palette.new)
|
|
8
|
-
@col_offset, @row_offset, crop_width, crop_height = computer.region.crop
|
|
9
|
-
super(width: crop_width, height: crop_height, left:, top:)
|
|
10
|
-
@computer = computer
|
|
11
|
-
@row_bytes = width * 4
|
|
12
|
-
@palette = palette.dwords
|
|
13
|
-
@buffer = ("\x00" * (height * @row_bytes)).b
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def render(renderer)
|
|
17
|
-
blit(renderer, framebuffer)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
# Repack only the scanlines the VIC has touched since the last frame.
|
|
23
|
-
def framebuffer
|
|
24
|
-
vic = @computer.vic
|
|
25
|
-
display = vic.display
|
|
26
|
-
vic_width = vic.width
|
|
27
|
-
dirty = vic.dirty_lines
|
|
28
|
-
|
|
29
|
-
height.times do |row|
|
|
30
|
-
next unless dirty[row + @row_offset]
|
|
31
|
-
|
|
32
|
-
@buffer[row * @row_bytes, @row_bytes] =
|
|
33
|
-
pack_row(display, vic_width, row)
|
|
34
|
-
end
|
|
35
|
-
vic.clear_dirty_lines!
|
|
36
|
-
@buffer
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def pack_row(display, vic_width, row)
|
|
40
|
-
line = display[((row + @row_offset) * vic_width) + @col_offset, width]
|
|
41
|
-
line.map! { |c| @palette[c] }
|
|
42
|
-
line.pack("V*")
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
data/lib/badline/gui/window.rb
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Badline
|
|
4
|
-
module GUI
|
|
5
|
-
class Window
|
|
6
|
-
DEFAULT_REFRESH_RATE = 60
|
|
7
|
-
|
|
8
|
-
# SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
|
|
9
|
-
DISPLAY_MODE_SIZE = 24
|
|
10
|
-
|
|
11
|
-
attr_reader :renderer
|
|
12
|
-
|
|
13
|
-
def initialize(title:, width:, height:, scale: 2, vsync: true)
|
|
14
|
-
SDL.check(SDL::InitSubSystem.call(SDL::INIT_VIDEO | SDL::INIT_EVENTS))
|
|
15
|
-
SDL::SetHint.call("SDL_RENDER_SCALE_QUALITY", "0") # nearest-neighbour
|
|
16
|
-
SDL::SetHint.call("SDL_MOUSE_RELATIVE_SCALING", "0") # host pixels, whatever the window size
|
|
17
|
-
|
|
18
|
-
@window = SDL.check_pointer(SDL::CreateWindow.call(
|
|
19
|
-
title,
|
|
20
|
-
SDL::WINDOWPOS_CENTERED, SDL::WINDOWPOS_CENTERED,
|
|
21
|
-
width * scale, height * scale,
|
|
22
|
-
SDL::WINDOW_RESIZABLE
|
|
23
|
-
))
|
|
24
|
-
|
|
25
|
-
flags = SDL::RENDERER_ACCELERATED
|
|
26
|
-
flags |= SDL::RENDERER_PRESENTVSYNC if vsync
|
|
27
|
-
@renderer = SDL.check_pointer(SDL::CreateRenderer.call(@window, -1, flags))
|
|
28
|
-
SDL.check(SDL::RenderSetLogicalSize.call(@renderer, width, height))
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def title=(title)
|
|
32
|
-
SDL::SetWindowTitle.call(@window, title)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def refresh_rate
|
|
36
|
-
mode = "\0".b * DISPLAY_MODE_SIZE
|
|
37
|
-
SDL.check(SDL::GetCurrentDisplayMode.call(0, mode))
|
|
38
|
-
rate = mode.unpack1("l", offset: 12)
|
|
39
|
-
rate.positive? ? rate : DEFAULT_REFRESH_RATE
|
|
40
|
-
rescue SDL::Error
|
|
41
|
-
DEFAULT_REFRESH_RATE
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def draw(panes)
|
|
45
|
-
SDL::SetRenderDrawColor.call(@renderer, 0, 0, 0, 255)
|
|
46
|
-
SDL::RenderClear.call(@renderer)
|
|
47
|
-
panes.each { |pane| pane.render(@renderer) }
|
|
48
|
-
SDL::RenderPresent.call(@renderer)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def close
|
|
52
|
-
return unless @window
|
|
53
|
-
|
|
54
|
-
SDL::DestroyRenderer.call(@renderer)
|
|
55
|
-
SDL::DestroyWindow.call(@window)
|
|
56
|
-
SDL::QuitSubSystem.call(SDL::INIT_VIDEO | SDL::INIT_EVENTS)
|
|
57
|
-
@window = nil
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
data/lib/badline/gui.rb
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "badline/sdl"
|
|
4
|
-
|
|
5
|
-
require "badline/gui/palette"
|
|
6
|
-
require "badline/gui/key_map"
|
|
7
|
-
require "badline/gui/joy_map"
|
|
8
|
-
require "badline/gui/gamepads"
|
|
9
|
-
require "badline/gui/pane"
|
|
10
|
-
require "badline/gui/screen_pane"
|
|
11
|
-
require "badline/gui/drive_led_pane"
|
|
12
|
-
require "badline/gui/window"
|
|
13
|
-
require "badline/gui/application"
|
data/lib/badline/sdl/events.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Badline
|
|
4
|
-
module SDL
|
|
5
|
-
KEY_TAB = 0x09
|
|
6
|
-
KEY_F10 = 0x4000_0043
|
|
7
|
-
KMOD_SHIFT = 0x0003
|
|
8
|
-
|
|
9
|
-
# The events the front end handles. SDL_Event is a 56-byte union, read
|
|
10
|
-
# here at the offsets its keyboard, mouse and controller members use.
|
|
11
|
-
Quit = Data.define
|
|
12
|
-
# A held key sends repeats, flagged at byte 13 of the event.
|
|
13
|
-
KeyDown = Data.define(:sym, :mod, :repeat) do
|
|
14
|
-
def initialize(sym:, mod:, repeat: false) = super
|
|
15
|
-
end
|
|
16
|
-
KeyUp = Data.define(:sym, :mod)
|
|
17
|
-
MouseMotion = Data.define(:xrel, :yrel)
|
|
18
|
-
MouseButton = Data.define(:button, :pressed)
|
|
19
|
-
ControllerDevice = Data.define
|
|
20
|
-
|
|
21
|
-
EVENT_SIZE = 56
|
|
22
|
-
|
|
23
|
-
# The next event the front end handles, skipping the others, or nil once
|
|
24
|
-
# the queue is empty.
|
|
25
|
-
def self.poll_event
|
|
26
|
-
buffer = (@event ||= Fiddle::Pointer.malloc(EVENT_SIZE, Fiddle::RUBY_FREE))
|
|
27
|
-
while PollEvent.call(buffer) == 1
|
|
28
|
-
event = decode(buffer.to_str(EVENT_SIZE))
|
|
29
|
-
return event if event
|
|
30
|
-
end
|
|
31
|
-
nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.decode(bytes)
|
|
35
|
-
case bytes.unpack1("L")
|
|
36
|
-
when 0x100 then Quit.new
|
|
37
|
-
when 0x300 then KeyDown.new(*bytes.unpack("lS", offset: 20), bytes.getbyte(13) != 0)
|
|
38
|
-
when 0x301 then KeyUp.new(*bytes.unpack("lS", offset: 20))
|
|
39
|
-
when 0x400 then MouseMotion.new(*bytes.unpack("l2", offset: 28))
|
|
40
|
-
when 0x401, 0x402 then MouseButton.new(bytes.getbyte(16), bytes.getbyte(17) == 1)
|
|
41
|
-
when 0x653..0x655 then ControllerDevice.new
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self.key_name(sym) = GetKeyName.call(sym)
|
|
46
|
-
end
|
|
47
|
-
end
|