diff --git a/docs/PC.md b/docs/PC.md index 2573282b0..515d4cffa 100644 --- a/docs/PC.md +++ b/docs/PC.md @@ -126,7 +126,12 @@ key is optional; these are the defaults: "input": { "mouse_sensitivity": 0.1, // right-stick deflection (1 = full) per pixel moved in one tick "stick_sensitivity": 1.33, // gamepad stick scale before the game's dead zone (PCSX2's default) - "gyro_sensitivity": 0.5, // right-stick deflection per radian per second the pad turns, while gyro is on + "stick_invert_x": false, // the gamepad's camera stick, flipped left to right + "stick_invert_y": false, // ...and up and down + "gyro": "held", // when the gyroscope turns the camera: off, always, first_person (R2's view) or held (gyro_hold) + "gyro_sensitivity": 0.5, // camera-stick deflection per radian per second the pad turns + "gyro_invert_x": false, + "gyro_invert_y": false, "mouse_invert_y": false, "mouse_capture": true, // SDL relative mouse mode while the window has focus "mouse_release": ["Escape"], // keys that give the cursor back in a window ([]: none) @@ -238,7 +243,7 @@ dungeon's `PadInput_OK` is cross and `PadInput_NO` circle, | F3 | none | the FPS counter on and off (see "The FPS counter") | | gamepad L4 (`paddle2`) | none | the developer menu, in debug mode | | gamepad R4 (`paddle1`) | none | the town or dungeon debug menu, in debug mode, without Select+L2 | -| gamepad L5 (`paddle4`) | none | gyro camera on and off | +| gamepad L5 (`paddle4`) | none | held, the gyroscope turns the camera (`gyro` set to `held`) | The square button is not a guard in this game: the guard is R1 held while locked on, so right click is R1. The camera turns the way the view moves on @@ -250,7 +255,7 @@ default `ry = -MouseY` makes mouse up look up and `mouse_invert_y` flips it. The actions are `up down left right cross circle square triangle l1 r1 l2 r2 l3 r3 start select lx- lx+ ly- ly+ rx- rx+ ry- ry+ lx ly rx ry`, and the -port's own `fps_toggle`, `developer_menu`, `debug_menu` and `gyro_toggle`, which press no pad button. Keys are +port's own `fps_toggle`, `developer_menu`, `debug_menu` and `gyro_hold`, which press no pad button. Keys are SDL names (any case, `_` for a space; `Grave`, `Backquote` or `Backtick` for the key left of 1); `Mouse1` to `Mouse5` are left, right, middle and the two side buttons. The port's own actions also take @@ -270,10 +275,13 @@ Each key-down of a toggle's key counts once, however briefly it is held. onto the DualShock 2's square, keeping its direction: the game's dead zone takes 38% of the travel and the town runs past 0.85, so an unscaled stick only runs at over 90% tilt, and a round one never on a diagonal. -- **Gyro.** While `gyro_toggle` has it on, a gamepad's gyroscope turns the - camera when its right stick is centred: turning the pad left or right - turns the view, tilting it looks up or down, `gyro_sensitivity` times the - rate in radians per second, below 0.03 ignored as drift. +- **Gyro.** When `gyro` says (always, only in first-person view, or while + `gyro_hold` is held), a gamepad's gyroscope turns the camera while the + camera stick is centred: the right stick, or the left in first-person view + (R2). Turning the pad left or right turns the view and tilting it looks up + or down, `gyro_sensitivity` times the rate in radians per second; below 0.03 + is ignored as drift. `gyro_invert_x`/`_y` flip it, and `stick_invert_x`/`_y` + flip the camera stick the same way. - **Mouse.** The right stick at each pad read is the motion since the previous read, divided by the ticks between them, times `mouse_sensitivity` (0.1: ten pixels in one tick is full deflection, diff --git a/port/src/gameloop.cpp b/port/src/gameloop.cpp index ea2d5f19f..0c7b9cfa8 100644 --- a/port/src/gameloop.cpp +++ b/port/src/gameloop.cpp @@ -40,6 +40,7 @@ int EditLoop(); void SndInit(); extern s32 mode; +extern s32 gameTask; extern s32 mc_mode; extern s32 NextMapNo; @@ -789,6 +790,7 @@ int RunGame(int argc, char **argv) { result = ModeLoop(skip_title); GameApplyLoopResult(old_main_mode, result); + InputSetLookOnLeftStick(mode == GAME_MODE_DUNGEON && gameTask == GAME_TASK_EYE_CAMERA); GamePad.UpDate(); GamePad.VibrationEnable(ConfigWords()[kConfigVibrationOff] == 0); GamePad.Step(); diff --git a/port/src/menu_option.cpp b/port/src/menu_option.cpp index 9c43e61dc..08d2d510b 100644 --- a/port/src/menu_option.cpp +++ b/port/src/menu_option.cpp @@ -392,6 +392,18 @@ void RestoreStickSensitivity(Config &config, const Config &defaults) { config.stick_sensitivity = defaults.stick_sensitivity; } +int GyroCount(const Config &) { + return 4; +} + +int GyroChoice(const Config &config) { + return static_cast(config.gyro); +} + +void SetGyro(Config &config, int choice) { + config.gyro = static_cast(choice); +} + int GyroSensitivityCount(const Config &) { return 39; } @@ -465,9 +477,19 @@ const Row kControlRows[] = { Row{"input.stick_sensitivity", "Stick Sensitivity", "\"Stick Sensitivity\"\nHow far a gamepad's\nstick has to tilt.", -1, StickSensitivityCount, StickSensitivityChoice, SetStickSensitivity, StickSensitivityText, nullptr, RestoreStickSensitivity}, + OnOffRow<&Config::stick_invert_x>("input.stick_invert_x", "Invert Stick X", + "\"Invert Stick X\"\nFlips the camera's\nleft and right."), + OnOffRow<&Config::stick_invert_y>("input.stick_invert_y", "Invert Stick Y", + "\"Invert Stick Y\"\nFlips the camera's\nup and down."), + Row{"input.gyro", "Gyro", "\"Gyro\"\nWhen tilting the pad\nturns the camera.", -1, GyroCount, GyroChoice, SetGyro, + nullptr, "Off|Always|First Person|While Held"}, Row{"input.gyro_sensitivity", "Gyro Sensitivity", "\"Gyro Sensitivity\"\nHow fast tilting\nturns the camera.", -1, GyroSensitivityCount, GyroSensitivityChoice, SetGyroSensitivity, GyroSensitivityText, nullptr, RestoreGyroSensitivity}, + OnOffRow<&Config::gyro_invert_x>("input.gyro_invert_x", "Invert Gyro X", + "\"Invert Gyro X\"\nFlips the gyro's\nleft and right."), + OnOffRow<&Config::gyro_invert_y>("input.gyro_invert_y", "Invert Gyro Y", + "\"Invert Gyro Y\"\nFlips the gyro's\nup and down."), }; // A page of the screen. Every page so far is a list of rows; one that needs its own layout, such as diff --git a/port/src/platform/config.cpp b/port/src/platform/config.cpp index 8469ae28e..9f033b252 100644 --- a/port/src/platform/config.cpp +++ b/port/src/platform/config.cpp @@ -106,6 +106,22 @@ bool ReadAspect(const Json &value, ConfigAspect &out) { return true; } +constexpr const char *kGyroNames[] = {"off", "always", "first_person", "held"}; + +bool ReadGyro(const Json &value, ConfigGyro &out) { + if (!value.is_string()) { + return false; + } + std::string gyro = Lower(value.get()); + for (std::size_t i = 0; i < std::size(kGyroNames); ++i) { + if (gyro == kGyroNames[i]) { + out = static_cast(i); + return true; + } + } + return false; +} + bool ReadList(const Json &value, std::vector &out) { std::vector items; if (value.is_string()) { @@ -226,6 +242,21 @@ bool Apply(Config &config, std::string_view name, const Json &value) { config.stick_sensitivity = sensitivity; return true; } + if (name == "input.gyro") { + return ReadGyro(value, config.gyro); + } + if (name == "input.stick_invert_x") { + return ReadBool(value, config.stick_invert_x); + } + if (name == "input.stick_invert_y") { + return ReadBool(value, config.stick_invert_y); + } + if (name == "input.gyro_invert_x") { + return ReadBool(value, config.gyro_invert_x); + } + if (name == "input.gyro_invert_y") { + return ReadBool(value, config.gyro_invert_y); + } if (name == "input.gyro_sensitivity") { float sensitivity = 0.0f; if (!ReadNumber(value, sensitivity) || !(sensitivity > 0.0f) || !std::isfinite(sensitivity)) { @@ -431,7 +462,12 @@ std::string ConfigSerialize(const Config &config) { root["audio"]["sound"] = options.stereo ? "stereo" : "mono"; root["input"]["mouse_sensitivity"] = Shortest(config.mouse_sensitivity); root["input"]["stick_sensitivity"] = Shortest(config.stick_sensitivity); + root["input"]["stick_invert_x"] = config.stick_invert_x; + root["input"]["stick_invert_y"] = config.stick_invert_y; + root["input"]["gyro"] = kGyroNames[static_cast(config.gyro)]; root["input"]["gyro_sensitivity"] = Shortest(config.gyro_sensitivity); + root["input"]["gyro_invert_x"] = config.gyro_invert_x; + root["input"]["gyro_invert_y"] = config.gyro_invert_y; root["input"]["mouse_invert_y"] = config.mouse_invert_y; root["input"]["mouse_capture"] = config.mouse_capture; root["input"]["mouse_release"] = config.mouse_release_keys; diff --git a/port/src/platform/config.hpp b/port/src/platform/config.hpp index 578802a44..0ef057e00 100644 --- a/port/src/platform/config.hpp +++ b/port/src/platform/config.hpp @@ -16,6 +16,13 @@ enum class ConfigAspect { FourThree, }; +enum class ConfigGyro { + Off, + Always, + FirstPerson, + Held, +}; + struct ConfigKeyBinding { std::string action; std::vector keys; @@ -63,7 +70,12 @@ struct Config { std::vector key_bindings; float mouse_sensitivity = 0.1f; float stick_sensitivity = 1.33f; + bool stick_invert_x = false; + bool stick_invert_y = false; + ConfigGyro gyro = ConfigGyro::Held; float gyro_sensitivity = 0.5f; + bool gyro_invert_x = false; + bool gyro_invert_y = false; bool mouse_invert_y = false; bool mouse_capture = true; std::vector mouse_release_keys = {"Escape"}; diff --git a/port/src/platform/input.cpp b/port/src/platform/input.cpp index 5567016d7..52097b124 100644 --- a/port/src/platform/input.cpp +++ b/port/src/platform/input.cpp @@ -78,7 +78,7 @@ constexpr Action kActions[] = { {"fps_toggle", ActionKind::Host, 0, kAxisLeftX, 0, "F3"}, {"developer_menu", ActionKind::Host, 0, kAxisLeftX, 0, "Gamepad:paddle2"}, {"debug_menu", ActionKind::Host, 0, kAxisLeftX, 0, "Gamepad:paddle1"}, - {"gyro_toggle", ActionKind::Host, 0, kAxisLeftX, 0, "Gamepad:paddle4"}, + {"gyro_hold", ActionKind::Host, 0, kAxisLeftX, 0, "Gamepad:paddle4"}, }; struct ButtonMap { @@ -109,7 +109,7 @@ constexpr std::size_t kFirstHostAction = kActionCount - 4; constexpr std::size_t kHostActionCount = kActionCount - kFirstHostAction; static_assert(kActions[kFirstHostAction].name == "fps_toggle"); static_assert(static_cast(InputHostAction::FpsToggle) == 0); -static_assert(static_cast(InputHostAction::GyroToggle) == kHostActionCount - 1); +static_assert(static_cast(InputHostAction::GyroHold) == kHostActionCount - 1); // AxisCalibration (ps2/src/gamepad.cpp): a byte within 49 above or 50 below the centre reads as // zero, and the remaining 78 steps each side span the game's +-128. @@ -287,8 +287,13 @@ std::uint8_t StickToByte(Sint16 value) { // input.stick_sensitivity (1.33 by default, PCSX2's analog sensitivity) and the circle is stretched // onto the square, keeping the direction. float g_stick_sensitivity = 1.33f; -float g_gyro_sensitivity = 0.5f; -bool g_gyro_on = false; +float g_gyro_sensitivity = 0.5f; +ConfigGyro g_gyro = ConfigGyro::Off; +bool g_gyro_invert_x = false; +bool g_gyro_invert_y = false; +bool g_stick_invert_x = false; +bool g_stick_invert_y = false; +bool g_look_left = false; void StickPairToBytes(Sint16 x, Sint16 y, std::uint8_t &byte_x, std::uint8_t &byte_y) { float fx = std::clamp(static_cast(x) / 32767.0f, -1.0f, 1.0f); @@ -322,6 +327,23 @@ std::uint8_t &AxisOf(InputPadState &state, Axis axis) { return state.right_y; } +Sint16 Flip(Sint16 value) { + return value == SDL_JOYSTICK_AXIS_MIN ? SDL_JOYSTICK_AXIS_MAX : static_cast(-value); +} + +bool GyroActive() { + switch (g_gyro) { + case ConfigGyro::Always: + return true; + case ConfigGyro::FirstPerson: + return g_look_left; + case ConfigGyro::Held: + return InputHostHeld(InputHostAction::GyroHold); + default: + return false; + } +} + void ReadGamepad(SDL_Gamepad *gamepad, InputPadState &state) { for (const ButtonMap &map : kGamepadButtons) { if (SDL_GetGamepadButton(gamepad, map.button)) { @@ -334,24 +356,35 @@ void ReadGamepad(SDL_Gamepad *gamepad, InputPadState &state) { if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > kTriggerThreshold) { state.buttons |= kInputR2; } - StickPairToBytes(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX), - SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY), state.left_x, state.left_y); - StickPairToBytes(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX), - SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY), state.right_x, state.right_y); - float rate[3]; - if (!g_gyro_on || Deflected(state.right_x) || Deflected(state.right_y) || - !SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_GYRO, rate, 3)) { + Sint16 axes[4] = {SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX), + SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY), + SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX), + SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY)}; + int look = g_look_left ? 0 : 2; + if (g_stick_invert_x) { + axes[look] = Flip(axes[look]); + } + if (g_stick_invert_y) { + axes[look + 1] = Flip(axes[look + 1]); + } + StickPairToBytes(axes[0], axes[1], state.left_x, state.left_y); + StickPairToBytes(axes[2], axes[3], state.right_x, state.right_y); + std::uint8_t &x = g_look_left ? state.left_x : state.right_x; + std::uint8_t &y = g_look_left ? state.left_y : state.right_y; + float rate[3]; + if (!GyroActive() || Deflected(x) || Deflected(y) || !SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_GYRO, rate, 3)) { return; } float yaw = std::fabs(rate[1]) > kGyroDeadband ? -rate[1] * g_gyro_sensitivity : 0.0f; - float pitch = std::fabs(rate[0]) > kGyroDeadband ? rate[0] * g_gyro_sensitivity : 0.0f; - state.right_x = InputStickByte(yaw); - state.right_y = InputStickByte(pitch); + float pitch = std::fabs(rate[0]) > kGyroDeadband ? -rate[0] * g_gyro_sensitivity : 0.0f; + x = InputStickByte(g_gyro_invert_x ? -yaw : yaw); + pitch = g_gyro_invert_y ? -pitch : pitch; + y = InputStickByte(g_look_left ? -pitch : pitch); } void EnableGyro(SDL_Gamepad *gamepad) { if (SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO)) { - SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, g_gyro_on); + SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, g_gyro != ConfigGyro::Off); } } @@ -491,6 +524,18 @@ void InputApplyConfig(const Config &config) { } g_stick_sensitivity = config.stick_sensitivity; g_gyro_sensitivity = config.gyro_sensitivity; + g_gyro_invert_x = config.gyro_invert_x; + g_gyro_invert_y = config.gyro_invert_y; + g_stick_invert_x = config.stick_invert_x; + g_stick_invert_y = config.stick_invert_y; + if (config.gyro != g_gyro) { + g_gyro = config.gyro; + for (PadSlot &slot : g_slots) { + if (slot.gamepad != nullptr) { + EnableGyro(slot.gamepad); + } + } + } InputMouseSettings mouse; mouse.sensitivity = config.mouse_sensitivity; mouse.invert_y = config.mouse_invert_y; @@ -515,7 +560,7 @@ void InputShutdown() { g_scripted = {}; g_host_presses.fill(0); g_host_polled.fill(false); - g_gyro_on = false; + g_gyro = ConfigGyro::Off; for (PadSlot &slot : g_slots) { if (slot.gamepad != nullptr) { SDL_CloseGamepad(slot.gamepad); @@ -533,14 +578,6 @@ void InputPoll() { if (g_gamepad_subsystem) { SyncGamepads(); } - if (InputHostPressed(InputHostAction::GyroToggle)) { - g_gyro_on = !g_gyro_on; - for (PadSlot &slot : g_slots) { - if (slot.gamepad != nullptr) { - EnableGyro(slot.gamepad); - } - } - } for (int pad = 0; pad < kInputPadCount; ++pad) { InputPadState state; state.connected = pad == 0 || g_slots[pad].gamepad != nullptr; @@ -823,6 +860,10 @@ bool InputHostHeld(InputHostAction action) { return HostPolledHeld(host); } +void InputSetLookOnLeftStick(bool left) { + g_look_left = left; +} + bool InputHostPressed(InputHostAction action) { int &presses = g_host_presses[static_cast(action)]; if (presses == 0) { diff --git a/port/src/platform/input.hpp b/port/src/platform/input.hpp index dd64d28b9..db1c516b4 100644 --- a/port/src/platform/input.hpp +++ b/port/src/platform/input.hpp @@ -100,6 +100,8 @@ bool InputLeftStickLive(); // The pad as the game reads it: the device state or the override, stick_dpad applied. const InputPadState &InputGetPad(int pad); +void InputSetLookOnLeftStick(bool left); + void InputSetRumble(int pad, InputRumble rumble); InputRumble InputGetRumble(int pad); @@ -155,7 +157,7 @@ enum class InputHostAction { FpsToggle, DeveloperMenu, DebugMenu, - GyroToggle, + GyroHold, }; // Whether a key, mouse button or gamepad button bound to the action is held, live or scripted. diff --git a/port/src/tests/platform_config_test.cpp b/port/src/tests/platform_config_test.cpp index 65a753468..4dcf276db 100644 --- a/port/src/tests/platform_config_test.cpp +++ b/port/src/tests/platform_config_test.cpp @@ -95,6 +95,12 @@ TEST(PlatformConfig, ParsesMouseSettingsAndBindings) { Config defaults = ConfigParse(""); ASSERT_TRUE(defaults.mouse_sensitivity == 0.1f && !defaults.mouse_invert_y && defaults.mouse_capture); ASSERT_TRUE(defaults.stick_sensitivity == 1.33f && defaults.gyro_sensitivity == 0.5f); + ASSERT_TRUE(defaults.gyro == ConfigGyro::Held && !defaults.gyro_invert_x && !defaults.stick_invert_y); + ASSERT_TRUE(ConfigParse(R"({"input": {"gyro": "first_person"}})").gyro == ConfigGyro::FirstPerson); + ASSERT_TRUE(ConfigParse(R"({"input": {"gyro": true}})").gyro == ConfigGyro::Held); + Config inverted = + ConfigParse(ConfigSerialize(ConfigParse(R"({"input": {"gyro": "always", "gyro_invert_y": true, "stick_invert_x": true}})"))); + ASSERT_TRUE(inverted.gyro == ConfigGyro::Always && inverted.gyro_invert_y && inverted.stick_invert_x); ASSERT_TRUE(ConfigParse(R"({"input": {"gyro_sensitivity": 1.25}})").gyro_sensitivity == 1.25f); ASSERT_TRUE(ConfigParse(R"({"input": {"gyro_sensitivity": -1}})").gyro_sensitivity == 0.5f); ASSERT_TRUE(ConfigParse(ConfigSerialize(ConfigParse(R"({"input": {"gyro_sensitivity": 1.25}})"))).gyro_sensitivity == diff --git a/port/src/tests/platform_pad_test.cpp b/port/src/tests/platform_pad_test.cpp index 609128066..da88f070d 100644 --- a/port/src/tests/platform_pad_test.cpp +++ b/port/src/tests/platform_pad_test.cpp @@ -273,7 +273,7 @@ TEST(PlatformPad, BackButtonActions) { std::string_view paddles[][1] = {{"Gamepad:paddle2"}, {"Gamepad:paddle1"}, {"Gamepad:paddle4"}}; ASSERT_TRUE(InputBindKeys("developer_menu", paddles[0])); ASSERT_TRUE(InputBindKeys("debug_menu", paddles[1])); - ASSERT_TRUE(InputBindKeys("gyro_toggle", paddles[2])); + ASSERT_TRUE(InputBindKeys("gyro_hold", paddles[2])); std::string_view f9[] = {"F9"}; std::string_view f10[] = {"F10"}; ASSERT_TRUE(InputBindKeys("debug_menu", f9));