Files
DarkflameServer/dGame/EntityManager.cpp
Aaron Kimbrell d4e9423264 feat(servers): time the main loops' phases and the known heavy spots
Frames and phase scopes in the world, auth, chat and UGC loops (packets per type, entities, physics, ghosting, replica, spawners, log flush, saves, web requests by route). Scopes at LoadPlayer, CreateEntity, each component's construction, InventoryComponent::LoadXml, script timers and a world's zone load; game database queries in the query helpers; CDClient statements timed through sqlite3_trace_v2 as CDClient <table> (CppSQLite3DB gets a handle accessor). Task 96.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:26:17 -05:00

703 lines
22 KiB
C++

#include "EntityManager.h"
#include "Profiler.h"
#include "RakNetTypes.h"
#include "Game.h"
#include "User.h"
#include "ObjectIDManager.h"
#include "Character.h"
#include "GeneralUtils.h"
#include "dServer.h"
#include "Spawner.h"
#include "SkillComponent.h"
#include "SwitchComponent.h"
#include "UserManager.h"
#include "dZoneManager.h"
#include "Zone.h"
#include "ZoneScenes.h"
#include "MissionComponent.h"
#include "Game.h"
#include "Logger.h"
#include "MessageIdentifiers.h"
#include "dConfig.h"
#include "eTriggerEventType.h"
#include "eObjectBits.h"
#include "eGameMasterLevel.h"
#include "eReplicaComponentType.h"
#include "eReplicaPacketType.h"
#include "PlayerManager.h"
#include "GhostComponent.h"
#include "CharacterComponent.h"
#include <ranges>
// Configure which zones have ghosting disabled, mostly small worlds.
std::vector<LWOMAPID> EntityManager::m_GhostingExcludedZones = {
// Small zones
1000,
// Racing zones
1203,
1261,
1303,
1403,
// Property zones
1150,
1151,
1250,
1251,
1350,
1450
};
// Configure some exceptions for ghosting, nessesary for some special objects.
std::vector<LOT> EntityManager::m_GhostingExcludedLOTs = {
// AG - Footrace
4967
};
template<typename T>
void ParseDelimSetting(std::set<T>& setting, const std::string_view settingName, const char delim = ',') {
const auto str = Game::config->GetValue(settingName.data());
setting.clear();
for (const auto& strVal : GeneralUtils::SplitString(str, delim)) {
const auto val = GeneralUtils::TryParse<T>(strVal);
if (val) {
setting.insert(val.value());
}
}
}
void EntityManager::ReloadConfig() {
auto hcmode = Game::config->GetValue("hardcore_mode");
m_HardcoreMode = hcmode.empty() ? false : (hcmode == "1");
auto hcUscorePercent = Game::config->GetValue("hardcore_lose_uscore_on_death_percent");
m_HardcoreLoseUscoreOnDeathPercent = hcUscorePercent.empty() ? 10 : GeneralUtils::TryParse<uint32_t>(hcUscorePercent).value_or(10);
auto hcUscoreMult = Game::config->GetValue("hardcore_uscore_enemies_multiplier");
m_HardcoreUscoreEnemiesMultiplier = hcUscoreMult.empty() ? 2 : GeneralUtils::TryParse<uint32_t>(hcUscoreMult).value_or(2);
auto hcDropInv = Game::config->GetValue("hardcore_dropinventory_on_death");
m_HardcoreDropinventoryOnDeath = hcDropInv.empty() ? false : (hcDropInv == "1");
ParseDelimSetting<LOT>(m_HardcoreExcludedItemDrops, "hardcore_excluded_item_drops");
// We don't need to save the worlds, just need to check if this one is in the list
std::set<LWOMAPID> worlds;
ParseDelimSetting<LWOMAPID>(worlds, "hardcore_uscore_reduced_worlds");
m_HardcoreUscoreReduced = worlds.contains(Game::zoneManager->GetZoneID().GetMapID());
ParseDelimSetting<LOT>(m_HardcoreUscoreReducedLots, "hardcore_uscore_reduced_lots");
ParseDelimSetting<LOT>(m_HardcoreUscoreExcludedEnemies, "hardcore_uscore_excluded_enemies");
ParseDelimSetting<LWOMAPID>(m_HardcoreDisabledWorlds, "hardcore_disabled_worlds");
auto hcXpReduction = Game::config->GetValue("hardcore_uscore_reduction");
m_HardcoreUscoreReduction = hcXpReduction.empty() ? 1.0f : GeneralUtils::TryParse<float>(hcXpReduction).value_or(1.0f);
m_HardcoreMode = GetHardcoreDisabledWorlds().contains(Game::zoneManager->GetZoneID().GetMapID()) ? false : m_HardcoreMode;
auto hcCoinKeep = Game::config->GetValue("hardcore_coin_keep");
m_HardcoreCoinKeep = hcCoinKeep.empty() ? false : GeneralUtils::TryParse<float>(hcCoinKeep).value_or(0.0f);
}
void EntityManager::Initialize() {
// Check if this zone has ghosting enabled
m_GhostingEnabled = std::find(
m_GhostingExcludedZones.begin(),
m_GhostingExcludedZones.end(),
Game::zoneManager->GetZoneID().GetMapID()
) == m_GhostingExcludedZones.end();
// Scene ghosting (on unless ghosting_scenes=0) needs the terrain's scene map; without one it stays off
if (m_GhostingEnabled && GeneralUtils::TryParse<bool>(Game::config->GetValue("ghosting_scenes")).value_or(true)) {
auto* zone = Game::zoneManager->GetZoneMut();
m_SceneGhosting = zone && zone->LoadSceneMap();
LOG("Scene ghosting is %s", m_SceneGhosting ? "on" : "off: the zone has no scene map");
}
// grab hardcore mode settings and load them with sane defaults
Game::config->AddConfigHandler([]() {Game::entityManager->ReloadConfig();});
Game::entityManager->ReloadConfig();
// If cloneID is not zero, then hardcore mode is disabled
// aka minigames and props
if (Game::zoneManager->GetZoneID().GetCloneID() != 0) m_HardcoreMode = false;
}
Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentEntity, const bool controller, const LWOOBJID explicitId) {
Profiler::Scope profile("CreateEntity");
// Determine the objectID for the new entity
LWOOBJID id;
// If an explicit ID was provided, use it
if (explicitId != LWOOBJID_EMPTY) {
id = explicitId;
}
// For non player entites, we'll generate a new ID or set the appropiate flags
else if (user == nullptr || info.lot != 1) {
// Entities with no ID already set, often spawned entities, we'll generate a new sequencial ID
if (info.id == 0) {
id = ObjectIDManager::GenerateObjectID();
}
// Entities with an ID already set, often level entities, we'll use that ID as a base
else {
id = info.id;
}
// Exclude the zone control object from any flags
if (!controller) {
// The client flags means the client should render the entity
GeneralUtils::SetBit(id, eObjectBits::CLIENT);
// Spawned entities require the spawned flag to render
if (info.spawnerID != 0) {
GeneralUtils::SetBit(id, eObjectBits::SPAWNED);
}
}
}
// For players, we'll use the persistent ID for that character
else {
id = user->GetLastUsedChar()->GetObjectID();
}
info.id = id;
Entity* entity = new Entity(id, info, user, parentEntity);
// Initialize the entity
entity->Initialize();
// Add the entity to the entity map
m_Entities.insert_or_assign(id, entity);
// Set the zone control entity if the entity is a zone control object, this should only happen once
if (controller) {
m_ZoneControlEntity = entity;
// Proooooobably shouldn't ghost zoneControl
m_ZoneControlEntity->SetIsGhostingCandidate(false);
}
// Check if this entity is a respawn point, if so add it to the registry
const auto& spawnName = entity->GetVar<std::u16string>(u"respawnname");
if (!spawnName.empty()) {
m_SpawnPoints.insert_or_assign(GeneralUtils::UTF16ToWTF8(spawnName), entity->GetObjectID());
}
return entity;
}
void EntityManager::_addEntity(Entity* entity) {
m_Entities.insert_or_assign(entity->GetObjectID(), entity);
}
void EntityManager::DestroyEntity(const LWOOBJID& objectID) {
DestroyEntity(GetEntity(objectID));
}
void EntityManager::DestroyEntity(Entity* entity) {
if (!entity) return;
entity->TriggerEvent(eTriggerEventType::DESTROY, entity);
const auto id = entity->GetObjectID();
if (std::count(m_EntitiesToDelete.begin(), m_EntitiesToDelete.end(), id)) {
return;
}
// Destruct networked entities
if (entity->GetNetworkId() != 0) {
DestructEntity(entity);
}
// Delete this entity at the end of the frame
ScheduleForDeletion(id);
}
void EntityManager::SerializeEntities() {
for (size_t i = 0; i < m_EntitiesToSerialize.size(); i++) {
const LWOOBJID toSerialize = m_EntitiesToSerialize[i];
auto* entity = GetEntity(toSerialize);
if (!entity) continue;
m_SerializationCounter++;
RakNet::BitStream stream;
stream.Write<char>(ID_REPLICA_MANAGER_SERIALIZE);
stream.Write<unsigned short>(entity->GetNetworkId());
entity->WriteBaseReplicaData(stream, eReplicaPacketType::SERIALIZATION);
entity->WriteComponents(stream, eReplicaPacketType::SERIALIZATION);
if (entity->GetIsGhostingCandidate()) {
for (auto* player : PlayerManager::GetAllPlayers()) {
auto* ghostComponent = player->GetComponent<GhostComponent>();
if (ghostComponent && ghostComponent->IsObserved(toSerialize)) {
Game::server->Send(stream, player->GetSystemAddress(), false);
}
}
} else {
Game::server->Send(stream, UNASSIGNED_SYSTEM_ADDRESS, true);
}
}
m_EntitiesToSerialize.clear();
}
void EntityManager::KillEntities() {
for (size_t i = 0; i < m_EntitiesToKill.size(); i++) {
const LWOOBJID toKill = m_EntitiesToKill[i];
auto* entity = GetEntity(toKill);
if (!entity) {
LOG("Attempting to kill null entity %llu", toKill);
continue;
}
if (entity->GetScheduledKiller()) {
entity->Smash(entity->GetScheduledKiller()->GetObjectID(), eKillType::SILENT);
} else {
entity->Smash(LWOOBJID_EMPTY, eKillType::SILENT);
}
}
m_EntitiesToKill.clear();
}
void EntityManager::DeleteEntities() {
for (size_t i = 0; i < m_EntitiesToDelete.size(); i++) {
const LWOOBJID toDelete = m_EntitiesToDelete[i];
auto entityToDelete = GetEntity(toDelete);
if (entityToDelete) {
// Get all this info first before we delete the player.
auto networkIdToErase = entityToDelete->GetNetworkId();
const auto& ghostingToDelete = std::find(m_EntitiesToGhost.begin(), m_EntitiesToGhost.end(), entityToDelete);
delete entityToDelete;
entityToDelete = nullptr;
if (networkIdToErase != 0) m_LostNetworkIds.push(networkIdToErase);
if (ghostingToDelete != m_EntitiesToGhost.end()) m_EntitiesToGhost.erase(ghostingToDelete);
} else {
LOG("Attempted to delete non-existent entity %llu", toDelete);
}
m_Entities.erase(toDelete);
}
m_EntitiesToDelete.clear();
}
void EntityManager::UpdateEntities(const float deltaTime) {
{
Profiler::Scope profile("Entity updates");
for (auto* entity : m_Entities | std::views::values) {
entity->Update(deltaTime);
}
}
{
Profiler::Scope profile("Serialize entities", Profiler::Phase::REPLICA);
SerializeEntities();
}
Profiler::Scope profile("Kill and delete entities");
KillEntities();
DeleteEntities();
}
Entity* EntityManager::GetEntity(const LWOOBJID& objectId) const {
const auto& index = m_Entities.find(objectId);
if (index == m_Entities.end()) {
return nullptr;
}
return index->second;
}
std::vector<Entity*> EntityManager::GetEntitiesInGroup(const std::string& group) {
std::vector<Entity*> entitiesInGroup;
for (auto* entity : m_Entities | std::views::values) {
for (const auto& entityGroup : entity->GetGroups()) {
if (entityGroup == group) {
entitiesInGroup.push_back(entity);
}
}
}
return entitiesInGroup;
}
std::vector<Entity*> EntityManager::GetEntitiesByComponent(const eReplicaComponentType componentType) const {
std::vector<Entity*> withComp;
if (componentType != eReplicaComponentType::INVALID) {
for (auto* entity : m_Entities | std::views::values) {
if (!entity->HasComponent(componentType)) continue;
withComp.push_back(entity);
}
} else {
for (auto* const entity : m_Entities | std::views::values) withComp.push_back(entity);
}
return withComp;
}
std::vector<Entity*> EntityManager::GetEntitiesByLOT(const LOT& lot) const {
std::vector<Entity*> entities;
for (auto* entity : m_Entities | std::views::values) {
if (entity->GetLOT() == lot) entities.push_back(entity);
}
return entities;
}
std::vector<Entity*> EntityManager::GetEntitiesByProximity(NiPoint3 reference, float radius) const {
std::vector<Entity*> entities;
if (radius <= 1000.0f) { // The client has a 1000 unit limit on this same logic, so we'll use the same limit
for (auto* entity : m_Entities | std::views::values) {
if (NiPoint3::Distance(reference, entity->GetPosition()) <= radius) entities.push_back(entity);
}
}
return entities;
}
Entity* EntityManager::GetZoneControlEntity() const {
return m_ZoneControlEntity;
}
Entity* EntityManager::GetSpawnPointEntity(const std::string& spawnName) const {
// Lookup the spawn point entity in the map
const auto& spawnPoint = m_SpawnPoints.find(spawnName);
// Check if the spawn point entity is valid just in case
return spawnPoint == m_SpawnPoints.end() ? nullptr : GetEntity(spawnPoint->second);
}
const std::unordered_map<std::string, LWOOBJID>& EntityManager::GetSpawnPointEntities() const {
return m_SpawnPoints;
}
void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr) {
if (!entity) {
LOG("Attempted to construct null entity");
return;
}
// Don't construct GM invisible entities unless it's for the GM themselves
// GMs can see other GMs if they are the same or lower level
GameMessages::GetGMInvis getGMInvisMsg;
getGMInvisMsg.Send(entity->GetObjectID());
if (getGMInvisMsg.bGMInvis && sysAddr != entity->GetSystemAddress()) {
auto* toUser = UserManager::Instance()->GetUser(sysAddr);
if (!toUser) return;
auto* constructedUser = UserManager::Instance()->GetUser(entity->GetSystemAddress());
if (!constructedUser) return;
if (toUser->GetMaxGMLevel() < constructedUser->GetMaxGMLevel()) return;
}
if (entity->GetNetworkId() == 0) {
uint16_t networkId;
if (!m_LostNetworkIds.empty()) {
networkId = m_LostNetworkIds.top();
m_LostNetworkIds.pop();
} else networkId = ++m_NetworkIdCounter;
entity->SetNetworkId(networkId);
}
if (entity->GetIsGhostingCandidate()) {
if (std::find(m_EntitiesToGhost.begin(), m_EntitiesToGhost.end(), entity) == m_EntitiesToGhost.end()) {
m_EntitiesToGhost.push_back(entity);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
CheckGhosting(entity);
return;
}
}
m_SerializationCounter++;
RakNet::BitStream stream;
stream.Write<uint8_t>(ID_REPLICA_MANAGER_CONSTRUCTION);
stream.Write(true);
stream.Write<uint16_t>(entity->GetNetworkId());
entity->WriteBaseReplicaData(stream, eReplicaPacketType::CONSTRUCTION);
entity->WriteComponents(stream, eReplicaPacketType::CONSTRUCTION);
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
for (auto* player : PlayerManager::GetAllPlayers()) {
// Don't need to construct the player to themselves
if (entity->GetObjectID() == player->GetObjectID()) continue;
if (player->GetPlayerReadyForUpdates()) {
Game::server->Send(stream, player->GetSystemAddress(), false);
} else {
auto* ghostComponent = player->GetComponent<GhostComponent>();
if (ghostComponent) ghostComponent->AddLimboConstruction(entity->GetObjectID());
}
}
} else {
Game::server->Send(stream, sysAddr, false);
}
}
void EntityManager::ConstructAllEntities(const SystemAddress& sysAddr) {
//ZoneControl is special:
ConstructEntity(m_ZoneControlEntity, sysAddr);
for (auto* entity : m_Entities | std::views::values) {
if (entity && (entity->GetSpawnerID() != 0 || entity->GetLOT() == 1) && !entity->GetIsGhostingCandidate()) {
ConstructEntity(entity, sysAddr);
}
}
UpdateGhosting(PlayerManager::GetPlayer(sysAddr));
}
void EntityManager::DestructEntity(Entity* entity, const SystemAddress& sysAddr) {
if (!entity || entity->GetNetworkId() == 0) return;
RakNet::BitStream stream;
stream.Write<uint8_t>(ID_REPLICA_MANAGER_DESTRUCTION);
stream.Write<uint16_t>(entity->GetNetworkId());
Game::server->Send(stream, sysAddr, sysAddr == UNASSIGNED_SYSTEM_ADDRESS);
// A player taken down for everyone is leaving the world: live then sent the meters its client hadn't been told
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS && entity->IsPlayer()) {
auto* characterComponent = entity->GetComponent<CharacterComponent>();
if (characterComponent) characterComponent->FlushMovementStatistics();
}
// Players still loading were to get it once loaded: only the ones it was taken down for (all of them, or the one
// player; taking it down for one player, e.g. out of their ghosting range, leaves the others' queue alone)
for (auto* player : PlayerManager::GetAllPlayers()) {
if (sysAddr != UNASSIGNED_SYSTEM_ADDRESS && player->GetSystemAddress() != sysAddr) continue;
if (!player->GetPlayerReadyForUpdates()) {
auto* ghostComponent = player->GetComponent<GhostComponent>();
if (ghostComponent) ghostComponent->RemoveLimboConstruction(entity->GetObjectID());
}
}
}
void EntityManager::SerializeEntity(Entity* entity) {
if (!entity) return;
EntityManager::SerializeEntity(*entity);
}
void EntityManager::SerializeEntity(const Entity& entity) {
if (entity.GetNetworkId() == 0) return;
if (std::find(m_EntitiesToSerialize.cbegin(), m_EntitiesToSerialize.cend(), entity.GetObjectID()) == m_EntitiesToSerialize.cend()) {
m_EntitiesToSerialize.push_back(entity.GetObjectID());
}
}
void EntityManager::DestructAllEntities(const SystemAddress& sysAddr) {
for (auto* entity : m_Entities | std::views::values) {
DestructEntity(entity, sysAddr);
}
}
void EntityManager::SetGhostDistanceMax(float value) {
m_GhostDistanceMaxSquared = value * value;
}
void EntityManager::SetGhostDistanceMin(float value) {
m_GhostDistanceMinSqaured = value * value;
}
void EntityManager::QueueGhostUpdate(LWOOBJID playerID) {
if (std::find(m_PlayersToUpdateGhosting.begin(), m_PlayersToUpdateGhosting.end(), playerID) == m_PlayersToUpdateGhosting.end()) {
m_PlayersToUpdateGhosting.push_back(playerID);
}
}
void EntityManager::UpdateGhosting() {
for (const auto playerID : m_PlayersToUpdateGhosting) {
auto* player = PlayerManager::GetPlayer(playerID);
if (!player) continue;
UpdateGhosting(player);
}
m_PlayersToUpdateGhosting.clear();
}
namespace {
// The scenes a player gets the objects of (ZoneScenes::SceneGraph::Loaded): around the ghost reference point, and
// around the player too while the reference is overridden
std::set<uint32_t> ClientScenes(const Zone& zone, const GhostComponent& ghost) {
const auto& reference = ghost.GetGhostReferencePoint();
const auto& position = ghost.GetOriginGhostReferencePoint();
const auto& map = zone.GetSceneMap();
return zone.GetSceneGraph().Loaded(map.SceneAt(reference.x, reference.z), map.SceneAt(position.x, position.z), ghost.GetGhostOverride());
}
}
void EntityManager::UpdateGhosting(Entity* player) {
if (!player) return;
auto* missionComponent = player->GetComponent<MissionComponent>();
auto* ghostComponent = player->GetComponent<GhostComponent>();
if (!missionComponent || !ghostComponent) return;
const auto& referencePoint = ghostComponent->GetGhostReferencePoint();
const auto isOverride = ghostComponent->GetGhostOverride();
const auto* zone = m_SceneGhosting ? Game::zoneManager->GetZone() : nullptr;
const auto loaded = zone ? ClientScenes(*zone, *ghostComponent) : std::set<uint32_t>{};
for (auto* entity : m_EntitiesToGhost) {
const auto& entityPoint = entity->GetPosition();
const auto id = entity->GetObjectID();
const auto observed = ghostComponent->IsObserved(id);
bool tooFar = false, nearEnough = false;
if (zone) {
// As the client streams scenes: the objects of the scenes loaded around the player
nearEnough = ZoneScenes::InLoadedScene(entity->GetScene(), zone->GetSceneMap().SceneAt(entityPoint.x, entityPoint.z), loaded);
tooFar = !nearEnough;
} else {
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
auto ghostingDistanceMax = m_GhostDistanceMaxSquared;
auto ghostingDistanceMin = m_GhostDistanceMinSqaured;
const auto isAudioEmitter = entity->GetLOT() == 6368; // https://explorer.lu/objects/6368
if (isAudioEmitter) {
ghostingDistanceMax = ghostingDistanceMin;
}
tooFar = distance > ghostingDistanceMax;
nearEnough = ghostingDistanceMin > distance;
}
if (observed && tooFar && !isOverride) {
ghostComponent->GhostEntity(id);
DestructEntity(entity, player->GetSystemAddress());
entity->SetObservers(entity->GetObservers() - 1);
} else if (!observed && nearEnough) {
// Check collectables, don't construct if it has been collected
uint32_t collectionId = entity->GetCollectibleID();
if (collectionId != 0) {
collectionId = static_cast<uint32_t>(collectionId) + static_cast<uint32_t>(Game::server->GetZoneID() << 8);
if (missionComponent->HasCollectible(collectionId)) {
continue;
}
}
ghostComponent->ObserveEntity(id);
entity->SetObservers(entity->GetObservers() + 1);
ConstructEntity(entity, player->GetSystemAddress());
}
}
}
void EntityManager::CheckGhosting(Entity* entity) {
if (!entity) return;
const auto& referencePoint = entity->GetPosition();
const auto* zone = m_SceneGhosting ? Game::zoneManager->GetZone() : nullptr;
const auto entityScene = zone ? zone->GetSceneMap().SceneAt(referencePoint.x, referencePoint.z) : 0;
for (auto* player : PlayerManager::GetAllPlayers()) {
auto* ghostComponent = player->GetComponent<GhostComponent>();
if (!ghostComponent) continue;
const auto& entityPoint = ghostComponent->GetGhostReferencePoint();
const auto id = entity->GetObjectID();
const auto observed = ghostComponent->IsObserved(id);
bool tooFar = false, nearEnough = false;
if (zone) {
const auto loaded = ClientScenes(*zone, *ghostComponent);
nearEnough = ZoneScenes::InLoadedScene(entity->GetScene(), entityScene, loaded);
tooFar = !nearEnough;
} else {
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
tooFar = distance > m_GhostDistanceMaxSquared;
nearEnough = m_GhostDistanceMinSqaured > distance;
}
if (observed && tooFar) {
ghostComponent->GhostEntity(id);
DestructEntity(entity, player->GetSystemAddress());
entity->SetObservers(entity->GetObservers() - 1);
} else if (!observed && nearEnough) {
ghostComponent->ObserveEntity(id);
ConstructEntity(entity, player->GetSystemAddress());
entity->SetObservers(entity->GetObservers() + 1);
}
}
}
Entity* EntityManager::GetGhostCandidate(LWOOBJID id) const {
for (auto* entity : m_EntitiesToGhost) {
if (entity->GetObjectID() == id) {
return entity;
}
}
return nullptr;
}
bool EntityManager::GetGhostingEnabled() const {
return m_GhostingEnabled;
}
void EntityManager::ScheduleForKill(Entity* entity) {
// Deactivate switches if they die
if (!entity)
return;
SwitchComponent* switchComp = entity->GetComponent<SwitchComponent>();
if (switchComp) {
entity->TriggerEvent(eTriggerEventType::DEACTIVATED, entity);
}
const auto objectId = entity->GetObjectID();
if (std::find(m_EntitiesToKill.begin(), m_EntitiesToKill.end(), objectId) == m_EntitiesToKill.end()) {
m_EntitiesToKill.push_back(objectId);
}
}
void EntityManager::ScheduleForDeletion(LWOOBJID entity) {
if (std::find(m_EntitiesToDelete.begin(), m_EntitiesToDelete.end(), entity) == m_EntitiesToDelete.end()) {
m_EntitiesToDelete.push_back(entity);
}
}
void EntityManager::FireEventServerSide(Entity* origin, std::string args) {
for (const auto entity : m_Entities | std::views::values) {
if (entity) {
entity->OnFireEventServerSide(origin, args);
}
}
}
bool EntityManager::IsExcludedFromGhosting(LOT lot) {
return std::find(m_GhostingExcludedLOTs.begin(), m_GhostingExcludedLOTs.end(), lot) != m_GhostingExcludedLOTs.end();
}
bool EntityManager::SendMessage(GameMessages::GameMsg& msg) const {
bool handled = false;
const auto entityItr = m_Entities.find(msg.target);
if (entityItr != m_Entities.end()) {
auto* const entity = entityItr->second;
if (entity) handled = entity->HandleMsg(msg);
}
return handled;
}