Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions UnleashedRecomp/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <os/process.h>
#include <patches/audio_patches.h>
#include <patches/inspire_patches.h>
#include <patches/player_patches.h>
#include <ui/game_window.h>
#include <user/config.h>
#include <user/paths.h>
Expand Down Expand Up @@ -62,6 +63,9 @@ PPC_FUNC(sub_822C1130)
App::s_deltaTime = ctx.f1.f64;
App::s_time += App::s_deltaTime;

// Player-state change notifications.
PlayerPatches::Update();

// This function can also be called by the loading thread,
// which SDL does not like. To prevent the OS from thinking
// the process is unresponsive, we will flush while waiting
Expand Down
8 changes: 8 additions & 0 deletions UnleashedRecomp/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ class App
static inline double s_deltaTime;
static inline double s_time = 0.0; // How much time elapsed since the game started.

// True while the user is holding the right trigger past the boost threshold and
// RightTriggerAction==Boost remap is live for DAY Sonic.
static inline std::atomic<bool> s_rtBoost;

// Steady-clock milliseconds of the most recent QTE prompt window update, stamped
// by QTEPromptActiveMidAsmHook.
static inline std::atomic<uint32_t> s_lastQTEPromptMs;

static void Restart(std::vector<std::string> restartArgs = {});
static void Exit();
};
Expand Down
175 changes: 175 additions & 0 deletions UnleashedRecomp/hid/driver/sdl_hid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,173 @@
#include <os/logger.h>
#include <ui/game_window.h>
#include <kernel/xdm.h>
#include <api/SWA.h>
#include <app.h>

#define TRANSLATE_INPUT(S, X) SDL_GameControllerGetButton(controller, S) << FirstBitLow(X)
#define VIBRATION_TIMEOUT_MS 5000

static constexpr uint8_t BOOST_TRIGGER_THRESHOLD = 30;

class Controller
{
private:
bool IsBoostOnRightTriggerActive()
{
bool userConfigIsBoost = Config::RightTriggerAction == ERightTriggerAction::Boost;

if (!userConfigIsBoost || App::s_isWerehog)
return false;

// During a QTE prompt, keep the remap (and the boost aura) alive only while the
// right trigger is held continuously from before the prompt. If it isn't held,
// or gets released while the prompt is up, suspend the remap so the fabricated
// X can't answer the QTE and the real face button can.
bool qteOnScreen = IsQTEPromptOnScreen();
bool rtHeld = uint8_t(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7) >= 30;

if (!qteOnScreen)
qteRemapReleased = false;
else if (!rtHeld)
qteRemapReleased = true;

if (qteOnScreen && qteRemapReleased)
return false;

return true;
}

bool DetermineChipPlayerStatus() {
auto pGameDocument = SWA::CGameDocument::GetInstance();

if (pGameDocument == NULL)
return false;

const char* stageName = pGameDocument->m_pMember->m_StageName.c_str();
const bool hasStage = stageName && strlen(stageName);

if (hasStage == false)
return false;

// There's no "BossDarkGaia1_2Air" so regex is NOT needeed.
const bool playingAsChip = !strcmp(stageName, "BossDarkGaia1_1Air");

return playingAsChip;
}

uint32_t GetBoostCancelDurationMs()
{
int32_t fps = Config::FPS > 0 ? Config::FPS : 60;
return static_cast<uint32_t>(2000 / fps);
}

bool IsQTEPromptOnScreen()
{
// QTEPromptActiveMidAsmHook stamps this every frame a trick-QTE prompt is on
// screen; treat a recent stamp as "a QTE is waiting".
uint32_t last = App::s_lastQTEPromptMs;
if (last == 0)
return false;

uint32_t now = uint32_t(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());

return now - last < 250;
}

// Chip still disguise the right trigger as X for boost (their boost has
// no action-layer entry we can drive).
bool IsBossGuestXRemapSuspended()
{
bool qteOnScreen = IsQTEPromptOnScreen();
bool rtHeld = uint8_t(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7) >= BOOST_TRIGGER_THRESHOLD;

if (!qteOnScreen)
qteRemapReleased = false;
else if (!rtHeld)
qteRemapReleased = true;

return qteOnScreen && qteRemapReleased;
}

void ApplyChipControls()
{
auto& pad = state;

bool lbHeld = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) != 0;
bool rbHeld = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) != 0;
bool ltPulled = uint8_t(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) >> 7) >= 30;
bool rtPulled = uint8_t(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7) >= 30;

// Attack: the bumpers drive the guest triggers (vanilla LT & RT punches).
pad.bLeftTrigger = lbHeld ? 255 : 0;
pad.bRightTrigger = rbHeld ? 255 : 0;

// Guard: LT drives the guest left bumper (vanilla LB guard).
if (ltPulled)
pad.wButtons |= XAMINPUT_GAMEPAD_LEFT_SHOULDER;
else
pad.wButtons &= ~XAMINPUT_GAMEPAD_LEFT_SHOULDER;

// The physical right bumper is attack now; hide it from the guest.
pad.wButtons &= ~XAMINPUT_GAMEPAD_RIGHT_SHOULDER;
}

void ApplyRightTriggerBoost()
{
auto& pad = state;

bool boostOnRT = IsBoostOnRightTriggerActive();

if (!boostOnRT)
{
App::s_rtBoost.store(false, std::memory_order_relaxed);
return;
}

bool rtPulled = uint8_t(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7) >= BOOST_TRIGGER_THRESHOLD;

// Sonic & Super Sonic supports a more "native" way of pressing the right trigger for boosting
// while playing as the Gaia Colossus (Chip) still needs the guests inputs switched.
if (!DetermineChipPlayerStatus())
{
App::s_rtBoost.store(rtPulled, std::memory_order_relaxed);

if (rtPulled)
pad.bRightTrigger = 0;
}
else
{
App::s_rtBoost.store(false, std::memory_order_relaxed);

if (!IsBossGuestXRemapSuspended())
{
bool xHeldPhysically = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X) != 0;
bool xRisingEdge = xHeldPhysically && !xWasHeldLastPoll;

if (xRisingEdge && rtPulled)
xCancelUntilTick = SDL_GetTicks() + GetBoostCancelDurationMs();

bool inCancelWindow = SDL_TICKS_PASSED(xCancelUntilTick, SDL_GetTicks());

if (inCancelWindow)
pad.wButtons &= ~XAMINPUT_GAMEPAD_X;
else if (xHeldPhysically || rtPulled)
pad.wButtons |= XAMINPUT_GAMEPAD_X;
else
pad.wButtons &= ~XAMINPUT_GAMEPAD_X;

if (rtPulled)
pad.bRightTrigger = 0;

xWasHeldLastPoll = xHeldPhysically;

ApplyChipControls();
}
}
}


public:
SDL_GameController* controller{};
SDL_Joystick* joystick{};
Expand All @@ -20,6 +180,15 @@ class Controller
XAMINPUT_VIBRATION vibration{ 0, 0 };
int index{};

// For when user sets Config::RightTriggerAction to ERightTriggerAction this
// increases stability to allow square/X to be recognised by the game while
// right trigger is being pressed down especially when the user is moving
// the thumbsticks.
bool xWasHeldLastPoll{};
uint32_t xCancelUntilTick{};
bool qteRemapReleased{}; // Handles the remap suspension. Resets when no prompt is on screen.
// bool chipIsActive{};

Controller() = default;

explicit Controller(int index) : Controller(SDL_GameControllerOpen(index))
Expand Down Expand Up @@ -99,6 +268,9 @@ class Controller

pad.bLeftTrigger = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) >> 7;
pad.bRightTrigger = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7;


ApplyRightTriggerBoost();
}

void Poll()
Expand Down Expand Up @@ -129,6 +301,9 @@ class Controller
pad.wButtons |= TRANSLATE_INPUT(SDL_CONTROLLER_BUTTON_B, XAMINPUT_GAMEPAD_B);
pad.wButtons |= TRANSLATE_INPUT(SDL_CONTROLLER_BUTTON_X, XAMINPUT_GAMEPAD_X);
pad.wButtons |= TRANSLATE_INPUT(SDL_CONTROLLER_BUTTON_Y, XAMINPUT_GAMEPAD_Y);


ApplyRightTriggerBoost();
}

void SetVibration(const XAMINPUT_VIBRATION& vibration)
Expand Down
60 changes: 60 additions & 0 deletions UnleashedRecomp/locale/config_locale.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,66 @@ CONFIG_DEFINE_ENUM_LOCALE(ETimeOfDayTransition)
}
};

// Translation required
// Japanese Notes: This localization should include furigana in its description.
CONFIG_DEFINE_LOCALE(RightTriggerAction)
{
{ ELanguage::English, { "Right Trigger Action", "Choose what the right trigger does in day stages." } },
{ ELanguage::Japanese, { "Right Trigger Action", "Choose what the right trigger does in day stages." } }, // Translation required
{ ELanguage::German, { "Right Trigger Action", "Choose what the right trigger does in day stages." } }, // Translation required
{ ELanguage::French, { "Right Trigger Action", "Choose what the right trigger does in day stages." } }, // Translation required
{ ELanguage::Spanish, { "Right Trigger Action", "Choose what the right trigger does in day stages." } }, // Translation required
{ ELanguage::Italian, { "Right Trigger Action", "Choose what the right trigger does in day stages." } } // Translation required
};

// Translation required
// Japanese Notes: This localization should include furigana in its description.
CONFIG_DEFINE_ENUM_LOCALE(ERightTriggerAction)
{
{
ELanguage::English,
{
{ ERightTriggerAction::Drift, { "DRIFT", "Default: the right trigger acts as drift, matching the original Xbox 360/PS3 controls." } },
{ ERightTriggerAction::Boost, { "BOOST", "The right trigger becomes the boost button. Drift stays on the left trigger, and X/Square still boosts too." } }
}
},
{
ELanguage::Japanese,
{
{ ERightTriggerAction::Drift, { "DRIFT", "" } },
{ ERightTriggerAction::Boost, { "BOOST", "" } }
}
},
{
ELanguage::German,
{
{ ERightTriggerAction::Drift, { "DRIFT", "" } },
{ ERightTriggerAction::Boost, { "BOOST", "" } }
}
},
{
ELanguage::French,
{
{ ERightTriggerAction::Drift, { "DRIFT", "" } },
{ ERightTriggerAction::Boost, { "BOOST", "" } }
}
},
{
ELanguage::Spanish,
{
{ ERightTriggerAction::Drift, { "DRIFT", "" } },
{ ERightTriggerAction::Boost, { "BOOST", "" } }
}
},
{
ELanguage::Italian,
{
{ ERightTriggerAction::Drift, { "DRIFT", "" } },
{ ERightTriggerAction::Boost, { "BOOST", "" } }
}
}
};

// Japanese Notes: This localization should include furigana.
CONFIG_DEFINE_LOCALE(ControllerIcons)
{
Expand Down
Loading