---@class Utility Utility = Utility or {} local blipIDs = {} local spawnedPeds = {} Locales = Locales or Require('modules/locales/shared.lua') -- === Local Helpers === ---Get the hash of a model (string or number) ---@param model string|number ---@return number local function getModelHash(model) if type(model) ~= 'number' then return joaat(model) end return model end ---Ensure a model is loaded into memory ---@param model string|number ---@return boolean, number local function ensureModelLoaded(model) local hash = getModelHash(model) if not IsModelValid(hash) and not IsModelInCdimage(hash) then return false, hash end RequestModel(hash) local count = 0 while not HasModelLoaded(hash) and count < 30000 do Wait(0) count = count + 1 end return HasModelLoaded(hash), hash end ---Add a text entry if possible ---@param key string ---@param text string local function addTextEntryOnce(key, text) if not AddTextEntry then return end AddTextEntry(key, text) end ---Create a blip safely and store its reference ---@param coords vector3 ---@param sprite number ---@param color number ---@param scale number ---@param label string ---@param shortRange boolean ---@param displayType number ---@return number local function safeAddBlip(coords, sprite, color, scale, label, shortRange, displayType) local blip = AddBlipForCoord(coords.x, coords.y, coords.z) SetBlipSprite(blip, sprite or 8) SetBlipColour(blip, color or 3) SetBlipScale(blip, scale or 0.8) SetBlipDisplay(blip, displayType or 2) SetBlipAsShortRange(blip, shortRange) addTextEntryOnce(label, label) BeginTextCommandSetBlipName(label) EndTextCommandSetBlipName(blip) table.insert(blipIDs, blip) return blip end ---Create a entiyty blip safely and store its reference ---@param entity number ---@param sprite number ---@param color number ---@param scale number ---@param label string ---@param shortRange boolean ---@param displayType number ---@return number local function safeAddEntityBlip(entity, sprite, color, scale, label, shortRange, displayType) local blip = AddBlipForEntity(entity) SetBlipSprite(blip, sprite or 8) SetBlipColour(blip, color or 3) SetBlipScale(blip, scale or 0.8) SetBlipDisplay(blip, displayType or 2) SetBlipAsShortRange(blip, shortRange) ShowHeadingIndicatorOnBlip(blip, true) addTextEntryOnce(label, label) BeginTextCommandSetBlipName(label) EndTextCommandSetBlipName(blip) table.insert(blipIDs, blip) return blip end ---Remove a blip safely from the stored list ---@param blip number ---@return boolean local function safeRemoveBlip(blip) for i, storedBlip in ipairs(blipIDs) do if storedBlip == blip then RemoveBlip(storedBlip) table.remove(blipIDs, i) return true end end return false end ---Add a text entry if possible (shortcut) ---@param text string local function safeAddTextEntry(text) if not AddTextEntry then return end AddTextEntry(text, text) end -- === Public Utility Functions === ---Create a prop with the given model and coordinates ---@param model string|number ---@param coords vector3 ---@param heading number ---@param networked boolean ---@return number|nil function Utility.CreateProp(model, coords, heading, networked) local loaded, hash = ensureModelLoaded(model) if not loaded then return nil, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end local propEntity = CreateObject(hash, coords.x, coords.y, coords.z, networked, false, false) SetEntityHeading(propEntity, heading) SetModelAsNoLongerNeeded(hash) return propEntity end ---Get street and crossing names at given coordinates ---@param coords vector3 ---@return string, string function Utility.GetStreetNameAtCoords(coords) local streetHash, crossingHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z) return GetStreetNameFromHashKey(streetHash), GetStreetNameFromHashKey(crossingHash) end ---Create a vehicle with the given model and coordinates ---@param model string|number ---@param coords vector3 ---@param heading number ---@param networked boolean ---@return number|nil, table function Utility.CreateVehicle(model, coords, heading, networked) local loaded, hash = ensureModelLoaded(model) if not loaded then return nil, {}, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end local vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, heading, networked, false) SetVehicleHasBeenOwnedByPlayer(vehicle, true) SetVehicleNeedsToBeHotwired(vehicle, false) SetVehRadioStation(vehicle, "OFF") SetModelAsNoLongerNeeded(hash) return vehicle, { networkid = NetworkGetNetworkIdFromEntity(vehicle) or 0, coords = GetEntityCoords(vehicle), heading = GetEntityHeading(vehicle), } end ---Create a ped with the given model and coordinates ---@param model string|number ---@param coords vector3 ---@param heading number ---@param networked boolean ---@param settings table|nil ---@return number|nil function Utility.CreatePed(model, coords, heading, networked, settings) local loaded, hash = ensureModelLoaded(model) if not loaded then return nil, Prints and Prints.Error and Prints.Error("Model Has Not Loaded") end local spawnedEntity = CreatePed(0, hash, coords.x, coords.y, coords.z, heading, networked, false) SetModelAsNoLongerNeeded(hash) table.insert(spawnedPeds, spawnedEntity) return spawnedEntity end ---Show a busy spinner with the given text ---@param text string ---@return boolean function Utility.StartBusySpinner(text) safeAddTextEntry(text) BeginTextCommandBusyString(text) AddTextComponentSubstringPlayerName(text) EndTextCommandBusyString(0) return true end ---Stop the busy spinner if active ---@return boolean function Utility.StopBusySpinner() if BusyspinnerIsOn() then BusyspinnerOff() return true end return false end ---Create a blip at the given coordinates ---@param coords vector3 ---@param sprite number ---@param color number ---@param scale number ---@param label string ---@param shortRange boolean ---@param displayType number ---@return number function Utility.CreateBlip(coords, sprite, color, scale, label, shortRange, displayType) return safeAddBlip(coords, sprite, color, scale, label, shortRange, displayType) end ---Create a blip on the provided entity ---@param entity number ---@param sprite number ---@param color number ---@param scale number ---@param label string ---@param shortRange boolean ---@param displayType number ---@return number function Utility.CreateEntityBlip(entity, sprite, color, scale, label, shortRange, displayType) return safeAddEntityBlip(entity, sprite, color, scale, label, shortRange, displayType) end ---Remove a blip if it exists ---@param blip number ---@return boolean function Utility.RemoveBlip(blip) return safeRemoveBlip(blip) end ---Load a model into memory ---@param model string|number ---@return boolean function Utility.LoadModel(model) local loaded = ensureModelLoaded(model) return loaded end ---Request an animation dictionary ---@param dict string ---@return boolean function Utility.RequestAnimDict(dict) RequestAnimDict(dict) local count = 0 while not HasAnimDictLoaded(dict) and count < 30000 do Wait(0) count = count + 1 end return HasAnimDictLoaded(dict) end ---Remove a ped if it exists ---@param entity number ---@return boolean function Utility.RemovePed(entity) local success = false if DoesEntityExist(entity) then DeleteEntity(entity) end for i, storedEntity in ipairs(spawnedPeds) do if storedEntity == entity then table.remove(spawnedPeds, i) success = true break end end return success end ---Show a native input menu and return the result ---@param text string ---@param length number ---@return string|boolean function Utility.NativeInputMenu(text, length) local maxLength = Math and Math.Clamp and Math.Clamp(length, 1, 50) or math.min(math.max(length or 10, 1), 50) local menuText = text or 'enter text' safeAddTextEntry(menuText) DisplayOnscreenKeyboard(1, menuText, "", "", "", "", "", maxLength) while (UpdateOnscreenKeyboard() == 0) do DisableAllControlActions(0) Wait(0) end if (GetOnscreenKeyboardResult()) then return GetOnscreenKeyboardResult() end return false end ---Get the skin data of a ped ---@param entity number ---@return table function Utility.GetEntitySkinData(entity) local skinData = { clothing = {}, props = {} } for i = 0, 11 do skinData.clothing[i] = { GetPedDrawableVariation(entity, i), GetPedTextureVariation(entity, i) } end for i = 0, 13 do skinData.props[i] = { GetPedPropIndex(entity, i), GetPedPropTextureIndex(entity, i) } end return skinData end ---Apply skin data to a ped ---@param entity number ---@param skinData table ---@return boolean function Utility.SetEntitySkinData(entity, skinData) for i = 0, 11 do SetPedComponentVariation(entity, i, skinData.clothing[i][1], skinData.clothing[i][2], 0) end for i = 0, 13 do SetPedPropIndex(entity, i, skinData.props[i][1], skinData.props[i][2], 0) end return true end ---Reload the player's skin and remove attached objects ---@return boolean function Utility.ReloadSkin() local skinData = Utility.GetEntitySkinData(cache.ped) Utility.SetEntitySkinData(cache.ped, skinData) for _, props in pairs(GetGamePool("CObject")) do if IsEntityAttachedToEntity(cache.ped, props) then SetEntityAsMissionEntity(props, true, true) DeleteObject(props) DeleteEntity(props) end end return true end ---Show a native help text ---@param text string ---@param duration number function Utility.HelpText(text, duration) safeAddTextEntry(text) BeginTextCommandDisplayHelp(text) EndTextCommandDisplayHelp(0, false, true, duration or 5000) end ---Draw 3D help text in the world ---@param coords vector3 ---@param text string ---@param scale number function Utility.Draw3DHelpText(coords, text, scale) local onScreen, x, y = GetScreenCoordFromWorldCoord(coords.x, coords.y, coords.z) if onScreen then SetTextScale(scale or 0.35, scale or 0.35) SetTextFont(4) SetTextProportional(1) SetTextColour(255, 255, 255, 215) SetTextEntry("STRING") SetTextCentre(1) AddTextComponentString(text) DrawText(x, y) local factor = (string.len(text)) / 370 DrawRect(x, y + 0.0125, 0.015 + factor, 0.03, 41, 11, 41, 100) end end ---Show a native notification ---@param text string function Utility.NotifyText(text) safeAddTextEntry(text) SetNotificationTextEntry(text) DrawNotification(false, true) end ---Teleport the player to given coordinates ---@param coords vector3 ---@param conditionFunction function|nil ---@param afterTeleportFunction function|nil function Utility.TeleportPlayer(coords, conditionFunction, afterTeleportFunction) if conditionFunction ~= nil then if not conditionFunction() then return end end DoScreenFadeOut(2500) Wait(2500) SetEntityCoords(cache.ped, coords.x, coords.y, coords.z, false, false, false, false) if coords.w then SetEntityHeading(cache.ped, coords.w) end FreezeEntityPosition(cache.ped, true) local count = 0 while not HasCollisionLoadedAroundEntity(cache.ped) and count <= 30000 do RequestCollisionAtCoord(coords.x, coords.y, coords.z) Wait(0) count = count + 1 end FreezeEntityPosition(cache.ped, false) DoScreenFadeIn(1000) if afterTeleportFunction ~= nil then afterTeleportFunction() end end ---Get the hash from a model ---@param model string|number ---@return number function Utility.GetEntityHashFromModel(model) return getModelHash(model) end ---Get the closest player to given coordinates ---@param coords vector3|nil ---@param distanceScope number|nil ---@param includeMe boolean|nil ---@return number, number, number function Utility.GetClosestPlayer(coords, distanceScope, includeMe) local players = GetActivePlayers() local closestPlayer = 0 local selfPed = cache.ped local selfCoords = coords or GetEntityCoords(cache.ped) local closestDistance = distanceScope or 5 for _, player in ipairs(players) do local playerPed = GetPlayerPed(player) if includeMe or playerPed ~= selfPed then local playerCoords = GetEntityCoords(playerPed) local distance = #(selfCoords - playerCoords) if closestDistance == -1 or distance < closestDistance then closestPlayer = player closestDistance = distance end end end return closestPlayer, closestDistance, GetPlayerServerId(closestPlayer) end ---Get the closest vehicle to given coordinates ---@param coords vector3|nil ---@param distanceScope number|nil ---@param includePlayerVeh boolean|nil ---@return number|nil, vector3|nil, number|nil function Utility.GetClosestVehicle(coords, distanceScope, includePlayerVeh) local vehicleEntity = nil local vehicleNetID = nil local vehicleCoords = nil local selfCoords = coords or GetEntityCoords(cache.ped) local closestDistance = distanceScope or 5 local includeMyVeh = includePlayerVeh or false local gamePoolVehicles = GetGamePool("CVehicle") local playerVehicle = IsPedInAnyVehicle(cache.ped, false) and GetVehiclePedIsIn(cache.ped, false) or 0 for i = 1, #gamePoolVehicles do local thisVehicle = gamePoolVehicles[i] if DoesEntityExist(thisVehicle) and (includeMyVeh or thisVehicle ~= playerVehicle) then local thisVehicleCoords = GetEntityCoords(thisVehicle) local distance = #(selfCoords - thisVehicleCoords) if closestDistance == -1 or distance < closestDistance then vehicleEntity = thisVehicle vehicleNetID = NetworkGetNetworkIdFromEntity(thisVehicle) or nil vehicleCoords = thisVehicleCoords closestDistance = distance end end end return vehicleEntity, vehicleCoords, vehicleNetID end -- Deprecated point functions (no changes) function Utility.RegisterPoint(pointID, pointCoords, pointDistance, _onEnter, _onExit, _nearby) return Point.Register(pointID, pointCoords, pointDistance, nil, _onEnter, _onExit, _nearby) end function Utility.GetPointById(pointID) return Point.Get(pointID) end function Utility.GetActivePoints() return Point.GetAll() end function Utility.RemovePoint(pointID) return Point.Remove(pointID) end ---Simple switch-case function ---@generic T ---@param value T The value to match against the cases ---@param cases table Table with case functions and an optional default (false key) ---@return any|false result The return value of the matched case function, or false if none matched function Utility.Switch(value, cases) local caseFunc = cases[value] or cases[false] if caseFunc and type(caseFunc) == "function" then local ok, result = pcall(caseFunc) return ok and result or false end return false end function Utility.CopyToClipboard(text) if not text then return false end if type(text) ~= "string" then text = json.encode(text, { indent = true }) end SendNUIMessage({ type = "copytoclipboard", text = text }) local message = Locales and Locales.Locale("clipboard.copy") --TriggerEvent('community_bridge:Client:Notify', message, 'success') return true end --- Pattern match-like function ---@generic T ---@param value T The value to match ---@param patterns table A list of matchers and their handlers ---@return any|false result The result of the first matched case, or false if none function Utility.Match(value, patterns) for pattern, handler in pairs(patterns) do if type(pattern) == "function" then local ok, matched = pcall(pattern, value) if ok and matched then local success, result = pcall(handler) return success and result or false end elseif pattern == value then local success, result = pcall(handler) return success and result or false end end if patterns[false] then local ok, result = pcall(patterns[false]) return ok and result or false end return false end ---Get zone name at coordinates ---@param coords vector3 ---@return string function Utility.GetZoneName(coords) local zoneHash = GetNameOfZone(coords.x, coords.y, coords.z) return GetLabelText(zoneHash) end local SpecialKeyCodes = { ['b_116'] = 'Scroll Up', ['b_115'] = 'Scroll Down', ['b_100'] = 'LMB', ['b_101'] = 'RMB', ['b_102'] = 'MMB', ['b_103'] = 'Extra 1', ['b_104'] = 'Extra 2', ['b_105'] = 'Extra 3', ['b_106'] = 'Extra 4', ['b_107'] = 'Extra 5', ['b_108'] = 'Extra 6', ['b_109'] = 'Extra 7', ['b_110'] = 'Extra 8', ['b_1015'] = 'AltLeft', ['b_1000'] = 'ShiftLeft', ['b_2000'] = 'Space', ['b_1013'] = 'ControlLeft', ['b_1002'] = 'Tab', ['b_1014'] = 'ControlRight', ['b_140'] = 'Numpad4', ['b_142'] = 'Numpad6', ['b_144'] = 'Numpad8', ['b_141'] = 'Numpad5', ['b_143'] = 'Numpad7', ['b_145'] = 'Numpad9', ['b_200'] = 'Insert', ['b_1012'] = 'CapsLock', ['b_170'] = 'F1', ['b_171'] = 'F2', ['b_172'] = 'F3', ['b_173'] = 'F4', ['b_174'] = 'F5', ['b_175'] = 'F6', ['b_176'] = 'F7', ['b_177'] = 'F8', ['b_178'] = 'F9', ['b_179'] = 'F10', ['b_180'] = 'F11', ['b_181'] = 'F12', ['b_194'] = 'ArrowUp', ['b_195'] = 'ArrowDown', ['b_196'] = 'ArrowLeft', ['b_197'] = 'ArrowRight', ['b_1003'] = 'Enter', ['b_1004'] = 'Backspace', ['b_198'] = 'Delete', ['b_199'] = 'Escape', ['b_1009'] = 'PageUp', ['b_1010'] = 'PageDown', ['b_1008'] = 'Home', ['b_131'] = 'NumpadAdd', ['b_130'] = 'NumpadSubstract', ['b_211'] = 'Insert', ['b_210'] = 'Delete', ['b_212'] = 'End', ['b_1055'] = 'Home', ['b_1056'] = 'PageUp', } local function translateKey(key) if string.find(key, "t_") then return string.gsub(key, "t_", "") elseif SpecialKeyCodes[key] then return SpecialKeyCodes[key] else return key end end function Utility.GetCommandKey(commandName) local hash = GetHashKey(commandName) | 0x80000000 local button = GetControlInstructionalButton(2, hash, true) if not button or button == "" or button == "NULL" then hash = GetHashKey(commandName) button = GetControlInstructionalButton(2, hash, true) end return translateKey(button) end AddEventHandler('onResourceStop', function(resource) if resource ~= GetCurrentResourceName() then return end for _, blip in pairs(blipIDs) do if blip and DoesBlipExist(blip) then RemoveBlip(blip) end end for _, ped in pairs(spawnedPeds) do if ped and DoesEntityExist(ped) then DeleteEntity(ped) end end end) exports('Utility', Utility) return Utility