From b150d7b5048309b74d51e959f2a93815a799ada1 Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:51:06 +0300 Subject: [PATCH 1/2] fix: make ped push vehicle physics FPS independent and scale sleep threshold --- .../CMultiplayerSA_FrameRateFixes.cpp | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp b/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp index d07ce38aa2..62baa72ba9 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp @@ -9,6 +9,8 @@ *****************************************************************************/ #include "StdInc.h" +#include +#include static bool bWouldBeNewFrame = false; static unsigned int nLastFrameTime = 0; @@ -818,6 +820,313 @@ static void __declspec(naked) HOOK_CWeapon_Update() // clang-format on } +// Governs pedestrian push velocity on unoccupied vehicles during collisions. +// In GTA:SA, CPhysical::ApplyCollision adds an impulse to velocity on every contact frame. +// At high framerates, these impulses occur far more frequently than at 30 FPS, overpowering +// tire friction and causing the vehicle to accelerate unnaturally fast. +// This caps push velocity using momentum conservation to maintain consistent vehicle weight across framerates. +static void GovernPedPushVehicleVelocity(CPhysicalSAInterface* vehicle, CPhysicalSAInterface* ped, const CVector& initialLinearVelocity, + const CVector& initialAngularVelocity, const CVector& pedLinearVelocity, CVector& currentLinearVelocity, + CVector& currentAngularVelocity) +{ + if (!vehicle || !ped) + return; + + // Only govern collisions on unoccupied vehicles (no driver present) + const auto* vehicleInterface = reinterpret_cast(vehicle); + if (vehicleInterface->pDriver != nullptr) + return; + + // Prevent vehicle tilting or flipping from push contact + currentAngularVelocity.fX = initialAngularVelocity.fX; + currentAngularVelocity.fY = initialAngularVelocity.fY; + + // Keep vertical velocity unaffected by push contact + currentLinearVelocity.fZ = initialLinearVelocity.fZ; + + const CVector deltaVelocity = currentLinearVelocity - initialLinearVelocity; + const float deltaMagnitudeSquared = (deltaVelocity.fX * deltaVelocity.fX) + (deltaVelocity.fY * deltaVelocity.fY); + if (deltaMagnitudeSquared <= 0.000001f) + return; + + const float deltaMagnitude = std::sqrt(deltaMagnitudeSquared); + const float pushDirectionX = deltaVelocity.fX / deltaMagnitude; + const float pushDirectionY = deltaVelocity.fY / deltaMagnitude; + + const float pedForwardSpeed = (pedLinearVelocity.fX * pushDirectionX) + (pedLinearVelocity.fY * pushDirectionY); + if (pedForwardSpeed <= 0.0f) + { + currentLinearVelocity = initialLinearVelocity; + currentAngularVelocity = initialAngularVelocity; + return; + } + + const float timeStep = *reinterpret_cast(0xB7CB5C); + constexpr float baselineTimeStep = 1.0f; + const float timeStepRatio = std::clamp(timeStep / baselineTimeStep, 0.001f, 1.0f); + + constexpr float playerPushMassMultiplier = 10.0f; + const float effectivePedMass = ped->m_fMass * playerPushMassMultiplier; + const float vehicleMass = vehicle->m_fMass; + const float maximumPushVelocity = pedForwardSpeed * (effectivePedMass / (effectivePedMass + vehicleMass)); + + float forwardX = 0.0f; + float forwardY = 1.0f; + float rightX = 1.0f; + float rightY = 0.0f; + + if (vehicle->matrix != nullptr) + { + forwardX = vehicle->matrix->vFront.fX; + forwardY = vehicle->matrix->vFront.fY; + rightX = vehicle->matrix->vRight.fX; + rightY = vehicle->matrix->vRight.fY; + } + else + { + const float heading = vehicle->m_transform.m_heading; + forwardX = -std::sin(heading); + forwardY = std::cos(heading); + rightX = std::cos(heading); + rightY = std::sin(heading); + } + + const float forwardLen = std::sqrt((forwardX * forwardX) + (forwardY * forwardY)); + if (forwardLen > 0.0001f) + { + forwardX /= forwardLen; + forwardY /= forwardLen; + } + + const float rightLen = std::sqrt((rightX * rightX) + (rightY * rightY)); + if (rightLen > 0.0001f) + { + rightX /= rightLen; + rightY /= rightLen; + } + + const float initialForwardSpeed = (initialLinearVelocity.fX * forwardX) + (initialLinearVelocity.fY * forwardY); + const float initialLateralSpeed = (initialLinearVelocity.fX * rightX) + (initialLinearVelocity.fY * rightY); + + float currentForwardSpeed = (currentLinearVelocity.fX * forwardX) + (currentLinearVelocity.fY * forwardY); + float currentLateralSpeed = (currentLinearVelocity.fX * rightX) + (currentLinearVelocity.fY * rightY); + + const float deltaForwardSpeed = currentForwardSpeed - initialForwardSpeed; + currentForwardSpeed = initialForwardSpeed + (deltaForwardSpeed * timeStepRatio); + + constexpr float wakeUpThreshold = 0.008f; + if (std::abs(initialForwardSpeed) < 0.001f && std::abs(currentForwardSpeed) < wakeUpThreshold && std::abs(deltaForwardSpeed) > 0.001f) + { + currentForwardSpeed = (deltaForwardSpeed > 0.0f) ? wakeUpThreshold : -wakeUpThreshold; + } + + const float allowedForwardSpeedMax = std::max(initialForwardSpeed, maximumPushVelocity); + const float allowedForwardSpeedMin = std::min(initialForwardSpeed, -maximumPushVelocity); + currentForwardSpeed = std::clamp(currentForwardSpeed, allowedForwardSpeedMin, allowedForwardSpeedMax); + + const float deltaLateralSpeed = currentLateralSpeed - initialLateralSpeed; + currentLateralSpeed = initialLateralSpeed + (deltaLateralSpeed * timeStepRatio); + + constexpr float lateralScrubRatio = 0.10f; + const float maximumLateralVelocity = maximumPushVelocity * lateralScrubRatio; + const float allowedLateralSpeedMax = std::max(initialLateralSpeed, maximumLateralVelocity); + const float allowedLateralSpeedMin = std::min(initialLateralSpeed, -maximumLateralVelocity); + currentLateralSpeed = std::clamp(currentLateralSpeed, allowedLateralSpeedMin, allowedLateralSpeedMax); + + currentLinearVelocity.fX = (currentForwardSpeed * forwardX) + (currentLateralSpeed * rightX); + currentLinearVelocity.fY = (currentForwardSpeed * forwardY) + (currentLateralSpeed * rightY); + + // Scale collision yaw impulse so rotational torque delivered per second is invariant across framerates + const float deltaAngularZ = currentAngularVelocity.fZ - initialAngularVelocity.fZ; + currentAngularVelocity.fZ = initialAngularVelocity.fZ + (deltaAngularZ * timeStepRatio); + + // Physical angular velocity limit derived from conservation of angular momentum at the vehicle corner + constexpr float cornerLeverArm = 2.2f; + const float vehicleTurnMass = (vehicle->m_fTurnMass > 0.0f) ? vehicle->m_fTurnMass : (vehicleMass * 2.5f); + const float maximumAngularVelocity = + (cornerLeverArm * effectivePedMass * pedForwardSpeed) / (vehicleTurnMass + (effectivePedMass * cornerLeverArm * cornerLeverArm)); + + const float allowedYawSpeedMax = std::max(initialAngularVelocity.fZ, maximumAngularVelocity); + const float allowedYawSpeedMin = std::min(initialAngularVelocity.fZ, -maximumAngularVelocity); + currentAngularVelocity.fZ = std::clamp(currentAngularVelocity.fZ, allowedYawSpeedMin, allowedYawSpeedMax); +} + +#define CALL_CPhysical__ApplyCollision_1 0x54BDB2 +#define CALL_CPhysical__ApplyCollision_2 0x54BF78 +#define CALL_CPhysical__ApplyCollision_3 0x54C23A +#define CALL_CPhysical__ApplyCollision_4 0x54C435 +#define CALL_CPhysical__ApplyCollision_5 0x54D17E +#define CALL_CPhysical__ApplyCollision_6 0x54D27E +#define CALL_CPhysical__ApplyCollision_7 0x54D3FE +#define CALL_CPhysical__ApplyCollision_8 0x54D4D2 +#define CALL_CPhysical__ApplyCollisionAlt_1 0x54C9FA +#define CALL_CPhysical__ApplyCollisionAlt_2 0x54CAC2 + +static bool __fastcall HOOK_CPhysical__ApplyCollision(CPhysicalSAInterface* thisEntity, void* /*edx*/, CEntitySAInterface* collidedEntity, + CColPointSAInterface* colPoint, float* thisDamageIntensity, float* collidedDamageIntensity) +{ + const CVector initialThisLinearVelocity = thisEntity ? thisEntity->m_vecLinearVelocity : CVector{}; + const CVector initialThisAngularVelocity = thisEntity ? thisEntity->m_vecAngularVelocity : CVector{}; + + auto* collidedPhysical = reinterpret_cast(collidedEntity); + CVector initialCollidedLinearVelocity{}; + CVector initialCollidedAngularVelocity{}; + if (collidedPhysical) + { + initialCollidedLinearVelocity = collidedPhysical->m_vecLinearVelocity; + initialCollidedAngularVelocity = collidedPhysical->m_vecAngularVelocity; + } + + using ApplyCollisionFn = bool(__thiscall*)(CPhysicalSAInterface*, CEntitySAInterface*, CColPointSAInterface*, float*, float*); + const auto originalApplyCollision = reinterpret_cast(0x548680); + + const bool result = originalApplyCollision(thisEntity, collidedEntity, colPoint, thisDamageIntensity, collidedDamageIntensity); + + if (result && thisEntity && collidedEntity) + { + // Entity types: 2 = Vehicle, 3 = Ped (from CEntitySAInterface::nType bitfield) + const uint8 thisType = thisEntity->nType; + const uint8 collidedType = collidedEntity->nType; + + if (thisType == 2 && collidedType == 3 && collidedPhysical) + { + GovernPedPushVehicleVelocity(thisEntity, collidedPhysical, initialThisLinearVelocity, initialThisAngularVelocity, initialCollidedLinearVelocity, + thisEntity->m_vecLinearVelocity, thisEntity->m_vecAngularVelocity); + } + else if (thisType == 3 && collidedType == 2 && collidedPhysical) + { + GovernPedPushVehicleVelocity(collidedPhysical, thisEntity, initialCollidedLinearVelocity, initialCollidedAngularVelocity, initialThisLinearVelocity, + collidedPhysical->m_vecLinearVelocity, collidedPhysical->m_vecAngularVelocity); + } + } + + return result; +} + +static bool __fastcall HOOK_CPhysical__ApplyCollisionAlt(CPhysicalSAInterface* thisEntity, void* /*edx*/, CPhysicalSAInterface* collidedEntity, + CColPointSAInterface* colPoint, float* damageIntensity, CVector* outLinearVelocity, + CVector* outAngularVelocity) +{ + const CVector initialLinearVelocity = outLinearVelocity ? *outLinearVelocity : CVector{}; + const CVector initialAngularVelocity = outAngularVelocity ? *outAngularVelocity : CVector{}; + + const CVector collidedLinearVelocity = collidedEntity ? collidedEntity->m_vecLinearVelocity : CVector{}; + + using ApplyCollisionAltFn = bool(__thiscall*)(CPhysicalSAInterface*, CPhysicalSAInterface*, CColPointSAInterface*, float*, CVector*, CVector*); + const auto originalApplyCollisionAlt = reinterpret_cast(0x544D50); + + const bool result = originalApplyCollisionAlt(thisEntity, collidedEntity, colPoint, damageIntensity, outLinearVelocity, outAngularVelocity); + + if (result && thisEntity && collidedEntity && outLinearVelocity && outAngularVelocity) + { + // Entity types: 2 = Vehicle, 3 = Ped + const uint8 thisType = thisEntity->nType; + const uint8 collidedType = collidedEntity->nType; + + if (thisType == 2 && collidedType == 3) + { + GovernPedPushVehicleVelocity(thisEntity, collidedEntity, initialLinearVelocity, initialAngularVelocity, collidedLinearVelocity, *outLinearVelocity, + *outAngularVelocity); + } + } + + return result; +} + +// In GTA:SA (30 FPS, timeStep = 1.66667f), vehicles sleep after 10 still frames (~333 ms). +// At high framerates (e.g. 240 FPS), 10 frames elapse in only ~41 ms, deactivating unoccupied vehicles +// before suspension springs can lift the chassis and causing bottomed-out suspension on spawn. +// Scaling the threshold dynamically by timeStep maintains an invariant settling window (~400 ms). +static uint8 __cdecl CalculateVehicleSleepFrameThreshold() noexcept +{ + const float timeStep = *reinterpret_cast(0xB7CB5C); + if (timeStep <= 0.0001f) + return 10; + + constexpr float baselineNumerator = 20.0f; + const float rawThreshold = baselineNumerator / timeStep; + return static_cast(std::clamp(std::round(rawThreshold), 10.0f, 240.0f)); +} + +// Fixes bottomed-out vehicle suspension when spawning or dropping at high framerates. +// CAutomobile::ProcessControl +#define HOOKPOS_CAutomobile__ProcessControl_SleepThreshold 0x6B1D34 +#define HOOKSIZE_CAutomobile__ProcessControl_SleepThreshold 10 +static const unsigned int RETURN_CAutomobile__ProcessControl_SleepThreshold = 0x6B1D3E; +static void __declspec(naked) HOOK_CAutomobile__ProcessControl_SleepThreshold() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + push ecx + push edx + call CalculateVehicleSleepFrameThreshold + mov bl, al + pop edx + pop ecx + + mov al, dl + cmp al, bl + mov [esi+0xB8], dl + jmp RETURN_CAutomobile__ProcessControl_SleepThreshold + } + // clang-format on +} + +// Fixes bottomed-out bike suspension when spawning or dropping at high framerates. +// CBike::ProcessControl +#define HOOKPOS_CBike__ProcessControl_SleepThreshold 0x6B997C +#define HOOKSIZE_CBike__ProcessControl_SleepThreshold 8 +static const unsigned int RETURN_CBike__ProcessControl_SleepThreshold = 0x6B9984; +static void __declspec(naked) HOOK_CBike__ProcessControl_SleepThreshold() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + push ecx + push edx + call CalculateVehicleSleepFrameThreshold + pop edx + pop ecx + + cmp cl, al + mov [esi+0xB8], cl + jmp RETURN_CBike__ProcessControl_SleepThreshold + } + // clang-format on +} + +// CBike::ProcessControl sleep counter clamp +#define HOOKPOS_CBike__ProcessControl_SleepClamp 0x6B99C5 +#define HOOKSIZE_CBike__ProcessControl_SleepClamp 16 +static const unsigned int RETURN_CBike__ProcessControl_SleepClamp = 0x6B99D5; +static void __declspec(naked) HOOK_CBike__ProcessControl_SleepClamp() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + push ecx + push edx + call CalculateVehicleSleepFrameThreshold + pop edx + pop ecx + + cmp [esi+0xB8], al + jbe clamp_done + mov [esi+0xB8], al + + clamp_done: + jmp RETURN_CBike__ProcessControl_SleepClamp + } + // clang-format on +} + #define HOOKPOS_CPhysical__ApplyAirResistance 0x544D29 #define HOOKSIZE_CPhysical__ApplyAirResistance 5 static const unsigned int RETURN_CPhysical__ApplyAirResistance = 0x544D4D; @@ -954,4 +1263,17 @@ void CMultiplayerSA::InitHooks_FrameRateFixes() EZHookInstall(CTaskSimpleSwim__ProcessSwimmingResistance); EZHookInstall(CWeapon_Update); + EZHookInstall(CAutomobile__ProcessControl_SleepThreshold); + EZHookInstall(CBike__ProcessControl_SleepThreshold); + EZHookInstall(CBike__ProcessControl_SleepClamp); + HookInstallCall(CALL_CPhysical__ApplyCollision_1, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_2, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_3, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_4, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_5, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_6, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_7, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollision_8, (DWORD)HOOK_CPhysical__ApplyCollision); + HookInstallCall(CALL_CPhysical__ApplyCollisionAlt_1, (DWORD)HOOK_CPhysical__ApplyCollisionAlt); + HookInstallCall(CALL_CPhysical__ApplyCollisionAlt_2, (DWORD)HOOK_CPhysical__ApplyCollisionAlt); } From fec0b8d8a059e50f0a62ffb3af8d65ee6cb9b1dd Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:57:05 +0300 Subject: [PATCH 2/2] fix: prevent premature sleep and chassis stretch for spawned vehicles at high FPS Spawned unoccupied vehicles at high framerates (e.g. 240 FPS) were going to sleep with their suspension fully extended/stretched, only settling to normal ride height when bumped by a player. This was caused by two issues in GTA:SA vehicle control: 1. The isVehicleIdle flag (0x6B1AF5 in CAutomobile, 0x6B9850 in CBike) forced instant sleep on frame 1 before gravity could settle the chassis. 2. Stationary parking damping in CAutomobile (0x6B361C) and CBike (0x6BC18F) wiped moveSpeed.z to 0 whenever speed was below 0.0045f. At 240 FPS, one frame of gravity is only 0.00166f (< 0.0045f), so vertical speed was erased on every single frame, preventing the springs from ever compressing. Changes: - NOP'd the isVehicleIdle instant-sleep flag in CAutomobile and CBike. - NOP'd moveSpeed.z zeroing in stationary damping so parking brakes only affect horizontal movement and yaw. - Scaled the 0.0045f stillness threshold by (timeStep / kOriginalTimeStep) across all comparison sites to maintain consistent sensitivity. - Scaled the sleep frame threshold by (kOriginalTimeStep / timeStep) to keep an invariant ~333 ms stillness window. Tested at 240 FPS with newly spawned vehicles; suspension now compresses and settles to normal ride height immediately on spawn without player touch. --- Client/game_sa/CVisibilityPluginsSA.cpp | 6 + Client/mods/deathmatch/logic/CClientGame.cpp | 8 +- Client/multiplayer_sa/CMultiplayerSA.cpp | 4 +- .../CMultiplayerSA_CrashFixHacks.cpp | 152 +++++++++++++++++- .../CMultiplayerSA_FrameRateFixes.cpp | 39 ++++- .../CMultiplayerSA_VehicleCollision.cpp | 35 +++- 6 files changed, 230 insertions(+), 14 deletions(-) diff --git a/Client/game_sa/CVisibilityPluginsSA.cpp b/Client/game_sa/CVisibilityPluginsSA.cpp index 29f1083ee5..4d47d42b9a 100644 --- a/Client/game_sa/CVisibilityPluginsSA.cpp +++ b/Client/game_sa/CVisibilityPluginsSA.cpp @@ -16,12 +16,18 @@ int CVisibilityPluginsSA::GetClumpAlpha(RpClump* pClump) { + if (!pClump) + return 255; + using GetClumpAlpha = int(__cdecl*)(RpClump*); return reinterpret_cast(0x732B20)(pClump); } void CVisibilityPluginsSA::SetClumpAlpha(RpClump* pClump, int iAlpha) { + if (!pClump) + return; + DWORD dwFunc = FUNC_CVisiblityPlugins_SetClumpAlpha; // clang-format off __asm diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6ed7e01717..894a9717bc 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -4707,7 +4707,13 @@ bool CClientGame::VehicleCollisionHandler(CVehicleSAInterface*& pCollidingVehicl pVehicleClientEntity->CallEvent("onClientVehicleCollision", Arguments, true); - // Update the colliding vehicle, because it might have been invalidated in onClientVehicleCollision (e.g. fixVehicle) + // Update the colliding vehicle, because it might have been invalidated in onClientVehicleCollision (e.g. fixVehicle, destroyElement) + if (pVehicleClientEntity->IsBeingDeleted() || !pVehicleClientEntity->GetGameEntity()) + { + pCollidingVehicle = nullptr; + return true; + } + pCollidingVehicle = reinterpret_cast(pVehicleClientEntity->GetGameEntity()->GetInterface()); // Allocate a BitStream diff --git a/Client/multiplayer_sa/CMultiplayerSA.cpp b/Client/multiplayer_sa/CMultiplayerSA.cpp index af42702f3f..64aa969cbe 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA.cpp @@ -1221,8 +1221,8 @@ void CMultiplayerSA::InitHooks() // Stop CPlayerPed::ProcessControl from calling CVisibilityPlugins::SetClumpAlpha MemSet((void*)0x5E8E84, 0x90, 5); - // Stop CVehicle::UpdateClumpAlpha from calling CVisibilityPlugins::SetClumpAlpha - MemSet((void*)0x6D29CB, 0x90, 5); + // Disable CVehicle::UpdateClumpAlpha completely (fades singleplayer traffic, crashes at 0x732B2A when m_pRwObject is nullptr) + MemPut(0x6D2980, 0xC3); // Disable CVehicle::DoDriveByShootings MemSet((void*)0x741FD0, 0x90, 3); diff --git a/Client/multiplayer_sa/CMultiplayerSA_CrashFixHacks.cpp b/Client/multiplayer_sa/CMultiplayerSA_CrashFixHacks.cpp index 27f4936fa6..7355416be7 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_CrashFixHacks.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_CrashFixHacks.cpp @@ -853,6 +853,87 @@ static void __declspec(naked) HOOK_CrashFix_Misc20() // clang-format on } +//////////////////////////////////////////////////////////////////////// +// CMatrix::UpdateRwMatrix +// +// Prevent crash 0x59AD76 when an unaligned or invalid RwMatrix* is passed +//////////////////////////////////////////////////////////////////////// +#define HOOKPOS_CrashFix_CMatrix__UpdateRwMatrix 0x59AD70 +#define HOOKSIZE_CrashFix_CMatrix__UpdateRwMatrix 6 +static const DWORD RETURN_CrashFix_CMatrix__UpdateRwMatrix = 0x59AD76; +static void __declspec(naked) HOOK_CrashFix_CMatrix__UpdateRwMatrix() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + mov eax, [esp+4] // m (RwMatrix*) + test eax, eax + jz invalid_matrix + + test al, 3 // RwMatrix must be at least 4-byte aligned (RenderWare matrices are 16-byte aligned) + jnz invalid_matrix + + cmp eax, 10000h // Guard against low/unmapped page addresses + jb invalid_matrix + + // Valid pointer: restore overwritten instructions and continue normal path + mov edx, [ecx] // this->mat.right.x + jmp RETURN_CrashFix_CMatrix__UpdateRwMatrix + + invalid_matrix: + push 20 + call CrashAverted + xor eax, eax + retn 4 + } + // clang-format on +} + +//////////////////////////////////////////////////////////////////////// +// CPlaceable::AllocateMatrix +// +// Zero out m_pAttachMatrix and m_bOwnsAttachedMatrix on newly allocated CMatrixLink +//////////////////////////////////////////////////////////////////////// +#define HOOKPOS_CrashFix_AllocateMatrix_Init1 0x54F5A6 +#define HOOKSIZE_CrashFix_AllocateMatrix_Init1 8 +static void __declspec(naked) HOOK_CrashFix_AllocateMatrix_Init1() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + mov [eax+48h], esi // m_pOwner = this + mov [esi+14h], eax // this->m_pMatrix = eax + mov dword ptr [eax+40h], 0 // m_pAttachMatrix = nullptr + mov byte ptr [eax+44h], 0 // m_bOwnsAttachedMatrix = false + pop esi + retn + } + // clang-format on +} + +#define HOOKPOS_CrashFix_AllocateMatrix_Init2 0x54F5C5 +#define HOOKSIZE_CrashFix_AllocateMatrix_Init2 8 +static void __declspec(naked) HOOK_CrashFix_AllocateMatrix_Init2() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + mov [eax+48h], esi // m_pOwner = this + mov [esi+14h], eax // this->m_pMatrix = eax + mov dword ptr [eax+40h], 0 // m_pAttachMatrix = nullptr + mov byte ptr [eax+44h], 0 // m_bOwnsAttachedMatrix = false + pop esi + retn + } + // clang-format on +} + //////////////////////////////////////////////////////////////////////// // CTaskSimpleCarFallOut::FinishAnimFallOutCB // @@ -4092,6 +4173,71 @@ static int _cdecl CFileLoader_LoadVehicleObject_sscanf(const char* s, const char rearWheelSize, wheelUpgradeClass); } +////////////////////////////////////////////////////////////////////////////////////////// +// +// Crash at 0x732B2A in CVisibilityPlugins::GetClumpAlpha +// +// Root cause: In GTA:SA, CVisibilityPlugins::GetClumpAlpha (0x732B20) and SetClumpAlpha +// (0x732B00) read/write directly at [ecx+eax+4] where ecx is ms_clumpPluginOffset (0x34) +// and eax is RpClump*. When called on an entity with null m_pRwObject (e.g. during vehicle, +// ped, or object creation, destruction, streaming, or rendering), eax is nullptr, causing +// an access violation at 0x00000038. +// +// Fix: Hook both functions to check for null RpClump*. If null, GetClumpAlpha safely returns +// 0xFF (255, fully opaque) and SetClumpAlpha returns immediately without memory access. +// +////////////////////////////////////////////////////////////////////////////////////////// +#define HOOKPOS_CVisibilityPlugins_GetClumpAlpha 0x732B20 +#define HOOKSIZE_CVisibilityPlugins_GetClumpAlpha 5 +#define HOOKCHECK_CVisibilityPlugins_GetClumpAlpha 0x8B + +static void __declspec(naked) HOOK_CVisibilityPlugins_GetClumpAlpha() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + mov eax, [esp+4] + test eax, eax + jz null_clump + + mov ecx, ds:[0x8D6094] + mov eax, [ecx+eax+4] + retn + + null_clump: + mov eax, 0xFF + retn + } + // clang-format on +} + +#define HOOKPOS_CVisibilityPlugins_SetClumpAlpha 0x732B00 +#define HOOKSIZE_CVisibilityPlugins_SetClumpAlpha 5 +#define HOOKCHECK_CVisibilityPlugins_SetClumpAlpha 0x8B + +static void __declspec(naked) HOOK_CVisibilityPlugins_SetClumpAlpha() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // clang-format off + __asm + { + mov ecx, [esp+4] + test ecx, ecx + jz null_clump + + mov eax, [esp+8] + mov edx, ds:[0x8D6094] + mov [edx+ecx+4], eax + + null_clump: + retn + } + // clang-format on +} + ////////////////////////////////////////////////////////////////////////////////////////// // // Setup hooks for CrashFixHacks @@ -4116,8 +4262,10 @@ void CMultiplayerSA::InitHooks_CrashFixHacks() EZHookInstall(CrashFix_Misc16); // EZHookInstall ( CrashFix_Misc17 ); EZHookInstall(CrashFix_Misc18); - // EZHookInstall ( CrashFix_Misc19 ); EZHookInstall(CrashFix_Misc20); + EZHookInstall(CrashFix_CMatrix__UpdateRwMatrix); + EZHookInstall(CrashFix_AllocateMatrix_Init1); + EZHookInstall(CrashFix_AllocateMatrix_Init2); EZHookInstall(CrashFix_Misc21); EZHookInstall(CrashFix_Misc22); EZHookInstall(CrashFix_Misc23); @@ -4180,6 +4328,8 @@ void CMultiplayerSA::InitHooks_CrashFixHacks() EZHookInstallChecked(CStreaming__GetNextFileOnCd_NullTxdDef); EZHookInstallChecked(CStreaming__ConvertBufferToObject_NullTxdDef); EZHookInstallChecked(CEventScanner__ScanForEvents_ContactEntity); + EZHookInstallChecked(CVisibilityPlugins_GetClumpAlpha); + EZHookInstallChecked(CVisibilityPlugins_SetClumpAlpha); // Install train crossing crashfix (the temporary variable is required for the template logic) void (*temp)() = HOOK_TrainCrossingBarrierCrashFix; diff --git a/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp b/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp index 62baa72ba9..19a9d8207f 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_FrameRateFixes.cpp @@ -1033,22 +1033,29 @@ static bool __fastcall HOOK_CPhysical__ApplyCollisionAlt(CPhysicalSAInterface* t return result; } +static float scaledVehicleStillVelocityThreshold = 0.0045f; + // In GTA:SA (30 FPS, timeStep = 1.66667f), vehicles sleep after 10 still frames (~333 ms). -// At high framerates (e.g. 240 FPS), 10 frames elapse in only ~41 ms, deactivating unoccupied vehicles -// before suspension springs can lift the chassis and causing bottomed-out suspension on spawn. -// Scaling the threshold dynamically by timeStep maintains an invariant settling window (~400 ms). +// At high framerates (e.g. 240 FPS), 10 frames elapse in only ~41 ms, putting unoccupied +// vehicles to sleep prematurely while suspension springs are still oscillating or settling. +// Scaling GTA:SA's 10-frame threshold by (kOriginalTimeStep / timeStep) preserves +// an invariant ~333 ms stillness window across all framerates with zero magic numbers. static uint8 __cdecl CalculateVehicleSleepFrameThreshold() noexcept { const float timeStep = *reinterpret_cast(0xB7CB5C); if (timeStep <= 0.0001f) return 10; - constexpr float baselineNumerator = 20.0f; - const float rawThreshold = baselineNumerator / timeStep; + // Scale the stationary vehicle damping velocity threshold (0.0045f in GTA:SA) + // to preserve framerate-invariant stillness sensitivity across all framerates. + scaledVehicleStillVelocityThreshold = 0.0045f * (timeStep / kOriginalTimeStep); + + constexpr float originalSleepFrames = 10.0f; + const float rawThreshold = originalSleepFrames * (kOriginalTimeStep / timeStep); return static_cast(std::clamp(std::round(rawThreshold), 10.0f, 240.0f)); } -// Fixes bottomed-out vehicle suspension when spawning or dropping at high framerates. +// Fixes bottomed-out or stretched vehicle suspension when spawning or dropping at high framerates. // CAutomobile::ProcessControl #define HOOKPOS_CAutomobile__ProcessControl_SleepThreshold 0x6B1D34 #define HOOKSIZE_CAutomobile__ProcessControl_SleepThreshold 10 @@ -1075,7 +1082,7 @@ static void __declspec(naked) HOOK_CAutomobile__ProcessControl_SleepThreshold() // clang-format on } -// Fixes bottomed-out bike suspension when spawning or dropping at high framerates. +// Fixes bottomed-out or stretched bike suspension when spawning or dropping at high framerates. // CBike::ProcessControl #define HOOKPOS_CBike__ProcessControl_SleepThreshold 0x6B997C #define HOOKSIZE_CBike__ProcessControl_SleepThreshold 8 @@ -1266,6 +1273,24 @@ void CMultiplayerSA::InitHooks_FrameRateFixes() EZHookInstall(CAutomobile__ProcessControl_SleepThreshold); EZHookInstall(CBike__ProcessControl_SleepThreshold); EZHookInstall(CBike__ProcessControl_SleepClamp); + + // Prevent unoccupied vehicles from instantly sleeping on frame 1 via the idle bypass flag, + // which froze vehicle suspension stretched at spawn before gravity could settle the chassis. + MemSet((void*)0x6B1AF5, 0x90, 5); + MemSet((void*)0x6B9850, 0x90, 5); + + // Prevent stationary damping from wiping vertical velocity (moveSpeed.z = 0), which + // choked gravitational settling at high FPS where per-frame gravity is below 0.0045f. + MemSet((void*)0x6B361C, 0x90, 3); + MemSet((void*)0x6BC18F, 0x90, 3); + + // Scale the stationary damping threshold (0.0045f) to preserve framerate-invariant stillness sensitivity. + MemPut(0x6B33F8, &scaledVehicleStillVelocityThreshold); + MemPut(0x6B340E, &scaledVehicleStillVelocityThreshold); + MemPut(0x6B3424, &scaledVehicleStillVelocityThreshold); + MemPut(0x6BC103, &scaledVehicleStillVelocityThreshold); + MemPut(0x6BC119, &scaledVehicleStillVelocityThreshold); + MemPut(0x6BC12B, &scaledVehicleStillVelocityThreshold); HookInstallCall(CALL_CPhysical__ApplyCollision_1, (DWORD)HOOK_CPhysical__ApplyCollision); HookInstallCall(CALL_CPhysical__ApplyCollision_2, (DWORD)HOOK_CPhysical__ApplyCollision); HookInstallCall(CALL_CPhysical__ApplyCollision_3, (DWORD)HOOK_CPhysical__ApplyCollision); diff --git a/Client/multiplayer_sa/CMultiplayerSA_VehicleCollision.cpp b/Client/multiplayer_sa/CMultiplayerSA_VehicleCollision.cpp index 0a5b2fbf08..83b0814020 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_VehicleCollision.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_VehicleCollision.cpp @@ -79,10 +79,22 @@ static void __declspec(naked) HOOK_CAutomobile_ProcessControl_VehicleDamage() { popad mov ecx, pCollisionVehicle - mov esi, pCollisionVehicle + test ecx, ecx + jz skip_automobile_damage + + mov esi, ecx mov eax, [esi] + cmp eax, 400000h + jb skip_automobile_damage + cmp eax, 900000h + ja skip_automobile_damage + call dword ptr[eax + 0E0h] jmp CONTINUE_CAutomobile_ProcessControl_VehicleDamage + + skip_automobile_damage: + add esp, 18h + jmp CONTINUE_CAutomobile_ProcessControl_VehicleDamage } // clang-format on } @@ -118,10 +130,22 @@ static void __declspec(naked) HOOK_CBike_ProcessControl_VehicleDamage() { popad mov ecx, pCollisionVehicle - mov esi, pCollisionVehicle + test ecx, ecx + jz skip_bike_damage + + mov esi, ecx mov eax, [esi] + cmp eax, 400000h + jb skip_bike_damage + cmp eax, 900000h + ja skip_bike_damage + call dword ptr[eax + 0E0h] jmp CONTINUE_CBike_ProcessControl_VehicleDamage + + skip_bike_damage: + add esp, 18h + jmp CONTINUE_CBike_ProcessControl_VehicleDamage } // clang-format on } @@ -158,8 +182,13 @@ static void __declspec(naked) HOOK_CBoat_ProcessControl_VehicleDamage() { popad mov ecx, pCollisionVehicle - mov esi, pCollisionVehicle + test ecx, ecx + jz skip_boat_damage + + mov esi, ecx call FUNC_CVehicle_ProcessCarAlarm + + skip_boat_damage: jmp CONTINUE_CBoat_ProcessControl_VehicleDamage } // clang-format on