From 26ee1fa3b68038f09aa4a802e9ba926d6fc1ff27 Mon Sep 17 00:00:00 2001 From: bluechilliz3 <212103429+bluechilliz3@users.noreply.github.com> Date: Mon, 11 May 2026 00:21:12 +0100 Subject: [PATCH 1/3] implemented the option to set right trigger to be the boost button which matches the modern boost sonic games like sonic frontiers and shadow generations. The entry in the config.toml file will be an enum with options for "drift" or "boost" under "RightTriggerAction" the benifits rather than the user using controller remapping software is this ONLY effects the day stages so the trigger will function normally when the using plays the night stages. The option especially works well in Eggmanland. The right trigger seemlessly de/activates when switching between sonics that stage --- UnleashedRecomp/hid/driver/sdl_hid.cpp | 74 ++++++++++++++++++++++++ UnleashedRecomp/locale/config_locale.cpp | 60 +++++++++++++++++++ UnleashedRecomp/user/config.cpp | 6 ++ UnleashedRecomp/user/config.h | 6 ++ UnleashedRecomp/user/config_def.h | 1 + 5 files changed, 147 insertions(+) diff --git a/UnleashedRecomp/hid/driver/sdl_hid.cpp b/UnleashedRecomp/hid/driver/sdl_hid.cpp index 76235524..76489cb3 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -10,6 +10,18 @@ #define TRANSLATE_INPUT(S, X) SDL_GameControllerGetButton(controller, S) << FirstBitLow(X) #define VIBRATION_TIMEOUT_MS 5000 +static bool IsBoostOnRightTriggerActive() +{ + // true if sonic is werehog and right trigger preference is boost + return !App::s_isWerehog && Config::RightTriggerAction == ERightTriggerAction::Boost; +} + +static uint32_t GetBoostCancelDurationMs() +{ + int32_t fps = Config::FPS > 0 ? Config::FPS : 60; + return static_cast(2000 / fps); +} + class Controller { public: @@ -20,6 +32,13 @@ 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{}; + Controller() = default; explicit Controller(int index) : Controller(SDL_GameControllerOpen(index)) @@ -99,6 +118,33 @@ class Controller pad.bLeftTrigger = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) >> 7; pad.bRightTrigger = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7; + + + + if (IsBoostOnRightTriggerActive()) + { + bool xHeldPhysically = SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X) != 0; + bool xRisingEdge = xHeldPhysically && !xWasHeldLastPoll; + bool rtPulled = pad.bRightTrigger >= 30; // TODO: change this to a pressure preference + + // these checks are in place to improve responsiveness of square/X while right trigger is held down + 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; + } } void Poll() @@ -129,6 +175,34 @@ 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); + + // when playing day stages keep the right trigger mirrored onto square/X + // so so the game knows the user is boosting. This will remove the actual + // right trigger from the game so sonic wouldn't drift + if (IsBoostOnRightTriggerActive()) + { + bool xHeldPhysically = (pad.wButtons & XAMINPUT_GAMEPAD_X) != 0; + bool xRisingEdge = xHeldPhysically && !xWasHeldLastPoll; + uint8_t rtRaw = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7; + bool rtPulled = rtRaw >= 30; // TODO: change this to a pressure preference + + // like in Poll() these checks are in place to improve responsiveness + // of square/X while right trigger is held down + if (xRisingEdge && rtPulled) + xCancelUntilTick = SDL_GetTicks() + GetBoostCancelDurationMs(); + + bool inCancelWindow = SDL_TICKS_PASSED(xCancelUntilTick, SDL_GetTicks()); + + if (inCancelWindow) + pad.wButtons &= ~XAMINPUT_GAMEPAD_X; + else if (rtPulled) + pad.wButtons |= XAMINPUT_GAMEPAD_X; + + if (rtPulled) + pad.bRightTrigger = 0; + + xWasHeldLastPoll = xHeldPhysically; + } } void SetVibration(const XAMINPUT_VIBRATION& vibration) diff --git a/UnleashedRecomp/locale/config_locale.cpp b/UnleashedRecomp/locale/config_locale.cpp index 1242e4eb..2e03be74 100644 --- a/UnleashedRecomp/locale/config_locale.cpp +++ b/UnleashedRecomp/locale/config_locale.cpp @@ -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/H.A", "EXPERIMENTAL: The right trigger acts as boost or homing attack. Drift is still available on the left trigger and X/Square still triggers boost 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) { diff --git a/UnleashedRecomp/user/config.cpp b/UnleashedRecomp/user/config.cpp index bd622b7f..1de78e32 100644 --- a/UnleashedRecomp/user/config.cpp +++ b/UnleashedRecomp/user/config.cpp @@ -30,6 +30,12 @@ CONFIG_DEFINE_ENUM_TEMPLATE(ECameraRotationMode) { "Reverse", ECameraRotationMode::Reverse }, }; +CONFIG_DEFINE_ENUM_TEMPLATE(ERightTriggerAction) +{ + { "Drift", ERightTriggerAction::Drift }, + { "Boost", ERightTriggerAction::Boost } +}; + CONFIG_DEFINE_ENUM_TEMPLATE(EControllerIcons) { { "Auto", EControllerIcons::Auto }, diff --git a/UnleashedRecomp/user/config.h b/UnleashedRecomp/user/config.h index 9e87d12e..ee106d0a 100644 --- a/UnleashedRecomp/user/config.h +++ b/UnleashedRecomp/user/config.h @@ -52,6 +52,12 @@ enum class ECameraRotationMode : uint32_t Reverse }; +enum class ERightTriggerAction : uint32_t +{ + Drift, + Boost +}; + enum class EControllerIcons : uint32_t { Auto, diff --git a/UnleashedRecomp/user/config_def.h b/UnleashedRecomp/user/config_def.h index 80eb29c9..96e909c5 100644 --- a/UnleashedRecomp/user/config_def.h +++ b/UnleashedRecomp/user/config_def.h @@ -12,6 +12,7 @@ CONFIG_DEFINE("System", bool, ShowConsole, false); CONFIG_DEFINE_ENUM_LOCALISED("Input", ECameraRotationMode, HorizontalCamera, ECameraRotationMode::Normal); CONFIG_DEFINE_ENUM_LOCALISED("Input", ECameraRotationMode, VerticalCamera, ECameraRotationMode::Normal); +CONFIG_DEFINE_ENUM_LOCALISED("Input", ERightTriggerAction, RightTriggerAction, ERightTriggerAction::Drift); CONFIG_DEFINE_LOCALISED("Input", bool, Vibration, true); CONFIG_DEFINE_LOCALISED("Input", bool, AllowBackgroundInput, false); CONFIG_DEFINE_ENUM_LOCALISED("Input", EControllerIcons, ControllerIcons, EControllerIcons::Auto); From 01a14d2af2f46756372661be7788965f0e643260 Mon Sep 17 00:00:00 2001 From: bluechilliz3 <212103429+bluechilliz3@users.noreply.github.com> Date: Sat, 20 Jun 2026 10:59:05 +0100 Subject: [PATCH 2/3] chip's controls will now use the original controls regardless if Config::RightTriggerAction == ERightTriggerAction::Boost. This is a conscious decision since the HUD will tell you to press the wrong button if right trigger is set to boost. Super sonic will remain matching day sonic's controls. Pressing right trigger won't also move the sun while right trigger set to boost anymore thanks to `hasStage`. --- UnleashedRecomp/hid/driver/sdl_hid.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/UnleashedRecomp/hid/driver/sdl_hid.cpp b/UnleashedRecomp/hid/driver/sdl_hid.cpp index 76489cb3..bb35fba3 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -12,8 +12,26 @@ static bool IsBoostOnRightTriggerActive() { - // true if sonic is werehog and right trigger preference is boost - return !App::s_isWerehog && Config::RightTriggerAction == ERightTriggerAction::Boost; + const bool userConfigIsBoost = Config::RightTriggerAction == ERightTriggerAction::Boost; + + if (!userConfigIsBoost || App::s_isWerehog) + return false; + + auto gameDocInstance = SWA::CGameDocument::GetInstance(); + + if (gameDocInstance == NULL) + return false + + const char* stageName = gameDocInstance->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; } static uint32_t GetBoostCancelDurationMs() From 3608e7237cb6852d0a6ad92401cd9e8d02aa3740 Mon Sep 17 00:00:00 2001 From: bluechilliz3 <212103429+bluechilliz3@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:30:01 +0100 Subject: [PATCH 3/3] button prompt is updated to reflect `Config::RightTriggerAction` option. this also works on skin mods set by the user. since the HUD for chip's section of the final boss too the remapping is restored when playing him for adding `ChipOverlayCsdPatchMidAsmHook` it rewrites the Gaia Colossus (Chip) control overlay icons when boost is remapped to the right trigger. 0x830C17A8 is the CSD project parser entry (r8 = raw yncp file bytes, checked against FAPC/CPAF magic at 0x830C17C8 before any offset fixups); the hook identifies ui_playscreen_su.yncp by its footer cast name table and retargets each button cast's sprite index, pixel box and quad (X->RT, LB->LT. In the function No code picks these icons, so when boost is remapped to the right trigger we rewrite the asset as it loads: this hook fires at the CSD project parser entry (r8 = raw file bytes, before offset fixups, so file offsets apply directly), identifies this exact file by its footer cast name table, then retargets each button cast's sprite index, pixel box and quad to show and lay out the remapped buttons (X->RT boost, LB->LT guard, LT->LB / RT->RB attack).No code picks these icons, so when boost is remapped to the right trigger we rewrite the asset as it loads: this hook fires at the CSD project parser entry (r8 = raw file bytes, before offset fixups, so file offsets apply directly), identifies this exact file by its footer cast name table, then retargets each button cast's sprite index, pixel box and quad to show and lay out the remapped buttons (X->RT boost, LB->LT guard, LT->LB / RT->RB attack). for adding `QTEPromptActiveMidAsmHook` per-frame update of the trick QTE prompt bar (0x823329F8, r3 = widget) using the new `App::s_lastQTEPromptMs` in `app.h`. The hook reads the widget's button sequence (this+120/124, position this+152) and stamps a timestamp while the currently expected button is X/Square, which the input driver uses to suspend the boost trigger remaps for just those prompts for adding `BoostPromptCastMidAsmHook` it swaps the day-stage boost nav sign prompt (sub_8239F4A8) from X to RT when boost is remapped to the right trigger. 0x8239F510 = "addi r5,r10,-8140" materializes r5 = 0x8200E034 "btn_1"; hook the next instruction (0x8239F514) so r5 is still the cast name before GetCast at 0x8239F51C consumes it for adding `BoostPromptPatternMidAsmHook` 0x8239F5B8 = "li r4,2" (2 = X icon); hook the SetPatternIndex call at 0x8239F5BC where r4 is the pattern index passed to sub_830BF300 (btn_3 patterns: 0=LT, 1=RT) also added the Config::RightTriggerAction option in the ui --- UnleashedRecomp/app.h | 4 + UnleashedRecomp/hid/driver/sdl_hid.cpp | 121 ++++++++++++++++++----- UnleashedRecomp/patches/misc_patches.cpp | 110 +++++++++++++++++++++ UnleashedRecomp/ui/options_menu.cpp | 1 + UnleashedRecompLib/config/SWA.toml | 28 +++++- 5 files changed, 235 insertions(+), 29 deletions(-) diff --git a/UnleashedRecomp/app.h b/UnleashedRecomp/app.h index 9714d108..e774312a 100644 --- a/UnleashedRecomp/app.h +++ b/UnleashedRecomp/app.h @@ -17,6 +17,10 @@ class App static inline double s_deltaTime; static inline double s_time = 0.0; // How much time elapsed since the game started. + // Steady-clock milliseconds of the most recent QTE prompt window update, stamped + // by QTEPromptActiveMidAsmHook. + static inline std::atomic s_lastQTEPromptMs; + static void Restart(std::vector restartArgs = {}); static void Exit(); }; diff --git a/UnleashedRecomp/hid/driver/sdl_hid.cpp b/UnleashedRecomp/hid/driver/sdl_hid.cpp index bb35fba3..796b7e74 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -5,43 +5,102 @@ #include #include #include +#include #include #define TRANSLATE_INPUT(S, X) SDL_GameControllerGetButton(controller, S) << FirstBitLow(X) #define VIBRATION_TIMEOUT_MS 5000 -static bool IsBoostOnRightTriggerActive() +class Controller { - const bool userConfigIsBoost = Config::RightTriggerAction == ERightTriggerAction::Boost; +private: + bool IsBoostOnRightTriggerActive() + { + const bool userConfigIsBoost = Config::RightTriggerAction == ERightTriggerAction::Boost; - if (!userConfigIsBoost || App::s_isWerehog) - return false; + if (!userConfigIsBoost || App::s_isWerehog) + return false; - auto gameDocInstance = SWA::CGameDocument::GetInstance(); - - if (gameDocInstance == NULL) - 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; - const char* stageName = gameDocInstance->m_pMember->m_StageName.c_str(); - const bool hasStage = stageName && strlen(stageName); + if (qteOnScreen && qteRemapReleased) + return false; - if (hasStage == false) - return false - - // there's no "BossDarkGaia1_2Air" so regex is NOT needeed - const bool playingAsChip = !strcmp(stageName, "BossDarkGaia1_1Air"); - - return !playingAsChip; -} + 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(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::steady_clock::now().time_since_epoch()).count()); + + return now - last < 250; + } + + 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; + } -static uint32_t GetBoostCancelDurationMs() -{ - int32_t fps = Config::FPS > 0 ? Config::FPS : 60; - return static_cast(2000 / fps); -} -class Controller -{ public: SDL_GameController* controller{}; SDL_Joystick* joystick{}; @@ -50,12 +109,14 @@ class Controller XAMINPUT_VIBRATION vibration{ 0, 0 }; int index{}; - // for when user sets Config::RightTriggerAction to ERightTriggerAction this + // 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 + // the thumbsticks. bool xWasHeldLastPoll{}; uint32_t xCancelUntilTick{}; + bool qteRemapReleased{}; // Handles the remap suspension. Resets when no prompt is on screen. + // bool chipIsActive{}; Controller() = default; @@ -162,6 +223,9 @@ class Controller pad.bRightTrigger = 0; xWasHeldLastPoll = xHeldPhysically; + + if (DetermineChipPlayerStatus()) + ApplyChipControls(); } } @@ -220,6 +284,9 @@ class Controller pad.bRightTrigger = 0; xWasHeldLastPoll = xHeldPhysically; + + if (DetermineChipPlayerStatus()) + ApplyChipControls(); } } diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index ec4e1a8b..660a824e 100644 --- a/UnleashedRecomp/patches/misc_patches.cpp +++ b/UnleashedRecomp/patches/misc_patches.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -39,6 +40,115 @@ bool DisableEvilControlTutorialMidAsmHook(PPCRegister& r4, PPCRegister& r5) return r4.u32 == 1 && r5.u32 == 1; } +// The day-stage boost navigation sign (sub_8239F4A8) hardcodes cast "btn_1" with img +// pattern 2 (X button). When boost is remapped to the right trigger, redirect the +// prompt to the trigger cast so the HUD matches the actual control. The play-screen +// scene's button casts map as: btn_1 = face buttons (0=A 1=B 2=X 3=Y), btn_2 = bumpers +// (0=LB 1=RB), btn_3 = triggers (0=LT 1=RT). +void BoostPromptCastMidAsmHook(PPCRegister& r5) +{ + if (Config::RightTriggerAction == ERightTriggerAction::Boost) + r5.u32 = 0x820116D8; // "btn_3" +} + +void BoostPromptPatternMidAsmHook(PPCRegister& r4) +{ + if (Config::RightTriggerAction == ERightTriggerAction::Boost) + r4.u32 = 1; // RT +} + +// The Gaia Colossus (Chip) control overlay ("Boost / Guard / Attack") lives in the +// "footer" scene of ui_playscreen_su.yncp (a common archive, cached from boot), with +// the button art baked into the asset as sprite crops of the shared x360 button atlas +// (crop order A, B, X, Y, LB, RB, LT, RT; bumpers are double width). +void ChipOverlayCsdPatchMidAsmHook(PPCRegister& r8) +{ + auto base = (uint8_t*)g_memory.Translate(r8.u32); + + // Identify ui_playscreen_su.yncp first: container size in the FAPC header, then + // the footer cast name table. Everything else returns before touching config. + if (*(be*)(base + 4) != 0x4970) + return; + + static constexpr char s_footerCastNames[] = "btn_lb\0\0btn_lt\0\0btn_rt\0\0btn_x"; + if (memcmp(base + 0xC08, s_footerCastNames, sizeof(s_footerCastNames) - 1) != 0) + return; + + if (Config::RightTriggerAction != ERightTriggerAction::Boost) + return; + + // Layout constants, hand-tuned in-game (widths/heights are quad half-extents in + // scene units; X/Y are cast translations). AttackL* is btn_lt's offset relative + // to its parent btn_rt (the pair's spacing); the others are absolute. + struct QuadLayout { float w, h, x, y; }; + constexpr QuadLayout kBoost = { 0.0308f, 0.0556f, -0.1289f, -0.103f }; + constexpr QuadLayout kGuard = { 0.031f, 0.0502f, -0.1369f, -0.0545f }; + constexpr QuadLayout kAttackR = { 0.0625f, 0.05556f, -0.1064f, -0.011f }; + constexpr float kAttackLX = -0.055f; // btn_lt X relative to btn_rt + constexpr float kAttackLY = 0.0f; // btn_lt Y relative to btn_rt + + // The footer's button casts (records at 0x1C30 btn_x, 0x1CA4 btn_lb, 0x1D18 + // btn_lt, 0x1D8C btn_rt) each show their icon via a sprite-index array + // ([0, 0, crop], live entry at +8) into the scene crop table (entry 0 at file + // 0x734, 20-byte records): 13 = X, 15 = LB (double width), 16 = RB, 17 = LT, + // 18 = RT. Redirect the indices instead of rewriting the shared crop table. + *(be*)(base + 0x3570 + 8) = 18; // Boost row: btn_x X -> RT + *(be*)(base + 0x362C + 8) = 17; // Guard row: btn_lb LB -> LT + *(be*)(base + 0x36E8 + 8) = 15; // Attack row: btn_lt LT -> LB + *(be*)(base + 0x37A4 + 8) = 16; // btn_rt RT -> RB + + // Position each cast via its quad corners (8 floats at +0x14: TL BL TR BR as x,y + // pairs, right-edge / vertical-centre anchored) and its info-block translation + // (+0x0C x, +0x10 y). btn_lt is a CHILD of btn_rt, so its translation is relative. + auto setQuad = [&](uint32_t rec, const QuadLayout& q) + { + *(be*)(base + rec + 0x14) = -q.w; // TL.x + *(be*)(base + rec + 0x1C) = -q.w; // BL.x + *(be*)(base + rec + 0x18) = -q.h * 0.5f; // TL.y + *(be*)(base + rec + 0x28) = -q.h * 0.5f; // TR.y + *(be*)(base + rec + 0x20) = q.h * 0.5f; // BL.y + *(be*)(base + rec + 0x30) = q.h * 0.5f; // BR.y + }; + + QuadLayout attackL = { kAttackR.w, kAttackR.h, kAttackLX, kAttackLY }; + setQuad(0x1C30, kBoost); // btn_x + setQuad(0x1CA4, kGuard); // btn_lb + setQuad(0x1D18, attackL); // btn_lt + setQuad(0x1D8C, kAttackR); // btn_rt + + *(be*)(base + 0x35F0 + 0x0C) = kBoost.x; + *(be*)(base + 0x35F0 + 0x10) = kBoost.y; + *(be*)(base + 0x36AC + 0x0C) = kGuard.x; + *(be*)(base + 0x36AC + 0x10) = kGuard.y; + *(be*)(base + 0x3768 + 0x0C) = attackL.x; + *(be*)(base + 0x3768 + 0x10) = attackL.y; + *(be*)(base + 0x3824 + 0x0C) = kAttackR.x; + *(be*)(base + 0x3824 + 0x10) = kAttackR.y; + + // The rendered shape comes from each cast's baked pixel box (u32 width at +0x58, + // height at +0x5C) — the art is fitted into it, which is what stretched the + // swapped icons. Give each cast the box of the art it now shows: the guard cast + // drops from the 80x40 bumper box to the 40x40 trigger box, the attack casts + // grow from 40x40 to 80x40. + *(be*)(base + 0x1CA4 + 0x58) = 40; // btn_lb: square trigger box + *(be*)(base + 0x1D18 + 0x58) = 80; // btn_lt: wide bumper box + *(be*)(base + 0x1D8C + 0x58) = 80; // btn_rt: wide bumper box +} + +// Stamp the time whenever a prompt is on screen (this+101 = active, this+102 = done). +// The input driver keeps the boost trigger remap alive during a QTE only while the +// trigger is held continuously from before the prompt (so the boost aura persists). +// The QTE is edge-triggered, so a held trigger registers no press and can't answer it. +// Releasing the trigger suspends the remap so the real face button can. +void QTEPromptActiveMidAsmHook(PPCRegister& pThis) +{ + bool isActive = *(uint8_t*)g_memory.Translate(pThis.u32 + 101) != 0; + bool isDone = *(uint8_t*)g_memory.Translate(pThis.u32 + 102) != 0; + if (isActive && !isDone) + App::s_lastQTEPromptMs = uint32_t(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); +} + bool DisableDLCIconMidAsmHook() { return Config::DisableDLCIcon; diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index 33ac5c69..d5a2afa3 100644 --- a/UnleashedRecomp/ui/options_menu.cpp +++ b/UnleashedRecomp/ui/options_menu.cpp @@ -1239,6 +1239,7 @@ static void DrawConfigOptions() case 1: // INPUT DrawConfigOption(rowCount++, yOffset, &Config::HorizontalCamera, true); DrawConfigOption(rowCount++, yOffset, &Config::VerticalCamera, true); + DrawConfigOption(rowCount++, yOffset, &Config::RightTriggerAction, true); DrawConfigOption(rowCount++, yOffset, &Config::Vibration, true); DrawConfigOption(rowCount++, yOffset, &Config::AllowBackgroundInput, true); DrawConfigOption(rowCount++, yOffset, &Config::ControllerIcons, true); diff --git a/UnleashedRecompLib/config/SWA.toml b/UnleashedRecompLib/config/SWA.toml index 5063e712..5f20b468 100644 --- a/UnleashedRecompLib/config/SWA.toml +++ b/UnleashedRecompLib/config/SWA.toml @@ -197,18 +197,42 @@ address = 0x823A4FF0 registers = ["r4", "r5"] return_on_false = true -# Set default value for XButtonHoming. +# Update Chip Control Tutorial Overlay For Right Trigger Boost +[[midasm_hook]] +name = "ChipOverlayCsdPatchMidAsmHook" +address = 0x830C17A8 +registers = ["r8"] + +# Trick QTE prompt bar Per-frame Update +[[midasm_hook]] +name = "QTEPromptActiveMidAsmHook" +address = 0x823329F8 +registers = ["r3"] + +# Set default value for XButtonHoming [[midasm_hook]] name = "SetXButtonHomingMidAsmHook" address = 0x8237AC90 registers = ["r1"] -# Disable XML reading for XButtonHoming. +# Disable XML reading for XButtonHoming [[midasm_hook]] name = "IsHomingAttackOnJump" address = 0x8237ACE4 jump_address_on_true = 0x8237ACE8 +# Change boost prompt cast for right trigger option +[[midasm_hook]] +name = "BoostPromptCastMidAsmHook" +address = 0x8239F514 +registers = ["r5"] + +# Change boost prompt pattern for right trigger option +[[midasm_hook]] +name = "BoostPromptPatternMidAsmHook" +address = 0x8239F5BC +registers = ["r4"] + # Down force HFR fix [[midasm_hook]] name = "DownForceDeltaTimeFixMidAsmHook"