From fe4cd8835b3fc78f78c0c0abc5590b2aca0410f4 Mon Sep 17 00:00:00 2001 From: Kuba Orlik Date: Sun, 26 Jul 2026 11:54:47 +0200 Subject: [PATCH] Checkpoint with dolphin configs --- README.md | 9 +- deps.sh | 2 + dolphin-presets/ini-to-json.js | 54 +++ dolphin-presets/pick-preset.sh | 22 + dolphin-presets/presets/passthrough.js | 41 ++ .../presets/single-player-steampad.js | 119 +++++ gamepad-utils/list-gamepads.py | 18 + gamepad-utils/pick-a-controller.py | 407 ++++++++++++++++++ gamepad-utils/script-selector.py | 156 +++++++ 9 files changed, 827 insertions(+), 1 deletion(-) create mode 100755 deps.sh create mode 100755 dolphin-presets/ini-to-json.js create mode 100755 dolphin-presets/pick-preset.sh create mode 100644 dolphin-presets/presets/passthrough.js create mode 100755 dolphin-presets/presets/single-player-steampad.js create mode 100644 gamepad-utils/list-gamepads.py create mode 100644 gamepad-utils/pick-a-controller.py create mode 100755 gamepad-utils/script-selector.py diff --git a/README.md b/README.md index 9daeafb..e1c6081 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ -test +# Kazzite + +set of scripts for neat bazzite tricks + +# Setup + +./deps.sh +./setup.sh diff --git a/deps.sh b/deps.sh new file mode 100755 index 0000000..3221c87 --- /dev/null +++ b/deps.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sudo rpm-ostree install python3-pygame diff --git a/dolphin-presets/ini-to-json.js b/dolphin-presets/ini-to-json.js new file mode 100755 index 0000000..b34cc4b --- /dev/null +++ b/dolphin-presets/ini-to-json.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const iniFile = process.argv[2]; + +if (!iniFile) { + console.error("Usage: ini-to-json.js file.ini"); + process.exit(1); +} + +const system = path.basename(iniFile, path.extname(iniFile)); + +const lines = fs.readFileSync(iniFile, "utf8").split(/\r?\n/); + +const result = { + [system]: {} +}; + +let section = null; + +for (const line of lines) { + const trimmed = line.trim(); + + if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) { + continue; + } + + const sectionMatch = trimmed.match(/^\[(.+)\]$/); + + if (sectionMatch) { + section = sectionMatch[1]; + result[system][section] ??= {}; + continue; + } + + if (!section) { + continue; + } + + const eq = trimmed.indexOf("="); + + if (eq === -1) { + continue; + } + + const key = trimmed.slice(0, eq).trim(); + const value = trimmed.slice(eq + 1).trim(); + + result[system][section][key] = value; +} + +console.log(JSON.stringify(result, null, 2)); diff --git a/dolphin-presets/pick-preset.sh b/dolphin-presets/pick-preset.sh new file mode 100755 index 0000000..190b0d3 --- /dev/null +++ b/dolphin-presets/pick-preset.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +export PYGAME_HIDE_SUPPORT_PROMPT=1 +export DOLPHIN_CONFIG_DIR=/home/kuba/.var/app/org.DolphinEmu.dolphin-emu/config/dolphin-emu + +KAZZITE_DIR=/home/kuba/kazzite + +SCRIPTS_DIR=$KAZZITE_DIR/dolphin-presets/presets + +PICKED_SCRIPT=$($KAZZITE_DIR/gamepad-utils/script-selector.py --scripts "$SCRIPTS_DIR") + +echo PICKED!!!!!!!!!!! $PICKED_SCRIPT + +mapfile -t EXTRA_ARGS < <("$PICKED_SCRIPT") + +/usr/bin/flatpak run \ + --branch=stable \ + --arch=x86_64 \ + --command=/app/bin/dolphin-emu-wrapper \ + org.DolphinEmu.dolphin-emu \ + "${EXTRA_ARGS[@]}" \ + "$@" diff --git a/dolphin-presets/presets/passthrough.js b/dolphin-presets/presets/passthrough.js new file mode 100644 index 0000000..2f34d37 --- /dev/null +++ b/dolphin-presets/presets/passthrough.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node +const path = require("path"); +const fs = require("fs"); + +const config = { + "Dolphin": { + "BluetoothPassthrough": { + "Enabled": true + } + } +} + + + +function format_value(value){ + if(typeof value== "boolean"){ + return value? "True" : "False"; + }else{return value} +} + +for (const [system, sections] of Object.entries(config)) { + if(system =="WiimoteNew"){ + let content = ""; + for (const [section, keys] of Object.entries(sections)) { + content += `[${section}]\n`; + + for (const [key, value] of Object.entries(keys)) { + content += `${key} = ${format_value(value)}\n`; + } + + content += "\n"; + } + fs.writeFileSync(path.resolve(process.env.DOLPHIN_CONFIG_DIR, "WiimoteNew.ini"), content); + continue; + } + for (const [section, keys] of Object.entries(sections)) { + for (const [key, value] of Object.entries(keys)) { + console.log(`--config=${system}.${section}.${key}=${format_value(value)}`); + } + } +} diff --git a/dolphin-presets/presets/single-player-steampad.js b/dolphin-presets/presets/single-player-steampad.js new file mode 100755 index 0000000..f48ef38 --- /dev/null +++ b/dolphin-presets/presets/single-player-steampad.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node +const path = require("path"); +const fs = require("fs"); + +const config = { + "Dolphin": { + "BluetoothPassthrough": { + "Enabled": false + } + }, + "WiimoteNew": { + "Wiimote1": { + "Device": "SDL/0/Steam Controller", + "Buttons/A": "`Button S`", + "Buttons/B": "`Trigger R` | `Button E`", + "Buttons/1": "`Button W`", + "Buttons/2": "`Button N`", + "Buttons/-": "Back", + "Buttons/+": "Start", + "Buttons/Home": "Return", + "D-Pad/Up": "`Pad N`", + "D-Pad/Down": "`Pad S`", + "D-Pad/Left": "`Pad W`", + "D-Pad/Right": "`Pad E`", + "IR/Up": "`XInput2/0/Virtual core pointer:Cursor Y-`", + "IR/Down": "`XInput2/0/Virtual core pointer:Cursor Y+`", + "IR/Left": "`XInput2/0/Virtual core pointer:Cursor X-`", + "IR/Right": "`XInput2/0/Virtual core pointer:Cursor X+`", + "Shake/X": "`Button W`", + "Shake/Y": "`Click 2`", + "Shake/Z": "`Click 2`", + "IRPassthrough/Object 1 X": "`IR Object 1 X`", + "IRPassthrough/Object 1 Y": "`IR Object 1 Y`", + "IRPassthrough/Object 1 Size": "`IR Object 1 Size`", + "IRPassthrough/Object 2 X": "`IR Object 2 X`", + "IRPassthrough/Object 2 Y": "`IR Object 2 Y`", + "IRPassthrough/Object 2 Size": "`IR Object 2 Size`", + "IRPassthrough/Object 3 X": "`IR Object 3 X`", + "IRPassthrough/Object 3 Y": "`IR Object 3 Y`", + "IRPassthrough/Object 3 Size": "`IR Object 3 Size`", + "IRPassthrough/Object 4 X": "`IR Object 4 X`", + "IRPassthrough/Object 4 Y": "`IR Object 4 Y`", + "IRPassthrough/Object 4 Size": "`IR Object 4 Size`", + "IMUAccelerometer/Up": "`Shoulder R`", + "IMUAccelerometer/Down": "`Accel Down`", + "IMUAccelerometer/Left": "`Accel Left`", + "IMUAccelerometer/Right": "`Accel Right`", + "IMUAccelerometer/Forward": "`Accel Forward`", + "IMUAccelerometer/Backward": "`Accel Backward`", + "IMUGyroscope/Pitch Up": "`Gyro Pitch Up`", + "IMUGyroscope/Pitch Down": "`Gyro Pitch Down`", + "IMUGyroscope/Roll Left": "`Gyro Roll Left`", + "IMUGyroscope/Roll Right": "`Gyro Roll Right`", + "IMUGyroscope/Yaw Left": "`Gyro Yaw Left`", + "IMUGyroscope/Yaw Right": "`Gyro Yaw Right`", + "Extension": "Nunchuk", + "Nunchuk/Buttons/C": "`Shoulder L`", + "Nunchuk/Buttons/Z": "`Trigger L`", + "Nunchuk/Stick/Up": "`Left Y+`", + "Nunchuk/Stick/Down": "`Left Y-`", + "Nunchuk/Stick/Left": "`Left X-`", + "Nunchuk/Stick/Right": "`Left X+`", + "Nunchuk/Stick/Calibration": "100.00 101.96 108.24 115.81 116.93 116.65 108.24 101.96 100.00 101.96 108.24 109.55 109.52 109.58 108.24 101.96 100.00 101.96 108.24 112.38 113.77 113.06 108.24 101.96 100.00 101.96 108.24 112.86 114.09 114.41 108.24 101.96", + "Nunchuk/Shake/X": "`Click 2`", + "Nunchuk/Shake/Y": "`Click 2`", + "Nunchuk/Shake/Z": "`Click 2`", + "Source": "1", + "IMUIR/Enabled": "False", + "IR/Auto-Hide": "True", + "Extension/Attach MotionPlus": "False" + }, + "Wiimote2": { + "Device": "XInput2/0/Virtual core pointer", + "Source": "0" + }, + "Wiimote3": { + "Device": "XInput2/0/Virtual core pointer", + "Source": "0" + }, + "Wiimote4": { + "Device": "XInput2/0/Virtual core pointer", + "Source": "0" + }, + "BalanceBoard": { + "Device": "XInput2/0/Virtual core pointer", + "Source": "0" + } + } +} + + + +function format_value(value){ + if(typeof value== "boolean"){ + return value? "True" : "False"; + }else{return value} +} + +for (const [system, sections] of Object.entries(config)) { + if(system =="WiimoteNew"){ + let content = ""; + for (const [section, keys] of Object.entries(sections)) { + content += `[${section}]\n`; + + for (const [key, value] of Object.entries(keys)) { + content += `${key} = ${format_value(value)}\n`; + } + + content += "\n"; + } + fs.writeFileSync(path.resolve(process.env.DOLPHIN_CONFIG_DIR, "WiimoteNew.ini"), content); + continue; + } + for (const [section, keys] of Object.entries(sections)) { + for (const [key, value] of Object.entries(keys)) { + console.log(`--config=${system}.${section}.${key}=${format_value(value)}`); + } + } +} diff --git a/gamepad-utils/list-gamepads.py b/gamepad-utils/list-gamepads.py new file mode 100644 index 0000000..f7d8685 --- /dev/null +++ b/gamepad-utils/list-gamepads.py @@ -0,0 +1,18 @@ +import sys +import sdl2 +import sdl2.ext +import time + +def main(): + if sdl2.SDL_Init(sdl2.SDL_INIT_GAMECONTROLLER | sdl2.SDL_INIT_EVENTS) != 0: + print(f"SDL_Init error: {sdl2.SDL_GetError().decode()}") + return 1 + num_joysticks = sdl2.SDL_NumJoysticks() + print("Joystick count: ", num_joysticks) + for i in range(num_joysticks): + print(i) + name = sdl2.SDL_GameControllerNameForIndex(i) + print(name) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gamepad-utils/pick-a-controller.py b/gamepad-utils/pick-a-controller.py new file mode 100644 index 0000000..ecddbfb --- /dev/null +++ b/gamepad-utils/pick-a-controller.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 + +import argparse +import sys +import sdl2 +import sdl2.ext +import sdl2.sdlttf as ttf + + +WIDTH = 1280 +HEIGHT = 720 +FONT = "/usr/share/fonts/open-sans/OpenSans-Semibold.ttf" + + +def debug(msg, enabled): + if enabled: + print(f"[DEBUG] {msg}", file=sys.stderr) + + +class ControllerManager: + + def __init__(self, ignore, debug_enabled): + self.ignore = ignore + self.debug_enabled = debug_enabled + self.controllers = {} + self.unsupported = {} + + def scan(self): + self.controllers.clear() + self.unsupported.clear() + + count = sdl2.SDL_NumJoysticks() + + for i in range(count): + + name_ptr = sdl2.SDL_JoystickNameForIndex(i) + + name = ( + name_ptr.decode() + if name_ptr else + "Unknown Device" + ) + + if sdl2.SDL_IsGameController(i): + + if any( + x.lower() in name.lower() + for x in self.ignore + ): + debug( + f"Ignoring {name}", + self.debug_enabled + ) + continue + + c = sdl2.SDL_GameControllerOpen(i) + + if c: + + joy = sdl2.SDL_GameControllerGetJoystick(c) + + instance = sdl2.SDL_JoystickInstanceID( + joy + ) + + self.controllers[instance] = { + "name": name, + "controller": c + } + + debug( + f"Added controller {name}", + self.debug_enabled + ) + + else: + self.unsupported[i] = name + + debug( + f"Unsupported joystick {name}", + self.debug_enabled + ) + + def add(self, index): + + name_ptr = sdl2.SDL_JoystickNameForIndex(index) + + name = ( + name_ptr.decode() + if name_ptr else + "Unknown Device" + ) + + if sdl2.SDL_IsGameController(index): + + c = sdl2.SDL_GameControllerOpen(index) + + if c: + + joy = sdl2.SDL_GameControllerGetJoystick(c) + + instance = sdl2.SDL_JoystickInstanceID( + joy + ) + + self.controllers[instance] = { + "name": name, + "controller": c + } + + debug( + f"Hotplugged {name}", + self.debug_enabled + ) + + else: + + self.unsupported[index] = name + + debug( + f"Hotplugged unsupported {name}", + self.debug_enabled + ) + + def remove(self, instance): + + if instance in self.controllers: + name = self.controllers[instance]["name"] + + sdl2.SDL_GameControllerClose( + self.controllers[instance]["controller"] + ) + + del self.controllers[instance] + + debug( + f"Removed {name}", + self.debug_enabled + ) + + +def draw_text(renderer, font, text, x, y, color): + + surface = ttf.TTF_RenderUTF8_Blended( + font, + text.encode("utf-8"), + color + ) + + if not surface: + print( + "TTF render failed: " + + ttf.TTF_GetError().decode(), + file=sys.stderr + ) + return + + texture = sdl2.SDL_CreateTextureFromSurface( + renderer, + surface + ) + + if not texture: + print( + "Texture creation failed: " + + sdl2.SDL_GetError().decode(), + file=sys.stderr + ) + sdl2.SDL_FreeSurface(surface) + return + + rect = sdl2.SDL_Rect() + rect.x = x + rect.y = y + rect.w = surface.contents.w + rect.h = surface.contents.h + + result = sdl2.SDL_RenderCopy( + renderer, + texture, + None, + rect + ) + + if result != 0: + print( + "RenderCopy failed: " + + sdl2.SDL_GetError().decode(), + file=sys.stderr + ) + + sdl2.SDL_DestroyTexture(texture) + sdl2.SDL_FreeSurface(surface) + +def main(): + + parser = argparse.ArgumentParser() + + parser.add_argument("question") + + parser.add_argument( + "--ignore", + nargs="*", + default=[] + ) + + parser.add_argument( + "--debug", + action="store_true" + ) + + args = parser.parse_args() + + + sdl2.SDL_Init( + sdl2.SDL_INIT_VIDEO | + sdl2.SDL_INIT_GAMECONTROLLER | + sdl2.SDL_INIT_JOYSTICK + ) + + + ttf.TTF_Init() + + + window = sdl2.SDL_CreateWindow( + args.question.encode(), + sdl2.SDL_WINDOWPOS_CENTERED, + sdl2.SDL_WINDOWPOS_CENTERED, + WIDTH, + HEIGHT, + sdl2.SDL_WINDOW_FULLSCREEN + ) + + + renderer = sdl2.SDL_CreateRenderer( + window, + -1, + sdl2.SDL_RENDERER_ACCELERATED + ) + + info = sdl2.SDL_RendererInfo() + sdl2.SDL_GetRendererInfo(renderer, info) + + print( + "Renderer:", + info.name.decode(), + file=sys.stderr + ) + + + font = ttf.TTF_OpenFont( + FONT.encode(), + 42 + ) + + + manager = ControllerManager( + args.ignore, + args.debug + ) + + manager.scan() + + + event = sdl2.SDL_Event() + + running = True + + selected_controller = None + selected_time = 0 + + while running: + + while sdl2.SDL_PollEvent(event): + + if event.type == sdl2.SDL_CONTROLLERDEVICEADDED: + manager.add( + event.cdevice.which + ) + + elif event.type == sdl2.SDL_JOYDEVICEADDED: + manager.add( + event.jdevice.which + ) + + elif event.type == sdl2.SDL_JOYDEVICEREMOVED: + manager.remove( + event.jdevice.which + ) + + + elif event.type == sdl2.SDL_CONTROLLERBUTTONDOWN: + + instance = event.cbutton.which + + if instance in manager.controllers: + selected_controller = manager.controllers[instance]["name"] + + debug( + f"Selected {selected_controller}", + args.debug + ) + + selected_time = sdl2.SDL_GetTicks() + + # draw + sdl2.SDL_SetRenderDrawColor( + renderer, + 0, + 0, + 0, + 255 + ) + + sdl2.SDL_RenderClear(renderer) + + + draw_text( + renderer, + font, + args.question, + 50, + 50, + sdl2.SDL_Color(255,255,255,255) + ) + + + y = 150 + + + for c in manager.controllers.values(): + + draw_text( + renderer, + font, + "✓ " + c["name"], + 100, + y, + sdl2.SDL_Color(255,255,255,255) + ) + + y += 60 + + + for name in manager.unsupported.values(): + + draw_text( + renderer, + font, + "✗ " + name, + 100, + y, + sdl2.SDL_Color(100,100,100,255) + ) + + y += 60 + + + if selected_controller: + + elapsed = sdl2.SDL_GetTicks() - selected_time + + draw_text( + renderer, + font, + "Selected:", + 100, + 550, + sdl2.SDL_Color(255,255,255,255) + ) + + draw_text( + renderer, + font, + selected_controller, + 100, + 610, + sdl2.SDL_Color(0,255,0,255) + ) + + sdl2.SDL_RenderPresent(renderer) + + if elapsed > 2000: + print(selected_controller) + running = False + + continue + else: + sdl2.SDL_RenderPresent(renderer) + + # cleanup + for c in manager.controllers.values(): + sdl2.SDL_GameControllerClose( + c["controller"] + ) + + ttf.TTF_CloseFont(font) + + sdl2.SDL_DestroyRenderer(renderer) + sdl2.SDL_DestroyWindow(window) + + ttf.TTF_Quit() + sdl2.SDL_Quit() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gamepad-utils/script-selector.py b/gamepad-utils/script-selector.py new file mode 100755 index 0000000..18fcb81 --- /dev/null +++ b/gamepad-utils/script-selector.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 + +import argparse +import base64 +import io +import json +import subprocess +import sys +import time +from pathlib import Path + +import pygame +from PIL import Image + +import sdl2 +import sdl2.ext + + +# ---------- Configuration ---------- +DEFAULT_SCRIPTS_PATH = "/home/kuba/kazzite" +TITLE = "Select script" +BIG_FONT_SIZE = 100 +SMALL_FONT_SIZE = 40 +AXIS_DEADZONE = 0.6 +HAT_REPEAT_DELAY = 0.18 +FPS = 60 + +# ---------- Helpers ---------- + +def load_scripts(directory): + return [ + { + "name": p.stem, + "path": str(p.resolve()) + } + for p in Path(directory).iterdir() + ] + +# ---------- UI with SDL2 input ---------- + +def run_ui(scripts): + pygame.init() + + screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) + screen_w, screen_h = screen.get_size() + pygame.display.set_caption(TITLE) + + scale = min(screen_w / 3840.0, screen_h / 2160.0) + big_font = pygame.font.SysFont(None, int(BIG_FONT_SIZE * scale)) + small_font = pygame.font.SysFont(None, int(SMALL_FONT_SIZE * scale)) + + clock = pygame.time.Clock() + selected = 0 + last_nav = 0 + + # ---------- SDL2 joystick init ---------- + if sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK) != 0: + print("SDL_Init Error:", sdl2.SDL_GetError().decode()) + sys.exit(1) + + joystick_count = sdl2.SDL_NumJoysticks() + sdl_joysticks = [] + for i in range(joystick_count): + js = sdl2.SDL_JoystickOpen(i) + if js: + sdl_joysticks.append(js) + + + running = True + event = sdl2.SDL_Event() + while running: + now = time.time() + pygame.event.pump() # Update pygame keyboard events + + # Handle pygame keyboard events + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_UP: + selected = max(0, selected - 1) + elif event.key == pygame.K_DOWN: + selected = min(len(scripts) - 1, selected + 1) + elif event.key in (pygame.K_RETURN, pygame.K_KP_ENTER): + path = scripts[selected]['path'] + print(path) + running = False + elif event.key == pygame.K_ESCAPE: + print("ESCAPE") + running = False + + sdl2.SDL_JoystickUpdate() + for i, js in enumerate(sdl_joysticks): + # Buttons + for b in range(sdl2.SDL_JoystickNumButtons(js)): + if sdl2.SDL_JoystickGetButton(js, b): + if b == 0: # confirm button + path = scripts[selected]['path'] + print(path) + running = False + elif b == 1: # cancel button + print("CANCEL") + running = False + + # Hats (D-pad) + for h in range(sdl2.SDL_JoystickNumHats(js)): + hat = sdl2.SDL_JoystickGetHat(js, h) + if hat & sdl2.SDL_HAT_UP and now - last_nav > HAT_REPEAT_DELAY: + selected = max(0, selected - 1) + last_nav = now + elif hat & sdl2.SDL_HAT_DOWN and now - last_nav > HAT_REPEAT_DELAY: + selected = min(len(scripts) - 1, selected + 1) + last_nav = now + + # ---------- Render pygame UI ---------- + screen.fill((10, 10, 10)) + + title_surf = small_font.render(TITLE, True, (230, 230, 230)) + screen.blit(title_surf, ((screen_w - title_surf.get_width()) // 2, int(screen_h * 0.08))) + + total = len(scripts) + item_height = int(300 * scale) + start_y = (screen_h - total * item_height) // 2 + + for i, script in enumerate(scripts): + y = start_y + i * item_height + color = (255, 255, 255) if i == selected else (180, 180, 180) + + if i == selected: + pygame.draw.rect(screen, (50, 50, 50), (screen_w * 0.1, y - 20, screen_w * 0.8, item_height - 40), border_radius=20) + + name_surf = big_font.render(script['name'], True, color) + screen.blit(name_surf, (int(screen_w * 0.4), y + item_height // 3)) + + pygame.display.flip() + clock.tick(FPS) + + # Cleanup SDL2 joysticks + for js in sdl_joysticks: + sdl2.SDL_JoystickClose(js) + sdl2.SDL_QuitSubSystem(sdl2.SDL_INIT_JOYSTICK) + pygame.quit() + +# ---------- Main ---------- + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--scripts", default=str(DEFAULT_SCRIPTS_PATH)) + args = parser.parse_args() + + scripts = load_scripts(Path(args.scripts)) + run_ui(scripts) + + +if __name__ == "__main__": + main()