Skip to content

Commit

Permalink
feat(server/state): add [GET/SET]_ENTITY_ORPHAN_MODE
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
AvarianKnight committed Sep 25, 2024
1 parent 8a1586d commit e7ec01f
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, float, bool, std::string>;
Expand Down Expand Up @@ -832,6 +839,7 @@ struct SyncEntityState
bool passedFilter = false;
bool wantsReassign = false;
bool firstOwnerDropped = false;
EntityOrphanMode orphanMode = EntityOrphanMode::DeleteWhenNotRelevant;

std::list<std::function<void(const fx::ClientSharedPtr& ptr)>> onCreationRPC;

Expand Down
21 changes: 18 additions & 3 deletions code/components/citizen-server-impl/src/state/ServerGameState.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -2903,14 +2911,21 @@ 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))
{
if (entity->IsOwnedByClientScript() && !entity->firstOwnerDropped)
{
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<fx::ServerInstanceBaseRef>()->Get();

// get the server's game state
auto gameState = instance->GetComponent<fx::ServerGameState>();

// parse the client ID
auto id = context.GetArgument<uint32_t>(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)
{
Expand Down Expand Up @@ -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<fx::ServerInstanceBaseRef>()->Get();

// get the server's game state
auto gameState = instance->GetComponent<fx::ServerGameState>();

// parse the client ID
auto id = context.GetArgument<uint32_t>(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<int>(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<fx::sync::EntityOrphanMode>(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)
{
Expand Down
15 changes: 15 additions & 0 deletions ext/native-decls/GetEntityOrphanMode.md
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 32 additions & 0 deletions ext/native-decls/SetEntityOrphanMode.md
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit e7ec01f

Please sign in to comment.