From e7ec01f531a71a9174badf679ab5d12bdc4784d7 Mon Sep 17 00:00:00 2001 From: Dillon Skaggs Date: Wed, 21 Aug 2024 00:22:10 -0500 Subject: [PATCH] feat(server/state): add `[GET/SET]_ENTITY_ORPHAN_MODE` - this allows the end user to be able to define how they want an entity to be deleted instead of automatically cleaning up every entity on entity GC - this is mainly useful for allowing the server to keep specific client entities (like vehicles) from being automatically deleted - this allows for the same persistence as server created entities but without the draw backs. - also allows for specific entities (like objects) to be marked for deletion whenever the client leaves the server --- .../include/state/ServerGameState.h | 8 ++ .../src/state/ServerGameState.cpp | 21 ++++- .../src/state/ServerGameState_Scripting.cpp | 82 +++++++++++++++++++ ext/native-decls/GetEntityOrphanMode.md | 15 ++++ ext/native-decls/SetEntityOrphanMode.md | 32 ++++++++ 5 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 ext/native-decls/GetEntityOrphanMode.md create mode 100644 ext/native-decls/SetEntityOrphanMode.md diff --git a/code/components/citizen-server-impl/include/state/ServerGameState.h b/code/components/citizen-server-impl/include/state/ServerGameState.h index 58c48e137c..8a23400249 100644 --- a/code/components/citizen-server-impl/include/state/ServerGameState.h +++ b/code/components/citizen-server-impl/include/state/ServerGameState.h @@ -788,6 +788,13 @@ struct SyncTreeBase virtual bool IsEntityVisible(bool* visible) = 0; }; +enum EntityOrphanMode : uint8_t +{ + DeleteWhenNotRelevant = 0, + DeleteOnOwnerDisconnect = 1, + KeepEntity = 2, +}; + struct SyncEntityState { using TData = std::variant; @@ -832,6 +839,7 @@ struct SyncEntityState bool passedFilter = false; bool wantsReassign = false; bool firstOwnerDropped = false; + EntityOrphanMode orphanMode = EntityOrphanMode::DeleteWhenNotRelevant; std::list> onCreationRPC; diff --git a/code/components/citizen-server-impl/src/state/ServerGameState.cpp b/code/components/citizen-server-impl/src/state/ServerGameState.cpp index e5ec2033fa..94f413a809 100644 --- a/code/components/citizen-server-impl/src/state/ServerGameState.cpp +++ b/code/components/citizen-server-impl/src/state/ServerGameState.cpp @@ -946,7 +946,7 @@ void ServerGameState::Tick(fx::ServerInstanceBase* instance) continue; } // it's a client-owned entity, let's check for a few things - else if (entity->IsOwnedByClientScript()) + else if (entity->IsOwnedByClientScript() && entity->orphanMode != sync::KeepEntity) { // is the original owner offline? if (entity->firstOwnerDropped) @@ -957,7 +957,7 @@ void ServerGameState::Tick(fx::ServerInstanceBase* instance) } } // it's a script-less entity, we can collect it. - else if (!entity->IsOwnedByScript() && (entity->type != sync::NetObjEntityType::Player || !entity->GetClient())) + else if (!entity->IsOwnedByScript() && (entity->type != sync::NetObjEntityType::Player || !entity->GetClient()) && entity->orphanMode != sync::KeepEntity) { FinalizeClone({}, entity, entity->handle, 0, "Regular entity GC"); continue; @@ -2882,11 +2882,19 @@ void ServerGameState::HandleClientDrop(const fx::ClientSharedPtr& client, uint16 { continue; } + + bool markedForDeletion = false; auto firstOwner = entity->GetFirstOwner(); if (firstOwner && firstOwner->GetNetId() == client->GetNetId()) { entity->firstOwnerDropped = true; + + if (entity->orphanMode == sync::DeleteOnOwnerDisconnect) + { + toErase.insert(entity->handle); + markedForDeletion = true; + } } if (hasSlotId) @@ -2903,6 +2911,12 @@ void ServerGameState::HandleClientDrop(const fx::ClientSharedPtr& client, uint16 entity->lastFramesSent[slotId] = 0; } } + + // we don't want to re-assign the entity at all, this entity should be deleted on the next tick + if (markedForDeletion) + { + continue; + } if (!MoveEntityToCandidate(entity, client)) { @@ -2910,7 +2924,8 @@ void ServerGameState::HandleClientDrop(const fx::ClientSharedPtr& client, uint16 { ReassignEntity(entity->handle, firstOwner); } - else + // we don't want to add these to the list to remove if they're set to be kept when orphaned + else if (entity->orphanMode != sync::KeepEntity) { toErase.insert(entity->handle); } diff --git a/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp b/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp index c7458085b4..b8549e4f55 100644 --- a/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp +++ b/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp @@ -139,6 +139,41 @@ static void Init() context.SetResult(true); }); + +#if _DEBUG + fx::ScriptEngine::RegisterNativeHandler("IS_ENTITY_RELEVANT", [](fx::ScriptContext& context) + { + // get the current resource manager + auto resourceManager = fx::ResourceManager::GetCurrent(); + + // get the owning server instance + auto instance = resourceManager->GetComponent()->Get(); + + // get the server's game state + auto gameState = instance->GetComponent(); + + // parse the client ID + auto id = context.GetArgument(0); + + if (!id) + { + context.SetResult(false); + return; + } + + auto entity = gameState->GetEntity(id); + + if (!entity || entity->finalizing || entity->deleting) + { + context.SetResult(false); + return; + } + + std::lock_guard l(entity->guidMutex); + + context.SetResult(entity->relevantTo.any()); + }); +#endif fx::ScriptEngine::RegisterNativeHandler("NETWORK_GET_ENTITY_FROM_NETWORK_ID", [](fx::ScriptContext& context) { @@ -196,6 +231,53 @@ static void Init() return retval; })); + + fx::ScriptEngine::RegisterNativeHandler("SET_ENTITY_ORPHAN_MODE", [](fx::ScriptContext& context) + { + // get the current resource manager + auto resourceManager = fx::ResourceManager::GetCurrent(); + + // get the owning server instance + auto instance = resourceManager->GetComponent()->Get(); + + // get the server's game state + auto gameState = instance->GetComponent(); + + // parse the client ID + auto id = context.GetArgument(0); + + if (!id) + { + return; + } + + auto entity = gameState->GetEntity(id); + + if (!entity) + { + throw std::runtime_error(va("Tried to access invalid entity: %d", id)); + } + + auto orphanMode = context.GetArgument(1); + + if (orphanMode < 0 || orphanMode > fx::sync::KeepEntity) + { + throw std::runtime_error(va("Tried to set entities (%d) orphan mode to an invalid orphan mode: %d", id, orphanMode)); + } + + entity->orphanMode = static_cast(orphanMode); + + // if they set the orphan mode to `DeleteOnOwnerDisconnect` and the entity already doesn't have an owner then treat this as a `DELETE_ENTITY` call + if (entity->orphanMode == fx::sync::DeleteOnOwnerDisconnect && entity->firstOwnerDropped) + { + gameState->DeleteEntity(entity); + } + }); + + fx::ScriptEngine::RegisterNativeHandler("GET_ENTITY_ORPHAN_MODE", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) + { + return entity->orphanMode; + })); fx::ScriptEngine::RegisterNativeHandler("GET_ENTITY_COORDS", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) { diff --git a/ext/native-decls/GetEntityOrphanMode.md b/ext/native-decls/GetEntityOrphanMode.md new file mode 100644 index 0000000000..3cf8f0c4db --- /dev/null +++ b/ext/native-decls/GetEntityOrphanMode.md @@ -0,0 +1,15 @@ +--- +ns: CFX +apiset: server +--- +## GET_ENTITY_ORPHAN_MODE + +```c +int GET_ENTITY_ORPHAN_MODE(Entity entity); +``` + +## Parameters +* **entity**: The entity to get the orphan mode of + +## Return value +Returns the entities current orphan mode, refer to enum in [SET_ENTITY_ORPHAN_MODE](#_0x489E9162) diff --git a/ext/native-decls/SetEntityOrphanMode.md b/ext/native-decls/SetEntityOrphanMode.md new file mode 100644 index 0000000000..cc6db7f3b2 --- /dev/null +++ b/ext/native-decls/SetEntityOrphanMode.md @@ -0,0 +1,32 @@ +--- +ns: CFX +apiset: server +--- +## SET_ENTITY_ORPHAN_MODE + +```c +void SET_ENTITY_ORPHAN_MODE(Entity entity, int orphanMode); +``` + +```c +enum EntityOrphanMode { + // Default, this will delete the entity when it isn't relevant to any players + // NOTE: this *doesn't* mean when they're no longer in scope + DeleteWhenNotRelevant = 0, + // The entity will be deleted whenever its original owner disconnects + // NOTE: if this is set when the entities original owner has already left it will be + // marked for deletion (similar to just calling DELETE_ENTITY) + DeleteOnOwnerDisconnect = 1, + // The entity will never be deleted by the server when it does relevancy checks + // you should only use this on entities that need to be relatively persistent + KeepEntity = 2 +} +``` + +Sets what happens when the entity is orphaned and no longer has its original owner. + +**NOTE**: This native doesn't guarantee the persistence of the entity. + +## Parameters +* **entity**: The entity to set the orphan mode of +* **orphanMode**: The mode that the server should use for determining if an entity should be removed.