Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions imports/entity/shared.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
--[[
https://github.com/overextended/ox_lib

This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>

Copyright © 2025 Linden <https://github.com/thelindat>
]]

-- Capture the global `Entity()` state-bag accessor before our local class shadows it.
local getEntityStateBag = Entity

---Base class wrapping a CFX entity handle. Used directly via `lib.entity:new(handle)`
---to wrap any pre-existing entity, or as the parent of `lib.object`, `lib.ped`, and
---`lib.vehicle` for typed spawn wrappers.
---@class Entity : OxClass
---@field handle number Native entity handle.
---@field script string Resource that created or wrapped this entity.
local Entity = lib.class('Entity')

local IS_SERVER = IsDuplicityVersion()

---@param handle number
function Entity:constructor(handle)
local handleType = type(handle)
assert(handleType == 'number' and handle ~= 0, ('expected non-zero entity handle, got %s (%s)'):format(handleType, tostring(handle)))

self.handle = handle
self.script = GetInvokingResource() or cache.resource
end

function Entity:exists()
return DoesEntityExist(self.handle)
end

function Entity:delete()
if self:exists() then
DeleteEntity(self.handle)
end
end

---@return vector3
function Entity:getCoords()
return GetEntityCoords(self.handle)
end

---@param coords vector3
---@param alive? boolean Unused by the game; debug-only assert flag. Default `false`.
---@param deadFlag? boolean Disable physics for dead peds as well. Default `false`.
---@param ragdollFlag? boolean Special flag used for ragdolling peds. Default `false`.
---@param clearArea? boolean Clear any entities in the target area. Default `false`.
function Entity:setCoords(coords, alive, deadFlag, ragdollFlag, clearArea)
SetEntityCoords(self.handle, coords.x, coords.y, coords.z, alive or false, deadFlag or false, ragdollFlag or false, clearArea or false)
end

---@return number
function Entity:getHeading()
return GetEntityHeading(self.handle)
end

---@param heading number
function Entity:setHeading(heading)
SetEntityHeading(self.handle, heading + 0.0)
end

---@return vector3
function Entity:getRotation()
return GetEntityRotation(self.handle, 2)
end

---@param rotation vector3
function Entity:setRotation(rotation)
SetEntityRotation(self.handle, rotation.x + 0.0, rotation.y + 0.0, rotation.z + 0.0, 2, true)
end

---@return number
function Entity:getModel()
return GetEntityModel(self.handle)
end

---Returns the entity's state bag.
function Entity:getState()
return getEntityStateBag(self.handle).state
end

---Re-spawn the entity at new coords, preserving the original constructor data.
---Subclasses must provide a static `spawn(modelHash, data)` returning a new handle.
---@param coords? vector3 Defaults to the entity's current coords (or the original spawn coords).
---@param heading? number Defaults to the entity's current heading.
---@return number? handle New entity handle, or nil on failure.
function Entity:respawn(coords, heading)
local cls = getmetatable(self)
if type(cls.spawn) ~= 'function' then
error(('%s:respawn is not implemented (missing static `spawn`)'):format(cls.__name or 'Entity'), 2)
end

local priv = self.private or {}
local exists = self:exists()
local modelHash = priv.modelHash or (exists and GetEntityModel(self.handle)) or nil
local fallbackCoords = exists and self:getCoords() or nil
local fallbackHeading = exists and self:getHeading() or nil

coords = coords or fallbackCoords or (priv.spawnData and priv.spawnData.coords)
if not coords then return nil end
heading = heading or fallbackHeading or (priv.spawnData and priv.spawnData.heading)

if not modelHash then return nil end

if exists then DeleteEntity(self.handle) end

local data = priv.spawnData and table.clone(priv.spawnData) or {}
data.coords = coords
data.heading = heading

local newHandle = cls.spawn(modelHash, data)
if newHandle == 0 then return nil end

self.handle = newHandle

if heading then self:setHeading(heading) end
if data.rotation then self:setRotation(data.rotation) end

if IS_SERVER and cache.game ~= 'redm' then
self:setOrphanMode(data.orphanMode or 2)
end

-- Cache the latest spawn data in case it was mutated.
if self.private then self.private.spawnData = data end

self:onAfterRespawn(data)

return newHandle
end

---@protected
---@param data table The cloned spawn data used for this respawn.
function Entity:onAfterRespawn(data) end

if IS_SERVER then
local allowClientServerEntityCreation = GetConvarInt('ox:allowClientServerEntityCreation', 0) == 1

---@return number networkId
function Entity:getNetworkId()
return NetworkGetNetworkIdFromEntity(self.handle)
end

---@param mode EntityOrphanMode
function Entity:setOrphanMode(mode)
if cache.game == 'redm' then
lib.print.warn('Entity:setOrphanMode is unavailable on RedM (no SetEntityOrphanMode native); ignoring call.')
return
end

SetEntityOrphanMode(self.handle, mode)
end

---@protected
---@param spawn fun(modelHash: number, data: table): number Native spawner returning the entity handle.
---@param data table Spawn data; `data.model` may be a string or precomputed hash.
---@param assetType string Label used in error messages (`'object'`, `'ped'`, `'vehicle'`).
---@return number handle
---@return number modelHash
function Entity.createServer(spawn, data, assetType)
local modelHash = type(data.model) == 'number' and data.model or joaat(data.model) --[[@as number]]
local handle = spawn(modelHash, data)

if handle == 0 then
error(('failed to spawn %s %s'):format(assetType, data.model), 3)
end

local ok, err = pcall(lib.waitFor, function()
if DoesEntityExist(handle) then return true end
end, ('%s %s did not materialize'):format(assetType, data.model), 5000)

if not ok then
lib.print.error(err)
if DoesEntityExist(handle) then DeleteEntity(handle) end
error(('%s failed to spawn within timeout'):format(assetType), 3)
end

return handle, modelHash
end

---Registers the client→server spawn proxy callback for a subclass.
---When `ox:allowClientServerEntityCreation` is enabled, the callback constructs
---@param cls table Subclass to instantiate (e.g. `ObjectServer`).
---@param callbackName string Callback identifier, e.g. `'ox_lib:createObject'`.
---@param assetType string Label used in warnings (`'object'`, `'ped'`, `'vehicle'`).
function Entity.registerCreateCallback(cls, callbackName, assetType)
lib.callback.register(callbackName, function(source, data)
if not allowClientServerEntityCreation then
lib.print.warn(('player %d attempted server-side %s spawn but convar is disabled'):format(source, assetType))
return nil
end

local ok, instance = pcall(cls.new, cls, data)
if not ok then
lib.print.error(instance)
return nil
end

return instance:getNetworkId()
end)
end
else
local allowClientEntityCreation = GetConvarInt('ox:allowClientEntityCreation', 0) == 1
local allowClientServerEntityCreation = GetConvarInt('ox:allowClientServerEntityCreation', 0) == 1

---@return boolean
function Entity:isNetworked()
return NetworkGetEntityIsNetworked(self.handle)
end

---@return number? networkId nil if the entity is not networked.
function Entity:getNetworkId()
if not NetworkGetEntityIsNetworked(self.handle) then return nil end
return NetworkGetNetworkIdFromEntity(self.handle)
end

---@protected
---Shared client spawn flow used by `lib.object`, `lib.ped`, and `lib.vehicle`.
---@param spawn fun(modelHash: number, data: table): number Native spawner used for local creation.
---@param data table Spawn data forwarded to the server callback or to `spawn`.
---@param callbackName string Server callback identifier, e.g. `'ox_lib:createObject'`.
---@param assetType string Label used in error messages (`'object'`, `'ped'`, `'vehicle'`).
---@return number handle
---@return number? modelHash
function Entity.createClient(spawn, data, callbackName, assetType)
local wantsNetwork = data.isNetwork == true

if wantsNetwork and not allowClientEntityCreation then
error(('client-side networked %s creation is disabled (set `ox:allowClientEntityCreation`)'):format(assetType), 3)
end

local useProxy = wantsNetwork and allowClientServerEntityCreation
local handle, modelHash

if useProxy then
local netId = lib.callback.await(callbackName, false, data)
if not netId or netId == 0 then
error(('server refused or failed to spawn %s %s'):format(assetType, data.model), 3)
end

local ok, syncedHandle = pcall(lib.waitFor, function()
local h = NetworkGetEntityFromNetworkId(netId)
if h ~= 0 and DoesEntityExist(h) then return h end
end, ('%s netId %s did not sync to client'):format(assetType, netId), 5000)

if not ok then
lib.print.error(syncedHandle)
end

handle = ok and syncedHandle or 0
modelHash = handle ~= 0 and GetEntityModel(handle) or nil
else
modelHash = lib.requestModel(data.model, 10000)
handle = spawn(modelHash, data)
SetModelAsNoLongerNeeded(modelHash)
end

if handle == 0 or not DoesEntityExist(handle) then
error(('failed to spawn %s %s'):format(assetType, data.model), 3)
end

return handle, modelHash
end
end

lib.entity = Entity

return lib.entity
61 changes: 61 additions & 0 deletions imports/object/client.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--[[
https://github.com/overextended/ox_lib

This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>

Copyright © 2025 Linden <https://github.com/thelindat>
]]

---@class ObjectInitClient
---@field model string | number Model name or precomputed hash.
---@field coords vector3 Spawn coordinate.
---@field heading? number Applied via `SetEntityHeading` after spawn.
---@field rotation? vector3 Applied via `SetEntityRotation` after spawn (rotation order 2).
---@field isNetwork? boolean Whether to create a network object. Default `false`.
---@field netMissionEntity? boolean **GTA5 only.** Pin to script host. Default `false`.
---@field doorFlag? boolean **GTA5 only.** Set true to spawn door models in network mode.
---@field bScriptHostObj? boolean **RedM only.** Pin to script host. Default `false`.
---@field dynamic? boolean **RedM only.** Whether the object should be dynamic.
---@field p7? boolean **RedM only.** Undocumented. Default `false`.
---@field p8? boolean **RedM only.** Undocumented. Default `false`.

---Client-side spawnable object.
---@class ObjectClient : Entity
local ObjectClient = lib.class('ObjectClient', lib.entity)

---@param data ObjectInitClient
function ObjectClient:constructor(data)
assert(type(data) == 'table', 'expected table init data')
assert(data.coords and data.coords.x and data.coords.y and data.coords.z, 'expected vector3 coords')
assert(type(data.model) == 'string' or type(data.model) == 'number', 'expected string or number model')

local handle, modelHash = lib.entity.createClient(ObjectClient.spawn, data, 'ox_lib:createObject', 'object')

self:super(handle)

self.private.spawnData = data
self.private.modelHash = modelHash

if data.heading then self:setHeading(data.heading) end
if data.rotation then self:setRotation(data.rotation) end
end

---@protected
---Internal spawn helper used by both the constructor and `:respawn()`.
---@param modelHash number
---@param data ObjectInitClient
---@return number handle
function ObjectClient.spawn(modelHash, data)
if cache.game == 'redm' then
return CreateObject(modelHash, data.coords.x, data.coords.y, data.coords.z,
data.isNetwork or false, data.bScriptHostObj or false,
data.dynamic or false, data.p7 or false, data.p8 or false)
end

return CreateObject(modelHash, data.coords.x, data.coords.y, data.coords.z,
data.isNetwork or false, data.netMissionEntity or false, data.doorFlag or false)
end

lib.object = ObjectClient

return lib.object
65 changes: 65 additions & 0 deletions imports/object/server.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--[[
https://github.com/overextended/ox_lib

This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>

Copyright © 2025 Linden <https://github.com/thelindat>
]]

---@class ObjectInitServer
---@field model string | number Model name or precomputed hash.
---@field coords vector3 Spawn coordinate.
---@field heading? number Applied via `SetEntityHeading` after spawn.
---@field rotation? vector3 Applied via `SetEntityRotation` after spawn (rotation order 2).
---@field orphanMode? EntityOrphanMode Server-side cleanup behavior. Default `2` (KeepEntity).
---@field doorFlag? boolean **GTA5 only.** Set true to spawn door models in network mode.
---@field dynamic? boolean **RedM only.** Whether the object should be dynamic (physics-driven).
---@field bScriptHostObj? boolean **RedM only.** Pin to script host. Defaults to `true`.
---@field p7? boolean **RedM only.** Undocumented. Default `false`.
---@field p8? boolean **RedM only.** Undocumented. Default `false`.

---Server-side spawnable object.
---@class ObjectServer : Entity
local ObjectServer = lib.class('ObjectServer', lib.entity)

---@param data ObjectInitServer
function ObjectServer:constructor(data)
assert(type(data) == 'table', 'expected table init data')
assert(data.coords and data.coords.x and data.coords.y and data.coords.z, 'expected vector3 coords')
assert(type(data.model) == 'string' or type(data.model) == 'number', 'expected string or number model')

local handle, modelHash = lib.entity.createServer(ObjectServer.spawn, data, 'object')

self:super(handle)

self.private.spawnData = data
self.private.modelHash = modelHash

if data.heading then self:setHeading(data.heading) end
if data.rotation then self:setRotation(data.rotation) end

if cache.game ~= 'redm' then
self:setOrphanMode(data.orphanMode or 2)
end
end

---@protected
---Internal spawn helper used by both the constructor and `:respawn()`.
---@param modelHash number
---@param data ObjectInitServer
---@return number handle
function ObjectServer.spawn(modelHash, data)
if cache.game == 'redm' then
return CreateObject(modelHash, data.coords.x, data.coords.y, data.coords.z,
true, data.bScriptHostObj or false, data.dynamic or false, data.p7 or false, data.p8 or false)
end

return CreateObject(modelHash, data.coords.x, data.coords.y, data.coords.z, true, true, data.doorFlag or false)
end

lib.object = ObjectServer

-- Client→server proxy (gated by `ox:allowClientServerEntityCreation`).
lib.entity.registerCreateCallback(ObjectServer, 'ox_lib:createObject', 'object')

return lib.object
Loading