diff --git a/fxmanifest.lua b/fxmanifest.lua index 526232f..939fe7e 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -4,10 +4,11 @@ lua54 'yes' use_experimental_fxv2_oal 'yes' author 'The Order of the Sacred Framework' description 'A bridge made for the community to use as a base for their own projects. This bridge will allow a single format to work with nearly all commonly used systems while still offering expandability.' -version '0.2.0' +version '0.2.5' shared_scripts { '@ox_lib/init.lua', + 'lib/init.lua', 'settings/sharedConfig.lua', 'use/*.lua', 'modules/utility/shared/*.lua', @@ -52,7 +53,10 @@ client_scripts { files { 'locales/*.json', - 'components/*.lua', + 'lib/client/*.lua', + 'lib/shared/*.lua', + 'lib/server/*.lua', + 'lib/init.lua', } dependencies { diff --git a/init.lua b/init.lua index df4016b..7c38a13 100644 --- a/init.lua +++ b/init.lua @@ -11,30 +11,59 @@ function Bridge.RegisterModule(moduleName, moduleTable) end local wrappedModule = {} + if type(moduleTable) == 'function' then + Bridge[moduleName] = wrappedModule + return + end for functionName, func in pairs(moduleTable) do wrappedModule[functionName] = func end print("Registering module:", moduleName) Bridge[moduleName] = wrappedModule + --trigger update object event + TriggerEvent("Bridge:Refresh", moduleName, wrappedModule) end +exports("RegisterModule", Bridge.RegisterModule) --TODO: Create a way to overide functions or create a new functions for module +function Bridge.RegisterModuleFunction(moduleName, functionName, func) + assert(moduleName and functionName and func, string.format("Bridge.RegisterModuleFunction(%s, %s, %s) - Invalid arguments", moduleName, functionName, func)) + Bridge[moduleName] = Bridge[moduleName] or {} + Bridge[moduleName][functionName] = func + --trigger update object event + TriggerEvent("Bridge:Refresh", moduleName, Bridge[moduleName]) +end + + +--Bridge Bridge.RegisterModule("Framework", Framework) Bridge.RegisterModule("Inventory", Inventory) Bridge.RegisterModule("Notify", Notify) -Bridge.RegisterModule("Utility", Utility) Bridge.RegisterModule("Clothing", Clothing) Bridge.RegisterModule("Language", Language) Bridge.RegisterModule("Doorlock", Doorlock) Bridge.RegisterModule("Phone", Phone) -Bridge.RegisterModule("Table", Table) -Bridge.RegisterModule("Math", Math) -Bridge.RegisterModule("Prints", Prints) -Bridge.RegisterModule("Callback", Callback) +Bridge.RegisterModule("Dispatch", Dispatch) +Bridge.RegisterModule("VehicleKey", VehicleKey) +Bridge.RegisterModule("Weather", Weather) +--lib +Bridge.RegisterModule("Table", cLib.Table) +Bridge.RegisterModule("Math", cLib.Math) +Bridge.RegisterModule("Prints", cLib.Prints) +Bridge.RegisterModule("Callback", cLib.Callback) + +--new +Bridge.RegisterModule("Require", Require) +Bridge.RegisterModule("Ids", cLib.Ids) +Bridge.RegisterModule("ReboundEntities", cLib.ReboundEntities) +Bridge.RegisterModule("LA", cLib.LA) +Bridge.RegisterModule("Perlin", cLib.Perlin) +Bridge.RegisterModule("Actions", cLib.Actions) CreateThread(function() + Wait(100) for moduleName, moduleFunction in pairs(Bridge) do if type(moduleFunction) == 'table' then exports(moduleName, function() @@ -50,17 +79,35 @@ exports('Bridge', function() return Bridge end) +-- ▄▀▀ ██▀ █▀▄ █ █ ██▀ █▀▄ +-- ▄█▀ █▄▄ █▀▄ ▀▄▀ █▄▄ █▀▄ +if not IsDuplicityVersion() then goto client end +Bridge.RegisterModule("SQL", cLib.SQL) +Bridge.RegisterModule("Logs", cLib.Logs) +Bridge.RegisterModule("LootTables", cLib.LootTables) + + +-- ▄▀▀ █ █ ██▀ █▄ █ ▀█▀ +-- ▀▄▄ █▄▄ █ █▄▄ █ ▀█ █ if IsDuplicityVersion() then return end +::client:: + -Bridge.RegisterModule("Dispatch", Dispatch) Bridge.RegisterModule("Fuel", Fuel) Bridge.RegisterModule("Input", Input) Bridge.RegisterModule("Progressbar", Progressbar) -Bridge.RegisterModule("VehicleKey", VehicleKey) -Bridge.RegisterModule("Weather", Weather) + Bridge.RegisterModule("Target", Target) Bridge.RegisterModule("Menu", Menu) +Bridge.RegisterModule("Utility", cLib.Utility) +Bridge.RegisterModule("Placeable", cLib.Placeable) +--new +-- Bridge.RegisterModule("Gizmo", cLib.Gizmo) +-- Bridge.RegisterModule("Scaleform", cLib.Scaleform) +-- Bridge.RegisterModule("PlaceableObject", cLib.PlaceableObject) +-- + -- Fill the bridge tables with player data. function FillBridgeTables() diff --git a/lib/client/markers.lua b/lib/client/markers.lua new file mode 100644 index 0000000..093974e --- /dev/null +++ b/lib/client/markers.lua @@ -0,0 +1,26 @@ +Marker = {} + +function Marker.DrawMarker(data) + local position = data.position or vector3(0.0, 0.0, 0.0) + local offset = data.offset or vector3(0.0, 0.0, 0.0) + local marker = data.marker or 1 + local pos = vector3(position.x + offset.x, position.y + offset.y, position.z + offset.z) + local rotation = data.rotation or vector3(0, 0, 0) + local size = data.size or vector3(1.0, 1.0, 1.0) + local color = data.color or vector3(0, 255, 0) + local alpha = data.alpha or 255 + local bobUpAndDown = data.bobUpAndDown + DrawMarker( + marker, + pos.x, pos.y, pos.z, + 0.0, 0.0, 0.0, + rotation.x, rotation.y, rotation.z, + size.x, size.y, size.z, + color.r, color.g, color.b, alpha, + bobUpAndDown, false, false + ) +end + +exports("Makrer", Marker) + +return Marker \ No newline at end of file diff --git a/lib/client/object_Placer.lua b/lib/client/object_Placer.lua new file mode 100644 index 0000000..5a59a7a --- /dev/null +++ b/lib/client/object_Placer.lua @@ -0,0 +1,110 @@ +Placeable = Placeable or {} +Utility = Utility or Require("lib/client/utility.lua") + +local activePlacementProp = nil +lib.locale() + +-- Object placer -- +local placementText = { + locale('place_object_place'), + locale('place_object_cancel'), + locale('place_object_scroll_up'), + locale('place_object_scroll_down') +} + +local function finishPlacing() + Bridge.Notify.HideHelpText() + if activePlacementProp == nil then return end + DeleteObject(activePlacementProp) + activePlacementProp = nil +end + +Placeable.PlaceObject = function(object, distance, snapToGround, allowedMats, offset) + if activePlacementProp then return end + + if not object then Prints.Error('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) or placementText, { + position = "left-center", + }) + + 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 \ No newline at end of file diff --git a/lib/client/scaleform.lua b/lib/client/scaleform.lua new file mode 100644 index 0000000..31763cd --- /dev/null +++ b/lib/client/scaleform.lua @@ -0,0 +1,53 @@ +Scaleform = {} +local function SetupScaleform(scaleform, Buttons) + local scaleform = RequestScaleformMovie(scaleform) + local timeout = 5000 + while not HasScaleformMovieLoaded(scaleform) and timeout > 0 do + timeout = timeout - 1 + Wait(0) + end + assert(timeout > 0, 'Scaleform failed to load') + + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 0, 0) + for i = 1,#Buttons do + PushScaleformMovieFunction(scaleform, Buttons[i].type) + if Buttons[i].int then PushScaleformMovieFunctionParameterInt(Buttons[i].int) end + if Buttons[i].keyIndex then + if type(Buttons[i].keyIndex) == "table" then + for _, v in pairs(Buttons[i].keyIndex) do N_0xe83a3e3557a56640(GetControlInstructionalButton(2, v, true)) end + else + ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(2, Buttons[i].keyIndex[1], true)) + end + end + if Buttons[i].name then + BeginTextCommandScaleformString("STRING") + AddTextComponentScaleform(Buttons[i].name) + EndTextCommandScaleformString() + end + if Buttons[i].type == 'SET_BACKGROUND_COLOUR' then + for u = 1,4 do PushScaleformMovieFunctionParameterInt(80) end + end + PopScaleformMovieFunctionVoid() + end + return scaleform +end + +function Scaleform.SetupInstructionalButtons(buttons) + buttons = buttons or { + -- {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"}, + } + local scaleform = SetupScaleform("instructional_buttons", buttons) + return scaleform +end + +exports("Scaleform", Scaleform) +return Scaleform \ No newline at end of file diff --git a/lib/client/utility.lua b/lib/client/utility.lua new file mode 100644 index 0000000..b9e0121 --- /dev/null +++ b/lib/client/utility.lua @@ -0,0 +1,291 @@ +Utility = Utility or {} +local blipIDs = {} +local spawnedPeds = {} +local ActivePoints = {} + +Utility.CreateProp = function(model, coords, heading, networked) + if networked == nil then networked = true end + Utility.LoadModel(model) + if not HasModelLoaded(model) then return Prints.Error("Model Has Not Loaded") end + local propEntity = CreateObject(model, coords.x, coords.y, coords.z, networked, false, false) + SetEntityHeading(propEntity, heading) + SetModelAsNoLongerNeeded(model) + return propEntity +end + +Utility.CreateVehicle = function(model, coords, heading, networked) + if networked == nil then networked = true end + Utility.LoadModel(model) + if not HasModelLoaded(model) then return Prints.Error("Model Has Not Loaded") end + local vehicle = CreateVehicle(model, coords.x, coords.y, coords.z, heading, networked, false) + SetVehicleHasBeenOwnedByPlayer(vehicle, true) + SetVehicleNeedsToBeHotwired(vehicle, false) + SetModelAsNoLongerNeeded(model) + SetVehRadioStation(vehicle, "OFF") + return vehicle, { networkid = NetworkGetNetworkIdFromEntity(vehicle) or 0, coords = GetEntityCoords(vehicle), heading = GetEntityHeading(vehicle), } +end + +Utility.CreatePed = function(model, coords, heading, networked, clothing) + if networked == nil then networked = true end + Utility.LoadModel(model) + if not HasModelLoaded(model) then return Prints.Error("Model Has Not Loaded") end + local spawnedEntity = CreatePed(0, model, coords.x, coords.y, coords.z, heading, networked, false) + if clothing then + for componentID, v in pairs(clothing) do + SetPedComponentVariation(spawnedEntity, componentID, v[1], v[2], 0) + end + end + FreezeEntityPosition(spawnedEntity, true) + SetEntityInvincible(spawnedEntity, true) + SetBlockingOfNonTemporaryEvents(spawnedEntity, true) + SetModelAsNoLongerNeeded(model) + table.insert(spawnedPeds, spawnedEntity) + return spawnedEntity +end + +Utility.StartBusySpinner = function(text) + AddTextEntry(text, text) + BeginTextCommandBusyString(text) + AddTextComponentSubstringPlayerName(text) + EndTextCommandBusyString(0) +end + +Utility.StopBusySpinner = function() + if BusyspinnerIsOn() then BusyspinnerOff() end +end + +Utility.CreateBlip = function(coords, sprite, color, scale, label, shortRange) + 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) + SetBlipAsShortRange(blip, shortRange) + AddTextEntry(label, label) + BeginTextCommandSetBlipName(label) + EndTextCommandSetBlipName(blip) + table.insert(blipIDs, blip) + return blip +end + +Utility.RemoveBlip = function(blip) + local success = false + for i, storedBlip in ipairs(blipIDs) do + if storedBlip == blip then + RemoveBlip(blip) + table.remove(blipIDs, i) + success = true + break + end + end + return success +end + +Utility.LoadModel = function(model) + if type(model) ~= 'number' then model = joaat(model) end + if not IsModelValid(model) and not IsModelInCdimage(model) then return false end + RequestModel(model) + local count = 0 + while not HasModelLoaded(model) and count < 30000 do + Wait(0) + count = count + 1 + end + return HasModelLoaded(model) +end + +Utility.RemovePed = function(entity) + local success = false + DeleteEntity(entity) + for i, storedEntity in ipairs(spawnedPeds) do + if storedEntity == entity then + table.remove(spawnedPeds, i) + success = true + break + end + end + return success +end + +Utility.NativeInputMenu = function(text, length) + local maxLength = Math.Clamp(length, 1, 50) + local menutText = text or 'enter text' + AddTextEntry(menutText, menutText) + DisplayOnscreenKeyboard(1, menutText, "", "", "", "", "", maxLength) + while(UpdateOnscreenKeyboard() == 0) do + DisableAllControlActions(0) + Wait(0) + end + if(GetOnscreenKeyboardResult()) then + local result = GetOnscreenKeyboardResult() + return result + end + return false +end + +Utility.GetEntitySkinData = function(entity) + local skinData = {} + skinData.clothing = {} + for i = 0, 11 do + skinData.clothing[i] = {GetPedDrawableVariation(entity, i), GetPedTextureVariation(entity, i)} + end + return skinData +end + +Utility.SetEntitySkinData = function(entity, skinData) + for i = 0, 11 do + SetPedComponentVariation(entity, i, skinData.clothing[i][1], skinData.clothing[i][2], 0) + end + return true +end + +Utility.NativeAlert = function(data) + local message = data.message or 'No message provided' + local coords = data.coords or GetEntityCoords(cache.ped) + local job = data.job + local scale = data.scale or 0.8 + local sprite = data.sprite or 1 + TriggerServerEvent('community_bridge:server:alert', {message = message, coords = coords, job = job, scale = scale, sprite = sprite}) +end + +Utility.ReloadSkin = function() + 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 + +Utility.RestoreAppearance = function(data) + return Utility.SetEntitySkinData(cache.ped, data) +end + +Utility.HelpText = function(text, duration) + AddTextEntry(text, text) + BeginTextCommandDisplayHelp(text) + EndTextCommandDisplayHelp(0, false, true, duration or 5000) +end + +Utility.NotifyText = function(text) + AddTextEntry(text, text) + SetNotificationTextEntry(text) + DrawNotification(false, true) +end + +Utility.TeleportPlayer = function(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 + +Utility.GetEntityHashFromModel = function(model) + if type(model) ~= 'number' then model = joaat(model) end + return model +end + +Utility.GetClosestPlayer = function(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 + +-- register points +Utility.RegisterPoint = function(pointCoords, pointDistance, pointOnEnter, pointOnExit) + local enterZone = lib.points.new({ + coords = pointCoords, + distance = pointDistance, + onEnter = function(self) + pointOnEnter(self) + end, + onExit = function(self) + pointOnExit(self) + end, + }) + local pointID = Utility.CreateUniqueId(nil, 5, nil) + ActivePoints[pointID] = enterZone + return enterZone, pointID +end + +-- Function to retrieve the enterZone using pointID +Utility.GetPointById = function(pointID) + return ActivePoints[pointID] +end + +Utility.RemovePoint = function(pointID) + local point = ActivePoints[pointID] + if point then + --print("Removing point with ID " .. pointID) + point:remove() -- Call the remove method on the point (ensure this method stops all event listeners) + ActivePoints[pointID] = nil -- Remove from ActivePoints table + --print("Point removed successfully.") + return true + else + --print("Point with ID " .. pointID .. " not found.") + return false + end +end + +AddEventHandler('onResourceStop', function(resource) + if resource == GetCurrentResourceName() then + for _, blip in pairs(blipIDs) do + if DoesBlipExist(blip.id) then + RemoveBlip(blip.id) + end + end + for _, ped in pairs(spawnedPeds) do + if DoesEntityExist(ped) then + DeleteEntity(ped) + end + end + end +end) + +RegisterNetEvent('community_bridge:client:alert', function(data) + local blip = Utility.CreateBlip(data.coords, data.sprite, 3, data.scale, data.message, true) + SetTimeout(40000, function() + Utility.RemoveBlip(blip) + end) +end) + +exports('Utility', Utility) + +return Utility \ No newline at end of file diff --git a/lib/client/wip_gizmo.lua b/lib/client/wip_gizmo.lua new file mode 100644 index 0000000..2348b1c --- /dev/null +++ b/lib/client/wip_gizmo.lua @@ -0,0 +1,458 @@ +-- Credit: https://github.com/citizenfx/lua/blob/luaglm-dev/cfx/libs/scripts/examples/dataview.lua +local dataView = setmetatable({ + EndBig = ">", + EndLittle = "<", + Types = { + Int8 = { code = "i1" }, + Uint8 = { code = "I1" }, + Int16 = { code = "i2" }, + Uint16 = { code = "I2" }, + Int32 = { code = "i4" }, + Uint32 = { code = "I4" }, + Int64 = { code = "i8" }, + Uint64 = { code = "I8" }, + Float32 = { code = "f", size = 4 }, -- a float (native size) + Float64 = { code = "d", size = 8 }, -- a double (native size) + + LuaInt = { code = "j" }, -- a lua_Integer + UluaInt = { code = "J" }, -- a lua_Unsigned + LuaNum = { code = "n" }, -- a lua_Number + String = { code = "z", size = -1, }, -- zero terminated string + }, + + FixedTypes = { + String = { code = "c" }, -- a fixed-sized string with n bytes + Int = { code = "i" }, -- a signed int with n bytes + Uint = { code = "I" }, -- an unsigned int with n bytes + }, +}, { + __call = function(_, length) + return dataView.ArrayBuffer(length) + end +}) +dataView.__index = dataView + +--[[ Create an ArrayBuffer with a size in bytes --]] +function dataView.ArrayBuffer(length) + return setmetatable({ + blob = string.blob(length), + length = length, + offset = 1, + cangrow = true, + }, dataView) +end + +--[[ Wrap a non-internalized string --]] +function dataView.Wrap(blob) + return setmetatable({ + blob = blob, + length = blob:len(), + offset = 1, + cangrow = true, + }, dataView) +end + +--[[ Return the underlying bytebuffer --]] +function dataView:Buffer() return self.blob end +function dataView:ByteLength() return self.length end +function dataView:ByteOffset() return self.offset end +function dataView:SubView(offset, length) + return setmetatable({ + blob = self.blob, + length = length or self.length, + offset = 1 + offset, + cangrow = false, + }, dataView) +end + +--[[ Return the Endianness format character --]] +local function ef(big) return (big and dataView.EndBig) or dataView.EndLittle end + +--[[ Helper function for setting fixed datatypes within a buffer --]] +local function packblob(self, offset, value, code) + -- If cangrow is false the dataview represents a subview, i.e., a subset + -- of some other string view. Ensure the references are the same before + -- updating the subview + local packed = self.blob:blob_pack(offset, code, value) + if self.cangrow or packed == self.blob then + self.blob = packed + self.length = packed:len() + return true + else + return false + end +end + +--[[ + Create the API by using dataView.Types +--]] +for label,datatype in pairs(dataView.Types) do + if not datatype.size then -- cache fixed encoding size + datatype.size = string.packsize(datatype.code) + elseif datatype.size >= 0 and string.packsize(datatype.code) ~= datatype.size then + local msg = "Pack size of %s (%d) does not match cached length: (%d)" + error(msg:format(label, string.packsize(datatype.code), datatype.size)) + return nil + end + + dataView["Get" .. label] = function(self, offset, endian) + offset = offset or 0 + if offset >= 0 then + local o = self.offset + offset + local v,_ = self.blob:blob_unpack(o, ef(endian) .. datatype.code) + return v + end + return nil + end + + dataView["Set" .. label] = function(self, offset, value, endian) + if offset >= 0 and value then + local o = self.offset + offset + local v_size = (datatype.size < 0 and value:len()) or datatype.size + if self.cangrow or ((o + (v_size - 1)) <= self.length) then + if not packblob(self, o, value, ef(endian) .. datatype.code) then + error("cannot grow subview") + end + else + error("cannot grow dataview") + end + end + return self + end +end + +for label,datatype in pairs(dataView.FixedTypes) do + datatype.size = -1 -- Ensure cached encoding size is invalidated + + dataView["GetFixed" .. label] = function(self, offset, typelen, endian) + if offset >= 0 then + local o = self.offset + offset + if (o + (typelen - 1)) <= self.length then + local code = ef(endian) .. "c" .. tostring(typelen) + local v,_ = self.blob:blob_unpack(o, code) + return v + end + end + return nil -- Out of bounds + end + + dataView["SetFixed" .. label] = function(self, offset, typelen, value, endian) + if offset >= 0 and value then + local o = self.offset + offset + if self.cangrow or ((o + (typelen - 1)) <= self.length) then + local code = ef(endian) .. "c" .. tostring(typelen) + if not packblob(self, o, value, code) then + error("cannot grow subview") + end + else + error("cannot grow dataview") + end + end + return self + end +end + + +-- CREDITS +-- Andyyy7666: https://github.com/overextended/ox_lib/pull/453 +-- AvarianKnight: https://forum.cfx.re/t/allow-drawgizmo-to-be-used-outside-of-fxdk/5091845/8?u=demi-automatic +local dataview = dataView +local enableScale = false -- allow scaling mode. doesnt scale collisions and resets when physics are applied it seems +local gizmoEnabled = false +local currentMode = 'translate' +local isRelative = false +local currentEntity + +-- FUNCTIONS + +local function normalize(x, y, z) + local length = math.sqrt(x * x + y * y + z * z) + if length == 0 then + return 0, 0, 0 + end + return x / length, y / length, z / length +end + +local identityMatrix = { + vector3(1, 0, 0), + vector3(0, 1, 0), + vector3(0, 0, 1) +} +local function makeEntityMatrix(entity) + local f, r, u, a = GetEntityMatrix(entity) + local view = dataview.ArrayBuffer(60) + + view:SetFloat32(0, r[1]) + :SetFloat32(4, r[2]) + :SetFloat32(8, r[3]) + :SetFloat32(12, 0) + :SetFloat32(16, f[1]) + :SetFloat32(20, f[2]) + :SetFloat32(24, f[3]) + :SetFloat32(28, 0) + :SetFloat32(32, u[1]) + :SetFloat32(36, u[2]) + :SetFloat32(40, u[3]) + :SetFloat32(44, 0) + :SetFloat32(48, a[1]) + :SetFloat32(52, a[2]) + :SetFloat32(56, a[3]) + :SetFloat32(60, 1) + + return view +end + +local function applyEntityMatrix(entity, view) + local x1, y1, z1 = view:GetFloat32(16), view:GetFloat32(20), view:GetFloat32(24) + local x2, y2, z2 = view:GetFloat32(0), view:GetFloat32(4), view:GetFloat32(8) + local x3, y3, z3 = view:GetFloat32(32), view:GetFloat32(36), view:GetFloat32(40) + local tx, ty, tz = view:GetFloat32(48), view:GetFloat32(52), view:GetFloat32(56) + + if not enableScale then + x1, y1, z1 = normalize(x1, y1, z1) + x2, y2, z2 = normalize(x2, y2, z2) + x3, y3, z3 = normalize(x3, y3, z3) + end + + SetEntityMatrix(entity, + x1, y1, z1, + x2, y2, z2, + x3, y3, z3, + tx, ty, tz + ) +end + +-- LOOPS + +function RegisterButtonHandler(button, onPressed, onReleased) + onPressed = onPressed and type(onPressed) == 'function' and onPressed + onReleased = onReleased and type(onReleased) == 'function' and onReleased + DisableControlAction(0, button, true) + local pressed = IsDisabledControlJustPressed(0, button) + local released = IsDisabledControlJustReleased(0, button) + return (pressed and onPressed and onPressed()) or (released and onReleased and onReleased()) +end + + +local function gizmoLoop(entity, onConfirm, onUpdate, onCancel) + entity = tonumber(entity) + EnterCursorMode() + + if IsEntityAPed(entity) then + SetEntityAlpha(entity, 200) + else + SetEntityDrawOutline(entity, true) + end + + while gizmoEnabled and DoesEntityExist(entity) do + Wait(0) + + DisableControlAction(0, 24, true) -- lmb + DisableControlAction(0, 25, true) -- rmb + DisableControlAction(0, 140, true) -- r + DisableControlAction(0, 245, true) --t + DisablePlayerFiring(PlayerId(), true) + SetEntityCollision(entity, false, false) + FreezeEntityPosition(entity, false) + local matrixBuffer = makeEntityMatrix(entity) + local changed = DrawGizmo(matrixBuffer:Buffer(), 'Editor1') + if changed then + print(GetEntityCoords(entity)) + applyEntityMatrix(entity, matrixBuffer) + SetEntityCoords(entity, GetEntityCoords(entity)) + end + + if onUpdate and type(onUpdate) == 'function' then onUpdate(entity, GetEntityCoords(entity), GetEntityRotation(entity)) end + --controles + --frontend confirm 18 [ENTER] + RegisterButtonHandler(191, function() + if not gizmoEnabled then return end + gizmoEnabled = false + if onConfirm and type(onConfirm) == 'function' then onConfirm(entity, GetEntityCoords(entity), GetEntityRotation(entity)) end + end) + --frontend cancel 202 [BACKSPACE] + RegisterButtonHandler(202, function() + if not gizmoEnabled then return end + gizmoEnabled = false + if onCancel and type(onCancel) == 'function' then onCancel(entity, GetEntityCoords(entity), GetEntityRotation(entity)) end + CreateThread(function() + local timeout = 100 + while timeout > 0 do + DisableFrontendThisFrame() + timeout = timeout - 1 + Wait(1) + end + end) + end) + --register e + RegisterButtonHandler(38, function() + if not gizmoEnabled then return end + gizmoEnabled = false + local playerPos = GetEntityCoords(PlayerPedId()) + local entityPos = GetFinalRenderedCamCoord() + local depth = #(playerPos - entityPos) -- Vector3 distance + PlaceableObject.Create({spawned = entity}, nil, onConfirm, onUpdate, onCancel, {depth = depth}) + end) + + --ctrl + RegisterButtonHandler(36, function() + if not gizmoEnabled then return end + LeaveCursorMode() + end, function() + EnterCursorMode() + end) + + RegisterButtonHandler(19, function() + if not gizmoEnabled then return end + PlaceObjectOnGroundProperly_2(currentEntity) + end) + FreezeEntityPosition(entity, false) + end + + LeaveCursorMode() + + if DoesEntityExist(entity) then + if IsEntityAPed(entity) then SetEntityAlpha(entity, 255) end + SetEntityDrawOutline(entity, false) + end + + gizmoEnabled = false + currentEntity = nil + return true +end + +-- local function textUILoop() +-- CreateThread(function() +-- while gizmoEnabled do +-- Wait(100) +-- local scaleText = (enableScale and '[S] - Scale Mode \n') or '' +-- lib.showTextUI( +-- ('Current Mode: %s | %s \n'):format(currentMode, (isRelative and 'Relative') or 'World') .. +-- '[W] - Translate Mode \n' .. +-- '[R] - Rotate Mode \n' .. +-- scaleText .. +-- '[Q] - Relative/World \n' .. +-- '[LALT] - Snap To Ground \n' .. +-- '[ENTER] - Done Editing \n' +-- ) +-- end +-- lib.hideTextUI() +-- end) +-- end + +-- EXPORTS + + +local function useGizmo(entity, onConfirm, onUpdate, onCancel) + gizmoEnabled = true + currentEntity = entity + -- textUILoop() + gizmoLoop(entity, onConfirm, onUpdate, onCancel) + + return { + handle = entity, + position = GetEntityCoords(entity), + rotation = GetEntityRotation(entity) + } +end + +exports("useGizmo", useGizmo) + +Gizmo = {} +local running = false +function Gizmo.UseGizmo(obj, onConfirm, onUpdate, onCancel) + if running then return end + running = true + + --instructional buttons + local scaleform = Scaleform.SetupInstructionalButtons({ + {type = "CLEAR_ALL"}, + {type = "SET_CLEAR_SPACE", int = 200}, + {type = "SET_DATA_SLOT", name = 'Translate:', keyIndex = {245}, int = 1}, + {type = "SET_DATA_SLOT", name = 'Scale:', keyIndex = {33}, int = 2}, + {type = "SET_DATA_SLOT", name = 'Rotate:', keyIndex = {45}, int = 3}, + {type = "SET_DATA_SLOT", name = 'Snap to Ground:', keyIndex = {19}, int = 4}, + {type = "SET_DATA_SLOT", name = 'Toggle Camera:', keyIndex = {36}, int = 5}, + {type = "SET_DATA_SLOT", name = 'Cancel Placement:', keyIndex = {202}, int = 6}, + {type = "SET_DATA_SLOT", name = 'Toggle Gizmo:', keyIndex = {38}, int = 7}, + {type = "DRAW_INSTRUCTIONAL_BUTTONS"}, + {type = "SET_BACKGROUND_COLOUR"}, + }) + local quit = false + CreateThread(function() + while not quit do + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) + Wait(0) + end + running = false + end) + + quit = useGizmo(obj, onConfirm, onUpdate, onCancel) + running = false +end +exports("UseGizmo", Gizmo.UseGizmo) + + +--got rid of the cancer +-- lib.addKeybind({ +-- name = '_gizmoLocal', +-- description = 'toggle gizmo to be local to the entity instead of world', +-- defaultKey = 'Q', +-- onPressed = function(self) +-- if not gizmoEnabled then return end +-- isRelative = not isRelative +-- ExecuteCommand('+gizmoLocal') +-- end, +-- onReleased = function (self) +-- ExecuteCommand('-gizmoLocal') +-- end +-- }) + +-- lib.addKeybind({ +-- name = 'gizmoclose', +-- description = 'close gizmo', +-- defaultKey = 'RETURN', +-- onPressed = function(self) +-- if not gizmoEnabled then return end +-- gizmoEnabled = false +-- end, +-- }) + +-- lib.addKeybind({ +-- name = 'gizmoSnapToGround', +-- description = 'snap current gizmo object to floor/surface', +-- defaultKey = 'LMENU', +-- onPressed = function(self) +-- if not gizmoEnabled then return end +-- PlaceObjectOnGroundProperly_2(currentEntity) +-- end, +-- }) + +-- if enableScale then +-- lib.addKeybind({ +-- name = '_gizmoScale', +-- description = 'Sets mode for the gizmo to scale', +-- defaultKey = 'S', +-- onPressed = function(self) +-- if not gizmoEnabled then return end +-- currentMode = 'Scale' +-- ExecuteCommand('+gizmoScale') +-- end, +-- onReleased = function (self) +-- ExecuteCommand('-gizmoScale') +-- end +-- }) +-- end + + +--onreousrcestop + +AddEventHandler('onResourceStop', function(resource) + if resource == GetCurrentResourceName() then + if not gizmoEnabled then return end + gizmoEnabled = false + LeaveCursorMode() + end +end) + +return Gizmo \ No newline at end of file diff --git a/lib/client/wip_object_placer.lua b/lib/client/wip_object_placer.lua new file mode 100644 index 0000000..a849e6c --- /dev/null +++ b/lib/client/wip_object_placer.lua @@ -0,0 +1,168 @@ +Scaleform = Scaleform or Require("client/scaleform.lua") +Utility = Utility or Require("client/utility.lua") +Gizmo = Gizmo or Require("client/gizmo.lua") + +PlaceableObject = PlaceableObject or {} +local data = { + entity = nil, + placing = false, +} + +function PlaceableObject.Eyetrace(depth, disableSphere) + local screenX = GetDisabledControlNormal(0, 239) + local screenY = GetDisabledControlNormal(0, 240) + + local world, normal = GetWorldCoordFromScreenCoord(screenX, screenY) + local playerPos = GetEntityCoords(PlayerPedId()) + local target = playerPos + normal * depth + if not disableSphere then + DrawSphere(target.x, target.y, target.z, 0.5, 255, 0, 0, 0.5) + end + return target +end + +-- Creates a placeable object in the game world. +-- @param model (string) The model of the object to create. +-- @param onConfirm (function) [optional] The callback function to execute when the object is confirmed. +-- @param onUpdate (function) [optional] The callback function to execute when the object is moved. +-- @param onCancel (function) [optional] The callback function to execute when the object is canceled. +-- @param settings (table) [optional] Additional settings for the placeable object. +-- - depthMin (number) The minimum depth for the object placement. Default is 2.0. +-- - depthMax (number) The maximum depth for the object placement. Default is 10.0. +-- - rotationStep (number) The rotation step for the object. Default is 15.0. +-- - depthStep (number) The depth step for the object. Default is 1.0. +function PlaceableObject.Create(data, model, onConfirm, onUpdate, onCancel, settings, config) + local settings = settings or {} + local depthMin = settings.depthMin or 2.0 + depthMax = settings.depthMax or 10.0 + rotationStep = settings.rotationStep or 15.0 + depthStep = settings.depthStep or 1.0 + local depth = settings.depth or 10.0 + print(depth) + data.placing = true + data.paused = false + local initialPos = nil + local placeOnGround = true + + --scaleform + local scaleform = Scaleform.SetupInstructionalButtons({ + {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 = "SET_DATA_SLOT", name = config?.gizmo?.name or 'Toggle Gizmo:', keyIndex = config?.gizmo?.key or {38}, int = 6}, + {type = "DRAW_INSTRUCTIONAL_BUTTONS"}, + {type = "SET_BACKGROUND_COLOUR"}, + }) + CreateThread(function() + local heading = -GetEntityHeading(PlayerPedId()) + + local pos = PlaceableObject.Eyetrace(depth) + if not data.spawned and model then + --get player heading + local obj = Utility.CreateProp(model, pos, vector3(0, 0, heading), nil) + data.spawned = obj + end + assert(data.spawned, "Failed to create object") + SetEntityCollision(data.spawned, false, false) + SetEntityAlpha(data.spawned, 150, false) + + SetPedConfigFlag(PlayerPedId(), 146, true) + SetCanClimbOnEntity(data.spawned, false) + SetEntityCompletelyDisableCollision(data.spawned, true, false) + SetEntityNoCollisionEntity(PlayerPedId(), data.spawned, false) + + while data.placing do + if data.paused then goto continue end + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) + DisableControlAction(0, 24, true) -- Attack + DisableControlAction(0, 25, true) -- Aim + DisableControlAction(0, 36, true) -- INPUT_DUCK. + + if IsControlJustPressed(0, 223) then -- left click + if onConfirm and type(onConfirm) == 'function' then onConfirm(data.spawned, pos, heading) end + data.placing = false + end + + if IsControlJustPressed(0, 222) then -- right click + data.placing = false + if onCancel and type(onCancel) == 'function' then + onCancel(data.spawned, pos, heading) + else + DeleteEntity(data.spawned) + end + return + end + + if IsControlPressed(0,19) then + placeOnGround = not (placeOnGround or false) + end + + if IsControlJustPressed(0, 38) then -- TAB key to switch to gizmo + data.placing = false + return Gizmo.UseGizmo(data.spawned, onConfirm, onUpdate, onCancel) + end + + if IsControlPressed(0,224) then -- left ctrl + if IsControlJustPressed(0, 261) then -- scroll up + depth = depth + depthStep + if depth > depthMax then + depth = depthMax + end + end + + if IsControlJustPressed(0, 262) then -- scroll down + depth = depth - depthStep + if depth < depthMin then + depth = depthMin + end + end + else + if IsControlJustPressed(0, 261) then -- scroll up + heading = heading + rotationStep + if heading > 360 then + heading = 0 + end + end + if IsControlJustPressed(0, 262) then -- scroll down + heading = heading - rotationStep + if heading < 0 then + heading = 360 + end + end + end + + pos = PlaceableObject.Eyetrace(depth) + if data.spawned then + SetEntityCoords(data.spawned, pos.x, pos.y, pos.z) + SetEntityHeading(data.spawned, heading) + if placeOnGround then + PlaceObjectOnGroundProperly(data.spawned) + pos = GetEntityCoords(data.spawned) + end + end + if onUpdate and type(onUpdate) == 'function' then onUpdate(data.spawned, pos, heading) end + ::continue:: + if IsControlJustPressed(0, 199) then -- home + data.paused = not data.paused + end + Wait(1) + end + end) +end + + + +RegisterCommand("testers", function() + PlaceableObject.Create({}, "prop_beachflag_01", function(entity, pos, heading) + + end, function(entity, pos, heading) + + end, function(entity, pos, heading) + + end) +end) diff --git a/lib/init.lua b/lib/init.lua new file mode 100644 index 0000000..38721aa --- /dev/null +++ b/lib/init.lua @@ -0,0 +1,66 @@ +loadedModules = {} + +function Require(modulePath, resourceName) + if resourceName and type(resourceName) ~= "string" then + resourceName = GetCurrentResourceName() + end + + if not resourceName then + resourceName = "community_bridge" + end + + local id = resourceName .. ":" .. modulePath + if loadedModules[id] then + print("Returning cached module [" .. id .. "]") + 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/shared/callbacks.lua"), + Ids = Ids or Require("lib/shared/ids.lua"), + ReboundEntities = ReboundEntities or Require("lib/shared/rebound_entities.lua"), + Tables = Tables or Require("lib/shared/tables.lua"), + Math = Math or Require("lib/shared/math.lua"), + LA = LA or Require("lib/shared/la.lua"), + Perlin = Perlin or Require("lib/shared/perlin.lua"), + Actions = Actions or Require("lib/shared/actions.lua"), +} + +exports('cLib', cLib) + +if not IsDuplicityVersion() then goto client end + +cLib.SQL = SQL or Require("lib/server/sqlHandler.lua") +cLib.Logs = Logs or Require("lib/server/logs.lua") +cLib.LootTables = LootTables or Require("lib/server/lootTables.lua") + +if IsDuplicityVersion() then return cLib end +::client:: + +-- cLib.Gizmo = Gizmo or Require("lib/client/gizmo.lua") +-- cLib.Scaleform = Scaleform or Require("lib/client/scaleform.lua") +cLib.Placeable = Placeable or Require("lib/client/object_placer.lua") +cLib.Utility = Utility or Require("lib/client/utility.lua") +-- cLib.PlaceableObject = PlaceableObject or Require("lib/client/object_placer.lua") + + +return cLib \ No newline at end of file diff --git a/lib/server/logs.lua b/lib/server/logs.lua new file mode 100644 index 0000000..5049854 --- /dev/null +++ b/lib/server/logs.lua @@ -0,0 +1,53 @@ +Logs = Logs or {} + +local WebhookURL = "" +local LogoForEmbed = "" + +Logs.Send = function(src, message) + if not src or not message then return end + if BridgeServerConfig.LogSystem == "builtin" then + PerformHttpRequest(WebhookURL, function(err, text, headers) end, 'POST', json.encode( + { + username = "Community_Bridge's Logger", + avatar_url = 'https://cdn.discordapp.com/avatars/299410129982455808/31ce635662206e8bd0132c34ce9ce683?size=1024', + embeds = { + { + color = "15769093", + title = GetCurrentResourceName(), + url = 'https://discord.gg/wsPAK5mEPg', + --description = message, + 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 | "..GetCurrentResourceName(), + icon_url = 'https://cdn.discordapp.com/avatars/299410129982455808/31ce635662206e8bd0132c34ce9ce683?size=1024', + }, + } + } + }), { ['Content-Type']= 'application/json' }) + elseif BridgeServerConfig.LogSystem == "qb" then + return TriggerEvent('qb-log:server:CreateLog', GetCurrentResourceName(), GetCurrentResourceName(), 'green', message) + elseif BridgeServerConfig.LogSystem == "ox" then + return exports.ox_lib:logger(src, GetCurrentResourceName(), message) + end +end + +exports('Logs', Logs) +return Logs \ No newline at end of file diff --git a/lib/server/lootTables.lua b/lib/server/lootTables.lua new file mode 100644 index 0000000..89c3147 --- /dev/null +++ b/lib/server/lootTables.lua @@ -0,0 +1,73 @@ +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 \ No newline at end of file diff --git a/lib/server/sqlHandler.lua b/lib/server/sqlHandler.lua new file mode 100644 index 0000000..05843ea --- /dev/null +++ b/lib/server/sqlHandler.lua @@ -0,0 +1,66 @@ +SQL = {} + +Require("lib/MySQL.lua", "oxmysql") + +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', SQL) + +return SQL \ No newline at end of file diff --git a/lib/shared/actions.lua b/lib/shared/actions.lua new file mode 100644 index 0000000..8ee1308 --- /dev/null +++ b/lib/shared/actions.lua @@ -0,0 +1,47 @@ +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 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) + + + diff --git a/lib/shared/callbacks.lua b/lib/shared/callbacks.lua new file mode 100644 index 0000000..7b23e0e --- /dev/null +++ b/lib/shared/callbacks.lua @@ -0,0 +1,146 @@ +Callbacks = {} +cbData = {} +Callback = Callback or {} + +local resource = GetCurrentResourceName() or 'unknown' + +local clientToServerName = resource .. ':server:CS:TriggerCallback' +local clientToServerBackToClientName = resource .. ':client:CSC:TriggerCallback' + + +local serverToClientName = resource .. ':client:SC:TriggerCallback' +local serverToClientBackToServerName = resource .. ':server:SCS:TriggerCallback' + + +exports('Callback', Callback) + +if not IsDuplicityVersion() then goto client end + +-- -- -- -- -- -- -- -- -- -- +-- ▄▀▀ ██▀ █▀▄ █ █ ██▀ █▀▄ +-- ▄█▀ █▄▄ █▀▄ ▀▄▀ █▄▄ █▀▄ +-- -- -- -- -- -- -- -- -- -- + + + +-- client to server rebound +function Callback.Register(name, callback, all) + Callback[name] = { + callback = callback, + toAll = all + } +end + +RegisterNetEvent(clientToServerName, function(name, ...) + local src = source + local data = Callback[name] + if not data then return end + local func = data.callback + if not func then return end + local toAll = data.toAll and -1 or src + local cb = function(...) + TriggerClientEvent(clientToServerBackToClientName, toAll, name, ...) + end + func(src, cb, ...) +end) +----------------------------- + +-- server to client rebound + +function Callback.Trigger(name, src, func, ...) + if src == nil then src = -1 end + local p = promise.new() + cbData[name] = {cb = func, p = p} + if type(src) == 'table' then + for i, d in pairs(src) do + TriggerClientEvent(serverToClientName, tonumber(d), name, ...) + end + return Citizen.Await(p) + end + TriggerClientEvent(serverToClientName, tonumber(src), name, ...) + return Citizen.Await(p) +end + +RegisterNetEvent(serverToClientBackToServerName, function(name, ...) + print('Rebound') + local data = cbData[name] + if not data then return end + local func = data.cb + if not func then return end + data.p:resolve(func(...) or true) + cbData[name] = nil +end) +----------------------------- +exports('RegisterCallback', Callback.Register) +exports('TriggerCallback', Callback.Trigger) + + +-- -- -- -- -- -- -- -- -- -- +-- ▄▀▀ █ █ ██▀ █▄ █ ▀█▀ +-- ▀▄▄ █▄▄ █ █▄▄ █ ▀█ █ +-- -- -- -- -- -- -- -- -- -- +if IsDuplicityVersion() then return Callback end +::client:: + + +cbRebounds = {} +function Callback.Register(name, callback, all) + Callback[name] = { + callback = callback, + } +end + + +function Callback.RegisterRebound(name, callback) + cbRebounds[name] = callback +end + + +-- client to server rebound +function Callback.Trigger(name, func, ...) + local p = promise.new() + TriggerServerEvent(clientToServerName, name, ...) + cbData[name] = {cb = func, p = p} + CreateThread(function() + Wait(5000) + if cbData[name] then + cbData[name].p:resolve() + cbData[name] = nil + end + end) + print('Triggered') + return Citizen.Await(p) +end + +RegisterNetEvent(clientToServerBackToClientName, function(name, ...) + local clientRebound = cbRebounds[name] + if clientRebound then + clientRebound(...) + end + local data = cbData[name] + if not data then return end + local func = data.cb + if not func then return end + data.p:resolve(func(...) or true) + cbData[name] = nil +end) +----------------------------- + +-- server to client rebound +RegisterNetEvent(serverToClientName, function(name, ...) + local data = Callback[name] + if not data then return end + local func = data.callback + if not func then return end + local cb = function(...) + TriggerServerEvent(serverToClientBackToServerName, name, ...) + end + func(cb, ...) +end) +----------------------------- + +exports('RegisterCallback', Callback.Register) +exports('RegisterRebound', Callback.RegisterRebound) +exports('TriggerCallback', Callback.Trigger) + +return Callback \ No newline at end of file diff --git a/lib/shared/ids.lua b/lib/shared/ids.lua new file mode 100644 index 0000000..17a3099 --- /dev/null +++ b/lib/shared/ids.lua @@ -0,0 +1,44 @@ +Ids = Ids or {} +-- for unique ids +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 + char = pattern:sub(math.random(1, #pattern), math.random(1, #pattern)) + 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 + +Ids.RandomUpper = function(tbl, len) + return Ids.CreateUniqueId(tbl, len, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") +end + +Ids.RandomLower = function(tbl, len) + return Ids.CreateUniqueId(tbl, len, "abcdefghijklmnopqrstuvwxyz") +end + +Ids.RandomString = function(tbl, len) + return Ids.CreateUniqueId(tbl, len, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +end + +Ids.RandomNumber = function(tbl, len) + return Ids.CreateUniqueId(tbl, len, "0123456789") +end + +Ids.Random = function(tbl, len) + return Ids.CreateUniqueId(tbl, len) +end + +exports("Ids", Ids) +return Ids \ No newline at end of file diff --git a/use/linear_algebra.lua b/lib/shared/la.lua similarity index 98% rename from use/linear_algebra.lua rename to lib/shared/la.lua index a391a0a..20b0987 100644 --- a/use/linear_algebra.lua +++ b/lib/shared/la.lua @@ -1,4 +1,5 @@ LA = LA or {} + --https://easings.net LA.Lerp = function(a, b, t) return a + (b - a) * t @@ -355,6 +356,10 @@ LA.Circle = function(t, radius, center) 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 @@ -397,6 +402,8 @@ LA.MultiplyMatrixByVector = function(m, v) return result end +exports('LA', LA) +return LA -- local easingTypes = { -- "linear", diff --git a/lib/shared/math.lua b/lib/shared/math.lua new file mode 100644 index 0000000..196e749 --- /dev/null +++ b/lib/shared/math.lua @@ -0,0 +1,50 @@ +Math = {} + +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 + +exports('Math', Math) +return Math \ No newline at end of file diff --git a/lib/shared/perlin.lua b/lib/shared/perlin.lua new file mode 100644 index 0000000..36ff967 --- /dev/null +++ b/lib/shared/perlin.lua @@ -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 diff --git a/use/prints.lua b/lib/shared/prints.lua similarity index 95% rename from use/prints.lua rename to lib/shared/prints.lua index 59e30de..2fc00ef 100644 --- a/use/prints.lua +++ b/lib/shared/prints.lua @@ -1,4 +1,4 @@ -Prints = {} +Prints = Prints or {} local function printMessage(level, color, message) if type(message) == 'table' then message = json.encode(message) diff --git a/lib/shared/rebound_entities.lua b/lib/shared/rebound_entities.lua new file mode 100644 index 0000000..b75ecc6 --- /dev/null +++ b/lib/shared/rebound_entities.lua @@ -0,0 +1,469 @@ +-- -- -- -- -- -- -- -- -- -- +-- ▄▀▀ █▄█ ▄▀▄ █▀▄ ██▀ █▀▄ -- +-- ▄█▀ █ █ █▀█ █▀▄ █▄▄ █▄▀ -- +-- -- -- -- -- -- -- -- -- -- +local Entities = {} +local isServer = IsDuplicityVersion() + +local registeredFunctions = {} +ReboundEntities = {} + +--- Global Add function which fire when an entity is registered +--- @param func function = function(entityData: table) +--- @return number = ID +function ReboundEntities.AddFunction(func) + assert(func, "Func is nil") + table.insert(registeredFunctions, func) + return #registeredFunctions +end + +--- Remove a registered function +--- @param id number +function ReboundEntities.RemoveFunction(id) + assert(id, "ID is nil") + assert(registeredFunctions[id], "ID not found") + registeredFunctions[id] = false +end + +--- Internal: Fire all registered functions +--- @vararg any +local function FireFunctions(...) + for k, v in pairs(registeredFunctions) do + if v then + v(...) + end + end +end + +--- Convert a table to an entityData object +--- @param entityData table = {model: string, position: vector3, id: string || nil} +--- @return table = {model: string, position: vector3, id: string, entity: number, isServer: boolean || nil} +function ReboundEntities.Register(entityData) + assert(entityData, "Entity data is nil") + local position = entityData.position + assert(position, "Position is nil") + local id = entityData.id or Ids.CreateUniqueId(Entities) + entityData.isServer = isServer + entityData.id = tostring(id) + local entityData = setmetatable(entityData, { + __tostring = function() + return entityData.id + end + }) + Entities[id] = entityData + FireFunctions(entityData) + return entityData +end + +-- Internal +local function Unregister(id) + Entities[id] = nil +end + +function ReboundEntities.GetSyncData(entityData, key) + return entityData[key] +end + +--- Get all entityDatas registered +--- @return table = {entityData: table} +function ReboundEntities.GetAll() + return Entities +end + +--- Get an entityData by its id +--- @param id string +--- @return table = {model: string, position: vector3, id: string, entity: number, isServer: boolean} +function ReboundEntities.GetById(id) + id = tostring(id) + return Entities[id] +end + +--- Get all entities by their model +--- @param model string +--- @return table = {entityData: table} +function ReboundEntities.GetByModel(model) + local entities = {} + for k, v in pairs(Entities) do + if v.model == model then + table.insert(entities, v) + end + end + return entities +end + +--- Get the closest entityData to a position +--- @param pos vector3 +--- @return table = {model: string, position: vector3, id: string, entity: number, isServer: boolean} +function ReboundEntities.GetClosest(pos) + local closest = nil + local closestDist = 9999 + for k, v in pairs(Entities) do + local dist = #(pos - v.pos) + if dist < closestDist then + closest = v + closestDist = dist + end + end + return closest +end + +--- Get all entities within a radius of a position +--- @param pos vector3 +--- @param radius number +--- @return table = {entityData: table} +function ReboundEntities.GetWithinRadius(pos, radius) + local entities = {} + for k, v in pairs(Entities) do + local dist = #(pos - v.pos) + if dist < radius then + table.insert(entities, v) + end + end + return entities +end + +function ReboundEntities.SetOnSyncKeyChange(entityData, cb) + print("SetOnSyncKeyChange", entityData, cb) + local id = entityData.id or entityData + local data = ReboundEntities.GetById(id) + assert(data, "Entity not found") + data.onSyncKeyChange = cb +end + +exports('ReboundEntities', ReboundEntities) + +if not isServer then goto client end +-- -- -- -- -- -- -- -- -- -- +-- ▄▀▀ ██▀ █▀▄ █ █ ██▀ █▀▄ -- +-- ▄█▀ █▄▄ █▀▄ ▀▄▀ █▄▄ █▀▄ -- +-- -- -- -- -- -- -- -- -- -- + +--- Create a rebound entity +--- @param entityData object = {model: string, position: vector3, rotation: vector3 || nil} +--- @param src number = Who to send to. Defaults: -1 +--- @return table = {model: string, position: vector3, rotation: vector3, id: string, entity: number, isServer: boolean} +function ReboundEntities.Create(entityData, src) + local entity = ReboundEntities.Register(entityData) + assert(entity, "Failed to register entity") + assert(entity.position, "Entity position is nil") + entity.rotation = entity.rotation or vector3(0, 0, 0) + src = src and tonumber(src) or -1 + TriggerClientEvent(GetCurrentResourceName() ..":client:CreateReboundEntity", src, 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) + src = tonumber(src) + if not entityData.restricted then return false end + if src == -1 then return true end + return entityData.restricted(src, entityData) or false +end + +function ReboundEntities.Refresh(src) + src= tonumber(src) + local list = ReboundEntities.GetAccessibleList(src) + TriggerClientEvent(GetCurrentResourceName() ..":client:CreateReboundEntities", src, list) +end + +--- Delete a rebound entity +--- @param id string = The entity id: string || entityData: object +--- @return boolean +function ReboundEntities.Delete(id) + Unregister(id) + TriggerClientEvent(GetCurrentResourceName() ..":client:DeleteReboundEntity", -1, id) +end + +-- Create multiple rebound entities +--- @param entityDatas table = {entityData: object} +--- @param src number = Who to send to. Defaults: -1 +--- @return table = {entityData: object} +function ReboundEntities.CreateMultiple(entityDatas, src, restricted) + src = src and tonumber(src) or -1 + local _entityDatas = {} + for k, v in pairs(entityDatas) do + local entity = ReboundEntities.Register(v) + assert(entity, "Failed to register entity") + assert(entity.position, "Entity position is nil") + 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, _entityDatas) + return _entityDatas +end + +--- Sets data to be synced to clients for a rebound entity +--- @param entityData object = {model: string, position: vector3, rotation: vector3, id: string, entity: number, isServer: boolean} +--- @param key string +--- @param value any +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) + src = tonumber(src) + local list = {} + for k, 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(src, _) + ReboundEntities.Refresh(src) +end) + +AddEventHandler('onResourceStart', function(resource) + if resource == GetCurrentResourceName() then + Wait(1000) + local players = GetPlayers() + for k, src in pairs(players) do + ReboundEntities.Refresh(src) + end + end +end) + +::client:: +if isServer then return ReboundEntities end +-- -- -- -- -- -- -- -- -- -- +-- ▄▀▀ █ █ ██▀ █▄ █ ▀█▀ -- +-- ▀▄▄ █▄▄ █ █▄▄ █ ▀█ █ -- +-- -- -- -- -- -- -- -- -- -- + +--- Loads a model into memory +--- @param model : Model name || Model hash +--- @return modelHash : number +function ReboundEntities.LoadModel(model) + assert(model, "Model is nil") + model = type(model) == "number" and model or GetHashKey(model) + local timeout = 100 + RequestModel(model) + while not HasModelLoaded(model) and timeout > 0 do + timeout = timeout - 1 + Wait(100) + end + assert(timeout > 0, string.format("Failed to load model %s", model)) + return model +end + +--- Spawns an entity +--- @param entityData : table = {model: string, position: vector3, rotation: vector3 || nil, onSpawn: function || nil} +--- @return entity : number +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 = nil + if model then + model = ReboundEntities.LoadModel(model) + entity = CreateObject(model, position.x, position.y, position.z, false, false, false) + SetEntityRotation(entity, rotation.x, rotation.y, rotation.z) + end + + local onSpawn = entityData.onSpawn + print(onSpawn) + if onSpawn then + onSpawn(entityData, entity) + end + -- dont forget to save the entity to the entityData table in the spawn loop + return entity, true +end + +--- Set the onSpawn callback for an entity. Triggered after the entity is created. +--- @param id : string || entityData : object +--- @param cb : function(entityData: object, entity: number) +--- @return success : boolean +function ReboundEntities.SetOnSpawn(id, cb) + id = tostring(id) + local entity = ReboundEntities.GetById(id) + assert(entity, "Entity not found") + entity.onSpawn = cb + return true +end + +--- Despawns an entity +--- @param entityData : entity || table = {entity: number, onDespawn: function || nil} +function ReboundEntities.Despawn(entityData) + assert(entityData, "Entity data is nil") + local entity = entityData.entity + local onDespawn = entityData.onDespawn + if onDespawn and type(onDespawn) == "function" then + onDespawn(entityData, entity) + end + if entity and DoesEntityExist(entity) then + DeleteEntity(entity) + end + return nil, nil +end + +--- Set the onDespawn callback for an entity. Triggered before the entity is deleted. +--- @param id : string || entityData : object +--- @param cb : function(entityData: object, entity: number) +--- @return success : boolean +function ReboundEntities.SetOnDespawn(id, cb) + id = tostring(id) + local entity = ReboundEntities.GetById(id) + assert(entity, "Entity not found") + entity.onDespawn = cb + return true +end + +local spawnLoopRunning = false + +--- Internal function to spawn entities in a loop +--- @param distanceToCheck : number || nil +--- @param waitTime : number || nil +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 k, v in pairs(Entities) do + + if not v.restricted then + local pos = GetEntityCoords(PlayerPedId()) + local dist = #(pos - v.position) + if dist <= distanceToCheck and (not v.entity and not v.inRange) then + v.entity, v.inRange = ReboundEntities.Spawn(v) + elseif dist > distanceToCheck and (v.entity or v.inRange) then + v.entity, v.inRange = ReboundEntities.Despawn(v) + end + elseif v.entity then + v.entity = ReboundEntities.Despawn(v) + end + end + Wait(waitTime) + end + end) +end + +--- Get an entity by its entity number +--- @param entity : number +--- @return entityData : table +function ReboundEntities.GetByEntity(entity) + for k, v in pairs(Entities) do + if v.entity == entity then + return v + end + end +end + +--- Create a client side only, rebound entity +--- @param entityData : table = {model: string, position: vector3, rotation: vector3 || nil} +--- @return entity : table +function ReboundEntities.CreateClient(entityData) + local entity = ReboundEntities.Register(entityData) + if not entity then return end + ReboundEntities.SpawnLoop() + return entity +end + +--- Delete a client side only, rebound entity +--- @param id : string || entityData : table +--- @return success : boolean +function ReboundEntities.DeleteClient(id) + id = tostring(id) + local entityData = ReboundEntities.GetById(id) + local isServer = entityData.isServer + assert(not isServer, "Cannot delete server entity from client") + local entity = entityData.entity + if entity then + ReboundEntities.Despawn(entityData) + end + Unregister(id) + return true +end + +--- Create multiple client only, rebound entities +--- @param entityDatas : table = {entityData: table = {model: string, position: vector3, rotation: vector3 || nil}} +--- @return _entityDatas : objects +function ReboundEntities.CreateMultipleClient(entityDatas) + local _entityDatas = {} + for k, v in pairs(entityDatas) do + local entityData = ReboundEntities.CreateClient(v) + entityDatas[entityData.id] = entityData + end + return _entityDatas +end + +--- Delete multiple client only, rebound entities +--- @param ids : table = {string || entityData: table} +function ReboundEntities.DeleteMultipleClient(ids) + for k, v in pairs(ids) do + ReboundEntities.DeleteClient(v) + end +end + +-- -- -- -- internal -- -- -- -- +local function DeleteFromServer(id) + id = tostring(id) + local entityData = ReboundEntities.GetById(id) + if not entityData then return end + entityData.isServer = nil + ReboundEntities.DeleteClient(id) +end + +local function DeleteMultipleFromServer(ids) + for k, v in pairs(ids) do + DeleteFromServer(v) + 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) + DeleteMultipleFromServer(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 + spawnLoopRunning = false + for k, v in pairs(Entities) do + if v.entity then + DeleteEntity(tonumber(v.entity)) + end + end + end +end) + +return ReboundEntities diff --git a/lib/shared/rebound_entitiy_actions.lua b/lib/shared/rebound_entitiy_actions.lua new file mode 100644 index 0000000..ba45e1f --- /dev/null +++ b/lib/shared/rebound_entitiy_actions.lua @@ -0,0 +1,35 @@ +local ReboundEntities = ReboundEntities or Require("shared/rebound_entities.lua") +local Actions = Actions or Require("shared/actions.lua") +local LA = LA or Require("shared/la.lua") + +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.Get(reboundId) + assert(re, "Rebound entity not found") + local entity = re and re.entity + if not entity then return end + 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 + t = (GetGameTimer() - startTime) / duration + local lerpPosition = LA.LerpVector(start, position, t) + SetEntityCoords(entity, lerpPosition) + Wait(0) + + end + end) +end) diff --git a/lib/shared/tables.lua b/lib/shared/tables.lua new file mode 100644 index 0000000..148db7f --- /dev/null +++ b/lib/shared/tables.lua @@ -0,0 +1,109 @@ +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.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 + occupiedSlots[v.slot] = true + 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 + +exports("Table", Table) +return Table \ No newline at end of file diff --git a/modules/target/ox_target/client.lua b/modules/target/ox_target/client.lua index bcb2a75..f3c955e 100644 --- a/modules/target/ox_target/client.lua +++ b/modules/target/ox_target/client.lua @@ -14,38 +14,42 @@ local targetZones = {} Target = {} -Target.AddGlobalPlayer = function(options) +Target.FixOptions = function(options) for k, v in pairs(options) do - options[k].onSelect = v.onSelect or v.action + local action = v.onSelect or v.action + local select = function(entityOrData) + if type(entityOrData) == 'table' then + return action(entityOrData.entity) + end + return action(entityOrData) + end + options[k].onSelect = action end + return options +end + +Target.AddGlobalPlayer = function(options) + options = Target.FixOptions(options) ox_target:addGlobalPlayer(options) end Target.AddGlobalVehicle = function(options) - for k, v in pairs(options) do - options[k].onSelect = v.onSelect or v.action - end + options = Target.FixOptions(options) ox_target:addGlobalVehicle(options) end Target.AddLocalEntity = function(entities, options) - for k, v in pairs(options) do - options[k].onSelect = v.onSelect or v.action - end + options = Target.FixOptions(options) ox_target:addLocalEntity(entities, options) end Target.AddModel = function(models, options) - for k, v in pairs(options) do - options[k].onSelect = v.onSelect or v.action - end + options = Target.FixOptions(options) ox_target:addModel(models, options) end Target.AddBoxZone = function(name, coords, size, heading, options) - for k, v in pairs(options) do - options[k].onSelect = v.onSelect or v.action - end + options = Target.FixOptions(options) local target = ox_target:addBoxZone({ coords = coords, size = size, diff --git a/modules/target/qb-target/client.lua b/modules/target/qb-target/client.lua index cefff29..ec54bde 100644 --- a/modules/target/qb-target/client.lua +++ b/modules/target/qb-target/client.lua @@ -13,10 +13,22 @@ local targetZones = {} Target = {} -Target.AddGlobalPlayer = function(options) +Target.FixOptions = function(options) for k, v in pairs(options) do - options[k].action = v.action or v.onSelect + local action = v.onSelect or v.action + local select = function(entityOrData) + if type(entityOrData) == 'table' then + return action(entityOrData.entity) + end + return action(entityOrData) + end + options[k].onSelect = action end + return options +end + +Target.AddGlobalPlayer = function(options) + options = Target.FixOptions(options) exports['qb-target']:AddGlobalPlayer({ options = options, distance = options.distance or 1.5 @@ -25,9 +37,7 @@ end Target.AddGlobalVehicle = function(options) - for k, v in pairs(options) do - options[k].action = v.action or v.onSelect - end + options = Target.FixOptions(options) exports['qb-target']:AddGlobalVehicle({ options = options, distance = options.distance or 1.5 @@ -35,9 +45,7 @@ Target.AddGlobalVehicle = function(options) end Target.AddLocalEntity = function(entities, options) - for k, v in pairs(options) do - options[k].action = v.action or v.onSelect - end + options = Target.FixOptions(options) exports['qb-target']:AddTargetEntity(entities, { options = options, distance = options.distance or 1.5 @@ -45,9 +53,7 @@ Target.AddLocalEntity = function(entities, options) end Target.AddModel = function(models, options) - for k, v in pairs(options) do - options[k].action = v.action or v.onSelect - end + options = Target.FixOptions(options) exports['qb-target']:AddTargetModel(models, { options = options, distance = options.distance or 1.5, @@ -55,9 +61,7 @@ Target.AddModel = function(models, options) end Target.AddBoxZone = function(name, coords, size, heading, options) - for k, v in pairs(options) do - options[k].action = v.action or v.onSelect - end + options = Target.FixOptions(options) exports['qb-target']:AddBoxZone(name, coords, size.x, size.y, { name = name, debugPoly = targetDebug, diff --git a/modules/utility/client/object_Placer.lua b/old/client/object_Placer.lua similarity index 100% rename from modules/utility/client/object_Placer.lua rename to old/client/object_Placer.lua diff --git a/modules/utility/client/utility.lua b/old/client/utility.lua similarity index 100% rename from modules/utility/client/utility.lua rename to old/client/utility.lua diff --git a/old/server/VersionChecker.lua b/old/server/VersionChecker.lua new file mode 100644 index 0000000..9ee14aa --- /dev/null +++ b/old/server/VersionChecker.lua @@ -0,0 +1,38 @@ +Utility = Utility or {} + +function Utility.VersionChecker(repoPath) + local resource = repoPath:match("([^/]+)$") + if not resource then return Prints.Error('^1Invalid repository format. Expected format: "username/reponame"^0') end + + local version = GetResourceMetadata(resource, "version", 0) + if not version then return Prints.Error(('^1Unable to determine current resource version for %s ^0'):format(resource)) end + + version = version:match('%d+%.%d+%.%d+') + if not version then return Prints.Error(('^1Invalid version format for %s ^0'):format(resource)) end + + local username, reponame = repoPath:match("([^/]+)/([^/]+)") + if not username or not reponame then return Prints.Error('^1Invalid repository format. Expected format: "username/reponame"^0') end + PerformHttpRequest(('https://api.github.com/repos/%s/%s/releases/latest'):format(username, reponame), function(status, response) + local failure = ('^1Unable locate github repo for resource %s ^0'):format(resource) + + if status ~= 200 then return Prints.Error(failure) end + response = json.decode(response) + + if response.prerelease then return Prints.Error(failure) end + + local latest = response.tag_name:match('%d+%.%d+%.%d+') + if not latest then return Prints.Error(failure) end + + if latest == version then return print(('^2 %s is on the latest version.^0'):format(resource)) end + local currentVersion, latestVersion = version:gsub('%D', ''), latest:gsub('%D', '') + + if currentVersion < latestVersion then + return print(('^1An update is available for %s (current version: %s)\r\n - ^5Please download the latest version from the GitHub release page.^7\r\n%s'):format(resource, version, response.html_url)) + end + end, 'GET') +end + +AddEventHandler('onResourceStart', function(resourceName) + if GetCurrentResourceName() ~= resourceName then return end + Utility.VersionChecker("The-Order-Of-The-Sacred-Framework/community_bridge") +end) \ No newline at end of file diff --git a/modules/utility/server/logs.lua b/old/server/logs.lua similarity index 100% rename from modules/utility/server/logs.lua rename to old/server/logs.lua diff --git a/modules/utility/server/lootTables.lua b/old/server/lootTables.lua similarity index 100% rename from modules/utility/server/lootTables.lua rename to old/server/lootTables.lua diff --git a/old/server/supplement_events.lua b/old/server/supplement_events.lua new file mode 100644 index 0000000..11181c0 --- /dev/null +++ b/old/server/supplement_events.lua @@ -0,0 +1,6 @@ +RegisterNetEvent("community_bridge:server:alert", function(data) + local playersByJob = Framework.GetPlayersByJob(data.job) + for _, src in pairs(playersByJob) do + TriggerClientEvent("community_bridge:client:alert", src, data) + end +end) \ No newline at end of file diff --git a/modules/utility/shared/callbacks.lua b/old/shared/callbacks.lua similarity index 100% rename from modules/utility/shared/callbacks.lua rename to old/shared/callbacks.lua diff --git a/modules/utility/shared/prints.lua b/old/shared/prints.lua similarity index 100% rename from modules/utility/shared/prints.lua rename to old/shared/prints.lua diff --git a/modules/utility/shared/tables.lua b/old/shared/tables.lua similarity index 100% rename from modules/utility/shared/tables.lua rename to old/shared/tables.lua diff --git a/modules/utility/shared/uniqueids.lua b/old/shared/uniqueids.lua similarity index 100% rename from modules/utility/shared/uniqueids.lua rename to old/shared/uniqueids.lua diff --git a/use/ids.lua b/use/ids.lua deleted file mode 100644 index a12eec4..0000000 --- a/use/ids.lua +++ /dev/null @@ -1,22 +0,0 @@ -Ids = {} -function Ids.CreateUniqueId(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 - char = pattern:sub(math.random(1, #pattern), math.random(1, #pattern)) - 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 - -return Ids \ No newline at end of file diff --git a/use/require.lua b/use/require.lua deleted file mode 100644 index c151da3..0000000 --- a/use/require.lua +++ /dev/null @@ -1,36 +0,0 @@ -local loadedModules = {} - -function Require(modulePath, resourceName) - if resourceName and type(resourceName) ~= "string" then - resourceName = GetCurrentResourceName() - end - - if not resourceName then - resourceName = "community_bridge" - end - - local id = resourceName .. ":" .. modulePath - if loadedModules[id] then - print("Returning cached module [" .. id .. "]") - 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 - -exports('Require', Require) \ No newline at end of file