diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6f2794443e2..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,6 +5527,44 @@ void CClientGame::SendProjectileSync(CClientProjectile* pProjectile) } } +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) + { + 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); + + if (attachedToID != INVALID_ELEMENT_ID) + { + 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); + + 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..b8a5ca88669 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -685,6 +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, + 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 048334da6ad..a1433645cb8 100644 --- a/Client/mods/deathmatch/logic/CClientProjectile.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectile.cpp @@ -154,6 +154,29 @@ 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); + + // 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 = INVALID_ELEMENT_ID; + CVector vecAttachOffsetPosition, vecAttachOffsetRotation; + if (pAttachedTo) + { + attachedToID = pAttachedTo->GetID(); + GetSatchelAttachOffsets(vecAttachOffsetPosition, vecAttachOffsetRotation); + } + + g_pClientGame->SendProjectileRestPosition(GetWeaponType(), *GetOrigin(), vecRestPosition, attachedToID, vecAttachOffsetPosition, + vecAttachOffsetRotation); + } } } @@ -331,3 +354,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 862ebc68c40..eb7ad2aea4f 100644 --- a/Client/mods/deathmatch/logic/CClientProjectileManager.cpp +++ b/Client/mods/deathmatch/logic/CClientProjectileManager.cpp @@ -55,6 +55,95 @@ void CClientProjectileManager::DoPulse() pProjectile->DoPulse(); } } + + ProcessPendingCreations(); +} + +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) +{ + SPendingProjectileCreation pending; + pending.creatorID = creatorID; + pending.weaponType = eWeapon; + pending.vecOrigin = vecOrigin; + pending.fForce = fForce; + pending.targetID = targetID; + pending.originSourceID = originSourceID; + 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, 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. + if (weaponType != WEAPONTYPE_REMOTE_SATCHEL_CHARGE || fForce != 0.0f || vecVelocity != CVector()) + return; + + pProjectile->SetStaticUntilCollisionLoaded(); + + if (pOriginSource) + pProjectile->AttachSatchelToEntity(pOriginSource, vecAttachOffsetPosition, vecAttachOffsetRotation); +} + +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); + + CClientEntity* pOriginSource = NULL; + if (pending.originSourceID != INVALID_ELEMENT_ID) + pOriginSource = CElementIDs::GetElement(pending.originSourceID); + SettleResyncedSatchel(pProjectile, pending.weaponType, pending.fForce, pending.vecVelocity, pOriginSource, pending.vecAttachOffsetPosition, + pending.vecAttachOffsetRotation); + + 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..2a032fe6c9a 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,20 @@ 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, 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); + protected: void AddToList(CClientProjectile* pProjectile) { m_List.push_back(pProjectile); } void RemoveFromList(CClientProjectile* pProjectile); @@ -48,6 +63,8 @@ class CClientProjectileManager void TakeOutTheTrash(); private: + void ProcessPendingCreations(); + CClientManager* m_pManager; std::list m_List; @@ -56,4 +73,22 @@ class CClientProjectileManager bool m_bCreating; CClientProjectilePtr m_pLastCreated; + + struct SPendingProjectileCreation + { + ElementID creatorID; + eWeaponType weaponType; + CVector vecOrigin; + float fForce; + ElementID targetID; + ElementID originSourceID; + 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 b46c7497c31..79c5f32eb5a 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; @@ -4844,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 @@ -4861,6 +4880,21 @@ 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; @@ -4908,6 +4942,7 @@ void CPacketHandler::Packet_ProjectileSync(NetBitStreamInterface& bitStream) if (bCreateProjectile) { + bool bCreated = false; if (pCreator) { if (pCreator->GetType() == CCLIENTPED || pCreator->GetType() == CCLIENTPLAYER) @@ -4922,8 +4957,22 @@ 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, vecAttachOffsetPosition, vecAttachOffsetRotation); + 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, 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 bdccfaea790..f5273f0d4d5 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();); @@ -1250,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)); @@ -2818,6 +2823,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 +2838,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 +3027,138 @@ 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); + } + } + } + } +} + +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; + + // 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(); + + // 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; + break; } } diff --git a/Server/mods/deathmatch/logic/CGame.h b/Server/mods/deathmatch/logic/CGame.h index 9d0ace7b1aa..daf0bd242ae 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,8 @@ 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); void Packet_VehiclePuresync(class CVehiclePuresyncPacket& Packet); @@ -557,6 +560,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/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/CPlayer.h b/Server/mods/deathmatch/logic/CPlayer.h index b332f40eb12..a474c099601 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; diff --git a/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp new file mode 100644 index 00000000000..39a86eca987 --- /dev/null +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.cpp @@ -0,0 +1,81 @@ +/***************************************************************************** + * + * 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; + m_AttachedToID = INVALID_ELEMENT_ID; +} + +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; + + bool bHasAttachedTo; + if (!BitStream.ReadBit(bHasAttachedTo)) + return false; + + m_AttachedToID = INVALID_ELEMENT_ID; + 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; +} + +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); + + if (m_AttachedToID != INVALID_ELEMENT_ID) + { + 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); + + 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..322f2453a8d --- /dev/null +++ b/Server/mods/deathmatch/logic/packets/CProjectileRestPositionPacket.h @@ -0,0 +1,37 @@ +/***************************************************************************** + * + * 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; + 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; }; 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,