From 6aa7cd98e017e52713cc0e35e82d848b9ba602fb Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Sun, 21 Jun 2026 23:38:06 -0300 Subject: [PATCH 1/6] Sync persistent projectiles (satchels, teargas, molotov) to players who come into range later Packet_ProjectileSync only broadcasts a projectile's creation once, to whoever is in range at that instant. Satchel charges, teargas clouds and molotov fires stick around in the world afterwards, so a player who was out of range when one appeared never sees it, even after walking right up to it. Server now remembers these per-owner, and periodically checks if any other player has come within sync range, sending them the original creation packet when they do. Satchels are tracked until detonated/destroyed explicitly; teargas/molotov entries expire after 20s, comfortably outlasting their visual effect. Fixes https://github.com/multitheftauto/mtasa-blue/issues/369 Fixes https://github.com/multitheftauto/mtasa-blue/issues/368 --- Server/mods/deathmatch/logic/CGame.cpp | 86 ++++++++++++++++++++++++++ Server/mods/deathmatch/logic/CGame.h | 2 + Server/mods/deathmatch/logic/CPlayer.h | 13 ++++ 3 files changed, 101 insertions(+) diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index bdccfaea790..cc9fb1df39f 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -532,6 +532,7 @@ void CGame::DoPulse() #ifdef WITH_OBJECT_SYNC CLOCK_CALL1(m_pObjectSync->DoPulse();); #endif + CLOCK_CALL1(ProcessProjectileStreamIn();); CLOCK_CALL1(m_pBanManager->DoPulse();); CLOCK_CALL1(m_pAccountManager->DoPulse();); CLOCK_CALL1(m_pRegistryManager->DoPulse();); @@ -2818,6 +2819,8 @@ void CGame::Packet_DetonateSatchels(CDetonateSatchelsPacket& Packet) m_pPlayerManager->BroadcastOnlyJoined(Packet); // Take away their detonator CStaticFunctionDefinitions::TakeWeapon(pPlayer, 40); + // They're gone now, stop tracking them for stream-in + pPlayer->GetPersistentProjectilesList().clear(); } } @@ -2831,6 +2834,8 @@ void CGame::Packet_DestroySatchels(CDestroySatchelsPacket& Packet) m_pPlayerManager->BroadcastOnlyJoined(Packet); // Take away their detonator CStaticFunctionDefinitions::TakeWeapon(pPlayer, 40); + // They're gone now, stop tracking them for stream-in + pPlayer->GetPersistentProjectilesList().clear(); } } @@ -3018,6 +3023,87 @@ void CGame::Packet_ProjectileSync(CProjectileSyncPacket& Packet) } } CPlayerManager::Broadcast(Packet, sendList); + + // Some projectiles have a lasting world presence (satchels stay until detonated/destroyed, teargas leaves + // a lingering gas cloud, molotovs leave a burning fire) so players who weren't close enough at creation time + // never see them, even after walking right up to them. Remember these and keep checking for players who + // come into range later (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). + // PERSISTENT_PROJECTILE_LIFETIME (20s) comfortably outlasts the native gas/fire visual effect, while satchels + // (expiryTime left at zero) are only ever removed explicitly, since they have no lifespan of their own. + constexpr unsigned long PERSISTENT_PROJECTILE_LIFETIME = 20000; + + bool bIsPersistentProjectile = false; + CTickCount expiryTime; // Zero means "doesn't expire on its own" + if (Packet.m_ucWeaponType == WEAPONTYPE_REMOTE_SATCHEL_CHARGE) + bIsPersistentProjectile = true; + else if (Packet.m_ucWeaponType == WEAPONTYPE_TEARGAS || Packet.m_ucWeaponType == WEAPONTYPE_MOLOTOV) + { + bIsPersistentProjectile = true; + expiryTime = CTickCount::Now() + CTickCount(static_cast(PERSISTENT_PROJECTILE_LIFETIME)); + } + + if (bIsPersistentProjectile) + { + CPlayer::SPersistentProjectileInfo info; + info.packet = Packet; + info.expiryTime = expiryTime; + info.notifiedPlayers.insert(pPlayer); + for (const auto& [usBitStreamVersion, pSendPlayer] : sendList) + info.notifiedPlayers.insert(pSendPlayer); + pPlayer->GetPersistentProjectilesList().push_back(std::move(info)); + } + } +} + +void CGame::ProcessProjectileStreamIn() +{ + if (m_ProjectileStreamInTimer.Get() < 500) + return; + m_ProjectileStreamInTimer.Reset(); + + const CTickCount now = CTickCount::Now(); + + for (auto iter = m_pPlayerManager->IterBegin(); iter != m_pPlayerManager->IterEnd(); ++iter) + { + CPlayer* pOwner = *iter; + + auto& projectilesList = pOwner->GetPersistentProjectilesList(); + if (projectilesList.empty()) + continue; + + // Drop expired entries (teargas/molotov) before doing any range checks + projectilesList.erase(std::remove_if(projectilesList.begin(), projectilesList.end(), + [&](const CPlayer::SPersistentProjectileInfo& info) + { return info.expiryTime != CTickCount() && now >= info.expiryTime; }), + projectilesList.end()); + + for (auto& projectileInfo : projectilesList) + { + CVector vecPosition = projectileInfo.packet.m_vecOrigin; + if (projectileInfo.packet.m_OriginID != INVALID_ELEMENT_ID) + { + if (CElement* pOriginSource = CElementIDs::GetElement(projectileInfo.packet.m_OriginID)) + vecPosition += pOriginSource->GetPosition(); + } + + for (auto playerIter = m_pPlayerManager->IterBegin(); playerIter != m_pPlayerManager->IterEnd(); ++playerIter) + { + CPlayer* pOther = *playerIter; + if (pOther == pOwner || !pOther->IsJoined() || projectileInfo.notifiedPlayers.count(pOther) > 0) + continue; + + CVector vecCameraPosition; + pOther->GetCamera()->GetPosition(vecCameraPosition); + + if (IsPointNearPoint3D(vecPosition, vecCameraPosition, MAX_PROJECTILE_SYNC_DISTANCE)) + { + CProjectileSyncPacket sendPacket = projectileInfo.packet; + sendPacket.SetSourceElement(pOwner); + pOther->Send(sendPacket); + projectileInfo.notifiedPlayers.insert(pOther); + } + } + } } } diff --git a/Server/mods/deathmatch/logic/CGame.h b/Server/mods/deathmatch/logic/CGame.h index 9d0ace7b1aa..1e8200e2699 100644 --- a/Server/mods/deathmatch/logic/CGame.h +++ b/Server/mods/deathmatch/logic/CGame.h @@ -500,6 +500,7 @@ class CGame void Packet_DestroySatchels(class CDestroySatchelsPacket& Packet); void Packet_ExplosionSync(class CExplosionSyncPacket& Packet); void Packet_ProjectileSync(class CProjectileSyncPacket& Packet); + void ProcessProjectileStreamIn(); void Packet_Command(class CCommandPacket& Packet); void Packet_VehicleDamageSync(class CVehicleDamageSyncPacket& Packet); void Packet_VehiclePuresync(class CVehiclePuresyncPacket& Packet); @@ -557,6 +558,7 @@ class CGame #ifdef WITH_OBJECT_SYNC CObjectSync* m_pObjectSync; #endif + CElapsedTime m_ProjectileStreamInTimer; CMarkerManager* m_pMarkerManager; CClock* m_pClock; CBanManager* m_pBanManager; diff --git a/Server/mods/deathmatch/logic/CPlayer.h b/Server/mods/deathmatch/logic/CPlayer.h index b332f40eb12..bc828c75a5e 100644 --- a/Server/mods/deathmatch/logic/CPlayer.h +++ b/Server/mods/deathmatch/logic/CPlayer.h @@ -23,6 +23,7 @@ class CPlayer; #include "CObject.h" #include "packets/CPacket.h" #include "packets/CPlayerStatsPacket.h" +#include "packets/CProjectileSyncPacket.h" #include "CStringName.h" class CKeyBinds; class CPlayerCamera; @@ -167,6 +168,16 @@ class CPlayer final : public CPed, public CClient std::list::const_iterator IterSyncingObjectBegin() { return m_SyncingObjects.begin(); }; std::list::const_iterator IterSyncingObjectEnd() { return m_SyncingObjects.end(); }; + // Projectiles with a lasting world presence (satchels, teargas clouds, molotov fires) planted/thrown by this + // player, kept so players who come into range later still see them (https://github.com/multitheftauto/mtasa-blue/issues/369, #368) + struct SPersistentProjectileInfo + { + CProjectileSyncPacket packet; // Original creation packet data (source element re-applied on resend) + std::unordered_set notifiedPlayers; + CTickCount expiryTime; // Default (zero) means it never expires on its own (e.g. satchels, cleared explicitly instead) + }; + std::vector& GetPersistentProjectilesList() { return m_PersistentProjectilesList; }; + unsigned int GetScriptDebugLevel() { return m_uiScriptDebugLevel; }; bool SetScriptDebugLevel(std::uint8_t level); @@ -385,6 +396,8 @@ class CPlayer final : public CPed, public CClient std::list m_SyncingPeds; std::list m_SyncingObjects; + std::vector m_PersistentProjectilesList; + unsigned int m_uiScriptDebugLevel; ElementID m_PlayerAttackerID; From 0d4346f82e02c8498e9022c27f15b3aefeb4255a Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Sat, 27 Jun 2026 17:02:23 -0300 Subject: [PATCH 2/6] Retry projectile creation when the creator's game entity isn't streamed in yet Packet_ProjectileSync's client handler silently drops the projectile if CClientProjectileManager::Create() can't get a game-layer entity for the creator (pPed->GetGameEntity() null), which happens whenever the thrower's ped/vehicle hasn't streamed into the GTA world yet for the receiving client - e.g. right after connecting, or right after coming into range of a satchel/ teargas/molotov that was planted earlier while out of view (see the ProcessProjectileStreamIn resync added previously for issues #369/#368). Queue the creation and keep retrying every pulse for up to a minute instead, giving the engine time to stream the creator in. --- .../logic/CClientProjectileManager.cpp | 62 +++++++++++++++++++ .../logic/CClientProjectileManager.h | 23 +++++++ .../mods/deathmatch/logic/CPacketHandler.cpp | 12 ++++ 3 files changed, 97 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp index 862ebc68c40..929727a881c 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp @@ -55,6 +55,68 @@ void CClientProjectileManager::DoPulse() pProjectile->DoPulse(); } } + + ProcessPendingCreations(); +} + +void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, + const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel) +{ + SPendingProjectileCreation pending; + pending.creatorID = creatorID; + pending.weaponType = eWeapon; + pending.vecOrigin = vecOrigin; + pending.fForce = fForce; + pending.targetID = targetID; + pending.vecRotation = vecRotation; + pending.vecVelocity = vecVelocity; + pending.usModel = usModel; + pending.llCreationTime = GetTickCount64_(); + m_PendingCreations.push_back(pending); +} + +void CClientProjectileManager::ProcessPendingCreations() +{ + if (m_PendingCreations.empty()) + return; + + // Generous timeout: the creator's ped/vehicle just needs to come within the game's own streaming distance, + // which can take a while if whoever it belongs to is approaching on foot from the edge of sync range. + constexpr long long PENDING_CREATION_TIMEOUT = 60000; + const long long llNow = GetTickCount64_(); + + for (auto iter = m_PendingCreations.begin(); iter != m_PendingCreations.end();) + { + SPendingProjectileCreation& pending = *iter; + + bool bResolved = false; + CClientEntity* pCreator = CElementIDs::GetElement(pending.creatorID); + if (pCreator) + { + if (pCreator->GetType() == CCLIENTPED || pCreator->GetType() == CCLIENTPLAYER) + { + CClientVehicle* pVehicle = static_cast(pCreator)->GetOccupiedVehicle(); + if (pVehicle) + pCreator = pVehicle; + } + + CClientEntity* pTargetEntity = NULL; + if (pending.targetID != INVALID_ELEMENT_ID) + pTargetEntity = CElementIDs::GetElement(pending.targetID); + + CClientProjectile* pProjectile = Create(pCreator, pending.weaponType, pending.vecOrigin, pending.fForce, NULL, pTargetEntity); + if (pProjectile) + { + pProjectile->Initiate(pending.vecOrigin, pending.vecRotation, pending.vecVelocity, pending.usModel); + bResolved = true; + } + } + + if (bResolved || (llNow - pending.llCreationTime) > PENDING_CREATION_TIMEOUT) + iter = m_PendingCreations.erase(iter); + else + ++iter; + } } void CClientProjectileManager::RemoveAll() diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.h b/Client/mods/deathmatch/logic/CClientProjectileManager.h index 1992ab9d6cf..b68cb9779b2 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.h +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.h @@ -12,6 +12,7 @@ #include "CClientProjectile.h" #include +#include typedef void(ProjectileInitiateHandler)(CClientProjectile*); class CClientManager; @@ -41,6 +42,12 @@ class CClientProjectileManager float fForce, CVector* target, CEntity* pGameTarget); CClientProjectile* Create(CClientEntity* pCreator, eWeaponType eWeapon, CVector& vecOrigin, float fForce, CVector* target, CClientEntity* pTargetEntity); + // The creator's in-game ped/vehicle may not have streamed in yet (e.g. they just connected, or we only just came + // into range of a projectile they planted earlier while out of view). Queue the creation and keep retrying for a + // while instead of silently dropping it. + void QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, + const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel); + protected: void AddToList(CClientProjectile* pProjectile) { m_List.push_back(pProjectile); } void RemoveFromList(CClientProjectile* pProjectile); @@ -48,6 +55,8 @@ class CClientProjectileManager void TakeOutTheTrash(); private: + void ProcessPendingCreations(); + CClientManager* m_pManager; std::list m_List; @@ -56,4 +65,18 @@ class CClientProjectileManager bool m_bCreating; CClientProjectilePtr m_pLastCreated; + + struct SPendingProjectileCreation + { + ElementID creatorID; + eWeaponType weaponType; + CVector vecOrigin; + float fForce; + ElementID targetID; + CVector vecRotation; + CVector vecVelocity; + unsigned short usModel; + long long llCreationTime; + }; + std::vector m_PendingCreations; }; diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index b46c7497c31..57fdb646777 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -4908,6 +4908,7 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) if (bCreateProjectile) { + bool bCreated = false; if (pCreator) { if (pCreator->GetType() == CCLIENTPED || pCreator->GetType() == CCLIENTPLAYER) @@ -4922,8 +4923,19 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) if (pProjectile) { pProjectile->Initiate(origin.data.vecPosition, rotation.data.vecRotation, velocity.data.vecVelocity, usModel); + bCreated = true; } } + + // The creator's in-game ped/vehicle might not have streamed in yet (e.g. they just connected, or we only + // just came into sync range of a projectile they planted earlier while we were out of view - see + // CGame::ProcessProjectileStreamIn on the server). Keep retrying instead of silently dropping it. + if (!bCreated && CreatorID != INVALID_ELEMENT_ID) + { + ElementID TargetID = pTargetEntity ? pTargetEntity->GetID() : INVALID_ELEMENT_ID; + g_pClientGame->m_pManager->GetProjectileManager()->QueuePendingCreation(CreatorID, weaponType, origin.data.vecPosition, fForce, TargetID, + rotation.data.vecRotation, velocity.data.vecVelocity, usModel); + } } } From d9c0c2bfff97cb1a52a824c85b647b4a4da287bf Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Sat, 27 Jun 2026 18:28:54 -0300 Subject: [PATCH 3/6] Resync satchel charges to their actual resting position instead of replaying the throw Late stream-in (just connected, or just came into range of a satchel planted earlier) resent the original throw packet - origin + velocity - which replays the whole toss client-side. Since physics replay isn't perfectly deterministic, the satchel can visibly fly through the air again and occasionally settle somewhere slightly different (floating, or in a different spot) than the one everyone else has been looking at. The owning client now reports the actual resting position once CClientProjectile::CorrectPhysics finishes settling the satchel (new PACKET_ID_PROJECTILE_REST_POSITION packet). The server swaps the tracked persistent-projectile entry over to that position with zero velocity/force, so any later stream-in places it directly instead of re-throwing it. Scoped to satchel charges only - they're the only persistent projectile type with no lifespan of its own, so a wrong replay stays wrong indefinitely instead of being a momentary blip like teargas/molotov. https://github.com/multitheftauto/mtasa-blue/issues/369 https://github.com/multitheftauto/mtasa-blue/issues/368 --- Client/mods/deathmatch/logic/CClientGame.cpp | 21 +++++++++ Client/mods/deathmatch/logic/CClientGame.h | 1 + .../deathmatch/logic/CClientProjectile.cpp | 9 ++++ Server/mods/deathmatch/logic/CGame.cpp | 37 +++++++++++++++ Server/mods/deathmatch/logic/CGame.h | 2 + .../deathmatch/logic/CPacketTranslator.cpp | 5 ++ .../packets/CProjectileRestPositionPacket.cpp | 47 +++++++++++++++++++ .../packets/CProjectileRestPositionPacket.h | 34 ++++++++++++++ Shared/mods/deathmatch/logic/Enums.cpp | 1 + Shared/sdk/net/Packets.h | 1 + 10 files changed, 158 insertions(+) create mode 100644 Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp create mode 100644 Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6f2794443e2..74def368633 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -5511,6 +5511,27 @@ void CClientGame::SendProjectileSync(CClientProjectile* pProjectile) } } +void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition) +{ + NetBitStreamInterface* pBitStream = g_pNet->AllocateNetBitStream(); + if (pBitStream) + { + pBitStream->Write(static_cast(weaponType)); + + pBitStream->Write(vecOrigin.fX); + pBitStream->Write(vecOrigin.fY); + pBitStream->Write(vecOrigin.fZ); + + pBitStream->Write(vecRestPosition.fX); + pBitStream->Write(vecRestPosition.fY); + pBitStream->Write(vecRestPosition.fZ); + + g_pNet->SendPacket(PACKET_ID_PROJECTILE_REST_POSITION, pBitStream, PACKET_PRIORITY_LOW, PACKET_RELIABILITY_RELIABLE_ORDERED); + + g_pNet->DeallocateNetBitStream(pBitStream); + } +} + void CClientGame::ResetAmmoInClip() { memset(&m_wasWeaponAmmoInClip[0], 0, sizeof(m_wasWeaponAmmoInClip)); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index cf8048eb67c..9888e7df3be 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -685,6 +685,7 @@ class CClientGame std::optional vehicleBlowState = std::nullopt); void SendFireSync(CFire* pFire); void SendProjectileSync(CClientProjectile* pProjectile); + void SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition); void SetServerVersionSortable(const SString& strVersion) { m_strServerVersionSortable = strVersion; } const SString& GetServerVersionSortable() { return m_strServerVersionSortable; } diff --git a/Client/mods/deathmatch/logic/CClientProjectile.cpp b/Client/mods/deathmatch/logic/CClientProjectile.cpp index 048334da6ad..24ee5758f67 100644 --- a/Client/mods/deathmatch/logic/CClientProjectile.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectile.cpp @@ -154,6 +154,15 @@ void CClientProjectile::DoPulse() if (m_bCorrected == false && m_pProjectile != NULL && GetWeaponType() == eWeaponType::WEAPONTYPE_REMOTE_SATCHEL_CHARGE) { m_bCorrected = m_pProjectile->CorrectPhysics(); + + // Tell the server where we actually settled, once, so a player who streams in later gets placed here + // directly instead of having the whole throw replayed at them (https://github.com/multitheftauto/mtasa-blue/issues/369, #368) + if (m_bCorrected && IsLocal() && GetCreator() == g_pClientGame->GetLocalPlayer()) + { + CVector vecRestPosition; + GetPosition(vecRestPosition); + g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition); + } } } diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index cc9fb1df39f..138dec91338 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -1251,6 +1251,10 @@ bool CGame::ProcessPacket(CPacket& Packet) Packet_ProjectileSync(static_cast(Packet)); return true; + case PACKET_ID_PROJECTILE_REST_POSITION: + Packet_ProjectileRestPosition(static_cast(Packet)); + return true; + case PACKET_ID_COMMAND: { Packet_Command(static_cast(Packet)); @@ -3107,6 +3111,39 @@ void CGame::ProcessProjectileStreamIn() } } +void CGame::Packet_ProjectileRestPosition(CProjectileRestPositionPacket& Packet) +{ + // Sent by the owning client once a satchel charge has settled (CClientProjectile::CorrectPhysics finished). + // Until now, ProcessProjectileStreamIn resent late-joining players the original throw packet (origin + velocity), + // which replays the whole toss client-side - looking like it was just thrown, and since physics replay isn't + // perfectly deterministic, occasionally settling somewhere slightly different (floating, or in a different spot) + // than the satchel everyone else has been looking at for the last while. Swap the tracked entry over to the + // actual resting spot with no velocity so future stream-ins just place it there directly + // (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). + CPlayer* pPlayer = Packet.GetSourcePlayer(); + if (!pPlayer || !pPlayer->IsJoined()) + return; + + if (Packet.m_ucWeaponType != WEAPONTYPE_REMOTE_SATCHEL_CHARGE) + return; + + for (auto& info : pPlayer->GetPersistentProjectilesList()) + { + if (info.packet.m_ucWeaponType != Packet.m_ucWeaponType) + continue; + + // Match against the throw's original origin (the key the client knows the entry by) + if (!IsPointNearPoint3D(info.packet.m_vecOrigin, Packet.m_vecOrigin, 1.0f)) + continue; + + info.packet.m_vecOrigin = Packet.m_vecRestPosition; + info.packet.m_OriginID = INVALID_ELEMENT_ID; + info.packet.m_vecMoveSpeed = CVector(); + info.packet.m_fForce = 0.0f; + break; + } +} + void CGame::Packet_Vehicle_InOut(CVehicleInOutPacket& Packet) { // Grab the source player diff --git a/Server/mods/deathmatch/logic/CGame.h b/Server/mods/deathmatch/logic/CGame.h index 1e8200e2699..484519e1e6e 100644 --- a/Server/mods/deathmatch/logic/CGame.h +++ b/Server/mods/deathmatch/logic/CGame.h @@ -24,6 +24,7 @@ class CGame; #include "packets/CCommandPacket.h" #include "packets/CExplosionSyncPacket.h" #include "packets/CProjectileSyncPacket.h" +#include "packets/CProjectileRestPositionPacket.h" #include "packets/CPedWastedPacket.h" #include "packets/CPlayerJoinDataPacket.h" #include "packets/CPlayerQuitPacket.h" @@ -500,6 +501,7 @@ class CGame void Packet_DestroySatchels(class CDestroySatchelsPacket& Packet); void Packet_ExplosionSync(class CExplosionSyncPacket& Packet); void Packet_ProjectileSync(class CProjectileSyncPacket& Packet); + void Packet_ProjectileRestPosition(class CProjectileRestPositionPacket& Packet); void ProcessProjectileStreamIn(); void Packet_Command(class CCommandPacket& Packet); void Packet_VehicleDamageSync(class CVehicleDamageSyncPacket& Packet); diff --git a/Server/mods/deathmatch/logic/CPacketTranslator.cpp b/Server/mods/deathmatch/logic/CPacketTranslator.cpp index 20ced1ab808..163f0924cfa 100644 --- a/Server/mods/deathmatch/logic/CPacketTranslator.cpp +++ b/Server/mods/deathmatch/logic/CPacketTranslator.cpp @@ -28,6 +28,7 @@ #include "packets/CCommandPacket.h" #include "packets/CExplosionSyncPacket.h" #include "packets/CProjectileSyncPacket.h" +#include "packets/CProjectileRestPositionPacket.h" #include "packets/CVehicleInOutPacket.h" #include "packets/CVehicleDamageSyncPacket.h" #include "packets/CVehicleTrailerPacket.h" @@ -133,6 +134,10 @@ CPacket* CPacketTranslator::Translate(const NetServerPlayerID& Socket, ePacketID pTemp = new CProjectileSyncPacket; break; + case PACKET_ID_PROJECTILE_REST_POSITION: + pTemp = new CProjectileRestPositionPacket; + break; + case PACKET_ID_VEHICLE_INOUT: pTemp = new CVehicleInOutPacket; break; diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp new file mode 100644 index 00000000000..16140f3d897 --- /dev/null +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp @@ -0,0 +1,47 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp + * PURPOSE: Projectile final resting position packet class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" +#include "CProjectileRestPositionPacket.h" + +CProjectileRestPositionPacket::CProjectileRestPositionPacket() +{ + m_ucWeaponType = 0; +} + +bool CProjectileRestPositionPacket::Read(NetBitStreamInterface& BitStream) +{ + if (!BitStream.Read(m_ucWeaponType)) + return false; + + if (!BitStream.Read(m_vecOrigin.fX) || !BitStream.Read(m_vecOrigin.fY) || !BitStream.Read(m_vecOrigin.fZ)) + return false; + + if (!BitStream.Read(m_vecRestPosition.fX) || !BitStream.Read(m_vecRestPosition.fY) || !BitStream.Read(m_vecRestPosition.fZ)) + return false; + + return true; +} + +bool CProjectileRestPositionPacket::Write(NetBitStreamInterface& BitStream) const +{ + BitStream.Write(m_ucWeaponType); + + BitStream.Write(m_vecOrigin.fX); + BitStream.Write(m_vecOrigin.fY); + BitStream.Write(m_vecOrigin.fZ); + + BitStream.Write(m_vecRestPosition.fX); + BitStream.Write(m_vecRestPosition.fY); + BitStream.Write(m_vecRestPosition.fZ); + + return true; +} diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h new file mode 100644 index 00000000000..58710e10702 --- /dev/null +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h @@ -0,0 +1,34 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h + * PURPOSE: Projectile final resting position packet class + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once + +#include "CPacket.h" +#include + +// Sent by the owning client once a persistent projectile (currently just satchel charges) has settled, so the +// server can resync late-joining/streaming-in players with the actual resting spot instead of replaying the +// original throw (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). +class CProjectileRestPositionPacket final : public CPacket +{ +public: + CProjectileRestPositionPacket(); + + ePacketID GetPacketID() const { return PACKET_ID_PROJECTILE_REST_POSITION; }; + unsigned long GetFlags() const { return PACKET_HIGH_PRIORITY | PACKET_RELIABLE | PACKET_SEQUENCED; }; + + bool Read(NetBitStreamInterface& BitStream); + bool Write(NetBitStreamInterface& BitStream) const; + + unsigned char m_ucWeaponType; + CVector m_vecOrigin; // Matches the original throw's m_vecOrigin, used to find the tracked entry + CVector m_vecRestPosition; +}; diff --git a/Shared/mods/deathmatch/logic/Enums.cpp b/Shared/mods/deathmatch/logic/Enums.cpp index 8bb99db974f..d7f855b0a17 100644 --- a/Shared/mods/deathmatch/logic/Enums.cpp +++ b/Shared/mods/deathmatch/logic/Enums.cpp @@ -163,6 +163,7 @@ ADD_ENUM1(PACKET_ID_RETURN_SYNC) ADD_ENUM1(PACKET_ID_EXPLOSION) ADD_ENUM1(PACKET_ID_FIRE) ADD_ENUM1(PACKET_ID_PROJECTILE) +ADD_ENUM1(PACKET_ID_PROJECTILE_REST_POSITION) ADD_ENUM1(PACKET_ID_DETONATE_SATCHELS) ADD_ENUM1(PACKET_ID_DESTROY_SATCHELS) ADD_ENUM1(PACKET_ID_COMMAND) diff --git a/Shared/sdk/net/Packets.h b/Shared/sdk/net/Packets.h index 5331cc54667..f37f3d67a6b 100644 --- a/Shared/sdk/net/Packets.h +++ b/Shared/sdk/net/Packets.h @@ -67,6 +67,7 @@ enum ePacketID PACKET_ID_EXPLOSION, PACKET_ID_FIRE, PACKET_ID_PROJECTILE, + PACKET_ID_PROJECTILE_REST_POSITION, PACKET_ID_DETONATE_SATCHELS, PACKET_ID_DESTROY_SATCHELS, From d562ece49e6335075f0b7b9694f5e240c90529cd Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Sat, 27 Jun 2026 19:11:21 -0300 Subject: [PATCH 4/6] Pin resynced satchels in place and keep them stuck to whatever they landed on Placing a satchel at its resting position via the new rest-position resync still let normal physics run on it client-side, which caused two more visible issues once it had to materialize at an arbitrary point in the world instead of being thrown into it: - The area's collision may not be streamed in yet when a player walks up to a satchel that's been sitting there for a while, so it fell through the floor under gravity until something already-loaded caught it. - A satchel stuck to a vehicle/ped only got a one-off absolute position, so it stayed behind, detached, the moment that vehicle/ped moved. The owning client now also reports what the satchel stuck to (if anything), via CProjectile::GetAttachedEntity()/GetAttachedOffsets() - the same native attach GTA already uses for satchels. The server stores that as a position relative to the attached entity (reusing the m_OriginID/m_vecOrigin mechanism CProjectileSyncPacket already has for heat-seeking rockets). On creation, a resynced satchel (recognised by zero velocity/force, which a live throw never has) is pinned with SetStaticWaitingForCollision() - the same engine flag used for script-placed objects whose collision isn't loaded yet - and natively re-attached via AttachEntityToEntity() if it was stuck to something, so it keeps following it afterwards exactly like the original. https://github.com/multitheftauto/mtasa-blue/issues/369 https://github.com/multitheftauto/mtasa-blue/issues/368 --- Client/mods/deathmatch/logic/CClientGame.cpp | 10 +++++- Client/mods/deathmatch/logic/CClientGame.h | 2 +- .../deathmatch/logic/CClientProjectile.cpp | 33 ++++++++++++++++++- .../mods/deathmatch/logic/CClientProjectile.h | 3 ++ .../logic/CClientProjectileManager.cpp | 24 +++++++++++++- .../logic/CClientProjectileManager.h | 9 ++++- .../mods/deathmatch/logic/CPacketHandler.cpp | 8 +++-- Server/mods/deathmatch/logic/CGame.cpp | 16 +++++++-- .../packets/CProjectileRestPositionPacket.cpp | 17 ++++++++++ .../packets/CProjectileRestPositionPacket.h | 1 + 10 files changed, 114 insertions(+), 9 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 74def368633..9db9226d319 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -5511,7 +5511,7 @@ void CClientGame::SendProjectileSync(CClientProjectile* pProjectile) } } -void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition) +void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition, ElementID attachedToID) { NetBitStreamInterface* pBitStream = g_pNet->AllocateNetBitStream(); if (pBitStream) @@ -5526,6 +5526,14 @@ void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVect pBitStream->Write(vecRestPosition.fY); pBitStream->Write(vecRestPosition.fZ); + if (attachedToID != INVALID_ELEMENT_ID) + { + pBitStream->WriteBit(true); + pBitStream->Write(attachedToID); + } + else + pBitStream->WriteBit(false); + g_pNet->SendPacket(PACKET_ID_PROJECTILE_REST_POSITION, pBitStream, PACKET_PRIORITY_LOW, PACKET_RELIABILITY_RELIABLE_ORDERED); g_pNet->DeallocateNetBitStream(pBitStream); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index 9888e7df3be..5a223a0eb12 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -685,7 +685,7 @@ class CClientGame std::optional vehicleBlowState = std::nullopt); void SendFireSync(CFire* pFire); void SendProjectileSync(CClientProjectile* pProjectile); - void SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition); + void SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition, ElementID attachedToID); void SetServerVersionSortable(const SString& strVersion) { m_strServerVersionSortable = strVersion; } const SString& GetServerVersionSortable() { return m_strServerVersionSortable; } diff --git a/Client/mods/deathmatch/logic/CClientProjectile.cpp b/Client/mods/deathmatch/logic/CClientProjectile.cpp index 24ee5758f67..8a7dd2191b9 100644 --- a/Client/mods/deathmatch/logic/CClientProjectile.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectile.cpp @@ -161,7 +161,13 @@ void CClientProjectile::DoPulse() { CVector vecRestPosition; GetPosition(vecRestPosition); - g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition); + + // Did it stick to a vehicle/ped? Report that too, so the resync can keep it glued to it rather than + // leaving it behind at a position that's stale the moment that vehicle/ped moves. + CClientEntity* pAttachedTo = GetSatchelAttachedTo(); + ElementID attachedToID = pAttachedTo ? pAttachedTo->GetID() : INVALID_ELEMENT_ID; + + g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition, attachedToID); } } } @@ -340,3 +346,28 @@ CClientEntity* CClientProjectile::GetSatchelAttachedTo() CPools* pPools = g_pGame->GetPools(); return pPools->GetClientEntity((DWORD*)pAttachedToSA->GetInterface()); } + +void CClientProjectile::GetSatchelAttachOffsets(CVector& vecOffsetPosition, CVector& vecOffsetRotation) +{ + if (m_pProjectile) + m_pProjectile->GetAttachedOffsets(vecOffsetPosition, vecOffsetRotation); +} + +void CClientProjectile::AttachSatchelToEntity(CClientEntity* pEntity, const CVector& vecOffsetPosition, const CVector& vecOffsetRotation) +{ + if (!m_pProjectile || !pEntity) + return; + + CPhysical* pGamePhysical = dynamic_cast(pEntity->GetGameEntity()); + if (pGamePhysical) + m_pProjectile->AttachEntityToEntity(*pGamePhysical, vecOffsetPosition, vecOffsetRotation); +} + +void CClientProjectile::SetStaticUntilCollisionLoaded() +{ + // Used when this satchel was just placed by a resync (CGame::Packet_ProjectileRestPosition) rather than thrown + // locally - the area's collision might not be streamed in yet, so without this it falls through the world + // until something already-loaded catches it (https://github.com/multitheftauto/mtasa-blue/issues/369, #368) + if (m_pProjectile) + m_pProjectile->SetStaticWaitingForCollision(true); +} diff --git a/Client/mods/deathmatch/logic/CClientProjectile.h b/Client/mods/deathmatch/logic/CClientProjectile.h index c3701249475..73ee88d9f34 100644 --- a/Client/mods/deathmatch/logic/CClientProjectile.h +++ b/Client/mods/deathmatch/logic/CClientProjectile.h @@ -93,6 +93,9 @@ class CClientProjectile final : public CClientEntity float GetForce() { return m_fForce; } bool IsLocal() { return m_bLocal; } CClientEntity* GetSatchelAttachedTo(); + void GetSatchelAttachOffsets(CVector& vecOffsetPosition, CVector& vecOffsetRotation); + void AttachSatchelToEntity(CClientEntity* pEntity, const CVector& vecOffsetPosition, const CVector& vecOffsetRotation); + void SetStaticUntilCollisionLoaded(); protected: CClientProjectileManager* m_pProjectileManager; diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp index 929727a881c..2794d1319b3 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp @@ -60,7 +60,8 @@ void CClientProjectileManager::DoPulse() } void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, - const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel) + ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, + unsigned short usModel) { SPendingProjectileCreation pending; pending.creatorID = creatorID; @@ -68,6 +69,7 @@ void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeapon pending.vecOrigin = vecOrigin; pending.fForce = fForce; pending.targetID = targetID; + pending.originSourceID = originSourceID; pending.vecRotation = vecRotation; pending.vecVelocity = vecVelocity; pending.usModel = usModel; @@ -75,6 +77,20 @@ void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeapon m_PendingCreations.push_back(pending); } +void CClientProjectileManager::SettleResyncedSatchel(CClientProjectile* pProjectile, eWeaponType weaponType, float fForce, const CVector& vecVelocity, + CClientEntity* pOriginSource) +{ + // A live throw always has some non-zero force/velocity - zero on both is only ever seen on a satchel resync + // packet (CGame::Packet_ProjectileRestPosition), which deliberately zeroes them once the satchel has settled. + if (weaponType != WEAPONTYPE_REMOTE_SATCHEL_CHARGE || fForce != 0.0f || vecVelocity != CVector()) + return; + + pProjectile->SetStaticUntilCollisionLoaded(); + + if (pOriginSource) + pProjectile->AttachSatchelToEntity(pOriginSource, CVector(), CVector()); +} + void CClientProjectileManager::ProcessPendingCreations() { if (m_PendingCreations.empty()) @@ -108,6 +124,12 @@ void CClientProjectileManager::ProcessPendingCreations() if (pProjectile) { pProjectile->Initiate(pending.vecOrigin, pending.vecRotation, pending.vecVelocity, pending.usModel); + + CClientEntity* pOriginSource = NULL; + if (pending.originSourceID != INVALID_ELEMENT_ID) + pOriginSource = CElementIDs::GetElement(pending.originSourceID); + SettleResyncedSatchel(pProjectile, pending.weaponType, pending.fForce, pending.vecVelocity, pOriginSource); + bResolved = true; } } diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.h b/Client/mods/deathmatch/logic/CClientProjectileManager.h index b68cb9779b2..44dcefd28eb 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.h +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.h @@ -46,7 +46,13 @@ class CClientProjectileManager // into range of a projectile they planted earlier while out of view). Queue the creation and keep retrying for a // while instead of silently dropping it. void QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, - const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel); + ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel); + + // Pins a satchel that was just placed by a rest-position resync (zero velocity/force) in place instead of + // letting physics run on it: the area's collision might not be streamed in yet, and if it was originally stuck + // to a vehicle/ped it should keep following it (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). + void SettleResyncedSatchel(CClientProjectile* pProjectile, eWeaponType weaponType, float fForce, const CVector& vecVelocity, + CClientEntity* pOriginSource); protected: void AddToList(CClientProjectile* pProjectile) { m_List.push_back(pProjectile); } @@ -73,6 +79,7 @@ class CClientProjectileManager CVector vecOrigin; float fForce; ElementID targetID; + ElementID originSourceID; CVector vecRotation; CVector vecVelocity; unsigned short usModel; diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index 57fdb646777..fb80c343526 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -4824,9 +4824,11 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) CClientEntity* pCreator = NULL; if (CreatorID != INVALID_ELEMENT_ID) pCreator = CElementIDs::GetElement(CreatorID); + + CClientEntity* pOriginSource = NULL; if (OriginID != INVALID_ELEMENT_ID) { - CClientEntity* pOriginSource = CElementIDs::GetElement(OriginID); + pOriginSource = CElementIDs::GetElement(OriginID); if (pOriginSource) { CVector vecTemp; @@ -4923,6 +4925,8 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) if (pProjectile) { pProjectile->Initiate(origin.data.vecPosition, rotation.data.vecRotation, velocity.data.vecVelocity, usModel); + g_pClientGame->m_pManager->GetProjectileManager()->SettleResyncedSatchel(pProjectile, weaponType, fForce, velocity.data.vecVelocity, + pOriginSource); bCreated = true; } } @@ -4934,7 +4938,7 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) { ElementID TargetID = pTargetEntity ? pTargetEntity->GetID() : INVALID_ELEMENT_ID; g_pClientGame->m_pManager->GetProjectileManager()->QueuePendingCreation(CreatorID, weaponType, origin.data.vecPosition, fForce, TargetID, - rotation.data.vecRotation, velocity.data.vecVelocity, usModel); + OriginID, rotation.data.vecRotation, velocity.data.vecVelocity, usModel); } } } diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index 138dec91338..9346c3fd6a9 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -3136,8 +3136,20 @@ void CGame::Packet_ProjectileRestPosition(CProjectileRestPositionPacket& Packet) if (!IsPointNearPoint3D(info.packet.m_vecOrigin, Packet.m_vecOrigin, 1.0f)) continue; - info.packet.m_vecOrigin = Packet.m_vecRestPosition; - info.packet.m_OriginID = INVALID_ELEMENT_ID; + // If it stuck to a vehicle/ped, store the position relative to it instead of an absolute world position, + // so a late stream-in places it correctly even if that vehicle/ped has since moved + // (m_OriginID/m_vecOrigin is the same relative-origin mechanism WEAPONTYPE_ROCKET_HS already uses). + CElement* pAttachedTo = (Packet.m_AttachedToID != INVALID_ELEMENT_ID) ? CElementIDs::GetElement(Packet.m_AttachedToID) : nullptr; + if (pAttachedTo) + { + info.packet.m_OriginID = Packet.m_AttachedToID; + info.packet.m_vecOrigin = Packet.m_vecRestPosition - pAttachedTo->GetPosition(); + } + else + { + info.packet.m_OriginID = INVALID_ELEMENT_ID; + info.packet.m_vecOrigin = Packet.m_vecRestPosition; + } info.packet.m_vecMoveSpeed = CVector(); info.packet.m_fForce = 0.0f; break; diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp index 16140f3d897..69977f95680 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp @@ -15,6 +15,7 @@ CProjectileRestPositionPacket::CProjectileRestPositionPacket() { m_ucWeaponType = 0; + m_AttachedToID = INVALID_ELEMENT_ID; } bool CProjectileRestPositionPacket::Read(NetBitStreamInterface& BitStream) @@ -28,6 +29,14 @@ bool CProjectileRestPositionPacket::Read(NetBitStreamInterface& BitStream) if (!BitStream.Read(m_vecRestPosition.fX) || !BitStream.Read(m_vecRestPosition.fY) || !BitStream.Read(m_vecRestPosition.fZ)) return false; + bool bHasAttachedTo; + if (!BitStream.ReadBit(bHasAttachedTo)) + return false; + + m_AttachedToID = INVALID_ELEMENT_ID; + if (bHasAttachedTo && !BitStream.Read(m_AttachedToID)) + return false; + return true; } @@ -43,5 +52,13 @@ bool CProjectileRestPositionPacket::Write(NetBitStreamInterface& BitStream) cons BitStream.Write(m_vecRestPosition.fY); BitStream.Write(m_vecRestPosition.fZ); + if (m_AttachedToID != INVALID_ELEMENT_ID) + { + BitStream.WriteBit(true); + BitStream.Write(m_AttachedToID); + } + else + BitStream.WriteBit(false); + return true; } diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h index 58710e10702..51a84e2547a 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h @@ -31,4 +31,5 @@ class CProjectileRestPositionPacket final : public CPacket unsigned char m_ucWeaponType; CVector m_vecOrigin; // Matches the original throw's m_vecOrigin, used to find the tracked entry CVector m_vecRestPosition; + ElementID m_AttachedToID; // INVALID_ELEMENT_ID if it didn't stick to anything }; From 89186d8906fedbe5d2a029420872131094cf8156 Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Sat, 27 Jun 2026 19:42:23 -0300 Subject: [PATCH 5/6] Carry the exact satchel attach offset through the resync, not just the entity Re-attaching a resynced satchel with a zero offset always snapped it to the attached vehicle/ped's origin, so a satchel stuck to e.g. the hood instead showed up at the vehicle's centre for anyone who streamed in late. The owning client now also reports GTA's own native attach offset (CProjectile::GetAttachedOffsets() - the same position/rotation pair AttachEntityToEntity expects) when the satchel settles, and the resync carries it through end to end: - CProjectileRestPositionPacket (client -> server) gains the offset fields. - CGame::Packet_ProjectileRestPosition stores them on the tracked entry. - CProjectileSyncPacket (server -> client) gains a satchel-only optional attach-offset block, sent only when resending a settled satchel - live throws never have one yet, so their wire format is untouched. - The receiving client's manual bitstream read (mirroring the above) and CClientGame::SendProjectileSync's own write both had to move the satchel case out of the shared GRENADE/TEARGAS/MOLOTOV switch arm to add it. https://github.com/multitheftauto/mtasa-blue/issues/369 https://github.com/multitheftauto/mtasa-blue/issues/368 --- Client/mods/deathmatch/logic/CClientGame.cpp | 27 +++++++++- Client/mods/deathmatch/logic/CClientGame.h | 3 +- .../deathmatch/logic/CClientProjectile.cpp | 16 ++++-- .../logic/CClientProjectileManager.cpp | 14 +++-- .../logic/CClientProjectileManager.h | 11 ++-- .../mods/deathmatch/logic/CPacketHandler.cpp | 39 +++++++++++++- Server/mods/deathmatch/logic/CGame.cpp | 7 +++ .../packets/CProjectileRestPositionPacket.cpp | 23 +++++++- .../packets/CProjectileRestPositionPacket.h | 4 +- .../logic/packets/CProjectileSyncPacket.cpp | 54 +++++++++++++++++++ .../logic/packets/CProjectileSyncPacket.h | 7 +++ 11 files changed, 187 insertions(+), 18 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 9db9226d319..534f8e13489 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -5467,6 +5467,18 @@ void CClientGame::SendProjectileSync(CClientProjectile* pProjectile) case WEAPONTYPE_GRENADE: case WEAPONTYPE_TEARGAS: case WEAPONTYPE_MOLOTOV: + { + SFloatSync<7, 17> projectileForce; + projectileForce.data.fValue = pProjectile->GetForce(); + pBitStream->Write(&projectileForce); + + SVelocitySync velocity; + pProjectile->GetVelocity(velocity.data.vecVelocity); + pBitStream->Write(&velocity); + + break; + } + case WEAPONTYPE_REMOTE_SATCHEL_CHARGE: { SFloatSync<7, 17> projectileForce; @@ -5477,6 +5489,10 @@ void CClientGame::SendProjectileSync(CClientProjectile* pProjectile) pProjectile->GetVelocity(velocity.data.vecVelocity); pBitStream->Write(&velocity); + // No attach offset at throw time - that's only ever added later by a server resync + // (CGame::Packet_ProjectileRestPosition) once it's known whether/where it stuck to something + pBitStream->WriteBit(false); + break; } case WEAPONTYPE_ROCKET: @@ -5511,7 +5527,8 @@ void CClientGame::SendProjectileSync(CClientProjectile* pProjectile) } } -void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition, ElementID attachedToID) +void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition, ElementID attachedToID, + const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation) { NetBitStreamInterface* pBitStream = g_pNet->AllocateNetBitStream(); if (pBitStream) @@ -5530,6 +5547,14 @@ void CClientGame::SendProjectileRestPosition(eWeaponType weaponType, const CVect { pBitStream->WriteBit(true); pBitStream->Write(attachedToID); + + pBitStream->Write(vecAttachOffsetPosition.fX); + pBitStream->Write(vecAttachOffsetPosition.fY); + pBitStream->Write(vecAttachOffsetPosition.fZ); + + pBitStream->Write(vecAttachOffsetRotation.fX); + pBitStream->Write(vecAttachOffsetRotation.fY); + pBitStream->Write(vecAttachOffsetRotation.fZ); } else pBitStream->WriteBit(false); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index 5a223a0eb12..b8a5ca88669 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -685,7 +685,8 @@ class CClientGame std::optional vehicleBlowState = std::nullopt); void SendFireSync(CFire* pFire); void SendProjectileSync(CClientProjectile* pProjectile); - void SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition, ElementID attachedToID); + void SendProjectileRestPosition(eWeaponType weaponType, const CVector& vecOrigin, const CVector& vecRestPosition, ElementID attachedToID, + const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); void SetServerVersionSortable(const SString& strVersion) { m_strServerVersionSortable = strVersion; } const SString& GetServerVersionSortable() { return m_strServerVersionSortable; } diff --git a/Client/mods/deathmatch/logic/CClientProjectile.cpp b/Client/mods/deathmatch/logic/CClientProjectile.cpp index 8a7dd2191b9..2d300a4ac62 100644 --- a/Client/mods/deathmatch/logic/CClientProjectile.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectile.cpp @@ -162,12 +162,20 @@ void CClientProjectile::DoPulse() CVector vecRestPosition; GetPosition(vecRestPosition); - // Did it stick to a vehicle/ped? Report that too, so the resync can keep it glued to it rather than - // leaving it behind at a position that's stale the moment that vehicle/ped moves. + // Did it stick to a vehicle/ped? Report that too, along with exactly where on it (GTA's own attach + // offset, e.g. the hood instead of the vehicle's centre), so the resync keeps it glued to the same + // spot instead of leaving it behind - or snapping it to the vehicle's origin - the moment it moves. CClientEntity* pAttachedTo = GetSatchelAttachedTo(); - ElementID attachedToID = pAttachedTo ? pAttachedTo->GetID() : INVALID_ELEMENT_ID; + ElementID attachedToID = INVALID_ELEMENT_ID; + CVector vecAttachOffsetPosition, vecAttachOffsetRotation; + if (pAttachedTo) + { + attachedToID = pAttachedTo->GetID(); + GetSatchelAttachOffsets(vecAttachOffsetPosition, vecAttachOffsetRotation); + } - g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition, attachedToID); + g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition, attachedToID, vecAttachOffsetPosition, + vecAttachOffsetRotation); } } } diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp index 2794d1319b3..4bfc5ae58f7 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp @@ -61,7 +61,8 @@ void CClientProjectileManager::DoPulse() void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, - unsigned short usModel) + unsigned short usModel, bool bHasAttachOffset, const CVector& vecAttachOffsetPosition, + const CVector& vecAttachOffsetRotation) { SPendingProjectileCreation pending; pending.creatorID = creatorID; @@ -73,12 +74,16 @@ void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeapon pending.vecRotation = vecRotation; pending.vecVelocity = vecVelocity; pending.usModel = usModel; + pending.bHasAttachOffset = bHasAttachOffset; + pending.vecAttachOffsetPosition = vecAttachOffsetPosition; + pending.vecAttachOffsetRotation = vecAttachOffsetRotation; pending.llCreationTime = GetTickCount64_(); m_PendingCreations.push_back(pending); } void CClientProjectileManager::SettleResyncedSatchel(CClientProjectile* pProjectile, eWeaponType weaponType, float fForce, const CVector& vecVelocity, - CClientEntity* pOriginSource) + CClientEntity* pOriginSource, const CVector& vecAttachOffsetPosition, + const CVector& vecAttachOffsetRotation) { // A live throw always has some non-zero force/velocity - zero on both is only ever seen on a satchel resync // packet (CGame::Packet_ProjectileRestPosition), which deliberately zeroes them once the satchel has settled. @@ -88,7 +93,7 @@ void CClientProjectileManager::SettleResyncedSatchel(CClientProjectile* pProject pProjectile->SetStaticUntilCollisionLoaded(); if (pOriginSource) - pProjectile->AttachSatchelToEntity(pOriginSource, CVector(), CVector()); + pProjectile->AttachSatchelToEntity(pOriginSource, vecAttachOffsetPosition, vecAttachOffsetRotation); } void CClientProjectileManager::ProcessPendingCreations() @@ -128,7 +133,8 @@ void CClientProjectileManager::ProcessPendingCreations() CClientEntity* pOriginSource = NULL; if (pending.originSourceID != INVALID_ELEMENT_ID) pOriginSource = CElementIDs::GetElement(pending.originSourceID); - SettleResyncedSatchel(pProjectile, pending.weaponType, pending.fForce, pending.vecVelocity, pOriginSource); + SettleResyncedSatchel(pProjectile, pending.weaponType, pending.fForce, pending.vecVelocity, pOriginSource, pending.vecAttachOffsetPosition, + pending.vecAttachOffsetRotation); bResolved = true; } diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.h b/Client/mods/deathmatch/logic/CClientProjectileManager.h index 44dcefd28eb..ef030a38dff 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.h +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.h @@ -46,13 +46,15 @@ class CClientProjectileManager // into range of a projectile they planted earlier while out of view). Queue the creation and keep retrying for a // while instead of silently dropping it. void QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, - ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel); + ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel, + bool bHasAttachOffset, const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); // Pins a satchel that was just placed by a rest-position resync (zero velocity/force) in place instead of // letting physics run on it: the area's collision might not be streamed in yet, and if it was originally stuck - // to a vehicle/ped it should keep following it (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). + // to a vehicle/ped it should keep following it, at the exact same spot it was stuck at, instead of snapping to + // the vehicle/ped's origin (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). void SettleResyncedSatchel(CClientProjectile* pProjectile, eWeaponType weaponType, float fForce, const CVector& vecVelocity, - CClientEntity* pOriginSource); + CClientEntity* pOriginSource, const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); protected: void AddToList(CClientProjectile* pProjectile) { m_List.push_back(pProjectile); } @@ -83,6 +85,9 @@ class CClientProjectileManager CVector vecRotation; CVector vecVelocity; unsigned short usModel; + bool bHasAttachOffset; + CVector vecAttachOffsetPosition; + CVector vecAttachOffsetRotation; long long llCreationTime; }; std::vector m_PendingCreations; diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index fb80c343526..1f3af39b8f2 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -4846,12 +4846,29 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) CClientEntity* pTargetEntity = NULL; SVelocitySync velocity; SRotationRadiansSync rotation(true); + bool bHasAttachOffset = false; + CVector vecAttachOffsetPosition, vecAttachOffsetRotation; switch (weaponType) { case WEAPONTYPE_GRENADE: case WEAPONTYPE_TEARGAS: case WEAPONTYPE_MOLOTOV: + { + // Read the force + SFloatSync<7, 17> projectileForce; + if (!bitStream.Read(&projectileForce)) + return; + fForce = projectileForce.data.fValue; + + // Read the velocity + if (!bitStream.Read(&velocity)) + return; + bCreateProjectile = true; + + break; + } + case WEAPONTYPE_REMOTE_SATCHEL_CHARGE: { // Read the force @@ -4863,6 +4880,23 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) // Read the velocity if (!bitStream.Read(&velocity)) return; + + // Only ever set on a resync (CGame::Packet_ProjectileRestPosition) - where on the entity it was stuck, + // in GTA's own native attach offset terms (e.g. the hood, not the entity's centre) + if (!bitStream.ReadBit(bHasAttachOffset)) + return; + + if (bHasAttachOffset) + { + if (!bitStream.Read(vecAttachOffsetPosition.fX) || !bitStream.Read(vecAttachOffsetPosition.fY) || + !bitStream.Read(vecAttachOffsetPosition.fZ)) + return; + + if (!bitStream.Read(vecAttachOffsetRotation.fX) || !bitStream.Read(vecAttachOffsetRotation.fY) || + !bitStream.Read(vecAttachOffsetRotation.fZ)) + return; + } + bCreateProjectile = true; break; @@ -4926,7 +4960,7 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) { pProjectile->Initiate(origin.data.vecPosition, rotation.data.vecRotation, velocity.data.vecVelocity, usModel); g_pClientGame->m_pManager->GetProjectileManager()->SettleResyncedSatchel(pProjectile, weaponType, fForce, velocity.data.vecVelocity, - pOriginSource); + pOriginSource, vecAttachOffsetPosition, vecAttachOffsetRotation); bCreated = true; } } @@ -4938,7 +4972,8 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) { ElementID TargetID = pTargetEntity ? pTargetEntity->GetID() : INVALID_ELEMENT_ID; g_pClientGame->m_pManager->GetProjectileManager()->QueuePendingCreation(CreatorID, weaponType, origin.data.vecPosition, fForce, TargetID, - OriginID, rotation.data.vecRotation, velocity.data.vecVelocity, usModel); + OriginID, rotation.data.vecRotation, velocity.data.vecVelocity, usModel, + bHasAttachOffset, vecAttachOffsetPosition, vecAttachOffsetRotation); } } } diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index 9346c3fd6a9..b039780b7e5 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -3144,11 +3144,18 @@ void CGame::Packet_ProjectileRestPosition(CProjectileRestPositionPacket& Packet) { info.packet.m_OriginID = Packet.m_AttachedToID; info.packet.m_vecOrigin = Packet.m_vecRestPosition - pAttachedTo->GetPosition(); + + // GTA's own attach offset on that entity (e.g. the hood, not its centre) - passed through as-is so the + // resync can re-attach at the exact same spot instead of snapping it to the entity's origin. + info.packet.m_bHasAttachOffset = true; + info.packet.m_vecAttachOffsetPosition = Packet.m_vecAttachOffsetPosition; + info.packet.m_vecAttachOffsetRotation = Packet.m_vecAttachOffsetRotation; } else { info.packet.m_OriginID = INVALID_ELEMENT_ID; info.packet.m_vecOrigin = Packet.m_vecRestPosition; + info.packet.m_bHasAttachOffset = false; } info.packet.m_vecMoveSpeed = CVector(); info.packet.m_fForce = 0.0f; diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp index 69977f95680..72064df7456 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp @@ -34,8 +34,19 @@ bool CProjectileRestPositionPacket::Read(NetBitStreamInterface& BitStream) return false; m_AttachedToID = INVALID_ELEMENT_ID; - if (bHasAttachedTo && !BitStream.Read(m_AttachedToID)) - return false; + if (bHasAttachedTo) + { + if (!BitStream.Read(m_AttachedToID)) + return false; + + if (!BitStream.Read(m_vecAttachOffsetPosition.fX) || !BitStream.Read(m_vecAttachOffsetPosition.fY) || + !BitStream.Read(m_vecAttachOffsetPosition.fZ)) + return false; + + if (!BitStream.Read(m_vecAttachOffsetRotation.fX) || !BitStream.Read(m_vecAttachOffsetRotation.fY) || + !BitStream.Read(m_vecAttachOffsetRotation.fZ)) + return false; + } return true; } @@ -56,6 +67,14 @@ bool CProjectileRestPositionPacket::Write(NetBitStreamInterface& BitStream) cons { BitStream.WriteBit(true); BitStream.Write(m_AttachedToID); + + BitStream.Write(m_vecAttachOffsetPosition.fX); + BitStream.Write(m_vecAttachOffsetPosition.fY); + BitStream.Write(m_vecAttachOffsetPosition.fZ); + + BitStream.Write(m_vecAttachOffsetRotation.fX); + BitStream.Write(m_vecAttachOffsetRotation.fY); + BitStream.Write(m_vecAttachOffsetRotation.fZ); } else BitStream.WriteBit(false); diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h index 51a84e2547a..e2a6fbb89c8 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h @@ -31,5 +31,7 @@ class CProjectileRestPositionPacket final : public CPacket unsigned char m_ucWeaponType; CVector m_vecOrigin; // Matches the original throw's m_vecOrigin, used to find the tracked entry CVector m_vecRestPosition; - ElementID m_AttachedToID; // INVALID_ELEMENT_ID if it didn't stick to anything + ElementID m_AttachedToID; // INVALID_ELEMENT_ID if it didn't stick to anything + CVector m_vecAttachOffsetPosition; // Only valid if m_AttachedToID is set - GTA's own attach offset (e.g. the hood, not the centre) + CVector m_vecAttachOffsetRotation; }; diff --git a/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.cpp b/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.cpp index 8387f6ed699..639b16ea548 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.cpp @@ -17,6 +17,7 @@ CProjectileSyncPacket::CProjectileSyncPacket() { m_usModel = 0; + m_bHasAttachOffset = false; } bool CProjectileSyncPacket::Read(NetBitStreamInterface& BitStream) @@ -47,6 +48,19 @@ bool CProjectileSyncPacket::Read(NetBitStreamInterface& BitStream) case 16: // WEAPONTYPE_GRENADE case 17: // WEAPONTYPE_TEARGAS case 18: // WEAPONTYPE_MOLOTOV + { + SFloatSync<7, 17> projectileForce; + if (!BitStream.Read(&projectileForce)) + return false; + m_fForce = projectileForce.data.fValue; + + SVelocitySync velocity; + if (!BitStream.Read(&velocity)) + return false; + m_vecMoveSpeed = velocity.data.vecVelocity; + + break; + } case 39: // WEAPONTYPE_REMOTE_SATCHEL_CHARGE { SFloatSync<7, 17> projectileForce; @@ -59,6 +73,20 @@ bool CProjectileSyncPacket::Read(NetBitStreamInterface& BitStream) return false; m_vecMoveSpeed = velocity.data.vecVelocity; + if (!BitStream.ReadBit(m_bHasAttachOffset)) + return false; + + if (m_bHasAttachOffset) + { + if (!BitStream.Read(m_vecAttachOffsetPosition.fX) || !BitStream.Read(m_vecAttachOffsetPosition.fY) || + !BitStream.Read(m_vecAttachOffsetPosition.fZ)) + return false; + + if (!BitStream.Read(m_vecAttachOffsetRotation.fX) || !BitStream.Read(m_vecAttachOffsetRotation.fY) || + !BitStream.Read(m_vecAttachOffsetRotation.fZ)) + return false; + } + break; } case 19: // WEAPONTYPE_ROCKET @@ -131,6 +159,17 @@ bool CProjectileSyncPacket::Write(NetBitStreamInterface& BitStream) const case 16: // WEAPONTYPE_GRENADE case 17: // WEAPONTYPE_TEARGAS case 18: // WEAPONTYPE_MOLOTOV + { + SFloatSync<7, 17> projectileForce; + projectileForce.data.fValue = m_fForce; + BitStream.Write(&projectileForce); + + SVelocitySync velocity; + velocity.data.vecVelocity = m_vecMoveSpeed; + BitStream.Write(&velocity); + + break; + } case 39: // WEAPONTYPE_REMOTE_SATCHEL_CHARGE { SFloatSync<7, 17> projectileForce; @@ -141,6 +180,21 @@ bool CProjectileSyncPacket::Write(NetBitStreamInterface& BitStream) const velocity.data.vecVelocity = m_vecMoveSpeed; BitStream.Write(&velocity); + if (m_bHasAttachOffset) + { + BitStream.WriteBit(true); + + BitStream.Write(m_vecAttachOffsetPosition.fX); + BitStream.Write(m_vecAttachOffsetPosition.fY); + BitStream.Write(m_vecAttachOffsetPosition.fZ); + + BitStream.Write(m_vecAttachOffsetRotation.fX); + BitStream.Write(m_vecAttachOffsetRotation.fY); + BitStream.Write(m_vecAttachOffsetRotation.fZ); + } + else + BitStream.WriteBit(false); + break; } case 19: // WEAPONTYPE_ROCKET diff --git a/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.h b/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.h index 678a341c780..94ed7ab5e83 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.h +++ b/Server/mods/deathmatch/logic/packets/CProjectileSyncPacket.h @@ -35,4 +35,11 @@ class CProjectileSyncPacket final : public CPacket CVector m_vecRotation; CVector m_vecMoveSpeed; unsigned short m_usModel; + + // Only ever set on a satchel resync (CGame::Packet_ProjectileRestPosition) when it was stuck to m_OriginID - + // GTA's own native attach offset on that entity (e.g. the hood, not its centre), so the receiving client can + // re-attach it at the exact same spot instead of the entity's origin (https://github.com/multitheftauto/mtasa-blue/issues/369, #368) + bool m_bHasAttachOffset; + CVector m_vecAttachOffsetPosition; + CVector m_vecAttachOffsetRotation; }; From d61d6c9f64cc2906981f29b1889ce7c234fc2333 Mon Sep 17 00:00:00 2001 From: Federico Romero Date: Sat, 27 Jun 2026 19:51:05 -0300 Subject: [PATCH 6/6] Apply clang-format --- Client/mods/deathmatch/logic/CClientProjectile.cpp | 2 +- .../deathmatch/logic/CClientProjectileManager.cpp | 7 +++---- .../mods/deathmatch/logic/CClientProjectileManager.h | 10 +++++----- Client/mods/deathmatch/logic/CPacketHandler.cpp | 10 ++++------ Server/mods/deathmatch/logic/CGame.cpp | 11 +++++------ Server/mods/deathmatch/logic/CGame.h | 2 +- Server/mods/deathmatch/logic/CPlayer.h | 4 ++-- .../logic/packets/CProjectileRestPositionPacket.cpp | 6 ++---- .../logic/packets/CProjectileRestPositionPacket.h | 4 ++-- 9 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientProjectile.cpp b/Client/mods/deathmatch/logic/CClientProjectile.cpp index 2d300a4ac62..a1433645cb8 100644 --- a/Client/mods/deathmatch/logic/CClientProjectile.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectile.cpp @@ -175,7 +175,7 @@ void CClientProjectile::DoPulse() } g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition, attachedToID, vecAttachOffsetPosition, - vecAttachOffsetRotation); + vecAttachOffsetRotation); } } } diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp index 4bfc5ae58f7..eb7ad2aea4f 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp @@ -60,9 +60,8 @@ void CClientProjectileManager::DoPulse() } void CClientProjectileManager::QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, - ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, - unsigned short usModel, bool bHasAttachOffset, const CVector& vecAttachOffsetPosition, - const CVector& vecAttachOffsetRotation) + ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel, + bool bHasAttachOffset, const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation) { SPendingProjectileCreation pending; pending.creatorID = creatorID; @@ -104,7 +103,7 @@ void CClientProjectileManager::ProcessPendingCreations() // Generous timeout: the creator's ped/vehicle just needs to come within the game's own streaming distance, // which can take a while if whoever it belongs to is approaching on foot from the edge of sync range. constexpr long long PENDING_CREATION_TIMEOUT = 60000; - const long long llNow = GetTickCount64_(); + const long long llNow = GetTickCount64_(); for (auto iter = m_PendingCreations.begin(); iter != m_PendingCreations.end();) { diff --git a/Client/mods/deathmatch/logic/CClientProjectileManager.h b/Client/mods/deathmatch/logic/CClientProjectileManager.h index ef030a38dff..2a032fe6c9a 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.h +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.h @@ -45,16 +45,16 @@ class CClientProjectileManager // The creator's in-game ped/vehicle may not have streamed in yet (e.g. they just connected, or we only just came // into range of a projectile they planted earlier while out of view). Queue the creation and keep retrying for a // while instead of silently dropping it. - void QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, - ElementID originSourceID, const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel, - bool bHasAttachOffset, const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); + void QueuePendingCreation(ElementID creatorID, eWeaponType eWeapon, const CVector& vecOrigin, float fForce, ElementID targetID, ElementID originSourceID, + const CVector& vecRotation, const CVector& vecVelocity, unsigned short usModel, bool bHasAttachOffset, + const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); // Pins a satchel that was just placed by a rest-position resync (zero velocity/force) in place instead of // letting physics run on it: the area's collision might not be streamed in yet, and if it was originally stuck // to a vehicle/ped it should keep following it, at the exact same spot it was stuck at, instead of snapping to // the vehicle/ped's origin (https://github.com/multitheftauto/mtasa-blue/issues/369, #368). - void SettleResyncedSatchel(CClientProjectile* pProjectile, eWeaponType weaponType, float fForce, const CVector& vecVelocity, - CClientEntity* pOriginSource, const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); + void SettleResyncedSatchel(CClientProjectile* pProjectile, eWeaponType weaponType, float fForce, const CVector& vecVelocity, CClientEntity* pOriginSource, + const CVector& vecAttachOffsetPosition, const CVector& vecAttachOffsetRotation); protected: void AddToList(CClientProjectile* pProjectile) { m_List.push_back(pProjectile); } diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index 1f3af39b8f2..79c5f32eb5a 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -4888,12 +4888,10 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) if (bHasAttachOffset) { - if (!bitStream.Read(vecAttachOffsetPosition.fX) || !bitStream.Read(vecAttachOffsetPosition.fY) || - !bitStream.Read(vecAttachOffsetPosition.fZ)) + if (!bitStream.Read(vecAttachOffsetPosition.fX) || !bitStream.Read(vecAttachOffsetPosition.fY) || !bitStream.Read(vecAttachOffsetPosition.fZ)) return; - if (!bitStream.Read(vecAttachOffsetRotation.fX) || !bitStream.Read(vecAttachOffsetRotation.fY) || - !bitStream.Read(vecAttachOffsetRotation.fZ)) + if (!bitStream.Read(vecAttachOffsetRotation.fX) || !bitStream.Read(vecAttachOffsetRotation.fY) || !bitStream.Read(vecAttachOffsetRotation.fZ)) return; } @@ -4971,8 +4969,8 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) if (!bCreated && CreatorID != INVALID_ELEMENT_ID) { ElementID TargetID = pTargetEntity ? pTargetEntity->GetID() : INVALID_ELEMENT_ID; - g_pClientGame->m_pManager->GetProjectileManager()->QueuePendingCreation(CreatorID, weaponType, origin.data.vecPosition, fForce, TargetID, - OriginID, rotation.data.vecRotation, velocity.data.vecVelocity, usModel, + g_pClientGame->m_pManager->GetProjectileManager()->QueuePendingCreation(CreatorID, weaponType, origin.data.vecPosition, fForce, TargetID, OriginID, + rotation.data.vecRotation, velocity.data.vecVelocity, usModel, bHasAttachOffset, vecAttachOffsetPosition, vecAttachOffsetRotation); } } diff --git a/Server/mods/deathmatch/logic/CGame.cpp b/Server/mods/deathmatch/logic/CGame.cpp index b039780b7e5..f5273f0d4d5 100644 --- a/Server/mods/deathmatch/logic/CGame.cpp +++ b/Server/mods/deathmatch/logic/CGame.cpp @@ -3036,8 +3036,8 @@ void CGame::Packet_ProjectileSync(CProjectileSyncPacket& Packet) // (expiryTime left at zero) are only ever removed explicitly, since they have no lifespan of their own. constexpr unsigned long PERSISTENT_PROJECTILE_LIFETIME = 20000; - bool bIsPersistentProjectile = false; - CTickCount expiryTime; // Zero means "doesn't expire on its own" + bool bIsPersistentProjectile = false; + CTickCount expiryTime; // Zero means "doesn't expire on its own" if (Packet.m_ucWeaponType == WEAPONTYPE_REMOTE_SATCHEL_CHARGE) bIsPersistentProjectile = true; else if (Packet.m_ucWeaponType == WEAPONTYPE_TEARGAS || Packet.m_ucWeaponType == WEAPONTYPE_MOLOTOV) @@ -3076,10 +3076,9 @@ void CGame::ProcessProjectileStreamIn() continue; // Drop expired entries (teargas/molotov) before doing any range checks - projectilesList.erase(std::remove_if(projectilesList.begin(), projectilesList.end(), - [&](const CPlayer::SPersistentProjectileInfo& info) - { return info.expiryTime != CTickCount() && now >= info.expiryTime; }), - projectilesList.end()); + projectilesList.erase(std::remove_if(projectilesList.begin(), projectilesList.end(), [&](const CPlayer::SPersistentProjectileInfo& info) + { return info.expiryTime != CTickCount() && now >= info.expiryTime; }), + projectilesList.end()); for (auto& projectileInfo : projectilesList) { diff --git a/Server/mods/deathmatch/logic/CGame.h b/Server/mods/deathmatch/logic/CGame.h index 484519e1e6e..daf0bd242ae 100644 --- a/Server/mods/deathmatch/logic/CGame.h +++ b/Server/mods/deathmatch/logic/CGame.h @@ -560,7 +560,7 @@ class CGame #ifdef WITH_OBJECT_SYNC CObjectSync* m_pObjectSync; #endif - CElapsedTime m_ProjectileStreamInTimer; + CElapsedTime m_ProjectileStreamInTimer; CMarkerManager* m_pMarkerManager; CClock* m_pClock; CBanManager* m_pBanManager; diff --git a/Server/mods/deathmatch/logic/CPlayer.h b/Server/mods/deathmatch/logic/CPlayer.h index bc828c75a5e..a474c099601 100644 --- a/Server/mods/deathmatch/logic/CPlayer.h +++ b/Server/mods/deathmatch/logic/CPlayer.h @@ -172,9 +172,9 @@ class CPlayer final : public CPed, public CClient // player, kept so players who come into range later still see them (https://github.com/multitheftauto/mtasa-blue/issues/369, #368) struct SPersistentProjectileInfo { - CProjectileSyncPacket packet; // Original creation packet data (source element re-applied on resend) + CProjectileSyncPacket packet; // Original creation packet data (source element re-applied on resend) std::unordered_set notifiedPlayers; - CTickCount expiryTime; // Default (zero) means it never expires on its own (e.g. satchels, cleared explicitly instead) + CTickCount expiryTime; // Default (zero) means it never expires on its own (e.g. satchels, cleared explicitly instead) }; std::vector& GetPersistentProjectilesList() { return m_PersistentProjectilesList; }; diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp index 72064df7456..39a86eca987 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp @@ -39,12 +39,10 @@ bool CProjectileRestPositionPacket::Read(NetBitStreamInterface& BitStream) if (!BitStream.Read(m_AttachedToID)) return false; - if (!BitStream.Read(m_vecAttachOffsetPosition.fX) || !BitStream.Read(m_vecAttachOffsetPosition.fY) || - !BitStream.Read(m_vecAttachOffsetPosition.fZ)) + if (!BitStream.Read(m_vecAttachOffsetPosition.fX) || !BitStream.Read(m_vecAttachOffsetPosition.fY) || !BitStream.Read(m_vecAttachOffsetPosition.fZ)) return false; - if (!BitStream.Read(m_vecAttachOffsetRotation.fX) || !BitStream.Read(m_vecAttachOffsetRotation.fY) || - !BitStream.Read(m_vecAttachOffsetRotation.fZ)) + if (!BitStream.Read(m_vecAttachOffsetRotation.fX) || !BitStream.Read(m_vecAttachOffsetRotation.fY) || !BitStream.Read(m_vecAttachOffsetRotation.fZ)) return false; } diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h index e2a6fbb89c8..322f2453a8d 100644 --- a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h @@ -29,9 +29,9 @@ class CProjectileRestPositionPacket final : public CPacket bool Write(NetBitStreamInterface& BitStream) const; unsigned char m_ucWeaponType; - CVector m_vecOrigin; // Matches the original throw's m_vecOrigin, used to find the tracked entry + CVector m_vecOrigin; // Matches the original throw's m_vecOrigin, used to find the tracked entry CVector m_vecRestPosition; - ElementID m_AttachedToID; // INVALID_ELEMENT_ID if it didn't stick to anything + ElementID m_AttachedToID; // INVALID_ELEMENT_ID if it didn't stick to anything CVector m_vecAttachOffsetPosition; // Only valid if m_AttachedToID is set - GTA's own attach offset (e.g. the hood, not the centre) CVector m_vecAttachOffsetRotation; };