Repository navigation
Add shake to trick #378
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add shake to trick #378
Changes from all commits
42e035d
cc63a8c
4ac4c90
02e643c
2f65138
99e93df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| #pragma once | ||
|
|
||
| // Shake to trick: a flick of a controller's motion sensor briefly presses the | ||
| // matching D-pad direction, which the game treats as a trick (or a wheelie on | ||
| // bikes), like shaking a Wii Remote. | ||
|
|
||
| #include <cstdint> | ||
|
|
||
| #include <dolphin/pad.h> | ||
|
|
||
| #include "motion_trick_detector.h" | ||
|
|
||
| namespace MotionTrick { | ||
|
|
||
| bool Enabled() noexcept; | ||
| void SetEnabled(bool enabled) noexcept; | ||
| int Sensitivity() noexcept; | ||
| void SetSensitivity(int level) noexcept; | ||
|
|
||
| // The controller on this port reports a gyro or accelerometer of its own. | ||
| bool PortHasMotion(uint32_t port) noexcept; | ||
|
|
||
| // Call once per guest PADRead, after every other input source has been merged. | ||
| void Apply(PADStatus* statuses) noexcept; | ||
|
|
||
| struct PortStatus { | ||
| float intensity = 0.0f; // decaying peak, 1 being the trigger threshold | ||
| Direction lastTrick = Direction::None; | ||
| uint64_t lastTrickUs = 0; | ||
| bool sensorsUnavailable = false; // the controller refused to turn its sensors on | ||
| }; | ||
| PortStatus Status(uint32_t port) noexcept; | ||
|
|
||
| uint64_t NowUs() noexcept; | ||
|
|
||
| } // namespace MotionTrick |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| #pragma once | ||
|
|
||
| // Shake detection for controllers with motion sensors. Pure host code, so it is | ||
| // unit-testable without SDL. | ||
|
|
||
| #include <cstdint> | ||
|
|
||
| namespace MotionTrick { | ||
|
|
||
| enum class Direction : uint8_t { None, Up, Down, Left, Right }; | ||
|
|
||
| inline constexpr int kMinSensitivity = 1; | ||
| inline constexpr int kMaxSensitivity = 5; | ||
| inline constexpr int kDefaultSensitivity = 3; | ||
|
|
||
| // SDL gamepad sensor axes: x right, y top, z toward the player. | ||
| struct MotionSample { | ||
| bool hasGyro = false; | ||
| bool hasAccel = false; | ||
| float gyro[3]{}; // rad/s: pitch, yaw, roll | ||
| float accel[3]{}; // m/s^2, gravity included | ||
| }; | ||
|
|
||
| // Fed one sample per game frame; reports at most one trick per flick. | ||
| class ShakeDetector { | ||
| public: | ||
| ShakeDetector() { SetSensitivity(kDefaultSensitivity); } | ||
|
|
||
| void SetSensitivity(int level); | ||
| Direction Update(const MotionSample& sample, uint64_t nowUs); | ||
|
|
||
| // Strength of the last sample, 1 being the trigger threshold. | ||
| float Intensity() const { return m_intensity; } | ||
|
|
||
| private: | ||
| float m_gyroThreshold = 0.0f; | ||
| float m_accelThreshold = 0.0f; | ||
| float m_intensity = 0.0f; | ||
| bool m_armed = true; | ||
| uint64_t m_lastFireUs = 0; | ||
| }; | ||
|
|
||
| } // namespace MotionTrick |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| #include "motion_trick.h" | ||
|
|
||
| #include "input_bindings.h" | ||
| #include "runtime_config.h" | ||
|
|
||
| #include <algorithm> | ||
| #include <array> | ||
| #include <atomic> | ||
| #include <cstring> | ||
| #include <mutex> | ||
|
|
||
| #include <SDL3/SDL_gamepad.h> | ||
| #include <SDL3/SDL_keyboard.h> | ||
| #include <SDL3/SDL_timer.h> | ||
|
|
||
| namespace MotionTrick { | ||
| namespace { | ||
|
|
||
| // Long enough for the game to see the press, short enough not to hold a trick. | ||
| constexpr int kPulseFrames = 4; | ||
| // Sensors report zeros or stale data for a moment after being switched on. | ||
| constexpr int kWarmupFrames = 6; | ||
| constexpr float kIntensityDecay = 0.85f; | ||
| // Real sensors always jitter; identical readings mean the stream was switched | ||
| // off behind our back, so ask for it again a few times. | ||
| constexpr int kStaleFrames = 30; | ||
| constexpr uint64_t kResyncIntervalUs = 2'000'000; | ||
| constexpr int kMaxResyncs = 3; | ||
|
|
||
| struct PortState { | ||
| SDL_JoystickID id = 0; | ||
| bool gyro = false; | ||
| bool accel = false; | ||
| int warmup = 0; | ||
| MotionSample previous; | ||
| int staleFrames = 0; | ||
| int resyncs = 0; | ||
| uint64_t lastResyncUs = 0; | ||
| ShakeDetector detector; | ||
| float displayIntensity = 0.0f; | ||
| Direction pulse = Direction::None; | ||
| int pulseFrames = 0; | ||
| Direction lastTrick = Direction::None; | ||
| uint64_t lastTrickUs = 0; | ||
| }; | ||
|
|
||
| std::atomic<bool> g_enabled{RuntimeConfigFile::MotionTrickEnabled(false)}; | ||
| std::atomic<int> g_sensitivity{RuntimeConfigFile::MotionTrickSensitivity(kDefaultSensitivity)}; | ||
|
|
||
| std::mutex g_mutex; | ||
| std::array<PortState, PAD_CHANMAX> g_ports; | ||
|
|
||
| // A keyboard player keeps their controller assigned, but aurora ignores it. | ||
| SDL_Gamepad* GamepadForPort(uint32_t port) { | ||
| uint32_t keyCount = 0; | ||
| if (PADGetKeyButtonBindings(port, &keyCount) != nullptr) { | ||
| return nullptr; | ||
| } | ||
| const s32 index = PADGetIndexForPort(port); | ||
| return index < 0 ? nullptr : PADGetSDLGamepadForIndex(static_cast<u32>(index)); | ||
| } | ||
|
|
||
| uint16_t ButtonFor(Direction direction) { | ||
| switch (direction) { | ||
| case Direction::Up: return PAD_BUTTON_UP; | ||
| case Direction::Down: return PAD_BUTTON_DOWN; | ||
| case Direction::Left: return PAD_BUTTON_LEFT; | ||
| case Direction::Right: return PAD_BUTTON_RIGHT; | ||
| default: return 0; | ||
| } | ||
| } | ||
|
|
||
| bool HeldByOtherPort(uint32_t port, SDL_JoystickID id) { | ||
| for (uint32_t other = 0; other < g_ports.size(); ++other) { | ||
| if (other != port && g_ports[other].id == id) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| void SetSensors(SDL_Gamepad* gamepad, const PortState& state, bool enabled) { | ||
| if (state.gyro) SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, enabled); | ||
| if (state.accel) SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, enabled); | ||
| } | ||
|
|
||
| void Attach(uint32_t port, SDL_Gamepad* gamepad) { | ||
| PortState& state = g_ports[port]; | ||
| const SDL_JoystickID id = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0; | ||
| if (id == state.id) { | ||
| return; | ||
| } | ||
| if (state.id != 0 && !HeldByOtherPort(port, state.id)) { | ||
| if (SDL_Gamepad* previous = SDL_GetGamepadFromID(state.id)) { | ||
| SetSensors(previous, state, false); | ||
| } | ||
| } | ||
| state = PortState{}; | ||
| if (gamepad == nullptr) { | ||
| return; | ||
| } | ||
| state.id = id; | ||
| state.gyro = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO) && | ||
| SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, true); | ||
| state.accel = SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL) && | ||
| SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, true); | ||
| state.warmup = kWarmupFrames; | ||
| } | ||
|
|
||
| bool SameReading(const MotionSample& a, const MotionSample& b) { | ||
| return a.hasGyro == b.hasGyro && a.hasAccel == b.hasAccel && | ||
| std::memcmp(a.gyro, b.gyro, sizeof(a.gyro)) == 0 && std::memcmp(a.accel, b.accel, sizeof(a.accel)) == 0; | ||
| } | ||
|
|
||
| // Returns true when the sensors were restarted. | ||
| bool ResyncIfStale(SDL_Gamepad* gamepad, PortState& state, const MotionSample& sample, uint64_t now) { | ||
| const bool stale = SameReading(sample, state.previous) && SDL_GetKeyboardFocus() != nullptr; | ||
| state.previous = sample; | ||
| state.staleFrames = stale ? state.staleFrames + 1 : 0; | ||
| if (!stale) { | ||
| state.resyncs = 0; | ||
| } | ||
| if (state.staleFrames < kStaleFrames || state.resyncs >= kMaxResyncs || | ||
| now - state.lastResyncUs < kResyncIntervalUs) { | ||
| return false; | ||
| } | ||
| SetSensors(gamepad, state, false); | ||
| SetSensors(gamepad, state, true); | ||
| state.staleFrames = 0; | ||
| ++state.resyncs; | ||
| state.lastResyncUs = now; | ||
| state.warmup = kWarmupFrames; | ||
| return true; | ||
| } | ||
|
|
||
| MotionSample ReadSample(SDL_Gamepad* gamepad, const PortState& state) { | ||
| MotionSample sample; | ||
| sample.hasGyro = state.gyro && SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_GYRO, sample.gyro, 3); | ||
| sample.hasAccel = state.accel && SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_ACCEL, sample.accel, 3) && | ||
| (sample.accel[0] != 0.0f || sample.accel[1] != 0.0f || sample.accel[2] != 0.0f); | ||
| return sample; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| bool Enabled() noexcept { | ||
| return g_enabled.load(std::memory_order_relaxed); | ||
| } | ||
|
|
||
| void SetEnabled(bool enabled) noexcept { | ||
| g_enabled.store(enabled, std::memory_order_relaxed); | ||
| RuntimeConfigFile::SetMotionTrickEnabled(enabled); | ||
| } | ||
|
|
||
| int Sensitivity() noexcept { | ||
| return g_sensitivity.load(std::memory_order_relaxed); | ||
| } | ||
|
|
||
| void SetSensitivity(int level) noexcept { | ||
| level = std::clamp(level, kMinSensitivity, kMaxSensitivity); | ||
| g_sensitivity.store(level, std::memory_order_relaxed); | ||
| RuntimeConfigFile::SetMotionTrickSensitivity(level); | ||
| } | ||
|
|
||
| bool PortHasMotion(uint32_t port) noexcept { | ||
| SDL_Gamepad* gamepad = GamepadForPort(port); | ||
| return gamepad != nullptr && | ||
| (SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO) || SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL)); | ||
| } | ||
|
|
||
| uint64_t NowUs() noexcept { | ||
| return SDL_GetTicksNS() / 1000; | ||
| } | ||
|
|
||
| void Apply(PADStatus* statuses) noexcept { | ||
| if (statuses == nullptr) { | ||
| return; | ||
| } | ||
| const bool enabled = Enabled(); | ||
| const int sensitivity = Sensitivity(); | ||
| const bool blocked = InputBindings::InputBlocked(); | ||
| const uint64_t now = NowUs(); | ||
|
|
||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| for (uint32_t port = 0; port < PAD_CHANMAX; ++port) { | ||
| SDL_Gamepad* gamepad = | ||
| enabled && statuses[port].err == PAD_ERR_NONE ? GamepadForPort(port) : nullptr; | ||
| Attach(port, gamepad); | ||
| PortState& state = g_ports[port]; | ||
| if (gamepad == nullptr || (!state.gyro && !state.accel)) { | ||
| continue; | ||
| } | ||
| if (state.warmup > 0) { | ||
| --state.warmup; | ||
| } else if (const MotionSample sample = ReadSample(gamepad, state); | ||
| !ResyncIfStale(gamepad, state, sample, now)) { | ||
| state.detector.SetSensitivity(sensitivity); | ||
| const Direction fired = state.detector.Update(sample, now); | ||
| state.displayIntensity = | ||
| std::max(state.detector.Intensity(), state.displayIntensity * kIntensityDecay); | ||
| if (fired != Direction::None) { | ||
| state.lastTrick = fired; | ||
| state.lastTrickUs = now; | ||
| state.pulse = fired; | ||
| state.pulseFrames = kPulseFrames; | ||
| } | ||
| } | ||
| if (blocked) { | ||
| state.pulseFrames = 0; | ||
| } | ||
| if (state.pulseFrames > 0) { | ||
| statuses[port].button |= ButtonFor(state.pulse); | ||
| --state.pulseFrames; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| PortStatus Status(uint32_t port) noexcept { | ||
| if (port >= PAD_CHANMAX) { | ||
| return {}; | ||
| } | ||
| std::lock_guard<std::mutex> lock(g_mutex); | ||
| const PortState& state = g_ports[port]; | ||
| return {state.displayIntensity, state.lastTrick, state.lastTrickUs, state.id != 0 && !state.gyro && !state.accel}; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| } // namespace MotionTrick | ||
Uh oh!
There was an error while loading. Please reload this page.