diff --git a/Client/game_sa/CVehicleSA.cpp b/Client/game_sa/CVehicleSA.cpp index 6cc75f5808b..8753d56a7b9 100644 --- a/Client/game_sa/CVehicleSA.cpp +++ b/Client/game_sa/CVehicleSA.cpp @@ -57,6 +57,12 @@ static void __fastcall RehideRhinoMiddleWheels(CAutomobileSAInterface* vehicle) } } +static void __cdecl OnAutomobilePostPreRender(CAutomobileSAInterface* vehicleInterface) +{ + if (g_pCore && g_pCore->GetMultiplayer()) + g_pCore->GetMultiplayer()->RestoreVehicleSuspensionAfterAutomobilePreRender(reinterpret_cast(vehicleInterface)); +} + static void __declspec(naked) HOOK_Vehicle_PreRender(void) { MTA_VERIFY_HOOK_LOCAL_SIZE; @@ -77,6 +83,9 @@ static void __declspec(naked) HOOK_Vehicle_PreRender(void) pushad mov ecx, esi call RehideRhinoMiddleWheels + push esi + call OnAutomobilePostPreRender + add esp, 4 popad mov [esp+0D4h], edi diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 69edcc4337b..eba4deeb3ad 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -289,6 +289,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) g_pMultiplayer->SetPreWorldProcessHandler(CClientGame::StaticPreWorldProcessHandler); g_pMultiplayer->SetPostWorldProcessHandler(CClientGame::StaticPostWorldProcessHandler); g_pMultiplayer->SetPostWorldProcessPedsAfterPreRenderHandler(CClientGame::StaticPostWorldProcessPedsAfterPreRenderHandler); + g_pMultiplayer->SetVehicleAutomobilePostPreRenderHandler(CClientGame::StaticVehicleAutomobilePostPreRenderHandler); g_pMultiplayer->SetPreFxRenderHandler(CClientGame::StaticPreFxRenderHandler); g_pMultiplayer->SetPostColorFilterRenderHandler(CClientGame::StaticPostColorFilterRenderHandler); g_pMultiplayer->SetPreHudRenderHandler(CClientGame::StaticPreHudRenderHandler); @@ -505,6 +506,7 @@ CClientGame::~CClientGame() g_pMultiplayer->SetPreWorldProcessHandler(NULL); g_pMultiplayer->SetPostWorldProcessHandler(NULL); g_pMultiplayer->SetPostWorldProcessPedsAfterPreRenderHandler(nullptr); + g_pMultiplayer->SetVehicleAutomobilePostPreRenderHandler(nullptr); g_pMultiplayer->SetPreFxRenderHandler(NULL); g_pMultiplayer->SetPostColorFilterRenderHandler(nullptr); g_pMultiplayer->SetPreHudRenderHandler(NULL); @@ -3661,6 +3663,18 @@ void CClientGame::StaticPostWorldProcessPedsAfterPreRenderHandler() g_pClientGame->PostWorldProcessPedsAfterPreRenderHandler(); } +void CClientGame::StaticVehicleAutomobilePostPreRenderHandler(CEntitySAInterface* pGameEntity) +{ + if (!pGameEntity || !g_pClientGame) + return; + + CClientEntity* pClientEntity = g_pGame->GetPools()->GetClientEntity((DWORD*)pGameEntity); + if (!pClientEntity || pClientEntity->GetType() != CCLIENTVEHICLE) + return; + + static_cast(pClientEntity)->ApplyWheelComponentPositionsAfterPreRender(); +} + void CClientGame::StaticPreFxRenderHandler() { // RenderFadingInEntities is done at this point, so alpha entity list callbacks diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index cf8048eb67c..7d9fe923b9e 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -581,6 +581,7 @@ class CClientGame static void StaticPreWorldProcessHandler(); static void StaticPostWorldProcessHandler(); static void StaticPostWorldProcessPedsAfterPreRenderHandler(); + static void StaticVehicleAutomobilePostPreRenderHandler(CEntitySAInterface* pVehicle); static void StaticPreFxRenderHandler(); static void StaticPostColorFilterRenderHandler(); static void StaticPreHudRenderHandler(); diff --git a/Client/mods/deathmatch/logic/CClientVehicle.cpp b/Client/mods/deathmatch/logic/CClientVehicle.cpp index af916f2f00c..c50b5ec5b06 100644 --- a/Client/mods/deathmatch/logic/CClientVehicle.cpp +++ b/Client/mods/deathmatch/logic/CClientVehicle.cpp @@ -4641,6 +4641,26 @@ void CClientVehicle::RemoveVehicleSirens() m_tSirenBeaconInfo.m_ucSirenCount = 0; } +void CClientVehicle::ApplyWheelComponentPositionsAfterPreRender() +{ + if (!m_pVehicle) + return; + + // PreRender overwrites wheel dummy Z from suspension lines every frame; re-apply + // script offsets after the wheel block so setVehicleComponentPosition Z sticks. + static const char* wheelComponents[] = {"wheel_lf_dummy", "wheel_rf_dummy", "wheel_lb_dummy", "wheel_rb_dummy"}; + + for (const char* componentName : wheelComponents) + { + const auto it = m_ComponentData.find(componentName); + if (it == m_ComponentData.end() || !it->second.m_bPositionChanged) + continue; + + if (it->second.m_vecOriginalComponentPosition != it->second.m_vecComponentPosition) + SetComponentPosition(componentName, it->second.m_vecComponentPosition); + } +} + bool CClientVehicle::SetComponentPosition(const SString& vehicleComponent, CVector vecPosition, EComponentBaseType inputBase) { // Ensure position is parent relative diff --git a/Client/mods/deathmatch/logic/CClientVehicle.h b/Client/mods/deathmatch/logic/CClientVehicle.h index 2c3c75e89c2..feeb2dc05dc 100644 --- a/Client/mods/deathmatch/logic/CClientVehicle.h +++ b/Client/mods/deathmatch/logic/CClientVehicle.h @@ -516,6 +516,8 @@ class CClientVehicle : public CClientStreamElement bool SetComponentPosition(const SString& vehicleComponent, CVector vecPosition, EComponentBaseType base = EComponentBase::PARENT); bool GetComponentPosition(const SString& vehicleComponent, CVector& vecPosition, EComponentBaseType base = EComponentBase::PARENT); + void ApplyWheelComponentPositionsAfterPreRender(); + bool ResetComponentRotation(const SString& vehicleComponent); bool SetComponentRotation(const SString& vehicleComponent, CVector vecRotation, EComponentBaseType base = EComponentBase::PARENT); bool GetComponentRotation(const SString& vehicleComponent, CVector& vecRotation, EComponentBaseType base = EComponentBase::PARENT); diff --git a/Client/multiplayer_sa/CMultiplayerSA.cpp b/Client/multiplayer_sa/CMultiplayerSA.cpp index 741426c9723..ea345967d5d 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA.cpp @@ -189,6 +189,12 @@ DWORD RETURN_CHandlingData_isNotFWD = 0x6A04C3; #define CALL_CMonsterTruck_ProcessEntityCollision 0x6C8B9E DWORD RETURN_ProcessEntityCollision = 0x4185C0; +// CAutomobile::PreRender entry - swap per-model suspension lines to per-vehicle private +// data before the wheel positioning block reads them (same pattern as collision hook). +#define HOOKPOS_CAutomobile_PreRender 0x6AAB50 +#define HOOKSIZE_CAutomobile_PreRender 5 +DWORD RETURN_CAutomobile_PreRender = 0x6AAB55; + #define HOOKPOS_PreFxRender 0x049E650 DWORD RETURN_PreFxRender = 0x0404D1E; @@ -418,6 +424,7 @@ ObjectBreakHandler* m_pObjectBreakHandler = NULL; FxSystemDestructionHandler* m_pFxSystemDestructionHandler = NULL; DrivebyAnimationHandler* m_pDrivebyAnimationHandler = NULL; AudioZoneRadioSwitchHandler* m_pAudioZoneRadioSwitchHandler = NULL; +VehicleAutomobilePostPreRenderHandler* m_pVehicleAutomobilePostPreRenderHandler = nullptr; CEntitySAInterface* dwSavedPlayerPointer = 0; CEntitySAInterface* activeEntityForStreaming = 0; // the entity that the streaming system considers active @@ -2729,6 +2736,11 @@ void CMultiplayerSA::SetAudioZoneRadioSwitchHandler(AudioZoneRadioSwitchHandler* m_pAudioZoneRadioSwitchHandler = pHandler; } +void CMultiplayerSA::SetVehicleAutomobilePostPreRenderHandler(VehicleAutomobilePostPreRenderHandler* pHandler) +{ + m_pVehicleAutomobilePostPreRenderHandler = pHandler; +} + // What we do here is check if the idle handler has been set bool CMultiplayerSA::IsConnected() { @@ -6628,6 +6640,55 @@ void SetModelSuspensionLines(CVehicleSAInterface* pVehicleIntf, void* pSuspensio CModelInfo* pModelInfo = pGameInterface->GetModelInfo(pVehicleIntf->m_pVehicle->GetModelIndex()); pModelInfo->SetVehicleSuspensionData(pSuspensionLines); } + +static void* g_pPreRenderSavedSuspensionLines = nullptr; +static CVehicleSAInterface* g_pPreRenderSuspensionVehicle = nullptr; + +static bool ShouldUsePrivateSuspensionLinesForPreRender(CVehicleSAInterface* pVehicleIntf) +{ + if (!pVehicleIntf) + return false; + + CVehicle* pVehicle = pVehicleIntf->m_pVehicle; + if (!pVehicle) + return false; + + CModelInfo* pModelInfo = pGameInterface->GetModelInfo(pVehicle->GetModelIndex()); + return pModelInfo && (pModelInfo->IsCar() || pModelInfo->IsMonsterTruck()); +} + +static void __cdecl BeginAutomobilePreRenderSuspension(CVehicleSAInterface* pVehicleIntf) +{ + if (!ShouldUsePrivateSuspensionLinesForPreRender(pVehicleIntf)) + return; + + g_pPreRenderSavedSuspensionLines = SetModelSuspensionLinesToVehiclePrivate(pVehicleIntf); + g_pPreRenderSuspensionVehicle = pVehicleIntf; +} + +static void __declspec(naked) HOOK_CAutomobile_PreRenderStart() +{ + MTA_VERIFY_HOOK_LOCAL_SIZE; + + // Replaced bytes at 0x6AAB50: sub esp, 74h; push ebx; push esi + // clang-format off + __asm + { + sub esp, 74h + push ebx + push esi + + pushad + push ecx + call BeginAutomobilePreRenderSuspension + add esp, 4 + popad + + jmp RETURN_CAutomobile_PreRender + } + // clang-format on +} + // Some variables. DWORD dwSuspensionChangedJump = 0x4185C0; bool bSuspensionChanged = false; @@ -6722,6 +6783,23 @@ void CMultiplayerSA::UpdateVehicleSuspension() const noexcept { HookInstallCall(CALL_CAutomobile_ProcessEntityCollision, reinterpret_cast(HOOK_ProcessVehicleCollision)); HookInstallCall(CALL_CMonsterTruck_ProcessEntityCollision, reinterpret_cast(HOOK_ProcessVehicleCollision)); + HookInstall(HOOKPOS_CAutomobile_PreRender, reinterpret_cast(HOOK_CAutomobile_PreRenderStart), HOOKSIZE_CAutomobile_PreRender); +} + +void CMultiplayerSA::RestoreVehicleSuspensionAfterAutomobilePreRender(CEntitySAInterface* pVehicleIntf) +{ + auto* pVehicle = static_cast(pVehicleIntf); + + // Restore the per-model suspension lines that were swapped at PreRender entry. + if (pVehicle && pVehicle == g_pPreRenderSuspensionVehicle && g_pPreRenderSavedSuspensionLines) + { + SetModelSuspensionLines(pVehicle, g_pPreRenderSavedSuspensionLines); + g_pPreRenderSavedSuspensionLines = nullptr; + g_pPreRenderSuspensionVehicle = nullptr; + } + + if (m_pVehicleAutomobilePostPreRenderHandler) + m_pVehicleAutomobilePostPreRenderHandler(pVehicleIntf); } // Variables diff --git a/Client/multiplayer_sa/CMultiplayerSA.h b/Client/multiplayer_sa/CMultiplayerSA.h index 166566ac9f9..4d732d0b829 100644 --- a/Client/multiplayer_sa/CMultiplayerSA.h +++ b/Client/multiplayer_sa/CMultiplayerSA.h @@ -320,6 +320,8 @@ class CMultiplayerSA : public CMultiplayer CLimits* GetLimits() { return &m_limits; } void UpdateVehicleSuspension() const noexcept; + void RestoreVehicleSuspensionAfterAutomobilePreRender(CEntitySAInterface* pVehicleIntf) override; + void SetVehicleAutomobilePostPreRenderHandler(VehicleAutomobilePostPreRenderHandler* pHandler) override; virtual void FlushClothesCache(); virtual void SetFastClothesLoading(EFastClothesLoading fastClothesLoading); diff --git a/Client/sdk/multiplayer/CMultiplayer.h b/Client/sdk/multiplayer/CMultiplayer.h index 28d2e074088..28e03e6c432 100644 --- a/Client/sdk/multiplayer/CMultiplayer.h +++ b/Client/sdk/multiplayer/CMultiplayer.h @@ -136,6 +136,7 @@ typedef void(FxSystemDestructionHandler)(void* pFxSA); typedef AnimationId(DrivebyAnimationHandler)(AnimationId animGroup, AssocGroupId animId); typedef void(PedStepHandler)(CPedSAInterface* pPed, bool bFoot); typedef void(AudioZoneRadioSwitchHandler)(DWORD dwStationID); +typedef void(VehicleAutomobilePostPreRenderHandler)(CEntitySAInterface* pVehicle); using VehicleWeaponHitHandler = void(SVehicleWeaponHitEvent& event); @@ -432,6 +433,8 @@ class CMultiplayer virtual CLimits* GetLimits() = 0; virtual void UpdateVehicleSuspension() const noexcept = 0; + virtual void RestoreVehicleSuspensionAfterAutomobilePreRender(CEntitySAInterface* pVehicleIntf) = 0; + virtual void SetVehicleAutomobilePostPreRenderHandler(VehicleAutomobilePostPreRenderHandler* pHandler) = 0; virtual void FlushClothesCache() = 0; virtual void SetFastClothesLoading(EFastClothesLoading fastClothesLoading) = 0;