diff --git a/imports/entity/shared.lua b/imports/entity/shared.lua new file mode 100644 index 000000000..9c52e0634 --- /dev/null +++ b/imports/entity/shared.lua @@ -0,0 +1,270 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +-- 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 diff --git a/imports/object/client.lua b/imports/object/client.lua new file mode 100644 index 000000000..589706ca5 --- /dev/null +++ b/imports/object/client.lua @@ -0,0 +1,61 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@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 diff --git a/imports/object/server.lua b/imports/object/server.lua new file mode 100644 index 000000000..43b4d2f9a --- /dev/null +++ b/imports/object/server.lua @@ -0,0 +1,65 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@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 diff --git a/imports/ped/client.lua b/imports/ped/client.lua new file mode 100644 index 000000000..a3c4b106a --- /dev/null +++ b/imports/ped/client.lua @@ -0,0 +1,63 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@class PedInitClient +---@field model string | number Model name or precomputed hash. +---@field coords vector3 Spawn coordinate. +---@field heading? number Heading the ped should face, in degrees. +---@field isNetwork? boolean Whether to create a network ped. Default `false`. +---@field bScriptHostPed? boolean Pin to script host. Default `false`. +---@field pedType? number **GTA5 only.** AI behavior type. 4 = CIVMALE, etc. Default 0. +---@field p7? boolean **RedM only.** Undocumented. Default `false`. +---@field p8? boolean **RedM only.** Undocumented. Default `false`. + +---Client-side spawnable ped. +---@class PedClient : Entity +local PedClient = lib.class('PedClient', lib.entity) + +---@param data PedInitClient +function PedClient: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(PedClient.spawn, data, 'ox_lib:createPed', 'ped') + + self:super(handle) + + self.private.spawnData = data + self.private.modelHash = modelHash +end + +---@protected +---@param modelHash number +---@param data PedInitClient +---@return number handle +function PedClient.spawn(modelHash, data) + local headingValue = data.heading and data.heading + 0.0 or 0.0 + + if cache.game == 'redm' then + return CreatePed(modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, + data.isNetwork or false, data.bScriptHostPed or false, data.p7 or false, data.p8 or false) + end + + return CreatePed(data.pedType or 0, modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, + data.isNetwork or false, data.bScriptHostPed or false) +end + +---Warp the ped into a vehicle seat. Client-only; uses `TaskWarpPedIntoVehicle`. +---@param vehicle number | Entity Vehicle handle or wrapper instance. +---@param seat? number Seat index. Default `-1` (driver). +function PedClient:warpInto(vehicle, seat) + local vehicleHandle = type(vehicle) == 'table' and vehicle.handle or vehicle + TaskWarpPedIntoVehicle(self.handle, vehicleHandle, seat or -1) +end + +lib.ped = PedClient + +return lib.ped diff --git a/imports/ped/server.lua b/imports/ped/server.lua new file mode 100644 index 000000000..d4b70ebc4 --- /dev/null +++ b/imports/ped/server.lua @@ -0,0 +1,66 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@class PedInitServer +---@field model string | number Model name or precomputed hash. +---@field coords vector3 Spawn coordinate. +---@field heading? number Heading the ped should face, in degrees. +---@field pedType? number **GTA5 only.** AI behavior type. 4 = CIVMALE, 5 = CIVFEMALE, etc. Default 0. Ignored on RedM. +---@field orphanMode? EntityOrphanMode Server-side cleanup behavior. Default `2` (KeepEntity). + +---Server-side spawnable ped. +---@class PedServer : Entity +local PedServer = lib.class('PedServer', lib.entity) + +---@param data PedInitServer +function PedServer: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(PedServer.spawn, data, 'ped') + + self:super(handle) + + self.private.spawnData = data + self.private.modelHash = modelHash + + if cache.game ~= 'redm' then + self:setOrphanMode(data.orphanMode or 2) + end +end + +---@protected +---@param modelHash number +---@param data PedInitServer +---@return number handle +function PedServer.spawn(modelHash, data) + local headingValue = data.heading and data.heading + 0.0 or 0.0 + + if cache.game == 'redm' then + -- RedM CFX CreatePed: pedType is `Unused` per the native docs but still required as the first arg. + return CreatePed(0, modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, true, true) + end + + return CreatePed(data.pedType or 0, modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, true, true) +end + +---Place the ped into a vehicle seat. Uses the server-side `SetPedIntoVehicle` native +---(no `TaskWarpPedIntoVehicle` on the server). +---@param vehicle number | Entity Vehicle handle or wrapper instance. +---@param seat? number Seat index. Default `-1` (driver). +function PedServer:setIntoVehicle(vehicle, seat) + local vehicleHandle = type(vehicle) == 'table' and vehicle.handle or vehicle + SetPedIntoVehicle(self.handle, vehicleHandle, seat or -1) +end + +lib.ped = PedServer + +lib.entity.registerCreateCallback(PedServer, 'ox_lib:createPed', 'ped') + +return lib.ped diff --git a/imports/vehicle/client.lua b/imports/vehicle/client.lua new file mode 100644 index 000000000..2fbd23ea3 --- /dev/null +++ b/imports/vehicle/client.lua @@ -0,0 +1,95 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---Seat index for vehicle natives. Accepts the `eSeatPosition` enum constants +---(`-2 = SF_ANY`, `-1 = driver`, `0 = front passenger`, …) or any raw integer. +---@alias SeatPosition eSeatPosition | number + +---@class VehicleInitClient +---@field model string | number Model name or precomputed hash. +---@field coords vector3 Spawn coordinate. +---@field heading? number Heading the vehicle should face, in degrees. +---@field isNetwork? boolean Whether to create a network vehicle. Default `false`. +---@field netMissionEntity? boolean **GTA5 only.** Pin to script host. Default `false`. +---@field bScriptHostVeh? boolean **RedM only.** Pin to script host. Default `false`. +---@field bDontAutoCreateDraftAnimals? boolean **RedM only.** Skip auto-creation of draft animals. Default `false`. +---@field p8? boolean **RedM only.** Undocumented. Default `false`. +---@field properties? table **GTA5 only.** Properties applied via `lib.setVehicleProperties` after spawning. + +---Client-side spawnable vehicle. +---@class VehicleClient : Entity +local VehicleClient = lib.class('VehicleClient', lib.entity) + +---@param data VehicleInitClient +function VehicleClient: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(VehicleClient.spawn, data, 'ox_lib:createVehicle', 'vehicle') + + self:super(handle) + + self.private.spawnData = data + self.private.modelHash = modelHash + + if data.properties and cache.game ~= 'redm' then + lib.setVehicleProperties(handle, data.properties) + end +end + +---@protected +---@param modelHash number +---@param data VehicleInitClient +---@return number handle +function VehicleClient.spawn(modelHash, data) + local headingValue = data.heading and data.heading + 0.0 or 0.0 + + if cache.game == 'redm' then + return CreateVehicle(modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, + data.isNetwork or false, data.bScriptHostVeh or false, + data.bDontAutoCreateDraftAnimals or false, data.p8 or false) + end + + return CreateVehicle(modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, data.isNetwork or false, data.netMissionEntity or false) +end + +---Apply vehicle properties via `lib.setVehicleProperties`. GTA5 only — no-op on RedM. +---@param properties table +function VehicleClient:setProperties(properties) + if cache.game == 'redm' then return end + lib.setVehicleProperties(self.handle, properties) + if self.private and self.private.spawnData then + self.private.spawnData.properties = properties + end +end + +---@protected +---Re-applies stored vehicle properties after the entity is re-spawned (GTA5 only). +---@param data table +function VehicleClient:onAfterRespawn(data) + if data.properties and cache.game ~= 'redm' then + lib.setVehicleProperties(self.handle, data.properties) + end +end + +---Warp a ped into one of this vehicle's seats. Client-only; uses `TaskWarpPedIntoVehicle`. +---@param ped number | Entity Ped handle or wrapper instance. +---@param seat? SeatPosition Seat index. Default `-1` (driver). +---@return boolean +function VehicleClient:warpPed(ped, seat) + local pedHandle = type(ped) == 'table' and ped.handle or ped --[[@as number]] + seat = seat or -1 --[[@as number]] + if not IsVehicleSeatFree(self.handle, seat) then return false end + TaskWarpPedIntoVehicle(pedHandle, self.handle, seat) + return true +end + +lib.vehicle = VehicleClient + +return lib.vehicle diff --git a/imports/vehicle/server.lua b/imports/vehicle/server.lua new file mode 100644 index 000000000..efffefe9a --- /dev/null +++ b/imports/vehicle/server.lua @@ -0,0 +1,100 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---Vehicle category for `CreateVehicleServerSetter` (GTA5 only). +---@alias VehicleType +---| 'automobile' +---| 'bike' +---| 'boat' +---| 'heli' +---| 'plane' +---| 'submarine' +---| 'trailer' +---| 'train' + +---@class VehicleInitServer +---@field model string | number Model name or precomputed hash. +---@field coords vector3 Spawn coordinate. +---@field heading? number Heading the vehicle should face, in degrees. +---@field type? VehicleType **GTA5 only.** Vehicle category passed to `CreateVehicleServerSetter`. Default `'automobile'`. +---@field properties? table **GTA5 only.** Properties applied via `lib.setVehicleProperties` after spawning. +---@field orphanMode? EntityOrphanMode Server-side cleanup behavior. Default `2` (KeepEntity). + +---Server-side spawnable vehcle. +---@class VehicleServer : Entity +local VehicleServer = lib.class('VehicleServer', lib.entity) + +---@param data VehicleInitServer +function VehicleServer: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(VehicleServer.spawn, data, 'vehicle') + + self:super(handle) + + self.private.spawnData = data + self.private.modelHash = modelHash + + if cache.game ~= 'redm' then + self:setOrphanMode(data.orphanMode or 2) + + if data.properties then + lib.setVehicleProperties(handle, data.properties) + end + end +end + +---@protected +---@param modelHash number +---@param data VehicleInitServer +---@return number handle +function VehicleServer.spawn(modelHash, data) + local headingValue = data.heading and data.heading + 0.0 or 0.0 + + if cache.game == 'redm' then + return CreateVehicle(modelHash, data.coords.x, data.coords.y, data.coords.z, headingValue, true, true) + end + + return CreateVehicleServerSetter(modelHash, data.type or 'automobile', data.coords.x, data.coords.y, data.coords.z, headingValue) +end + +---Place a ped into one of this vehicle's seats. Uses the server-side +---`SetPedIntoVehicle` native (no `TaskWarpPedIntoVehicle` on the server). +---@param ped number | Entity Ped handle or wrapper instance. +---@param seat? number Seat index. Default `-1` (driver). +function VehicleServer:placePed(ped, seat) + local pedHandle = type(ped) == 'table' and ped.handle or ped --[[@as number]] + SetPedIntoVehicle(pedHandle, self.handle, seat or -1) +end + +---Apply vehicle properties via `lib.setVehicleProperties`. GTA5 only — no-op on RedM. +---@param properties table +function VehicleServer:setProperties(properties) + if cache.game == 'redm' then return end + lib.setVehicleProperties(self.handle, properties) + if self.private and self.private.spawnData then + self.private.spawnData.properties = properties + end +end + +---@protected +---Re-applies stored vehicle properties after the entity is re-spawned (GTA5 only). +---@param data table +function VehicleServer:onAfterRespawn(data) + if data.properties and cache.game ~= 'redm' then + lib.setVehicleProperties(self.handle, data.properties) + end +end + +lib.vehicle = VehicleServer + +lib.entity.registerCreateCallback(VehicleServer, 'ox_lib:createVehicle', 'vehicle') + +return lib.vehicle