mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2024-11-13 19:58:21 +00:00
b261e63233
* chore: Change entity and component logic to use bitstream references * merge
601 lines
17 KiB
C++
601 lines
17 KiB
C++
#include "EntityManager.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 "Metrics.hpp"
|
|
#include "dZoneManager.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 <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
|
|
};
|
|
|
|
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();
|
|
|
|
// grab hardcore mode settings and load them with sane defaults
|
|
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 : std::stoi(hcUscorePercent);
|
|
auto hcUscoreMult = Game::config->GetValue("hardcore_uscore_enemies_multiplier");
|
|
m_HardcoreUscoreEnemiesMultiplier = hcUscoreMult.empty() ? 2 : std::stoi(hcUscoreMult);
|
|
auto hcDropInv = Game::config->GetValue("hardcore_dropinventory_on_death");
|
|
m_HardcoreDropinventoryOnDeath = hcDropInv.empty() ? false : (hcDropInv == "1");
|
|
|
|
// 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) {
|
|
|
|
// 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 && info.lot != 14) {
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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::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) {
|
|
for (auto* entity : m_Entities | std::views::values) {
|
|
entity->Update(deltaTime);
|
|
}
|
|
|
|
SerializeEntities();
|
|
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);
|
|
}
|
|
}
|
|
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, const bool skipChecks) {
|
|
if (!entity) {
|
|
LOG("Attempted to construct null entity");
|
|
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) {
|
|
if (skipChecks) {
|
|
Game::server->Send(stream, UNASSIGNED_SYSTEM_ADDRESS, true);
|
|
} else {
|
|
for (auto* player : PlayerManager::GetAllPlayers()) {
|
|
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);
|
|
}
|
|
|
|
if (entity->IsPlayer()) {
|
|
if (entity->GetGMLevel() > eGameMasterLevel::CIVILIAN) {
|
|
GameMessages::SendToggleGMInvis(entity->GetObjectID(), true, sysAddr);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
for (auto* player : PlayerManager::GetAllPlayers()) {
|
|
if (!player->GetPlayerReadyForUpdates()) {
|
|
auto* ghostComponent = player->GetComponent<GhostComponent>();
|
|
if (ghostComponent) ghostComponent->RemoveLimboConstruction(entity->GetObjectID());
|
|
}
|
|
}
|
|
}
|
|
|
|
void EntityManager::SerializeEntity(Entity* entity) {
|
|
if (!entity || entity->GetNetworkId() == 0) return;
|
|
|
|
if (std::find(m_EntitiesToSerialize.begin(), m_EntitiesToSerialize.end(), entity->GetObjectID()) == m_EntitiesToSerialize.end()) {
|
|
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 == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
UpdateGhosting(player);
|
|
}
|
|
|
|
m_PlayersToUpdateGhosting.clear();
|
|
}
|
|
|
|
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();
|
|
|
|
for (auto* entity : m_EntitiesToGhost) {
|
|
const auto& entityPoint = entity->GetPosition();
|
|
|
|
const auto id = entity->GetObjectID();
|
|
|
|
const auto observed = ghostComponent->IsObserved(id);
|
|
|
|
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;
|
|
}
|
|
|
|
if (observed && distance > ghostingDistanceMax && !isOverride) {
|
|
ghostComponent->GhostEntity(id);
|
|
|
|
DestructEntity(entity, player->GetSystemAddress());
|
|
|
|
entity->SetObservers(entity->GetObservers() - 1);
|
|
} else if (!observed && ghostingDistanceMin > distance) {
|
|
// 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);
|
|
|
|
ConstructEntity(entity, player->GetSystemAddress());
|
|
|
|
entity->SetObservers(entity->GetObservers() + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EntityManager::CheckGhosting(Entity* entity) {
|
|
if (entity == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const auto& referencePoint = entity->GetPosition();
|
|
|
|
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);
|
|
|
|
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
|
|
|
|
if (observed && distance > m_GhostDistanceMaxSquared) {
|
|
ghostComponent->GhostEntity(id);
|
|
|
|
DestructEntity(entity, player->GetSystemAddress());
|
|
|
|
entity->SetObservers(entity->GetObservers() - 1);
|
|
} else if (!observed && m_GhostDistanceMinSqaured > distance) {
|
|
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();
|
|
}
|