diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index b43b1cb17b0..6df093c2009 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -10,6 +10,7 @@ *****************************************************************************/ #include "StdInc.h" +#include #include #include #include @@ -135,6 +136,11 @@ bool CPacketHandler::ProcessPacket(unsigned char ucPacketID, NetBitStreamInterfa Packet_EntityRemove(bitStream); return true; + // Bulk delete element trees + case PACKET_ID_ENTITY_REMOVE_TREE: + Packet_EntityRemoveTree(bitStream); + return true; + // Respawns/hides pickups case PACKET_ID_PICKUP_HIDESHOW: Packet_PickupHideShow(bitStream); @@ -4359,6 +4365,112 @@ void CPacketHandler::Packet_EntityRemove(NetBitStreamInterface& bitStream) } } +void CPacketHandler::Packet_EntityRemoveTree(NetBitStreamInterface& bitStream) +{ + // std::uint16_t (2) - number of root elements + // ElementID (2) - root element ids (repeating) + + std::uint16_t rootElementCount; + if (!bitStream.ReadCompressed(rootElementCount)) + return; + + std::vector rootElements; + rootElements.reserve(rootElementCount); + + // Read all root element IDs first + for (std::uint16_t i = 0; i < rootElementCount; ++i) + { + ElementID rootID; + if (!bitStream.Read(rootID)) + return; + + CClientEntity* rootEntity = CElementIDs::GetElement(rootID); + if (rootEntity) + { + if (rootEntity->GetType() == CCLIENTPLAYER) + { + RaiseProtocolError(45); // Same as individual entity removal - cannot remove players + return; + } + rootElements.push_back(rootEntity); + } + } + + // Delete each tree hierarchically + for (auto* rootEntity : rootElements) + { + RemoveEntityTree(rootEntity); + } +} + +void CPacketHandler::RemoveEntityTree(CClientEntity* rootEntity) +{ + if (!rootEntity || rootEntity->IsSystemEntity()) + return; + + // Collect all entities in this tree + std::vector entitiesToDelete; + CollectEntityTree(rootEntity, entitiesToDelete); + + // Handle vehicle/ped specific cleanup for each entity + CMappedList listOfPeds(g_pClientGame->GetPedSync()->GetList()); + listOfPeds.push_front(g_pClientGame->GetLocalPlayer()); + + for (auto* entity : entitiesToDelete) + { + if (entity->GetType() == CCLIENTVEHICLE || entity->GetType() == CCLIENTPED) + { + ElementID entityID = entity->GetID(); + + if (entity->GetType() == CCLIENTVEHICLE) + { + for (auto* ped : listOfPeds) + { + if (ped->m_VehicleInOutID == entityID) + ped->ResetVehicleInOut(); + + if (ped->m_bNoNewVehicleTask && ped->m_NoNewVehicleTaskReasonID == entityID) + { + ped->m_bNoNewVehicleTask = false; + ped->m_NoNewVehicleTaskReasonID = INVALID_ELEMENT_ID; + } + } + } + else + { + auto* removedPed = static_cast(entity); + for (auto* ped : listOfPeds) + { + if (ped->m_bIsGettingJacked && ped->m_pGettingJackedBy == removedPed) + { + ped->ResetVehicleInOut(); + ped->RemoveFromVehicle(false); + ped->SetVehicleInOutState(VEHICLE_INOUT_NONE); + } + } + } + } + + // Delete clientside children and the entity + entity->DeleteClientChildren(); + g_pClientGame->m_ElementDeleter.Delete(entity); + } +} + +void CPacketHandler::CollectEntityTree(CClientEntity* entity, std::vector& entities) +{ + if (!entity) + return; + + entities.push_back(entity); + + // Recursively collect all children + for (auto iter = entity->IterBegin(); iter != entity->IterEnd(); ++iter) + { + CollectEntityTree(*iter, entities); + } +} + void CPacketHandler::Packet_PickupHideShow(NetBitStreamInterface& bitStream) { // bool - show it? diff --git a/Client/mods/deathmatch/logic/CPacketHandler.h b/Client/mods/deathmatch/logic/CPacketHandler.h index 9c8de37454b..6f85d98a0e7 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.h +++ b/Client/mods/deathmatch/logic/CPacketHandler.h @@ -78,6 +78,7 @@ class CPacketHandler void Packet_PlayerNetworkStatus(NetBitStreamInterface& bitStream); void Packet_EntityAdd(NetBitStreamInterface& bitStream); void Packet_EntityRemove(NetBitStreamInterface& bitStream); + void Packet_EntityRemoveTree(NetBitStreamInterface& bitStream); void Packet_PickupHideShow(NetBitStreamInterface& bitStream); void Packet_PickupHitConfirm(NetBitStreamInterface& bitStream); void Packet_Lua(unsigned char ucPacketID, NetBitStreamInterface& bitStream); @@ -114,5 +115,9 @@ class CPacketHandler NetBitStreamInterface* m_pEntityAddBitStream; uint m_uiEntityAddNumEntities; +private: + void RemoveEntityTree(CClientEntity* rootEntity); + void CollectEntityTree(CClientEntity* entity, std::vector& entities); + std::list> m_displayTextList; }; diff --git a/Server/mods/deathmatch/logic/CElementDeleter.cpp b/Server/mods/deathmatch/logic/CElementDeleter.cpp index c45f7e3873e..a1e402eae91 100644 --- a/Server/mods/deathmatch/logic/CElementDeleter.cpp +++ b/Server/mods/deathmatch/logic/CElementDeleter.cpp @@ -51,19 +51,69 @@ void CElementDeleter::DoDeleteAll() delete *m_List.begin(); } -void CElementDeleter::Unreference(CElement* pElement) +void CElementDeleter::Unreference(CElement* element) { - m_List.remove(pElement); + m_List.remove(element); } -bool CElementDeleter::IsBeingDeleted(CElement* pElement) +bool CElementDeleter::IsBeingDeleted(CElement* element) const { - return ListContains(m_List, pElement); + return ListContains(m_List, element); } -void CElementDeleter::CleanUpForVM(CLuaMain* pLuaMain) +void CElementDeleter::DeleteTree(CElement* rootElement, bool unlink, bool updatePerPlayerEntities) { - CElementListType::const_iterator iter = m_List.begin(); - for (; iter != m_List.end(); iter++) - (*iter)->DeleteEvents(pLuaMain, false); + if (!rootElement || IsBeingDeleted(rootElement)) + return; + + std::vector elementsToDelete; + CollectTreeElements(rootElement, elementsToDelete); + + // Fire destroy events for all elements + for (auto* element : elementsToDelete) + { + if (IsBeingDeleted(element)) + continue; + + CLuaArguments arguments; + element->CallEvent("onElementDestroy", arguments); + + if (!element->IsBeingDeleted()) + m_List.push_back(element); + + g_pGame->GetPlayerManager()->ClearElementData(element); + element->SetIsBeingDeleted(true); + } + + // Clear children and unlink elements + for (auto* element : elementsToDelete) + { + element->ClearChildren(); + element->SetParentObject(nullptr, false); + + if (unlink) + element->Unlink(); + } + + if (updatePerPlayerEntities && rootElement) + rootElement->UpdatePerPlayerEntities(); +} + +void CElementDeleter::CollectTreeElements(CElement* element, std::vector& elements) +{ + if (!element || IsBeingDeleted(element)) + return; + + elements.push_back(element); + + for (auto iter = element->IterBegin(); iter != element->IterEnd(); ++iter) + { + CollectTreeElements(*iter, elements); + } +} + +void CElementDeleter::CleanUpForVM(CLuaMain* luaMain) +{ + for (auto* element : m_List) + element->DeleteEvents(luaMain, false); } diff --git a/Server/mods/deathmatch/logic/CElementDeleter.h b/Server/mods/deathmatch/logic/CElementDeleter.h index 254996c44a2..9afb63ffd2a 100644 --- a/Server/mods/deathmatch/logic/CElementDeleter.h +++ b/Server/mods/deathmatch/logic/CElementDeleter.h @@ -13,6 +13,7 @@ #include "CElement.h" #include +#include class CElementDeleter { @@ -20,13 +21,14 @@ class CElementDeleter ~CElementDeleter() { DoDeleteAll(); }; void Delete(class CElement* pElement, bool bUnlink = true, bool bUpdatePerPlayerEntities = true); + void DeleteTree(CElement* rootElement, bool unlink = true, bool updatePerPlayerEntities = true); void DoDeleteAll(); - bool IsBeingDeleted(class CElement* pElement); - void Unreference(CElement* pElement); - - void CleanUpForVM(CLuaMain* pLuaMain); + bool IsBeingDeleted(CElement* element) const; + void Unreference(CElement* element); + void CleanUpForVM(CLuaMain* luaMain); private: + void CollectTreeElements(CElement* element, std::vector& elements); CElementListType m_List; }; diff --git a/Server/mods/deathmatch/logic/CResource.cpp b/Server/mods/deathmatch/logic/CResource.cpp index 59d0d29f7a4..04b9055706f 100644 --- a/Server/mods/deathmatch/logic/CResource.cpp +++ b/Server/mods/deathmatch/logic/CResource.cpp @@ -31,6 +31,7 @@ #include "CHTTPD.h" #include "Utils.h" #include "packets/CResourceClientScriptsPacket.h" +#include "packets/CEntityRemoveTreePacket.h" #include "lua/CLuaFunctionParseHelpers.h" #include #include @@ -1181,26 +1182,26 @@ bool CResource::Stop(bool bManualStop) // Destroy the virtual machine for this resource DestroyVM(); - // Remove the resource element from the client - CEntityRemovePacket removePacket; + // Remove resource elements using bulk tree deletion + CEntityRemoveTreePacket removeTreePacket; if (m_pResourceElement) { - removePacket.Add(m_pResourceElement); - g_pGame->GetElementDeleter()->Delete(m_pResourceElement); + removeTreePacket.AddRootElement(m_pResourceElement); + g_pGame->GetElementDeleter()->DeleteTree(m_pResourceElement); m_pResourceElement = nullptr; } - // Remove the dynamic resource element from the client if (m_pResourceDynamicElementRoot) { - removePacket.Add(m_pResourceDynamicElementRoot); - g_pGame->GetElementDeleter()->Delete(m_pResourceDynamicElementRoot); + removeTreePacket.AddRootElement(m_pResourceDynamicElementRoot); + g_pGame->GetElementDeleter()->DeleteTree(m_pResourceDynamicElementRoot); m_pResourceDynamicElementRoot = nullptr; } - // Broadcast the packet to joined players - g_pGame->GetPlayerManager()->BroadcastOnlyJoined(removePacket); + // Broadcast single packet for entire resource cleanup + if (!removeTreePacket.IsEmpty()) + g_pGame->GetPlayerManager()->BroadcastOnlyJoined(removeTreePacket); // Clear the list of players where this resource is running std::exchange(m_isRunningForPlayer, {}); diff --git a/Server/mods/deathmatch/logic/packets/CEntityRemoveTreePacket.cpp b/Server/mods/deathmatch/logic/packets/CEntityRemoveTreePacket.cpp new file mode 100644 index 00000000000..dc41744db64 --- /dev/null +++ b/Server/mods/deathmatch/logic/packets/CEntityRemoveTreePacket.cpp @@ -0,0 +1,28 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" +#include "CEntityRemoveTreePacket.h" +#include "CElement.h" +#include + +bool CEntityRemoveTreePacket::Write(NetBitStreamInterface& bitStream) const +{ + if (m_rootElements.empty()) + return false; + + bitStream.WriteCompressed(static_cast(m_rootElements.size())); + + for (const auto* element : m_rootElements) + { + bitStream.Write(element->GetID()); + } + + return true; +} diff --git a/Server/mods/deathmatch/logic/packets/CEntityRemoveTreePacket.h b/Server/mods/deathmatch/logic/packets/CEntityRemoveTreePacket.h new file mode 100644 index 00000000000..158ab3ad631 --- /dev/null +++ b/Server/mods/deathmatch/logic/packets/CEntityRemoveTreePacket.h @@ -0,0 +1,30 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto + * LICENSE: See LICENSE in the top level directory + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once + +#include "CPacket.h" +#include + +class CEntityRemoveTreePacket final : public CPacket +{ +public: + ePacketID GetPacketID() const noexcept override { return PACKET_ID_ENTITY_REMOVE_TREE; } + unsigned long GetFlags() const noexcept override { return PACKET_HIGH_PRIORITY | PACKET_RELIABLE | PACKET_SEQUENCED; } + + bool Write(NetBitStreamInterface& bitStream) const override; + + void AddRootElement(CElement* element) { m_rootElements.push_back(element); } + void Clear() noexcept { m_rootElements.clear(); } + bool IsEmpty() const noexcept { return m_rootElements.empty(); } + std::size_t GetRootElementCount() const noexcept { return m_rootElements.size(); } + +private: + std::vector m_rootElements; +}; diff --git a/Shared/sdk/net/Packets.h b/Shared/sdk/net/Packets.h index 2f587cad140..a79376faf55 100644 --- a/Shared/sdk/net/Packets.h +++ b/Shared/sdk/net/Packets.h @@ -177,5 +177,6 @@ enum ePacketID PACKET_ID_SERVER_INFO_SYNC, PACKET_ID_DISCORD_JOIN, PACKET_ID_PLAYER_RESOURCE_START, - PACKET_ID_PLAYER_WORLD_SPECIAL_PROPERTY + PACKET_ID_PLAYER_WORLD_SPECIAL_PROPERTY, + PACKET_ID_ENTITY_REMOVE_TREE };