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)