using System; using System.Numerics; using Godosa.Core.Diagnostics; using Godosa.Core.Input; using Silk.NET.SDL; namespace Godosa.Gamepad; /// /// Reads the first attached game controller through windowless SDL. Returns raw /// held state; each game maps it (and applies deadzones). Any SDL failure disables /// the pad for the session and the game stays keyboard-only. /// public sealed unsafe class SdlGamepadSource : IDisposable { private Sdl? _sdl; private GameController* _pad; private bool _failed; public GamepadState? Poll() { if (_failed) return null; try { if (_sdl is null && !Init()) return null; var sdl = _sdl!; sdl.GameControllerUpdate(); // also detects hot-plug (~1.4 µs/call) if (_pad != null && sdl.GameControllerGetAttached(_pad) != SdlBool.True) { sdl.GameControllerClose(_pad); _pad = null; if (EngineDebug.Input) EngineDebug.Write("input", "gamepad detached"); } if (_pad == null && !Open(sdl)) return null; var buttons = PadButtons.None; foreach (var (sdlButton, pad) in ButtonMap) if (sdl.GameControllerGetButton(_pad, sdlButton) != 0) buttons |= pad; return new GamepadState( new Vector2(Axis(sdl, GameControllerAxis.Leftx), Axis(sdl, GameControllerAxis.Lefty)), new Vector2(Axis(sdl, GameControllerAxis.Rightx), Axis(sdl, GameControllerAxis.Righty)), Math.Max(0f, Axis(sdl, GameControllerAxis.Triggerleft)), Math.Max(0f, Axis(sdl, GameControllerAxis.Triggerright)), buttons); } catch (Exception ex) { if (EngineDebug.Input) EngineDebug.Write("input", $"gamepad disabled: {ex.Message}"); _failed = true; return null; } } private static readonly (GameControllerButton Sdl, PadButtons Pad)[] ButtonMap = { (GameControllerButton.A, PadButtons.A), (GameControllerButton.B, PadButtons.B), (GameControllerButton.X, PadButtons.X), (GameControllerButton.Y, PadButtons.Y), (GameControllerButton.Back, PadButtons.Back), (GameControllerButton.Start, PadButtons.Start), (GameControllerButton.Leftshoulder, PadButtons.LeftShoulder), (GameControllerButton.Rightshoulder, PadButtons.RightShoulder), (GameControllerButton.DpadUp, PadButtons.DpadUp), (GameControllerButton.DpadDown, PadButtons.DpadDown), (GameControllerButton.DpadLeft, PadButtons.DpadLeft), (GameControllerButton.DpadRight, PadButtons.DpadRight), }; private bool Init() { var sdl = Sdl.GetApi(); sdl.SetHint(Sdl.HintJoystickAllowBackgroundEvents, "1"); // the Avalonia window has focus, not SDL sdl.SetHint(Sdl.HintNoSignalHandlers, "1"); if (sdl.InitSubSystem(Sdl.InitGamecontroller) != 0) { if (EngineDebug.Input) EngineDebug.Write("input", $"gamepad disabled: {sdl.GetErrorS()}"); _failed = true; return false; } sdl.JoystickEventState(0); // SDL_IGNORE: state is polled; nothing drains SDL's event queue sdl.GameControllerEventState(0); _sdl = sdl; return true; } private bool Open(Sdl sdl) { int count = sdl.NumJoysticks(); for (int i = 0; i < count; i++) { if (sdl.IsGameController(i) != SdlBool.True) continue; _pad = sdl.GameControllerOpen(i); if (_pad == null) continue; if (EngineDebug.Input) EngineDebug.Write("input", $"gamepad attached: {sdl.GameControllerNameS(_pad)}"); return true; } return false; } private float Axis(Sdl sdl, GameControllerAxis axis) => Math.Clamp(sdl.GameControllerGetAxis(_pad, axis) / 32767f, -1f, 1f); public void Dispose() { if (_sdl is null) return; if (_pad != null) _sdl.GameControllerClose(_pad); _pad = null; _sdl.QuitSubSystem(Sdl.InitGamecontroller); _sdl = null; } }