Files
community_bridge/lib/entities/client/client_entity_actions_ext.lua
T
gononono64 c23df31199 Server and client entity update
syncing non networked entities to all players in range
allow for firing functions when entering or exiting the range of the entity
2025-04-28 22:51:54 -05:00

272 lines
13 KiB
Lua

DefaultActions = {}
ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions.lua") -- Added
--- Internal implementation for walking. Registered via RegisterAction.
function DefaultActions.WalkTo(entityData, coords, speed, timeout)
print("[ClientEntityActions] WalkTo action called", coords, speed, timeout)
local entity = entityData.spawned
local entityId = entityData.id -- Store ID locally for safety in thread
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
-- Clear previous tasks just in case
ClearPedTasks(entity)
local thread = CreateThread(function()
TaskGoToCoordAnyMeans(entity, coords.x, coords.y, coords.z, speed or 1.0, 0, false, 786603, timeout or -1)
-- Wait until task is completed/interrupted or entity is despawned/changed
local entityCoords = GetEntityCoords(entity)
while ClientEntityActions.IsActionRunning[entityId] and entityData.spawned == entity and DoesEntityExist(entity) and #(entityCoords - coords) > 2.0 do
entityCoords = GetEntityCoords(entity)
Wait(0) -- Yield to avoid freezing the game
end
ClientEntityActions.ActionThreads[entityId] = nil -- Clear thread reference
-- Only process next action if this thread was the one running the action
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
--- Internal implementation for playing an animation. Registered via RegisterAction.
-- @param entityData table
-- @param animDict string
-- @param animName string
-- @param blendIn number (Optional, default 8.0)
-- @param blendOut number (Optional, default -8.0)
-- @param duration number (Optional, default -1 for loop/until stopped)
-- @param flag number (Optional, default 0)
-- @param playbackRate number (Optional, default 0.0)
function DefaultActions.PlayAnim(entityData, animDict, animName, blendIn, blendOut, duration, flag, playbackRate)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
return
end
blendIn = blendIn or 8.0
blendOut = blendOut or -8.0
duration = duration or -1
flag = flag or 0
playbackRate = playbackRate or 0.0
local thread = CreateThread(function()
if not HasAnimDictLoaded(animDict) then
RequestAnimDict(animDict)
local timeout = 100
while not HasAnimDictLoaded(animDict) and timeout > 0 do
Wait(10)
timeout = timeout - 1
end
end
if HasAnimDictLoaded(animDict) then
TaskPlayAnim(entity, animDict, animName, blendIn, blendOut, duration, flag, playbackRate, false, false, false)
-- Wait for completion or interruption
local startTime = GetGameTimer()
local animTime = duration > 0 and (startTime + duration) or -1
while ClientEntityActions.IsActionRunning[entityId] and entityData.spawned == entity and DoesEntityExist(entity) do
local isPlaying = IsEntityPlayingAnim(entity, animDict, animName, 3)
-- Break conditions:
-- 1. Action was stopped/skipped externally
-- 2. Entity changed/despawned
-- 3. Animation finished naturally (if not looping based on flags/duration)
-- 4. Duration expired (if duration > 0)
if not ClientEntityActions.IsActionRunning[entityId] or entityData.spawned ~= entity or not DoesEntityExist(entity) then break end
if duration == -1 and not isPlaying and GetEntityAnimCurrentTime(entity, animDict, animName) > 0.1 then break end -- Check if non-looping anim finished
if animTime ~= -1 and GetGameTimer() >= animTime then break end
Wait(100) -- Check periodically
end
-- Don't remove dict here if other actions might use it immediately after
-- Consider a separate cleanup mechanism if needed
else
print(string.format("[ClientEntityActions] Failed to load anim dict '%s' for entity %s", animDict, entityId))
end
-- Crucial: Mark as finished and process next
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
--- Internal implementation for lerping. Registered via RegisterAction.
function DefaultActions.LerpTo(entityData, targetCoords, duration, easingType, easingDirection)
local entity = entityData.spawned
local entityId = entityData.id -- Store ID locally
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
local startCoords = GetEntityCoords(entity)
local startTime = GetGameTimer()
easingType = easingType or "linear"
easingDirection = easingDirection or "inout"
local thread = CreateThread(function()
while GetGameTimer() < startTime + duration do
-- Check if action should continue
if not ClientEntityActions.IsActionRunning[entityId] or not entityData.spawned or entityData.spawned ~= entity or not DoesEntityExist(entity) then
break -- Stop if entity despawned, changed, or action was stopped/skipped
end
local elapsed = GetGameTimer() - startTime
local t = LA.Clamp(elapsed / duration, 0.0, 1.0)
local easedT = LA.EaseInOut(t, easingType) -- Default
if easingDirection == "in" then
easedT = LA.EaseIn(t, easingType)
elseif easingDirection == "out" then
easedT = LA.EaseOut(t, easingType)
end
local currentPos = LA.LerpVector(startCoords, targetCoords, easedT)
SetEntityCoordsNoOffset(entity, currentPos.x, currentPos.y, currentPos.z, false, false, false)
Wait(0)
end
-- Ensure final position if completed fully and action wasn't stopped/skipped
if ClientEntityActions.IsActionRunning[entityId] and entityData.spawned == entity and DoesEntityExist(entity) then
SetEntityCoordsNoOffset(entity, targetCoords.x, targetCoords.y, targetCoords.z, false, false, false)
end
ClientEntityActions.ActionThreads[entityId] = nil -- Clear thread reference
-- Only process next action if this thread was the one running the action
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
--- Internal implementation for attaching a prop. Registered via RegisterAction.
-- This action completes immediately after attaching. Use DetachProp to remove.
-- @param entityData table
-- @param propModel string|number
-- @param boneIndex number (Optional, default -1 for root)
-- @param offsetPos vector3 (Optional, default vector3(0,0,0))
-- @param offsetRot vector3 (Optional, default vector3(0,0,0))
-- @param useSoftPinning boolean (Optional, default false)
-- @param collision boolean (Optional, default false)
-- @param isPed boolean (Optional, default false) - Seems unused in native?
-- @param vertexIndex number (Optional, default 2) - Seems unused in native?
-- @param fixedRot boolean (Optional, default true)
function DefaultActions.AttachProp(entityData, propModel, boneName, offsetPos, offsetRot, useSoftPinning, collision, isPed, vertexIndex, fixedRot)
local entity = entityData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
return
end
local modelHash = Utility.GetEntityHashFromModel(propModel)
print(modelHash, propModel)
if not Utility.LoadModel(modelHash) then
print(string.format("[ClientEntityActions] Failed to load prop model '%s' for entity %s", propModel, entityId))
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
return
end
local boneIndex = GetEntityBoneIndexByName(entity, boneName)
local coords = GetEntityCoords(entity)
local prop = CreateObject(modelHash, coords.x, coords.y, coords.z, false, false, false)
SetModelAsNoLongerNeeded(modelHash)
boneIndex = boneIndex or GetPedBoneIndex(entity, 60309) -- SKEL_R_Hand if not specified and is ped
if boneIndex == -1 then boneIndex = 0 end -- Default to root if bone not found or not ped
offsetPos = offsetPos or vector3(0.0, 0.0, 0.0)
offsetRot = offsetRot or vector3(0.0, 0.0, 0.0)
print(string.format("[ClientEntityActions] Attaching prop '%s' to entity %s at bone %d", propModel, entityId, boneIndex))
AttachEntityToEntity(prop, entity, boneIndex, offsetPos.x, offsetPos.y, offsetPos.z, offsetRot.x, offsetRot.y, offsetRot.z, false, useSoftPinning or false, collision or false, isPed or false, vertexIndex or 2, fixedRot == nil and true or fixedRot)
-- Store the attached prop handle for later removal
if not entityData.attachedProps then entityData.attachedProps = {} end
entityData.attachedProps[propModel] = prop -- Store by model name/hash for easy lookup
-- This action finishes immediately
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
--- Internal implementation for detaching a prop. Registered via RegisterAction.
-- @param entityData table
-- @param propModel string|number The model name/hash of the prop to detach.
function DefaultActions.DetachProp(entityData, propModel)
local entityId = entityData.id
if entityData.attachedProps and propModel then
local propHandle = entityData.attachedProps[propModel]
if propHandle and DoesEntityExist(propHandle) then
DetachEntity(propHandle, true, true) -- Detach
DeleteEntity(propHandle) -- Delete
entityData.attachedProps[propModel] = nil -- Remove from tracking
-- print(string.format("[ClientEntityActions] Detached prop '%s' from entity %s", propModel, entityId))
else
-- print(string.format("[ClientEntityActions] Prop '%s' not found attached to entity %s for detachment.", propModel, entityId))
end
else
-- print(string.format("[ClientEntityActions] No props tracked or propModel not specified for detachment on entity %s.", entityId))
end
-- This action finishes immediately
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
function DefaultActions.GetInCar(entityData, vehicleData, seatIndex, timeout)
local entity = entityData.spawned
local vehicle = vehicleData.spawned
local entityId = entityData.id
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
if not vehicle or not DoesEntityExist(vehicle) or not IsEntityAVehicle(vehicle) then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
return
end
-- Clear previous tasks just in case
ClearPedTasks(entity)
local thread = CreateThread(function()
TaskEnterVehicle(entity, vehicle, timeout or 1000, seatIndex or -1, 1.0, 1, 0) -- Enter vehicle
Wait(timeout or 1000) -- Wait for a bit to ensure the task is completed
ClientEntityActions.ActionThreads[entityId] = nil -- Clear thread reference
-- Only process next action if this thread was the one running the action
if ClientEntityActions.IsActionRunning[entityId] then
ClientEntityActions.IsActionRunning[entityId] = false
ClientEntityActions.ProcessNextAction(entityId)
end
end)
ClientEntityActions.ActionThreads[entityId] = thread
end
for name, func in pairs(DefaultActions) do
ClientEntityActions.RegisterAction(name, func)
end
return ClientEntityActions