diff --git a/UnleashedRecomp/app.cpp b/UnleashedRecomp/app.cpp index b298ebc3f..fb6f0b906 100644 --- a/UnleashedRecomp/app.cpp +++ b/UnleashedRecomp/app.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -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 diff --git a/UnleashedRecomp/app.h b/UnleashedRecomp/app.h index 9714d1085..a738d73fd 100644 --- a/UnleashedRecomp/app.h +++ b/UnleashedRecomp/app.h @@ -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 s_rtBoost; + + // 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 762355242..d5145160b 100644 --- a/UnleashedRecomp/hid/driver/sdl_hid.cpp +++ b/UnleashedRecomp/hid/driver/sdl_hid.cpp @@ -5,13 +5,173 @@ #include #include #include +#include #include #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(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; + } + + // 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{}; @@ -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)) @@ -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() @@ -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) diff --git a/UnleashedRecomp/locale/config_locale.cpp b/UnleashedRecomp/locale/config_locale.cpp index 1242e4eb6..8cf3385c8 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", "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) { diff --git a/UnleashedRecomp/patches/misc_patches.cpp b/UnleashedRecomp/patches/misc_patches.cpp index ec4e1a8be..660a824ec 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/patches/player_patches.cpp b/UnleashedRecomp/patches/player_patches.cpp index cce0a5852..c95fa9497 100644 --- a/UnleashedRecomp/patches/player_patches.cpp +++ b/UnleashedRecomp/patches/player_patches.cpp @@ -1,3 +1,4 @@ +#include "player_patches.h" #include #include #include @@ -10,6 +11,17 @@ static uint32_t g_lastTrickScore; static float g_lastDarkGaiaEnergy; static bool g_isUnleashCancelled; +// RT's rising edge, recomputed once per frame in PlayerPatches::Update. +static bool g_rtBoostPrev = false; +static bool g_rtBoostRisingEdge = false; + +// RT boost - the surgical hook (Config::RightTriggerAction == Boost) +static thread_local bool g_inBoostEntry = false; // sub_823787B0 (tapped -> start) +static thread_local bool g_inBoostUpdate = false; // sub_8239E3B8 (held -> sustain) +static thread_local bool g_inDiveDecision = false; // sub_82391898 / sub_82391408 (held -> dive) +static thread_local bool g_inSuperBoostEntry = false; // sub_82451F10 (Super Sonic tapped -> start) +static thread_local bool g_inSuperBoostUpdate = false; // sub_82452518 (Super Sonic held -> sustain) + /* Hook function for when checkpoints are activated to preserve the current checkpoint score. */ PPC_FUNC_IMPL(__imp__sub_82624308); @@ -116,3 +128,146 @@ PPC_FUNC(sub_823B4590) SDL_User_EvilSonic(false); } + +// "Native" Right Trigger Boost Support + +static bool RTBoostActive() +{ + bool userSelectedBoost = Config::RightTriggerAction == ERightTriggerAction::Boost; + return userSelectedBoost && App::s_rtBoost.load(std::memory_order_relaxed); +} + +PPC_FUNC_IMPL(__imp__sub_823787B0); +PPC_FUNC(sub_823787B0) +{ + g_inBoostEntry = true; + __imp__sub_823787B0(ctx, base); + g_inBoostEntry = false; +} + +// Air action decision (tapped-gated, then airboost/homing). +PPC_FUNC_IMPL(__imp__sub_82375748); +PPC_FUNC(sub_82375748) +{ + g_inBoostEntry = RTBoostActive(); + __imp__sub_82375748(ctx, base); + g_inBoostEntry = false; +} + +// Like the XButtonHoming flag, pressing right trigger in the air will always air boost +// (leaving homing attack button preference in tact). +PPC_FUNC_IMPL(__imp__sub_8245DB18); +PPC_FUNC(sub_8245DB18) +{ + uint32_t id = ctx.r4.u32; + __imp__sub_8245DB18(ctx, base); + if (id == 294 && g_inBoostEntry && g_rtBoostRisingEdge) + ctx.r3.u64 = 0; // 0 -> air boost, 1 -> homing attack. +} + +PPC_FUNC_IMPL(__imp__sub_8239E3B8); +PPC_FUNC(sub_8239E3B8) +{ + g_inBoostUpdate = true; + __imp__sub_8239E3B8(ctx, base); + g_inBoostUpdate = false; +} + +// Tapped: drive boost START from RT. Scoped to the boost entry decisions - +// day Sonic's sub_823787B0 (any id) and Super Sonic's flight update sub_82451F10 (which +// reads isTapped(action 4) to transition into the boost). +PPC_FUNC_IMPL(__imp__sub_82308360); +PPC_FUNC(sub_82308360) +{ + uint32_t id = ctx.r4.u32; + __imp__sub_82308360(ctx, base); + + bool userConfigIsBoost = Config::RightTriggerAction == ERightTriggerAction::Boost; + + if (g_rtBoostRisingEdge && userConfigIsBoost && (g_inBoostEntry || (g_inSuperBoostEntry && id == 4))) + { + ctx.r3.u64 = 1; + } +} + +// Held: keep boost alive from RT (boost state update), and drive the +// falling-section DIVE from RT. The X button path is unchanged +PPC_FUNC_IMPL(__imp__sub_82308378); +PPC_FUNC(sub_82308378) +{ + uint32_t id = ctx.r4.u32; + __imp__sub_82308378(ctx, base); + if (RTBoostActive() && (g_inBoostUpdate + || (g_inDiveDecision && id == 0x0B) + || (g_inSuperBoostUpdate && id == 4))) + { + ctx.r3.u64 = 1; + } +} + +// Falling-section dive: glide state DivingFloat::Update Scope RT into both +// so holding RT dives; the game shows the boost=RT prompt in these sections, +// so RT diving matches the prompt +PPC_FUNC_IMPL(__imp__sub_82391898); +PPC_FUNC(sub_82391898) +{ + g_inDiveDecision = true; + __imp__sub_82391898(ctx, base); + g_inDiveDecision = false; +} + +PPC_FUNC_IMPL(__imp__sub_82391408); +PPC_FUNC(sub_82391408) +{ + g_inDiveDecision = true; + __imp__sub_82391408(ctx, base); + g_inDiveDecision = false; +} + +/* --- super Sonic (final boss) boost --------------------------------------------------- + the final boss runs Super Sonic through two flight modes, each its own state machine + with a "tapped -> start" flight decision and a "held -> sustain" boost update, both + reading boost-button action 4, so RT starts and sustains Super Sonic's + boost in both phases, exactly like day Sonic */ +PPC_FUNC_IMPL(__imp__sub_82451F10); // Space Harrier: flight -> boost (tapped) +PPC_FUNC(sub_82451F10) +{ + g_inSuperBoostEntry = true; + __imp__sub_82451F10(ctx, base); + g_inSuperBoostEntry = false; +} + +PPC_FUNC_IMPL(__imp__sub_82452518); // Space Harrier: boost sustain (held) +PPC_FUNC(sub_82452518) +{ + g_inSuperBoostUpdate = true; + __imp__sub_82452518(ctx, base); + g_inSuperBoostUpdate = false; +} + +PPC_FUNC_IMPL(__imp__sub_82451038); // NiGHTS: flight -> boost (tapped) +PPC_FUNC(sub_82451038) +{ + g_inSuperBoostEntry = true; + __imp__sub_82451038(ctx, base); + g_inSuperBoostEntry = false; +} + +PPC_FUNC_IMPL(__imp__sub_824514D8); // NiGHTS: boost sustain (held) +PPC_FUNC(sub_824514D8) +{ + g_inSuperBoostUpdate = true; + __imp__sub_824514D8(ctx, base); + g_inSuperBoostUpdate = false; +} + +void PlayerPatches::Update() +{ + // RT rising edge for the boost-entry override (see the sub_82308360 hook): true + // only on the frame RT crosses into "held", so boost fires once per press. + { + bool rt = RTBoostActive(); + g_rtBoostRisingEdge = rt && !g_rtBoostPrev; + g_rtBoostPrev = rt; + } +} diff --git a/UnleashedRecomp/patches/player_patches.h b/UnleashedRecomp/patches/player_patches.h new file mode 100644 index 000000000..21819ba33 --- /dev/null +++ b/UnleashedRecomp/patches/player_patches.h @@ -0,0 +1,8 @@ +#pragma once + +class PlayerPatches +{ +public: + // Called once per frame from the App::Update hook. + static void Update(); +}; diff --git a/UnleashedRecomp/ui/options_menu.cpp b/UnleashedRecomp/ui/options_menu.cpp index 33ac5c696..d5a2afa3e 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/UnleashedRecomp/user/config.cpp b/UnleashedRecomp/user/config.cpp index bd622b7f8..1de78e327 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 9e87d12e2..ee106d0a3 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 80eb29c93..96e909c5c 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); diff --git a/UnleashedRecompLib/config/SWA.toml b/UnleashedRecompLib/config/SWA.toml index 5063e7123..5f20b468a 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"