This commit is contained in:
Nordi98 2025-08-06 16:37:06 +02:00
parent 510e3ffcf2
commit f43cf424cf
305 changed files with 34683 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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")

View 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

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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