diff --git a/client/src/CWorldObjectSync.cpp b/client/src/CWorldObjectSync.cpp new file mode 100644 index 00000000..f55236a6 --- /dev/null +++ b/client/src/CWorldObjectSync.cpp @@ -0,0 +1,508 @@ +#include "stdafx.h" +#include "CWorldObjectSync.h" +#include "CNetworkPlayerManager.h" +#include "CNetworkVehicleManager.h" +#include "CNetworkPedManager.h" +#include +#include + +using namespace Packets::World; + +std::vector CWorldObjectSync::ms_slots; +CWorldObjectSync::KeyMap CWorldObjectSync::ms_slotByKey; +CWorldObjectSync::StateMap CWorldObjectSync::ms_states; + +constexpr uint8_t OBJECT_GAME = 1; // eObjectType: made from a map dummy + +static CVector Cross(const CVector& a, const CVector& b) +{ + return CVector(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); +} + +static float Dot(const CVector& a, const CVector& b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z; +} + +static WorldObjectKey MakeKey(CDummyObject* pDummy) +{ + const CVector& pos = pDummy->GetPosition(); + WorldObjectKey key{}; + key.model = static_cast(pDummy->m_nModelIndex); + key.x = static_cast(std::lround(pos.x * 100.0f)); + key.y = static_cast(std::lround(pos.y * 100.0f)); + key.z = static_cast(std::lround(pos.z * 100.0f)); + return key; +} + +// the dummy's own placement: GetMatrix() would allocate a matrix for it +static WorldObjectTransform GetDummyTransform(CDummyObject* pDummy) +{ + WorldObjectTransform transform{}; + transform.pos = pDummy->GetPosition(); + if (pDummy->m_matrix) + { + transform.right = pDummy->m_matrix->right; + transform.forward = pDummy->m_matrix->up; + } + else + { + float heading = pDummy->m_placement.m_fHeading; + transform.right = CVector(std::cos(heading), std::sin(heading), 0.0f); + transform.forward = CVector(-std::sin(heading), std::cos(heading), 0.0f); + } + return transform; +} + +static WorldObjectTransform GetTransform(CObject* pObject) +{ + WorldObjectTransform transform{}; + transform.pos = pObject->GetPosition(); + transform.right = pObject->m_matrix->right; + transform.forward = pObject->m_matrix->up; + return transform; +} + +static bool IsDisplaced(const WorldObjectTransform& transform, const WorldObjectTransform& original) +{ + CVector delta = transform.pos - original.pos; + return Dot(delta, delta) > 0.05f * 0.05f || Dot(transform.right, original.right) < 0.999f || + Dot(transform.forward, original.forward) < 0.999f; +} + +static uint8_t CollectDamage(CObject* pObject) +{ + uint8_t flags = 0; + if (pObject->m_bRenderDamaged) + { + flags |= WorldObjectState::DAMAGED; + } + if (!pObject->m_bUsesCollision && !pObject->m_bIsVisible) + { + flags |= WorldObjectState::SMASHED; + } + if (pObject->m_nObjectFlags.bIsBroken) + { + flags |= WorldObjectState::BROKEN; + } + if (pObject->m_nObjectFlags.bIsExploded) + { + flags |= WorldObjectState::EXPLODED; + } + return flags; +} + +static void SetTransform(CObject* pObject, const WorldObjectTransform& transform, bool bStatic) +{ + // like CObject::Teleport, with the rotation: the sectors and the moving list follow the new matrix + CWorld::Remove(pObject); + CMatrix& matrix = *pObject->m_matrix; + matrix.right = transform.right; + matrix.up = transform.forward; + matrix.at = Cross(transform.right, transform.forward); + matrix.pos = transform.pos; + pObject->SetIsStatic(bStatic); + if (bStatic) + { + pObject->m_vecMoveSpeed = CVector(); + pObject->m_vecTurnSpeed = CVector(); + } + CWorld::Add(pObject); +} + +// the same CObject::ObjectDamage the other player had: the effects, the pieces, the explosion of a petrol pump +static void ReplayDamage(CObject* pObject) +{ + auto& flags = pObject->m_nPhysicalFlags; + auto saved = flags; + flags.bBulletProof = flags.bFireProof = flags.bCollisionProof = flags.bMeleeProof = flags.bInvulnerable = + flags.bExplosionProof = false; + + CVector origin = pObject->GetPosition(); + CVector direction(0.0f, 0.0f, 1.0f); + constexpr auto WEAPON_RAMMEDBYCAR = static_cast(49); // not in the plugin-sdk enum + pObject->ObjectDamage(10000.0f, &origin, &direction, FindPlayerPed(0), WEAPON_RAMMEDBYCAR); + + // ObjectDamage sets some of them itself (explosion proof after a smash) + flags.bBulletProof |= saved.bBulletProof; + flags.bFireProof |= saved.bFireProof; + flags.bCollisionProof |= saved.bCollisionProof; + flags.bMeleeProof |= saved.bMeleeProof; + flags.bInvulnerable |= saved.bInvulnerable; + flags.bExplosionProof |= saved.bExplosionProof; +} + +// the end state of CObject::ObjectDamage without the effects: an object made just now from its dummy +static void ForceDamage(CObject* pObject, uint8_t flags) +{ + if ((flags & WorldObjectState::DAMAGED) && !pObject->m_bRenderDamaged) + { + // the renderer makes the damaged model, as after COL_DAMAGE_EFFECT_CHANGE_MODEL + pObject->DeleteRwObject(); + pObject->m_bRenderDamaged = true; + } + + if ((flags & WorldObjectState::SMASHED) && (pObject->m_bUsesCollision || pObject->m_bIsVisible)) + { + pObject->m_bUsesCollision = false; + pObject->m_bIsVisible = false; + if (!pObject->m_bIsStatic) + { + pObject->RemoveFromMovingList(); + } + pObject->SetIsStatic(true); + pObject->m_nPhysicalFlags.bExplosionProof = true; + pObject->m_vecMoveSpeed = CVector(); + pObject->m_vecTurnSpeed = CVector(); + pObject->DeleteRwObject(); + pObject->m_fHealth = 0.0f; + } + + if (flags & WorldObjectState::BROKEN) + { + pObject->m_nObjectFlags.bIsBroken = true; + } + if (flags & WorldObjectState::EXPLODED) + { + pObject->m_nObjectFlags.bIsExploded = true; + } +} + +void CWorldObjectSync::Reset() +{ + ms_slots.clear(); + ms_slotByKey.clear(); + ms_states.clear(); +} + +CWorldObjectSync::SSlot* CWorldObjectSync::FindSlot(const WorldObjectKey& key) +{ + auto it = ms_slotByKey.find(key); + return it != ms_slotByKey.end() ? &ms_slots[it->second] : nullptr; +} + +void CWorldObjectSync::Remember(const WorldObjectKey& key, bool bTransform, const WorldObjectState& state) +{ + WorldObjectState& known = ms_states[key]; + known.flags |= state.flags & WorldObjectState::DAMAGE_MASK; + if (bTransform) + { + known.flags = (known.flags & ~WorldObjectState::MOVED) | (state.flags & WorldObjectState::MOVED); + known.transform = state.transform; + } + + if (known.flags == 0) + { + ms_states.erase(key); + } +} + +void CWorldObjectSync::SendState(const SSlot& slot, bool bTransform, uint8_t flags) +{ + WorldObjectStates packet{}; + packet.live = true; + packet.count = 1; + auto& entry = packet.entries[0]; + entry.key = slot.key; + entry.hasTransform = bTransform; + entry.state.flags = flags; + entry.state.transform = slot.lastTransform; + GetPacketFactory().Send(packet); + logger::info("world objects: sent %d at %d %d %d, flags %d%s", slot.key.model, slot.key.x, slot.key.y, + slot.key.z, flags, bTransform ? ", where it stopped" : ""); + + Remember(slot.key, bTransform, entry.state); +} + +void CWorldObjectSync::Apply(SSlot& slot, const WorldObjectState& state, bool bLive, bool bTransform) +{ + CObject* pObject = slot.pObject; + uint8_t target = state.flags & WorldObjectState::DAMAGE_MASK; + logger::info("world objects: %s %d at %d %d %d, flags %d", bLive ? "live" : "applied", slot.key.model, slot.key.x, + slot.key.y, slot.key.z, state.flags); + constexpr uint8_t BY_DAMAGE = WorldObjectState::DAMAGED | WorldObjectState::SMASHED | WorldObjectState::BROKEN; + + if (bLive) + { + // COL_DAMAGE_EFFECT_CHANGE_THEN_SMASH takes two hits + for (int i = 0; i < 2 && (target & ~CollectDamage(pObject) & BY_DAMAGE); i++) + { + ReplayDamage(pObject); + } + + if ((target & WorldObjectState::EXPLODED) && !pObject->m_nObjectFlags.bIsExploded) + { + pObject->TryToExplode(); + } + } + ForceDamage(pObject, target); + + if (bTransform) + { + if (state.flags & WorldObjectState::MOVED) + { + SetTransform(pObject, state.transform, true); + } + else if (IsDisplaced(GetTransform(pObject), slot.original)) + { + SetTransform(pObject, slot.original, true); + } + } + + slot.flags = CollectDamage(pObject); + // a damage-only state leaves the motion alone: whoever moves the object still sends where it stops + if (bTransform) + { + slot.bMoving = !pObject->m_bIsStatic; + slot.bMover = false; + slot.remoteUntil = GetTickCount() + REMOTE_REST_TIME; + } +} + +bool CWorldObjectSync::ShouldBecomeMover(CObject* pObject) +{ + CPlayerPed* pLocalPed = FindPlayerPed(0); + if (pLocalPed == nullptr) + { + return false; + } + + // who hit it (only compared, it may point to a deleted entity). A clone hits the object here too, its owner + // sends the motion + CEntity* pDamager = pObject->m_pDamageEntity; + if (pDamager != nullptr) + { + if (pDamager == pLocalPed || (pLocalPed->m_nPedFlags.bInVehicle && pDamager == pLocalPed->m_pVehicle)) + { + return true; + } + if (CNetworkVehicle* pNetworkVehicle = CNetworkVehicleManager::GetVehicle(pDamager)) + { + return pNetworkVehicle->m_bSyncing; + } + if (CNetworkPed* pNetworkPed = CNetworkPedManager::GetPed(pDamager)) + { + return pNetworkPed->m_bSyncing; + } + if (CNetworkPlayerManager::GetPlayer(pDamager) != nullptr) + { + return false; + } + } + + // an explosion, another object, a thrown thing: it moves here, maybe only here. If it moves for another player + // too, the lower player id keeps it (HandlePacket) + return true; +} + +void CWorldObjectSync::OnObjectAppeared(SSlot& slot) +{ + CObject* pObject = slot.pObject; + slot.key = MakeKey(pObject->m_pDummyObject); + slot.original = GetDummyTransform(pObject->m_pDummyObject); + slot.flags = CollectDamage(pObject); + slot.bMoving = !pObject->m_bIsStatic; + slot.bMover = false; + slot.remoteUntil = 0; + ms_slotByKey[slot.key] = static_cast(&slot - ms_slots.data()); + + // made from its dummy this frame, before it is rendered: as the others see it + auto it = ms_states.find(slot.key); + if (it != ms_states.end()) + { + Apply(slot, it->second, false, true); + } +} + +void CWorldObjectSync::OnObjectGone(SSlot& slot) +{ + // turned back into its dummy on the way: where we saw it last is where it stays + if (slot.bMover) + { + uint8_t flags = slot.flags; + if (IsDisplaced(slot.lastTransform, slot.original)) + { + flags |= WorldObjectState::MOVED; + } + SendState(slot, true, flags); + } + + auto it = ms_slotByKey.find(slot.key); + if (it != ms_slotByKey.end() && &ms_slots[it->second] == &slot) + { + ms_slotByKey.erase(it); + } + slot = SSlot{}; +} + +void CWorldObjectSync::ProcessSlot(SSlot& slot, uint32_t now) +{ + CObject* pObject = slot.pObject; + + uint8_t flags = CollectDamage(pObject); + if (flags != slot.flags) + { + // anyone may report it: the server only adds the damage up + slot.flags = flags; + SendState(slot, false, flags); + } + + bool bMoving = !pObject->m_bIsStatic; + bool bWasMoving = slot.bMoving; + slot.bMoving = bMoving; + + if (now < slot.remoteUntil) + { + return; // our physics plays the motion the mover sent + } + + if (bMoving && !bWasMoving) + { + // doors swing back, buoys float, held boxes go with their holder + bool bOwnMotion = pObject->m_nPhysicalFlags.bDisableMoveForce || pObject->m_nPhysicalFlags.bTouchingWater || + pObject->m_nPhysicalFlags.bAttachedToEntity || pObject->m_pAttachedTo != nullptr; + slot.bMover = !bOwnMotion && ShouldBecomeMover(pObject); + slot.lastMotionTime = 0; + if (slot.bMover) + { + logger::info("world objects: we move %d at %d %d %d", slot.key.model, slot.key.x, slot.key.y, slot.key.z); + } + } + + if (!slot.bMover) + { + return; + } + + slot.lastTransform = GetTransform(pObject); + + if (bMoving) + { + if (now - slot.lastMotionTime >= MOTION_INTERVAL) + { + slot.lastMotionTime = now; + + WorldObjectMotion packet{}; + packet.key = slot.key; + packet.transform = slot.lastTransform; + packet.moveSpeed = pObject->m_vecMoveSpeed; + packet.turnSpeed = pObject->m_vecTurnSpeed; + GetPacketFactory().Send(packet); + } + return; + } + + // came to rest + slot.bMover = false; + uint8_t state = slot.flags; + if (IsDisplaced(slot.lastTransform, slot.original)) + { + state |= WorldObjectState::MOVED; + } + SendState(slot, true, state); +} + +void CWorldObjectSync::Process() +{ + auto* pPool = CPools::ms_pObjectPool; + if (pPool == nullptr) + { + return; + } + + if (ms_slots.size() != static_cast(pPool->m_nSize)) + { + ms_slots.assign(pPool->m_nSize, SSlot{}); + ms_slotByKey.clear(); + } + + uint32_t now = GetTickCount(); + for (int i = 0; i < pPool->m_nSize; i++) + { + SSlot& slot = ms_slots[i]; + CObject* pObject = pPool->GetAt(i); + + // OBJECT_GAME with its dummy: a map object. Mission objects are the scripts', pieces and pickups are local + bool bMapObject = pObject != nullptr && pObject->m_nObjectType == OBJECT_GAME && pObject->m_pDummyObject && + pObject->m_matrix; + int ref = bMapObject ? CPools::GetObjectRef(pObject) + 1 : 0; + + if (slot.ref != 0 && slot.ref != ref) + { + OnObjectGone(slot); + } + + if (!bMapObject) + { + continue; + } + + if (slot.ref == 0) + { + slot.ref = ref; + slot.pObject = pObject; + OnObjectAppeared(slot); + continue; + } + + ProcessSlot(slot, now); + } +} + +void CWorldObjectSync::HandlePacket(WorldObjectStates* pPacket) +{ + for (int i = 0; i < pPacket->count; i++) + { + const auto& entry = pPacket->entries[i]; + if (entry.state.flags == 0) + { + ms_states.erase(entry.key); + } + else + { + Remember(entry.key, entry.hasTransform, entry.state); + } + + SSlot* pSlot = FindSlot(entry.key); + if (pSlot == nullptr) + { + continue; // still a dummy here, gets the state when it becomes an object + } + + if (entry.state.flags == 0) + { + // forgotten (nobody was near) - nothing to undo on a living object; pushed back - put it back + if (pPacket->live) + { + Apply(*pSlot, entry.state, false, true); + } + continue; + } + + Apply(*pSlot, entry.state, pPacket->live, entry.hasTransform); + } +} + +void CWorldObjectSync::HandlePacket(WorldObjectMotion* pPacket) +{ + SSlot* pSlot = FindSlot(pPacket->key); + if (pSlot == nullptr || !pSlot->pObject->m_bUsesCollision) + { + return; + } + + // both of us started it: the lower id keeps it + if (pSlot->bMover && CNetworkPlayerManager::m_localPlayerId < pPacket->mover) + { + return; + } + pSlot->bMover = false; + + CObject* pObject = pSlot->pObject; + SetTransform(pObject, pPacket->transform, false); + pObject->m_vecMoveSpeed = pPacket->moveSpeed; + pObject->m_vecTurnSpeed = pPacket->turnSpeed; + + pSlot->bMoving = true; + pSlot->remoteUntil = GetTickCount() + REMOTE_MOTION_TIME; +} diff --git a/client/src/CWorldObjectSync.h b/client/src/CWorldObjectSync.h new file mode 100644 index 00000000..36fdc3a5 --- /dev/null +++ b/client/src/CWorldObjectSync.h @@ -0,0 +1,59 @@ +#pragma once + +#include "stdafx.h" +#include +#include +#include + +// breakable and movable map objects (lamp posts, petrol pumps, bins, boxes) look the same for every player. An object +// is made from its dummy when a player comes within 80 m and turned back (as new) when it goes away, so the server +// keeps what happened to it and streams it to the players around; a new object gets the known state before it is +// rendered. Damage is reported by whoever sees it, the motion by the one who pushed the object +class CWorldObjectSync +{ +public: + static void Reset(); + static void Process(); + static void HandlePacket(Packets::World::WorldObjectStates* pPacket); + static void HandlePacket(Packets::World::WorldObjectMotion* pPacket); + +private: + static constexpr uint32_t MOTION_INTERVAL = 100; + // the local physics of an object isn't ours: until the mover's rest state (or its motion stops coming), and a bit + // after it while ours settles. A background window of the mover may run a frame a second + static constexpr uint32_t REMOTE_MOTION_TIME = 5000; + static constexpr uint32_t REMOTE_REST_TIME = 1000; + + struct SSlot + { + int ref = 0; // CPools::GetObjectRef + 1, 0 - no map object in the slot + CObject* pObject = nullptr; + Packets::World::WorldObjectKey key; + uint8_t flags = 0; // the damage flags last seen + bool bMoving = false; + bool bMover = false; // we pushed it: we send its motion and where it stopped + uint32_t lastMotionTime = 0; + uint32_t remoteUntil = 0; + Packets::World::WorldObjectTransform original; // the dummy's: the object or the dummy may be gone later + Packets::World::WorldObjectTransform lastTransform; + }; + + using KeyMap = std::unordered_map; + using StateMap = std::unordered_map; + + static SSlot* FindSlot(const Packets::World::WorldObjectKey& key); + static void OnObjectAppeared(SSlot& slot); + static void OnObjectGone(SSlot& slot); + static void ProcessSlot(SSlot& slot, uint32_t now); + static bool ShouldBecomeMover(CObject* pObject); + + static void Apply(SSlot& slot, const Packets::World::WorldObjectState& state, bool bLive, bool bTransform); + static void SendState(const SSlot& slot, bool bTransform, uint8_t flags); + static void Remember(const Packets::World::WorldObjectKey& key, bool bTransform, + const Packets::World::WorldObjectState& state); + + static std::vector ms_slots; // by the object pool index + static KeyMap ms_slotByKey; + static StateMap ms_states; // what the server told us, and our own changes +}; diff --git a/client/src/Main.cpp b/client/src/Main.cpp index 2e7eab7b..97dc1d15 100644 --- a/client/src/Main.cpp +++ b/client/src/Main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -74,6 +75,7 @@ class CoopAndreas else if (bBeenConnected && !CNetwork::m_bConnected) { bBeenConnected = false; + CWorldObjectSync::Reset(); CChat::AddMessage("{d6dce8}[Network] Disconnected from the server."); } }; @@ -86,6 +88,7 @@ class CoopAndreas if (/*CNetwork::m_bConnected*/ CNetwork::m_bAuthenticated) { + CWorldObjectSync::Process(); // TODO: refactor if (COpCodeSync::ms_bLoadingCutscene && !CLocalPlayer::m_bIsHost && CCutsceneMgr::ms_cutsceneName[0] && CCutsceneMgr::ms_cutsceneLoadStatus == 2) diff --git a/client/src/PacketHandlers/system.cpp b/client/src/PacketHandlers/system.cpp index 3cc378dd..44fc80f6 100644 --- a/client/src/PacketHandlers/system.cpp +++ b/client/src/PacketHandlers/system.cpp @@ -5,6 +5,7 @@ #include "stdafx.h" #include #include +#include #include PACKET_HANDLER(ePacketType::PLAYER_CONNECTED, Packets::System::PlayerConnected* pPlayerConnected) @@ -76,6 +77,7 @@ PACKET_HANDLER(ePacketType::PLAYER_HANDSHAKE, Packets::System::PlayerHandshake* CNetwork::m_bAuthenticated = true; CPatch::RevertTemporaryPatches(); + CWorldObjectSync::Reset(); logger::info("Authenticated, playerid %s", to_string(pPlayerHandshake->yourid).c_str()); } diff --git a/client/src/PacketHandlers/world.cpp b/client/src/PacketHandlers/world.cpp index fca07ed5..15add494 100644 --- a/client/src/PacketHandlers/world.cpp +++ b/client/src/PacketHandlers/world.cpp @@ -3,6 +3,7 @@ #include "stdafx.h" #include #include +#include #include PACKET_HANDLER(ePacketType::GAME_WEATHER_TIME, Packets::World::GameWeatherTime* pGameWeatherTime) @@ -66,4 +67,14 @@ PACKET_HANDLER(ePacketType::UPDATE_ALL_TAGS, Packets::World::UpdateAllTags* pUpd PACKET_HANDLER(ePacketType::UPDATE_MOON_SIZE, Packets::World::UpdateMoonSize* pUpdateMoonSize) { CMoonSync::HandlePacket(pUpdateMoonSize); -} \ No newline at end of file +} + +PACKET_HANDLER(ePacketType::WORLD_OBJECT_STATES, Packets::World::WorldObjectStates* pWorldObjectStates) +{ + CWorldObjectSync::HandlePacket(pWorldObjectStates); +} + +PACKET_HANDLER(ePacketType::WORLD_OBJECT_MOTION, Packets::World::WorldObjectMotion* pWorldObjectMotion) +{ + CWorldObjectSync::HandlePacket(pWorldObjectMotion); +} diff --git a/server/src/CNetwork.cpp b/server/src/CNetwork.cpp index cfcbaefc..e1eec5cc 100644 --- a/server/src/CNetwork.cpp +++ b/server/src/CNetwork.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "CWorldObjects.h" bool CNetwork::Init(unsigned short port) { @@ -40,6 +41,7 @@ bool CNetwork::Init(unsigned short port) { CServerTime::Update(); CRTTBroadcastManager::Update(); + CWorldObjects::Update(); while (enet_host_service(pENetHost, &eNetEvent, 1) > 0) { @@ -116,6 +118,7 @@ void CNetwork::HandlePlayerDisconnected(ENetEvent& event) CNetworkPedManager::RemoveAllHostedAndNotify(pNetworkPlayer); CNetworkVehicleManager::RemoveAllHostedAndNotify(pNetworkPlayer); + CWorldObjects::OnPlayerDisconnected(pNetworkPlayer); CNetworkPlayerManager::Remove(pNetworkPlayer); diff --git a/server/src/CNetworkPlayer.h b/server/src/CNetworkPlayer.h index ecd60af4..4b16aef7 100644 --- a/server/src/CNetworkPlayer.h +++ b/server/src/CNetworkPlayer.h @@ -23,6 +23,8 @@ class CNetworkPlayer int8_t m_nSeatId = -1; vehicleid_t m_vehicleId{}; bool m_bCorrectVersion = false; + CVector m_vecPosition{}; // last known, from the player's own updates + bool m_bHasPosition = false; float m_afStats[14]{}; Packets::Players::PlayerPlaceWaypoint m_waypointState{}; CPedClothesDesc m_pPedClothesDesc{}; diff --git a/server/src/CWorldObjects.cpp b/server/src/CWorldObjects.cpp new file mode 100644 index 00000000..251c81bc --- /dev/null +++ b/server/src/CWorldObjects.cpp @@ -0,0 +1,263 @@ +#include "stdafx.h" +#include "CWorldObjects.h" +#include "CNetworkPlayer.h" +#include "CNetworkPlayerManager.h" +#include "CServerTime.h" + +using namespace Packets::World; + +// the server doesn't link the plugin-sdk CVector methods +static bool IsNear(CNetworkPlayer* pNetworkPlayer, const CVector& position, float distance) +{ + if (!pNetworkPlayer->m_bHasPosition) + { + return false; + } + + float dx = pNetworkPlayer->m_vecPosition.x - position.x; + float dy = pNetworkPlayer->m_vecPosition.y - position.y; + float dz = pNetworkPlayer->m_vecPosition.z - position.z; + return dx * dx + dy * dy + dz * dz < distance * distance; +} + +uint32_t CWorldObjects::GetSentVersion(const SEntry& entry, CNetworkPlayer* pNetworkPlayer) +{ + for (const auto& sent : entry.sent) + { + if (sent.pPlayer == pNetworkPlayer) + { + return sent.version; + } + } + return 0; +} + +void CWorldObjects::SetSentVersion(SEntry& entry, CNetworkPlayer* pNetworkPlayer, uint32_t version) +{ + for (auto& sent : entry.sent) + { + if (sent.pPlayer == pNetworkPlayer) + { + sent.version = version; + return; + } + } + entry.sent.push_back({pNetworkPlayer, version}); +} + +void CWorldObjects::SendForgotten(const WorldObjectKey& key, const SEntry& entry) +{ + WorldObjectStates packet{}; + packet.count = 1; + packet.entries[0].key = key; + + for (const auto& sent : entry.sent) + { + GetPacketFactory().Send(packet, sent.pPlayer); + } +} + +void CWorldObjects::HandleStates(WorldObjectStates* pPacket, CNetworkPlayer* pSender) +{ + struct SChange + { + WorldObjectStates::Entry entry; + CVector position; + std::vector holders; // a restored object: who has it, near or not + }; + std::vector changes; + + for (int i = 0; i < pPacket->count; i++) + { + const auto& reported = pPacket->entries[i]; + auto it = ms_entries.find(reported.key); + bool bKnown = it != ms_entries.end(); + + // the damage only grows: two players may report the same pump, each from its own copy of the explosion + WorldObjectState state = bKnown ? it->second.state : WorldObjectState{}; + state.flags |= reported.state.flags & WorldObjectState::DAMAGE_MASK; + if (reported.hasTransform) + { + state.flags = (state.flags & ~WorldObjectState::MOVED) | (reported.state.flags & WorldObjectState::MOVED); + state.transform = reported.state.transform; + } + + bool bTransformChanged = reported.hasTransform && (state.flags & WorldObjectState::MOVED); + if (bKnown && state.flags == it->second.state.flags && !bTransformChanged) + { + continue; + } + + if (!bKnown && state.flags == 0) + { + continue; + } + + SChange change{}; + change.entry.key = reported.key; + change.entry.hasTransform = reported.hasTransform || state.flags == 0; + change.entry.state = state; + change.position = reported.key.GetPosition(); + + if (state.flags == 0) + { + // pushed back to where it stands on the map: as if nothing happened to it + for (const auto& sent : it->second.sent) + { + change.holders.push_back(sent.pPlayer); + } + ms_entries.erase(it); + } + else + { + SEntry& entry = ms_entries[reported.key]; + if (!bKnown) + { + entry.position = change.position; + entry.lastNearTime = g_serverTime; + } + entry.state = state; + entry.version = ms_nextVersion++; + SetSentVersion(entry, pSender, entry.version); + } + + changes.push_back(change); + } + + if (changes.empty()) + { + return; + } + + for (auto pNetworkPlayer : CNetworkPlayerManager::m_pPlayers) + { + if (pNetworkPlayer == pSender) + { + continue; + } + + WorldObjectStates packet{}; + packet.live = true; + for (const auto& change : changes) + { + bool bHolder = std::find(change.holders.begin(), change.holders.end(), pNetworkPlayer) != + change.holders.end(); + if (!bHolder && !IsNear(pNetworkPlayer, change.position, STREAM_DISTANCE)) + { + continue; // gets it from Update when it comes near + } + + packet.entries[packet.count++] = change.entry; + + auto it = ms_entries.find(change.entry.key); + if (it != ms_entries.end()) + { + SetSentVersion(it->second, pNetworkPlayer, it->second.version); + } + } + + if (packet.count > 0) + { + GetPacketFactory().Send(packet, pNetworkPlayer); + } + } +} + +void CWorldObjects::HandleMotion(WorldObjectMotion* pPacket, CNetworkPlayer* pSender) +{ + pPacket->mover = pSender->m_playerId; + + CVector position = pPacket->key.GetPosition(); + for (auto pNetworkPlayer : CNetworkPlayerManager::m_pPlayers) + { + if (pNetworkPlayer != pSender && IsNear(pNetworkPlayer, position, STREAM_DISTANCE)) + { + GetPacketFactory().Send(*pPacket, pNetworkPlayer); + } + } +} + +void CWorldObjects::Update() +{ + static server_time_t nLastUpdate = 0; + constexpr server_time_t UPDATE_INTERVAL = 500; + + if (g_serverTime - nLastUpdate < UPDATE_INTERVAL) + { + return; + } + nLastUpdate = g_serverTime; + + for (auto it = ms_entries.begin(); it != ms_entries.end();) + { + SEntry& entry = it->second; + bool bNear = std::any_of(CNetworkPlayerManager::m_pPlayers.begin(), CNetworkPlayerManager::m_pPlayers.end(), + [&](CNetworkPlayer* pNetworkPlayer) { return IsNear(pNetworkPlayer, entry.position, KEEP_DISTANCE); }); + + if (bNear) + { + entry.lastNearTime = g_serverTime; + } + else if (g_serverTime - entry.lastNearTime > FORGET_AFTER) + { + SendForgotten(it->first, entry); + it = ms_entries.erase(it); + continue; + } + ++it; + } + + // streaming: the objects around every player it doesn't have yet, or has an older state of + for (auto pNetworkPlayer : CNetworkPlayerManager::m_pPlayers) + { + if (!pNetworkPlayer->m_bHasPosition) + { + continue; + } + + WorldObjectStates packet{}; + for (auto& [key, entry] : ms_entries) + { + if (GetSentVersion(entry, pNetworkPlayer) == entry.version || + !IsNear(pNetworkPlayer, entry.position, STREAM_DISTANCE)) + { + continue; + } + + auto& packetEntry = packet.entries[packet.count++]; + packetEntry.key = key; + packetEntry.hasTransform = true; + packetEntry.state = entry.state; + SetSentVersion(entry, pNetworkPlayer, entry.version); + + if (packet.count == WorldObjectStates::MAX_ENTRIES) + { + GetPacketFactory().Send(packet, pNetworkPlayer); + packet.count = 0; + } + } + + if (packet.count > 0) + { + GetPacketFactory().Send(packet, pNetworkPlayer); + } + } +} + +void CWorldObjects::OnPlayerDisconnected(CNetworkPlayer* pNetworkPlayer) +{ + // the last one left: the next session may be another game + if (CNetworkPlayerManager::m_pPlayers.size() <= 1) + { + ms_entries.clear(); + return; + } + + for (auto& [key, entry] : ms_entries) + { + auto& sent = entry.sent; + sent.erase(std::remove_if(sent.begin(), sent.end(), + [pNetworkPlayer](const SSentVersion& s) { return s.pPlayer == pNetworkPlayer; }), + sent.end()); + } +} diff --git a/server/src/CWorldObjects.h b/server/src/CWorldObjects.h new file mode 100644 index 00000000..e84ec8cb --- /dev/null +++ b/server/src/CWorldObjects.h @@ -0,0 +1,49 @@ +#pragma once + +#include "network/packets/world.h" +#include +#include + +class CNetworkPlayer; + +// the map objects players broke or moved (lamp posts, petrol pumps, bins...). A player gets the ones around it as it +// comes near, so the place looks the same for someone who arrives later. Nobody near for a while - the object is +// forgotten: every client has long turned it back into its dummy, as the single player game does +class CWorldObjects +{ +public: + // a dummy becomes an object 80 m from the player (CPopulation::ManageDummy), the state is there long before that + static constexpr float STREAM_DISTANCE = 250.0f; + static constexpr float KEEP_DISTANCE = 200.0f; + static constexpr server_time_t FORGET_AFTER = 60000; + + static void HandleStates(Packets::World::WorldObjectStates* pPacket, CNetworkPlayer* pSender); + static void HandleMotion(Packets::World::WorldObjectMotion* pPacket, CNetworkPlayer* pSender); + static void Update(); + static void OnPlayerDisconnected(CNetworkPlayer* pNetworkPlayer); + +private: + struct SSentVersion + { + CNetworkPlayer* pPlayer; + uint32_t version; + }; + + struct SEntry + { + Packets::World::WorldObjectState state; + CVector position; // the dummy's, for the distances + uint32_t version = 0; + server_time_t lastNearTime = 0; + std::vector sent; // which version every player has + }; + + using EntryMap = std::unordered_map; + + static uint32_t GetSentVersion(const SEntry& entry, CNetworkPlayer* pNetworkPlayer); + static void SetSentVersion(SEntry& entry, CNetworkPlayer* pNetworkPlayer, uint32_t version); + static void SendForgotten(const Packets::World::WorldObjectKey& key, const SEntry& entry); + + static inline EntryMap ms_entries; + static inline uint32_t ms_nextVersion = 1; +}; diff --git a/server/src/PacketHandlers/players.cpp b/server/src/PacketHandlers/players.cpp index 6dba15c5..6b507295 100644 --- a/server/src/PacketHandlers/players.cpp +++ b/server/src/PacketHandlers/players.cpp @@ -6,6 +6,9 @@ PACKET_HANDLER( ePacketType::PLAYER_ONFOOT_UPDATE, Packets::Players::OnFootUpdate* pOnFootUpdate, CNetworkPlayer* pNetworkPlayer) { + pNetworkPlayer->m_vecPosition = pOnFootUpdate->vecPos; + pNetworkPlayer->m_bHasPosition = true; + pOnFootUpdate->senderPlayerid = pNetworkPlayer->m_playerId; GetPacketFactory().SendToAll(*pOnFootUpdate, pNetworkPlayer); } diff --git a/server/src/PacketHandlers/vehicles.cpp b/server/src/PacketHandlers/vehicles.cpp index 9b88a15c..9a61c6ce 100644 --- a/server/src/PacketHandlers/vehicles.cpp +++ b/server/src/PacketHandlers/vehicles.cpp @@ -82,6 +82,8 @@ PACKET_HANDLER(ePacketType::VEHICLE_DRIVER_UPDATE, Packets::Vehicles::VehicleDri { pNetworkVehicle->SetOccupant(0, pNetworkPlayer); pNetworkVehicle->m_vecPosition = pVehicleDriverUpdate->pos; + pNetworkPlayer->m_vecPosition = pVehicleDriverUpdate->pos; + pNetworkPlayer->m_bHasPosition = true; pNetworkVehicle->m_vecRotation = pVehicleDriverUpdate->rot; pNetworkVehicle->m_bUsedByPed = false; pNetworkVehicle->ReassignSyncer(pNetworkPlayer); @@ -185,6 +187,8 @@ PACKET_HANDLER(ePacketType::VEHICLE_PASSENGER_UPDATE, { pVehiclePassengerUpdate->senderPlayerid = pNetworkPlayer->m_playerId; GetPacketFactory().SendToAll(*pVehiclePassengerUpdate, pNetworkPlayer); + pNetworkPlayer->m_vecPosition = pNetworkVehicle->m_vecPosition; + pNetworkPlayer->m_bHasPosition = true; pNetworkVehicle->SetOccupant(pVehiclePassengerUpdate->seatid + 1, pNetworkPlayer); diff --git a/server/src/PacketHandlers/world.cpp b/server/src/PacketHandlers/world.cpp index 5e613389..7a9df899 100644 --- a/server/src/PacketHandlers/world.cpp +++ b/server/src/PacketHandlers/world.cpp @@ -2,6 +2,7 @@ #include "stdafx.h" #include "network/packet_handler.h" #include "network/packets/world.h" +#include "CWorldObjects.h" PACKET_HANDLER( ePacketType::GAME_WEATHER_TIME, Packets::World::GameWeatherTime* pGameWeatherTime, CNetworkPlayer* pNetworkPlayer) @@ -35,4 +36,14 @@ PACKET_HANDLER(ePacketType::UPDATE_ALL_TAGS, Packets::World::UpdateAllTags* pUpd PACKET_HANDLER(ePacketType::UPDATE_MOON_SIZE, Packets::World::UpdateMoonSize* pUpdateMoonSize, CNetworkPlayer* pNetworkPlayer) { GetPacketFactory().SendToAll(*pUpdateMoonSize, pNetworkPlayer); -} \ No newline at end of file +} + +PACKET_HANDLER(ePacketType::WORLD_OBJECT_STATES, Packets::World::WorldObjectStates* pWorldObjectStates, CNetworkPlayer* pNetworkPlayer) +{ + CWorldObjects::HandleStates(pWorldObjectStates, pNetworkPlayer); +} + +PACKET_HANDLER(ePacketType::WORLD_OBJECT_MOTION, Packets::World::WorldObjectMotion* pWorldObjectMotion, CNetworkPlayer* pNetworkPlayer) +{ + CWorldObjects::HandleMotion(pWorldObjectMotion, pNetworkPlayer); +} diff --git a/shared/network/packet_types.h b/shared/network/packet_types.h index 2575d2e6..56792a87 100644 --- a/shared/network/packet_types.h +++ b/shared/network/packet_types.h @@ -72,6 +72,8 @@ enum class ePacketType : uint16_t UPDATE_MOON_SIZE, CUTSCENE_SKIP_VOTE, CUTSCENE_SKIP_VOTE_STATE, + WORLD_OBJECT_STATES, + WORLD_OBJECT_MOTION, PACKET_ID_MAX }; @@ -93,7 +95,8 @@ extern inline const char* ePacketType_ToString(ePacketType packetType) "REMOVE_CHECKPOINT", "ENEX_SYNC", "CREATE_STATIC_BLIP", "SET_VEHICLE_CREATED_BY", "SET_PLAYER_TASK", "PED_SAY", "PED_CLAIM_ON_RELEASE", "PED_CANCEL_CLAIM", "PED_RESET_ALL_CLAIMS", "PED_TAKE_HOST", "PERFORM_TASK_SEQUENCE", "ADD_PROJECTILE", "TAG_UPDATE", "UPDATE_ALL_TAGS", "TELEPORT_PLAYER_SCRIPTED", "SERVER_TIME_REQUEST", "ENEX_TRANSITION", - "RTT_BROADCAST", "UPDATE_MOON_SIZE", "CUTSCENE_SKIP_VOTE", "CUTSCENE_SKIP_VOTE_STATE"}; + "RTT_BROADCAST", "UPDATE_MOON_SIZE", "CUTSCENE_SKIP_VOTE", "CUTSCENE_SKIP_VOTE_STATE", + "WORLD_OBJECT_STATES", "WORLD_OBJECT_MOTION"}; return array[(int)packetType]; } diff --git a/shared/network/packets/world.h b/shared/network/packets/world.h index b68d259a..38585278 100644 --- a/shared/network/packets/world.h +++ b/shared/network/packets/world.h @@ -1,6 +1,7 @@ #pragma once #include "CWeather.h" #include +#include "network/serializable_types.h" namespace Packets::World { @@ -202,4 +203,165 @@ class UpdateMoonSize : public Packet } }; +// a breakable / movable object of the map (lamp post, petrol pump, bin...) by the dummy it was made from: the model and +// the dummy's position come from the same IPL for everyone, unlike the pool slots +struct WorldObjectKey +{ + static constexpr int32_t MAX_XY = 600000; // cm + static constexpr int32_t MIN_Z = -100000; + static constexpr int32_t MAX_Z = 300000; + + int16_t model = 0; + int32_t x = 0; + int32_t y = 0; + int32_t z = 0; + + bool operator==(const WorldObjectKey& other) const + { + return model == other.model && x == other.x && y == other.y && z == other.z; + } + + CVector GetPosition() const { return CVector(x / 100.0f, y / 100.0f, z / 100.0f); } + + template + bool Serialize(Stream& stream) + { + if (Stream::IsWriting) + { + x = VCLAMP(x, -MAX_XY, MAX_XY); + y = VCLAMP(y, -MAX_XY, MAX_XY); + z = VCLAMP(z, MIN_Z, MAX_Z); + } + serialize_int(stream, model, 0, 32767); + serialize_int(stream, x, -MAX_XY, MAX_XY); + serialize_int(stream, y, -MAX_XY, MAX_XY); + serialize_int(stream, z, MIN_Z, MAX_Z); + return true; + } +}; + +struct WorldObjectKeyHash +{ + size_t operator()(const WorldObjectKey& key) const + { + size_t hash = static_cast(key.model); + hash = hash * 31 + static_cast(key.x); + hash = hash * 31 + static_cast(key.y); + hash = hash * 31 + static_cast(key.z); + return hash; + } +}; + +struct WorldObjectTransform +{ + WorldPositionCompressed pos; + CVector right; + CVector forward; // up = right x forward + + template + bool Serialize(Stream& stream) + { + serialize_object(stream, pos); + if (Stream::IsWriting) + { + right = CVector(VCLAMP(right.x, -1.0f, 1.0f), VCLAMP(right.y, -1.0f, 1.0f), VCLAMP(right.z, -1.0f, 1.0f)); + forward = CVector(VCLAMP(forward.x, -1.0f, 1.0f), VCLAMP(forward.y, -1.0f, 1.0f), + VCLAMP(forward.z, -1.0f, 1.0f)); + } + serialize_compressed_float(stream, right.x, -1.0f, 1.0f, 0.0005f); + serialize_compressed_float(stream, right.y, -1.0f, 1.0f, 0.0005f); + serialize_compressed_float(stream, right.z, -1.0f, 1.0f, 0.0005f); + serialize_compressed_float(stream, forward.x, -1.0f, 1.0f, 0.0005f); + serialize_compressed_float(stream, forward.y, -1.0f, 1.0f, 0.0005f); + serialize_compressed_float(stream, forward.z, -1.0f, 1.0f, 0.0005f); + return true; + } +}; + +// what happened to the object, see CObject::ObjectDamage. The damage only grows, the transform is the last mover's +struct WorldObjectState +{ + static constexpr uint8_t DAMAGED = 1 << 0; // m_bRenderDamaged: the damaged model + static constexpr uint8_t SMASHED = 1 << 1; // no collision, invisible: smashed or broken into pieces + static constexpr uint8_t BROKEN = 1 << 2; // objectFlags.bIsBroken + static constexpr uint8_t EXPLODED = 1 << 3; // objectFlags.bIsExploded + static constexpr uint8_t MOVED = 1 << 4; // not where its dummy is + static constexpr uint8_t DAMAGE_MASK = DAMAGED | SMASHED | BROKEN | EXPLODED; + + uint8_t flags = 0; + WorldObjectTransform transform; // when MOVED + + template + bool Serialize(Stream& stream) + { + serialize_bits(stream, flags, 5); + if (flags & MOVED) + { + serialize_object(stream, transform); + } + return true; + } +}; + +// client -> server: what changed on its object (live). Server -> client: the same live change, or the stored states of +// the objects around the player when it comes near them (streaming); flags 0 = the object is as on the map again +class WorldObjectStates : public Packet +{ + DEFINE_PACKET_TYPE(WorldObjectStates, ePacketType::WORLD_OBJECT_STATES, ePacketChannel::EVENT); + +public: + static constexpr int MAX_ENTRIES = 32; + + struct Entry + { + WorldObjectKey key; + bool hasTransform = false; // false: only the damage is reported, the position stays as it was + WorldObjectState state; + }; + + bool live = false; // happened just now: play the effects, otherwise apply quietly + uint8_t count = 0; + Entry entries[MAX_ENTRIES]{}; + +private: + template + bool Serialize(Stream& stream) + { + serialize_bool(stream, live); + serialize_int(stream, count, 0, MAX_ENTRIES); + for (int i = 0; i < count; i++) + { + serialize_object(stream, entries[i].key); + serialize_bool(stream, entries[i].hasTransform); + serialize_object(stream, entries[i].state); + } + return true; + } +}; + +// the mover of an object sends it while it flies / falls / slides, the rest state follows in WorldObjectStates +class WorldObjectMotion : public Packet +{ + DEFINE_PACKET_TYPE(WorldObjectMotion, ePacketType::WORLD_OBJECT_MOTION, ePacketChannel::SYNC); + +public: + playerid_t mover; // set by the server: two players who both started it decide by the id + WorldObjectKey key; + WorldObjectTransform transform; + CVector moveSpeed; + CVector turnSpeed; + +private: + template + bool Serialize(Stream& stream) + { + serialize_playerid(stream, mover); + serialize_object(stream, key); + serialize_object(stream, transform); + serialize_vector(stream, moveSpeed); + serialize_vector(stream, turnSpeed); + return true; + } +}; + } // namespace Packets::World