├── README.md ├── client ├── export.lua ├── functions │ ├── base64.lua │ ├── blips.lua │ ├── callbacks.lua │ ├── entities.lua │ ├── helptext.lua │ ├── keybinds.lua │ ├── markers.lua │ ├── text.lua │ └── utils.lua └── html │ ├── copy.js │ ├── handler.js │ ├── headshot_base64.js │ └── index.html ├── fxmanifest.lua ├── server ├── config.lua ├── export.lua └── functions │ ├── callbacks.lua │ ├── logs.lua │ └── utils.lua └── shared └── config.lua /README.md: -------------------------------------------------------------------------------- 1 | # Loaf Lib 2 | A work in progress resource that I use for some of my scripts. It handles stuff like markers, keybindings, 3d text, callbacks and more. Make sure to check out the config! 3 | 4 | ## Resource friendly 5 | The resource uses 0.00ms idle without any markers. With 300+ markers it uses ~0.02ms when drawing 2 markers. 6 | 7 | ## Usage for developers 8 | Guide coming soon. For now, look a the files & the wiki. -------------------------------------------------------------------------------- /client/export.lua: -------------------------------------------------------------------------------- 1 | functions = {} 2 | 3 | exports("GetLib", function() 4 | return functions 5 | end) -------------------------------------------------------------------------------- /client/functions/base64.lua: -------------------------------------------------------------------------------- 1 | local requests = {} 2 | 3 | local function ClearHeadshots() 4 | for i = 1, 32 do 5 | if IsPedheadshotValid(i) then 6 | UnregisterPedheadshot(i) 7 | end 8 | end 9 | end 10 | 11 | local function GetHeadshot(ped) 12 | ClearHeadshots() 13 | if not ped then ped = PlayerPedId() end 14 | if DoesEntityExist(ped) then 15 | local handle, timer = RegisterPedheadshot(ped), GetGameTimer() + 5000 16 | while not IsPedheadshotReady(handle) or not IsPedheadshotValid(handle) do 17 | Wait(50) 18 | if GetGameTimer() >= timer then 19 | return { success=false, error="Could not load ped headshot." } 20 | end 21 | end 22 | 23 | local txd = GetPedheadshotTxdString(handle) 24 | local url = string.format("https://nui-img/%s/%s", txd, txd) 25 | return { success=true, url=url, txd=txd, handle=handle } 26 | end 27 | end 28 | 29 | function functions.GetBase64(ped) 30 | if not ped then ped = PlayerPedId() end 31 | local headshot = GetHeadshot(ped) 32 | if headshot.success then 33 | local requestId = functions.GenerateUniqueKey(requests) 34 | requests[requestId] = nil 35 | SendNUIMessage({ 36 | type = "convert_base64", 37 | img = headshot.url, 38 | handle = headshot.handle, 39 | id = requestId 40 | }) 41 | 42 | local timer = GetGameTimer() + 5000 43 | while not requests[requestId] do 44 | Wait(250) 45 | if GetGameTimer() >= timer then 46 | return { success=false, error="Waiting for base64 conversion timed out." } 47 | end 48 | end 49 | return { success=true, base64=requests[requestId] } 50 | else 51 | return headshot 52 | end 53 | end 54 | 55 | RegisterNUICallback("base64", function(data, cb) 56 | if data.handle then 57 | UnregisterPedheadshot(data.handle) 58 | end 59 | if data.id then 60 | requests[data.id] = data.base64 61 | Wait(1500) 62 | requests[data.id] = nil 63 | end 64 | 65 | cb({ok=true}) 66 | end) -------------------------------------------------------------------------------- /client/functions/blips.lua: -------------------------------------------------------------------------------- 1 | local blips = {} 2 | 3 | function functions.AddBlip(blipData) 4 | local id = functions.GenerateUniqueKey(blips) 5 | 6 | local coords = blipData.coords 7 | if not coords then 8 | return 9 | end 10 | local blip = AddBlipForCoord(coords.x, coords.y, coords.z) 11 | SetBlipSprite(blip, blipData.sprite or 1) 12 | SetBlipColour(blip, blipData.colour or blipData.color or 0) 13 | SetBlipScale(blip, blipData.scale or 0.7) 14 | SetBlipAsShortRange(blip, blipData.shortRange or true) 15 | SetBlipDisplay(blip, blipData.display or 2) 16 | if blipData.category then 17 | SetBlipCategory(blip, blipData.category) 18 | end 19 | 20 | BeginTextCommandSetBlipName("STRING") 21 | AddTextComponentSubstringPlayerName(blipData.label or id) 22 | EndTextCommandSetBlipName(blip) 23 | 24 | blipData.creator = GetInvokingResource() 25 | blipData.blip = blip 26 | blips[id] = blipData 27 | 28 | return id 29 | end 30 | 31 | function functions.GetBlip(blipId) 32 | return blips[blipId] 33 | end 34 | 35 | function functions.RemoveBlip(blipId) 36 | if blips[blipId] then 37 | RemoveBlip(blips[blipId].blip) 38 | blips[blipId] = nil 39 | end 40 | end 41 | 42 | AddEventHandler("onResourceStop", function(resourceName) 43 | if resourceName ~= GetCurrentResourceName() then 44 | local blipsRemoved = 0 45 | for blipId, blipData in pairs(blips) do 46 | if blipData.creator == resourceName then 47 | functions.RemoveBlip(blipId) 48 | blipsRemoved += 1 49 | end 50 | end 51 | if blipsRemoved > 0 then 52 | print(string.format("Removed %i blip%s due to resource %s stopping.", blipsRemoved, blipsRemoved > 1 and "s" or "", resourceName)) 53 | end 54 | end 55 | end) -------------------------------------------------------------------------------- /client/functions/callbacks.lua: -------------------------------------------------------------------------------- 1 | local waitingCallbacks = {} 2 | 3 | function functions.TriggerCallback(callback, cb, ...) 4 | local requestId = functions.GenerateUniqueKey(waitingCallbacks) 5 | waitingCallbacks[requestId] = cb 6 | TriggerServerEvent("loaf_lib:trigger_callback", callback, requestId, ...) 7 | end 8 | functions.TriggerCallbackAsync = functions.TriggerCallback 9 | 10 | function functions.TriggerCallbackSync(callback, ...) 11 | local requestId = functions.GenerateUniqueKey(waitingCallbacks) 12 | local toreturn 13 | 14 | local promise = promise.new() 15 | waitingCallbacks[requestId] = function(...) 16 | toreturn = { ... } 17 | promise:resolve() 18 | end 19 | TriggerServerEvent("loaf_lib:trigger_callback", callback, requestId, ...) 20 | Citizen.Await(promise) 21 | 22 | return table.unpack(toreturn) 23 | end 24 | 25 | RegisterNetEvent("loaf_lib:callback_result", function(requestId, ...) 26 | if waitingCallbacks[requestId] then 27 | waitingCallbacks[requestId](...) 28 | waitingCallbacks[requestId] = nil 29 | end 30 | end) -------------------------------------------------------------------------------- /client/functions/entities.lua: -------------------------------------------------------------------------------- 1 | -- ENUMERATE ENTITIES 2 | local pools = { 3 | object = "CObject", 4 | ped = "CPed", 5 | vehicle = "CVehicle" 6 | } 7 | 8 | function functions.GetEntities(entities) 9 | if type(entities) ~= "table" then 10 | entities = {entities} 11 | end 12 | 13 | for _, eType in pairs(entities) do 14 | if not pools[eType] then 15 | return { success=false, error="Can't enumerate entity \""..eType.."\"" } 16 | end 17 | end 18 | 19 | local toReturn = {} 20 | for _, eType in pairs(entities) do 21 | toReturn[type] = GetGamePool(pools[eType]) 22 | end 23 | 24 | return { success=true, entities=toReturn } 25 | end 26 | -------------------------------------------------------------------------------- /client/functions/helptext.lua: -------------------------------------------------------------------------------- 1 | local currentHelptext, currentCoords, width, height 2 | local fontSize = 0.35 3 | local wrap = 0.2 4 | 5 | local textEntry = GetCurrentResourceName() .. "_helptext" 6 | 7 | function functions.HideHelpText() 8 | if currentHelptext then 9 | if Config.HelpTextStyle == "luke" then 10 | TriggerEvent("luke_textui:HideUI") 11 | elseif Config.HelpTextStyle == "cd" then 12 | TriggerEvent("cd_drawtextui:HideUI") 13 | elseif Config.HelpTextStyle == "esx" then 14 | TriggerEvent("ESX:HideUI") 15 | elseif Config.HelpTextStyle == "qbcore" then 16 | TriggerEvent("qb-core:client:HideText") 17 | elseif Config.HelpTextStyle == "gta" then 18 | ClearAllHelpMessages() 19 | ClearHelp(true) 20 | end 21 | end 22 | 23 | currentHelptext = nil 24 | end 25 | 26 | function functions.DrawHelpText(text, coords, key) 27 | functions.HideHelpText() 28 | if key then 29 | if Config.HelpTextStyle == "gta" or Config.HelpTextStyle == "3d-gta" then 30 | text = functions.GetInstructional(key) .. " " .. text 31 | elseif Config.HelpTextStyle == "3d" then 32 | text = string.format("~b~[~s~%s~b~]~s~ %s", string.gsub(GetControlInstructionalButton(0, functions.GetKey(key).joaat, true), "t_", ""), text) 33 | else 34 | text = string.format("[%s] %s", string.gsub(GetControlInstructionalButton(0, functions.GetKey(key).joaat, true), "t_", ""), text) 35 | end 36 | end 37 | currentHelptext = text 38 | currentCoords = coords 39 | 40 | if Config.HelpTextStyle == "luke" then 41 | TriggerEvent("luke_textui:ShowUI", currentHelptext) 42 | elseif Config.HelpTextStyle == "cd" then 43 | TriggerEvent("cd_drawtextui:ShowUI", "show", currentHelptext) 44 | elseif Config.HelpTextStyle == "esx" then 45 | TriggerEvent("ESX:TextUI", currentHelptext) 46 | elseif Config.HelpTextStyle == "qbcore" then 47 | TriggerEvent("qb-core:client:DrawText", currentHelptext) 48 | elseif Config.HelpTextStyle == "gta" then 49 | AddTextEntry(textEntry, currentHelptext) 50 | BeginTextCommandDisplayHelp(textEntry) 51 | EndTextCommandDisplayHelp(0, true, true, 0) 52 | end 53 | end 54 | 55 | -- 3D helptext 56 | CreateThread(function() 57 | if not (Config.HelpTextStyle == "3d" and Config.Distancescale3DText) then 58 | return 59 | end 60 | 61 | local startFontSize = fontSize 62 | while true do 63 | Wait(250) 64 | 65 | while currentHelptext do 66 | -- calculate font size 67 | local fov = GetGameplayCamFov() 68 | local camCoords = GetFinalRenderedCamCoord() 69 | local dist = #(camCoords - currentCoords) 70 | local size = 1/(2 * math.abs(math.tan(math.rad(fov)/2)) * dist) / startFontSize 71 | fontSize = math.min(0.8, size) 72 | 73 | local textSize = functions.GetTextSize({ 74 | text = currentHelptext, 75 | size = fontSize, 76 | font = 4, 77 | wrap = wrap 78 | }) 79 | width = textSize.x 80 | height = textSize.y 81 | 82 | Wait(10) 83 | end 84 | end 85 | end) 86 | 87 | CreateThread(function() 88 | if Config.HelpTextStyle ~= "3d-gta" and Config.HelpTextStyle ~= "3d" then 89 | return 90 | end 91 | 92 | while true do 93 | Wait(250) 94 | 95 | if not currentHelptext then 96 | goto continue 97 | end 98 | 99 | if Config.HelpTextStyle == "3d-gta" then 100 | local str = currentHelptext 101 | local start, stop = string.find(currentHelptext, "~([^~]+)~") 102 | if start and start > 1 then 103 | start = start - 2 104 | stop = stop + 2 105 | str = "" 106 | str = str .. string.sub(currentHelptext, 0, start) .. string.rep(" ", 3) .. string.sub(currentHelptext, start+2, stop-2) .. string.sub(currentHelptext, stop, #currentHelptext) 107 | end 108 | AddTextEntry(textEntry, str) 109 | elseif Config.HelpTextStyle == "3d" then 110 | AddTextEntry(textEntry, currentHelptext) 111 | 112 | local textSize = functions.GetTextSize({ 113 | text = currentHelptext, 114 | size = fontSize, 115 | font = 4, 116 | wrap = wrap 117 | }) 118 | width = textSize.x 119 | height = textSize.y 120 | end 121 | 122 | while currentHelptext do 123 | Wait(0) 124 | 125 | if Config.HelpTextStyle == "3d-gta" then 126 | BeginTextCommandDisplayHelp(textEntry) 127 | EndTextCommandDisplayHelp(2, false, false, -1) 128 | 129 | SetFloatingHelpTextWorldPosition(1, currentCoords.x, currentCoords.y, currentCoords.z) 130 | SetFloatingHelpTextStyle(1, 1, 2, -1, 3, 0) 131 | elseif Config.HelpTextStyle == "3d" then 132 | SetDrawOrigin(currentCoords.x, currentCoords.y, currentCoords.z, 0) 133 | 134 | BeginTextCommandDisplayText(textEntry) 135 | SetTextScale(fontSize, fontSize) 136 | SetTextWrap(0.0, wrap) -- TESTING 137 | SetTextCentre(true) 138 | SetTextFont(4) 139 | EndTextCommandDisplayText(0.0, 0.0) 140 | 141 | DrawRect(0.0, height/2, math.min(wrap + 0.0015, width), height, 45, 45, 45, 150) 142 | 143 | ClearDrawOrigin() 144 | end 145 | end 146 | 147 | ::continue:: 148 | end 149 | end) 150 | -------------------------------------------------------------------------------- /client/functions/keybinds.lua: -------------------------------------------------------------------------------- 1 | -- https://forum.cfx.re/t/create-get-key-mapping/2260585/2 2 | -- http://tools.povers.fr/hashgenerator/ 3 | -- https://discord.com/channels/192358910387159041/433008322732490778/849605181589946379 4 | local registeredKeys = {} 5 | 6 | function functions.AddKey(name, keyData) 7 | name = string.lower(name) 8 | if registeredKeys[name] then 9 | return false 10 | end 11 | 12 | local command = string.format("use_%s_key", name) 13 | 14 | local hash = GetHashKey("+" .. command) 15 | local hex = string.upper(string.format("%x", hash)) 16 | if hash < 0 then 17 | hex = string.gsub(hex, string.rep("F", 8), "") 18 | end 19 | registeredKeys[name] = { 20 | command = command, 21 | instructional = "~INPUT_"..hex.."~", 22 | joaat = hash | 0x80000000, 23 | joaat_hex = hex, 24 | default = keyData.defaultKey, 25 | status = { 26 | pressed = false, 27 | justReleased = false 28 | } 29 | } 30 | 31 | -- On key press 32 | RegisterCommand("+" .. command, function() 33 | registeredKeys[name].status.pressed = true 34 | TriggerEvent("loaf_lib:pressedKey", name) 35 | end, false) 36 | 37 | -- On key release 38 | RegisterCommand("-" .. command, function() 39 | TriggerEvent("loaf_lib:releasedKey", name) 40 | registeredKeys[name].status.pressed = false 41 | 42 | registeredKeys[name].status.justReleased = true 43 | Wait(0) 44 | registeredKeys[name].status.justReleased = false 45 | end, false) 46 | 47 | RegisterKeyMapping("+" .. command, keyData.description, "keyboard", keyData.defaultKey) 48 | end 49 | 50 | function functions.GetKey(name) 51 | return registeredKeys[name] 52 | end 53 | 54 | function functions.GetInstructional(name) 55 | if registeredKeys[name] then 56 | return registeredKeys[name].instructional 57 | else 58 | return "~r~Unknown~s~" 59 | end 60 | end 61 | 62 | function functions.IsKeyPressed(name) 63 | return registeredKeys[name].status.pressed == true 64 | end 65 | 66 | function functions.IsKeyJustReleased(name) 67 | return registeredKeys[name].status.justReleased == true 68 | end 69 | 70 | for name, keyData in pairs(Config.Keybindings) do 71 | functions.AddKey(name, keyData) 72 | end -------------------------------------------------------------------------------- /client/functions/markers.lua: -------------------------------------------------------------------------------- 1 | local markers, amountMarkers = {}, 0 2 | local nearbyMarkers, insideMarkers = {}, {} 3 | 4 | -- ADD MARKER 5 | function functions.AddMarker(markerData, onEnter, onExit, onPress) 6 | local markerId = functions.GenerateUniqueKey(markers) 7 | 8 | markerData.id = markerId 9 | 10 | markerData.type = markerData.type or 1 11 | markerData.dir = markerData.dir or vector3(0.0, 0.0, 0.0) 12 | markerData.rot = markerData.rot or vector3(0.0, 0.0, 0.0) 13 | markerData.scale = markerData.scale or vector3(1.0, 1.0, 0.5) 14 | markerData.r = markerData.r or Config.DefaultColour[1] 15 | markerData.g = markerData.g or Config.DefaultColour[2] 16 | markerData.b = markerData.b or Config.DefaultColour[3] 17 | markerData.alpha = markerData.alpha or Config.DefaultColour[4] 18 | 19 | markers[markerId] = { 20 | data = markerData, 21 | callbacks = { 22 | onEnter = onEnter, -- this callback will be triggered when you enter the marker 23 | onPress = onPress, -- this callback will be triggered when you press E (or markerData.Control) 24 | onExit = onExit -- this callback will be triggered when you exit the marker 25 | }, 26 | creator = GetInvokingResource() 27 | } 28 | amountMarkers += 1 29 | return markerId 30 | end 31 | 32 | -- REMOVE MARKER 33 | function functions.RemoveMarker(markerId) 34 | if markers[markerId] then 35 | if insideMarkers[markerId] and markers[markerId].data.text then 36 | functions.HideHelpText() 37 | end 38 | markers[markerId] = nil 39 | amountMarkers -= 1 40 | return true 41 | end 42 | return false 43 | end 44 | 45 | -- GET MARKER 46 | function functions.GetMarker(markerId) 47 | return markers[markerId] 48 | end 49 | 50 | -- GET MARKERS 51 | function functions.GetMarkers() 52 | return markers 53 | end 54 | 55 | function functions.IsInMarker(markerId) 56 | return insideMarkers[markerId] == true 57 | end 58 | 59 | -- check for nearby markers 60 | CreateThread(function() 61 | local lastCoords, lastAmount = vector3(0.0, 0.0, 0.0), 0 62 | 63 | while true do 64 | Wait(500) 65 | local newNearby = {} 66 | local selfCoords = GetEntityCoords(PlayerPedId()) 67 | 68 | if #(lastCoords - selfCoords) <= 5.0 and lastAmount == amountMarkers then 69 | goto continue 70 | end 71 | 72 | lastCoords = selfCoords 73 | lastAmount = amountMarkers 74 | 75 | local _markers = markers 76 | if not next(_markers) then 77 | goto continue 78 | end 79 | 80 | -- local startTime = GetGameTimer() 81 | for markerId, markerData in pairs(_markers) do 82 | if markerData and #(selfCoords - markerData.data.coords) <= (Config.DrawDistance or 150.0) then 83 | newNearby[#newNearby + 1] = markerId 84 | end 85 | end 86 | -- print(string.format("Looping through %i markers took %ims\nYou are nearby %i markers.", amountMarkers, GetGameTimer() - startTime, #newNearby)) 87 | 88 | nearbyMarkers = newNearby 89 | 90 | ::continue:: 91 | end 92 | end) 93 | 94 | -- draw nearby markers 95 | CreateThread(function() 96 | local lastChecked = 0 97 | while true do 98 | Wait(500) 99 | 100 | while #nearbyMarkers > 0 do 101 | Wait(0) 102 | local pPed = PlayerPedId() 103 | 104 | local shouldCheck = lastChecked < (GetGameTimer() - 250) 105 | if shouldCheck then 106 | lastChecked = GetGameTimer() 107 | end 108 | 109 | local newInside = {} 110 | for _, markerId in pairs(nearbyMarkers) do 111 | if not markers[markerId] then 112 | goto continue 113 | end 114 | 115 | local markerData = markers[markerId].data 116 | DrawMarker( 117 | markerData.type, 118 | markerData.coords.x, 119 | markerData.coords.y, 120 | markerData.coords.z, 121 | 122 | markerData.dir.x, 123 | markerData.dir.y, 124 | markerData.dir.z, 125 | 126 | markerData.rot.x, 127 | markerData.rot.y, 128 | markerData.rot.z, 129 | 130 | markerData.scale.x, 131 | markerData.scale.y, 132 | markerData.scale.z, 133 | 134 | markerData.r, 135 | markerData.g, 136 | markerData.b, 137 | markerData.alpha, 138 | ---@diagnostic disable-next-line: param-type-mismatch 139 | false, false, 2, false, nil, nil, false 140 | ) 141 | 142 | if not shouldCheck then 143 | goto continue 144 | end 145 | 146 | local bottomLeft = vector3(markerData.coords.x - markerData.scale.x/2, markerData.coords.y - markerData.scale.y/2, markerData.coords.z - markerData.scale.z) 147 | local topRight = vector3(markerData.coords.x + markerData.scale.x/2, markerData.coords.y + markerData.scale.y/2, markerData.coords.z + 1.5) 148 | local insideMarker = IsEntityInArea(pPed, bottomLeft.x, bottomLeft.y, bottomLeft.z, topRight.x, topRight.y, topRight.z, false, true, 0) 149 | 150 | if insideMarker then 151 | newInside[markerId] = true 152 | if not insideMarkers[markerId] then 153 | if markerData.text then 154 | functions.DrawHelpText(markerData.text, markerData.coords + vector3(0.0, 0.0, 1.0), markerData.key) 155 | end 156 | if markers[markerId].callbacks.onEnter then 157 | markers[markerId].callbacks.onEnter(markerData.callbackData.enter, markerData) 158 | end 159 | end 160 | elseif not insideMarker and insideMarkers[markerId] then 161 | if markerData.text then 162 | functions.HideHelpText() 163 | end 164 | if markers[markerId].callbacks.onExit then 165 | markers[markerId].callbacks.onExit(markerData.callbackData.exit, markerData) 166 | end 167 | end 168 | 169 | ::continue:: 170 | end 171 | if shouldCheck then 172 | insideMarkers = newInside 173 | end 174 | end 175 | 176 | insideMarkers = {} 177 | end 178 | end) 179 | 180 | RegisterNetEvent("loaf_lib:releasedKey", function(keyName) 181 | for markerId, inside in pairs(insideMarkers) do 182 | if not inside then 183 | goto continue 184 | end 185 | 186 | local markerData = markers[markerId] 187 | if markerData?.data?.key ~= keyName then 188 | goto continue 189 | end 190 | 191 | if markerData and markerData.callbacks.onPress then 192 | TriggerEvent("loaf_lib:usedMarker", markerId) 193 | end 194 | 195 | ::continue:: 196 | end 197 | end) 198 | 199 | RegisterNetEvent("loaf_lib:usedMarker", function(markerId) 200 | local markerData = markers[markerId] 201 | markerData.callbacks.onPress(markerData.data.callbackData.press, markerData.data) 202 | end) 203 | 204 | AddEventHandler("onResourceStop", function(resourceName) 205 | if resourceName == GetCurrentResourceName() then 206 | return 207 | end 208 | 209 | local markersRemoved = 0 210 | for markerId, markerData in pairs(markers) do 211 | if markerData.creator == resourceName then 212 | functions.RemoveMarker(markerId) 213 | markersRemoved += 1 214 | end 215 | end 216 | 217 | if markersRemoved > 0 then 218 | print(string.format("Removed %i marker%s due to resource %s stopping.", markersRemoved, markersRemoved > 1 and "s" or "", resourceName)) 219 | end 220 | end) -------------------------------------------------------------------------------- /client/functions/text.lua: -------------------------------------------------------------------------------- 1 | local texts, amountTexts = {}, 0 2 | local nearbyTexts = {} 3 | 4 | function functions.GetTextSize(textData) 5 | if type(textData) ~= "table" then return vector2(0.0, 0.0) end 6 | 7 | if textData.text then 8 | BeginTextCommandGetWidth("STRING") 9 | AddTextComponentSubstringPlayerName(textData.text) 10 | else 11 | BeginTextCommandGetWidth(textData.textEntry) 12 | end 13 | SetTextScale(textData.size or 0.35, textData.size or 0.35) 14 | SetTextFont(4) 15 | local textWidth = EndTextCommandGetWidth(true) 16 | local width = textWidth + 0.0015 17 | 18 | local newlines = 0 19 | if textData.text then 20 | local _, count = string.gsub(textData.text, "\n", "") 21 | newlines = newlines + count 22 | _, count = string.gsub(textData.text, "~n~", "") 23 | newlines = newlines + count 24 | end 25 | 26 | local lines = math.ceil(textWidth/(textData.wrap or 1.0)) + newlines 27 | 28 | local characterHeight = GetRenderedCharacterHeight(textData.size or 0.35, textData.font or 4) 29 | local height = characterHeight * lines + characterHeight * 0.3 30 | 31 | return vector2(width, height) 32 | end 33 | 34 | -- ADD MARKER 35 | function functions.Add3DText(textData) 36 | local textId = functions.GenerateUniqueKey(texts) 37 | 38 | texts[textId] = { 39 | coords = textData.coords or vector3(0.0, 0.0, 0.0), 40 | text = textData.text or "No text set.", 41 | size = textData.size or 0.35, 42 | initialSize = textData.size or 0.35, -- used for distanceScale calculation 43 | wrap = textData.wrap or 1.0, 44 | font = textData.font or 4, 45 | distanceScale = textData.distanceScale == true, 46 | viewDistance = textData.viewDistance or 5.0, 47 | textEntry = textData.textEntry or textId, 48 | creator = GetInvokingResource() 49 | } 50 | 51 | if not textData.textEntry then 52 | AddTextEntry(textId, texts[textId].text) 53 | end 54 | 55 | texts[textId].textSize = functions.GetTextSize({ 56 | textEntry = texts[textId].textEntry, 57 | text = texts[textId].text, 58 | font = texts[textId].font, 59 | wrap = texts[textId].wrap, 60 | size = texts[textId].size 61 | }) 62 | 63 | amountTexts += 1 64 | return textId 65 | end 66 | 67 | -- REMOVE MARKER 68 | function functions.Remove3DText(textId) 69 | if texts[textId] then 70 | texts[textId] = nil 71 | amountTexts -= 1 72 | return true 73 | end 74 | return false 75 | end 76 | 77 | -- check for nearby 3d texts 78 | CreateThread(function() 79 | local lastCoords, lastAmount = vector3(0.0, 0.0, 0.0), 0 80 | 81 | while true do 82 | Wait(500) 83 | local selfCoords = GetEntityCoords(PlayerPedId()) 84 | 85 | if #(lastCoords - selfCoords) > 5.0 or lastAmount ~= amountTexts then 86 | lastCoords = selfCoords 87 | lastAmount = amountTexts 88 | 89 | local newNearby = {} 90 | for textId, textData in pairs(texts) do 91 | if textData and #(selfCoords - textData.coords) <= (Config.DrawDistance or 150.0) then 92 | newNearby[#newNearby + 1] = textId 93 | end 94 | Wait(0) 95 | end 96 | 97 | nearbyTexts = newNearby 98 | end 99 | end 100 | end) 101 | 102 | CreateThread(function() 103 | if not Config.Distancescale3DText then 104 | return 105 | end 106 | 107 | while true do 108 | Wait(500) 109 | 110 | while #nearbyTexts > 0 do 111 | Wait(25) 112 | local selfCoords = GetEntityCoords(PlayerPedId()) 113 | for _, textId in pairs(nearbyTexts) do 114 | if texts[textId] then 115 | local text = texts[textId] 116 | if text.distanceScale and #(selfCoords - text.coords) <= text.viewDistance then 117 | -- calculate font size 118 | local fov = GetGameplayCamFov() 119 | local camCoords = GetFinalRenderedCamCoord() 120 | local dist = #(camCoords - text.coords) 121 | local size = 1/(2 * math.abs(math.tan(math.rad(fov)/2)) * dist) / text.initialSize 122 | 123 | text.size = math.min(0.8, size) 124 | 125 | text.textSize = functions.GetTextSize({ 126 | textEntry = text.textEntry, 127 | text = text.text, 128 | font = text.font, 129 | wrap = text.wrap, 130 | size = text.size 131 | }) 132 | end 133 | end 134 | end 135 | end 136 | end 137 | end) 138 | 139 | -- HANDLE 3D TEXTS 140 | CreateThread(function() 141 | while true do 142 | Wait(500) 143 | 144 | while #nearbyTexts > 0 do 145 | Wait(0) 146 | local selfCoords = GetEntityCoords(PlayerPedId()) 147 | for _, textId in pairs(nearbyTexts) do 148 | if texts[textId] then 149 | local text = texts[textId] 150 | if #(selfCoords - text.coords) <= text.viewDistance then 151 | SetDrawOrigin(text.coords.x, text.coords.y, text.coords.z, 0) 152 | 153 | BeginTextCommandDisplayText(text.textEntry) 154 | SetTextScale(text.size, text.size) 155 | SetTextWrap(0.0, text.wrap) 156 | SetTextCentre(true) 157 | SetTextFont(4) 158 | EndTextCommandDisplayText(0.0, 0.0) 159 | 160 | DrawRect(0.0, text.textSize.y/2, math.min(text.wrap + 0.0015, text.textSize.x), text.textSize.y, 45, 45, 45, 150) 161 | 162 | ClearDrawOrigin() 163 | end 164 | end 165 | end 166 | end 167 | end 168 | end) 169 | 170 | AddEventHandler("onResourceStop", function(resourceName) 171 | if resourceName ~= GetCurrentResourceName() then 172 | local textsRemoved = 0 173 | for textId, textData in pairs(texts) do 174 | if textData.creator == resourceName then 175 | functions.Remove3DText(textId) 176 | textsRemoved += 1 177 | end 178 | end 179 | if textsRemoved > 0 then 180 | print(string.format("Removed %i 3d text%s due to resource %s stopping.", textsRemoved, textsRemoved > 1 and "s" or "", resourceName)) 181 | end 182 | end 183 | end) -------------------------------------------------------------------------------- /client/functions/utils.lua: -------------------------------------------------------------------------------- 1 | function functions.LoadAnimDict(dict) 2 | if not DoesAnimDictExist(dict) then 3 | return { success=false, error="Anim dict " .. dict .. " does not exist." } 4 | end 5 | 6 | local timer = GetGameTimer() + 5000 7 | 8 | RequestAnimDict(dict) 9 | while not HasAnimDictLoaded(dict) do 10 | Wait(0) 11 | if timer < GetGameTimer() then 12 | return { success=false, error="Loading anim dict timed out." } 13 | end 14 | end 15 | 16 | return { success=true, dict=dict } 17 | end 18 | 19 | function functions.LoadModel(model) 20 | model = type(model) == "string" and GetHashKey(model) or model 21 | 22 | if not IsModelInCdimage(model) then 23 | return { success=false, error="Model " .. model .. " does not exist (not in cd image)." } 24 | end 25 | 26 | local timer = GetGameTimer() + 5000 27 | 28 | RequestModel(model) 29 | while not HasModelLoaded(model) do 30 | Wait(0) 31 | if timer < GetGameTimer() then 32 | return { success=false, error="Loading model timed out." } 33 | end 34 | end 35 | 36 | return { success=true, model=model } 37 | end 38 | 39 | function functions.CopyText(text) 40 | if not text or type(text) ~= "string" then 41 | return { success=false, error="No text to copy was specified." } 42 | end 43 | 44 | SendNUIMessage({ 45 | type = "copy_text", 46 | content = text 47 | }) 48 | return { success=true } 49 | end 50 | 51 | ---@diagnostic disable-next-line: duplicate-set-field 52 | function functions.GenerateString(length) 53 | local id = "" 54 | for _ = 1, length or 7 do 55 | local char = math.random(1, 2) == 1 and string.char(math.random(97, 122)) or tostring(math.random(0, 9)) 56 | if math.random(1, 2) == 1 then 57 | char = string.upper(char) 58 | end 59 | id = id .. char 60 | end 61 | return id 62 | end 63 | 64 | ---@diagnostic disable-next-line: duplicate-set-field 65 | function functions.GenerateUniqueKey(t, length) 66 | local id = functions.GenerateString(length) 67 | 68 | if not t[id] then 69 | return id 70 | else 71 | return functions.GenerateUniqueKey(t, length) 72 | end 73 | end -------------------------------------------------------------------------------- /client/html/copy.js: -------------------------------------------------------------------------------- 1 | // CRED: https://github.com/TerbSEC/FiveM-CoordsSaver/blob/master/html/init.js 2 | function copyText(content) { 3 | var element = document.createElement("textarea"); 4 | var selection = document.getSelection(); 5 | 6 | element.textContent = content; 7 | document.body.appendChild(element); 8 | selection.removeAllRanges(); 9 | element.select(); 10 | document.execCommand("copy"); 11 | selection.removeAllRanges(); 12 | document.body.removeChild(element); 13 | } -------------------------------------------------------------------------------- /client/html/handler.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("message", function(event) { 2 | switch (event.data.type) { 3 | case "convert_base64": 4 | sendBase64Server(event.data); 5 | break; 6 | case "copy_text": 7 | copyText(event.data.content); 8 | break; 9 | default: 10 | console.log(`${event.data.type} is not a valid event`); 11 | break; 12 | } 13 | }); -------------------------------------------------------------------------------- /client/html/headshot_base64.js: -------------------------------------------------------------------------------- 1 | // CRED: https://stackoverflow.com/questions/6150289/how-can-i-convert-an-image-into-base64-string-using-javascript/20285053#20285053 2 | function toDataUrl(url, callback) { 3 | var xhr = new XMLHttpRequest(); 4 | xhr.onload = function() { 5 | var reader = new FileReader(); 6 | reader.onloadend = function() { 7 | callback(reader.result); 8 | } 9 | reader.readAsDataURL(xhr.response); 10 | }; 11 | xhr.open("GET", url); 12 | xhr.responseType = "blob"; 13 | xhr.send(); 14 | } 15 | 16 | function sendBase64Server(data) { 17 | toDataUrl(data.img, function(base64) { 18 | fetch(`https://${GetParentResourceName()}/base64`, { 19 | method: "POST", 20 | headers: {"Content-Type": "application/json; charset=UTF-8"}, 21 | body: JSON.stringify({ 22 | base64: base64, 23 | handle: data.handle, 24 | id: data.id 25 | }) 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /client/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |