From deff689b84de3e05072ab36c18495e34c14bb8aa Mon Sep 17 00:00:00 2001 From: Dillon Skaggs Date: Thu, 28 May 2026 12:51:41 -0500 Subject: [PATCH] feat(server/state): add `NETWORK_SET_FIRST_ENTITY_OWNER` for server created entitys This allows the server to set the `firstOwner` of the entity when it is spawned, forcing that client (and that client only) to be in charge of the server created entity. This is mainly useful for vehicles, where you can typically expect a certain client to be close to the vehicle that it is spawning, but we need it to be server spawned for persistance and to ensure vehicles aren't created by modders. This is only meant for testing, this needs a proper way to tell if a vehicle, ped or object *can be* relevant to a client. Since it's currently possible that you set the entity owner and then the client never actually syncs the vehicle. This mainly relies on `hasSynced` relevancy checks to stop this from ever migrating the vehicle until we've properly synchronized the vehicle, since before this could just bounce between multiple different clients. --- .../src/state/ServerGameState_Scripting.cpp | 39 +++++++++++++++++++ .../NetworkSetFirstEntityOwner.md | 37 ++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 ext/native-decls/NetworkSetFirstEntityOwner.md 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 bb64bc2953..691e0fe091 100644 --- a/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp +++ b/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp @@ -242,6 +242,45 @@ static void Init() return retval; })); + + fx::ScriptEngine::RegisterNativeHandler("NETWORK_SET_FIRST_ENTITY_OWNER", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) + { + auto resourceManager = fx::ResourceManager::GetCurrent(); + auto instance = resourceManager->GetComponent()->Get(); + auto clientRegistry = instance->GetComponent(); + + auto playerSrc = context.GetArgument(1); + + int playerNetId = atoi(playerSrc); + if (playerNetId == 0) + { + throw std::runtime_error(va("NETWORK_SET_FIRST_ENTITY_OWNER: invalid player: %s", playerSrc)); + } + + bool firstOwnerExists = !!entity->GetFirstOwner(); + bool hasClient = !!entity->GetClient(); + + // We only allow this to be set whenever we don't have an initial owner and the entity isn't already sync'd to clients + if (firstOwnerExists || hasClient) + { + fx::scripting::Warningf("net", "Tried to set the first owner for entity: %u but it already had an owner or client, hasFirstOwner: %d, hasClient: %d", context.GetArgument(0), firstOwnerExists, hasClient); + return false; + } + + auto client = clientRegistry->GetClientByNetID(playerNetId); + if (!client) + { + return false; + } + + // set all of the owner fields that are expected, this is safe since this shouldn't be broadcasted yet if we just created the entity. + entity->GetFirstOwnerUnsafe() = client; + entity->GetLastOwnerUnsafe() = client; + entity->GetClientUnsafe() = client; + + return true; + })); + fx::ScriptEngine::RegisterNativeHandler("SET_ENTITY_ORPHAN_MODE", [](fx::ScriptContext& context) { // get the current resource manager diff --git a/ext/native-decls/NetworkSetFirstEntityOwner.md b/ext/native-decls/NetworkSetFirstEntityOwner.md new file mode 100644 index 0000000000..c0794eb5e2 --- /dev/null +++ b/ext/native-decls/NetworkSetFirstEntityOwner.md @@ -0,0 +1,37 @@ +--- +ns: CFX +apiset: server +--- +## NETWORK_SET_FIRST_ENTITY_OWNER + +```c +void NETWORK_SET_FIRST_ENTITY_OWNER(Entity entity, char* playerSrc); +``` + +**WARNING**: This native is not planned to be ported to Enhanced. You should make sure this native exists before calling it. + +In Lua you can do this by doing `if NetworkSetFirstEntityOwner then` in JS you can do this by checking that `typeof globalThis.NetworkGetFirstEntityOwner === "function"`. + +If you fail to do this when enhanced is released, it is likely that native will error and you will be expected to fix it. + +**NOTE**: This only works for server created entities, and only work in the same tick that the server-created entity was made, trying to call this after an owner has been assigned will do nothing. + +This native sets the first entity owner for server-created entitys in order to reduce the migration the server might do with the entity. + +## Parameters +* **entity**: The entity to set the first owner for. +* **playerSrc**: The player to set as the owner + +## Examples +```lua +-- for this example we just get the first player +local player = GetPlayers()[1] + +-- create the helicopter +local heli = CreateVehicleServerSetter(`seasparrow`, 'heli', GetEntityCoords(GetPlayerPed(player)) + vector3(0, 0, 15), 0.0) + +-- and set the first entity owner to the player +if NetworkSetFirstEntityOwner then + NetworkSetFirstEntityOwner(heli, player) +end +```