Framework.CachedPlayerData = { job = nil, gang = nil, cash = nil, bank = nil, dirtyMoney = nil, hunger = nil, thirst = nil, stress = nil, micRange = 2, radioActive = false, voiceModes = 3, } ---@param vehicle integer ---@return number fuelLevel function Framework.Client.VehicleGetFuel(vehicle) if not DoesEntityExist(vehicle) then return 0 end if (Config.FuelSystem == "LegacyFuel" or Config.FuelSystem == "lc_fuel" or Config.FuelSystem == "ps-fuel" or Config.FuelSystem == "lj-fuel" or Config.FuelSystem == "cdn-fuel" or Config.FuelSystem == "hyon_gas_station" or Config.FuelSystem == "okokGasStation" or Config.FuelSystem == "nd_fuel" or Config.FuelSystem == "myFuel") then return exports[Config.FuelSystem]:GetFuel(vehicle) elseif Config.FuelSystem == "ti_fuel" then local level = exports["ti_fuel"]:getFuel(vehicle) return level elseif Config.FuelSystem == "ox_fuel" or Config.FuelSystem == "Renewed-Fuel" then return GetVehicleFuelLevel(vehicle) elseif Config.FuelSystem == "rcore_fuel" then return exports.rcore_fuel:GetVehicleFuelPercentage(vehicle) else return 100 -- or set up custom fuel system here... end end ---Toggles the engine, this is used in the keybind & in the vehicle control panel ---@param vehicle integer ---@param on boolean on or off function Framework.Client.ToggleEngine(vehicle, on) -- !! Consider adding a key script check here - to disallow enabling the engine if no keys are present SetVehicleEngineOn(vehicle, on, false, true) end ---Code to run when seatbelt is toggled ---Please note; you need Config.UseCustomSeatbeltIntegration = true for this to take effect! ---@param vehicle integer ---@param seatbeltOn boolean true = seatbelt on, false = seatbelt off function Framework.Client.ToggleSeatbelt(vehicle, seatbeltOn) if GetResourceState("jim-mechanic") == "started" then if (seatbeltOn and not exports["jim-mechanic"]:seatBeltOn()) or (seatbeltOn and exports["jim-mechanic"]:seatBeltOn()) then TriggerEvent("jim-mechanic:client:ToggleSeatbelt") end end -- ...or add another custom seatbelt integration here! end ---@return table | false playerData function Framework.Client.GetPlayerData() if Config.Framework == "QBCore" then ---@diagnostic disable-next-line: undefined-field return QBCore.Functions.GetPlayerData() elseif Config.Framework == "Qbox" then return exports.qbx_core:GetPlayerData() elseif Config.Framework == "ESX" then ---@diagnostic disable-next-line: undefined-field return ESX.GetPlayerData() end return false end ---@return string? function Framework.Client.GetPlayerJob() local player = Framework.Client.GetPlayerData() if not player or not player.job then return nil end if Config.Framework == "QBCore" or Config.Framework == "Qbox" then return string.format("%s (%s)", player.job.label, player.job.grade.name) elseif Config.Framework == "ESX" then return string.format("%s (%s)", player.job.label, player.job.grade_label) end return nil end ---@return string? function Framework.Client.GetPlayerGang() if GetResourceState("rcore_gangs") == "started" then local gang = exports.rcore_gangs:GetPlayerGang() if not gang then return nil end return gang.tag end local player = Framework.Client.GetPlayerData() if not player or not player.gang then return nil end if Config.Framework == "QBCore" or Config.Framework == "Qbox" then return string.format("%s (%s)", player.gang.label, player.gang.grade.name) elseif Config.Framework == "ESX" then return nil end return nil end ---@return boolean dead function Framework.Client.IsPlayerDead() if Config.Framework == "QBCore" then local player = Framework.Client.GetPlayerData() if not player then return false end if player.metadata?.isdead or player.metadata?.inlaststand then return true end elseif Config.Framework == "Qbox" then return LocalPlayer.state.isDead elseif Config.Framework == "ESX" then return IsEntityDead(cache.ped) end return false end ---@param type "cash" | "bank" | "money" | "dirtyMoney" | "black_money" ---@return number? balance function Framework.Client.GetBalance(type) if Config.Framework == "QBCore" then ---@diagnostic disable-next-line: undefined-field local playerData = QBCore.Functions.GetPlayerData() if type == "dirtyMoney" then -- Fetch "markedbills" from inventory local dirty = 0 for _, item in pairs(playerData.items or {}) do if item.name == "markedbills" then if item.info and item.info.worth then dirty = dirty + item.info.worth end end end return dirty end return playerData?.money?[type] or 0 elseif Config.Framework == "Qbox" then if type == "dirtyMoney" and GetResourceState("ox_inventory") == "started" then -- Fetch dirty_money from ox_inv return exports.ox_inventory:GetItemCount("black_money") or 0 end return exports.qbx_core:GetPlayerData()?.money?[type] or 0 elseif Config.Framework == "ESX" then if type == "cash" then type = "money" end if type == "dirtyMoney" then type = "black_money" end ---@diagnostic disable-next-line: undefined-field local playerData = ESX.GetPlayerData() if not playerData then return 0 end for i, acc in pairs(playerData?.accounts or {}) do if acc.name == type then return acc.money or 0 end end return 0 end return nil end ---@param vehicle integer function Framework.Client.GetVehicleMileageInKm(vehicle) if not vehicle or vehicle == 0 then return false end -- t1ger_mechanic integration if GetResourceState("t1ger_mechanic") == "started" then return exports["t1ger_mechanic"]:GetVehicleMileage(vehicle) end -- jim-mechanic integration if GetResourceState("jim-mechanic") == "started" then local plate = GetVehicleNumberPlateText(vehicle) return exports["jim-mechanic"]:GetMilage(plate) end -- This is for jg-vehiclemileage (jg-mechanic), as it's stored in a statebag -- If you're using a different vehicle mileage script, you can add the export for it here! :) return Entity(vehicle).state.vehicleMileage or 0 end ---@param km number ---@return number function Framework.Client.ConvertKmToMiles(km) return km * 0.621371 end function Framework.Client.ConvertSpeed(speed, unit) if unit == "mph" then return speed * 2.236936 end if unit == "kph" then return speed * 3.6 end return speed end function Framework.Client.ConvertDistance(dist, unit) if unit == "meters" then return dist * 1 end if unit == "feet" then return dist * 3.28084 end return dist end -- -- From this point on, it's setting up framework event listeners -- local function updateCachedData(player) if Config.ShowComponents.job then Framework.CachedPlayerData.job = Framework.Client.GetPlayerJob() end if Config.ShowComponents.gang then Framework.CachedPlayerData.gang = Framework.Client.GetPlayerGang() end if Config.ShowComponents.cashBalance then Framework.CachedPlayerData.cash = Framework.Client.GetBalance("cash") end if Config.ShowComponents.bankBalance then Framework.CachedPlayerData.bank = Framework.Client.GetBalance("bank") end if Config.ShowComponents.dirtyMoneyBalance then Framework.CachedPlayerData.dirtyMoney = Framework.Client.GetBalance("dirtyMoney") end if Config.Framework == "QBCore" and player then Framework.CachedPlayerData.hunger = lib.math.clamp(player.metadata?.hunger or 0, 0, 100) Framework.CachedPlayerData.thirst = lib.math.clamp(player.metadata?.thirst or 0, 0, 100) end end function Framework.Client.SetupPlayerLoginListeners() if Config.Framework == "QBCore" then RegisterNetEvent("QBCore:Client:OnPlayerLoaded", function() LocalPlayer.state:set("jgHudPlayerLoggedIn", true) updateCachedData(Framework.Client.GetPlayerData()) end) RegisterNetEvent("QBCore:Client:OnPlayerUnload", function() LocalPlayer.state:set("jgHudPlayerLoggedIn", false) end) elseif Config.Framework == "Qbox" then AddStateBagChangeHandler("isLoggedIn", ("player:%s"):format(cache.serverId), function(_, _, loggedIn) LocalPlayer.state:set("jgHudPlayerLoggedIn", loggedIn) end) elseif Config.Framework == "ESX" then local function onPlayerLoad() lib.waitFor(function() return Framework.Client.GetPlayerData() and cache.ped end, "Ped has not loaded or GetPlayerData returned false (waited 30 seconds)", 30000) LocalPlayer.state:set("jgHudPlayerLoggedIn", true) Framework.CachedPlayerData.job = Framework.Client.GetPlayerJob() end RegisterNetEvent("esx:playerLoaded", onPlayerLoad) RegisterNetEvent("esx:onPlayerSpawn", onPlayerLoad) RegisterNetEvent("esx:onPlayerLogout", function() LocalPlayer.state:set("jgHudPlayerLoggedIn", false) end) else -- No framework, just say they are already logged in. -- Or you can add your own system or exports here! Just update the -- LocalPlayer jgHudPlayerLoggedIn true/false and everything will work! lib.waitFor(function() if cache.ped then LocalPlayer.state:set("jgHudPlayerLoggedIn", true) return true end end, "[Standalone] Ped never loaded in; could not login (waited 500 seconds)", 500000) end end function Framework.Client.CreateEventListeners() local state = LocalPlayer.state Framework.CachedPlayerData.hunger = lib.math.clamp(state.hunger or 0, 0, 100) Framework.CachedPlayerData.thirst = lib.math.clamp(state.thirst or 0, 0, 100) Framework.CachedPlayerData.stress = lib.math.clamp(state.stress or 0, 0, 100) if (Config.Framework == "ESX" or Config.Framework == "QBCore" or Config.Framework == "Qbox") and Framework.Client.GetPlayerData() then updateCachedData(Framework.Client.GetPlayerData()) end if Config.Framework == "ESX" then RegisterNetEvent("esx_status:onTick", function(data) local newHunger, newThirst for i = 1, #data do if data[i].name == "thirst" then newThirst = math.floor(data[i].percent) elseif data[i].name == "hunger" then newHunger = math.floor(data[i].percent) end end Framework.CachedPlayerData.hunger = lib.math.clamp(newHunger or 0, 0, 100) Framework.CachedPlayerData.thirst = lib.math.clamp(newThirst or 0, 0, 100) end) RegisterNetEvent("esx:setJob", function(job) Framework.CachedPlayerData.job = string.format("%s (%s)", job.label, job.grade_label) end) RegisterNetEvent("esx:setAccountMoney", function(account) if account.name == "money" then Framework.CachedPlayerData.cash = account.money end if account.name == "bank" then Framework.CachedPlayerData.bank = account.money end if account.name == "black_money" then Framework.CachedPlayerData.dirtyMoney = account.money end end) end if Config.Framework == "QBCore" or Config.Framework == "Qbox" then RegisterNetEvent("hud:client:UpdateNeeds", function(newHunger, newThirst) Framework.CachedPlayerData.hunger = newHunger Framework.CachedPlayerData.thirst = newThirst end) RegisterNetEvent("hud:client:OnMoneyChange", function(type) -- The reason I fetch them directly from `GetBalance` and don't use the amount/isMinus args is because -- they didn't consistently work for me - especially when using /setmoney in QB or Qbox if type == "cash" then Framework.CachedPlayerData.cash = Framework.Client.GetBalance("cash") end if type == "bank" then Framework.CachedPlayerData.bank = Framework.Client.GetBalance("bank") end end) RegisterNetEvent("QBCore:Client:OnJobUpdate", function() Framework.CachedPlayerData.job = Framework.Client.GetPlayerJob() end) RegisterNetEvent("QBCore:Client:OnGangUpdate", function() Framework.CachedPlayerData.gang = Framework.Client.GetPlayerGang() end) end if Config.Framework == "Qbox" then local playerState = LocalPlayer.state Framework.CachedPlayerData.hunger = lib.math.clamp(playerState.hunger or 0, 0, 100) Framework.CachedPlayerData.thirst = lib.math.clamp(playerState.thirst or 0, 0, 100) AddEventHandler("ox_inventory:itemCount", function(itemName, totalCount) if itemName == "black_money" then Framework.CachedPlayerData.dirtyMoney = totalCount end end) AddStateBagChangeHandler("hunger", ("player:%s"):format(cache.serverId), function(_, _, value) Framework.CachedPlayerData.hunger = lib.math.clamp(value or 0, 0, 100) end) AddStateBagChangeHandler("thirst", ("player:%s"):format(cache.serverId), function(_, _, value) Framework.CachedPlayerData.thirst = lib.math.clamp(value or 0, 0, 100) end) end -- rcore_gangs if GetResourceState("rcore_gangs") == "started" then RegisterNetEvent("rcore_gangs:client:set_gang", function(gang) Framework.CachedPlayerData.gang = gang?.tag end) end -- jg-stress integration AddStateBagChangeHandler("stress", ("player:%s"):format(cache.serverId), function(_, _, value) Framework.CachedPlayerData.stress = lib.math.clamp(value or 0, 0, 100) end) -- PMA Voice if GetResourceState("pma-voice") == "started" then AddEventHandler("pma-voice:setTalkingMode", function(mode) Framework.CachedPlayerData.micRange = mode end) AddEventHandler("pma-voice:radioActive", function(isActive) Framework.CachedPlayerData.radioActive = isActive end) AddEventHandler("pma-voice:settingsCallback", function(voiceSettings) Framework.CachedPlayerData.voiceModes = #voiceSettings.voiceModes end) end end