ed
This commit is contained in:
parent
510e3ffcf2
commit
f43cf424cf
305 changed files with 34683 additions and 0 deletions
|
@ -0,0 +1,155 @@
|
|||
Anim = Anim or {}
|
||||
Ids = Ids or Require("lib/utility/shared/ids.lua")
|
||||
Anim.Active = Anim.Active or {}
|
||||
Anim.isUpdateLoopRunning = Anim.isUpdateLoopRunning or false
|
||||
|
||||
--- This will request the animation dictionary.
|
||||
--- @param animDict string
|
||||
--- @return boolean
|
||||
function Anim.RequestDict(animDict)
|
||||
if not animDict or type(animDict) ~= "string" then
|
||||
return false
|
||||
end
|
||||
|
||||
if HasAnimDictLoaded(animDict) then
|
||||
return true
|
||||
end
|
||||
|
||||
RequestAnimDict(animDict)
|
||||
local timeout = GetGameTimer() + 2000
|
||||
while not HasAnimDictLoaded(animDict) and GetGameTimer() < timeout do
|
||||
Wait(50)
|
||||
end
|
||||
|
||||
return HasAnimDictLoaded(animDict)
|
||||
end
|
||||
|
||||
--- This will start the animation update loop.
|
||||
function Anim.Start()
|
||||
if Anim.isUpdateLoopRunning then return end
|
||||
Anim.isUpdateLoopRunning = true
|
||||
CreateThread(function()
|
||||
while Anim.isUpdateLoopRunning do
|
||||
local idsToProcess = {}
|
||||
for idKey, _ in pairs(Anim.Active) do
|
||||
table.insert(idsToProcess, idKey)
|
||||
end
|
||||
|
||||
if #idsToProcess == 0 then
|
||||
Wait(750)
|
||||
else
|
||||
for _, id in ipairs(idsToProcess) do
|
||||
local animData = Anim.Active[id]
|
||||
if animData then
|
||||
local entity = animData.entity
|
||||
local onComplete = animData.onComplete
|
||||
if not DoesEntityExist(entity) then
|
||||
if onComplete then onComplete(false, "despawned") end
|
||||
Anim.Active[id] = nil
|
||||
elseif animData.status == "pending_task" then
|
||||
TaskPlayAnim(entity, animData.animDict, animData.animName, animData.blendIn, animData.blendOut, animData.duration, animData.flag, animData.playbackRate, false, false, false)
|
||||
animData.startTime = GetGameTimer()
|
||||
animData.animEndTime = animData.duration > 0 and (animData.startTime + animData.duration) or -1
|
||||
animData.status = "playing"
|
||||
elseif animData.status == "playing" then
|
||||
local animationCompletedNaturally = false
|
||||
if animData.duration == -1 then
|
||||
if not IsEntityPlayingAnim(entity, animData.animDict, animData.animName, 3) and GetEntityAnimCurrentTime(entity, animData.animDict, animData.animName) > 0.8 then
|
||||
animationCompletedNaturally = true
|
||||
end
|
||||
elseif animData.animEndTime ~= -1 and GetGameTimer() >= animData.animEndTime then
|
||||
animationCompletedNaturally = true
|
||||
end
|
||||
if animationCompletedNaturally then
|
||||
if onComplete then onComplete(true, "completed") end
|
||||
Anim.Active[id] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Wait(100)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- This will play an animation on the specified ped.
|
||||
--- @param id string | nil
|
||||
--- @param entity number
|
||||
--- @param animDict string
|
||||
--- @param animName string
|
||||
--- @param blendIn number | nil
|
||||
--- @param blendOut number | nil
|
||||
--- @param duration number | nil
|
||||
--- @param flag number | nil
|
||||
--- @param playbackRate number | nil
|
||||
--- @param onComplete function | nil
|
||||
--- @return string | nil
|
||||
function Anim.Play(id, entity, animDict, animName, blendIn, blendOut, duration, flag, playbackRate, onComplete)
|
||||
local newId = id or Ids.CreateUniqueId(Anim.Active)
|
||||
if Anim.Active[newId] then
|
||||
if onComplete then
|
||||
onComplete(false, "id_in_use")
|
||||
end
|
||||
return newId
|
||||
end
|
||||
|
||||
if not entity or not DoesEntityExist(entity) or not IsEntityAPed(entity) then
|
||||
if onComplete then
|
||||
onComplete(false, "invalid_entity")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if not Anim.RequestDict(animDict) then
|
||||
if onComplete then
|
||||
onComplete(false, "dict_load_failed")
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
Anim.Active[newId] = {
|
||||
entity = entity,
|
||||
animDict = animDict,
|
||||
animName = animName,
|
||||
blendIn = blendIn or 8.0,
|
||||
blendOut = blendOut or -8.0,
|
||||
duration = duration or -1,
|
||||
flag = flag or 1,
|
||||
playbackRate = playbackRate or 0.0,
|
||||
onComplete = onComplete,
|
||||
status = "pending_task",
|
||||
startTime = 0,
|
||||
animEndTime = 0
|
||||
}
|
||||
|
||||
Anim.Start()
|
||||
return newId
|
||||
end
|
||||
|
||||
function Anim.Stop(id)
|
||||
if not id or not Anim.Active or not Anim.Active[id] then
|
||||
return false
|
||||
end
|
||||
|
||||
local animData = Anim.Active[id]
|
||||
|
||||
if animData.entity and DoesEntityExist(animData.entity) and IsEntityAPed(animData.entity) then
|
||||
if animData.status == "playing" or animData.status == "pending_task" then
|
||||
StopAnimTask(animData.entity, animData.animDict, animData.animName, 1.0)
|
||||
end
|
||||
end
|
||||
|
||||
if animData.onComplete then
|
||||
animData.onComplete(false, "stopped_by_id")
|
||||
end
|
||||
|
||||
Anim.Active[id] = nil
|
||||
local anyLeft = Anim.Active and next(Anim.Active) ~= nil
|
||||
if not anyLeft then
|
||||
Anim.isUpdateLoopRunning = false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return Anim
|
|
@ -0,0 +1,65 @@
|
|||
Batch = Batch or {}
|
||||
Batch.Event = Batch.Event or {}
|
||||
Batch.Event.Queued = Batch.Event.Queued or {}
|
||||
Batch.Event.IsQueued = Batch.Event.IsQueued or false
|
||||
|
||||
local SERVER = IsDuplicityVersion()
|
||||
|
||||
-- could do a callback from client to server back to client with a time stamp. Use that timestamp to generate some random string/number and use that fo
|
||||
-- this event name. Would help by masking from exploits. Thinking about making a module out of it
|
||||
|
||||
--- This is used to batch single events together to reduce network strain
|
||||
---
|
||||
if SERVER then
|
||||
function Batch.Event.Queue(src, event, ...)
|
||||
if src == -1 then
|
||||
src = GetPlayers()
|
||||
for k, v in pairs(src) do
|
||||
local strSrc = tostring(v)
|
||||
Batch.Event.Queued[strSrc] = Batch.Event.Queued[strSrc] or {}
|
||||
table.insert(Batch.Event.Queued[strSrc], {
|
||||
src = v,
|
||||
event = event,
|
||||
args = {...}
|
||||
})
|
||||
end
|
||||
else
|
||||
local strSrc = tostring(src)
|
||||
Batch.Event.Queued[strSrc] = Batch.Event.Queued[strSrc] or {}
|
||||
table.insert(Batch.Event.Queued[strSrc], {
|
||||
src = src,
|
||||
event = event,
|
||||
args = {...}
|
||||
})
|
||||
end
|
||||
|
||||
if Batch.Event.IsQueued then return end
|
||||
Batch.Event.IsQueued = true
|
||||
SetTimeout(100, function()
|
||||
for k, v in pairs(Batch.Event.Queued) do
|
||||
TriggerClientEvent('community_bridge:client:BatchEvents', v.src, v)
|
||||
end
|
||||
Batch.Event.IsQueued = false
|
||||
Batch.Event.Queued = {}
|
||||
end)
|
||||
end
|
||||
return Batch
|
||||
else
|
||||
|
||||
Batch.Event.Fire = function(array)
|
||||
local playerSrc = PlayerId()
|
||||
for k, v in pairs(array) do
|
||||
if v.src == playerSrc then
|
||||
local event = v.event
|
||||
local args = v.args
|
||||
TriggerEvent(event, table.unpack(args))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('community_bridge:client:BatchEvents', function(array)
|
||||
Batch.Event.Fire(array)
|
||||
end)
|
||||
|
||||
return Batch
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
-- Test file for FiveM Community Bridge Extension
|
||||
-- Try the following to test extension features:
|
||||
|
||||
-- 1. Type "CommunityBridge." (with the dot) and you should see auto-completion
|
||||
-- CommunityBridge.
|
||||
|
||||
-- 2. Type "AddEventHandler" and see parameter hints
|
||||
-- AddEventHandler(
|
||||
|
||||
-- 3. Hover over these function names to see documentation:
|
||||
-- GetPlayerData, SetPlayerData, TriggerEvent, RegisterNetEvent
|
||||
|
||||
-- 4. Try these snippets (type the word and press Tab):
|
||||
-- event
|
||||
-- thread
|
||||
-- command
|
||||
-- cbget
|
||||
-- cbset
|
||||
|
||||
-- 5. Test signature help - start typing a function call:
|
||||
-- CommunityBridge.GetPlayerData(
|
||||
|
||||
print("Extension test file loaded")
|
63
resources/[carscripts]/community_bridge/lib/cache/client/cache.lua
vendored
Normal file
63
resources/[carscripts]/community_bridge/lib/cache/client/cache.lua
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
Cache = Require("lib/cache/shared/cache.lua")
|
||||
local PlayerPedId = PlayerPedId
|
||||
local GetVehiclePedIsIn = GetVehiclePedIsIn
|
||||
local GetVehicleMaxNumberOfPassengers = GetVehicleMaxNumberOfPassengers
|
||||
local GetPedInVehicleSeat = GetPedInVehicleSeat
|
||||
local GetPlayerServerId = GetPlayerServerId
|
||||
local GetSelectedPedWeapon = GetSelectedPedWeapon
|
||||
local PlayerId = PlayerId
|
||||
|
||||
|
||||
-- Comparison functions for client-side state
|
||||
|
||||
--- Get the player's ped ID
|
||||
---@return integer
|
||||
function GetPed()
|
||||
return PlayerPedId()
|
||||
end
|
||||
|
||||
--- Get the vehicle the player is in, if any
|
||||
---@return integer | nil
|
||||
function GetVehicle()
|
||||
local ped = Cache.Get("Ped") -- Use cached ped for efficiency
|
||||
if not ped then return end -- Or handle error/default case
|
||||
return GetVehiclePedIsIn(ped, false)
|
||||
end
|
||||
|
||||
--- Get the seat of the player in the vehicle if the vehicle is occupied
|
||||
---@return integer | nil
|
||||
function GetVehicleSeat()
|
||||
local vehicle = Cache.Get("Vehicle")
|
||||
if not vehicle then return end
|
||||
local ped = Cache.Get("Ped")
|
||||
if not ped then return end
|
||||
for i = -1, GetVehicleMaxNumberOfPassengers(vehicle) - 1 do
|
||||
if ped == GetPedInVehicleSeat(vehicle, i) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Get The Server ID of the player
|
||||
---@return integer | nil
|
||||
function GetServerId()
|
||||
local ped <const> = PlayerId()
|
||||
if not ped then return end -- Return unarmed hash if no ped
|
||||
return GetPlayerServerId(ped)
|
||||
end
|
||||
|
||||
--- Get the current weapon of the player
|
||||
--- @return integer | nil
|
||||
function GetWeapon()
|
||||
local ped = Cache.Get("Ped")
|
||||
if not ped then return end -- Return unarmed hash if no ped
|
||||
return GetSelectedPedWeapon(ped)
|
||||
end
|
||||
|
||||
Cache.Create("Ped", GetPed, 1000) -- Check ped every second (adjust as needed)
|
||||
Cache.Create("Vehicle", GetVehicle, 500) -- Check vehicle every 500ms
|
||||
Cache.Create("Seat", GetVehicleSeat, 500) -- Check if in vehicle every 500ms
|
||||
Cache.Create("Weapon", GetWeapon , 250) -- Check weapon every 250ms
|
||||
Cache.Create("ServerId", GetServerId, 1000) -- Check server ID every second
|
||||
|
||||
return Cache
|
0
resources/[carscripts]/community_bridge/lib/cache/server/cache.lua
vendored
Normal file
0
resources/[carscripts]/community_bridge/lib/cache/server/cache.lua
vendored
Normal file
279
resources/[carscripts]/community_bridge/lib/cache/shared/cache.lua
vendored
Normal file
279
resources/[carscripts]/community_bridge/lib/cache/shared/cache.lua
vendored
Normal file
|
@ -0,0 +1,279 @@
|
|||
---@class CacheEntry
|
||||
---@field Name string
|
||||
---@field Compare fun(): any
|
||||
---@field WaitTime integer | nil
|
||||
---@field LastChecked integer|nil
|
||||
---@field OnChange fun(new:any, old:any)[]
|
||||
---@field Value any
|
||||
|
||||
---@class CacheModule
|
||||
---@field Caches table<string, CacheEntry>
|
||||
---@field LoopRunning boolean
|
||||
---@field Create fun(name:string, compare:fun():any, waitTime:integer | nil): CacheEntry
|
||||
---@field Get fun(name:string): any
|
||||
---@field OnChange fun(name:string, onChange:fun(new:any, old:any))
|
||||
---@field Remove fun(name:string)
|
||||
Cache = Cache or {} ---@type CacheModule -- <-- we use Cache as a global variable so that if we dont required it more than once
|
||||
|
||||
Table = Table or Require('lib/utility/shared/tables.lua')
|
||||
Id = Id or Require('lib/utility/shared/ids.lua')
|
||||
|
||||
local Config = Require("settings/sharedConfig.lua")
|
||||
local max = 5000
|
||||
local CreateThread = CreateThread
|
||||
local Wait = Wait
|
||||
local GetGameTimer = GetGameTimer
|
||||
|
||||
local resourceCallbacks = {} -- Add callbacks from resources
|
||||
local resourceTracker = {
|
||||
caches = {},
|
||||
callbacks = {},
|
||||
initialized = {}
|
||||
}
|
||||
|
||||
Cache.Caches = Cache.Caches or {}
|
||||
Cache.LoopRunning = Cache.LoopRunning or false
|
||||
|
||||
---@param ... any
|
||||
local function debugPrint(...)
|
||||
if Config.DebugLevel == 0 then return end
|
||||
print("^2[Cache]^0", ...)
|
||||
end
|
||||
|
||||
local function HasActiveCaches()
|
||||
for _, cache in pairs(Cache.Caches) do
|
||||
if cache.WaitTime ~= nil then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function processCacheEntry(now, cache)
|
||||
cache.LastChecked = cache.LastChecked or now
|
||||
cache.WaitTime = tonumber(cache.WaitTime) or max
|
||||
local elapsed = now - cache.LastChecked
|
||||
local remaining = cache.WaitTime - elapsed
|
||||
|
||||
if remaining <= 0 then
|
||||
local oldValue = cache.Value
|
||||
cache.Value = cache.Compare()
|
||||
if not Table.Compare(cache.Value, oldValue) and cache.OnChange then
|
||||
for i, onChange in pairs(cache.OnChange) do
|
||||
onChange(cache.Value, oldValue)
|
||||
end
|
||||
end
|
||||
cache.LastChecked = now
|
||||
remaining = cache.WaitTime
|
||||
end
|
||||
|
||||
return remaining
|
||||
end
|
||||
|
||||
local function getNextWait(now)
|
||||
local minWait = nil
|
||||
for name, cache in pairs(Cache.Caches) do
|
||||
if cache.Compare and cache.WaitTime ~= nil then
|
||||
local remaining = processCacheEntry(now, cache)
|
||||
if not minWait or remaining < minWait then
|
||||
minWait = remaining
|
||||
if minWait <= 0 then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
return minWait
|
||||
end
|
||||
|
||||
local function StartLoop()
|
||||
if Cache.LoopRunning then return end
|
||||
if not HasActiveCaches() then return end
|
||||
Cache.LoopRunning = true
|
||||
CreateThread(function()
|
||||
while Cache.LoopRunning do
|
||||
local now = GetGameTimer()
|
||||
local minWait = getNextWait(now)
|
||||
if minWait then
|
||||
Wait(math.max(0, minWait))
|
||||
else
|
||||
Wait(max)
|
||||
end
|
||||
if not HasActiveCaches() then
|
||||
Cache.LoopRunning = false
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function trackResource(resourceName, type, data)
|
||||
if not resourceName then return end
|
||||
|
||||
-- Initialize resource tracking if needed
|
||||
if not resourceTracker.initialized[resourceName] then
|
||||
resourceTracker.initialized[resourceName] = true
|
||||
resourceTracker.caches[resourceName] = resourceTracker.caches[resourceName] or {}
|
||||
resourceTracker.callbacks[resourceName] = resourceTracker.callbacks[resourceName] or {}
|
||||
|
||||
AddEventHandler('onResourceStop', function(stoppingResource)
|
||||
if stoppingResource ~= resourceName then return end
|
||||
|
||||
-- Clean up caches
|
||||
for _, cacheName in ipairs(resourceTracker.caches[resourceName] or {}) do
|
||||
if Cache.Caches[cacheName] then
|
||||
Cache.Caches[cacheName] = nil
|
||||
debugPrint(("Removed cache '%s' - resource '%s' stopped"):format(cacheName, resourceName))
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean up callbacks
|
||||
for _, cb in ipairs(resourceTracker.callbacks[resourceName] or {}) do
|
||||
local targetCache = Cache.Caches[cb.cacheName]
|
||||
if targetCache then
|
||||
table.remove(targetCache.OnChange, cb.index)
|
||||
debugPrint(("Removed OnChange callback from cache '%s' - resource '%s' stopped"):format(
|
||||
cb.cacheName,
|
||||
resourceName
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear tracking data
|
||||
resourceTracker.caches[resourceName] = nil
|
||||
resourceTracker.callbacks[resourceName] = nil
|
||||
resourceTracker.initialized[resourceName] = nil
|
||||
end)
|
||||
end
|
||||
|
||||
-- Track the new item
|
||||
if type == "cache" then
|
||||
table.insert(resourceTracker.caches[resourceName], data)
|
||||
elseif type == "callback" then
|
||||
table.insert(resourceTracker.callbacks[resourceName], data)
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param compare fun():any
|
||||
---@param waitTime integer | nil
|
||||
---@return CacheEntry | nil
|
||||
function Cache.Create(name, compare, waitTime)
|
||||
assert(name, "Cache name is required.")
|
||||
assert(compare, "Comparison function is required.")
|
||||
if type(waitTime) ~= "number" then
|
||||
waitTime = nil
|
||||
end
|
||||
local _name = tostring(name)
|
||||
local cache = Cache.Caches[_name]
|
||||
if cache and cache.Compare == compare then
|
||||
debugPrint(_name .. " already exists with the same comparison function.")
|
||||
return cache
|
||||
end
|
||||
local ok, result = pcall(compare)
|
||||
if not ok then
|
||||
debugPrint("Error creating cache '" .. _name .. "': " .. tostring(result))
|
||||
return nil
|
||||
end
|
||||
---@type CacheEntry
|
||||
local newCache = {
|
||||
Name = _name,
|
||||
Compare = compare,
|
||||
WaitTime = waitTime, -- can be nil
|
||||
LastChecked = nil,
|
||||
OnChange = {},
|
||||
Value = result
|
||||
}
|
||||
|
||||
-- Track the cache with its resource
|
||||
trackResource(GetInvokingResource(), "cache", _name)
|
||||
|
||||
Cache.Caches[_name] = newCache
|
||||
|
||||
debugPrint(_name .. " created with initial value: " .. tostring(result))
|
||||
for _, onChange in pairs(newCache.OnChange) do
|
||||
onChange(newCache.Value, 0)
|
||||
end
|
||||
StartLoop()
|
||||
return newCache
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return any
|
||||
function Cache.Get(name)
|
||||
assert(name, "Cache name is required.")
|
||||
local _name = tostring(name)
|
||||
local cache = Cache.Caches[_name]
|
||||
return cache and cache.Value or nil
|
||||
end
|
||||
|
||||
--- This add a callback to the cache entry that will be called when the value changes.
|
||||
--- The callback will be called with the new value and the old value.
|
||||
--- you can call the value again to delete the callback.
|
||||
---@param name string
|
||||
---@param onChange fun(new:any, old:any)
|
||||
function Cache.OnChange(name, onChange)
|
||||
assert(name, "Cache name is required.")
|
||||
local _name = tostring(name)
|
||||
local cache = Cache.Caches[_name]
|
||||
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
|
||||
|
||||
local id = Id.CreateUniqueId(Cache.Caches[_name]?.OnChange)
|
||||
-- Figure out which resource is trying to register this callback
|
||||
local invokingResource = GetInvokingResource()
|
||||
if not invokingResource then return end
|
||||
-- Add the new callback to our list
|
||||
local callbackIndex = id
|
||||
cache.OnChange[callbackIndex] = onChange
|
||||
|
||||
-- Track the callback with its resource
|
||||
trackResource(GetInvokingResource(), "callback", {
|
||||
cacheName = _name,
|
||||
index = callbackIndex
|
||||
})
|
||||
|
||||
debugPrint(("Added new OnChange callback to cache '%s' from resource '%s'"):format(_name, invokingResource))
|
||||
return callbackIndex
|
||||
end
|
||||
|
||||
function Cache.RemoveOnChange(name, id)
|
||||
assert(name, "Cache name is required.")
|
||||
local _name = tostring(name)
|
||||
local cache = Cache.Caches[_name]
|
||||
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
|
||||
if cache.OnChange[id] then
|
||||
cache.OnChange[id] = nil
|
||||
debugPrint("Removed OnChange callback from cache '" .. _name .. "' with ID: " .. id)
|
||||
else
|
||||
debugPrint("No OnChange callback found with ID: " .. id .. " in cache '" .. _name .. "'")
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
function Cache.Remove(name)
|
||||
assert(name, "Cache name is required.")
|
||||
local _name = tostring(name)
|
||||
local cache = Cache.Caches[_name]
|
||||
if cache then
|
||||
Cache.Caches[_name] = nil
|
||||
debugPrint(_name .. " removed from cache.")
|
||||
if next(Cache.Caches) == nil then
|
||||
Cache.LoopRunning = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param newValue any
|
||||
function Cache.Update(name, newValue)
|
||||
assert(name, "Cache name is required.")
|
||||
local _name = tostring(name)
|
||||
local cache = Cache.Caches[_name]
|
||||
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
|
||||
local oldValue = cache.Value
|
||||
if oldValue ~= newValue then
|
||||
cache.Value = newValue
|
||||
for _, onChange in pairs(cache.OnChange) do
|
||||
onChange(newValue, oldValue)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Cache
|
|
@ -0,0 +1,394 @@
|
|||
-- Cutscene Manager for FiveM
|
||||
-- Handles cutscene loading, playback, and character management
|
||||
|
||||
---@class Cutscene
|
||||
Cutscenes = Cutscenes or {}
|
||||
Cutscene = Cutscene or {}
|
||||
Cutscene.done = true
|
||||
|
||||
-- Constants
|
||||
local LOAD_TIMEOUT <const> = 10000
|
||||
local FADE_DURATION <const> = 1000
|
||||
local CUTSCENE_WAIT <const> = 1000
|
||||
|
||||
-- Character model definitions
|
||||
local characterTags <const> = {
|
||||
{ male = 'MP_1' },
|
||||
{ male = 'MP_2' },
|
||||
{ male = 'MP_3' },
|
||||
{ male = 'MP_4' },
|
||||
{ male = 'MP_Male_Character', female = 'MP_Female_Character' },
|
||||
{ male = 'MP_Male_Character_1', female = 'MP_Female_Character_1' },
|
||||
{ male = 'MP_Male_Character_2', female = 'MP_Female_Character_2' },
|
||||
{ male = 'MP_Male_Character_3', female = 'MP_Female_Character_3' },
|
||||
{ male = 'MP_Male_Character_4', female = 'MP_Female_Character_4' },
|
||||
{ male = 'MP_Plane_Passenger_1' },
|
||||
{ male = 'MP_Plane_Passenger_2' },
|
||||
{ male = 'MP_Plane_Passenger_3' },
|
||||
{ male = 'MP_Plane_Passenger_4' },
|
||||
{ male = 'MP_Plane_Passenger_5' },
|
||||
{ male = 'MP_Plane_Passenger_6' },
|
||||
{ male = 'MP_Plane_Passenger_7' },
|
||||
{ male = 'MP_Plane_Passenger_8' },
|
||||
{ male = 'MP_Plane_Passenger_9' },
|
||||
}
|
||||
|
||||
-- Component definitions for character customization
|
||||
local componentsToSave <const> = {
|
||||
-- Components
|
||||
{ name = "head", id = 0, type = "drawable" },
|
||||
{ name = "beard", id = 1, type = "drawable" },
|
||||
{ name = "hair", id = 2, type = "drawable" },
|
||||
{ name = "arms", id = 3, type = "drawable" },
|
||||
{ name = "pants", id = 4, type = "drawable" },
|
||||
{ name = "parachute", id = 5, type = "drawable" },
|
||||
{ name = "feet", id = 6, type = "drawable" },
|
||||
{ name = "accessories", id = 7, type = "drawable" },
|
||||
{ name = "undershirt", id = 8, type = "drawable" },
|
||||
{ name = "vest", id = 9, type = "drawable" },
|
||||
{ name = "decals", id = 10, type = "drawable" },
|
||||
{ name = "jacket", id = 11, type = "drawable" },
|
||||
-- Props
|
||||
{ name = "hat", id = 0, type = "prop" },
|
||||
{ name = "glasses", id = 1, type = "prop" },
|
||||
{ name = "ears", id = 2, type = "prop" },
|
||||
{ name = "watch", id = 3, type = "prop" },
|
||||
{ name = "bracelet", id = 4, type = "prop" },
|
||||
{ name = "misc", id = 5, type = "prop" },
|
||||
{ name = "left_wrist", id = 6, type = "prop" },
|
||||
{ name = "right_wrist", id = 7, type = "prop" },
|
||||
{ name = "prop8", id = 8, type = "prop" },
|
||||
{ name = "prop9", id = 9, type = "prop" },
|
||||
}
|
||||
|
||||
-- Utility Functions
|
||||
|
||||
function table.shallow_copy(t)
|
||||
local t2 = {}
|
||||
for k, v in pairs(t) do
|
||||
t2[k] = v
|
||||
end
|
||||
return t2
|
||||
end
|
||||
|
||||
local function WaitForModelLoad(modelHash)
|
||||
local timeout = GetGameTimer() + LOAD_TIMEOUT
|
||||
while not HasModelLoaded(modelHash) and GetGameTimer() < timeout do
|
||||
Wait(0)
|
||||
end
|
||||
return HasModelLoaded(modelHash)
|
||||
end
|
||||
|
||||
local function CreatePedFromModel(modelName, coords)
|
||||
local model = GetHashKey(modelName)
|
||||
RequestModel(model)
|
||||
|
||||
if not WaitForModelLoad(model) then
|
||||
print("Failed to load model: " .. modelName)
|
||||
return nil
|
||||
end
|
||||
|
||||
local ped = CreatePed(0, model, coords.x, coords.y, coords.z, 0.0, true, false)
|
||||
if not DoesEntityExist(ped) then
|
||||
print("Failed to create ped from model: " .. modelName)
|
||||
return nil
|
||||
end
|
||||
|
||||
return ped
|
||||
end
|
||||
|
||||
-- Cutscene Core Functions
|
||||
local function LoadCutscene(cutscene)
|
||||
assert(cutscene, "Cutscene.Load called without a cutscene name.")
|
||||
local playbackList = IsPedMale(PlayerPedId()) and 31 or 103
|
||||
RequestCutsceneWithPlaybackList(cutscene, playbackList, 8)
|
||||
|
||||
local timeout = GetGameTimer() + LOAD_TIMEOUT
|
||||
while not HasCutsceneLoaded() and GetGameTimer() < timeout do
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
if not HasCutsceneLoaded() then
|
||||
print("Cutscene failed to load: ", cutscene)
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function GetCutsceneTags(cutscene)
|
||||
if not LoadCutscene(cutscene) then return end
|
||||
|
||||
StartCutscene(0)
|
||||
Wait(CUTSCENE_WAIT)
|
||||
|
||||
local tags = {}
|
||||
for _, tag in pairs(characterTags) do
|
||||
if DoesCutsceneEntityExist(tag.male, 0) or DoesCutsceneEntityExist(tag.female, 0) then
|
||||
table.insert(tags, tag)
|
||||
end
|
||||
end
|
||||
|
||||
StopCutsceneImmediately()
|
||||
Wait(CUTSCENE_WAIT * 2)
|
||||
return tags
|
||||
end
|
||||
|
||||
-- Character Outfit Management
|
||||
local function SavePedOutfit(ped)
|
||||
local outfitData = {}
|
||||
|
||||
for _, component in ipairs(componentsToSave) do
|
||||
if component.type == "drawable" then
|
||||
outfitData[component.name] = {
|
||||
id = component.id,
|
||||
type = component.type,
|
||||
drawable = GetPedDrawableVariation(ped, component.id),
|
||||
texture = GetPedTextureVariation(ped, component.id),
|
||||
palette = GetPedPaletteVariation(ped, component.id),
|
||||
}
|
||||
elseif component.type == "prop" then
|
||||
outfitData[component.name] = {
|
||||
id = component.id,
|
||||
type = component.type,
|
||||
propIndex = GetPedPropIndex(ped, component.id),
|
||||
propTexture = GetPedPropTextureIndex(ped, component.id),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return outfitData
|
||||
end
|
||||
|
||||
local function ApplyPedOutfit(ped, outfitData)
|
||||
if not outfitData or type(outfitData) ~= "table" then
|
||||
print("ApplyPedOutfit: Invalid outfitData provided.")
|
||||
return
|
||||
end
|
||||
|
||||
for componentName, data in pairs(outfitData) do
|
||||
if data.type == "drawable" then
|
||||
SetPedComponentVariation(ped, data.id, data.drawable or 0, data.texture or 0, data.palette or 0)
|
||||
elseif data.type == "prop" then
|
||||
if data.propIndex == -1 or data.propIndex == nil then
|
||||
ClearPedProp(ped, data.id)
|
||||
else
|
||||
SetPedPropIndex(ped, data.id, data.propIndex, data.propTexture or 0, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Public Interface
|
||||
function Cutscene.GetTags(cutscene)
|
||||
return GetCutsceneTags(cutscene)
|
||||
end
|
||||
|
||||
function Cutscene.Load(cutscene)
|
||||
return LoadCutscene(cutscene)
|
||||
end
|
||||
|
||||
function Cutscene.SavePedOutfit(ped)
|
||||
return SavePedOutfit(ped)
|
||||
end
|
||||
|
||||
function Cutscene.ApplyPedOutfit(ped, outfitData)
|
||||
return ApplyPedOutfit(ped, outfitData)
|
||||
end
|
||||
|
||||
function Cutscene.Create(cutscene, coords, srcs)
|
||||
local lastCoords = coords or GetEntityCoords(PlayerPedId())
|
||||
DoScreenFadeOut(0)
|
||||
|
||||
local tagsFromCutscene = GetCutsceneTags(cutscene)
|
||||
if not LoadCutscene(cutscene) then
|
||||
print("Cutscene.Create: Failed to load cutscene", cutscene)
|
||||
DoScreenFadeIn(0)
|
||||
return false
|
||||
end
|
||||
|
||||
srcs = srcs or {}
|
||||
local clothes = {}
|
||||
local localped = PlayerPedId()
|
||||
|
||||
-- Process players and create necessary peds
|
||||
local playersToProcess = { { ped = localped, identifier = "localplayer", coords = lastCoords } }
|
||||
|
||||
for _, src_raw in ipairs(srcs) do
|
||||
if type(src_raw) == 'number' then
|
||||
if src_raw and not DoesEntityExist(src_raw) then
|
||||
local playerPed = GetPlayerPed(GetPlayerFromServerId(src_raw))
|
||||
if DoesEntityExist(playerPed) then
|
||||
local ped = ClonePed(playerPed, false, false, true)
|
||||
table.insert(playersToProcess, { ped = ped, identifier = "player" })
|
||||
end
|
||||
else
|
||||
table.insert(playersToProcess, { ped = src_raw, identifier = "user" })
|
||||
end
|
||||
elseif type(src_raw) == 'string' then
|
||||
local ped = CreatePedFromModel(src_raw, GetEntityCoords(localped))
|
||||
if ped then
|
||||
table.insert(playersToProcess, { ped = ped, identifier = 'script' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Process available tags and assign to players
|
||||
local availableTags = table.shallow_copy(tagsFromCutscene or {})
|
||||
local usedTags = {}
|
||||
local cleanupTags = {}
|
||||
|
||||
for _, playerData in ipairs(playersToProcess) do
|
||||
local currentPed = playerData.ped
|
||||
if not currentPed or not DoesEntityExist(currentPed) then goto continue end
|
||||
|
||||
local tagTable = table.remove(availableTags, 1)
|
||||
if not tagTable then
|
||||
print("Cutscene.Create: No available tags for player", playerData.identifier)
|
||||
break
|
||||
end
|
||||
|
||||
local isPedMale = IsPedMale(currentPed)
|
||||
local tag = isPedMale and tagTable.male or tagTable.female
|
||||
local unusedTag = not isPedMale and tagTable.male or tagTable.female -- needs to be this way as default has to be null for missing female
|
||||
|
||||
SetCutsceneEntityStreamingFlags(tag, 0, 1)
|
||||
RegisterEntityForCutscene(currentPed, tag, 0, GetEntityModel(currentPed), 64)
|
||||
SetCutscenePedComponentVariationFromPed(tag, currentPed, 0)
|
||||
|
||||
clothes[tag] = {
|
||||
clothing = SavePedOutfit(currentPed),
|
||||
ped = currentPed
|
||||
}
|
||||
|
||||
table.insert(usedTags, tag)
|
||||
if unusedTag then table.insert(cleanupTags, unusedTag) end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
-- Clean up unused tags
|
||||
for _, tag in ipairs(cleanupTags) do
|
||||
local ped = RegisterEntityForCutscene(0, tag, 3, 0, 64)
|
||||
if ped then
|
||||
SetEntityVisible(ped, false, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle remaining unused tags
|
||||
for _, tag in ipairs(availableTags) do
|
||||
for _, tagType in pairs({ tag.male, tag.female }) do
|
||||
if tagType then
|
||||
local ped = RegisterEntityForCutscene(0, tagType, 3, 0, 64)
|
||||
if ped then
|
||||
SetEntityVisible(ped, false, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
cutscene = cutscene,
|
||||
coords = coords,
|
||||
tags = usedTags,
|
||||
srcs = srcs,
|
||||
peds = playersToProcess,
|
||||
clothes = clothes,
|
||||
}
|
||||
end
|
||||
|
||||
function Cutscene.Start(cutsceneData)
|
||||
if not cutsceneData then
|
||||
print("Cutscene.Start: Cutscene data is nil.")
|
||||
return
|
||||
end
|
||||
|
||||
DoScreenFadeIn(FADE_DURATION)
|
||||
Cutscene.done = false
|
||||
|
||||
local clothes = cutsceneData.clothes
|
||||
local coords = cutsceneData.coords
|
||||
|
||||
-- Start cutscene
|
||||
if coords then
|
||||
if type(coords) == 'boolean' then
|
||||
coords = GetEntityCoords(PlayerPedId())
|
||||
end
|
||||
StartCutsceneAtCoords(coords.x, coords.y, coords.z, 0)
|
||||
else
|
||||
StartCutscene(0)
|
||||
end
|
||||
|
||||
Wait(100)
|
||||
|
||||
-- Apply clothing to cutscene characters
|
||||
for k, datam in pairs(clothes) do
|
||||
local ped = datam.ped
|
||||
if DoesEntityExist(ped) then
|
||||
SetCutscenePedComponentVariationFromPed(k, ped, 0)
|
||||
ApplyPedOutfit(ped, datam.clothing)
|
||||
end
|
||||
end
|
||||
|
||||
-- Scene loading thread
|
||||
CreateThread(function()
|
||||
local lastCoords
|
||||
while not Cutscene.done do
|
||||
local coords = GetWorldCoordFromScreenCoord(0.5, 0.5)
|
||||
if not lastCoords or #(lastCoords - coords) > 100 then
|
||||
NewLoadSceneStartSphere(coords.x, coords.y, coords.z, 2000, 0)
|
||||
lastCoords = coords
|
||||
end
|
||||
Wait(500)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Control blocking thread
|
||||
CreateThread(function()
|
||||
while not Cutscene.done do
|
||||
DisableAllControlActions(0)
|
||||
DisableFrontendThisFrame()
|
||||
Wait(3)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Main cutscene loop
|
||||
while not HasCutsceneFinished() and not Cutscene.done do
|
||||
Wait(0)
|
||||
if IsDisabledControlJustPressed(0, 200) then
|
||||
DoScreenFadeOut(FADE_DURATION)
|
||||
Wait(FADE_DURATION)
|
||||
StopCutsceneImmediately()
|
||||
Wait(500)
|
||||
Cutscene.done = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Cleanup
|
||||
DoScreenFadeIn(FADE_DURATION)
|
||||
for _, playerData in ipairs(cutsceneData.peds) do
|
||||
local ped = playerData.ped
|
||||
if not ped or not DoesEntityExist(ped) then goto continue end
|
||||
|
||||
if playerData.identifier == 'script' then
|
||||
DeleteEntity(ped)
|
||||
elseif playerData.identifier == 'localplayer' then
|
||||
SetEntityCoords(ped, playerData.coords.x, playerData.coords.y, playerData.coords.z, false, false, false, false)
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
Cutscene.done = true
|
||||
end
|
||||
|
||||
|
||||
-- RegisterCommand('cutscene', function(source, args, rawCommand)
|
||||
-- local cutscene = args[1]
|
||||
-- if cutscene then
|
||||
-- local cutsceneData = Cutscene.Create(cutscene, false, { 1,1,1 })
|
||||
-- Cutscene.Start(cutsceneData)
|
||||
|
||||
-- end
|
||||
-- end, false)
|
||||
|
||||
return Cutscene
|
329
resources/[carscripts]/community_bridge/lib/dui/client/dui.lua
Normal file
329
resources/[carscripts]/community_bridge/lib/dui/client/dui.lua
Normal file
|
@ -0,0 +1,329 @@
|
|||
-- -- DUI (Direct User Interface) Manager for FiveM
|
||||
-- -- Basic table-based implementation with full mouse support
|
||||
|
||||
-- ---@class DUI
|
||||
-- DUI = {}
|
||||
|
||||
-- -- Constants
|
||||
-- local DEFAULT_WIDTH <const> = 1280
|
||||
-- local DEFAULT_HEIGHT <const> = 720
|
||||
|
||||
-- -- Store active DUI instances
|
||||
-- local activeInstances = {}
|
||||
-- local instanceCounter = 0
|
||||
|
||||
-- -- Mouse tracking state
|
||||
-- local mouseState = {
|
||||
-- tracking = false,
|
||||
-- activeId = nil,
|
||||
-- lastX = 0,
|
||||
-- lastY = 0,
|
||||
-- isPressed = false,
|
||||
-- currentButton = nil
|
||||
-- }
|
||||
|
||||
-- -- Mouse button constants
|
||||
-- local MOUSE_BUTTONS <const> = {
|
||||
-- LEFT = "left",
|
||||
-- MIDDLE = "middle",
|
||||
-- RIGHT = "right"
|
||||
-- }
|
||||
|
||||
-- ---@class DUIInstance
|
||||
-- ---@field id number Unique identifier for this DUI instance
|
||||
-- ---@field url string URL loaded in the DUI
|
||||
-- ---@field width number Width of the DUI
|
||||
-- ---@field height number Height of the DUI
|
||||
-- ---@field handle number DUI browser handle
|
||||
-- ---@field txd string Texture dictionary name
|
||||
-- ---@field txn string Texture name
|
||||
-- ---@field active boolean Whether the DUI is active
|
||||
-- ---@field trackMouse boolean Whether mouse tracking is enabled for this instance
|
||||
-- ---@field mouseScale table<string, number> Scale factors for mouse coordinates
|
||||
|
||||
-- -- Create a new DUI instance
|
||||
-- ---@param url string URL to load
|
||||
-- ---@param width? number Width of the DUI (default: 1280)
|
||||
-- ---@param height? number Height of the DUI (default: 720)
|
||||
-- ---@return number|nil id The DUI instance ID if successful, nil if failed
|
||||
-- function DUI.Create(url, width, height)
|
||||
-- if not url or type(url) ~= "string" then
|
||||
-- return print("DUI.Create: URL is required and must be a string")
|
||||
-- end
|
||||
|
||||
-- width = width or DEFAULT_WIDTH
|
||||
-- height = height or DEFAULT_HEIGHT
|
||||
-- instanceCounter = instanceCounter + 1
|
||||
|
||||
-- local id = instanceCounter
|
||||
-- local handle = CreateDui(url, width, height)
|
||||
|
||||
-- if not handle then
|
||||
-- return print("DUI.Create: Failed to create DUI instance")
|
||||
-- end
|
||||
|
||||
-- local instance = {
|
||||
-- id = id,
|
||||
-- url = url,
|
||||
-- width = width,
|
||||
-- height = height,
|
||||
-- handle = handle,
|
||||
-- txd = "dui_" .. id,
|
||||
-- txn = "texture_" .. id,
|
||||
-- active = true,
|
||||
-- trackMouse = false,
|
||||
-- mouseScale = {
|
||||
-- x = 1.0,
|
||||
-- y = 1.0
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- -- Create runtime texture
|
||||
-- local duiHandle = GetDuiHandle(handle)
|
||||
-- CreateRuntimeTextureFromDuiHandle(CreateRuntimeTxd(instance.txd), instance.txn, duiHandle)
|
||||
|
||||
-- activeInstances[id] = instance
|
||||
-- return id
|
||||
-- end
|
||||
|
||||
-- -- Destroy a DUI instance
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.Destroy(id)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- if instance.handle then
|
||||
-- DestroyDui(instance.handle)
|
||||
-- end
|
||||
|
||||
-- activeInstances[id] = nil
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Set URL for a DUI instance
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param url string New URL to load
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.SetURL(id, url)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- SetDuiUrl(instance.handle, url)
|
||||
-- instance.url = url
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Send a message to the DUI
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param message table Message to send (will be JSON encoded)
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.SendMessage(id, message)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- SendDuiMessage(instance.handle, json.encode(message))
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Mouse interaction functions
|
||||
|
||||
-- -- Move mouse cursor on DUI
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param x number X coordinate
|
||||
-- ---@param y number Y coordinate
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.MoveMouse(id, x, y)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- SendDuiMouseMove(instance.handle, x, y)
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Simulate mouse button press
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param button "left"|"middle"|"right" Mouse button to simulate
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.MouseDown(id, button)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- if not MOUSE_BUTTONS[button:upper()] then
|
||||
-- return print("DUI.MouseDown: Invalid button. Must be 'left', 'middle', or 'right'")
|
||||
-- end
|
||||
|
||||
-- -- Update mouse state
|
||||
-- if instance.trackMouse then
|
||||
-- mouseState.isPressed = true
|
||||
-- mouseState.currentButton = button
|
||||
-- end
|
||||
|
||||
-- SendDuiMouseDown(instance.handle, button)
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Simulate mouse button release
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param button "left"|"middle"|"right" Mouse button to simulate
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.MouseUp(id, button)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- if not MOUSE_BUTTONS[button:upper()] then
|
||||
-- return print("DUI.MouseUp: Invalid button. Must be 'left', 'middle', or 'right'")
|
||||
-- end
|
||||
|
||||
-- -- Update mouse state
|
||||
-- if instance.trackMouse then
|
||||
-- mouseState.isPressed = false
|
||||
-- mouseState.currentButton = nil
|
||||
-- end
|
||||
|
||||
-- SendDuiMouseUp(instance.handle, button)
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Simulate mouse wheel movement
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param deltaY number Vertical scroll amount
|
||||
-- ---@param deltaX number Horizontal scroll amount
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.MouseWheel(id, deltaY, deltaX)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- SendDuiMouseWheel(instance.handle, deltaY, deltaX)
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Click helper function (combines MoveMouse, MouseDown, and MouseUp)
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param x number X coordinate
|
||||
-- ---@param y number Y coordinate
|
||||
-- ---@param button? "left"|"middle"|"right" Mouse button to click (default: "left")
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.Click(id, x, y, button)
|
||||
-- button = button or "left"
|
||||
|
||||
-- if not DUI.MoveMouse(id, x, y) then return false end
|
||||
-- if not DUI.MouseDown(id, button) then return false end
|
||||
|
||||
-- -- Small delay to simulate real click
|
||||
-- Citizen.Wait(50)
|
||||
|
||||
-- return DUI.MouseUp(id, button)
|
||||
-- end
|
||||
|
||||
-- -- Enable or disable mouse tracking for a DUI instance
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@param enabled boolean Whether to enable mouse tracking
|
||||
-- ---@param scaleX? number Scale factor for X coordinates (default: 1.0)
|
||||
-- ---@param scaleY? number Scale factor for Y coordinates (default: 1.0)
|
||||
-- ---@return boolean success Whether the operation was successful
|
||||
-- function DUI.TrackMouse(id, enabled, scaleX, scaleY)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return false end
|
||||
|
||||
-- instance.trackMouse = enabled
|
||||
-- instance.mouseScale.x = scaleX or 1.0
|
||||
-- instance.mouseScale.y = scaleY or 1.0
|
||||
|
||||
-- if enabled then
|
||||
-- mouseState.tracking = true
|
||||
-- mouseState.activeId = id
|
||||
-- elseif mouseState.activeId == id then
|
||||
-- mouseState.tracking = false
|
||||
-- mouseState.activeId = nil
|
||||
-- end
|
||||
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- -- Update mouse coordinates from screen position
|
||||
-- ---@param screenX number Screen X coordinate
|
||||
-- ---@param screenY number Screen Y coordinate
|
||||
-- ---@return boolean updated Whether coordinates were updated
|
||||
-- local function UpdateMousePosition(screenX, screenY)
|
||||
-- if not mouseState.tracking or not mouseState.activeId then return false end
|
||||
|
||||
-- local instance = activeInstances[mouseState.activeId]
|
||||
-- if not instance or not instance.trackMouse then return false end
|
||||
|
||||
-- -- Scale coordinates based on DUI dimensions and scale factors
|
||||
-- local scaledX = screenX * instance.mouseScale.x
|
||||
-- local scaledY = screenY * instance.mouseScale.y
|
||||
|
||||
-- -- Only update if position changed
|
||||
-- if scaledX ~= mouseState.lastX or scaledY ~= mouseState.lastY then
|
||||
-- mouseState.lastX = scaledX
|
||||
-- mouseState.lastY = scaledY
|
||||
-- DUI.MoveMouse(mouseState.activeId, scaledX, scaledY)
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
-- return false
|
||||
-- end
|
||||
|
||||
-- -- Mouse tracking thread
|
||||
-- Citizen.CreateThread(function()
|
||||
-- while true do
|
||||
-- if mouseState.tracking and mouseState.activeId then
|
||||
-- local instance = activeInstances[mouseState.activeId]
|
||||
-- if instance and instance.trackMouse then
|
||||
-- -- Get current cursor position
|
||||
-- local screenX, screenY = GetNuiCursorPosition()
|
||||
-- UpdateMousePosition(screenX, screenY)
|
||||
|
||||
-- -- Handle held mouse buttons
|
||||
-- if mouseState.isPressed and mouseState.currentButton then
|
||||
-- -- Keep the button pressed state
|
||||
-- SendDuiMouseDown(instance.handle, mouseState.currentButton)
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
-- Citizen.Wait(0)
|
||||
-- end
|
||||
-- end)
|
||||
|
||||
-- -- Utility functions
|
||||
|
||||
-- -- Get textures for a DUI instance
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@return string|nil txd, string|nil txn Texture dictionary and name, or nil if instance not found
|
||||
-- function DUI.GetTextures(id)
|
||||
-- local instance = activeInstances[id]
|
||||
-- if not instance then return nil, nil end
|
||||
|
||||
-- return instance.txd, instance.txn
|
||||
-- end
|
||||
|
||||
-- -- Check if a DUI instance exists
|
||||
-- ---@param id number DUI instance ID
|
||||
-- ---@return boolean exists Whether the instance exists and is active
|
||||
-- function DUI.Exists(id)
|
||||
-- local instance = activeInstances[id]
|
||||
-- return instance ~= nil and instance.active
|
||||
-- end
|
||||
|
||||
-- -- Get all active DUI instances
|
||||
-- ---@return table<number, DUIInstance> instances Table of active DUI instances
|
||||
-- function DUI.GetActiveInstances()
|
||||
-- return activeInstances
|
||||
-- end
|
||||
|
||||
-- -- Cleanup all DUI instances
|
||||
-- function DUI.CleanupAll()
|
||||
-- for id in pairs(activeInstances) do
|
||||
-- DUI.Destroy(id)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- -- Automatic cleanup on resource stop
|
||||
-- AddEventHandler('onResourceStop', function(resourceName)
|
||||
-- if GetCurrentResourceName() ~= resourceName then return end
|
||||
-- DUI.CleanupAll()
|
||||
-- end)
|
||||
|
||||
-- return DUI
|
|
@ -0,0 +1,248 @@
|
|||
Utility = Utility or Require("lib/utility/client/utility.lua")
|
||||
Ids = Ids or Require("lib/utility/shared/ids.lua")
|
||||
Point = Point or Require("lib/points/client/points.lua")
|
||||
ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions_ext.lua") -- Added
|
||||
|
||||
local Entities = {} -- Stores entity data received from server
|
||||
ClientEntity = {} -- Renamed from BaseEntity
|
||||
|
||||
local function SpawnEntity(entityData)
|
||||
entityData = entityData and entityData.args
|
||||
if entityData.spawned and DoesEntityExist(entityData.spawned) then return end -- Already spawned
|
||||
-- for k, v in pairs(entityData.args) do
|
||||
-- print(string.format("SpawnEntity %s: %s", k, v))
|
||||
-- end
|
||||
local model = entityData.model and type(entityData.model) == 'string' and GetHashKey(entityData.model) or entityData.model
|
||||
if model and not Utility.LoadModel(model) then
|
||||
print(string.format("[ClientEntity] Failed to load model %s for entity %s", entityData.model, entityData.id))
|
||||
return
|
||||
end
|
||||
|
||||
local entity = nil
|
||||
local coords = entityData.coords
|
||||
local rotation = entityData.rotation or vector3(0.0, 0.0, 0.0) -- Default rotation if not provided
|
||||
|
||||
if entityData.entityType == 'object' then
|
||||
entity = CreateObject(model, coords.x, coords.y, coords.z, false, false, false)
|
||||
SetEntityRotation(entity, rotation.x, rotation.y, rotation.z, 2, true)
|
||||
elseif entityData.entityType == 'ped' then
|
||||
entity = CreatePed(4, model, coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or rotation.z, false, false)
|
||||
elseif entityData.entityType == 'vehicle' then
|
||||
entity = CreateVehicle(model, coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or rotation.z, false, false)
|
||||
else
|
||||
print(string.format("[ClientEntity] Unknown entity type '%s' for entity %s", entityData.entityType, entityData.id))
|
||||
end
|
||||
if entity and model then
|
||||
entityData.spawned = entity
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
SetEntityAsMissionEntity(entity, true, true)
|
||||
FreezeEntityPosition(entity, true)
|
||||
else
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
end
|
||||
if entityData.OnSpawn then
|
||||
entityData.OnSpawn(entityData)
|
||||
end
|
||||
end
|
||||
|
||||
local function RemoveEntity(entityData)
|
||||
entityData = entityData and entityData.args or entityData
|
||||
if not entityData then return end
|
||||
ClientEntityActions.StopAction(entityData.id)
|
||||
if entityData.spawned and DoesEntityExist(entityData.spawned) then
|
||||
local entityHandle = entityData.spawned
|
||||
entityData.spawned = nil
|
||||
SetEntityAsMissionEntity(entityHandle, false, false)
|
||||
DeleteEntity(entityHandle)
|
||||
end
|
||||
if entityData.OnRemove then
|
||||
entityData.OnRemove(entityData)
|
||||
end
|
||||
end
|
||||
|
||||
--- Registers an entity received from the server and sets up proximity spawning.
|
||||
-- @param entityData table Data received from the server via 'community_bridge:client:CreateEntity'
|
||||
function ClientEntity.Create(entityData)
|
||||
entityData.id = entityData.id or Ids.CreateUniqueId(Entities)
|
||||
if Entities[entityData.id] then return Entities[entityData.id] end -- Already registered
|
||||
Entities[entityData.id] = entityData
|
||||
return Point.Register(entityData.id, entityData.coords, entityData.spawnDistance or 50.0, entityData, SpawnEntity, RemoveEntity, function() end)
|
||||
end
|
||||
|
||||
--Depricated use ClientEntity.Create instead
|
||||
--- Registers an entity and spawns it in the world if not already spawned.
|
||||
ClientEntity.Register = ClientEntity.Create
|
||||
|
||||
|
||||
function ClientEntity.CreateBulk(entities)
|
||||
local registeredEntities = {}
|
||||
for _, entityData in pairs(entities) do
|
||||
local entity = ClientEntity.Create(entityData)
|
||||
registeredEntities[entity.id] = entity
|
||||
end
|
||||
return registeredEntities
|
||||
end
|
||||
|
||||
-- Depricated use ClientEntity.CreateBulk instead
|
||||
ClientEntity.RegisterBulk = ClientEntity.CreateBulk
|
||||
|
||||
|
||||
|
||||
--- Unregisters an entity and removes it from the world if spawned.
|
||||
-- @param id string|number The ID of the entity to unregister.
|
||||
function ClientEntity.Destroy(id)
|
||||
local entityData = Entities[id]
|
||||
if not entityData then return end
|
||||
|
||||
Point.Remove(id)
|
||||
RemoveEntity(entityData)
|
||||
Entities[id] = nil
|
||||
end
|
||||
ClientEntity.Unregister = ClientEntity.Destroy
|
||||
|
||||
|
||||
--- Updates the data for a registered entity.
|
||||
-- @param id string|number The ID of the entity to update.
|
||||
-- @param data table The data fields to update.
|
||||
function ClientEntity.Update(id, data)
|
||||
local entityData = Entities[id]
|
||||
-- print(string.format("[ClientEntity] Updating entity %s", id))
|
||||
if not entityData then return end
|
||||
|
||||
local needsPointUpdate = false
|
||||
for key, value in pairs(data) do
|
||||
if key == 'coords' and #(entityData.coords - value) > 0.1 then
|
||||
needsPointUpdate = true
|
||||
end
|
||||
if key == 'spawnDistance' and entityData.spawnDistance ~= value then
|
||||
needsPointUpdate = true
|
||||
end
|
||||
entityData[key] = value
|
||||
end
|
||||
|
||||
-- If entity is currently spawned, apply updates
|
||||
if entityData.spawned and DoesEntityExist(entityData.spawned) then
|
||||
if data.coords then
|
||||
SetEntityCoords(entityData.spawned, entityData.coords.x, entityData.coords.y, entityData.coords.z, false, false, false, true)
|
||||
end
|
||||
if data.rotation then
|
||||
if entityData.entityType == 'object' then
|
||||
SetEntityRotation(entityData.spawned, entityData.rotation.x, entityData.rotation.y, entityData.rotation.z, 2, true)
|
||||
else -- Ped/Vehicle heading
|
||||
SetEntityHeading(entityData.spawned, type(entityData.rotation) == 'number' and entityData.rotation or entityData.rotation.z)
|
||||
end
|
||||
end
|
||||
if data.freeze ~= nil then
|
||||
FreezeEntityPosition(entityData.spawned, data.freeze)
|
||||
end
|
||||
-- Add other updatable properties as needed
|
||||
end
|
||||
|
||||
-- Update Point registration if coords or distance changed
|
||||
if needsPointUpdate then
|
||||
Point.Remove(id)
|
||||
Point.Register(
|
||||
entityData.id,
|
||||
entityData.coords,
|
||||
entityData.spawnDistance or 50.0,
|
||||
SpawnEntity,
|
||||
RemoveEntity,
|
||||
nil,
|
||||
entityData
|
||||
)
|
||||
end
|
||||
|
||||
if entityData.OnUpdate and type(entityData.OnUpdate) == 'function' then
|
||||
entityData.OnUpdate(entityData, data)
|
||||
end
|
||||
end
|
||||
|
||||
function ClientEntity.Get(id)
|
||||
return Entities[id]
|
||||
end
|
||||
|
||||
function ClientEntity.GetAll()
|
||||
return Entities
|
||||
end
|
||||
|
||||
function ClientEntity.RegisterAction(name, func)
|
||||
ClientEntityActions.RegisterAction(name, func)
|
||||
end
|
||||
|
||||
function ClientEntity.OnCreate(_type, func)
|
||||
ClientEntity.OnCreates = ClientEntity.OnCreates or {}
|
||||
if not ClientEntity.OnCreates[_type] then
|
||||
ClientEntity.OnCreates[_type] = {}
|
||||
end
|
||||
table.insert(ClientEntity.OnCreates[_type], func)
|
||||
end
|
||||
|
||||
-- Network Event Handlers
|
||||
RegisterNetEvent("community_bridge:client:CreateEntity", function(entityData)
|
||||
ClientEntity.Create(entityData)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:client:CreateEntities", function(entities)
|
||||
ClientEntity.CreateBulk(entities)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:client:DeleteEntity", function(id)
|
||||
ClientEntity.Unregister(id)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:client:UpdateEntity", function(id, data)
|
||||
ClientEntity.Update(id, data)
|
||||
end)
|
||||
|
||||
-- New handler for entity actions
|
||||
RegisterNetEvent("community_bridge:client:TriggerEntityAction", function(entityId, actionName, ...)
|
||||
local entityData = Entities[entityId]
|
||||
-- Check if entity exists locally (it doesn't need to be spawned to queue actions)
|
||||
if entityData then
|
||||
if actionName == "Stop" then
|
||||
ClientEntityActions.StopAction(entityId)
|
||||
elseif actionName == "Skip" then
|
||||
ClientEntityActions.SkipAction(entityId)
|
||||
else
|
||||
print(string.format("[ClientEntity] Triggering action '%s' for entity %s", actionName, entityId))
|
||||
local currentAction = ClientEntityActions.ActionQueue[entityId] and ClientEntityActions.ActionQueue[entityId][1]
|
||||
ClientEntityActions.QueueAction(entityData, actionName, ...)
|
||||
end
|
||||
-- else
|
||||
-- Optional: Log if action received but entity doesn't exist locally at all
|
||||
-- print(string.format("[ClientEntity] Received action '%s' for non-existent entity %s.", actionName, entityId))
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:client:TriggerEntityActions", function(entityId, actions, endPosition)
|
||||
local entityData = Entities[entityId]
|
||||
if entityData then
|
||||
for _, actionData in pairs(actions) do
|
||||
local actionName = actionData.name
|
||||
local actionParams = actionData.params
|
||||
if actionName == "Stop" then
|
||||
ClientEntityActions.StopAction(entityId)
|
||||
elseif actionName == "Skip" then
|
||||
ClientEntityActions.SkipAction(entityId)
|
||||
else
|
||||
local currentAction = ClientEntityActions.ActionQueue[entityId] and ClientEntityActions.ActionQueue[entityId][1]
|
||||
ClientEntityActions.QueueAction(entityData, actionName, table.unpack(actionParams))
|
||||
end
|
||||
end
|
||||
else
|
||||
print(string.format("[ClientEntity] Received actions for non-existent entity %s.", entityId))
|
||||
end
|
||||
end)
|
||||
|
||||
-- Resource Stop Cleanup
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if resourceName == GetCurrentResourceName() then
|
||||
for id, entityData in pairs(Entities) do
|
||||
Point.Remove(id) -- Clean up point registration
|
||||
RemoveEntity(entityData) -- Clean up spawned game entity
|
||||
end
|
||||
Entities = {} -- Clear local cache
|
||||
end
|
||||
end)
|
||||
|
||||
return ClientEntity
|
|
@ -0,0 +1,136 @@
|
|||
|
||||
|
||||
ClientEntityActions = {}
|
||||
ClientEntityActions.ActionThreads = {} -- Store running action threads { [entityId] = thread }
|
||||
ClientEntityActions.ActionQueue = {} -- Stores pending actions { [entityId] = {{name="ActionName", args={...}}, ...} }
|
||||
ClientEntityActions.IsActionRunning = {} -- Tracks if an action is currently running { [entityId] = boolean }
|
||||
ClientEntityActions.RegisteredActions = {} -- New: Registry for action implementations { [actionName] = function(entityData, ...) }
|
||||
-- Forward declaration
|
||||
|
||||
|
||||
--- Processes the next action in the queue for a given entity.
|
||||
-- @param entityId string|number The ID of the entity.
|
||||
function ClientEntityActions.ProcessNextAction(entityId)
|
||||
if ClientEntityActions.IsActionRunning [entityId] then return end -- Already running something
|
||||
local queue = ClientEntityActions.ActionQueue[entityId]
|
||||
if not queue or #queue == 0 then return end -- Queue is empty
|
||||
|
||||
local nextAction = table.remove(queue, 1) -- Dequeue (FIFO)
|
||||
local entityData = ClientEntity.Get(entityId) -- Assumes ClientEntity is accessible
|
||||
-- Check if entity is still valid and spawned before starting next action
|
||||
if not entityData or not entityData.spawned or not DoesEntityExist(entityData.spawned) then
|
||||
-- Entity despawned while idle, clear queue and do nothing
|
||||
ClientEntityActions.ActionQueue[entityId] = nil
|
||||
return
|
||||
end
|
||||
|
||||
-- Look up the action in the registry
|
||||
local actionFunc = ClientEntityActions.RegisteredActions [nextAction.name]
|
||||
if actionFunc then
|
||||
-- print(string.format("[ClientEntityActions] Starting action '%s' for entity %s", nextAction.name, entityId))
|
||||
ClientEntityActions.IsActionRunning [entityId] = true
|
||||
-- Call the registered function
|
||||
ClientEntityActions.ActionQueue[entityId] = actionFunc(entityData, table.unpack(nextAction.args))
|
||||
if not ClientEntityActions.ActionQueue[entityId] then
|
||||
-- If the action function doesn't return a queue, set it to nil
|
||||
ClientEntityActions.IsActionRunning[entityId] = false
|
||||
ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
|
||||
end
|
||||
else
|
||||
print(string.format("[ClientEntityActions] Unknown action '%s' dequeued for entity %s", nextAction.name, entityId))
|
||||
-- Skip unknown action and try the next one immediately
|
||||
ClientEntityActions.ActionQueue[entityId] = ClientEntityActions.ProcessNextAction(entityId)
|
||||
end
|
||||
end
|
||||
--- Registers a custom action implementation.
|
||||
-- The action function should handle its own logic, including threading if needed,
|
||||
-- and MUST call ClientEntityActions.ProcessNextAction(entityData.id) when it completes or fails,
|
||||
-- after setting ClientEntityActions.IsActionRunning [entityData.id] = false.
|
||||
-- @param actionName string The name used to trigger this action.
|
||||
-- @param actionFunc function The function to execute. Signature: function(entityData, ...)
|
||||
function ClientEntityActions.RegisterAction(actionName, actionFunc)
|
||||
if ClientEntityActions.RegisteredActions [actionName] then
|
||||
print(string.format("[ClientEntityActions] WARNING: Overwriting registered action '%s'", actionName))
|
||||
end
|
||||
assert(type(actionName) == "string", "actionName must be a string")
|
||||
ClientEntityActions.RegisteredActions[actionName] = actionFunc
|
||||
-- print(string.format("[ClientEntityActions] Registered action: %s", actionName))
|
||||
end
|
||||
|
||||
--- Queues an action for an entity. Starts processing if idle.
|
||||
-- @param entityData table The entity data.
|
||||
-- @param actionName string The name of the action.
|
||||
-- @param ... any Arguments for the action.
|
||||
function ClientEntityActions.QueueAction(entityData, actionName, ...)
|
||||
local entityId = entityData.id
|
||||
if not ClientEntityActions.ActionQueue[entityId] then
|
||||
ClientEntityActions.ActionQueue[entityId] = {}
|
||||
end
|
||||
|
||||
local actionArgs = {...}
|
||||
table.insert(ClientEntityActions.ActionQueue[entityId], { name = actionName, args = actionArgs })
|
||||
-- print(string.format("[ClientEntityActions] Queued action '%s' for entity %s. Queue size: %d", actionName, entityId, #ClientEntityActions.ActionQueue[entityId]))
|
||||
|
||||
-- If the entity isn't currently doing anything, start processing immediately
|
||||
if not ClientEntityActions.IsActionRunning [entityId] then
|
||||
ClientEntityActions.ProcessNextAction(entityId)
|
||||
end
|
||||
end
|
||||
--- Stops the current action and clears the queue for a specific entity.
|
||||
-- @param entityId string|number The ID of the entity.
|
||||
function ClientEntityActions.StopAction(entityId)
|
||||
-- print(string.format("[ClientEntityActions] Stopping all actions for entity %s", entityId))
|
||||
ClientEntityActions.ActionQueue[entityId] = nil -- Clear the queue
|
||||
ClientEntityActions.IsActionRunning [entityId] = false -- Mark as not running (this will stop loops in threads)
|
||||
|
||||
-- Stop current task/thread if applicable
|
||||
if ClientEntityActions.ActionThreads[entityId] then
|
||||
-- Lua threads stop themselves based on ClientEntityActions.IsActionRunning flag
|
||||
ClientEntityActions.ActionThreads[entityId] = nil
|
||||
end
|
||||
|
||||
-- Specific task clearing for peds
|
||||
local entityData = ClientEntity.Get(entityId)
|
||||
if entityData and entityData.spawned and DoesEntityExist(entityData.spawned) then
|
||||
if IsEntityAPed(entityData.spawned) then
|
||||
ClearPedTasksImmediately(entityData.spawned) -- Use Immediately for forceful stop
|
||||
end
|
||||
-- Other entity types might need different stop logic
|
||||
end
|
||||
end
|
||||
--- Skips the current action and starts the next one in the queue, if any.
|
||||
-- @param entityId string|number The ID of the entity.
|
||||
function ClientEntityActions.SkipAction(entityId)
|
||||
if not ClientEntityActions.IsActionRunning [entityId] then
|
||||
-- print(string.format("[ClientEntityActions] SkipAction called for %s, but no action running.", entityId))
|
||||
return -- Nothing to skip
|
||||
end
|
||||
-- print(string.format("[ClientEntityActions] Skipping current action for entity %s", entityId))
|
||||
|
||||
ClientEntityActions.IsActionRunning [entityId] = false -- Mark as not running (this will stop loops in threads)
|
||||
|
||||
-- Stop current task/thread if applicable
|
||||
if ClientEntityActions.ActionThreads[entityId] then
|
||||
ClientEntityActions.ActionThreads[entityId] = nil
|
||||
end
|
||||
|
||||
local entityData = ClientEntity.Get(entityId)
|
||||
if entityData and entityData.spawned and DoesEntityExist(entityData.spawned) then
|
||||
if IsEntityAPed(entityData.spawned) then
|
||||
ClearPedTasksImmediately(entityData.spawned)
|
||||
end
|
||||
end
|
||||
|
||||
-- Immediately try to process the next action
|
||||
ClientEntityActions.ProcessNextAction(entityId)
|
||||
end
|
||||
-- Add server-callable functions for Stop and Skip
|
||||
function ClientEntityActions.Stop(entityData)
|
||||
ClientEntityActions.StopAction(entityData.id)
|
||||
end
|
||||
|
||||
function ClientEntityActions.Skip(entityData)
|
||||
ClientEntityActions.SkipAction(entityData.id)
|
||||
end
|
||||
|
||||
return ClientEntityActions
|
|
@ -0,0 +1,359 @@
|
|||
DefaultActions = {}
|
||||
ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions.lua")
|
||||
LA = LA or Require("lib/utility/shared/la.lua")
|
||||
|
||||
--- Internal implementation for walking. Registered via RegisterAction.
|
||||
function DefaultActions.WalkTo(entityData, 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)
|
||||
if not Utility.LoadModel(modelHash) then
|
||||
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)
|
||||
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)
|
||||
entityData.props = entityData.props or {} -- Ensure props table exists
|
||||
table.insert(entityData.props, prop) -- Store the prop handle in the entity data
|
||||
-- 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
|
||||
|
||||
function DefaultActions.Freeze(entityData, freeze)
|
||||
local entity = entityData.spawned
|
||||
local entityId = entityData.id
|
||||
|
||||
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
|
||||
|
||||
FreezeEntityPosition(entity, freeze or true)
|
||||
|
||||
-- This action finishes immediately
|
||||
ClientEntityActions.IsActionRunning[entityId] = false
|
||||
ClientEntityActions.ProcessNextAction(entityId)
|
||||
end
|
||||
|
||||
function DefaultActions.PlaceOnGround(entityData)
|
||||
local entity = entityData.spawned
|
||||
local entityId = entityData.id
|
||||
|
||||
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
|
||||
PlaceObjectOnGroundProperly(entity)
|
||||
|
||||
-- This action finishes immediately
|
||||
-- ClientEntityActions.IsActionRunning[entityId] = false
|
||||
-- ClientEntityActions.ProcessNextAction(entityId)
|
||||
end
|
||||
|
||||
function DefaultActions.BobUpAndDown(entityData, speed, height)
|
||||
local entity = entityData.spawned
|
||||
local entityId = entityData.id
|
||||
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
|
||||
CreateThread(function()
|
||||
local coords = GetEntityCoords(entity)
|
||||
local originalZ = coords.z
|
||||
while DoesEntityExist(entity) do
|
||||
-- Calculate the new Z coordinate
|
||||
local newZ = originalZ + math.sin(GetGameTimer() * (speed / 1000)) * height
|
||||
-- Set the new coordinates
|
||||
SetEntityCoords(entity, coords.x, coords.y, newZ)
|
||||
-- Wait for 10 milliseconds
|
||||
Wait(10)
|
||||
end
|
||||
end)
|
||||
-- ClientEntityActions.IsActionRunning[entityId] = false
|
||||
-- ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
|
||||
end
|
||||
|
||||
function DefaultActions.Circle(entityData, radius, speed)
|
||||
local entity = entityData.spawned
|
||||
local entityId = entityData.id
|
||||
|
||||
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 coords = GetEntityCoords(entity)
|
||||
local angle = 0.0
|
||||
CreateThread(function()
|
||||
while DoesEntityExist(entity) do
|
||||
FreezeEntityPosition(entity, false)
|
||||
local pos = LA.Circle(angle, radius, coords)
|
||||
SetEntityCoords(entity, pos.x, pos.y, pos.z, false, false, false, false)
|
||||
angle = angle + speed * GetFrameTime()
|
||||
FreezeEntityPosition(entity, true)
|
||||
Wait(0)
|
||||
end
|
||||
end)
|
||||
-- ClientEntityActions.IsActionRunning[entityId] = false
|
||||
-- ClientEntityActions.ProcessNextAction(entityId) -- Try next action if this one failed immediately
|
||||
end
|
||||
|
||||
function DefaultActions.Collisions(entityData, enable, keepPhysics)
|
||||
local entity = entityData.spawned
|
||||
SetEntityCollision(entity, enable, keepPhysics)
|
||||
end
|
||||
|
||||
for name, func in pairs(DefaultActions) do
|
||||
ClientEntityActions.RegisterAction(name, func)
|
||||
end
|
||||
|
||||
|
||||
return ClientEntityActions
|
|
@ -0,0 +1,135 @@
|
|||
Ids = Ids or Require("lib/utility/shared/ids.lua")
|
||||
|
||||
local Entities = {}
|
||||
ServerEntity = {} -- Renamed from EntityRelay
|
||||
|
||||
--- Creates a server-side representation of an entity and notifies clients.
|
||||
-- @param entityType string 'object', 'ped', or 'vehicle'
|
||||
-- @param model string|number
|
||||
-- @param coords vector3
|
||||
-- @param rotation vector3|number Heading for peds/vehicles, rotation for objects
|
||||
-- @param meta table Optional additional data
|
||||
-- @return table The created entity data
|
||||
function ServerEntity.New(id, entityType, model, coords, rotation, meta)
|
||||
local self = meta or {}
|
||||
self.id = id or Ids.CreateUniqueId(Entities)
|
||||
self.entityType = entityType
|
||||
self.model = model
|
||||
self.coords = coords
|
||||
self.rotation = rotation or (entityType == 'object' and vector3(0.0, 0.0, 0.0) or 0.0) -- Default rotation or heading
|
||||
self.resource = GetInvokingResource()
|
||||
|
||||
assert(self.id, "ID Failed to generate")
|
||||
assert(self.entityType, "EntityType is required")
|
||||
assert(self.model, "Model is required for entity creation")
|
||||
assert(self.coords, "Coords are required for entity creation")
|
||||
ServerEntity.Add(self)
|
||||
return self
|
||||
end
|
||||
function ServerEntity.Create(id, entityType, model, coords, rotation, meta)
|
||||
local self = ServerEntity.New(id, entityType, model, coords, rotation, meta)
|
||||
if not self then
|
||||
print("Failed to create entity with ID: " .. tostring(id))
|
||||
return nil
|
||||
end
|
||||
TriggerClientEvent("community_bridge:client:CreateEntity", -1, self)
|
||||
return self
|
||||
end
|
||||
|
||||
function ServerEntity.CreateBulk(entities)
|
||||
local createdEntities = {}
|
||||
for _, entityData in pairs(entities) do
|
||||
local id = entityData.id or Ids.CreateUniqueId(Entities)
|
||||
local entity = ServerEntity.New(
|
||||
id,
|
||||
entityData.entityType,
|
||||
entityData.model,
|
||||
entityData.coords,
|
||||
entityData.rotation,
|
||||
entityData.meta
|
||||
)
|
||||
createdEntities[id] = entity
|
||||
end
|
||||
TriggerClientEvent("community_bridge:client:CreateEntities", -1, createdEntities)
|
||||
return createdEntities
|
||||
end
|
||||
|
||||
--- Deletes a server-side entity representation and notifies clients.
|
||||
-- @param id string|number The ID of the entity to delete.
|
||||
function ServerEntity.Delete(id)
|
||||
if Entities[id] then
|
||||
ServerEntity.Remove(id)
|
||||
TriggerClientEvent("community_bridge:client:DeleteEntity", -1, id)
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates data for a server-side entity and notifies clients.
|
||||
-- @param id string|number The ID of the entity to update.
|
||||
-- @param data table The data fields to update.
|
||||
function ServerEntity.Update(id, data)
|
||||
local entity = Entities[id]
|
||||
print("Updating entity: ", id, entity)
|
||||
if not entity then return false end
|
||||
|
||||
for key, value in pairs(data) do
|
||||
entity[key] = value
|
||||
end
|
||||
TriggerClientEvent("community_bridge:client:UpdateEntity", -1, id, data)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Triggers a specific action on the client-side entity.
|
||||
-- Clients will only execute the action if the entity is currently spawned for them.
|
||||
-- @param entityId string|number The ID of the entity.
|
||||
-- @param actionName string The name of the action to trigger (must match a function in ClientEntityActions).
|
||||
-- @param ... any Additional arguments for the action function.
|
||||
function ServerEntity.TriggerAction(entityId, actionName, endPosition, ...)
|
||||
print("Triggering action: ", entityId, actionName, ...)
|
||||
local entity = Entities[entityId]
|
||||
if not entity then
|
||||
print(string.format("[ServerEntity] Attempted to trigger action '%s' on non-existent entity %s", actionName, entityId))
|
||||
return
|
||||
end
|
||||
TriggerClientEvent("community_bridge:client:TriggerEntityAction", -1, entityId, actionName, endPosition, ...)
|
||||
end
|
||||
|
||||
function ServerEntity.TriggerActions(entityId, actions, endPosition)
|
||||
local entity = Entities[entityId]
|
||||
if not entity then
|
||||
print(string.format("[ServerEntity] Attempted to trigger actions on non-existent entity %s", entityId))
|
||||
return
|
||||
end
|
||||
TriggerClientEvent("community_bridge:client:TriggerEntityActions", -1, entityId, actions, endPosition)
|
||||
end
|
||||
|
||||
function ServerEntity.GetAll()
|
||||
return Entities
|
||||
end
|
||||
|
||||
function ServerEntity.Get(id)
|
||||
return Entities[id]
|
||||
end
|
||||
|
||||
function ServerEntity.Add(self)
|
||||
Entities[self.id] = self
|
||||
end
|
||||
|
||||
function ServerEntity.Remove(id)
|
||||
Entities[id] = nil
|
||||
end
|
||||
|
||||
-- Clean up entities associated with a stopped resource
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
local toDelete = {}
|
||||
for id, entity in pairs(Entities) do
|
||||
if entity.resource == resourceName then
|
||||
table.insert(toDelete, id)
|
||||
end
|
||||
end
|
||||
for _, id in pairs(toDelete) do
|
||||
ServerEntity.Delete(id)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
return ServerEntity
|
|
@ -0,0 +1,49 @@
|
|||
local Actions = {}
|
||||
Action = {}
|
||||
|
||||
|
||||
if not IsDuplicityVersion() then goto client end
|
||||
|
||||
function Action.Fire(id, players, ...)
|
||||
local action = Actions[id]
|
||||
if not action then return end
|
||||
if type(players) == "table" then
|
||||
for _, player in ipairs(players) do
|
||||
TriggerClientEvent(GetCurrentResourceName() .. "client:Action", tonumber(player), id, ...)
|
||||
end
|
||||
return
|
||||
end
|
||||
TriggerClientEvent(GetCurrentResourceName() .. "client:Action", tonumber(players or -1), id, ...)
|
||||
end
|
||||
|
||||
if IsDuplicityVersion() then return Actions end
|
||||
::client::
|
||||
|
||||
function Action.Create(id, action)
|
||||
assert(type(id) == "string", "id must be a string")
|
||||
assert(type(action) == "function", "action must be a function")
|
||||
Actions[id] = action
|
||||
end
|
||||
|
||||
function Action.Remove(id)
|
||||
Actions[id] = nil
|
||||
end
|
||||
|
||||
function Action.Get(id)
|
||||
return Actions[id]
|
||||
end
|
||||
|
||||
function Action.GetAll()
|
||||
return Actions
|
||||
end
|
||||
|
||||
RegisterNetEvent(GetCurrentResourceName() .. "client:Action", function(id, ...)
|
||||
local action = Actions[id]
|
||||
if not action then return end
|
||||
action(...)
|
||||
end)
|
||||
|
||||
exports("Action", Action)
|
||||
return Action
|
||||
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
ItemsBuilder = ItemsBuilder or {}
|
||||
|
||||
ItemsBuilder = {}
|
||||
|
||||
---This will generate the items in the formats of qb_core, qb_core_old and ox_inventory. It will then build a lua file in the generateditems folder of the community_bridge.
|
||||
---@param invoking string
|
||||
---@param itemsTable table
|
||||
ItemsBuilder.Generate = function(invoking, outputPrefix, itemsTable, useQB)
|
||||
if not itemsTable then return end
|
||||
invoking = invoking or GetInvokingResource() or GetCurrentResourceName() or "community_bridge"
|
||||
|
||||
local resourcePath = string.format("%s/%s/", GetResourcePath(invoking), outputPrefix or "generated_items")
|
||||
|
||||
-- remove any doubles slashes after initial double slash
|
||||
-- resourcePath = resourcePath:gsub("//", "/")
|
||||
-- check if directory exists
|
||||
local folder = io.open(resourcePath, "r")
|
||||
if not folder then
|
||||
local createDirectoryCMD = string.format("if not exist \"%s\" mkdir \"%s\"", resourcePath, resourcePath)
|
||||
local returned, err = io.popen(createDirectoryCMD)
|
||||
if not returned then
|
||||
print("🌮 Failed to create directory: ", err)
|
||||
return
|
||||
end
|
||||
returned:close()
|
||||
print("🌮 Created Directory: ", resourcePath)
|
||||
else
|
||||
folder:close()
|
||||
end
|
||||
|
||||
local qbOld = {}
|
||||
local qbNew = {}
|
||||
local oxInventory = {}
|
||||
if useQB then
|
||||
for key, item in pairs(itemsTable) do
|
||||
qbOld[key] = string.format(
|
||||
"['%s'] = {name = '%s', label = '%s', weight = %s, type = 'item', image = '%s', unique = %s, useable = %s, shouldClose = %s, description = '%s'},",
|
||||
key, key, item.label, item.weight, item.image or key .. 'png', item.unique, item.useable, item.shouldClose, item.description
|
||||
)
|
||||
qbNew[key] = string.format(
|
||||
"['%s'] = {['name'] = '%s', ['label'] = '%s', ['weight'] = %s, ['type'] = 'item',['image'] = '%s', ['unique'] = %s, ['useable'] = %s, ['shouldClose'] = %s, ['description'] = '%s'},",
|
||||
key, key, item.label, item.weight, item.image or key .. 'png', item.unique, item.useable, item.shouldClose, item.description
|
||||
)
|
||||
imagewithoutpng = item?.image and item.image:gsub(".png", "")
|
||||
shouldRenderImage = imagewithoutpng and imagewithoutpng ~= key
|
||||
oxInventory[key] = string.format(
|
||||
[[
|
||||
["%s"] = {
|
||||
label = "%s",
|
||||
description = "%s",
|
||||
weight = %s,
|
||||
stack = %s,
|
||||
close = %s,
|
||||
%s
|
||||
}, ]],
|
||||
key, item.label, item.description, item.weight, not item.unique, item.shouldClose,
|
||||
shouldRenderImage and item.image and string.format([[client = {
|
||||
image = '%s'
|
||||
}]], item.image) or ""
|
||||
)
|
||||
end
|
||||
|
||||
else
|
||||
for key, item in pairs(itemsTable) do
|
||||
-- ['peanut_butter'] = {['name'] = 'peanut_butter',['label'] = 'Peanut Butter',['weight'] = 1000,['type'] = 'item',['image'] = 'peanut_butter.png',['unique'] = false,['useable'] = false,['shouldClose'] = true,['combinable'] = nil,['description'] = 'A cooking ingredient'},
|
||||
|
||||
qbOld[key] = string.format(
|
||||
"['%s'] = {name = '%s', label = '%s', weight = %s, type = 'item', image = '%s', unique = %s, useable = %s, shouldClose = %s, description = '%s'},",
|
||||
key, key, item.label, item.weight, item?.client?.image or key .. 'png', not item.stack, true, item.close, item.description
|
||||
)
|
||||
qbNew[key] = string.format(
|
||||
"['%s'] = {['name'] = '%s', ['label'] = '%s', ['weight'] = %s, ['type'] = 'item', ['image'] = '%s', ['unique'] = %s, ['useable'] = %s, ['shouldClose'] = %s, ['description'] = '%s'},",
|
||||
key, key, item.label, item.weight, item?.client?.image or key .. 'png', not item.stack, true, item.close, item.description
|
||||
)
|
||||
imagewithoutpng = item?.client?.image and item.client.image:gsub(".png", "")
|
||||
shouldRenderImage = imagewithoutpng and imagewithoutpng ~= key
|
||||
oxInventory[key] = string.format(
|
||||
[[
|
||||
["%s"] = {
|
||||
label = "%s",
|
||||
description = "%s",
|
||||
weight = %s,
|
||||
stack = %s,
|
||||
close = %s,
|
||||
%s
|
||||
}, ]],
|
||||
key, item.label, item.description, item.weight, item.stack, item.close,
|
||||
shouldRenderImage and item?.client?.image and string.format( [[client = {
|
||||
image = '%s'
|
||||
}]], item.client.image) or ""
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function write(fileName, content)
|
||||
local filePath = resourcePath .. fileName
|
||||
local file = io.open(filePath, "w")
|
||||
if file then
|
||||
for key, value in pairs(content) do
|
||||
file:write(string.format("%s\n", value))
|
||||
end
|
||||
file:close()
|
||||
print("🌮 Items File Created: " .. filePath)
|
||||
else
|
||||
print("🌮 Something Broke for: " .. filePath)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
write(invoking.."(".."qb_core_old).lua", qbOld)
|
||||
write(invoking.."(".."qb_core_new).lua", qbNew)
|
||||
write(invoking.."(".."ox_inventory).lua", oxInventory)
|
||||
end
|
||||
|
||||
exports('ItemsBuilder', ItemsBuilder)
|
||||
return ItemsBuilder
|
|
@ -0,0 +1,74 @@
|
|||
LootTable = LootTable or {}
|
||||
|
||||
local lootTables = {}
|
||||
|
||||
LootTable.Register = function(name, items)
|
||||
local repackedTable = {}
|
||||
for k, v in pairs(items) do
|
||||
table.insert(repackedTable, {
|
||||
name = k,
|
||||
min = v.min,
|
||||
max = v.max,
|
||||
chance = v.chance,
|
||||
tier = v.tier,
|
||||
item = v.item,
|
||||
metadata = v.metadata
|
||||
})
|
||||
end
|
||||
lootTables[name] = repackedTable
|
||||
return lootTables[name]
|
||||
end
|
||||
|
||||
LootTable.Get = function(name)
|
||||
assert(name, "No Name Passed For Loot Table")
|
||||
return lootTables[name] or {}
|
||||
end
|
||||
|
||||
LootTable.GetRandomItem = function(name, tier, randomNumber)
|
||||
assert(name, "No Name Passed For Loot Table")
|
||||
if tier == nil then tier = 1 end
|
||||
local lootTable = lootTables[name] or {}
|
||||
math.randomseed(GetGameTimer())
|
||||
local chance = randomNumber or math.random(1, 100)
|
||||
for _, v in pairs(lootTable) do
|
||||
if v.chance <= chance and tier == v.tier then
|
||||
return { item = v.item, metadata = v.metadata, count = math.random(v.min, v.max), tier = v.tier, chance = v.chance}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
LootTable.GetRandomItems = function(name, tier, randomNumber)
|
||||
assert(name, "No Name Passed For Loot Table")
|
||||
if tier == nil then tier = 1 end
|
||||
local lootTable = LootTable.Get(name)
|
||||
math.randomseed(GetGameTimer())
|
||||
local chance = randomNumber or math.random(1, 100)
|
||||
local items = {}
|
||||
for _, v in pairs(lootTable) do
|
||||
if v.chance <= chance and tier == v.tier then
|
||||
table.insert(items,{item = v.item, metadata = v.metadata, count = math.random(v.min, v.max), tier = v.tier, chance = v.chance})
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
LootTable.GetRandomItemsWithLimit = function(name, tier, randomNumber)
|
||||
assert(name, "No Name Passed For Loot Table")
|
||||
if tier == nil then tier = 1 end
|
||||
local lootTable = LootTable.Get(name)
|
||||
math.randomseed(GetGameTimer())
|
||||
local chance = randomNumber or math.random(1, 100)
|
||||
local items = {}
|
||||
for k = #lootTable, 1, -1 do
|
||||
local v = lootTable[k]
|
||||
if chance <= v.chance and tier == v.tier then
|
||||
table.insert(items, {v.item, v.metadata, math.random(v.min, v.max), v.tier, v.chance})
|
||||
table.remove(lootTable, k)
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
--
|
||||
|
||||
exports('LootTable', LootTable)
|
||||
return LootTable
|
86
resources/[carscripts]/community_bridge/lib/init.lua
Normal file
86
resources/[carscripts]/community_bridge/lib/init.lua
Normal file
|
@ -0,0 +1,86 @@
|
|||
loadedModules = {}
|
||||
|
||||
function Require(modulePath, resourceName)
|
||||
if resourceName and type(resourceName) ~= "string" then
|
||||
resourceName = GetInvokingResource()
|
||||
end
|
||||
|
||||
if not resourceName then
|
||||
resourceName = "community_bridge"
|
||||
end
|
||||
|
||||
local id = resourceName .. ":" .. modulePath
|
||||
if loadedModules[id] then
|
||||
if BridgeSharedConfig.DebugLevel ~= 0 then
|
||||
print("^2 Returning cached module [" .. id .. "] ^0")
|
||||
end
|
||||
return loadedModules[id]
|
||||
end
|
||||
|
||||
local file = LoadResourceFile(resourceName, modulePath)
|
||||
if not file then
|
||||
error("Error loading file [" .. id .. "]")
|
||||
end
|
||||
|
||||
local chunk, loadErr = load(file, id)
|
||||
if not chunk then
|
||||
error("Error wrapping module [" .. id .. "] Message: " .. loadErr)
|
||||
end
|
||||
|
||||
local success, result = pcall(chunk)
|
||||
if not success then
|
||||
error("Error executing module [" .. id .. "] Message: " .. result)
|
||||
end
|
||||
loadedModules[id] = result
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
cLib = {
|
||||
Require = Require,
|
||||
Callback = Callback or Require("lib/utility/shared/callbacks.lua"),
|
||||
Ids = Ids or Require("lib/utility/shared/ids.lua"),
|
||||
ReboundEntities = ReboundEntities or Require("lib/utility/shared/rebound_entities.lua"),
|
||||
Tables = Tables or Require("lib/utility/shared/tables.lua"),
|
||||
Prints = Prints or Require("lib/utility/shared/prints.lua"),
|
||||
Math = Math or Require("lib/utility/shared/math.lua"),
|
||||
LA = LA or Require("lib/utility/shared/la.lua"),
|
||||
Perlin = Perlin or Require("lib/utility/shared/perlin.lua"),
|
||||
-- Action = Action or Require("lib/entities/shared/actions.lua"),
|
||||
}
|
||||
|
||||
exports('cLib', cLib)
|
||||
|
||||
if not IsDuplicityVersion() then goto client end
|
||||
|
||||
cLib.SQL = SQL or Require("lib/sql/server/sqlHandler.lua")
|
||||
cLib.Logs = Logs or Require("lib/logs/server/logs.lua")
|
||||
cLib.ItemsBuilder = ItemsBuilder or Require("lib/generators/server/ItemsBuilder.lua")
|
||||
cLib.LootTables = LootTables or Require("lib/generators/server/lootTables.lua")
|
||||
cLib.Cache = Cache or Require("lib/cache/shared/cache.lua")
|
||||
cLib.ServerEntity = ServerEntity or Require("lib/entities/server/server_entity.lua")
|
||||
cLib.Marker = Marker or Require("lib/markers/server/server.lua")
|
||||
cLib.Particle = Particle or Require("lib/particles/server/particles.lua")
|
||||
cLib.Shell = Shells or Require("lib/shells/server/shells.lua")
|
||||
if IsDuplicityVersion() then return cLib end
|
||||
::client::
|
||||
|
||||
cLib.Scaleform = Scaleform or Require("lib/scaleform/client/scaleform.lua")
|
||||
cLib.Placeable = Placeable or Require("lib/placers/client/object_placer.lua")
|
||||
cLib.Utility = Utility or Require("lib/utility/client/utility.lua")
|
||||
cLib.PlaceableObject = ObjectPlacer or Require("lib/placers/client/placeable_object.lua")
|
||||
cLib.Raycast = Raycast or Require("lib/raycast/client/raycast.lua")
|
||||
cLib.Point = Point or Require("lib/points/client/points.lua")
|
||||
cLib.Particle = Particle or Require("lib/particles/client/particles.lua")
|
||||
cLib.Cache = Cache or Require("lib/cache/client/cache.lua")
|
||||
cLib.ClientEntity = ClientEntity or Require("lib/entities/client/client_entity.lua")
|
||||
cLib.ClientEntityActions = ClientEntityActions or Require("lib/entities/client/client_entity_actions.lua")
|
||||
cLib.ClientStateBag = ClientStateBag or Require("lib/statebags/client/client.lua")
|
||||
cLib.Marker = Marker or Require("lib/markers/client/markers.lua")
|
||||
cLib.Anim = Anim or Require("lib/anim/client/client.lua")
|
||||
cLib.Cutscene = Cutscene or Require("lib/cutscenes/client/cutscene.lua")
|
||||
--cLib.DUI = DUI or Require("lib/dui/client/dui.lua")
|
||||
cLib.Particle = Particle or Require("lib/particles/client/particles.lua")
|
||||
|
||||
return cLib
|
|
@ -0,0 +1,59 @@
|
|||
Logs = Logs or {}
|
||||
|
||||
local WebhookURL = "" -- Add webhook URL here if using built-in logging system
|
||||
local LogoForEmbed = "https://cdn.discordapp.com/avatars/299410129982455808/31ce635662206e8bd0132c34ce9ce683?size=1024"
|
||||
local LogSystem = "built-in" -- Default log system, can be "built-in", "qb", or "ox_lib"
|
||||
|
||||
|
||||
---This will send a log to the configured webhook or log system.
|
||||
---@param src number
|
||||
---@param message string
|
||||
---@return nil
|
||||
Logs.Send = function(src, message)
|
||||
if not src or not message then return end
|
||||
local logType = LogSystem or "built-in"
|
||||
if logType == "built-in" then
|
||||
PerformHttpRequest(WebhookURL, function(err, text, headers) end, 'POST', json.encode(
|
||||
{
|
||||
username = "Community_Bridge's Logger",
|
||||
avatar_url = 'https://avatars.githubusercontent.com/u/192999457?s=400&u=da632e8f64c85def390cfd1a73c3b664d6882b38&v=4',
|
||||
embeds = {
|
||||
{
|
||||
color = "15769093",
|
||||
title = GetCurrentResourceName(),
|
||||
url = 'https://discord.gg/Gm6rYEXUsn',
|
||||
thumbnail = { url = LogoForEmbed },
|
||||
fields = {
|
||||
{
|
||||
name = '**Player ID**',
|
||||
value = src,
|
||||
inline = true,
|
||||
},
|
||||
{
|
||||
name = '**Player Identifier**',
|
||||
value = Framework.GetPlayerIdentifier(src),
|
||||
inline = true,
|
||||
},
|
||||
{
|
||||
name = 'Log Message',
|
||||
value = "```"..message.."```",
|
||||
inline = false,
|
||||
},
|
||||
},
|
||||
timestamp = os.date('!%Y-%m-%dT%H:%M:%S'),
|
||||
footer = {
|
||||
text = "Community_Bridge | ",
|
||||
icon_url = 'https://avatars.githubusercontent.com/u/192999457?s=400&u=da632e8f64c85def390cfd1a73c3b664d6882b38&v=4',
|
||||
},
|
||||
}
|
||||
}
|
||||
}), { ['Content-Type']= 'application/json' })
|
||||
elseif logType == "qb" then
|
||||
return TriggerEvent('qb-log:server:CreateLog', GetCurrentResourceName(), GetCurrentResourceName(), 'green', message)
|
||||
elseif logType == "ox_lib" then
|
||||
return lib.logger(src, GetCurrentResourceName(), message)
|
||||
end
|
||||
end
|
||||
|
||||
exports('Logs', Logs)
|
||||
return Logs
|
|
@ -0,0 +1,147 @@
|
|||
---@diagnostic disable: duplicate-set-field
|
||||
Marker = {}
|
||||
local Created = setmetatable({}, { __mode = "k" }) --what is mode?
|
||||
local Ids = Ids or Require("lib/utility/shared/ids.lua")
|
||||
local point = Point or Require("lib/points/client/points.lua")
|
||||
local loopRunning = false
|
||||
local Drawing = {}
|
||||
|
||||
--- This will validate the marker data.
|
||||
--- @param data table{position: vector3, offset: vector3, rotation: vector3, size: vector3, color: vector3, alpha: number, bobUpAndDown: boolean, marker: number, interaction: function}
|
||||
--- @return boolean
|
||||
local function validateMarkerData(data)
|
||||
if not data.position or not data.position.x or not data.position.y or not data.position.z then
|
||||
print("Invalid marker position. Must be a vector3 with x, y, and z coordinates.")
|
||||
return false
|
||||
end
|
||||
if data.offset and (not data.offset.x or not data.offset.y or not data.offset.z) then
|
||||
print("Invalid marker offset. Must be a vector3 with x, y, and z coordinates.")
|
||||
return false
|
||||
end
|
||||
if data.rotation and (not data.rotation.x or not data.rotation.y or not data.rotation.z) then
|
||||
print("Invalid marker rotation. Must be a vector3 with x, y, and z coordinates.")
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- This will create a marker.
|
||||
--- @param data table{position: vector3, offset: vector3, rotation: vector3, size: vector3, color: vector3, alpha: number, bobUpAndDown: boolean, marker: number, interaction: function}
|
||||
--- @return string|nil
|
||||
function Marker.Create(data)
|
||||
local _id = data.id or Ids.CreateUniqueId(Created)
|
||||
if not validateMarkerData(data) then return end
|
||||
local basePosition = data.position or vector3(0.0, 0.0, 0.0)
|
||||
local baseOffset = data.offset or vector3(0.0, 0.0, 0.0)
|
||||
|
||||
local data = {
|
||||
id = _id,
|
||||
position = vector3(basePosition.x + baseOffset.x, basePosition.y + baseOffset.y, basePosition.z + baseOffset.z),
|
||||
marker = data.marker or 1,
|
||||
rotation = data.rotation or vector3(0, 0, 0),
|
||||
size = data.size or vector3(1.0, 1.0, 1.0),
|
||||
color = data.color or vector3(0, 255, 0),
|
||||
alpha = data.alpha or 255,
|
||||
bobUpAndDown = data.bobUpAndDown,
|
||||
-- interaction = data.interaction or false, -- Uncomment if you re-implement interaction logic
|
||||
rotate = data.rotate,
|
||||
textureDict = data.textureDict,
|
||||
textureName = data.textureName,
|
||||
drawOnEnts = data.drawOnEnts,
|
||||
}
|
||||
Created[_id] = data
|
||||
|
||||
point.Register(
|
||||
_id, data.position, data.drawDistance or 50.0, data,
|
||||
function(_)
|
||||
if not Created[_id] then return point.Remove(_id) end
|
||||
Drawing[_id] = Created[_id]
|
||||
Marker.Run()
|
||||
end,
|
||||
function(markerData)
|
||||
Drawing[_id] = nil
|
||||
end, nil
|
||||
)
|
||||
return _id
|
||||
end
|
||||
|
||||
--- This will remove a marker.
|
||||
--- @param id string
|
||||
--- @return boolean
|
||||
function Marker.Remove(id)
|
||||
if not Created[id] then return false end
|
||||
point.Remove(id)
|
||||
Drawing[id] = nil
|
||||
Created[id] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
--- This will remove all markers.
|
||||
--- @param id string
|
||||
function Marker.RemoveAll()
|
||||
for id, _ in pairs(Created) do
|
||||
point.Remove(id)
|
||||
Created[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- This will loop through all marker(points) in range and draw them.
|
||||
-- @param data table{marker: number, position: vector3, rotation: vector3, size: vector3, color: vector3, alpha: number, bobUpAndDown: boolean, faceCamera: boolean, rotate: boolean, textureDict: string, textureName: string, drawOnEnts: boolean}
|
||||
-- @return nil
|
||||
function Marker.Run()
|
||||
if loopRunning then return end
|
||||
loopRunning = true
|
||||
CreateThread(function()
|
||||
while loopRunning do
|
||||
if not next(Created) then
|
||||
loopRunning = false
|
||||
return
|
||||
end
|
||||
for drawingId, drawingData in pairs(Drawing) do
|
||||
if not Created[drawingId] then
|
||||
Drawing[drawingId] = nil
|
||||
goto continue
|
||||
end
|
||||
DrawMarker(
|
||||
drawingData.marker,
|
||||
drawingData.position.x, drawingData.position.y, drawingData.position.z,
|
||||
0.0, 0.0, 0.0,
|
||||
drawingData.rotation.x, drawingData.rotation.y, drawingData.rotation.z,
|
||||
drawingData.size.x, drawingData.size.y, drawingData.size.z,
|
||||
drawingData.color.x, drawingData.color.y, drawingData.color.z, drawingData.alpha,
|
||||
drawingData.bobUpAndDown, drawingData.faceCamera, false,
|
||||
drawingData.rotate, drawingData.textureDict, drawingData.textureName, drawingData.drawOnEnts
|
||||
)
|
||||
::continue::
|
||||
end
|
||||
Wait(3)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:Marker", function(data)
|
||||
Marker.Create(data)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:MarkerBulk", function(datas)
|
||||
if not datas then return end
|
||||
for _, data in pairs(datas) do
|
||||
Marker.Create(data)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:MarkerRemove", function(id)
|
||||
Marker.Remove(id)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:MarkerRemoveBulk", function(ids)
|
||||
if not ids then return end
|
||||
for _, id in pairs(ids) do
|
||||
Marker.Remove(id)
|
||||
end
|
||||
end)
|
||||
|
||||
exports("Marker", Marker)
|
||||
|
||||
|
||||
return Marker
|
|
@ -0,0 +1,75 @@
|
|||
---@diagnostic disable: duplicate-set-field
|
||||
local id = Ids or Require("lib/utility/shared/ids.lua")
|
||||
Marker = {}
|
||||
local Created = {}
|
||||
|
||||
function Marker.New(data)
|
||||
if not data.position or not data.position.x or not data.position.y or not data.position.z then
|
||||
print("Invalid marker position. Must be a vector3 with x, y, and z coordinates.")
|
||||
return
|
||||
end
|
||||
|
||||
local _id = data.id or id.CreateUniqueId(Created)
|
||||
local data = {
|
||||
id = _id,
|
||||
position = data.position,
|
||||
offset = data.offset or vector3(0.0, 0.0, 0.0),
|
||||
marker = data.marker or 1,
|
||||
rotation = data.rotation or vector3(0, 0, 0),
|
||||
size = data.size or vector3(1.0, 1.0, 1.0),
|
||||
color = data.color or vector3(0, 255, 0),
|
||||
alpha = data.alpha or 255,
|
||||
bobUpAndDown = data.bobUpAndDown,
|
||||
-- interaction = data.interaction or false, -- Uncomment if you re-implement interaction logic
|
||||
rotate = data.rotate,
|
||||
textureDict = data.textureDict,
|
||||
textureName = data.textureName,
|
||||
drawOnEnts = data.drawOnEnts,
|
||||
}
|
||||
Created[_id] = data
|
||||
return _id
|
||||
end
|
||||
function Marker.Destroy(id)
|
||||
if not id or not Created[id] then return end
|
||||
Created[id] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
function Marker.Create(data)
|
||||
local _id = Marker.New(data)
|
||||
if not _id then return end
|
||||
TriggerClientEvent("community_bridge:Client:Marker", -1, Created[_id])
|
||||
return _id
|
||||
end
|
||||
|
||||
function Marker.Remove(id)
|
||||
if not Marker.Destroy(id) then return end
|
||||
TriggerClientEvent("community_bridge:Client:MarkerRemove", -1, id)
|
||||
end
|
||||
|
||||
function Marker.CreateBulk(datas)
|
||||
if not datas then return end
|
||||
local toClient = {}
|
||||
for k, v in pairs(datas) do
|
||||
table.insert(toClient, Marker.Create(v))
|
||||
end
|
||||
TriggerClientEvent("community_bridge:Client:MarkerBulk", -1, toClient)
|
||||
return toClient
|
||||
end
|
||||
|
||||
function Marker.RemoveBulk(ids)
|
||||
if not ids then return end
|
||||
local toClient = {}
|
||||
for k, v in pairs(ids) do
|
||||
local id = v
|
||||
if type(v) == "table" then
|
||||
id = v.id
|
||||
end
|
||||
Marker.Destroy(id)
|
||||
table.insert(toClient, id)
|
||||
end
|
||||
TriggerClientEvent("community_bridge:Client:MarkerRemoveBulk", -1, toClient)
|
||||
end
|
||||
|
||||
exports("Marker", Marker)
|
||||
return Marker
|
|
@ -0,0 +1,182 @@
|
|||
Particles = {}
|
||||
Particle = {}
|
||||
---@diagnostic disable: duplicate-set-field
|
||||
local Ids = Ids or Require("lib/utility/shared/ids.lua")
|
||||
local point = Require("lib/points/client/points.lua")
|
||||
|
||||
---Loads a ptfx asset into memory.
|
||||
---@param dict string
|
||||
---@return boolean
|
||||
function LoadPtfxAsset(dict)
|
||||
local failed = 100
|
||||
while not HasNamedPtfxAssetLoaded(dict) and failed >= 0 do
|
||||
RequestNamedPtfxAsset(dict)
|
||||
failed = failed - 1
|
||||
Wait(100)
|
||||
end
|
||||
assert(failed > 0, "Failed to load dict asset: " .. dict)
|
||||
return HasNamedPtfxAssetLoaded(dict)
|
||||
end
|
||||
|
||||
--- Create a particle effect at the specified position and rotation.
|
||||
--- @param dict string
|
||||
--- @param ptfx string
|
||||
--- @param pos vector3
|
||||
--- @param rot vector3
|
||||
--- @param scale number
|
||||
--- @param color vector3
|
||||
--- @param looped boolean
|
||||
--- @param loopLength number|nil
|
||||
--- @return number|nil ptfxHandle -- The handle of the particle effect, or nil if it failed to create.
|
||||
function Particle.Play(dict, ptfx, pos, rot, scale, color, looped, loopLength)
|
||||
LoadPtfxAsset(dict)
|
||||
UseParticleFxAssetNextCall(dict)
|
||||
SetParticleFxNonLoopedColour(color.x, color.y, color.z)
|
||||
local particle = nil
|
||||
if looped then
|
||||
particle = StartParticleFxLoopedAtCoord(ptfx, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, scale, false, false, false, false)
|
||||
CreateThread(function()
|
||||
if loopLength then
|
||||
Wait(loopLength)
|
||||
Particle.Remove(particle)
|
||||
end
|
||||
end)
|
||||
else
|
||||
particle = StartParticleFxNonLoopedAtCoord(ptfx, pos.x, pos.y, pos.z, rot.x, rot.y, rot.z, scale, false, false, false, false)
|
||||
end
|
||||
|
||||
return particle
|
||||
end
|
||||
|
||||
function Particle.Stop(particle)
|
||||
if not particle then return end
|
||||
StopParticleFxLooped(particle, false)
|
||||
RemoveParticleFx(particle, false)
|
||||
RemoveNamedPtfxAsset(particle)
|
||||
end
|
||||
|
||||
function Particle.Create(data)
|
||||
assert(data, "Particle data is nil")
|
||||
assert(data.dict, "Invalid particle data. Must contain string dict.")
|
||||
assert(data.ptfx, "Invalid particle data. Must contain string ptfx.")
|
||||
|
||||
local _id = data.id or Ids.CreateUniqueId(Particles)
|
||||
data = {
|
||||
id = _id,
|
||||
dict = data.dict,
|
||||
ptfx = data.ptfx,
|
||||
position = data.position or vector3(0.0, 0.0, 0.0),
|
||||
rotation = data.rotation or vector3(0, 0, 0),
|
||||
size = data.size or 1.0,
|
||||
color = data.color or vector3(255, 255, 255),
|
||||
looped = data.looped or false,
|
||||
loopLength = data.loopLength or nil,
|
||||
spawned = false,
|
||||
}
|
||||
Particles[_id] = data
|
||||
|
||||
point.Register( -- checking if players in range
|
||||
_id,
|
||||
data.position,
|
||||
data.drawDistance or 50.0,
|
||||
data,
|
||||
function(_)
|
||||
if not Particles[_id] then return point.Remove(_id) end
|
||||
local particleData = Particles[_id]
|
||||
if particleData.spawned then return end
|
||||
particleData.spawned = Particle.Play(
|
||||
particleData.dict,
|
||||
particleData.ptfx,
|
||||
particleData.position,
|
||||
particleData.rotation,
|
||||
particleData.size,
|
||||
particleData.color,
|
||||
particleData.looped,
|
||||
particleData.loopLength
|
||||
)
|
||||
end,
|
||||
function(markerData)
|
||||
if not Particles[_id] then return end
|
||||
local particleData = Particles[_id]
|
||||
if not particleData.spawned then return end
|
||||
Particle.Stop(particleData.spawned)
|
||||
particleData.spawned = nil
|
||||
end
|
||||
)
|
||||
return _id
|
||||
end
|
||||
|
||||
function Particle.Remove(id)
|
||||
if not id then return end
|
||||
local particle = Particles[id]
|
||||
if not particle then return end
|
||||
Particle.Stop(particle.spawned)
|
||||
Particles[id] = nil
|
||||
point.Remove(id)
|
||||
end
|
||||
|
||||
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:Particle", function(data)
|
||||
if not data then return end
|
||||
Particle.Create(data)
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:ParticleBulk", function(datas)
|
||||
if not datas then return end
|
||||
for _, data in pairs(datas) do
|
||||
Particle.Create(data)
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:ParticleRemove", function(id)
|
||||
local particle = Particles[id]
|
||||
if not particle then return end
|
||||
Particle.Remove(Drawing[id])
|
||||
end)
|
||||
|
||||
RegisterNetEvent("community_bridge:Client:ParticleRemoveBulk", function(ids)
|
||||
for _, id in pairs(ids) do
|
||||
Particle.Remove(id)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
function Particle.CreateOnEntity(dict, ptfx, entity, offset, rot, scale, color, looped, loopLength)
|
||||
LoadPtfxAsset(dict)
|
||||
UseParticleFxAssetNextCall(dict)
|
||||
SetParticleFxNonLoopedColour(color.x, color.y, color.z)
|
||||
local particle = nil
|
||||
if looped then
|
||||
particle = StartNetworkedParticleFxLoopedOnEntity(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, scale, false, false, false)
|
||||
if loopLength then
|
||||
Wait(loopLength)
|
||||
RemoveParticleFxFromEntity(entity)
|
||||
end
|
||||
else
|
||||
particle = StartNetworkedParticleFxLoopedOnEntity(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, scale, false, false, false)
|
||||
end
|
||||
RemoveNamedPtfxAsset(ptfx)
|
||||
return particle
|
||||
end
|
||||
|
||||
function Particle.CreateOnEntityBone(dict, ptfx, entity, bone, offset, rot, scale, color, looped, loopLength)
|
||||
LoadPtfxAsset(dict)
|
||||
UseParticleFxAssetNextCall(dict)
|
||||
SetParticleFxNonLoopedColour(color.x, color.y, color.z)
|
||||
local particle = nil
|
||||
if looped then
|
||||
particle = StartNetworkedParticleFxLoopedOnEntityBone(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, bone, scale, false, false, false)
|
||||
if loopLength then
|
||||
Wait(loopLength)
|
||||
RemoveParticleFxFromEntity(entity)
|
||||
end
|
||||
else
|
||||
particle = StartNetworkedParticleFxNonLoopedOnEntityBone(ptfx, entity, offset.x, offset.y, offset.z, rot.x, rot.y, rot.z, bone, scale, false, false, false)
|
||||
end
|
||||
RemoveNamedPtfxAsset(ptfx)
|
||||
return particle
|
||||
end
|
||||
|
||||
return Particle
|
|
@ -0,0 +1,72 @@
|
|||
---@diagnostic disable: duplicate-set-field
|
||||
|
||||
Particles = {}
|
||||
Particle = Particles or {}
|
||||
|
||||
function Particle.New(data)
|
||||
assert(data, "Particle data is nil")
|
||||
assert(data.dict, "Invalid particle data. Must contain string dict.")
|
||||
assert(data.ptfx, "Invalid particle data. Must contain string ptfx.")
|
||||
|
||||
local _id = data.id or id.CreateUniqueId(Particles)
|
||||
data = {
|
||||
id = _id,
|
||||
dict = data.dict,
|
||||
ptfx = data.ptfx,
|
||||
position = data.position or vector3(0.0, 0.0, 0.0),
|
||||
rotation = data.rotation or vector3(0, 0, 0),
|
||||
size = data.size or 1.0,
|
||||
color = data.color or vector3(255, 255, 255),
|
||||
looped = data.looped or false,
|
||||
loopLength = data.loopLength or nil,
|
||||
}
|
||||
Particles[_id] = data
|
||||
return data
|
||||
end
|
||||
|
||||
function Particle.Destroy(id)
|
||||
if not id or not Particles[id] then return end
|
||||
Particles[id] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
function Particle.Create(data)
|
||||
local particleData = Particle.New(data)
|
||||
if not particleData then return end
|
||||
TriggerClientEvent("community_bridge:Client:Particle", -1, particleData)
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
function Particle.Remove(id)
|
||||
if not Particle.Destroy(id) then return end
|
||||
TriggerClientEvent("community_bridge:Client:ParticleRemove", -1, id)
|
||||
end
|
||||
|
||||
function Particle.CreateBulk(datas)
|
||||
if not datas then return end
|
||||
local toClient = {}
|
||||
for k, v in pairs(datas) do
|
||||
local data = Particle.New(v)
|
||||
table.insert(toClient, data)
|
||||
end
|
||||
TriggerClientEvent("community_bridge:Client:ParticleBulk", -1, toClient)
|
||||
return toClient
|
||||
end
|
||||
|
||||
function Particle.RemoveBulk(ids)
|
||||
if not ids then return end
|
||||
local toClient = {}
|
||||
for k, v in pairs(ids) do
|
||||
local id = v
|
||||
if type(v) == "table" then
|
||||
id = v.id
|
||||
end
|
||||
Particle.Destroy(id)
|
||||
table.insert(toClient, id)
|
||||
end
|
||||
TriggerClientEvent("community_bridge:Client:ParticleRemoveBulk", -1, toClient)
|
||||
return ids
|
||||
end
|
||||
|
||||
return Particle
|
|
@ -0,0 +1,116 @@
|
|||
Placeable = Placeable or {}
|
||||
Utility = Utility or Require("lib/utility/client/utility.lua")
|
||||
|
||||
local activePlacementProp = nil
|
||||
lib.locale()
|
||||
|
||||
-- Object placer --
|
||||
local placementText = {
|
||||
locale('placeable_object.place_object_place'),
|
||||
locale('placeable_object.place_object_cancel'),
|
||||
locale('placeable_object.place_object_scroll_up'),
|
||||
locale('placeable_object.place_object_scroll_down')
|
||||
}
|
||||
|
||||
local function finishPlacing()
|
||||
Bridge.Notify.HideHelpText()
|
||||
if activePlacementProp == nil then return end
|
||||
DeleteObject(activePlacementProp)
|
||||
activePlacementProp = nil
|
||||
end
|
||||
|
||||
--[[
|
||||
|
||||
RegisterCommand('testplacement', function()
|
||||
Placeable.PlaceObject("prop_cs_cardbox_01", 10, true, nil, 0.0)
|
||||
end, false)
|
||||
|
||||
--]]
|
||||
|
||||
Placeable.PlaceObject = function(object, distance, snapToGround, allowedMats, offset)
|
||||
distance = tonumber(distance or 10.0 )
|
||||
if activePlacementProp then return end
|
||||
|
||||
if not object then Prints.Error('placeable_object.no_prop_defined') end
|
||||
|
||||
local propObject = type(object) == 'string' and joaat(object) or object
|
||||
local heading = 0.0
|
||||
local checkDist = distance or 10.0
|
||||
|
||||
Utility.LoadModel(propObject)
|
||||
|
||||
activePlacementProp = CreateObject(propObject, 1.0, 1.0, 1.0, false, true, true)
|
||||
SetModelAsNoLongerNeeded(propObject)
|
||||
SetEntityAlpha(activePlacementProp, 150, false)
|
||||
SetEntityCollision(activePlacementProp, false, false)
|
||||
SetEntityInvincible(activePlacementProp, true)
|
||||
FreezeEntityPosition(activePlacementProp, true)
|
||||
|
||||
Bridge.Notify.ShowHelpText(type(placementText) == 'table' and table.concat(placementText))
|
||||
|
||||
local outLine = false
|
||||
|
||||
while activePlacementProp do
|
||||
--local hit, _, coords, _, materialHash = lib.raycast.fromCamera(1, 4)
|
||||
--local hit, _, coords, _, materialHash = lib.raycast.fromCamera(1, 4, nil)
|
||||
local hit, _, coords, _, materialHash = lib.raycast.fromCamera(1, 4)
|
||||
if hit then
|
||||
if offset then
|
||||
coords += offset
|
||||
end
|
||||
|
||||
SetEntityCoords(activePlacementProp, coords.x, coords.y, coords.z, false, false, false, false)
|
||||
local distCheck = #(GetEntityCoords(cache.ped) - coords)
|
||||
SetEntityHeading(activePlacementProp, heading)
|
||||
|
||||
if snapToGround then
|
||||
PlaceObjectOnGroundProperly(activePlacementProp)
|
||||
end
|
||||
|
||||
if outLine then
|
||||
outLine = false
|
||||
SetEntityDrawOutline(activePlacementProp, false)
|
||||
end
|
||||
|
||||
if (allowedMats and not allowedMats[materialHash]) or distCheck >= checkDist then
|
||||
if not outLine then
|
||||
outLine = true
|
||||
SetEntityDrawOutline(activePlacementProp, true)
|
||||
end
|
||||
end
|
||||
|
||||
if IsControlJustReleased(0, 38) then
|
||||
if not outLine and (not allowedMats or allowedMats[materialHash]) and distCheck < checkDist then
|
||||
finishPlacing()
|
||||
return coords, heading
|
||||
end
|
||||
end
|
||||
|
||||
if IsControlJustReleased(0, 73) then
|
||||
finishPlacing()
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
if IsControlJustReleased(0, 14) then
|
||||
heading = heading + 5
|
||||
if heading > 360 then heading = 0.0 end
|
||||
end
|
||||
|
||||
if IsControlJustReleased(0, 15) then
|
||||
heading = heading - 5
|
||||
if heading < 0 then
|
||||
heading = 360.0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Placeable.StopPlacing = function()
|
||||
if not activePlacementProp then return end
|
||||
finishPlacing()
|
||||
end
|
||||
|
||||
return Placeable
|
||||
-- This is derrived and slightly altered from its creator and licensed under GPL-3.0 license Author:Zoo, the original is located here https://github.com/Renewed-Scripts/Renewed-Lib/tree/main
|
|
@ -0,0 +1,787 @@
|
|||
Scaleform = Scaleform or Require("lib/scaleform/client/scaleform.lua")
|
||||
Utility = Utility or Require("lib/utility/client/utility.lua")
|
||||
Raycast = Raycast or Require("lib/raycast/client/raycast.lua")
|
||||
Language = Language or Require("modules/locales/shared.lua")
|
||||
|
||||
PlaceableObject = PlaceableObject or {}
|
||||
|
||||
-- Register key mappings for placement controls
|
||||
RegisterKeyMapping('+place_object', locale('placeable_object.object_place'), 'mouse_button', 'MOUSE_LEFT')
|
||||
RegisterKeyMapping('+cancel_placement', locale('placeable_object.object_cancel'), 'mouse_button', 'MOUSE_RIGHT')
|
||||
RegisterKeyMapping('+rotate_left', locale('placeable_object.rotate_left'), 'keyboard', 'LEFT')
|
||||
RegisterKeyMapping('+rotate_right', locale('placeable_object.rotate_right'), 'keyboard', 'RIGHT')
|
||||
RegisterKeyMapping('+scroll_up', locale('placeable_object.object_scroll_up'), 'mouse_wheel', 'IOM_WHEEL_UP')
|
||||
RegisterKeyMapping('+scroll_down', locale('placeable_object.object_scroll_down'), 'mouse_wheel', 'IOM_WHEEL_DOWN')
|
||||
RegisterKeyMapping('+depth_modifier', locale('placeable_object.depth_modifier'), 'keyboard', 'LCONTROL')
|
||||
|
||||
local state = {
|
||||
isPlacing = false,
|
||||
currentEntity = nil,
|
||||
mode = 'normal', -- 'normal' or 'movement'
|
||||
promise = nil,
|
||||
scaleform = nil,
|
||||
|
||||
-- Placement settings
|
||||
depth = 2.0,
|
||||
heading = 0.0,
|
||||
height = 0.0,
|
||||
snapToGround = true,
|
||||
paused = false,
|
||||
|
||||
-- Current settings
|
||||
settings = {},
|
||||
boundaryCheck = nil,
|
||||
|
||||
-- Key press states
|
||||
keys = {
|
||||
placeObject = false,
|
||||
cancelPlacement = false,
|
||||
rotateLeft = false,
|
||||
rotateRight = false,
|
||||
scrollUp = false,
|
||||
scrollDown = false,
|
||||
depthModifier = false
|
||||
}
|
||||
}
|
||||
|
||||
-- Command handlers for key mappings
|
||||
RegisterCommand('+place_object', function()
|
||||
if state.isPlacing then
|
||||
state.keys.placeObject = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-place_object', function()
|
||||
state.keys.placeObject = false
|
||||
end, false)
|
||||
|
||||
RegisterCommand('+cancel_placement', function()
|
||||
if state.isPlacing then
|
||||
state.keys.cancelPlacement = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-cancel_placement', function()
|
||||
state.keys.cancelPlacement = false
|
||||
end, false)
|
||||
|
||||
RegisterCommand('+rotate_left', function()
|
||||
if state.isPlacing then
|
||||
state.keys.rotateLeft = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-rotate_left', function()
|
||||
state.keys.rotateLeft = false
|
||||
end, false)
|
||||
|
||||
RegisterCommand('+rotate_right', function()
|
||||
if state.isPlacing then
|
||||
state.keys.rotateRight = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-rotate_right', function()
|
||||
state.keys.rotateRight = false
|
||||
end, false)
|
||||
|
||||
RegisterCommand('+scroll_up', function()
|
||||
if state.isPlacing then
|
||||
state.keys.scrollUp = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-scroll_up', function()
|
||||
state.keys.scrollUp = false
|
||||
end, false)
|
||||
|
||||
RegisterCommand('+scroll_down', function()
|
||||
if state.isPlacing then
|
||||
state.keys.scrollDown = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-scroll_down', function()
|
||||
state.keys.scrollDown = false
|
||||
end, false)
|
||||
|
||||
RegisterCommand('+depth_modifier', function()
|
||||
if state.isPlacing then
|
||||
state.keys.depthModifier = true
|
||||
end
|
||||
end, false)
|
||||
|
||||
RegisterCommand('-depth_modifier', function()
|
||||
state.keys.depthModifier = false
|
||||
end, false)
|
||||
|
||||
-- Utility functions
|
||||
local function getMouseWorldPos(depth)
|
||||
local screenX = GetDisabledControlNormal(0, 239)
|
||||
local screenY = GetDisabledControlNormal(0, 240)
|
||||
|
||||
local world, normal = GetWorldCoordFromScreenCoord(screenX, screenY)
|
||||
local playerPos = GetEntityCoords(PlayerPedId())
|
||||
return playerPos + normal * depth
|
||||
end
|
||||
|
||||
-- local function isInBoundary(pos, boundary)
|
||||
-- if not boundary then return true end
|
||||
|
||||
-- local x, y, z = table.unpack(pos)
|
||||
|
||||
-- -- Handle legacy min/max boundary format for backwards compatibility
|
||||
-- if boundary.min and boundary.max then
|
||||
-- local minX, minY, minZ = table.unpack(boundary.min)
|
||||
-- local maxX, maxY, maxZ = table.unpack(boundary.max)
|
||||
-- return x >= minX and x <= maxX and y >= minY and y <= maxY and z >= minZ and z <= maxZ
|
||||
-- end
|
||||
|
||||
-- -- Handle list of points (polygon boundary)
|
||||
-- if boundary.points and #boundary.points > 0 then
|
||||
-- local points = boundary.points
|
||||
-- local minZ = boundary.minZ or -math.huge
|
||||
-- local maxZ = boundary.maxZ or math.huge
|
||||
|
||||
-- -- Check Z bounds first
|
||||
-- if z < minZ or z > maxZ then
|
||||
-- return false
|
||||
-- end
|
||||
|
||||
-- -- Point-in-polygon test using ray casting algorithm (improved version)
|
||||
-- local inside = false
|
||||
-- local n = #points
|
||||
|
||||
-- for i = 1, n do
|
||||
-- local j = i == n and 1 or i + 1 -- Next point (wrap around)
|
||||
|
||||
-- local xi, yi = points[i].x or points[i][1], points[i].y or points[i][2]
|
||||
-- local xj, yj = points[j].x or points[j][1], points[j].y or points[j][2]
|
||||
|
||||
-- -- Ensure xi, yi, xj, yj are numbers
|
||||
-- if not (xi and yi and xj and yj) then
|
||||
-- goto continue
|
||||
-- end
|
||||
|
||||
-- -- Ray casting test
|
||||
-- if ((yi > y) ~= (yj > y)) then
|
||||
-- -- Calculate intersection point
|
||||
-- local intersect = (xj - xi) * (y - yi) / (yj - yi) + xi
|
||||
-- if x < intersect then
|
||||
-- inside = not inside
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- ::continue::
|
||||
-- end
|
||||
|
||||
-- return inside
|
||||
-- end
|
||||
|
||||
-- -- Fallback to true if boundary format is not recognized
|
||||
-- return true
|
||||
-- end
|
||||
|
||||
local function checkMaterialAndBoundary()
|
||||
if not state.currentEntity then return true end
|
||||
|
||||
local pos = GetEntityCoords(state.currentEntity)
|
||||
local inBounds = Bridge.Math.InBoundary(pos, state.settings.boundary)
|
||||
|
||||
-- Check built-in boundary first
|
||||
if state.settings.boundary and not inBounds then return false end
|
||||
|
||||
-- Check custom boundary function if provided
|
||||
if state.settings.customCheck then
|
||||
local customResult = state.settings.customCheck(pos, state.currentEntity, state.settings)
|
||||
if not customResult then return false end
|
||||
end
|
||||
|
||||
-- Check allowed materials
|
||||
if state.settings.allowedMats then
|
||||
local hit, _, _, _, materialHash = GetShapeTestResult(StartShapeTestRay(pos.x, pos.y, pos.z + 1.0, pos.x, pos.y, pos.z - 5.0, -1, 0, 7))
|
||||
if hit == 1 then
|
||||
for _, allowedMat in ipairs(state.settings.allowedMats) do
|
||||
if materialHash == GetHashKey(allowedMat) then
|
||||
return inBounds
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return inBounds
|
||||
end
|
||||
|
||||
local function checkMaterialAndBoundaryDetailed()
|
||||
if not state.currentEntity then return true, true, true end
|
||||
|
||||
local pos = GetEntityCoords(state.currentEntity)
|
||||
local inBounds = Bridge.Math.InBoundary(pos, state.settings.boundary)
|
||||
local customCheckPassed = true
|
||||
local materialCheckPassed = true
|
||||
|
||||
-- Check built-in boundary first
|
||||
if state.settings.boundary and not inBounds then
|
||||
return false, false, customCheckPassed
|
||||
end
|
||||
|
||||
-- Check custom boundary function if provided
|
||||
if state.settings.customCheck then
|
||||
customCheckPassed = state.settings.customCheck(pos, state.currentEntity, state.settings)
|
||||
if not customCheckPassed then
|
||||
return false, inBounds, false
|
||||
end
|
||||
end
|
||||
|
||||
-- Check allowed materials
|
||||
if state.settings.allowedMats then
|
||||
local hit, _, _, _, materialHash = GetShapeTestResult(StartShapeTestRay(pos.x, pos.y, pos.z + 1.0, pos.x, pos.y, pos.z - 5.0, -1, 0, 7))
|
||||
if hit == 1 then
|
||||
for _, allowedMat in ipairs(state.settings.allowedMats) do
|
||||
if materialHash == GetHashKey(allowedMat) then
|
||||
return inBounds, inBounds, customCheckPassed
|
||||
end
|
||||
end
|
||||
materialCheckPassed = false
|
||||
return false, inBounds, customCheckPassed
|
||||
end
|
||||
end
|
||||
|
||||
return inBounds, inBounds, customCheckPassed
|
||||
end
|
||||
|
||||
-- local function setupInstructionalButtons()
|
||||
-- local buttons = {}
|
||||
|
||||
-- -- Common buttons
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = state.settings.config?.place_object?.name or 'Place Object:', keyIndex = state.settings.config?.place_object?.key or {223}, int = 5})
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = state.settings.config?.cancel_placement?.name or 'Cancel:', keyIndex = state.settings.config?.cancel_placement?.key or {25}, int = 4})
|
||||
|
||||
-- if state.mode == 'normal' then
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {241, 242}, int = 3})
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Depth:', keyIndex = {224}, int = 2})
|
||||
-- if state.settings.allowVertical then
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Height:', keyIndex = {16, 17}, int = 1})
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Toggle Ground Snap:', keyIndex = {19}, int = 0})
|
||||
-- end
|
||||
-- if state.settings.allowMovement then
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Movement Mode:', keyIndex = {38}, int = 6})
|
||||
-- end
|
||||
-- elseif state.mode == 'movement' then
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Move:', keyIndex = {32, 33, 34, 35}, int = 3})
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {174, 175}, int = 2})
|
||||
-- if state.settings.allowVertical then
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Up/Down:', keyIndex = {85, 48}, int = 1})
|
||||
-- end
|
||||
-- if state.settings.allowNormal then
|
||||
-- table.insert(buttons, {type = "SET_DATA_SLOT", name = 'Normal Mode:', keyIndex = {38}, int = 0})
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- table.insert(buttons, {type = "DRAW_INSTRUCTIONAL_BUTTONS"})
|
||||
-- table.insert(buttons, {type = "SET_BACKGROUND_COLOUR"})
|
||||
|
||||
-- -- return Scaleform.SetupInstructionalButtons(buttons)
|
||||
-- return nil -- Scaleform disabled for now
|
||||
-- end
|
||||
|
||||
local function drawBoundaryBox(boundary)
|
||||
if not boundary then return end
|
||||
|
||||
-- Handle legacy min/max boundary format for backwards compatibility
|
||||
if boundary.min and boundary.max then
|
||||
local min = boundary.min
|
||||
local max = boundary.max
|
||||
|
||||
-- Define the 8 corners of the box
|
||||
local corners = {
|
||||
vector3(min.x, min.y, min.z), -- 1
|
||||
vector3(max.x, min.y, min.z), -- 2
|
||||
vector3(max.x, max.y, min.z), -- 3
|
||||
vector3(min.x, max.y, min.z), -- 4
|
||||
vector3(min.x, min.y, max.z), -- 5
|
||||
vector3(max.x, min.y, max.z), -- 6
|
||||
vector3(max.x, max.y, max.z), -- 7
|
||||
vector3(min.x, max.y, max.z), -- 8
|
||||
}
|
||||
|
||||
-- Draw wireframe box
|
||||
local r, g, b, a = 0, 255, 0, 100
|
||||
|
||||
-- Bottom face
|
||||
DrawLine(corners[1].x, corners[1].y, corners[1].z, corners[2].x, corners[2].y, corners[2].z, r, g, b, a)
|
||||
DrawLine(corners[2].x, corners[2].y, corners[2].z, corners[3].x, corners[3].y, corners[3].z, r, g, b, a)
|
||||
DrawLine(corners[3].x, corners[3].y, corners[3].z, corners[4].x, corners[4].y, corners[4].z, r, g, b, a)
|
||||
DrawLine(corners[4].x, corners[4].y, corners[4].z, corners[1].x, corners[1].y, corners[1].z, r, g, b, a)
|
||||
|
||||
-- Top face
|
||||
DrawLine(corners[5].x, corners[5].y, corners[5].z, corners[6].x, corners[6].y, corners[6].z, r, g, b, a)
|
||||
DrawLine(corners[6].x, corners[6].y, corners[6].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, a)
|
||||
DrawLine(corners[7].x, corners[7].y, corners[7].z, corners[8].x, corners[8].y, corners[8].z, r, g, b, a)
|
||||
DrawLine(corners[8].x, corners[8].y, corners[8].z, corners[5].x, corners[5].y, corners[5].z, r, g, b, a)
|
||||
|
||||
-- Vertical edges
|
||||
DrawLine(corners[1].x, corners[1].y, corners[1].z, corners[5].x, corners[5].y, corners[5].z, r, g, b, a)
|
||||
DrawLine(corners[2].x, corners[2].y, corners[2].z, corners[6].x, corners[6].y, corners[6].z, r, g, b, a)
|
||||
DrawLine(corners[3].x, corners[3].y, corners[3].z, corners[7].x, corners[7].y, corners[7].z, r, g, b, a)
|
||||
DrawLine(corners[4].x, corners[4].y, corners[4].z, corners[8].x, corners[8].y, corners[8].z, r, g, b, a)
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle list of points (polygon boundary)
|
||||
if boundary.points and #boundary.points > 0 then
|
||||
local points = boundary.points
|
||||
local minZ = boundary.minZ or 0
|
||||
local maxZ = boundary.maxZ or 50
|
||||
local r, g, b, a = 0, 255, 0, 100
|
||||
|
||||
-- Draw bottom polygon outline
|
||||
for i = 1, #points do
|
||||
local currentPoint = points[i]
|
||||
local nextPoint = points[i % #points + 1] -- Wrap around to first point
|
||||
|
||||
local x1, y1 = currentPoint.x or currentPoint[1], currentPoint.y or currentPoint[2]
|
||||
local x2, y2 = nextPoint.x or nextPoint[1], nextPoint.y or nextPoint[2]
|
||||
|
||||
-- Bottom edge
|
||||
DrawLine(x1, y1, minZ, x2, y2, minZ, r, g, b, a)
|
||||
|
||||
-- Top edge
|
||||
DrawLine(x1, y1, maxZ, x2, y2, maxZ, r, g, b, a)
|
||||
|
||||
-- Vertical edge
|
||||
DrawLine(x1, y1, minZ, x1, y1, maxZ, r, g, b, a)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local function drawEntityBoundingBox(entity, inBounds)
|
||||
if not entity or not DoesEntityExist(entity) then return end
|
||||
|
||||
-- Enable entity outline
|
||||
SetEntityDrawOutlineShader(1)
|
||||
SetEntityDrawOutline(entity, true)
|
||||
-- Set color based on boundary status
|
||||
if inBounds then
|
||||
-- Green outline for valid placement
|
||||
SetEntityDrawOutlineColor(0, 255, 0, 255)
|
||||
else
|
||||
-- Red outline for invalid placement
|
||||
SetEntityDrawOutlineColor(255, 0, 0, 255)
|
||||
end
|
||||
end
|
||||
|
||||
local function handleNormalMode()
|
||||
if not state.isPlacing or state.mode ~= 'normal' or state.paused then
|
||||
return
|
||||
end
|
||||
|
||||
-- Disable conflicting controls
|
||||
DisableControlAction(0, 24, true) -- Attack
|
||||
DisableControlAction(0, 25, true) -- Aim
|
||||
DisableControlAction(0, 36, true) -- Duck
|
||||
|
||||
local moveSpeed = state.keys.depthModifier and (state.settings.depthStep or 0.1) or (state.settings.rotationStep or 0.5)
|
||||
|
||||
-- Scroll wheel controls using key mappings
|
||||
if state.keys.depthModifier then -- Depth modifier held - depth control
|
||||
if state.keys.scrollUp then
|
||||
state.keys.scrollUp = false -- Reset the key state
|
||||
state.depth = math.min(state.settings.maxDepth, state.depth + moveSpeed) -- Use maxDepth setting
|
||||
elseif state.keys.scrollDown then
|
||||
state.keys.scrollDown = false -- Reset the key state
|
||||
state.depth = math.max(1.0, state.depth - moveSpeed) -- Fixed: scroll down decreases distance
|
||||
end
|
||||
else -- Regular scroll - rotation
|
||||
if state.keys.scrollUp then
|
||||
state.keys.scrollUp = false -- Reset the key state
|
||||
state.heading = state.heading - 5.0 -- Fixed: scroll up = counterclockwise
|
||||
elseif state.keys.scrollDown then
|
||||
state.keys.scrollDown = false -- Reset the key state
|
||||
state.heading = state.heading + 5.0 -- Fixed: scroll down = clockwise
|
||||
end
|
||||
end
|
||||
|
||||
-- -- Arrow key rotation using key mappings
|
||||
-- if state.keys.rotateLeft then
|
||||
-- state.heading = state.heading + 2.0
|
||||
-- elseif state.keys.rotateRight then
|
||||
-- state.heading = state.heading - 2.0
|
||||
-- end
|
||||
|
||||
-- Height controls (only if vertical movement allowed and not snapped to ground)
|
||||
if state.settings.allowVertical and not state.snapToGround then
|
||||
if IsControlPressed(0, 16) then -- Q
|
||||
state.height = state.height + (state.settings.heightStep or 0.5)
|
||||
elseif IsControlPressed(0, 17) then -- E
|
||||
state.height = state.height - (state.settings.heightStep or 0.5)
|
||||
end
|
||||
end
|
||||
-- Toggle ground snap
|
||||
if state.settings.allowVertical and IsControlJustPressed(0, 19) then -- Alt
|
||||
state.snapToGround = not state.snapToGround
|
||||
if state.snapToGround then
|
||||
state.height = 0.0
|
||||
end
|
||||
end
|
||||
|
||||
-- Switch to movement mode
|
||||
if state.settings.allowMovement and IsControlJustPressed(0, 38) then -- E
|
||||
state.mode = 'movement'
|
||||
SetEntityCollision(state.currentEntity, false, false)
|
||||
end
|
||||
-- Update entity position
|
||||
local pos = getMouseWorldPos(state.depth)
|
||||
|
||||
if not state.snapToGround and state.settings.allowVertical then
|
||||
pos = pos + vector3(0, 0, state.height)
|
||||
end
|
||||
|
||||
if state.currentEntity then
|
||||
SetEntityCoords(state.currentEntity, pos.x, pos.y, pos.z, false, false, false, false)
|
||||
SetEntityHeading(state.currentEntity, state.heading)
|
||||
if state.snapToGround then
|
||||
local slerp = PlaceObjectOnGroundProperly(state.currentEntity)
|
||||
if not slerp then
|
||||
-- If the object can't be placed on the ground, adjust its Z position
|
||||
local groundZ, _z = GetGroundZFor_3dCoord(pos.x, pos.y, pos.z + 50, false)
|
||||
if groundZ then
|
||||
SetEntityCoords(state.currentEntity, pos.x, pos.y, _z, false, false, false, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Visual feedback
|
||||
if not state.settings.disableSphere then
|
||||
DrawSphere(pos.x, pos.y, pos.z, 0.5, 255, 0, 0, 50)
|
||||
end
|
||||
end
|
||||
|
||||
local function handleMovementMode()
|
||||
if not state.isPlacing or state.mode ~= 'movement' or not DoesEntityExist(state.currentEntity) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Disable player movement
|
||||
DisableControlAction(0, 30, true) -- Move Left/Right
|
||||
DisableControlAction(0, 31, true) -- Move Forward/Back
|
||||
DisableControlAction(0, 36, true) -- Duck
|
||||
DisableControlAction(0, 21, true) -- Sprint
|
||||
DisableControlAction(0, 22, true) -- Jump
|
||||
|
||||
local coords = GetEntityCoords(state.currentEntity)
|
||||
local heading = GetEntityHeading(state.currentEntity)
|
||||
local moveSpeed = IsControlPressed(0, 21) and (state.settings.movementStepFast or 0.5) or (state.settings.movementStep or 0.1) -- Faster with shift
|
||||
local moved = false
|
||||
|
||||
-- Get camera direction for relative movement
|
||||
local camRot = GetGameplayCamRot(2)
|
||||
local camHeading = math.rad(camRot.z)
|
||||
local camForward = vector3(-math.sin(camHeading), math.cos(camHeading), 0)
|
||||
local camRight = vector3(math.cos(camHeading), math.sin(camHeading), 0)
|
||||
|
||||
-- WASD movement
|
||||
if IsControlPressed(0, 32) then -- W
|
||||
coords = coords + camForward * moveSpeed
|
||||
moved = true
|
||||
elseif IsControlPressed(0, 33) then -- S
|
||||
coords = coords - camForward * moveSpeed
|
||||
moved = true
|
||||
end
|
||||
|
||||
if IsControlPressed(0, 34) then -- A
|
||||
coords = coords - camRight * moveSpeed
|
||||
moved = true
|
||||
elseif IsControlPressed(0, 35) then -- D
|
||||
coords = coords + camRight * moveSpeed
|
||||
moved = true
|
||||
end
|
||||
|
||||
-- Vertical movement
|
||||
if state.settings.allowVertical then
|
||||
if IsControlPressed(0, 85) then -- Q
|
||||
coords = coords + vector3(0, 0, moveSpeed)
|
||||
moved = true
|
||||
elseif IsControlPressed(0, 48) then -- Z
|
||||
coords = coords + vector3(0, 0, -moveSpeed)
|
||||
moved = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Rotation
|
||||
if IsControlPressed(0, 174) then -- Left arrow
|
||||
heading = heading + 2.0
|
||||
moved = true
|
||||
elseif IsControlPressed(0, 175) then -- Right arrow
|
||||
heading = heading - 2.0
|
||||
moved = true
|
||||
end
|
||||
|
||||
-- Apply changes
|
||||
if moved then
|
||||
SetEntityCoords(state.currentEntity, coords.x, coords.y, coords.z, false, false, false, true)
|
||||
SetEntityHeading(state.currentEntity, heading)
|
||||
end
|
||||
|
||||
-- Switch to normal mode
|
||||
if state.settings.allowNormal and IsControlJustPressed(0, 38) then -- E
|
||||
state.mode = 'normal'
|
||||
SetEntityCollision(state.currentEntity, true, true)
|
||||
end
|
||||
|
||||
-- Snap to ground
|
||||
if IsControlJustPressed(0, 19) then -- Alt
|
||||
PlaceObjectOnGroundProperly(state.currentEntity)
|
||||
end
|
||||
end
|
||||
|
||||
local function placementLoop()
|
||||
CreateThread(function()
|
||||
while state.isPlacing do
|
||||
Wait(0)
|
||||
|
||||
-- Handle input based on mode
|
||||
if state.mode == 'normal' then
|
||||
handleNormalMode()
|
||||
elseif state.mode == 'movement' then
|
||||
handleMovementMode()
|
||||
end
|
||||
|
||||
-- Common controls using key mappings
|
||||
if state.keys.placeObject then
|
||||
state.keys.placeObject = false -- Reset the key state
|
||||
local canPlace = checkMaterialAndBoundary()
|
||||
if canPlace then
|
||||
Wait(100)
|
||||
local coords = GetEntityCoords(state.currentEntity)
|
||||
if not state.settings.allowVertical or state.snapToGround then
|
||||
local groundZ, _z = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z + 50, false)
|
||||
if groundZ then
|
||||
coords = vector3(coords.x, coords.y, _z)
|
||||
end
|
||||
end
|
||||
|
||||
local rotation = GetEntityRotation(state.currentEntity)
|
||||
if state.promise then
|
||||
state.promise:resolve({
|
||||
entity = state.currentEntity,
|
||||
coords = coords,
|
||||
rotation = rotation,
|
||||
placed = true
|
||||
})
|
||||
end
|
||||
|
||||
PlaceableObject.Stop()
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Cancel placement using key mapping
|
||||
if state.keys.cancelPlacement then
|
||||
state.keys.cancelPlacement = false -- Reset the key state
|
||||
if state.promise then
|
||||
state.promise:resolve(false)
|
||||
end
|
||||
|
||||
PlaceableObject.Stop()
|
||||
break
|
||||
end
|
||||
|
||||
-- Check if entity is outside boundary and cancel if so
|
||||
if state.settings.boundary and state.currentEntity then
|
||||
local canPlace = checkMaterialAndBoundary()
|
||||
if not canPlace then
|
||||
if state.promise then
|
||||
state.promise:resolve(false)
|
||||
end
|
||||
|
||||
PlaceableObject.Stop()
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw boundary if exists and enabled
|
||||
if state.settings.drawBoundary then
|
||||
drawBoundaryBox(state.settings.boundary)
|
||||
end
|
||||
|
||||
-- Draw entity bounding box if enabled
|
||||
if state.settings.drawInBoundary and state.currentEntity then
|
||||
local overallResult, boundaryResult, customResult = checkMaterialAndBoundaryDetailed()
|
||||
-- Show red if any check fails, green if all pass
|
||||
local inBounds = overallResult
|
||||
drawEntityBoundingBox(state.currentEntity, inBounds)
|
||||
end
|
||||
|
||||
-- Show help text for placement controls
|
||||
local placementText = {
|
||||
string.format(locale('placeable_object.place_object_place'), Bridge.Utility.GetCommandKey('+place_object')),
|
||||
string.format(locale('placeable_object.place_object_cancel'), Bridge.Utility.GetCommandKey('+cancel_placement')),
|
||||
-- string.format(locale('placeable_object.rotate_left'), Bridge.Utility.GetCommandKey('+rotate_left')),
|
||||
-- string.format(locale('placeable_object.rotate_right'), Bridge.Utility.GetCommandKey('+rotate_right')),
|
||||
string.format(locale('placeable_object.place_object_scroll_up'), Bridge.Utility.GetCommandKey('+scroll_up')),
|
||||
string.format(locale('placeable_object.place_object_scroll_down'), Bridge.Utility.GetCommandKey('+scroll_down')),
|
||||
string.format(locale('placeable_object.depth_modifier'), Bridge.Utility.GetCommandKey('+depth_modifier'))
|
||||
}
|
||||
Bridge.Notify.ShowHelpText(type(placementText) == 'table' and table.concat(placementText))
|
||||
|
||||
-- -- Draw entity bounding box
|
||||
-- drawEntityBoundingBox(state.currentEntity, checkMaterialAndBoundary())
|
||||
|
||||
-- -- Update instructional buttons
|
||||
-- if state.scaleform then
|
||||
-- Scaleform.RenderInstructionalButtons(state.scaleform)
|
||||
-- end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Main functions
|
||||
|
||||
|
||||
---@param model - Model name or hash
|
||||
---@param settings - Configuration table:
|
||||
--[[
|
||||
depth (3.0): Starting distance from player,
|
||||
allowVertical (false): Enable height controls,
|
||||
allowMovement (false): Enable WASD mode,
|
||||
disableSphere (false): Hide position indicator,
|
||||
boundary: Area restriction {min = vector3(), max = vector3()},
|
||||
allowedMats: Surface materials {"concrete", "grass"},
|
||||
depthStep (0.1): Step size for depth adjustment,
|
||||
rotationStep (0.5): Step size for rotation,
|
||||
heightStep (0.5): Step size for height adjustment,
|
||||
movementStep (0.1): Step size for normal movement,
|
||||
movementStepFast (0.5): Step size for fast movement (with shift),
|
||||
maxDepth (50.0): Maximum distance from player,
|
||||
--]]
|
||||
---@returns Promise with: {entity, coords, heading, placed, cancelled}
|
||||
--[[
|
||||
Example:
|
||||
local result = Citizen.Await(PlaceableObject.Create("prop_barrier_work05", {
|
||||
depth = 5.0,
|
||||
allowVertical = false
|
||||
}))
|
||||
--]]
|
||||
|
||||
function PlaceableObject.Create(model, settings)
|
||||
if state.isPlacing then
|
||||
PlaceableObject.Stop()
|
||||
end
|
||||
|
||||
-- Default settings
|
||||
settings = settings or {}
|
||||
settings.depth = settings.depth or 3.0 -- Start closer to player
|
||||
settings.allowVertical = settings.allowVertical or false
|
||||
settings.allowMovement = settings.allowMovement or false
|
||||
settings.allowNormal = settings.allowNormal or false
|
||||
settings.disableSphere = settings.disableSphere or false
|
||||
settings.drawBoundary = settings.drawBoundary or false
|
||||
settings.drawInBoundary = settings.drawInBoundary or false
|
||||
|
||||
-- Movement speed settings
|
||||
settings.depthStep = settings.depthStep or 0.1 -- Fine control for depth adjustment
|
||||
settings.rotationStep = settings.rotationStep or 0.5 -- Normal rotation speed
|
||||
settings.heightStep = settings.heightStep or 0.5 -- Height adjustment speed
|
||||
settings.movementStep = settings.movementStep or 0.1 -- Normal movement speed
|
||||
settings.movementStepFast = settings.movementStepFast or 0.5 -- Fast movement speed (with shift)
|
||||
settings.maxDepth = settings.maxDepth or 5.0 -- Maximum distance from player
|
||||
|
||||
state.settings = settings
|
||||
state.depth = settings.depth -- Use the settings depth
|
||||
state.heading = -GetEntityHeading(PlayerPedId())
|
||||
state.height = 0.0
|
||||
state.snapToGround = not settings.allowVertical
|
||||
state.mode = 'normal'
|
||||
|
||||
local p = promise.new()
|
||||
state.promise = p
|
||||
|
||||
local point = Bridge.ClientEntity.Create({
|
||||
id = 'placeable_object',
|
||||
entityType = 'object',
|
||||
model = model,
|
||||
coords = GetEntityCoords(PlayerPedId()),
|
||||
rotation = vector3(0.0, 0.0, state.heading),
|
||||
OnSpawn= function(data)
|
||||
state.currentEntity = data.spawned
|
||||
SetEntityCollision(state.currentEntity, false, false)
|
||||
FreezeEntityPosition(state.currentEntity, false)
|
||||
|
||||
-- Set initial position based on depth
|
||||
local playerPos = GetEntityCoords(PlayerPedId())
|
||||
local forward = GetEntityForwardVector(PlayerPedId())
|
||||
local spawnPos = playerPos + forward * state.depth
|
||||
SetEntityCoords(state.currentEntity, spawnPos.x, spawnPos.y, spawnPos.z + state.height, false, false, false, true)
|
||||
end,
|
||||
})
|
||||
-- Setup instructional buttons
|
||||
-- state.scaleform = setupInstructionalButtons()
|
||||
state.scaleform = nil
|
||||
|
||||
state.isPlacing = true
|
||||
|
||||
-- Show help text for placement controls
|
||||
local placementText = {
|
||||
string.format(locale('placeable_object.place_object_place'), Bridge.Utility.GetCommandKey('+place_object')),
|
||||
string.format(locale('placeable_object.place_object_cancel'), Bridge.Utility.GetCommandKey('+cancel_placement')),
|
||||
-- string.format(locale('placeable_object.rotate_left'), Bridge.Utility.GetCommandKey('+rotate_left')),
|
||||
-- string.format(locale('placeable_object.rotate_right'), Bridge.Utility.GetCommandKey('+rotate_right')),
|
||||
string.format(locale('placeable_object.place_object_scroll_up'), Bridge.Utility.GetCommandKey('+scroll_up')),
|
||||
string.format(locale('placeable_object.place_object_scroll_down'), Bridge.Utility.GetCommandKey('+scroll_down')),
|
||||
string.format(locale('placeable_object.depth_modifier'), Bridge.Utility.GetCommandKey('+depth_modifier'))
|
||||
}
|
||||
Bridge.Notify.ShowHelpText(type(placementText) == 'table' and table.concat(placementText))
|
||||
|
||||
placementLoop()
|
||||
|
||||
|
||||
return Citizen.Await(p)
|
||||
end
|
||||
|
||||
function PlaceableObject.Stop()
|
||||
Bridge.Notify.HideHelpText()
|
||||
if state.currentEntity and DoesEntityExist(state.currentEntity) then
|
||||
-- Disable entity outline before deleting
|
||||
SetEntityDrawOutline(state.currentEntity, false)
|
||||
DeleteObject(state.currentEntity)
|
||||
end
|
||||
|
||||
if state.scaleform then
|
||||
Scaleform.Unload(state.scaleform)
|
||||
end
|
||||
ClientEntity.Unregister('placeable_object')
|
||||
-- Reset state
|
||||
state.isPlacing = false
|
||||
state.currentEntity = nil
|
||||
state.mode = 'normal'
|
||||
state.promise = nil
|
||||
state.scaleform = nil
|
||||
state.settings = {}
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Status functions
|
||||
function PlaceableObject.IsPlacing()
|
||||
return state.isPlacing
|
||||
end
|
||||
|
||||
function PlaceableObject.GetCurrentEntity()
|
||||
return state.currentEntity
|
||||
end
|
||||
|
||||
function PlaceableObject.GetCurrentMode()
|
||||
return state.mode
|
||||
end
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
if state.isPlacing then
|
||||
PlaceableObject.Stop()
|
||||
end
|
||||
end)
|
||||
|
||||
return PlaceableObject
|
|
@ -0,0 +1,255 @@
|
|||
-- Grid-based point system
|
||||
Point = {}
|
||||
local ActivePoints = {}
|
||||
local GridCells = {}
|
||||
local LoopStarted = false
|
||||
local insidePoints = {}
|
||||
local data = {} -- 👈 Move this outside the function
|
||||
|
||||
-- Grid configuration
|
||||
local GRID_SIZE = 500.0 -- Size of each grid cell
|
||||
local CELL_BUFFER = 1 -- Number of adjacent cells to check
|
||||
|
||||
-- Consider adding these optimizations
|
||||
local ADAPTIVE_WAIT = true -- Adjust wait time based on player speed
|
||||
|
||||
---This is an internal function, do not call this externally
|
||||
function Point.GetCellKey(coords)
|
||||
local cellX = math.floor(coords.x / GRID_SIZE)
|
||||
local cellY = math.floor(coords.y / GRID_SIZE)
|
||||
return cellX .. ":" .. cellY
|
||||
end
|
||||
|
||||
---This is an internal function, do not call this externally
|
||||
function Point.RegisterInGrid(point)
|
||||
local cellKey = Point.GetCellKey(point.coords)
|
||||
|
||||
-- Initialize cell if it doesn't exist
|
||||
GridCells[cellKey] = GridCells[cellKey] or {
|
||||
points = {},
|
||||
count = 0
|
||||
}
|
||||
|
||||
-- Add point to cell
|
||||
GridCells[cellKey].points[point.id] = point
|
||||
GridCells[cellKey].count = GridCells[cellKey].count + 1
|
||||
|
||||
-- Store cell reference in point
|
||||
point.cellKey = cellKey
|
||||
end
|
||||
|
||||
---This is an internal function, do not call this externally
|
||||
function Point.UpdateInGrid(point, oldCellKey)
|
||||
-- Remove from old cell if cell key changed
|
||||
if oldCellKey and oldCellKey ~= point.cellKey then
|
||||
if GridCells[oldCellKey] and GridCells[oldCellKey].points[point.id] then
|
||||
GridCells[oldCellKey].points[point.id] = nil
|
||||
GridCells[oldCellKey].count = GridCells[oldCellKey].count - 1
|
||||
|
||||
-- Clean up empty cells
|
||||
if GridCells[oldCellKey].count <= 0 then
|
||||
GridCells[oldCellKey] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Add to new cell
|
||||
Point.RegisterInGrid(point)
|
||||
end
|
||||
end
|
||||
|
||||
---Gets nearby cells based on the coords
|
||||
---@param coords table
|
||||
---@return table
|
||||
function Point.GetNearbyCells(coords)
|
||||
local cellX = math.floor(coords.x / GRID_SIZE)
|
||||
local cellY = math.floor(coords.y / GRID_SIZE)
|
||||
local nearbyCells = {}
|
||||
|
||||
-- Get current and adjacent cells
|
||||
for x = cellX - CELL_BUFFER, cellX + CELL_BUFFER do
|
||||
for y = cellY - CELL_BUFFER, cellY + CELL_BUFFER do
|
||||
local key = x .. ":" .. y
|
||||
if GridCells[key] then
|
||||
table.insert(nearbyCells, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nearbyCells
|
||||
end
|
||||
|
||||
---Returns all points in the same cell as the given point
|
||||
---This will require you to pass the point object
|
||||
---@param point table
|
||||
---@return table
|
||||
function Point.CheckPointsInSameCell(point)
|
||||
local cellKey = point.cellKey
|
||||
if not GridCells[cellKey] then return {} end
|
||||
|
||||
local nearbyPoints = {}
|
||||
for id, otherPoint in pairs(GridCells[cellKey].points) do
|
||||
if id ~= point.id then
|
||||
local distance = #(point.coords - otherPoint.coords)
|
||||
if distance < (point.distance + otherPoint.distance) then
|
||||
nearbyPoints[id] = otherPoint
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nearbyPoints
|
||||
end
|
||||
|
||||
---Internal function that starts the loop. Do not call this function directly.
|
||||
function Point.StartLoop()
|
||||
if LoopStarted then return false end
|
||||
LoopStarted = true
|
||||
-- Remove the "local data = {}" line from here
|
||||
CreateThread(function()
|
||||
while LoopStarted do
|
||||
local playerPed = PlayerPedId()
|
||||
while playerPed == -1 do
|
||||
Wait(100)
|
||||
playerPed = PlayerPedId()
|
||||
end
|
||||
local playerCoords = GetEntityCoords(playerPed)
|
||||
local targetsExist = false
|
||||
local playerCellKey = Point.GetCellKey(playerCoords)
|
||||
local nearbyCells = Point.GetNearbyCells(playerCoords)
|
||||
|
||||
local playerSpeed = GetEntitySpeed(playerPed)
|
||||
local maxWeight = 1000
|
||||
local waitTime = ADAPTIVE_WAIT and math.max(maxWeight/10, maxWeight - playerSpeed * maxWeight/10) or maxWeight
|
||||
-- Process only points in nearby cells
|
||||
for _, cellKey in ipairs(nearbyCells) do
|
||||
if GridCells[cellKey] then
|
||||
for id, point in pairs(GridCells[cellKey].points) do
|
||||
targetsExist = true
|
||||
|
||||
-- Update entity position if needed
|
||||
local oldCellKey = point.cellKey
|
||||
if point.isEntity then
|
||||
point.coords = GetEntityCoords(point.target)
|
||||
point.cellKey = Point.GetCellKey(point.coords)
|
||||
Point.UpdateInGrid(point, oldCellKey)
|
||||
end
|
||||
local coords = point.coords and vector3(point.coords.x, point.coords.y, point.coords.z) or vector3(0, 0, 0)
|
||||
local distance = #(playerCoords - coords)
|
||||
--local distance = #(playerCoords - point.coords)
|
||||
|
||||
-- Check if player entered/exited the point
|
||||
if distance < point.distance then
|
||||
if not point.inside then
|
||||
point.inside = true
|
||||
data[point.id] = data[point.id] or point.args or {}
|
||||
data[point.id] = point.onEnter(point, data[point.id])
|
||||
insidePoints[point.id] = point
|
||||
end
|
||||
-- Modified main loop exit handler
|
||||
elseif point.inside then
|
||||
point.inside = false
|
||||
data[point.id] = data[point.id] or point.args or {}
|
||||
local result = point.onExit(point, data[point.id])
|
||||
data[point.id] = result ~= nil and result or data[point.id] -- ← Use consistent fallback
|
||||
point.args = data[point.id] -- ← Update point.args to match data[point.id]
|
||||
insidePoints[point.id] = nil
|
||||
end
|
||||
|
||||
if point.onNearby then
|
||||
point.onNearby(GridCells[cellKey]?.points, waitTime)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for id, insidepoint in pairs(insidePoints) do
|
||||
local pos = insidepoint.coords and vector3(insidepoint.coords.x, insidepoint.coords.y, insidepoint.coords.z) or vector3(0, 0, 0)
|
||||
local dist = #(playerCoords - pos)
|
||||
if dist > insidepoint.distance then
|
||||
insidepoint.inside = false
|
||||
data[insidepoint.id] = data[insidepoint.id] or insidepoint.args or {}
|
||||
local result = insidepoint.onExit(insidepoint, data[insidepoint.id])
|
||||
data[insidepoint.id] = result ~= nil and result or data[insidepoint.id]
|
||||
insidepoint.args = data[insidepoint.id] -- ← Keep data in sync
|
||||
insidePoints[insidepoint.id] = nil
|
||||
end
|
||||
end
|
||||
Wait(waitTime) -- Faster updates when moving quickly
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
---Create a point based on a vector or entityID
|
||||
---@param id string
|
||||
---@param target number || vector3
|
||||
---@param distance number
|
||||
---@param _onEnter function
|
||||
---@param _onExit function
|
||||
---@param _onNearby function
|
||||
---@param data self
|
||||
---@return table
|
||||
function Point.Register(id, target, distance, args, _onEnter, _onExit, _onNearby)
|
||||
local isEntity = type(target) == "number"
|
||||
local coords = isEntity and GetEntityCoords(target) or target
|
||||
|
||||
local self = {}
|
||||
self.id = id
|
||||
self.target = target -- Store entity ID or Vector3
|
||||
self.isEntity = isEntity
|
||||
self.coords = coords
|
||||
self.distance = distance
|
||||
self.onEnter = _onEnter or function() end
|
||||
self.onExit = _onExit or function() end
|
||||
self.onNearby = _onNearby or function() end
|
||||
self.inside = false -- Track if player is inside
|
||||
self.args = args or {}
|
||||
|
||||
ActivePoints[id] = self
|
||||
Point.RegisterInGrid(self)
|
||||
Point.StartLoop()
|
||||
return self
|
||||
end
|
||||
|
||||
---Remove an exsisting point by its id
|
||||
---@param id string
|
||||
function Point.Remove(id)
|
||||
local point = ActivePoints[id]
|
||||
if point then
|
||||
local cellKey = point.cellKey
|
||||
if GridCells[cellKey] and GridCells[cellKey].points[id] then
|
||||
GridCells[cellKey].points[id] = nil
|
||||
GridCells[cellKey].count = GridCells[cellKey].count - 1
|
||||
|
||||
if GridCells[cellKey].count <= 0 then
|
||||
GridCells[cellKey] = nil
|
||||
end
|
||||
end
|
||||
|
||||
ActivePoints[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---Returnes a point by its id
|
||||
---@param id string
|
||||
---@return table
|
||||
function Point.Get(id)
|
||||
return ActivePoints[id]
|
||||
end
|
||||
|
||||
function Point.UpdateCoords(id, coords)
|
||||
local point = ActivePoints[id]
|
||||
if point then
|
||||
point.coords = coords
|
||||
local oldCellKey = point.cellKey
|
||||
point.cellKey = Point.GetCellKey(coords)
|
||||
Point.UpdateInGrid(point, oldCellKey)
|
||||
end
|
||||
end
|
||||
|
||||
---Returns all points
|
||||
---@return table
|
||||
function Point.GetAll()
|
||||
return ActivePoints
|
||||
end
|
||||
|
||||
return Point
|
|
@ -0,0 +1,53 @@
|
|||
Raycast = {}
|
||||
|
||||
--Rework of oxs Raycast system
|
||||
function Raycast.GetForwardVector(rotation)
|
||||
local camRot = rotation or GetFinalRenderedCamRot(2)
|
||||
|
||||
-- Convert each component to radians
|
||||
local rx = math.rad(camRot.x)
|
||||
local ry = math.rad(camRot.y)
|
||||
local rz = math.rad(camRot.z)
|
||||
|
||||
-- Calculate sin and cos for each axis
|
||||
local sx = math.sin(rx)
|
||||
local cx = math.cos(rx)
|
||||
local sy = math.sin(ry)
|
||||
local cy = math.cos(ry)
|
||||
local sz = math.sin(rz)
|
||||
local cz = math.cos(rz)
|
||||
|
||||
-- Create forward vector components
|
||||
local x = -sz * math.abs(cx)
|
||||
local y = cz * math.abs(cx)
|
||||
local z = sx
|
||||
|
||||
return vector3(x, y, z)
|
||||
end
|
||||
|
||||
function Raycast.ToCoords(startCoords, endCoords, flag, ignore)
|
||||
local probe = StartShapeTestLosProbe(startCoords.x, startCoords.y, startCoords.z, endCoords.x, endCoords.y, endCoords.z, flag or 511, PlayerPedId(), ignore or 4)
|
||||
local retval, entity, finalCoords, normals, material = 1, nil, nil, nil, nil
|
||||
|
||||
local timeout = 500
|
||||
while retval == 1 and timeout > 0 do
|
||||
retval, entity, finalCoords, normals, material = GetShapeTestResultIncludingMaterial(probe)
|
||||
timeout = timeout - 1
|
||||
Wait(0)
|
||||
end
|
||||
return retval, entity, finalCoords, normals, material
|
||||
end
|
||||
|
||||
function Raycast.FromCamera(flags, ignore, distance)
|
||||
local coords = GetFinalRenderedCamCoord()
|
||||
distance = distance or 10
|
||||
local destination = coords + Raycast.GetForwardVector() * distance
|
||||
local retval, entity, finalCoords, normals, material = Raycast.ToCoords(coords, destination, flags, ignore)
|
||||
if retval ~= 1 then
|
||||
local newDest = destination - vector3(0, 0, 10)
|
||||
return Raycast.ToCoords(destination, newDest, flags, ignore)
|
||||
end
|
||||
return retval, entity, finalCoords, normals, material
|
||||
end
|
||||
|
||||
return Raycast
|
|
@ -0,0 +1,130 @@
|
|||
---@class Scaleform
|
||||
local Scaleform = {}
|
||||
|
||||
-- Constants
|
||||
local SCALEFORM_TIMEOUT = 5000
|
||||
local BACKGROUND_COLOR_VALUE = 80
|
||||
local CONTROL_TYPE = 2
|
||||
local RENDER_WAIT_TIME = 2
|
||||
|
||||
-- Local utility functions
|
||||
local function setupButton(scaleform, button)
|
||||
PushScaleformMovieFunction(scaleform, button.type)
|
||||
|
||||
if button.int then
|
||||
PushScaleformMovieFunctionParameterInt(button.int)
|
||||
end
|
||||
|
||||
-- Handle key index setup
|
||||
if button.keyIndex then
|
||||
if type(button.keyIndex) == "table" then
|
||||
for _, keyCode in pairs(button.keyIndex) do
|
||||
N_0xe83a3e3557a56640(GetControlInstructionalButton(CONTROL_TYPE, keyCode, true))
|
||||
end
|
||||
else
|
||||
ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(CONTROL_TYPE, button.keyIndex[1],
|
||||
true))
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle button name setup
|
||||
if button.name then
|
||||
BeginTextCommandScaleformString("STRING")
|
||||
AddTextComponentScaleform(button.name)
|
||||
EndTextCommandScaleformString()
|
||||
end
|
||||
|
||||
-- Handle background color
|
||||
if button.type == 'SET_BACKGROUND_COLOUR' then
|
||||
for _ = 1, 4 do
|
||||
PushScaleformMovieFunctionParameterInt(BACKGROUND_COLOR_VALUE)
|
||||
end
|
||||
end
|
||||
|
||||
PopScaleformMovieFunctionVoid()
|
||||
end
|
||||
|
||||
---Creates and sets up a scaleform movie
|
||||
---@param scaleformName string The name of the scaleform to load
|
||||
---@param buttons table Array of button configurations
|
||||
---@return number scaleform The loaded scaleform handle
|
||||
local function setupScaleform(scaleformName, buttons)
|
||||
local scaleform = RequestScaleformMovie(scaleformName)
|
||||
local timeout = SCALEFORM_TIMEOUT
|
||||
|
||||
-- Wait for scaleform to load
|
||||
while not HasScaleformMovieLoaded(scaleform) and timeout > 0 do
|
||||
timeout = timeout - 1
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
if timeout <= 0 then
|
||||
error('Scaleform failed to load: ' .. scaleformName)
|
||||
end
|
||||
|
||||
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 0, 0)
|
||||
|
||||
-- Setup each button
|
||||
for _, button in ipairs(buttons) do
|
||||
setupButton(scaleform, button)
|
||||
end
|
||||
|
||||
return scaleform
|
||||
end
|
||||
|
||||
---Sets up instructional buttons with default configuration
|
||||
---@param buttons table Optional custom button configuration
|
||||
---@return number scaleform The configured instructional buttons scaleform
|
||||
function Scaleform.SetupInstructionalButtons(buttons)
|
||||
buttons = buttons or {
|
||||
-- Default button configuration commented out
|
||||
-- Uncomment and modify as needed
|
||||
-- {type = "CLEAR_ALL"},
|
||||
-- {type = "SET_CLEAR_SPACE", int = 200},
|
||||
-- {type = "SET_DATA_SLOT", name = config?.place_object?.name or 'Place Object:', keyIndex = config?.place_object?.key or {223}, int = 5},
|
||||
-- {type = "SET_DATA_SLOT", name = config?.cancel_placement?.name or 'Cancel Placement:', keyIndex = config?.cancel_placement?.key or {222}, int = 4},
|
||||
-- {type = "SET_DATA_SLOT", name = config?.snap_to_ground?.name or 'Snap to Ground:', keyIndex = config?.snap_to_ground?.key or {19}, int = 1},
|
||||
-- {type = "SET_DATA_SLOT", name = config?.rotate?.name or 'Rotate:', keyIndex = config?.rotate?.key or {14, 15}, int = 2},
|
||||
-- {type = "SET_DATA_SLOT", name = config?.distance?.name or 'Distance:', keyIndex = config?.distance?.key or {14,15,36}, int = 3},
|
||||
-- {type = "SET_DATA_SLOT", name = config?.toggle_placement?.name or 'Toggle Placement:', keyIndex = config?.toggle_placement?.key or {199}, int = 0},
|
||||
-- {type = "DRAW_INSTRUCTIONAL_BUTTONS"},
|
||||
-- {type = "SET_BACKGROUND_COLOUR"},
|
||||
}
|
||||
|
||||
return setupScaleform("instructional_buttons", buttons)
|
||||
end
|
||||
|
||||
-- Active scaleform tracking
|
||||
local activeScaleform = nil
|
||||
|
||||
---Runs a scaleform with optional update callback
|
||||
---@param scaleform number The scaleform handle to run
|
||||
---@param onUpdate function Optional callback for updates during runtime
|
||||
function Scaleform.Run(scaleform, onUpdate)
|
||||
if activeScaleform then return end
|
||||
activeScaleform = scaleform
|
||||
|
||||
CreateThread(function()
|
||||
while activeScaleform do
|
||||
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0)
|
||||
|
||||
if onUpdate then
|
||||
local shouldStop = onUpdate()
|
||||
if shouldStop then
|
||||
Scaleform.Stop()
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
Wait(RENDER_WAIT_TIME)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Stops the currently running scaleform
|
||||
function Scaleform.Stop()
|
||||
activeScaleform = nil
|
||||
end
|
||||
|
||||
exports("Scaleform", Scaleform)
|
||||
return Scaleform
|
|
@ -0,0 +1,53 @@
|
|||
Shells = Shells or {}
|
||||
|
||||
|
||||
--Data table
|
||||
Shells.Targets = Shells.Targets or {
|
||||
['entrance'] = {
|
||||
['enter'] = {
|
||||
label = 'Enter',
|
||||
icon = 'fa-solid fa-door-open',
|
||||
onSelect = function(entity, shellId, objectId)
|
||||
TriggerServerEvent('community_bridge:server:EnterShell', shellId, objectId)
|
||||
end
|
||||
},
|
||||
},
|
||||
['exit'] = {
|
||||
['leave'] = {
|
||||
label = 'Exit',
|
||||
icon = 'fa-solid fa-door-closed',
|
||||
onSelect = function(entity, shellId, objectId)
|
||||
TriggerServerEvent('community_bridge:server:ExitShell', shellId, objectId)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
--Functions to set data table
|
||||
Shells.Target = {
|
||||
Set = function(shellType, options)
|
||||
assert(shellType, "Shells.Target.Set: 'shellType' is required")
|
||||
options = options or {}
|
||||
for key, value in pairs(options) do
|
||||
if Shells.Targets[shellType] and Shells.Targets[shellType][key] then
|
||||
value.onSelect = Shells.Targets[shellType][key].onSelect
|
||||
end
|
||||
Shells.Targets[shellType] = Shells.Targets[shellType] or {}
|
||||
Shells.Targets[shellType][key] = value
|
||||
end
|
||||
return true
|
||||
end,
|
||||
Get = function(shellType, shellId, objectId)
|
||||
local aOptions = {}
|
||||
for key, value in pairs(Shells.Targets[shellType] or {}) do
|
||||
local onSelect = value.onSelect
|
||||
value.onSelect = function(entity)
|
||||
onSelect(entity, shellId, objectId)
|
||||
end
|
||||
table.insert(aOptions, value)
|
||||
end
|
||||
return aOptions
|
||||
end
|
||||
}
|
||||
|
||||
return Shells
|
|
@ -0,0 +1,269 @@
|
|||
local Target = Require('modules/target/_default/init.lua')
|
||||
local ClientEntity = Require("lib/entities/client/client_entity.lua")
|
||||
|
||||
|
||||
Shells = Shells or Require("lib/shells/client/config.lua")
|
||||
Shells.All = Shells.All or {}
|
||||
local insideShell = false
|
||||
Shells.Events = {
|
||||
OnSpawn = {},
|
||||
OnRemove = {},
|
||||
}
|
||||
|
||||
--Set Target lang or additionals
|
||||
-- Shells.Target.Set('entrance', {
|
||||
-- enter = {
|
||||
-- label = 'yerp',
|
||||
-- icon = 'fa-solid fa-door-open',
|
||||
-- },
|
||||
-- })
|
||||
|
||||
-- Shells.Target.Set('exit', {
|
||||
-- leave = {
|
||||
-- label = 'leerp',
|
||||
-- icon = 'fa-solid fa-door-closed',
|
||||
-- },
|
||||
-- })
|
||||
|
||||
Shells.Event = {
|
||||
Add = function(eventName, callback)
|
||||
if Shells.Events[eventName] then
|
||||
table.insert(Shells.Events[eventName], callback)
|
||||
else
|
||||
print(string.format("Shells.Event.Add: Invalid event name '%s'", eventName))
|
||||
end
|
||||
end,
|
||||
Trigger = function(eventName, ...)
|
||||
if Shells.Events[eventName] then
|
||||
for _, callback in ipairs(Shells.Events[eventName]) do
|
||||
callback(...)
|
||||
end
|
||||
else
|
||||
print(string.format("Shells.Event.Trigger: Invalid event name '%s'", eventName))
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
-- Shells.Event.Add('OnSpawn', function(pointData, entity)
|
||||
-- print(string.format("Exterior point created with ID: %s, Type: %s, Entity: %s", pointData.id, pointData.type, entity))
|
||||
-- end)
|
||||
-- Shells.Event.Add('OnRemove', function(pointData)
|
||||
-- print(string.format("Interior point created with ID: %s, Type: %s", pointData.id, pointData.type))
|
||||
-- end)
|
||||
|
||||
function Shells.AddInteriorObject(shell, objectData)
|
||||
objectData.OnSpawn = function(pointData)
|
||||
Shells.Event.Trigger('OnSpawn', objectData, pointData.spawned)
|
||||
local targetOptions = Shells.Target.Get(objectData.type, shell.id, objectData.id)
|
||||
if targetOptions then
|
||||
local size = vector3(objectData.distance / 2, objectData.distance / 2, objectData.distance / 2) -- this might need to be size
|
||||
Target.AddBoxZone(objectData.id, objectData.coords, size, objectData.rotation.z, targetOptions, true)
|
||||
end
|
||||
end
|
||||
objectData.OnRemove = function(pointData)
|
||||
Shells.Event.Trigger('OnRemove', objectData, pointData.spawned)
|
||||
Target.RemoveZone(objectData.id)
|
||||
end
|
||||
return ClientEntity.Register(objectData)
|
||||
end
|
||||
|
||||
function Shells.SetupInterior(shell)
|
||||
if not shell or not shell.interior then return end
|
||||
for _, v in pairs(shell.interior) do
|
||||
local pointData = Shells.AddInteriorObject(shell, v)
|
||||
shell.interiorSpawned[pointData.id] = pointData
|
||||
end
|
||||
end
|
||||
|
||||
function Shells.SetupExterior(shell)
|
||||
if not shell or not shell.exterior then return end
|
||||
for k, v in pairs(shell.exterior) do
|
||||
local pointData = Shells.AddInteriorObject(shell, v)
|
||||
shell.exteriorSpawned[pointData.id] = pointData
|
||||
end
|
||||
end
|
||||
|
||||
function Shells.ClearInterior(shell)
|
||||
if not shell or not shell.interiorSpawned then return end
|
||||
for _, v in pairs(shell.interiorSpawned) do
|
||||
ClientEntity.Unregister(v.id)
|
||||
Target.RemoveZone(v.id)
|
||||
end
|
||||
shell.interiorSpawned = {}
|
||||
end
|
||||
|
||||
function Shells.ClearExterior(shell)
|
||||
if not shell or not shell.exteriorSpawned then return end
|
||||
for _, v in pairs(shell.exteriorSpawned) do
|
||||
ClientEntity.Unregister(v.id)
|
||||
Target.RemoveZone(v.id)
|
||||
end
|
||||
shell.exteriorSpawned = {}
|
||||
end
|
||||
|
||||
|
||||
function Shells.New(data)
|
||||
assert(data.id, "Shells.Create: 'id' is required")
|
||||
assert(data.model, "Shells.Create: 'shellModel' is required")
|
||||
assert(data.coords, "Shells.Create: 'coords' is required")
|
||||
local exterior = data.exterior or {}
|
||||
local exteriorSpawned = {}
|
||||
for k, v in pairs(exterior or {}) do
|
||||
v.OnSpawn = function(pointData)
|
||||
Shells.Event.Trigger('OnSpawn', v, pointData.spawned)
|
||||
local targetOptions = Shells.Target.Get(v.type, data.id, v.id)
|
||||
if targetOptions then
|
||||
local size = vector3(v.distance / 2, v.distance / 2, v.distance / 2)
|
||||
Target.AddBoxZone(v.id, v.coords, size, v.rotation.z, targetOptions, true)
|
||||
end
|
||||
end
|
||||
v.OnRemove = function(pointData)
|
||||
Shells.Event.Trigger('OnRemove', v, pointData.spawned)
|
||||
Target.RemoveZone(v.id)
|
||||
end
|
||||
local pointData = ClientEntity.Register(v)
|
||||
exteriorSpawned[pointData.id] = pointData
|
||||
end
|
||||
data.interiorSpawned = {}
|
||||
data.exteriorSpawned = exteriorSpawned
|
||||
Shells.All[data.id] = data
|
||||
return data
|
||||
end
|
||||
|
||||
local returnPoint = nil
|
||||
function Shells.Enter(id, entranceId)
|
||||
local shell = Shells.All[id]
|
||||
if not shell then
|
||||
print(string.format("Shells.Spawn: Shell with ID '%s' not found", id))
|
||||
return
|
||||
end
|
||||
local entrance = shell.interior[entranceId]
|
||||
if not entrance then
|
||||
print(string.format("Shells.Enter: Entrance with ID '%s' not found in shell '%s'", entranceId, id))
|
||||
return
|
||||
end
|
||||
local ped = PlayerPedId()
|
||||
returnPoint = GetEntityCoords(ped)
|
||||
DoScreenFadeOut(1000)
|
||||
Wait(1000)
|
||||
local entranceCoords = entrance.coords
|
||||
SetEntityCoords(ped, entranceCoords.x, entranceCoords.y, entranceCoords.z, false, false, false, true)
|
||||
FreezeEntityPosition(ped, true)
|
||||
local oldShell = insideShell and Shells.All[insideShell]
|
||||
if oldShell?.id ~= id then
|
||||
Shells.ClearExterior(oldShell)
|
||||
Shells.ClearInterior(oldShell)
|
||||
end
|
||||
Shells.ClearExterior(shell)
|
||||
Shells.SetupInterior(shell)
|
||||
ClientEntity.Register(shell)
|
||||
Wait(1000) -- Wait for the fade out to complete
|
||||
FreezeEntityPosition(ped, false)
|
||||
DoScreenFadeIn(1000)
|
||||
insideShell = shell.id
|
||||
end
|
||||
|
||||
function Shells.Exit(id, exitId)
|
||||
local shell = Shells.All[id]
|
||||
if not shell then
|
||||
print(string.format("Shells.Exit: Shell with ID '%s' not found", id))
|
||||
return
|
||||
end
|
||||
local oldCoords = GetEntityCoords(PlayerPedId())
|
||||
local oldPoint = shell.exterior[exitId]
|
||||
if not oldPoint then
|
||||
print(string.format("Shells.Exit: Old point with ID '%s' not found in shell '%s'", exitId, id))
|
||||
return
|
||||
end
|
||||
DoScreenFadeOut(1000)
|
||||
Wait(1000)
|
||||
SetEntityCoords(PlayerPedId(), oldPoint.coords.x, oldPoint.coords.y, oldPoint.coords.z, false, false, false, true)
|
||||
FreezeEntityPosition(PlayerPedId(), true)
|
||||
Shells.ClearInterior(shell)
|
||||
ClientEntity.Unregister(shell.id)
|
||||
Shells.SetupExterior(shell)
|
||||
shell.interiorSpawned = {}
|
||||
FreezeEntityPosition(PlayerPedId(), false)
|
||||
DoScreenFadeIn(1000)
|
||||
insideShell = false
|
||||
end
|
||||
|
||||
function Shells.Inside()
|
||||
return insideShell
|
||||
end
|
||||
|
||||
RegisterNetEvent('community_bridge:client:CreateShell', function(shell)
|
||||
Shells.New(shell)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('community_bridge:client:EnterShell', function(shellId, entranceId, oldId)
|
||||
local shell = Shells.All[shellId]
|
||||
if not shell then
|
||||
print(string.format("Shells.EnterShell: Shell with ID '%s' not found", shellId))
|
||||
return
|
||||
end
|
||||
|
||||
Shells.Enter(shellId, entranceId, oldId)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('community_bridge:client:ExitShell', function(shellId, oldId)
|
||||
local shell = Shells.All[shellId]
|
||||
print(string.format("Shells.ExitShell: Exiting shell '%s'", shellId))
|
||||
if not shell then
|
||||
print(string.format("Shells.ExitShell: Shell with ID '%s' not found", shellId))
|
||||
return
|
||||
end
|
||||
Shells.Exit(shellId, oldId)
|
||||
|
||||
end)
|
||||
|
||||
RegisterNetEvent('community_bridge:client:AddObjectsToShell', function (shellId, interiorObjects, exteriorObjects)
|
||||
local shell = Shells.All[shellId]
|
||||
print(string.format("Shells.AddObjectsToShell: Adding objects to shell '%s'", shellId),
|
||||
json.encode({interiorObjects = interiorObjects, exteriorObjects = exteriorObjects}, { indent = true }))
|
||||
if not shell then
|
||||
print(string.format("Shells.AddObjectsToShell: Shell with ID '%s' not found", shellId))
|
||||
return
|
||||
end
|
||||
local insideShell = Shells.Inside()
|
||||
if interiorObjects then
|
||||
for _, obj in pairs(interiorObjects) do
|
||||
if not shell.interior[obj.id] then
|
||||
shell.interior[obj.id] = obj
|
||||
if insideShell and insideShell == shellId then
|
||||
local pointData = Shells.AddInteriorObject(shell, obj)
|
||||
shell.interiorSpawned[pointData.id] = pointData
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if exteriorObjects then
|
||||
for _, obj in pairs(exteriorObjects) do
|
||||
if not shell.exterior[obj.id] then
|
||||
shell.exterior[obj.id] = obj
|
||||
if not insideShell then
|
||||
local pointData = Shells.AddInteriorObject(shell, obj)
|
||||
shell.exteriorSpawned[pointData.id] = pointData
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent('community_bridge:client:CreateShells', function(shells)
|
||||
print("Shells.CreateShells: Creating shells")
|
||||
for _, shell in pairs(shells) do
|
||||
Shells.New(shell)
|
||||
end
|
||||
end)
|
||||
-- TriggerClientEvent('community_bridge:client:CreateShells', -1, toClient)
|
||||
-- TriggerClientEvent('community_bridge:client:ExitShell', src, oldId)
|
||||
|
||||
|
||||
AddEventHandler('onResourceStart', function(resource)
|
||||
if resource == GetCurrentResourceName() then
|
||||
DoScreenFadeIn(1000) -- Fade in when resource stops
|
||||
if not returnPoint then return end
|
||||
SetEntityCoords(PlayerPedId(), returnPoint.x, returnPoint.y, returnPoint.z, false, false, false, true)
|
||||
end
|
||||
end)
|
|
@ -0,0 +1,379 @@
|
|||
Ids = Ids or Require("lib/utility/shared/ids.lua")
|
||||
Shells = Shells or {}
|
||||
Shells.All = Shells.All or {}
|
||||
Shells.ActivePlayers = Shells.ActivePlayers or {} -- Track players in shells
|
||||
|
||||
Shells.Interactable = Shells.Interactable or {}
|
||||
Shells.BucketsInUse = Shells.BucketsInUse or {} -- Track buckets in use
|
||||
|
||||
function Shells.Interactable.New(_type, id, model, coords, rotation, entityType, distance, meta)
|
||||
assert(_type, "Shells.Interactable.Create: 'type' is required")
|
||||
assert(id, "Shells.Interactable.Create: 'name' is required")
|
||||
assert(coords, "Shells.Interactable.Create: 'coords' is required")
|
||||
return {
|
||||
type = _type,
|
||||
id = id,
|
||||
model = model,
|
||||
coords = coords,
|
||||
rotation = rotation or vector3(0.0, 0.0, 0.0),
|
||||
distance = distance or 2.0,
|
||||
entityType = entityType or "object", -- Default entity type
|
||||
meta = meta or {},
|
||||
}
|
||||
end
|
||||
|
||||
function Shells.New(data)
|
||||
local id = data.id or Ids.CreateUniqueId(Shells.All)
|
||||
local _type = data.type or "none"
|
||||
local model = data.model
|
||||
local size = data.size or vector3(10.0, 10.0, 10.0) -- Default size if not provided
|
||||
local coords = data.coords or vector3(0.0, 0.0, 0.0) -- Default coordinates if not provided
|
||||
local rotation = data.rotation or vector3(0.0, 0.0, 0.0) -- Default rotation if not provided
|
||||
local interior = data.interior or {}
|
||||
local exterior = data.exterior or {}
|
||||
local bucket = data.bucket or Ids.RandomNumber(Shells.BucketsInUse, 4)
|
||||
assert(id, "Shells.Create: 'id' is required")
|
||||
assert(model, "Shells.Create: 'shellModel' is required")
|
||||
assert(coords, "Shells.Create: 'coords' is required")
|
||||
|
||||
local interiorInteractions = {}
|
||||
for k, v in pairs(interior or {}) do
|
||||
local interiorCoords = coords + (v.offset or vector3(0.0, 0.0, 0.0))
|
||||
local interaction = Shells.Interactable.New(
|
||||
v.type or "none",
|
||||
v.id or Ids.CreateUniqueId(interiorInteractions),
|
||||
v.model,
|
||||
interiorCoords,
|
||||
v.rotation or vector3(0.0, 0.0, 0.0),
|
||||
v.entityType or "object", -- Default entity type for interior interactions
|
||||
v.distance or 2.0,
|
||||
v.meta
|
||||
)
|
||||
interiorInteractions[interaction.id] = interaction
|
||||
end
|
||||
|
||||
local exteriorInteractions = {}
|
||||
for k, v in pairs(exterior or {}) do
|
||||
local interaction = Shells.Interactable.New(
|
||||
v.type or "none",
|
||||
v.id or Ids.CreateUniqueId(exteriorInteractions),
|
||||
v.model,
|
||||
v.coords,
|
||||
v.rotation or vector3(0.0, 0.0, 0.0),
|
||||
v.entityType or "object", -- Default entity type for exterior interactions
|
||||
v.distance or 2.0,
|
||||
v.meta
|
||||
)
|
||||
exteriorInteractions[interaction.id] = interaction
|
||||
end
|
||||
|
||||
local shellData = {
|
||||
id = id,
|
||||
type = _type,
|
||||
entityType = "object", -- Default entity type for shells
|
||||
model = model,
|
||||
coords = coords,
|
||||
size = size or vector3(10.0, 10.0, 10.0), -- Default size if not provided
|
||||
rotation = rotation or vector3(0.0, 0.0, 0.0),
|
||||
interior = interiorInteractions,
|
||||
exterior = exteriorInteractions,
|
||||
}
|
||||
|
||||
Shells.All[id] = shellData
|
||||
return shellData
|
||||
end
|
||||
|
||||
function Shells.Create(data)
|
||||
local shell = Shells.New(data)
|
||||
assert(shell, "Shells.Create: 'shell' is required")
|
||||
TriggerClientEvent('community_bridge:client:CreateShell', -1, shell)
|
||||
return shell
|
||||
end
|
||||
|
||||
function Shells.CreateBulk(shells)
|
||||
assert(shells, "Shells.CreateBulk: 'shells' is required")
|
||||
assert(type(shells) == "table", "Shells.CreateBulk: 'shells' must be a table")
|
||||
local toClient = {}
|
||||
for _, shellData in pairs(shells) do
|
||||
local shell = Shells.New(shellData)
|
||||
toClient[shell.id] = shell
|
||||
end
|
||||
TriggerClientEvent('community_bridge:client:CreateShells', -1, toClient)
|
||||
end
|
||||
|
||||
function Shells.Enter(src, shellId, entranceId)
|
||||
src = tonumber(src)
|
||||
assert(src, "Shells.EnterShell: 'src' is required")
|
||||
assert(shellId, "Shells.EnterShell: 'shellId' is required")
|
||||
print(shellId)
|
||||
local shell = Shells.All[shellId]
|
||||
assert(shell, "Shell not found: " .. tostring(shellId))
|
||||
if shell.onEnter then
|
||||
local canEnter = shell.onEnter(src, shellId)
|
||||
if not canEnter then return false end
|
||||
end
|
||||
if not shell.bucket then
|
||||
local randNum = Ids.RandomNumber(Shells.BucketsInUse, 4)
|
||||
shell.bucket = tonumber(randNum)
|
||||
Shells.BucketsInUse[tostring(randNum)] = true
|
||||
end
|
||||
print(shell.bucket)
|
||||
SetPlayerRoutingBucket(src, shell.bucket)
|
||||
local exit = shell.exterior[entranceId]?.meta?.link
|
||||
TriggerClientEvent('community_bridge:client:EnterShell', src, shellId, exit, Shells.ActivePlayers[tostring(src)])
|
||||
Shells.ActivePlayers[tostring(src)] = shellId
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function Shells.Exit(src, shellId, oldId)
|
||||
|
||||
print(oldId)
|
||||
src = tonumber(src)
|
||||
assert(src, "Shells.ExitShell: 'src' is required")
|
||||
assert(shellId, "Player is not in a shell")
|
||||
|
||||
local shell = Shells.All[shellId]
|
||||
assert(shell, "Shell not found: " .. tostring(shellId))
|
||||
|
||||
-- Restore original routing bucket
|
||||
SetPlayerRoutingBucket(src, 0)
|
||||
local exit = shell.interior[oldId]?.meta?.link
|
||||
-- Clear player's shell data
|
||||
Shells.ActivePlayers[tostring(src)] = nil
|
||||
if shell.onExit then
|
||||
shell.onExit(src, shellId)
|
||||
end
|
||||
TriggerClientEvent('community_bridge:client:ExitShell', src, shellId, exit)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
function Shells.Get(shellId)
|
||||
assert(shellId, "Shells.GetShellById: 'shellId' is required")
|
||||
return Shells.All[shellId]
|
||||
end
|
||||
|
||||
function Shells.Inside(src)
|
||||
src = tonumber(src)
|
||||
assert(src, "Shells.IsInside: 'src' is required")
|
||||
|
||||
local shellId = Shells.ActivePlayers[tostring(src)]
|
||||
if not shellId then
|
||||
return false
|
||||
end
|
||||
|
||||
local shell = Shells.All[shellId]
|
||||
if not shell then
|
||||
return false
|
||||
end
|
||||
|
||||
return shell
|
||||
end
|
||||
|
||||
function Shells.AddObjects(shellId, objects)
|
||||
assert(shellId, "Shells.AddObjects: 'shellId' is required")
|
||||
assert(objects, "Shells.AddObjects: 'objects' is required")
|
||||
assert(type(objects) == "table", "Shells.AddObjects: 'objects' must be a table")
|
||||
|
||||
local shell = Shells.All[shellId]
|
||||
assert(shell, "Shell not found: " .. tostring(shellId))
|
||||
|
||||
local interiors = objects.interior or {}
|
||||
local exteriors = objects.exterior or {}
|
||||
for _, objData in pairs(interiors) do
|
||||
local obj = Shells.Interactable.New(
|
||||
objData.type,
|
||||
objData.id,
|
||||
objData.model,
|
||||
objData.coords,
|
||||
objData.rotation,
|
||||
objData.entityType,
|
||||
objData.distance,
|
||||
objData.meta
|
||||
)
|
||||
shell.interior[obj.id] = obj -- Update the shell's interior with the new object
|
||||
end
|
||||
for _, objData in pairs(exteriors) do
|
||||
local obj = Shells.Interactable.New(
|
||||
objData.type,
|
||||
objData.id,
|
||||
objData.model,
|
||||
objData.coords,
|
||||
objData.rotation,
|
||||
objData.entityType,
|
||||
objData.distance,
|
||||
objData.meta
|
||||
)
|
||||
shell.exterior[obj.id] = obj -- Update the shell's exterior with the new object
|
||||
end
|
||||
|
||||
TriggerClientEvent('community_bridge:client:AddObjectsToShell', -1, shellId, shell.interior, shell.exterior)
|
||||
return shell
|
||||
end
|
||||
|
||||
function Shells.RemoveObjects(shellId, objectIds)
|
||||
assert(shellId, "Shells.RemoveObjects: 'shellId' is required")
|
||||
assert(objectIds, "Shells.RemoveObjects: 'objects' is required")
|
||||
if type(objectIds) ~= "table" then
|
||||
objectIds = {objectIds} -- Ensure it's a table
|
||||
end
|
||||
|
||||
local shell = Shells.All[shellId]
|
||||
assert(shell, "Shell not found: " .. tostring(shellId))
|
||||
|
||||
for _, objId in pairs(objectIds) do
|
||||
shell.interior[objId] = nil
|
||||
shell.exterior[objId] = nil
|
||||
end
|
||||
|
||||
TriggerClientEvent('community_bridge:client:RemoveObjectsFromShell', -1, shellId, objectIds)
|
||||
return shell
|
||||
end
|
||||
|
||||
RegisterNetEvent('community_bridge:server:EnterShell', function(shellId, entranceId)
|
||||
local src = source
|
||||
Shells.Enter(src, shellId, entranceId)
|
||||
end)
|
||||
|
||||
RegisterNetEvent('community_bridge:server:ExitShell', function(shellId, oldId)
|
||||
local src = source
|
||||
Shells.Exit(src, shellId, oldId)
|
||||
end)
|
||||
|
||||
AddEventHandler('onPlayerJoining', function(playerId)
|
||||
local src = source
|
||||
TriggerClientEvent('community_bridge:client:CreateShells', src, Shells.All)
|
||||
end)
|
||||
|
||||
function Shells.GetInteractable(shellId, id)
|
||||
assert(shellId, "Shells.GetInteractable: 'shellId' is required")
|
||||
assert(id, "Shells.GetInteractable: 'id' is required")
|
||||
|
||||
local shell = Shells.All[shellId]
|
||||
if not shell then
|
||||
return nil, "Shell not found: " .. tostring(shellId)
|
||||
end
|
||||
|
||||
local interactable = shell.interior[id] or shell.exterior[id]
|
||||
if not interactable then
|
||||
return nil, "Interactable not found: " .. tostring(id)
|
||||
end
|
||||
|
||||
return interactable
|
||||
end
|
||||
|
||||
function Shells.GetClosestInteractable(shellId, coords, exterior)
|
||||
assert(shellId, "Shells.GetClosestInteriorInteractable: 'shellId' is required")
|
||||
assert(coords, "Shells.GetClosestInteriorInteractable: 'coords' is required")
|
||||
|
||||
local shell = Shells.All[shellId]
|
||||
if not shell then
|
||||
return nil, "Shell not found: " .. tostring(shellId)
|
||||
end
|
||||
|
||||
local closest = nil
|
||||
local closestDistance = math.huge
|
||||
if exterior then
|
||||
for _, interactable in pairs(shell.exterior) do
|
||||
local distance = #(coords - interactable.coords)
|
||||
if distance < closestDistance then
|
||||
closestDistance = distance
|
||||
closest = interactable
|
||||
end
|
||||
end
|
||||
return closest, closestDistance
|
||||
end
|
||||
|
||||
for _, interactable in pairs(shell.interior) do
|
||||
local distance = #(coords - interactable.coords)
|
||||
if distance < closestDistance then
|
||||
closestDistance = distance
|
||||
closest = interactable
|
||||
end
|
||||
end
|
||||
return closest, closestDistance
|
||||
end
|
||||
|
||||
local testShell = nil
|
||||
RegisterCommand('shells:create', function(source, args, rawCommand)
|
||||
local coords = GetEntityCoords(GetPlayerPed(source))
|
||||
if testShell then
|
||||
Shells.Exit(source, testShell.id, 'exit1') -- Exit previous shell if it exists
|
||||
end
|
||||
testShell = Shells.Create({
|
||||
id = Ids.CreateUniqueId(Shells.All),
|
||||
type = "shell",
|
||||
model = "shell_garagem",
|
||||
coords = coords + vector3(0.0, 0.0, 100.0), -- Adjusted to place shell slightly below player
|
||||
rotation = vector3(0.0, 0.0, 0.0),
|
||||
size = vector3(10.0, 10.0, 10.0),
|
||||
interior = {
|
||||
{
|
||||
id = 'exit1',
|
||||
type = 'exit',
|
||||
coords = coords + vector3(0.0, 0.0, 1.0),
|
||||
rotation = vector3(0.0, 0.0, 0.0),
|
||||
distance = 2.0,
|
||||
meta = {
|
||||
link = 'entrance1',
|
||||
}
|
||||
}
|
||||
},
|
||||
exterior = {
|
||||
{
|
||||
id = 'entrance1',
|
||||
type = 'entrance',
|
||||
-- entityType = "object",
|
||||
-- model = "xm_int_lev_sub_chair_02",
|
||||
coords = coords - vector3(0.0, 0.0, 0.5),
|
||||
rotation = vector3(0.0, 0.0, 0.0),
|
||||
distance = 2.0,
|
||||
meta = {
|
||||
link = 'exit1',
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
end, true)
|
||||
|
||||
RegisterCommand('shells:addobject', function(source, args, rawCommand)
|
||||
if not testShell then
|
||||
print("No shell created. Use /shells:create first.")
|
||||
return
|
||||
end
|
||||
local model = args[1]
|
||||
if not model then
|
||||
print("Usage: /shells:addobject <shellId> <model>")
|
||||
return
|
||||
end
|
||||
|
||||
local shellId = testShell.id
|
||||
local coords = GetEntityCoords(GetPlayerPed(source))
|
||||
local objectData = {
|
||||
type = "none",
|
||||
entityType = "ped",
|
||||
id = Ids.CreateUniqueId(Shells.All[shellId].interior),
|
||||
model = model,
|
||||
coords = coords - vector3(0.0, 0.0, 0.5), -- Adjusted to place object slightly above player
|
||||
rotation = vector3(0.0, 0.0, 0.0),
|
||||
distance = 2.0,
|
||||
meta = {}
|
||||
}
|
||||
Shells.AddObjects(shellId, {interior = {objectData}})
|
||||
end, true)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource == GetCurrentResourceName() then
|
||||
for src, shellId in pairs(Shells.ActivePlayers) do
|
||||
SetPlayerRoutingBucket(tonumber(src), 0) -- Reset routing bucket for all players
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('community_bridge:Server:OnPlayerUnload', function(src)
|
||||
if not Shells.ActivePlayers[tostring(src)] then return end
|
||||
print(string.format("Player %d is exiting shell %s", src, Shells.ActivePlayers[tostring(src)]))
|
||||
Shells.Exit(src, Shells.ActivePlayers[tostring(src)])
|
||||
Shells.ActivePlayers[tostring(src)] = nil
|
||||
end)
|
|
@ -0,0 +1,73 @@
|
|||
SQL = {}
|
||||
|
||||
Require("lib/MySQL.lua", "oxmysql")
|
||||
|
||||
--- Creates a table in the database if it does not exist.
|
||||
-- @param tableName The name of the table to create. Example: {{ name = "identifier", type = "VARCHAR(50)", primary = true }}
|
||||
-- @param columns A table containing column definitions, where each column is a table with 'name' and 'type'.
|
||||
---@return nil
|
||||
|
||||
function SQL.Create(tableName, columns)
|
||||
assert(MySQL, "Tried using module SQL without MySQL being loaded")
|
||||
local columnsList = {}
|
||||
for i, column in pairs(columns) do
|
||||
table.insert(columnsList, string.format("%s %s", column.name, column.type))
|
||||
end
|
||||
|
||||
local query = string.format("CREATE TABLE IF NOT EXISTS %s (%s);",
|
||||
tableName,
|
||||
table.concat(columnsList, ", ")
|
||||
)
|
||||
|
||||
MySQL.query.await(query)
|
||||
end
|
||||
|
||||
-- insert if not exist otherwise update
|
||||
function SQL.InsertOrUpdate(tableName, data)
|
||||
assert(MySQL, "Tried using module SQL without MySQL being loaded")
|
||||
local columns = {}
|
||||
local values = {}
|
||||
local updates = {}
|
||||
|
||||
for column, value in pairs(data) do
|
||||
table.insert(columns, column)
|
||||
table.insert(values, "'" .. value .. "'") -- Ensure values are properly quoted
|
||||
table.insert(updates, column .. " = VALUES(" .. column .. ")") -- Use VALUES() for update
|
||||
end
|
||||
|
||||
local query = string.format(
|
||||
"INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s;",
|
||||
tableName,
|
||||
table.concat(columns, ", "),
|
||||
table.concat(values, ", "),
|
||||
table.concat(updates, ", ")
|
||||
)
|
||||
|
||||
MySQL.query.await(query)
|
||||
end
|
||||
|
||||
function SQL.Get(tableName, where)
|
||||
assert(MySQL, "Tried using module SQL without MySQL being loaded")
|
||||
local query = string.format("SELECT * FROM %s WHERE %s;", tableName, where)
|
||||
local result = MySQL.query.await(query)
|
||||
return result
|
||||
end
|
||||
|
||||
function SQL.GetAll(tableName)
|
||||
assert(MySQL, "Tried using module SQL without MySQL being loaded")
|
||||
local query = string.format("SELECT * FROM %s;", tableName)
|
||||
local result = MySQL.query.await(query)
|
||||
return result
|
||||
end
|
||||
|
||||
function SQL.Delete(tableName, where)
|
||||
assert(MySQL, "Tried using module SQL without MySQL being loaded")
|
||||
local query = string.format("DELETE FROM %s WHERE %s;", tableName, where)
|
||||
MySQL.query.await(query)
|
||||
end
|
||||
|
||||
exports('SQL', function()
|
||||
return SQL
|
||||
end)
|
||||
|
||||
return SQL
|
|
@ -0,0 +1,48 @@
|
|||
ClientStateBag = {}
|
||||
|
||||
---Gets an entity from a statebag name
|
||||
---@param entityId string The entity ID or statebag name
|
||||
---@return number|nil entity The entity handle or nil if not found
|
||||
local function getStateBagEntity(entityId)
|
||||
local _entity = GetEntityFromStateBagName(entityId)
|
||||
return _entity
|
||||
end
|
||||
|
||||
local function getPlayerFromStateBagName(stateBagName)
|
||||
return getPlayerFromStateBagName(stateBagName)
|
||||
end
|
||||
|
||||
---Adds a handler for entity statebag changes
|
||||
---@param keyName string The statebag key to watch for changes
|
||||
---@param entityId string|nil The specific entity ID to watch, or nil for all entities
|
||||
---@param callback function The callback function to handle changes (function(entityId, key, value, lastValue, replicated))
|
||||
---@return number handler The handler ID
|
||||
function ClientStateBag.AddEntityChangeHandler(keyName, entityId, callback)
|
||||
return AddStateBagChangeHandler(keyName, entityId or nil, function(bagName, key, value, lastValue, replicated)
|
||||
local entity = getStateBagEntity(bagName)
|
||||
if not DoesEntityExist(entity) then return end
|
||||
if entity then
|
||||
return callback(entity, key, value, lastValue, replicated)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Adds a handler for player statebag changes
|
||||
---@param keyName string The statebag key to watch for changes
|
||||
---@param filter boolean|nil If true, only watch for changes from the current player
|
||||
---@param callback function The callback function to handle changes (function(playerId, key, value, lastValue, replicated))
|
||||
---@return number handler The handler ID
|
||||
function ClientStateBag.AddPlayerChangeHandler(keyName, filter, callback)
|
||||
return AddStateBagChangeHandler(keyName, filter and ("player:%s"):format(GetPlayerServerId(PlayerId())) or nil,
|
||||
function(bagName, key, value, lastValue, replicated)
|
||||
local actualPlayerId = getPlayerFromStateBagName(bagName)
|
||||
if DoesEntityExist(actualPlayerId) and (actualPlayerId ~= PlayerPedId()) then -- you cant have a statebag value if you are not the player
|
||||
return false
|
||||
end
|
||||
if actualPlayerId and actualPlayerId ~= 0 then
|
||||
return callback(tonumber(actualPlayerId), key, value, lastValue, replicated)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return ClientStateBag
|
|
@ -0,0 +1,640 @@
|
|||
---@class Utility
|
||||
Utility = Utility or {}
|
||||
local blipIDs = {}
|
||||
local spawnedPeds = {}
|
||||
|
||||
Locales = Locales or Require('modules/locales/shared.lua')
|
||||
|
||||
-- === Local Helpers ===
|
||||
|
||||
---Get the hash of a model (string or number)
|
||||
---@param model string|number
|
||||
---@return number
|
||||
local function getModelHash(model)
|
||||
if type(model) ~= 'number' then
|
||||
return joaat(model)
|
||||
end
|
||||
return model
|
||||
end
|
||||
|
||||
---Ensure a model is loaded into memory
|
||||
---@param model string|number
|
||||
---@return boolean, number
|
||||
local function ensureModelLoaded(model)
|
||||
local hash = getModelHash(model)
|
||||
if not IsModelValid(hash) and not IsModelInCdimage(hash) then return false, hash end
|
||||
RequestModel(hash)
|
||||
local count = 0
|
||||
while not HasModelLoaded(hash) and count < 30000 do
|
||||
Wait(0)
|
||||
count = count + 1
|
||||
end
|
||||
return HasModelLoaded(hash), hash
|
||||
end
|
||||
|
||||
---Add a text entry if possible
|
||||
---@param key string
|
||||
---@param text string
|
||||
local function addTextEntryOnce(key, text)
|
||||
if not AddTextEntry then return end
|
||||
AddTextEntry(key, text)
|
||||
end
|
||||
|
||||
---Create a blip safely and store its reference
|
||||
---@param coords vector3
|
||||
---@param sprite number
|
||||
---@param color number
|
||||
---@param scale number
|
||||
---@param label string
|
||||
---@param shortRange boolean
|
||||
---@param displayType number
|
||||
---@return number
|
||||
local function safeAddBlip(coords, sprite, color, scale, label, shortRange, displayType)
|
||||
local blip = AddBlipForCoord(coords.x, coords.y, coords.z)
|
||||
SetBlipSprite(blip, sprite or 8)
|
||||
SetBlipColour(blip, color or 3)
|
||||
SetBlipScale(blip, scale or 0.8)
|
||||
SetBlipDisplay(blip, displayType or 2)
|
||||
SetBlipAsShortRange(blip, shortRange)
|
||||
addTextEntryOnce(label, label)
|
||||
BeginTextCommandSetBlipName(label)
|
||||
EndTextCommandSetBlipName(blip)
|
||||
table.insert(blipIDs, blip)
|
||||
return blip
|
||||
end
|
||||
|
||||
---Create a entiyty blip safely and store its reference
|
||||
---@param entity number
|
||||
---@param sprite number
|
||||
---@param color number
|
||||
---@param scale number
|
||||
---@param label string
|
||||
---@param shortRange boolean
|
||||
---@param displayType number
|
||||
---@return number
|
||||
local function safeAddEntityBlip(entity, sprite, color, scale, label, shortRange, displayType)
|
||||
local blip = AddBlipForEntity(entity)
|
||||
SetBlipSprite(blip, sprite or 8)
|
||||
SetBlipColour(blip, color or 3)
|
||||
SetBlipScale(blip, scale or 0.8)
|
||||
SetBlipDisplay(blip, displayType or 2)
|
||||
SetBlipAsShortRange(blip, shortRange)
|
||||
ShowHeadingIndicatorOnBlip(blip, true)
|
||||
addTextEntryOnce(label, label)
|
||||
BeginTextCommandSetBlipName(label)
|
||||
EndTextCommandSetBlipName(blip)
|
||||
table.insert(blipIDs, blip)
|
||||
return blip
|
||||
end
|
||||
|
||||
---Remove a blip safely from the stored list
|
||||
---@param blip number
|
||||
---@return boolean
|
||||
local function safeRemoveBlip(blip)
|
||||
for i, storedBlip in ipairs(blipIDs) do
|
||||
if storedBlip == blip then
|
||||
RemoveBlip(storedBlip)
|
||||
table.remove(blipIDs, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Add a text entry if possible (shortcut)
|
||||
---@param text string
|
||||
local function safeAddTextEntry(text)
|
||||
if not AddTextEntry then return end
|
||||
AddTextEntry(text, text)
|
||||
end
|
||||
|
||||
-- === Public Utility Functions ===
|
||||
|
||||
---Create a prop with the given model and coordinates
|
||||
---@param model string|number
|
||||
---@param coords vector3
|
||||
---@param heading number
|
||||
---@param networked boolean
|
||||
---@return number|nil
|
||||
function Utility.CreateProp(model, coords, heading, networked)
|
||||
local loaded, hash = ensureModelLoaded(model)
|
||||
if not loaded then return nil, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end
|
||||
local propEntity = CreateObject(hash, coords.x, coords.y, coords.z, networked, false, false)
|
||||
SetEntityHeading(propEntity, heading)
|
||||
SetModelAsNoLongerNeeded(hash)
|
||||
return propEntity
|
||||
end
|
||||
|
||||
---Get street and crossing names at given coordinates
|
||||
---@param coords vector3
|
||||
---@return string, string
|
||||
function Utility.GetStreetNameAtCoords(coords)
|
||||
local streetHash, crossingHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
|
||||
return GetStreetNameFromHashKey(streetHash), GetStreetNameFromHashKey(crossingHash)
|
||||
end
|
||||
|
||||
---Create a vehicle with the given model and coordinates
|
||||
---@param model string|number
|
||||
---@param coords vector3
|
||||
---@param heading number
|
||||
---@param networked boolean
|
||||
---@return number|nil, table
|
||||
function Utility.CreateVehicle(model, coords, heading, networked)
|
||||
local loaded, hash = ensureModelLoaded(model)
|
||||
if not loaded then return nil, {}, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end
|
||||
local vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, heading, networked, false)
|
||||
SetVehicleHasBeenOwnedByPlayer(vehicle, true)
|
||||
SetVehicleNeedsToBeHotwired(vehicle, false)
|
||||
SetVehRadioStation(vehicle, "OFF")
|
||||
SetModelAsNoLongerNeeded(hash)
|
||||
return vehicle, {
|
||||
networkid = NetworkGetNetworkIdFromEntity(vehicle) or 0,
|
||||
coords = GetEntityCoords(vehicle),
|
||||
heading = GetEntityHeading(vehicle),
|
||||
}
|
||||
end
|
||||
|
||||
---Create a ped with the given model and coordinates
|
||||
---@param model string|number
|
||||
---@param coords vector3
|
||||
---@param heading number
|
||||
---@param networked boolean
|
||||
---@param settings table|nil
|
||||
---@return number|nil
|
||||
function Utility.CreatePed(model, coords, heading, networked, settings)
|
||||
local loaded, hash = ensureModelLoaded(model)
|
||||
if not loaded then return nil, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end
|
||||
local spawnedEntity = CreatePed(0, hash, coords.x, coords.y, coords.z, heading, networked, false)
|
||||
SetModelAsNoLongerNeeded(hash)
|
||||
table.insert(spawnedPeds, spawnedEntity)
|
||||
return spawnedEntity
|
||||
end
|
||||
|
||||
---Show a busy spinner with the given text
|
||||
---@param text string
|
||||
---@return boolean
|
||||
function Utility.StartBusySpinner(text)
|
||||
safeAddTextEntry(text)
|
||||
BeginTextCommandBusyString(text)
|
||||
AddTextComponentSubstringPlayerName(text)
|
||||
EndTextCommandBusyString(0)
|
||||
return true
|
||||
end
|
||||
|
||||
---Stop the busy spinner if active
|
||||
---@return boolean
|
||||
function Utility.StopBusySpinner()
|
||||
if BusyspinnerIsOn() then
|
||||
BusyspinnerOff()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Create a blip at the given coordinates
|
||||
---@param coords vector3
|
||||
---@param sprite number
|
||||
---@param color number
|
||||
---@param scale number
|
||||
---@param label string
|
||||
---@param shortRange boolean
|
||||
---@param displayType number
|
||||
---@return number
|
||||
function Utility.CreateBlip(coords, sprite, color, scale, label, shortRange, displayType)
|
||||
return safeAddBlip(coords, sprite, color, scale, label, shortRange, displayType)
|
||||
end
|
||||
|
||||
---Create a blip on the provided entity
|
||||
---@param entity number
|
||||
---@param sprite number
|
||||
---@param color number
|
||||
---@param scale number
|
||||
---@param label string
|
||||
---@param shortRange boolean
|
||||
---@param displayType number
|
||||
---@return number
|
||||
function Utility.CreateEntityBlip(entity, sprite, color, scale, label, shortRange, displayType)
|
||||
return safeAddEntityBlip(entity, sprite, color, scale, label, shortRange, displayType)
|
||||
end
|
||||
|
||||
---Remove a blip if it exists
|
||||
---@param blip number
|
||||
---@return boolean
|
||||
function Utility.RemoveBlip(blip)
|
||||
return safeRemoveBlip(blip)
|
||||
end
|
||||
|
||||
---Load a model into memory
|
||||
---@param model string|number
|
||||
---@return boolean
|
||||
function Utility.LoadModel(model)
|
||||
local loaded = ensureModelLoaded(model)
|
||||
return loaded
|
||||
end
|
||||
|
||||
---Request an animation dictionary
|
||||
---@param dict string
|
||||
---@return boolean
|
||||
function Utility.RequestAnimDict(dict)
|
||||
RequestAnimDict(dict)
|
||||
local count = 0
|
||||
while not HasAnimDictLoaded(dict) and count < 30000 do
|
||||
Wait(0)
|
||||
count = count + 1
|
||||
end
|
||||
return HasAnimDictLoaded(dict)
|
||||
end
|
||||
|
||||
---Remove a ped if it exists
|
||||
---@param entity number
|
||||
---@return boolean
|
||||
function Utility.RemovePed(entity)
|
||||
local success = false
|
||||
if DoesEntityExist(entity) then
|
||||
DeleteEntity(entity)
|
||||
end
|
||||
for i, storedEntity in ipairs(spawnedPeds) do
|
||||
if storedEntity == entity then
|
||||
table.remove(spawnedPeds, i)
|
||||
success = true
|
||||
break
|
||||
end
|
||||
end
|
||||
return success
|
||||
end
|
||||
|
||||
---Show a native input menu and return the result
|
||||
---@param text string
|
||||
---@param length number
|
||||
---@return string|boolean
|
||||
function Utility.NativeInputMenu(text, length)
|
||||
local maxLength = Math and Math.Clamp and Math.Clamp(length, 1, 50) or math.min(math.max(length or 10, 1), 50)
|
||||
local menuText = text or 'enter text'
|
||||
safeAddTextEntry(menuText)
|
||||
DisplayOnscreenKeyboard(1, menuText, "", "", "", "", "", maxLength)
|
||||
while (UpdateOnscreenKeyboard() == 0) do
|
||||
DisableAllControlActions(0)
|
||||
Wait(0)
|
||||
end
|
||||
if (GetOnscreenKeyboardResult()) then
|
||||
return GetOnscreenKeyboardResult()
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Get the skin data of a ped
|
||||
---@param entity number
|
||||
---@return table
|
||||
function Utility.GetEntitySkinData(entity)
|
||||
local skinData = { clothing = {}, props = {} }
|
||||
for i = 0, 11 do
|
||||
skinData.clothing[i] = { GetPedDrawableVariation(entity, i), GetPedTextureVariation(entity, i) }
|
||||
end
|
||||
for i = 0, 13 do
|
||||
skinData.props[i] = { GetPedPropIndex(entity, i), GetPedPropTextureIndex(entity, i) }
|
||||
end
|
||||
return skinData
|
||||
end
|
||||
|
||||
---Apply skin data to a ped
|
||||
---@param entity number
|
||||
---@param skinData table
|
||||
---@return boolean
|
||||
function Utility.SetEntitySkinData(entity, skinData)
|
||||
for i = 0, 11 do
|
||||
SetPedComponentVariation(entity, i, skinData.clothing[i][1], skinData.clothing[i][2], 0)
|
||||
end
|
||||
for i = 0, 13 do
|
||||
SetPedPropIndex(entity, i, skinData.props[i][1], skinData.props[i][2], 0)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Reload the player's skin and remove attached objects
|
||||
---@return boolean
|
||||
function Utility.ReloadSkin()
|
||||
local skinData = Utility.GetEntitySkinData(cache.ped)
|
||||
Utility.SetEntitySkinData(cache.ped, skinData)
|
||||
for _, props in pairs(GetGamePool("CObject")) do
|
||||
if IsEntityAttachedToEntity(cache.ped, props) then
|
||||
SetEntityAsMissionEntity(props, true, true)
|
||||
DeleteObject(props)
|
||||
DeleteEntity(props)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Show a native help text
|
||||
---@param text string
|
||||
---@param duration number
|
||||
function Utility.HelpText(text, duration)
|
||||
safeAddTextEntry(text)
|
||||
BeginTextCommandDisplayHelp(text)
|
||||
EndTextCommandDisplayHelp(0, false, true, duration or 5000)
|
||||
end
|
||||
|
||||
---Draw 3D help text in the world
|
||||
---@param coords vector3
|
||||
---@param text string
|
||||
---@param scale number
|
||||
function Utility.Draw3DHelpText(coords, text, scale)
|
||||
local onScreen, x, y = GetScreenCoordFromWorldCoord(coords.x, coords.y, coords.z)
|
||||
if onScreen then
|
||||
SetTextScale(scale or 0.35, scale or 0.35)
|
||||
SetTextFont(4)
|
||||
SetTextProportional(1)
|
||||
SetTextColour(255, 255, 255, 215)
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(1)
|
||||
AddTextComponentString(text)
|
||||
DrawText(x, y)
|
||||
local factor = (string.len(text)) / 370
|
||||
DrawRect(x, y + 0.0125, 0.015 + factor, 0.03, 41, 11, 41, 100)
|
||||
end
|
||||
end
|
||||
|
||||
---Show a native notification
|
||||
---@param text string
|
||||
function Utility.NotifyText(text)
|
||||
safeAddTextEntry(text)
|
||||
SetNotificationTextEntry(text)
|
||||
DrawNotification(false, true)
|
||||
end
|
||||
|
||||
---Teleport the player to given coordinates
|
||||
---@param coords vector3
|
||||
---@param conditionFunction function|nil
|
||||
---@param afterTeleportFunction function|nil
|
||||
function Utility.TeleportPlayer(coords, conditionFunction, afterTeleportFunction)
|
||||
if conditionFunction ~= nil then
|
||||
if not conditionFunction() then
|
||||
return
|
||||
end
|
||||
end
|
||||
DoScreenFadeOut(2500)
|
||||
Wait(2500)
|
||||
SetEntityCoords(cache.ped, coords.x, coords.y, coords.z, false, false, false, false)
|
||||
if coords.w then
|
||||
SetEntityHeading(cache.ped, coords.w)
|
||||
end
|
||||
FreezeEntityPosition(cache.ped, true)
|
||||
local count = 0
|
||||
while not HasCollisionLoadedAroundEntity(cache.ped) and count <= 30000 do
|
||||
RequestCollisionAtCoord(coords.x, coords.y, coords.z)
|
||||
Wait(0)
|
||||
count = count + 1
|
||||
end
|
||||
FreezeEntityPosition(cache.ped, false)
|
||||
DoScreenFadeIn(1000)
|
||||
if afterTeleportFunction ~= nil then
|
||||
afterTeleportFunction()
|
||||
end
|
||||
end
|
||||
|
||||
---Get the hash from a model
|
||||
---@param model string|number
|
||||
---@return number
|
||||
function Utility.GetEntityHashFromModel(model)
|
||||
return getModelHash(model)
|
||||
end
|
||||
|
||||
---Get the closest player to given coordinates
|
||||
---@param coords vector3|nil
|
||||
---@param distanceScope number|nil
|
||||
---@param includeMe boolean|nil
|
||||
---@return number, number, number
|
||||
function Utility.GetClosestPlayer(coords, distanceScope, includeMe)
|
||||
local players = GetActivePlayers()
|
||||
local closestPlayer = 0
|
||||
local selfPed = cache.ped
|
||||
local selfCoords = coords or GetEntityCoords(cache.ped)
|
||||
local closestDistance = distanceScope or 5
|
||||
|
||||
for _, player in ipairs(players) do
|
||||
local playerPed = GetPlayerPed(player)
|
||||
if includeMe or playerPed ~= selfPed then
|
||||
local playerCoords = GetEntityCoords(playerPed)
|
||||
local distance = #(selfCoords - playerCoords)
|
||||
if closestDistance == -1 or distance < closestDistance then
|
||||
closestPlayer = player
|
||||
closestDistance = distance
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return closestPlayer, closestDistance, GetPlayerServerId(closestPlayer)
|
||||
end
|
||||
|
||||
---Get the closest vehicle to given coordinates
|
||||
---@param coords vector3|nil
|
||||
---@param distanceScope number|nil
|
||||
---@param includePlayerVeh boolean|nil
|
||||
---@return number|nil, vector3|nil, number|nil
|
||||
function Utility.GetClosestVehicle(coords, distanceScope, includePlayerVeh)
|
||||
local vehicleEntity = nil
|
||||
local vehicleNetID = nil
|
||||
local vehicleCoords = nil
|
||||
local selfCoords = coords or GetEntityCoords(cache.ped)
|
||||
local closestDistance = distanceScope or 5
|
||||
local includeMyVeh = includePlayerVeh or false
|
||||
local gamePoolVehicles = GetGamePool("CVehicle")
|
||||
|
||||
local playerVehicle = IsPedInAnyVehicle(cache.ped, false) and GetVehiclePedIsIn(cache.ped, false) or 0
|
||||
|
||||
for i = 1, #gamePoolVehicles do
|
||||
local thisVehicle = gamePoolVehicles[i]
|
||||
if DoesEntityExist(thisVehicle) and (includeMyVeh or thisVehicle ~= playerVehicle) then
|
||||
local thisVehicleCoords = GetEntityCoords(thisVehicle)
|
||||
local distance = #(selfCoords - thisVehicleCoords)
|
||||
if closestDistance == -1 or distance < closestDistance then
|
||||
vehicleEntity = thisVehicle
|
||||
vehicleNetID = NetworkGetNetworkIdFromEntity(thisVehicle) or nil
|
||||
vehicleCoords = thisVehicleCoords
|
||||
closestDistance = distance
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return vehicleEntity, vehicleCoords, vehicleNetID
|
||||
end
|
||||
|
||||
-- Deprecated point functions (no changes)
|
||||
function Utility.RegisterPoint(pointID, pointCoords, pointDistance, _onEnter, _onExit, _nearby)
|
||||
return Point.Register(pointID, pointCoords, pointDistance, nil, _onEnter, _onExit, _nearby)
|
||||
end
|
||||
|
||||
function Utility.GetPointById(pointID)
|
||||
return Point.Get(pointID)
|
||||
end
|
||||
|
||||
function Utility.GetActivePoints()
|
||||
return Point.GetAll()
|
||||
end
|
||||
|
||||
function Utility.RemovePoint(pointID)
|
||||
return Point.Remove(pointID)
|
||||
end
|
||||
|
||||
---Simple switch-case function
|
||||
---@generic T
|
||||
---@param value T The value to match against the cases
|
||||
---@param cases table<T|false, fun(): any> Table with case functions and an optional default (false key)
|
||||
---@return any|false result The return value of the matched case function, or false if none matched
|
||||
function Utility.Switch(value, cases)
|
||||
local caseFunc = cases[value] or cases[false]
|
||||
|
||||
if caseFunc and type(caseFunc) == "function" then
|
||||
local ok, result = pcall(caseFunc)
|
||||
return ok and result or false
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Utility.CopyToClipboard(text)
|
||||
if not text then return false end
|
||||
if type(text) ~= "string" then
|
||||
text = json.encode(text, { indent = true })
|
||||
end
|
||||
SendNUIMessage({
|
||||
type = "copytoclipboard",
|
||||
text = text
|
||||
})
|
||||
local message = Locales and Locales.Locale("clipboard.copy")
|
||||
--TriggerEvent('community_bridge:Client:Notify', message, 'success')
|
||||
return true
|
||||
end
|
||||
|
||||
--- Pattern match-like function
|
||||
---@generic T
|
||||
---@param value T The value to match
|
||||
---@param patterns table<T|fun(T):boolean|false, fun(): any> A list of matchers and their handlers
|
||||
---@return any|false result The result of the first matched case, or false if none
|
||||
function Utility.Match(value, patterns)
|
||||
for pattern, handler in pairs(patterns) do
|
||||
if type(pattern) == "function" then
|
||||
local ok, matched = pcall(pattern, value)
|
||||
if ok and matched then
|
||||
local success, result = pcall(handler)
|
||||
return success and result or false
|
||||
end
|
||||
elseif pattern == value then
|
||||
local success, result = pcall(handler)
|
||||
return success and result or false
|
||||
end
|
||||
end
|
||||
|
||||
if patterns[false] then
|
||||
local ok, result = pcall(patterns[false])
|
||||
return ok and result or false
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Get zone name at coordinates
|
||||
---@param coords vector3
|
||||
---@return string
|
||||
function Utility.GetZoneName(coords)
|
||||
local zoneHash = GetNameOfZone(coords.x, coords.y, coords.z)
|
||||
return GetLabelText(zoneHash)
|
||||
end
|
||||
|
||||
local SpecialKeyCodes = {
|
||||
['b_116'] = 'Scroll Up',
|
||||
['b_115'] = 'Scroll Down',
|
||||
['b_100'] = 'LMB',
|
||||
['b_101'] = 'RMB',
|
||||
['b_102'] = 'MMB',
|
||||
['b_103'] = 'Extra 1',
|
||||
['b_104'] = 'Extra 2',
|
||||
['b_105'] = 'Extra 3',
|
||||
['b_106'] = 'Extra 4',
|
||||
['b_107'] = 'Extra 5',
|
||||
['b_108'] = 'Extra 6',
|
||||
['b_109'] = 'Extra 7',
|
||||
['b_110'] = 'Extra 8',
|
||||
['b_1015'] = 'AltLeft',
|
||||
['b_1000'] = 'ShiftLeft',
|
||||
['b_2000'] = 'Space',
|
||||
['b_1013'] = 'ControlLeft',
|
||||
['b_1002'] = 'Tab',
|
||||
['b_1014'] = 'ControlRight',
|
||||
['b_140'] = 'Numpad4',
|
||||
['b_142'] = 'Numpad6',
|
||||
['b_144'] = 'Numpad8',
|
||||
['b_141'] = 'Numpad5',
|
||||
['b_143'] = 'Numpad7',
|
||||
['b_145'] = 'Numpad9',
|
||||
['b_200'] = 'Insert',
|
||||
['b_1012'] = 'CapsLock',
|
||||
['b_170'] = 'F1',
|
||||
['b_171'] = 'F2',
|
||||
['b_172'] = 'F3',
|
||||
['b_173'] = 'F4',
|
||||
['b_174'] = 'F5',
|
||||
['b_175'] = 'F6',
|
||||
['b_176'] = 'F7',
|
||||
['b_177'] = 'F8',
|
||||
['b_178'] = 'F9',
|
||||
['b_179'] = 'F10',
|
||||
['b_180'] = 'F11',
|
||||
['b_181'] = 'F12',
|
||||
['b_194'] = 'ArrowUp',
|
||||
['b_195'] = 'ArrowDown',
|
||||
['b_196'] = 'ArrowLeft',
|
||||
['b_197'] = 'ArrowRight',
|
||||
['b_1003'] = 'Enter',
|
||||
['b_1004'] = 'Backspace',
|
||||
['b_198'] = 'Delete',
|
||||
['b_199'] = 'Escape',
|
||||
['b_1009'] = 'PageUp',
|
||||
['b_1010'] = 'PageDown',
|
||||
['b_1008'] = 'Home',
|
||||
['b_131'] = 'NumpadAdd',
|
||||
['b_130'] = 'NumpadSubstract',
|
||||
['b_211'] = 'Insert',
|
||||
['b_210'] = 'Delete',
|
||||
['b_212'] = 'End',
|
||||
['b_1055'] = 'Home',
|
||||
['b_1056'] = 'PageUp',
|
||||
}
|
||||
|
||||
local function translateKey(key)
|
||||
if string.find(key, "t_") then
|
||||
return string.gsub(key, "t_", "")
|
||||
elseif SpecialKeyCodes[key] then
|
||||
return SpecialKeyCodes[key]
|
||||
else
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
function Utility.GetCommandKey(commandName)
|
||||
local hash = GetHashKey(commandName) | 0x80000000
|
||||
local button = GetControlInstructionalButton(2, hash, true)
|
||||
if not button or button == "" or button == "NULL" then
|
||||
hash = GetHashKey(commandName)
|
||||
button = GetControlInstructionalButton(2, hash, true)
|
||||
end
|
||||
|
||||
return translateKey(button)
|
||||
end
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
for _, blip in pairs(blipIDs) do
|
||||
if blip and DoesBlipExist(blip) then
|
||||
RemoveBlip(blip)
|
||||
end
|
||||
end
|
||||
for _, ped in pairs(spawnedPeds) do
|
||||
if ped and DoesEntityExist(ped) then
|
||||
DeleteEntity(ped)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
exports('Utility', Utility)
|
||||
return Utility
|
|
@ -0,0 +1,143 @@
|
|||
local Callback = {}
|
||||
local CallbackRegistry = {}
|
||||
|
||||
-- Constants
|
||||
local RESOURCE = GetCurrentResourceName() or 'unknown'
|
||||
local EVENT_NAMES = {
|
||||
CLIENT_TO_SERVER = RESOURCE .. ':CS:Callback',
|
||||
SERVER_TO_CLIENT = RESOURCE .. ':SC:Callback',
|
||||
CLIENT_RESPONSE = RESOURCE .. ':CSR:Callback',
|
||||
SERVER_RESPONSE = RESOURCE .. ':SCR:Callback'
|
||||
}
|
||||
|
||||
-- Utility functions
|
||||
local function generateCallbackId(name)
|
||||
return string.format('%s_%d', name, math.random(1000000, 9999999))
|
||||
end
|
||||
|
||||
local function handleResponse(registry, name, callbackId, ...)
|
||||
local data = registry[callbackId]
|
||||
if not data then return end
|
||||
|
||||
if data.callback then
|
||||
data.callback(...)
|
||||
end
|
||||
|
||||
if data.promise then
|
||||
data.promise:resolve({ ... })
|
||||
end
|
||||
|
||||
registry[callbackId] = nil
|
||||
end
|
||||
|
||||
local function triggerCallback(eventName, target, name, args, callback)
|
||||
local callbackId = generateCallbackId(name)
|
||||
local promise = promise.new()
|
||||
|
||||
CallbackRegistry[callbackId] = {
|
||||
callback = callback,
|
||||
promise = promise
|
||||
}
|
||||
|
||||
if type(target) == 'table' then
|
||||
for _, id in ipairs(target) do
|
||||
TriggerClientEvent(eventName, tonumber(id), name, callbackId, table.unpack(args))
|
||||
end
|
||||
else
|
||||
TriggerClientEvent(eventName, tonumber(target), name, callbackId, table.unpack(args))
|
||||
end
|
||||
|
||||
if not callback then
|
||||
local result = Citizen.Await(promise)
|
||||
local returnResults = (result and type(result) == 'table' ) and result or {result}
|
||||
return table.unpack(returnResults)
|
||||
end
|
||||
end
|
||||
|
||||
-- Server-side implementation
|
||||
if IsDuplicityVersion() then
|
||||
function Callback.Register(name, handler)
|
||||
Callback[name] = handler
|
||||
end
|
||||
|
||||
function Callback.Trigger(name, target, ...)
|
||||
local args = { ... }
|
||||
local callback = type(args[1]) == 'function' and table.remove(args, 1) or nil
|
||||
return triggerCallback(EVENT_NAMES.SERVER_TO_CLIENT, target or -1, name, args, callback)
|
||||
end
|
||||
|
||||
RegisterNetEvent(EVENT_NAMES.CLIENT_TO_SERVER, function(name, callbackId, ...)
|
||||
if not name or not callbackId then return print(string.format("[%s] Warning: Invalid callback parameters - name: %s, callbackId: %s", RESOURCE, tostring(name), tostring(callbackId))) end
|
||||
|
||||
local handler = Callback[name]
|
||||
if not handler then return end
|
||||
|
||||
local playerId = source
|
||||
if not playerId or playerId == 0 then return print(string.format("[%s] Warning: Invalid source for callback '%s'", RESOURCE, name)) end
|
||||
|
||||
local result = table.pack(handler(playerId, ...))
|
||||
TriggerClientEvent(EVENT_NAMES.CLIENT_RESPONSE, playerId, name, callbackId, table.unpack(result))
|
||||
end)
|
||||
|
||||
RegisterNetEvent(EVENT_NAMES.SERVER_RESPONSE, function(name, callbackId, ...)
|
||||
handleResponse(CallbackRegistry, name, callbackId, ...)
|
||||
end)
|
||||
|
||||
-- Client-side implementation
|
||||
else
|
||||
local ClientCallbacks = {}
|
||||
local ReboundCallbacks = {}
|
||||
|
||||
function Callback.Register(name, handler)
|
||||
ClientCallbacks[name] = handler
|
||||
end
|
||||
|
||||
function Callback.RegisterRebound(name, handler)
|
||||
ReboundCallbacks[name] = handler
|
||||
end
|
||||
|
||||
function Callback.Trigger(name, ...)
|
||||
local args = { ... }
|
||||
local callback = type(args[1]) == 'function' and table.remove(args, 1) or nil
|
||||
|
||||
local callbackId = generateCallbackId(name)
|
||||
local promise = promise.new()
|
||||
|
||||
CallbackRegistry[callbackId] = {
|
||||
callback = callback,
|
||||
promise = promise
|
||||
}
|
||||
|
||||
TriggerServerEvent(EVENT_NAMES.CLIENT_TO_SERVER, name, callbackId, table.unpack(args))
|
||||
|
||||
if not callback then
|
||||
local result = Citizen.Await(promise)
|
||||
return table.unpack(result)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent(EVENT_NAMES.CLIENT_RESPONSE, function(name, callbackId, ...)
|
||||
if ReboundCallbacks[name] then
|
||||
ReboundCallbacks[name](...)
|
||||
end
|
||||
handleResponse(CallbackRegistry, name, callbackId, ...)
|
||||
end)
|
||||
|
||||
RegisterNetEvent(EVENT_NAMES.SERVER_TO_CLIENT, function(name, callbackId, ...)
|
||||
local handler = ClientCallbacks[name]
|
||||
if not handler then return end
|
||||
|
||||
local result = table.pack(handler(...))
|
||||
TriggerServerEvent(EVENT_NAMES.SERVER_RESPONSE, name, callbackId, table.unpack(result))
|
||||
end)
|
||||
end
|
||||
|
||||
-- Exports
|
||||
exports('Callback', Callback)
|
||||
exports('RegisterCallback', Callback.Register)
|
||||
exports('TriggerCallback', Callback.Trigger)
|
||||
if not IsDuplicityVersion() then
|
||||
exports('RegisterRebound', Callback.RegisterRebound)
|
||||
end
|
||||
|
||||
return Callback
|
|
@ -0,0 +1,72 @@
|
|||
Ids = Ids or {}
|
||||
|
||||
|
||||
---This will generate a unique id.
|
||||
---@param tbl table | nil
|
||||
---@param len number | nil
|
||||
---@param pattern string | nil
|
||||
---@return string
|
||||
Ids.CreateUniqueId = function(tbl, len, pattern) -- both optional
|
||||
tbl = tbl or {} -- table to check uniqueness. Ids to check against must be the key to the tables value
|
||||
len = len or 8
|
||||
|
||||
local id = ""
|
||||
for i = 1, len do
|
||||
local char = ""
|
||||
if pattern then
|
||||
local charIndex = math.random(1, #pattern)
|
||||
char = pattern:sub(charIndex, charIndex)
|
||||
else
|
||||
char = math.random(1, 2) == 1 and string.char(math.random(65, 90)) or math.random(0, 9) -- CAP letter and number
|
||||
end
|
||||
id = id .. char
|
||||
end
|
||||
if tbl[id] then
|
||||
return Ids.CreateUniqueId(tbl, len, pattern)
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
|
||||
---This will generate a unique id.
|
||||
---@param tbl table
|
||||
---@param len number
|
||||
---@return string
|
||||
Ids.RandomUpper = function(tbl, len)
|
||||
return Ids.CreateUniqueId(tbl, len, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
end
|
||||
|
||||
---This will generate a unique id.
|
||||
---@param tbl table
|
||||
---@param len number
|
||||
---@return string
|
||||
Ids.RandomLower = function(tbl, len)
|
||||
return Ids.CreateUniqueId(tbl, len, "abcdefghijklmnopqrstuvwxyz")
|
||||
end
|
||||
|
||||
---This will generate a unique id.
|
||||
---@param tbl table
|
||||
---@param len number
|
||||
---@return string
|
||||
Ids.RandomString = function(tbl, len)
|
||||
return Ids.CreateUniqueId(tbl, len, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
end
|
||||
|
||||
---This will generate a unique id.
|
||||
---@param tbl table
|
||||
---@param len number
|
||||
---@return string
|
||||
Ids.RandomNumber = function(tbl, len)
|
||||
return Ids.CreateUniqueId(tbl, len, "0123456789")
|
||||
end
|
||||
|
||||
---This will generate a unique id.
|
||||
---@param tbl table
|
||||
---@param len number
|
||||
---@return string
|
||||
Ids.Random = function(tbl, len)
|
||||
return Ids.CreateUniqueId(tbl, len)
|
||||
end
|
||||
|
||||
exports("Ids", Ids)
|
||||
return Ids
|
|
@ -0,0 +1,532 @@
|
|||
LA = LA or {}
|
||||
|
||||
--https://easings.net
|
||||
LA.Lerp = function(a, b, t)
|
||||
return a + (b - a) * t
|
||||
end
|
||||
|
||||
LA.LerpVector = function(a, b, t)
|
||||
return vector3(LA.Lerp(a.x, b.x, t), LA.Lerp(a.y, b.y, t), LA.Lerp(a.z, b.z, t))
|
||||
end
|
||||
|
||||
LA.EaseInSine = function(t)
|
||||
return 1 - math.cos((t * math.pi) / 2)
|
||||
end
|
||||
|
||||
LA.EaseOutSine = function(t)
|
||||
return math.sin((t * math.pi) / 2)
|
||||
end
|
||||
|
||||
LA.EaseInOutSine = function(t)
|
||||
return -(math.cos(math.pi * t) - 1) / 2
|
||||
end
|
||||
|
||||
LA.EaseInCubic = function(t)
|
||||
return t ^ 3
|
||||
end
|
||||
|
||||
LA.EaseOutCubic = function(t)
|
||||
return 1 - (1 - t) ^ 3
|
||||
end
|
||||
|
||||
LA.EaseInOutCubic = function(t)
|
||||
return t < 0.5 and 4 * t ^ 3 or 1 - ((-2 * t + 2) ^ 3) / 2
|
||||
end
|
||||
|
||||
LA.EaseInQuint = function(t)
|
||||
return t ^ 5
|
||||
end
|
||||
|
||||
LA.EaseOutQuint = function(t)
|
||||
return 1 - (1 - t) ^ 5
|
||||
end
|
||||
|
||||
LA.EaseInOutQuint = function(t)
|
||||
return t < 0.5 and 16 * t ^ 5 or 1 - ((-2 * t + 2) ^ 5) / 2
|
||||
end
|
||||
|
||||
LA.EaseInCirc = function(t)
|
||||
return 1 - math.sqrt(1 - t ^ 2)
|
||||
end
|
||||
|
||||
LA.EaseOutCirc = function(t)
|
||||
return math.sqrt(1 - (t - 1) ^ 2)
|
||||
end
|
||||
|
||||
LA.EaseInOutCirc = function(t)
|
||||
return t < 0.5 and (1 - math.sqrt(1 - (2 * t) ^ 2)) / 2 or (math.sqrt(1 - (-2 * t + 2) ^ 2) + 1) / 2
|
||||
end
|
||||
|
||||
LA.EaseInElastic = function(t)
|
||||
return t == 0 and 0 or t == 1 and 1 or -2 ^ (10 * t - 10) * math.sin((t * 10 - 10.75) * (2 * math.pi) / 3)
|
||||
end
|
||||
|
||||
LA.EaseOutElastic = function(t)
|
||||
return t == 0 and 0 or t == 1 and 1 or 2 ^ (-10 * t) * math.sin((t * 10 - 0.75) * (2 * math.pi) / 3) + 1
|
||||
end
|
||||
|
||||
LA.EaseInOutElastic = function(t)
|
||||
return t == 0 and 0 or t == 1 and 1 or t < 0.5 and -(2 ^ (20 * t - 10) * math.sin((20 * t - 11.125) * (2 * math.pi) / 4.5)) / 2 or (2 ^ (-20 * t + 10) * math.sin((20 * t - 11.125) * (2 * math.pi) / 4.5)) / 2 + 1
|
||||
end
|
||||
|
||||
LA.EaseInQuad = function(t)
|
||||
return t ^ 2
|
||||
end
|
||||
|
||||
LA.EaseOutQuad = function(t)
|
||||
return 1 - (1 - t) ^ 2
|
||||
end
|
||||
|
||||
LA.EaseInOutQuad = function(t)
|
||||
return t < 0.5 and 2 * t ^ 2 or 1 - (-2 * t + 2) ^ 2 / 2
|
||||
end
|
||||
|
||||
LA.EaseInQuart = function(t)
|
||||
return t ^ 4
|
||||
end
|
||||
|
||||
LA.EaseOutQuart = function(t)
|
||||
return 1 - (1 - t) ^ 4
|
||||
end
|
||||
|
||||
LA.EaseInOutQuart = function(t)
|
||||
return t < 0.5 and 8 * t ^ 4 or 1 - ((-2 * t + 2) ^ 4) / 2
|
||||
end
|
||||
|
||||
LA.EaseInExpo = function(t)
|
||||
return t == 0 and 0 or 2 ^ (10 * t - 10)
|
||||
end
|
||||
|
||||
LA.EaseOutExpo = function(t)
|
||||
return t == 1 and 1 or 1 - 2 ^ (-10 * t)
|
||||
end
|
||||
|
||||
LA.EaseInOutExpo = function(t)
|
||||
return t == 0 and 0 or t == 1 and 1 or t < 0.5 and 2 ^ (20 * t - 10) / 2 or (2 - 2 ^ (-20 * t + 10)) / 2
|
||||
end
|
||||
|
||||
LA.EaseInBack = function(t)
|
||||
return 2.70158 * t ^ 3 - 1.70158 * t ^ 2
|
||||
end
|
||||
|
||||
LA.EaseOutBack = function(t)
|
||||
return 1 + 2.70158 * (t - 1) ^ 3 + 1.70158 * (t - 1) ^ 2
|
||||
end
|
||||
|
||||
LA.EaseInOutBack = function(t)
|
||||
return t < 0.5 and (2 * t) ^ 2 * ((1.70158 + 1) * 2 * t - 1.70158) / 2 or ((2 * t - 2) ^ 2 * ((1.70158 + 1) * (t * 2 - 2) + 1.70158) + 2) / 2
|
||||
end
|
||||
|
||||
LA.EaseInBounce = function(t)
|
||||
print(1 - LA.EaseOutBounce(1 - t))
|
||||
return 1 - LA.EaseOutBounce(1 - t)
|
||||
end
|
||||
|
||||
LA.EaseOutBounce = function(t)
|
||||
if t < 1 / 2.75 then
|
||||
return 7.5625 * t ^ 2
|
||||
elseif t < 2 / 2.75 then
|
||||
return 7.5625 * (t - 1.5 / 2.75) ^ 2 + 0.75
|
||||
elseif t < 2.5 / 2.75 then
|
||||
return 7.5625 * (t - 2.25 / 2.75) ^ 2 + 0.9375
|
||||
else
|
||||
return 7.5625 * (t - 2.625 / 2.75) ^ 2 + 0.984375
|
||||
end
|
||||
end
|
||||
|
||||
LA.EaseInOutBounce = function(t)
|
||||
return t < 0.5 and (1 - LA.EaseOutBounce(1 - 2 * t)) / 2 or (1 + LA.EaseOutBounce(2 * t - 1)) / 2
|
||||
end
|
||||
|
||||
LA.EaseIn = function(t, easingType)
|
||||
easingType = string.lower(easingType)
|
||||
if easingType == "linear" then
|
||||
return t
|
||||
elseif easingType == "sine" then
|
||||
return LA.EaseInSine(t)
|
||||
elseif easingType == "cubic" then
|
||||
return LA.EaseInCubic(t)
|
||||
elseif easingType == "quint" then
|
||||
return LA.EaseInQuint(t)
|
||||
elseif easingType == "circ" then
|
||||
return LA.EaseInCirc(t)
|
||||
elseif easingType == "elastic" then
|
||||
return LA.EaseInElastic(t)
|
||||
elseif easingType == "quad" then
|
||||
return LA.EaseInQuad(t)
|
||||
elseif easingType == "quart" then
|
||||
return LA.EaseInQuart(t)
|
||||
elseif easingType == "expo" then
|
||||
return LA.EaseInExpo(t)
|
||||
elseif easingType == "back" then
|
||||
return LA.EaseInBack(t)
|
||||
elseif easingType == "bounce" then
|
||||
return LA.EaseInBounce(t)
|
||||
end
|
||||
end
|
||||
|
||||
LA.EaseOut = function(t, easingType)
|
||||
easingType = string.lower(easingType)
|
||||
if easingType == "linear" then
|
||||
return t
|
||||
elseif easingType == "sine" then
|
||||
return LA.EaseOutSine(t)
|
||||
elseif easingType == "cubic" then
|
||||
return LA.EaseOutCubic(t)
|
||||
elseif easingType == "quint" then
|
||||
return LA.EaseOutQuint(t)
|
||||
elseif easingType == "circ" then
|
||||
return LA.EaseOutCirc(t)
|
||||
elseif easingType == "elastic" then
|
||||
return LA.EaseOutElastic(t)
|
||||
elseif easingType == "quad" then
|
||||
return LA.EaseOutQuad(t)
|
||||
elseif easingType == "quart" then
|
||||
return LA.EaseOutQuart(t)
|
||||
elseif easingType == "expo" then
|
||||
return LA.EaseOutExpo(t)
|
||||
elseif easingType == "back" then
|
||||
return LA.EaseOutBack(t)
|
||||
elseif easingType == "bounce" then
|
||||
return LA.EaseOutBounce(t)
|
||||
end
|
||||
end
|
||||
|
||||
LA.EaseInOut = function(t, easingType)
|
||||
easingType = string.lower(easingType)
|
||||
if easingType == "linear" then
|
||||
return t
|
||||
elseif easingType == "sine" then
|
||||
return LA.EaseInOutSine(t)
|
||||
elseif easingType == "cubic" then
|
||||
return LA.EaseInOutCubic(t)
|
||||
elseif easingType == "quint" then
|
||||
return LA.EaseInOutQuint(t)
|
||||
elseif easingType == "circ" then
|
||||
return LA.EaseInOutCirc(t)
|
||||
elseif easingType == "elastic" then
|
||||
return LA.EaseInOutElastic(t)
|
||||
elseif easingType == "quad" then
|
||||
return LA.EaseInOutQuad(t)
|
||||
elseif easingType == "quart" then
|
||||
return LA.EaseInOutQuart(t)
|
||||
elseif easingType == "expo" then
|
||||
return LA.EaseInOutExpo(t)
|
||||
elseif easingType == "back" then
|
||||
return LA.EaseInOutBack(t)
|
||||
elseif easingType == "bounce" then
|
||||
return LA.EaseInOutBounce(t)
|
||||
end
|
||||
end
|
||||
|
||||
LA.EaseInVector = function(a, b, t, easingType)
|
||||
local tEase = LA.EaseIn(t, easingType)
|
||||
local x, y, z = a.x, a.y, a.z
|
||||
local x2, y2, z2 = b.x, b.y, b.z
|
||||
local x3, y3, z3 = x2 - x, y2 - y, z2 - z
|
||||
local x4, y4, z4 = x3 * tEase, y3 * tEase, z3 * tEase
|
||||
local x5, y5, z5 = x + x4, y + y4, z + z4
|
||||
return vector3(x5, y5, z5)
|
||||
end
|
||||
|
||||
LA.EaseOutVector = function(a, b, t, easingType)
|
||||
local tEase = LA.EaseOut(t, easingType)
|
||||
local x, y, z = a.x, a.y, a.z
|
||||
local x2, y2, z2 = b.x, b.y, b.z
|
||||
local x3, y3, z3 = x2 - x, y2 - y, z2 - z
|
||||
local x4, y4, z4 = x3 * tEase, y3 * tEase, z3 * tEase
|
||||
local x5, y5, z5 = x + x4, y + y4, z + z4
|
||||
return vector3(x5, y5, z5)
|
||||
end
|
||||
|
||||
LA.EaseInOutVector = function(a, b, t, easingType)
|
||||
local tEase = LA.EaseInOut(t, easingType)
|
||||
local x, y, z = a.x, a.y, a.z
|
||||
local x2, y2, z2 = b.x, b.y, b.z
|
||||
local x3, y3, z3 = x2 - x, y2 - y, z2 - z
|
||||
local x4, y4, z4 = x3 * tEase, y3 * tEase, z3 * tEase
|
||||
local x5, y5, z5 = x + x4, y + y4, z + z4
|
||||
return vector3(x5, y5, z5)
|
||||
end
|
||||
|
||||
LA.EaseVector = function(inout, a, b, t, easingType)
|
||||
inout = string.lower(inout)
|
||||
if inout == "in" then
|
||||
return LA.EaseInVector(a, b, t, easingType)
|
||||
elseif inout == "out" then
|
||||
return LA.EaseOutVector(a, b, t, easingType)
|
||||
elseif inout == "inout" then
|
||||
return LA.EaseInOutVector(a, b, t, easingType)
|
||||
end
|
||||
assert(false, "Invalid type")
|
||||
end
|
||||
|
||||
LA.EaseOnAxis = function(inout, a, b, t, easingType, axis)
|
||||
axis = axis or vector3(0, 0, 0)
|
||||
local x, y, z = a.x, a.y, a.z
|
||||
local increment = 0
|
||||
if inout == "in" then
|
||||
increment = LA.EaseIn(t, easingType)
|
||||
elseif inout == "out" then
|
||||
increment = LA.EaseOut(t, easingType)
|
||||
elseif inout == "inout" then
|
||||
increment = LA.EaseInOut(t, easingType)
|
||||
end
|
||||
return axis * increment
|
||||
end
|
||||
|
||||
LA.TestCheck = function(point1, point2)
|
||||
local dx = point1.x - point2.x
|
||||
local dy = point1.y - point2.y
|
||||
local dz = point1.z - point2.z
|
||||
return math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
end
|
||||
|
||||
LA.BoxZoneCheck = function(point, lower, upper)
|
||||
local x1, y1, z1 = lower.x, lower.y, lower.z
|
||||
local x2, y2, z2 = upper.x, upper.y, upper.z
|
||||
return point.x > x1 and point.x < x2 and point.y > y1 and point.y < y2 and point.z > z1 and point.z < z2
|
||||
end
|
||||
|
||||
LA.DistanceCheck = function(pointA, pointB)
|
||||
return #(pointA - pointB) < 0.5
|
||||
end
|
||||
|
||||
LA.Chance = function(chance)
|
||||
assert(chance, "Chance must be passed")
|
||||
assert(type(chance) == "number", "Chance must be a number")
|
||||
assert(chance >= 0 and chance <= 100, "Chance must be between 0 and 100")
|
||||
return math.random(1, 100) <= chance
|
||||
end
|
||||
|
||||
LA.Vector4To3 = function(vector4)
|
||||
assert(vector4, "Vector4 must be passed")
|
||||
assert(type(vector4) == "vector4", "Vector4 must be a vector4")
|
||||
return vector3(vector4.x, vector4.y, vector4.z), vector4.w
|
||||
end
|
||||
|
||||
LA.Dot = function(vectorA, vectorB)
|
||||
return vectorA.x * vectorB.x + vectorA.y * vectorB.y + vectorA.z * vectorB.z
|
||||
end
|
||||
|
||||
LA.Length = function(vector)
|
||||
return math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)
|
||||
end
|
||||
|
||||
LA.Normalize = function(vector)
|
||||
local length = LA.Length(vector)
|
||||
return vector3(vector.x / length, vector.y / length, vector.z / length)
|
||||
end
|
||||
|
||||
LA.Create2DRotationMatrix = function(angle) -- angle in radians
|
||||
local c = math.cos(angle)
|
||||
local s = math.sin(angle)
|
||||
return {
|
||||
c, -s, 0, 0,
|
||||
s, c, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
}
|
||||
end
|
||||
|
||||
LA.Create3DAxisRotationMatrix = function(vector)
|
||||
local yaw = vector.z
|
||||
local pitch = vector.x
|
||||
local roll = vector.y
|
||||
|
||||
local cy = math.cos(yaw)
|
||||
local sy = math.sin(yaw)
|
||||
local cp = math.cos(pitch)
|
||||
local sp = math.sin(pitch)
|
||||
local cr = math.cos(roll)
|
||||
local sr = math.sin(roll)
|
||||
|
||||
local matrix = {
|
||||
vector3(cp * cy, cp * sy, -sp),
|
||||
vector3(cy * sp * sr - cr * sy, sy * sp * sr + cr * cy, cp * sr),
|
||||
vector3(cy * sp * cr + sr * sy, cr * cy * sp - sr * sy, cp * cr)
|
||||
}
|
||||
|
||||
return matrix
|
||||
end
|
||||
|
||||
LA.Circle = function(t, radius, center)
|
||||
local x = radius * math.cos(t) + center.x
|
||||
local y = radius * math.sin(t) + center.y
|
||||
return vector3(x, y, center.z)
|
||||
end
|
||||
|
||||
LA.Clamp = function(value, min, max)
|
||||
return math.min(math.max(value, min), max)
|
||||
end
|
||||
|
||||
LA.CrossProduct = function(a, b)
|
||||
local x = a.y * b.z - a.z * b.y
|
||||
local y = a.z * b.x - a.x * b.z
|
||||
local z = a.x * b.y - a.y * b.x
|
||||
return vector3(x, y, z)
|
||||
end
|
||||
|
||||
LA.CreateTranslateMatrix = function(x, y, z)
|
||||
return {
|
||||
1, 0, 0, x,
|
||||
0, 1, 0, y,
|
||||
0, 0, 1, z,
|
||||
0, 0, 0, 1,
|
||||
}
|
||||
end
|
||||
|
||||
LA.MultiplyMatrix = function(m1, m2)
|
||||
local result = {}
|
||||
for i = 1, 4 do
|
||||
for j = 1, 4 do
|
||||
local sum = 0
|
||||
for k = 1, 4 do
|
||||
sum = sum + m1[(i - 1) * 4 + k] * m2[(k - 1) * 4 + j]
|
||||
end
|
||||
result[(i - 1) * 4 + j] = sum
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
LA.MultiplyMatrixByVector = function(m, v)
|
||||
local result = {}
|
||||
for i = 1, 4 do
|
||||
local sum = 0
|
||||
for j = 1, 4 do
|
||||
sum = sum + m[(i - 1) * 4 + j] * v[j]
|
||||
end
|
||||
result[i] = sum
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--Slines
|
||||
LA.SplineLerp = function(nodes, start, endPos, t)
|
||||
-- Ensure t is clamped between 0 and 1
|
||||
t = LA.Clamp(t, 0, 1)
|
||||
|
||||
-- Find the two closest nodes around t
|
||||
local prevNode, nextNode = nil, nil
|
||||
for i = 1, #nodes - 1 do
|
||||
if nodes[i].time <= t and nodes[i + 1].time >= t then
|
||||
prevNode, nextNode = nodes[i], nodes[i + 1]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Edge cases: If t is before or after the defined range
|
||||
if not prevNode then return start end
|
||||
if not nextNode then return endPos end
|
||||
|
||||
-- Normalize t within the segment
|
||||
local segmentT = (t - prevNode.time) / (nextNode.time - prevNode.time)
|
||||
|
||||
-- Interpolate the value (y-axis)
|
||||
local smoothedT = LA.Lerp(prevNode.value, nextNode.value, segmentT)
|
||||
|
||||
-- Perform final Lerp using the interpolated smoothing factor
|
||||
return LA.Lerp(start, endPos, smoothedT)
|
||||
end
|
||||
|
||||
LA.SplineSmooth = function(nodes, points, t)
|
||||
-- Ensure valid input
|
||||
if #points < 2 then return points end
|
||||
t = LA.Clamp(t, 0, 1)
|
||||
|
||||
local smoothedPoints = {}
|
||||
|
||||
for i = 1, #points - 1 do
|
||||
local smoothedPos = LA.SplineLerp(nodes, points[i], points[i + 1], t)
|
||||
table.insert(smoothedPoints, smoothedPos)
|
||||
end
|
||||
|
||||
-- Ensure last point remains unchanged
|
||||
table.insert(smoothedPoints, points[#points])
|
||||
|
||||
return smoothedPoints
|
||||
end
|
||||
|
||||
LA.Spline = function(points, resolution)
|
||||
-- Ensure valid input
|
||||
if #points < 2 then return points end
|
||||
|
||||
-- Create a list of nodes
|
||||
local nodes = {}
|
||||
for i = 1, #points do
|
||||
table.insert(nodes, { time = i / (#points - 1), value = i })
|
||||
end
|
||||
|
||||
-- Smooth the points
|
||||
local smoothedPoints = {}
|
||||
for i = 0, 1, resolution do
|
||||
local smoothedPos = LA.SplineSmooth(nodes, points, i)
|
||||
table.insert(smoothedPoints, smoothedPos)
|
||||
end
|
||||
|
||||
return smoothedPoints
|
||||
end
|
||||
|
||||
LA.SplineCatmullRom = function(points, resolution)
|
||||
-- Ensure valid input
|
||||
if #points < 4 then return points end
|
||||
|
||||
-- Create a list of nodes
|
||||
local nodes = {}
|
||||
for i = 1, #points do
|
||||
table.insert(nodes, { time = i / (#points - 1), value = i })
|
||||
end
|
||||
|
||||
-- Smooth the points
|
||||
local smoothedPoints = {}
|
||||
for i = 0, 1, resolution do
|
||||
local smoothedPos = LA.SplineSmooth(nodes, points, i)
|
||||
table.insert(smoothedPoints, smoothedPos)
|
||||
end
|
||||
|
||||
return smoothedPoints
|
||||
end
|
||||
|
||||
exports('LA', LA)
|
||||
return LA
|
||||
|
||||
-- local easingTypes = {
|
||||
-- "linear",
|
||||
-- "sine",
|
||||
-- "cubic",
|
||||
-- "quint",
|
||||
-- "circ",
|
||||
-- "elastic",
|
||||
-- "quad",
|
||||
-- "quart",
|
||||
-- "expo",
|
||||
-- "back",
|
||||
-- "bounce"
|
||||
-- }
|
||||
|
||||
--local inout = {
|
||||
-- "in",
|
||||
-- "out",
|
||||
-- "inout"
|
||||
--}
|
||||
|
||||
|
||||
-- LA.LerpAngle = function(a, b, t)
|
||||
-- local num = math.abs(b - a) % 360
|
||||
-- local num2 = 360 - num
|
||||
-- if num < num2 then
|
||||
-- return a + num * t
|
||||
-- else
|
||||
-- return a - num2 * t
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- LA.LerpVectorAngle = function(a, b, t)
|
||||
-- local x, y, z = a.x, a.y, a.z
|
||||
-- local x2, y2, z2 = b.x, b.y, b.z
|
||||
-- local x3, y3, z3 = LA.LerpAngle(x, x2, t), LA.LerpAngle(y, y2, t), LA.LerpAngle(z, z2, t)
|
||||
-- return vector3(x3, y3, z3)
|
||||
-- end
|
||||
|
||||
-- return LA
|
|
@ -0,0 +1,146 @@
|
|||
Math = Math or {}
|
||||
|
||||
function Math.Clamp(value, min, max)
|
||||
return math.min(math.max(value, min), max)
|
||||
end
|
||||
|
||||
function Math.Remap(value, min, max, newMin, newMax)
|
||||
return newMin + (value - min) / (max - min) * (newMax - newMin)
|
||||
end
|
||||
|
||||
function Math.PointInRadius(radius)
|
||||
local angle = math.rad(math.random(0, 360))
|
||||
return vector2(radius * math.cos(angle), radius * math.sin(angle))
|
||||
end
|
||||
|
||||
function Math.Normalize(value, min, max)
|
||||
if max == min then return 0 end -- Avoid division by zero
|
||||
return (value - min) / (max - min)
|
||||
end
|
||||
|
||||
function Math.Normalize2D(x, y)
|
||||
if type(x) == "vector2" then
|
||||
x, y = x.x, x.y
|
||||
end
|
||||
local length = math.sqrt(x*x + y*y)
|
||||
return length ~= 0 and vector2(x / length, y / length) or vector2(0, 0)
|
||||
end
|
||||
|
||||
function Math.Normalize3D(x, y, z)
|
||||
if type(x) == "vector3" then
|
||||
x, y, z = x.x, x.y, x.z
|
||||
end
|
||||
local length = math.sqrt(x*x + y*y + z*z)
|
||||
return length ~= 0 and vector3(x / length, y / length, z / length) or vector3(0, 0, 0)
|
||||
end
|
||||
|
||||
function Math.Normalize4D(x, y, z, w)
|
||||
if type(x) == "vector4" then
|
||||
x, y, z, w = x.x, x.y, x.z, x.w
|
||||
end
|
||||
local length = math.sqrt(x*x + y*y + z*z + w*w)
|
||||
return length ~= 0 and vector4(x / length, y / length, z / length, w / length) or vector4(0, 0, 0, 0)
|
||||
end
|
||||
|
||||
function Math.DirectionToTarget(fromV3, toV3)
|
||||
return Math.Normalize3D(toV3.x - fromV3.x, toV3.y - fromV3.y, toV3.z - fromV3.z)
|
||||
end
|
||||
|
||||
function Deg2Rad(deg)
|
||||
return deg * math.pi / 180.0
|
||||
end
|
||||
|
||||
function RotVector(pos, rot)
|
||||
local pitch = Deg2Rad(rot.x)
|
||||
local roll = Deg2Rad(rot.y)
|
||||
local yaw = Deg2Rad(rot.z)
|
||||
|
||||
local cosY = math.cos(yaw)
|
||||
local sinY = math.sin(yaw)
|
||||
local cosP = math.cos(pitch)
|
||||
local sinP = math.sin(pitch)
|
||||
local cosR = math.cos(roll)
|
||||
local sinR = math.sin(roll)
|
||||
|
||||
local m11 = cosY * cosR + sinY * sinP * sinR
|
||||
local m12 = sinR * cosP
|
||||
local m13 = -sinY * cosR + cosY * sinP * sinR
|
||||
|
||||
local m21 = -cosY * sinR + sinY * sinP * cosR
|
||||
local m22 = cosR * cosP
|
||||
local m23 = sinR * sinY + cosY * sinP * cosR
|
||||
|
||||
local m31 = sinY * cosP
|
||||
local m32 = -sinP
|
||||
local m33 = cosY * cosP
|
||||
|
||||
return vector3(pos.x * m11 + pos.y * m21 + pos.z * m31, pos.x * m12 + pos.y * m22 + pos.z * m32, pos.x * m13 + pos.y * m23 + pos.z * m33)
|
||||
end
|
||||
|
||||
|
||||
function Math.GetOffsetFromMatrix(position, rotation, offset)
|
||||
local rotated = RotVector(offset, rotation)
|
||||
print("Rotated: " .. tostring(rotated))
|
||||
return position + rotated
|
||||
end
|
||||
|
||||
function Math.InBoundary(pos, boundary)
|
||||
if not boundary then return true end
|
||||
|
||||
local x, y, z = table.unpack(pos)
|
||||
|
||||
-- Handle legacy min/max boundary format for backwards compatibility
|
||||
if boundary.min and boundary.max then
|
||||
local minX, minY, minZ = table.unpack(boundary.min)
|
||||
local maxX, maxY, maxZ = table.unpack(boundary.max)
|
||||
return x >= minX and x <= maxX and y >= minY and y <= maxY and z >= minZ and z <= maxZ
|
||||
end
|
||||
|
||||
-- Handle list of points (polygon boundary)
|
||||
if boundary.points and #boundary.points > 0 then
|
||||
local points = boundary.points
|
||||
local minZ = boundary.minZ or -math.huge
|
||||
local maxZ = boundary.maxZ or math.huge
|
||||
|
||||
-- Check Z bounds first
|
||||
if z < minZ or z > maxZ then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Point-in-polygon test using ray casting algorithm (improved version)
|
||||
local inside = false
|
||||
local n = #points
|
||||
|
||||
for i = 1, n do
|
||||
local j = i == n and 1 or i + 1 -- Next point (wrap around)
|
||||
|
||||
local xi, yi = points[i].x or points[i][1], points[i].y or points[i][2]
|
||||
local xj, yj = points[j].x or points[j][1], points[j].y or points[j][2]
|
||||
|
||||
-- Ensure xi, yi, xj, yj are numbers
|
||||
if not (xi and yi and xj and yj) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- Ray casting test
|
||||
if ((yi > y) ~= (yj > y)) then
|
||||
-- Calculate intersection point
|
||||
local intersect = (xj - xi) * (y - yi) / (yj - yi) + xi
|
||||
if x < intersect then
|
||||
inside = not inside
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
return inside
|
||||
end
|
||||
|
||||
-- Fallback to true if boundary format is not recognized
|
||||
return true
|
||||
end
|
||||
|
||||
exports('Math', Math)
|
||||
|
||||
return Math
|
|
@ -0,0 +1,110 @@
|
|||
local Perlin = {}
|
||||
|
||||
-- Permutation table for random gradients
|
||||
local perm = {}
|
||||
local grad3 = {
|
||||
{1,1,0}, {-1,1,0}, {1,-1,0}, {-1,-1,0},
|
||||
{1,0,1}, {-1,0,1}, {1,0,-1}, {-1,0,-1},
|
||||
{0,1,1}, {0,-1,1}, {0,1,-1}, {0,-1,-1}
|
||||
}
|
||||
|
||||
-- Initialize permutation table
|
||||
local function ShufflePermutation()
|
||||
local p = {}
|
||||
for i = 0, 255 do
|
||||
p[i] = i
|
||||
end
|
||||
for i = 255, 1, -1 do
|
||||
local j = math.random(i + 1) - 1
|
||||
p[i], p[j] = p[j], p[i]
|
||||
end
|
||||
for i = 0, 255 do
|
||||
perm[i] = p[i]
|
||||
perm[i + 256] = p[i] -- Repeat for wrapping
|
||||
end
|
||||
end
|
||||
|
||||
ShufflePermutation() -- Randomize on load
|
||||
|
||||
-- Dot product helper
|
||||
local function Dot(g, x, y, z)
|
||||
return g[1] * x + g[2] * y + (z and g[3] * z or 0)
|
||||
end
|
||||
|
||||
-- Fade function (smootherstep)
|
||||
local function Fade(t)
|
||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||
end
|
||||
|
||||
-- Linear interpolation
|
||||
local function Lerp(a, b, t)
|
||||
return a + (b - a) * t
|
||||
end
|
||||
|
||||
-- **Perlin Noise 1D**
|
||||
function Perlin.Noise1D(x)
|
||||
local X = math.floor(x) & 255
|
||||
x = x - math.floor(x)
|
||||
local u = Fade(x)
|
||||
|
||||
local a = perm[X]
|
||||
local b = perm[X + 1]
|
||||
|
||||
return Lerp(a, b, u) * (2 / 255) - 1
|
||||
end
|
||||
|
||||
-- **Perlin Noise 2D**
|
||||
function Perlin.Noise2D(x, y)
|
||||
local X = math.floor(x) & 255
|
||||
local Y = math.floor(y) & 255
|
||||
x, y = x - math.floor(x), y - math.floor(y)
|
||||
|
||||
local u, v = Fade(x), Fade(y)
|
||||
|
||||
local aa = perm[X] + Y
|
||||
local ab = perm[X] + Y + 1
|
||||
local ba = perm[X + 1] + Y
|
||||
local bb = perm[X + 1] + Y + 1
|
||||
|
||||
return Lerp(
|
||||
Lerp(Dot(grad3[perm[aa] % 12 + 1], x, y), Dot(grad3[perm[ba] % 12 + 1], x - 1, y), u),
|
||||
Lerp(Dot(grad3[perm[ab] % 12 + 1], x, y - 1), Dot(grad3[perm[bb] % 12 + 1], x - 1, y - 1), u),
|
||||
v
|
||||
)
|
||||
end
|
||||
|
||||
-- **Perlin Noise 3D**
|
||||
function Perlin.Noise3D(x, y, z)
|
||||
local X = math.floor(x) & 255
|
||||
local Y = math.floor(y) & 255
|
||||
local Z = math.floor(z) & 255
|
||||
x, y, z = x - math.floor(x), y - math.floor(y), z - math.floor(z)
|
||||
|
||||
local u, v, w = Fade(x), Fade(y), Fade(z)
|
||||
|
||||
local aaa = perm[X] + Y + Z
|
||||
local aba = perm[X] + Y + Z + 1
|
||||
local aab = perm[X] + Y + 1 + Z
|
||||
local abb = perm[X] + Y + 1 + Z + 1
|
||||
local baa = perm[X + 1] + Y + Z
|
||||
local bba = perm[X + 1] + Y + Z + 1
|
||||
local bab = perm[X + 1] + Y + 1 + Z
|
||||
local bbb = perm[X + 1] + Y + 1 + Z + 1
|
||||
|
||||
return Lerp(
|
||||
Lerp(
|
||||
Lerp(Dot(grad3[perm[aaa] % 12 + 1], x, y, z), Dot(grad3[perm[baa] % 12 + 1], x - 1, y, z), u),
|
||||
Lerp(Dot(grad3[perm[aab] % 12 + 1], x, y - 1, z), Dot(grad3[perm[bab] % 12 + 1], x - 1, y - 1, z), u),
|
||||
v
|
||||
),
|
||||
Lerp(
|
||||
Lerp(Dot(grad3[perm[aba] % 12 + 1], x, y, z - 1), Dot(grad3[perm[bba] % 12 + 1], x - 1, y, z - 1), u),
|
||||
Lerp(Dot(grad3[perm[abb] % 12 + 1], x, y - 1, z - 1), Dot(grad3[perm[bbb] % 12 + 1], x - 1, y - 1, z - 1), u),
|
||||
v
|
||||
),
|
||||
w
|
||||
)
|
||||
end
|
||||
|
||||
exports('Perlin', Perlin)
|
||||
return Perlin
|
|
@ -0,0 +1,34 @@
|
|||
Prints = Prints or {}
|
||||
local function printMessage(level, color, message)
|
||||
if type(message) == 'table' then
|
||||
message = json.encode(message)
|
||||
end
|
||||
print(color .. '[' .. level .. ']:', message)
|
||||
end
|
||||
|
||||
|
||||
---This will print a colored message to the console with the designated prefix.
|
||||
---@param message string
|
||||
Prints.Info = function(message)
|
||||
printMessage('INFO', '^5', message)
|
||||
end
|
||||
|
||||
---This will print a colored message to the console with the designated prefix.
|
||||
---@param message string
|
||||
Prints.Warn = function(message)
|
||||
printMessage('WARN', '^3', message)
|
||||
end
|
||||
|
||||
---This will print a colored message to the console with the designated prefix.
|
||||
---@param message string
|
||||
Prints.Error = function(message)
|
||||
printMessage('ERROR', '^1', message)
|
||||
end
|
||||
|
||||
---This will print a colored message to the console with the designated prefix.
|
||||
---@param message string
|
||||
Prints.Debug = function(message)
|
||||
printMessage('DEBUG', '^2', message)
|
||||
end
|
||||
|
||||
return Prints
|
|
@ -0,0 +1,324 @@
|
|||
local Entities = {}
|
||||
local isServer = IsDuplicityVersion()
|
||||
local registeredFunctions = {}
|
||||
ReboundEntities = {}
|
||||
|
||||
function ReboundEntities.AddFunction(func)
|
||||
assert(func, "Func is nil")
|
||||
table.insert(registeredFunctions, func)
|
||||
return #registeredFunctions
|
||||
end
|
||||
|
||||
function ReboundEntities.RemoveFunction(id)
|
||||
assert(id and registeredFunctions[id], "Invalid ID")
|
||||
registeredFunctions[id] = false
|
||||
end
|
||||
|
||||
function FireFunctions(...)
|
||||
for _, func in pairs(registeredFunctions) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function ReboundEntities.Register(entityData)
|
||||
assert(entityData and entityData.position, "Invalid entity data")
|
||||
entityData.id = entityData.id or Ids.CreateUniqueId(Entities)
|
||||
entityData.isServer = isServer
|
||||
setmetatable(entityData, { __tostring = function() return entityData.id end })
|
||||
Entities[entityData.id] = entityData
|
||||
FireFunctions(entityData)
|
||||
return entityData
|
||||
end
|
||||
|
||||
local function Unregister(id)
|
||||
Entities[id] = nil
|
||||
end
|
||||
|
||||
function ReboundEntities.GetSyncData(entityData, key)
|
||||
return entityData[key]
|
||||
end
|
||||
|
||||
function ReboundEntities.GetAll()
|
||||
return Entities
|
||||
end
|
||||
|
||||
function ReboundEntities.GetById(id)
|
||||
return Entities[tostring(id)]
|
||||
end
|
||||
|
||||
function ReboundEntities.GetByModel(model)
|
||||
local entities = {}
|
||||
for _, entity in pairs(Entities) do
|
||||
if entity.model == model then
|
||||
table.insert(entities, entity)
|
||||
end
|
||||
end
|
||||
return entities
|
||||
end
|
||||
|
||||
function ReboundEntities.GetClosest(pos)
|
||||
local closest, closestDist = nil, 9999
|
||||
for _, entity in pairs(Entities) do
|
||||
local dist = #(pos - entity.position)
|
||||
if dist < closestDist then
|
||||
closest, closestDist = entity, dist
|
||||
end
|
||||
end
|
||||
return closest
|
||||
end
|
||||
|
||||
function ReboundEntities.GetWithinRadius(pos, radius)
|
||||
local entities = {}
|
||||
for _, entity in pairs(Entities) do
|
||||
if #(pos - entity.position) < radius then
|
||||
table.insert(entities, entity)
|
||||
end
|
||||
end
|
||||
return entities
|
||||
end
|
||||
|
||||
function ReboundEntities.SetOnSyncKeyChange(entityData, cb)
|
||||
local data = ReboundEntities.GetById(entityData.id or entityData)
|
||||
assert(data, "Entity not found")
|
||||
data.onSyncKeyChange = cb
|
||||
end
|
||||
|
||||
exports('ReboundEntities', ReboundEntities)
|
||||
|
||||
if not isServer then goto client end
|
||||
|
||||
function ReboundEntities.Create(entityData, src)
|
||||
local entity = ReboundEntities.Register(entityData)
|
||||
assert(entity and entity.position, "Invalid entity data")
|
||||
entity.rotation = entity.rotation or vector3(0, 0, entityData.heading or 0)
|
||||
TriggerClientEvent(GetCurrentResourceName() .. ":client:CreateReboundEntity", src or -1, entity)
|
||||
return entity
|
||||
end
|
||||
|
||||
|
||||
function ReboundEntities.SetCheckRestricted(entityData, cb)
|
||||
assert(type(cb) == "function", "Check restricted is not a function")
|
||||
entityData.restricted = cb
|
||||
end
|
||||
|
||||
|
||||
function ReboundEntities.CheckRestricted(src, entityData)
|
||||
return entityData.restricted and entityData.restricted(tonumber(src), entityData) or false
|
||||
end
|
||||
|
||||
function ReboundEntities.Refresh(src)
|
||||
TriggerClientEvent(GetCurrentResourceName() .. ":client:CreateReboundEntities", tonumber(src), ReboundEntities.GetAccessibleList(src))
|
||||
end
|
||||
|
||||
function ReboundEntities.Delete(id)
|
||||
Unregister(id)
|
||||
TriggerClientEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntity", -1, id)
|
||||
end
|
||||
|
||||
function ReboundEntities.DeleteMultiple(entityDatas)
|
||||
local ids = {}
|
||||
for _, data in pairs(entityDatas) do
|
||||
Unregister(data.id)
|
||||
table.insert(ids, data.id)
|
||||
end
|
||||
TriggerClientEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntities", -1, ids)
|
||||
end
|
||||
|
||||
function ReboundEntities.CreateMultiple(entityDatas, src, restricted)
|
||||
local _entityDatas = {}
|
||||
for _, data in pairs(entityDatas) do
|
||||
local entity = ReboundEntities.Register(data)
|
||||
assert(entity and entity.position, "Invalid entity data")
|
||||
entity.rotation = entity.rotation or vector3(0, 0, 0)
|
||||
if restricted then ReboundEntities.SetCheckRestricted(entity, restricted) end
|
||||
if not ReboundEntities.CheckRestricted(src, entity) then
|
||||
_entityDatas[entity.id] = entity
|
||||
end
|
||||
end
|
||||
TriggerClientEvent(GetCurrentResourceName() .. ":client:CreateReboundEntities", src or -1, _entityDatas)
|
||||
return _entityDatas
|
||||
end
|
||||
|
||||
function ReboundEntities.SetSyncData(entityData, key, value)
|
||||
entityData[key] = value
|
||||
if entityData.onSyncKeyChange then
|
||||
entityData.onSyncKeyChange(entityData, key, value)
|
||||
end
|
||||
TriggerClientEvent(GetCurrentResourceName() .. ":client:SetReboundSyncData", -1, entityData.id, key, value)
|
||||
end
|
||||
|
||||
function ReboundEntities.GetAccessibleList(src)
|
||||
local list = {}
|
||||
for _, data in pairs(ReboundEntities.GetAll()) do
|
||||
if not ReboundEntities.CheckRestricted(src, data) then
|
||||
list[data.id] = data
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
RegisterNetEvent("playerJoining", function()
|
||||
ReboundEntities.Refresh(source)
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStart', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
Wait(1000)
|
||||
for _, src in pairs(GetPlayers()) do
|
||||
ReboundEntities.Refresh(src)
|
||||
end
|
||||
end)
|
||||
|
||||
::client::
|
||||
if isServer then return ReboundEntities end
|
||||
|
||||
function ReboundEntities.LoadModel(model)
|
||||
assert(model, "Model is nil")
|
||||
model = type(model) == "number" and model or GetHashKey(model) -- Corrected to GetHashKey
|
||||
RequestModel(model)
|
||||
for i = 1, 100 do
|
||||
if HasModelLoaded(model) then return model end
|
||||
Wait(100)
|
||||
end
|
||||
error(string.format("Failed to load model %s", model))
|
||||
end
|
||||
|
||||
function ReboundEntities.Spawn(entityData)
|
||||
local model = entityData.model
|
||||
local position = entityData.position
|
||||
assert(position, "Position is nil")
|
||||
local rotation = entityData.rotation or vector3(0, 0, 0)
|
||||
local entity = model and CreateObject(ReboundEntities.LoadModel(model), position.x, position.y, position.z, false, false, false)
|
||||
if entity then SetEntityRotation(entity, rotation.x, rotation.y, rotation.z) end
|
||||
if entityData.onSpawn then entity = entityData.onSpawn(entityData, entity) or entity end
|
||||
return entity, true
|
||||
end
|
||||
|
||||
function ReboundEntities.SetOnSpawn(id, cb)
|
||||
local entity = ReboundEntities.GetById(tostring(id))
|
||||
assert(entity, "Entity not found")
|
||||
entity.onSpawn = cb
|
||||
return true
|
||||
end
|
||||
|
||||
function ReboundEntities.Despawn(entityData)
|
||||
local entity = entityData.entity
|
||||
if entityData.onDespawn then entityData.onDespawn(entityData, entity) end
|
||||
if entity and DoesEntityExist(entity) then DeleteEntity(entity) end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
function ReboundEntities.SetOnDespawn(id, cb)
|
||||
local entity = ReboundEntities.GetById(tostring(id))
|
||||
assert(entity, "Entity not found")
|
||||
entity.onDespawn = cb
|
||||
return true
|
||||
end
|
||||
|
||||
local spawnLoopRunning = false
|
||||
|
||||
function ReboundEntities.SpawnLoop(distanceToCheck, waitTime)
|
||||
if spawnLoopRunning then return end
|
||||
spawnLoopRunning = true
|
||||
distanceToCheck = distanceToCheck or 50
|
||||
waitTime = waitTime or 2500
|
||||
CreateThread(function()
|
||||
while spawnLoopRunning do
|
||||
for _, entity in pairs(Entities) do
|
||||
local pos = GetEntityCoords(PlayerPedId())
|
||||
local position = vector3(entity.position.x, entity.position.y, entity.position.z)
|
||||
local dist = #(pos - position)
|
||||
if dist <= distanceToCheck and not entity.entity then
|
||||
entity.entity, entity.inRange = ReboundEntities.Spawn(entity)
|
||||
elseif dist > distanceToCheck and entity.entity then
|
||||
entity.entity, entity.inRange = ReboundEntities.Despawn(entity)
|
||||
end
|
||||
end
|
||||
Wait(waitTime)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function ReboundEntities.GetByEntity(entity)
|
||||
for _, data in pairs(Entities) do
|
||||
if data.entity == entity then return data end
|
||||
end
|
||||
end
|
||||
|
||||
function ReboundEntities.CreateClient(entityData)
|
||||
local entity = ReboundEntities.Register(entityData)
|
||||
if entity then ReboundEntities.SpawnLoop() end
|
||||
return entity
|
||||
end
|
||||
|
||||
function ReboundEntities.DeleteClient(id)
|
||||
local entityData = ReboundEntities.GetById(tostring(id))
|
||||
assert(not entityData.isServer, "Cannot delete server entity from client")
|
||||
if entityData.entity then ReboundEntities.Despawn(entityData) end
|
||||
Unregister(id)
|
||||
return true
|
||||
end
|
||||
|
||||
function ReboundEntities.CreateMultipleClient(entityDatas)
|
||||
local _entityDatas = {}
|
||||
for _, data in pairs(entityDatas) do
|
||||
local entityData = ReboundEntities.CreateClient(data)
|
||||
_entityDatas[entityData.id] = entityData
|
||||
end
|
||||
return _entityDatas
|
||||
end
|
||||
|
||||
function ReboundEntities.DeleteMultipleClient(ids)
|
||||
for _, id in pairs(ids) do
|
||||
ReboundEntities.DeleteClient(id)
|
||||
end
|
||||
end
|
||||
|
||||
local function DeleteFromServer(id)
|
||||
local entityData = ReboundEntities.GetById(tostring(id))
|
||||
if entityData then
|
||||
entityData.isServer = nil
|
||||
ReboundEntities.DeleteClient(id)
|
||||
end
|
||||
end
|
||||
|
||||
local function DeleteMultipleFromServer(ids)
|
||||
for _, id in pairs(ids) do
|
||||
DeleteFromServer(id)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent(GetCurrentResourceName() .. ":client:CreateReboundEntity", function(entityData)
|
||||
ReboundEntities.CreateClient(entityData)
|
||||
end)
|
||||
|
||||
RegisterNetEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntity", function(id)
|
||||
DeleteFromServer(id)
|
||||
end)
|
||||
|
||||
RegisterNetEvent(GetCurrentResourceName() .. ":client:CreateReboundEntities", function(entityDatas)
|
||||
ReboundEntities.CreateMultipleClient(entityDatas)
|
||||
end)
|
||||
|
||||
RegisterNetEvent(GetCurrentResourceName() .. ":client:DeleteReboundEntities", function(ids)
|
||||
ReboundEntities.DeleteMultipleClient(ids)
|
||||
end)
|
||||
|
||||
RegisterNetEvent(GetCurrentResourceName() .. ":client:SetReboundSyncData", function(id, key, value)
|
||||
local entityData = ReboundEntities.GetById(id)
|
||||
if not entityData then return end
|
||||
entityData[key] = value
|
||||
if entityData.onSyncKeyChange then
|
||||
entityData.onSyncKeyChange(entityData, key, value)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
spawnLoopRunning = false
|
||||
for _, entity in pairs(Entities) do
|
||||
if entity.entity then DeleteEntity(tonumber(entity.entity)) end
|
||||
end
|
||||
end)
|
||||
|
||||
return ReboundEntities
|
|
@ -0,0 +1,45 @@
|
|||
local ReboundEntities = ReboundEntities or Require("lib/utility/shared/rebound_entities.lua") -- Fixed path
|
||||
local Action = Action or Require("lib/entities/shared/actions.lua") -- Fixed path and name
|
||||
local LA = LA or Require("lib/utility/shared/la.lua") -- Fixed path
|
||||
|
||||
REA = {}
|
||||
|
||||
if not IsDuplicityVersion() then goto client end
|
||||
|
||||
|
||||
|
||||
if IsDuplicityVersion() then return end
|
||||
::client::
|
||||
|
||||
local ActionsInProgress = {}
|
||||
|
||||
|
||||
Action.Create("LerpToPosition", function(reboundId, start, position, duration, shouldRepeat, overrideStartTime)
|
||||
local re = ReboundEntities.GetById(reboundId) -- Use GetById
|
||||
assert(re, "Rebound entity not found")
|
||||
local entity = re and re.entity
|
||||
if not entity or not DoesEntityExist(entity) then return end -- Check existence
|
||||
local startTime = GetGameTimer()
|
||||
local endTime = overrideStartTime or startTime + duration
|
||||
ActionsInProgress[reboundId] = {reboundId, start, position, duration, shouldRepeat, startTime}
|
||||
local t = 0
|
||||
CreateThread(function()
|
||||
while shouldRepeat or t < 1 do
|
||||
-- Check if entity still exists
|
||||
if not re.entity or not DoesEntityExist(re.entity) then
|
||||
ActionsInProgress[reboundId] = nil
|
||||
break
|
||||
end
|
||||
entity = re.entity -- Update entity handle in case it changed (unlikely but safe)
|
||||
t = (GetGameTimer() - startTime) / duration
|
||||
local lerpPosition = LA.LerpVector(start, position, t)
|
||||
SetEntityCoordsNoOffset(entity, lerpPosition.x, lerpPosition.y, lerpPosition.z, false, false, false) -- Use NoOffset
|
||||
Wait(0)
|
||||
end
|
||||
-- Set final position if not repeating
|
||||
if not shouldRepeat and re.entity and DoesEntityExist(re.entity) then
|
||||
SetEntityCoordsNoOffset(re.entity, position.x, position.y, position.z, false, false, false)
|
||||
end
|
||||
ActionsInProgress[reboundId] = nil -- Clear when done
|
||||
end)
|
||||
end)
|
|
@ -0,0 +1,138 @@
|
|||
Table = {}
|
||||
|
||||
Table.CheckPopulated = function(tbl)
|
||||
if #tbl == 0 then
|
||||
for _, _ in pairs(tbl) do
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
Table.DeepClone = function(tbl, out, omit)
|
||||
if type(tbl) ~= "table" then return tbl end
|
||||
local new = out or {}
|
||||
omit = omit or {}
|
||||
for key, data in pairs(tbl) do
|
||||
if not omit[key] then
|
||||
if type(data) == "table" then
|
||||
new[key] = Table.DeepClone(data)
|
||||
else
|
||||
new[key] = data
|
||||
end
|
||||
end
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
Table.TableContains = function(tbl, search, nested)
|
||||
for _, v in pairs(tbl) do
|
||||
if nested and type(v) == "table" then
|
||||
return Table.TableContains(v, search)
|
||||
elseif v == search then
|
||||
return true, v
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
Table.TableContainsKey = function(tbl, search)
|
||||
for k, _ in pairs(tbl) do
|
||||
if k == search then
|
||||
return true, k
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
Table.TableGetKeys = function(tbl)
|
||||
local keys = {}
|
||||
for k ,_ in pairs(tbl) do
|
||||
table.insert(keys,k)
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
Table.GetClosest = function(coords, tbl)
|
||||
local closestPoint = nil
|
||||
local dist = math.huge
|
||||
for k, v in pairs(tbl) do
|
||||
local c = v.coords
|
||||
local d = c and #(coords - c)
|
||||
if d < dist then
|
||||
dist = d
|
||||
closestPoint = v
|
||||
end
|
||||
end
|
||||
return closestPoint
|
||||
end
|
||||
|
||||
Table.FindFirstUnoccupiedSlot = function(tbl)
|
||||
local occupiedSlots = {}
|
||||
for _, v in pairs(tbl) do
|
||||
if v.slot then
|
||||
occupiedSlots[v.slot] = true
|
||||
end
|
||||
end
|
||||
for i = 1, BridgeServerConfig.MaxInventorySlots do
|
||||
if not occupiedSlots[i] then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
Table.Append = function(tbl1, tbl2)
|
||||
for _, v in pairs(tbl2) do
|
||||
table.insert(tbl1, v)
|
||||
end
|
||||
return tbl1
|
||||
end
|
||||
|
||||
Table.Split = function(tbl, size)
|
||||
local new1 = {}
|
||||
local new2 = {}
|
||||
size = size or math.floor(#tbl / 2)
|
||||
|
||||
if size > #tbl then
|
||||
assert(false, "Size is greater than the length of the table.")
|
||||
end
|
||||
for i = 1, size do
|
||||
table.insert(new1, tbl[i])
|
||||
end
|
||||
for i = size + 1, #tbl do
|
||||
table.insert(new2, tbl[i])
|
||||
end
|
||||
return new1, new2
|
||||
end
|
||||
|
||||
Table.Shuffle = function(tbl)
|
||||
for i = #tbl, 2, -1 do
|
||||
local j = math.random(i)
|
||||
tbl[i], tbl[j] = tbl[j], tbl[i]
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
Table.Compare = function(a, b)
|
||||
if type(a) == "table" then
|
||||
for k, v in pairs(a) do
|
||||
if not Table.Compare(v, b[k]) then return false end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return a == b
|
||||
end
|
||||
end
|
||||
|
||||
Table.Count = function(tbl)
|
||||
local count = 0
|
||||
for _ in pairs(tbl) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
exports("Table", Table)
|
||||
return Table
|
Loading…
Add table
Add a link
Reference in a new issue