diff --git a/lib/shells/client/config.lua b/lib/shells/client/config.lua new file mode 100644 index 0000000..c259a1b --- /dev/null +++ b/lib/shells/client/config.lua @@ -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 \ No newline at end of file diff --git a/lib/shells/client/shells.lua b/lib/shells/client/shells.lua new file mode 100644 index 0000000..2022c4f --- /dev/null +++ b/lib/shells/client/shells.lua @@ -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) \ No newline at end of file diff --git a/lib/shells/server/shells.lua b/lib/shells/server/shells.lua new file mode 100644 index 0000000..349c5ab --- /dev/null +++ b/lib/shells/server/shells.lua @@ -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 ") + 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) \ No newline at end of file diff --git a/modules/target/_default/init.lua b/modules/target/_default/init.lua new file mode 100644 index 0000000..d7a1e3f --- /dev/null +++ b/modules/target/_default/init.lua @@ -0,0 +1,16 @@ +-- local Target = Require('modules/target/_default/init.lua') +local requiredModules = { + 'modules/target/_default/client.lua', + 'modules/target/qb-target/client.lua', + 'modules/target/ox_target/client.lua' +} + +local Module = nil +for _, path in pairs(requiredModules) do + local module = Require(path) + if module then + Module = module + end +end + +return Module or error('No target module found. Please ensure you have one of the required target modules installed: ' .. table.concat(requiredModules, ', ')) \ No newline at end of file diff --git a/modules/target/ox_target/client.lua b/modules/target/ox_target/client.lua index 5582f75..363c224 100644 --- a/modules/target/ox_target/client.lua +++ b/modules/target/ox_target/client.lua @@ -28,6 +28,7 @@ Target.FixOptions = function(options) return action(entityOrData) end options[k].onSelect = select + options[k].groups = v.job or v.groups end return options end