diff --git a/frameworkCache.lua b/frameworkCache.lua new file mode 100644 index 0000000..f95f596 --- /dev/null +++ b/frameworkCache.lua @@ -0,0 +1,292 @@ +--[[ + Cached Resource Initialization Module + -------------------------------------- + This module initializes shared data (Items, Vehicles, Jobs, Gangs) + only once and stores it in _G.__jimBridgeDataCache for reuse across scripts. +]] + +local Exports = { + QBExport = "qb-core", + QBXExport = "qbx_core", + ESXExport = "es_extended", + OXCoreExport = "ox_core", + + OXInv = "ox_inventory", + QBInv = "qb-inventory", + PSInv = "ps-inventory", + QSInv = "qs-inventory", + CoreInv = "core_inventory", + CodeMInv = "codem-inventory", + OrigenInv = "origen_inventory", + TgiannInv = "tgiann-inventory", + + OXLibExport = "ox_lib", + + QBMenuExport = "qb-menu", + + QBTargetExport = "qb-target", + OXTargetExport = "ox_target", + + -- REDM + RSGExport = "rsg-core", + RSGInv = "rsg-inventory" +} + +-- Ensure cache only runs once +if _G.__jimBridgeDataCache then return end +_G.__jimBridgeDataCache = {} +local cache = _G.__jimBridgeDataCache + +function checkExists(resourceName) + return GetResourceState(resourceName):find("start") or GetResourceState(resourceName):find("stopped") +end + +local fileLoader = assert(load(LoadResourceFile("oxmysql", ('lib/MySQL.lua')), ('@@oxmysql/lib/MySQL.lua'))) +fileLoader() +if checkExists(Exports.OXCoreExport) then + -- Detected OX_Core in server, wait for it to be started if needed + while GetResourceState(Exports.OXCoreExport) ~= "started" do Wait(100) end + local fileLoader = assert(load(LoadResourceFile(Exports.OXCoreExport, ('lib/init.lua')), ('@@'..Exports.OXCoreExport..'/lib/init.lua'))) + fileLoader() +end +if checkExists(Exports.ESXExport) then + -- Detected ESX in server, wait for it to be started if needed + while GetResourceState(Exports.ESXExport) ~= "started" do Wait(100) end + local fileLoader = assert(load(LoadResourceFile(Exports.ESXExport, ('/imports.lua')), ('@@'..Exports.ESXExport..'/imports.lua'))) + fileLoader() +end + +-- Init variables +local Items, Vehicles, Jobs, Gangs, Core = nil, nil, nil, nil, nil +local itemResource, jobResource, vehResource = "", "", "" + +-- Print just to announce it knows the exports/scripts exist in the server +for _, v in pairs(Exports) do + if checkExists(v) then + print("^6Bridge^7: '^3"..v.."^7' detected") + end +end + +--------------------- +---- Load Items ----- +--------------------- +if checkExists(Exports.OXInv) then + -- Wait for OX Inventory to start if it's not already started + while GetResourceState(Exports.OXInv) ~= "started" do Wait(100) end + itemResource = Exports.OXInv + Items = exports[Exports.OXInv]:Items() + + -- Get Weapon info and duplicate them if they are uppercase + -- (duplicate incase anything checks for the uppercase version) + for k, v in pairs(Items) do + if k:find("WEAPON") then + Items[k:lower()] = Items[k] + Items[k:lower()].image = k..".png" + end + Items[k].image = (v.client and v.client.image) and v.client.image:gsub("nui://"..Exports.OXInv.."/web/images/", "") or k..".png" + Items[k].hunger = v.client and v.client.hunger + Items[k].thirst = v.client and v.client.thirst + end + +elseif checkExists(Exports.QBExport) then + while GetResourceState(Exports.QBExport) ~= "started" do Wait(100) end + itemResource = Exports.QBExport + Core = exports[Exports.QBExport]:GetCoreObject() + Items = Core.Shared.Items + if Items == nil then + print("^1ERROR^7: ^1Can NOT find shared ^7Items ^1table^7, ^1possible error in that file^7?") + end + +elseif checkExists(Exports.ESXExport) then + itemResource = Exports.ESXExport + if GetResourceState(Exports.QSInv):find("start") then + Items = exports[Exports.QSInv]:GetItemList() + else + Items = ESX.GetItems() + while not next(Items) do + Items = ESX.GetItems() + Wait(1000) + end + end + +elseif checkExists(Exports.RSGExport) then + while GetResourceState(Exports.RSGExport) ~= "started" do Wait(100) end + itemResource = Exports.RSGExport + Core = exports[Exports.RSGExport]:GetCoreObject() + Items = Core.Shared.Items + if Items == nil then + print("^1ERROR^7: ^1Can NOT find shared ^7Items ^1table^7, ^1possible error in that file^7?") + end + +end + +--------------------- +--- Load Vehicles --- +--------------------- +if checkExists(Exports.QBXExport) or checkExists(Exports.QBExport) then + vehResource = Exports.QBExport + Core = Core or exports[Exports.QBExport]:GetCoreObject() + Vehicles = Core.Shared.Vehicles + if Vehicles == nil then + print("^1ERROR^7: ^1Can NOT find shared ^7Vehicles ^1table^7, ^1possible error in that file^7?") + end + +elseif checkExists(Exports.OXCoreExport) then + vehResource = Exports.OXCoreExport + Vehicles = {} + for k, v in pairs(Ox.GetVehicleData()) do + Vehicles[k] = { + model = k, hash = GetHashKey(k), + price = v.price, + name = v.name, + brand = v.make + } + end + +elseif checkExists(Exports.ESXExport) then + vehResource = Exports.ESXExport + while not MySQL do Wait(1000) end + Vehicles = {} + for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do + Vehicles[v.model] = { + model = v.model, + hash = GetHashKey(v.model), + price = v.price, + name = v.name, + } + end + +elseif checkExists(Exports.RSGExport) then + vehResource = Exports.RSGExport + Core = Core or exports[Exports.RSGExport]:GetCoreObject() + Vehicles = Core.Shared.Vehicles + if Vehicles == nil then + print("^1ERROR^7: ^1Can NOT find shared ^7Vehicles ^1table^7, ^1possible error in that file^7?") + end + +end + +--------------------- +----- Load Jobs ----- +--------------------- +if checkExists(Exports.QBXExport) then + jobResource = Exports.QBXExport + Core = Core or exports[Exports.QBXExport]:GetCoreObject() + Jobs, Gangs = exports[Exports.QBXExport]:GetJobs(), exports[Exports.QBXExport]:GetGangs() + if Jobs == nil then + print("^1ERROR^7: ^1Can NOT find shared ^7Jobs ^1table^7, ^1possible error in that file^7?") + end + +elseif checkExists(Exports.OXCoreExport) then + jobResource = Exports.OXCoreExport + Jobs = {} + while not MySQL do Wait(1000) end + local tempJobs = MySQL.query.await('SELECT * FROM `ox_groups`') + local tempGrades = MySQL.query.await('SELECT * FROM `ox_group_grades`') + local gradeMap = {} + for _, grade in pairs(tempGrades) do + gradeMap[grade.group] = gradeMap[grade.group] or {} + gradeMap[grade.group][grade.grade] = { name = grade.label } + end + for _, job in pairs(tempJobs) do + Jobs[job.name] = { + label = job.label, + grades = gradeMap[job.name] or {} + } + end + Gangs = Jobs + +elseif checkExists(Exports.QBExport) then + jobResource = Exports.QBExport + Core = Core or exports[Exports.QBExport]:GetCoreObject() + Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs + +elseif checkExists(Exports.ESXExport) then + jobResource = Exports.ESXExport + ESX = exports[Exports.ESXExport]:getSharedObject() + Jobs = ESX.GetJobs() + while not next(Jobs) do + Wait(100) + Jobs = ESX.GetJobs() + end + for Role, Grades in pairs(Jobs) do + -- Check for if user has added grades + if Grades.grades == nil or not next(Grades.grades) then + goto continue + end + for grade, info in pairs(Grades.grades) do + if info.label and info.label:find("[Bb]oss") then + Jobs[Role].grades[grade].isBoss = true + goto continue + end + end + local highestGrade = nil + for k in pairs(Grades.grades) do + local num = tonumber(k) + if num and (not highestGrade or num > highestGrade) then + highestGrade = num + end + end + + if highestGrade then + Jobs[Role].grades[tostring(highestGrade)].isBoss = true + end + ::continue:: + end + Gangs = Jobs + +elseif checkExists(Exports.RSGExport) then + jobResource = Exports.RSGExport + Core = Core or exports[Exports.RSGExport]:GetCoreObject() + Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs + if Jobs == nil then + print("^1ERROR^7: ^1Can NOT find shared ^7Jobs ^1table^7, ^1possible error in that file^7?") + end + +end + +-- Save to global cache +cache.Items = Items +cache.Vehicles = Vehicles +cache.Jobs = Jobs +cache.Gangs = Gangs + +CreateThread(function() + local counts = { + Items = 0, + Vehicles = 0, + Jobs = 0, + Gangs = 0, + } + for k, v in pairs(cache) do + for count in pairs(v) do + counts[k] += 1 + end + end + print("^6FrameworkCache^7: ^2Loaded ^5"..tostring(counts.Items).."^2 Items from ^7"..itemResource) + print("^6FrameworkCache^7: ^2Loaded ^5"..tostring(counts.Vehicles).."^2 Vehicles from ^7"..vehResource) + print("^6FrameworkCache^7: ^2Loaded ^5"..tostring(counts.Jobs).."^2 Jobs from ^7"..jobResource) + print("^6FrameworkCache^7: ^2Loaded ^5"..tostring(counts.Gangs).."^2 Gangs from ^7"..jobResource) +end) + +--print(json.encode(cache.Items , { indent = true})) + +RegisterNetEvent("jim_bridge:requestCache", function() + local src = source + TriggerClientEvent("jim_bridge:receiveCache", src, _G.__jimBridgeDataCache) +end) + + +exports("GetSharedData", function() + -- Wait for data to be ready before returning it + local timeout = GetGameTimer() + 5000 + while ( + not _G.__jimBridgeDataCache or + (not _G.__jimBridgeDataCache.Items or next(_G.__jimBridgeDataCache.Items) == nil) or + (not _G.__jimBridgeDataCache.Vehicles or next(_G.__jimBridgeDataCache.Vehicles) == nil) or + (not _G.__jimBridgeDataCache.Jobs or next(_G.__jimBridgeDataCache.Jobs) == nil) + ) and GetGameTimer() < timeout do + Wait(50) + end + return _G.__jimBridgeDataCache +end) \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua index 2a2c436..a02c8d8 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -1,6 +1,6 @@ name "Jim_Bridge" author "Jimathy" -version "2.0.14" +version "2.0.15" description "Framework Bridge By Jimathy" fx_version "cerulean" rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' @@ -16,4 +16,11 @@ files { } -- Version checker -server_scripts { '_versioncheck.lua' } \ No newline at end of file +server_scripts { + 'frameworkCache.lua', + '_versioncheck.lua' +} + +client_scripts { + 'ui_modules/*.lua' +} \ No newline at end of file diff --git a/shared/_loaders.lua b/shared/_loaders.lua index 3112745..1c50bff 100644 --- a/shared/_loaders.lua +++ b/shared/_loaders.lua @@ -52,7 +52,7 @@ function onPlayerLoaded(func, onStart) onPlayerFramework = ESXExport AddEventHandler('esx:playerLoaded', function() if waitForSharedLoad() then - if isStarted(ESXExport) then Wait(11000) end + if isStarted(ESXExport) then Wait(1000) end tempFunc() end end @@ -105,12 +105,16 @@ end --- -- Initialization code on resource start. --- end, true) --- ``` +local hasPrinted = false function onResourceStart(func, thisScript) debugPrint("^6Bridge^7: ^2Registering ^3onResourceStart^7()") AddEventHandler('onResourceStart', function(resourceName) if getScript() == resourceName and (thisScript or true) then if waitForSharedLoad() then - debugPrint("^6Bridge^7: ^2Shared Load Detected^7.") + if not hasPrinted then + debugPrint("^6Bridge^7: ^2Shared Load Detected^7.") + hasPrinted = true + end if isStarted(ESXExport) then Wait(10000) end func() end @@ -186,22 +190,38 @@ function waitForLogin() end end +local messageShown = false function waitForSharedLoad() local timeout = 100000 -- 10 seconds in milliseconds local startTime = GetGameTimer() local loaded = true + --Wait(1000) + local count = {} + local loop = 0 while ((not Jobs or not next(Jobs)) and (not Items or not next(Items)) and (not Vehicles or not next(Vehicles))) and (GetGameTimer() - startTime) < timeout do - print((GetGameTimer() - startTime) < timeout) + if not messageShown then + if (not Jobs or not next(Jobs)) then + debugPrint("^4Debug^7: ^2Waiting for Jobs to be loaded") + end + if (not Items or not next(Items)) then + debugPrint("^4Debug^7: ^2Waiting for Items to be loaded") + end + if (not Vehicles or not next(Vehicles)) then + debugPrint("^4Debug^7: ^2Waiting for Vehicles to be loaded") + end + end + messageShown = true + --print((GetGameTimer() - startTime) < timeout) Wait(1000) - debugPrint("Waiting for Jobs, Items, and Vehicles to be loaded") if Jobs and Items and Vehicles then - print("^6Bridge^7: ^2Jobs, Items, and Vehicles Loaded^7.") + --print("^6Bridge^7: ^2Jobs, Items, and Vehicles Loaded^7.") loaded = true break end + loop += 1 end if not loaded then - print("^4Error^7: ^2Timeout reached while waiting for shared load^7.") + print("^4Error^7: ^1Timeout reached while waiting for shared load^7.") return false else return true diff --git a/shared/contextmenus.lua b/shared/contextmenus.lua index 48a0b39..d4d8f15 100644 --- a/shared/contextmenus.lua +++ b/shared/contextmenus.lua @@ -194,7 +194,7 @@ function openMenu(Menu, data) exports[QBMenuExport]:openMenu(Menu) elseif Config.System.Menu == "gta" then - WarMenu.CreateMenu(tostring(Menu), data.header, data.headertxt or " ", { + WarMenu.CreateMenu(tostring(Menu), data.header, " ", { titleColor = { 222, 255, 255 }, maxOptionCountOnScreen = 15, width = 0.25, @@ -232,6 +232,7 @@ function openMenu(Menu, data) if pressed and not Menu[k].isMenuHeader then WarMenu.CloseMenu() close = false + Wait(10) Menu[k].onSelect() end end diff --git a/shared/coreloader.lua b/shared/coreloader.lua index 6deddb0..5d32050 100644 --- a/shared/coreloader.lua +++ b/shared/coreloader.lua @@ -1,25 +1,8 @@ ---[[ - Resource Initialization Module - -------------------------------- - This module initializes and loads shared data (Items, Vehicles, Jobs, Gangs) from the - various frameworks/inventory systems (OX, QB, ESX, etc.). It also corrects export names, - caches framework exports into simple variables, and prints debug information if enabled. -]] - -------------------------------------------------------------- --- Global Variable Initialization -------------------------------------------------------------- Items, Vehicles, Jobs, Gangs, Core = {}, nil, nil, nil, nil -------------------------------------------------------------- --- Correct QB Inventory Export -------------------------------------------------------------- --- Correct ps-invetory to ls-inventory if somehow you still have that +-- Shared Exports Initialization Exports.PSInv = isStarted("lj-inventory") and "lj-inventory" or Exports.PSInv -------------------------------------------------------------- --- Framework Exports and Inventory Identifiers -------------------------------------------------------------- OXLibExport, QBXExport, QBExport, ESXExport, OXCoreExport = Exports.OXLibExport or "", Exports.QBXExport or "", @@ -37,310 +20,83 @@ OXInv, QBInv, PSInv, QSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv = Exports.OrigenInv or "", Exports.TgiannInv or "" -RSGExport, RSGInv = - Exports.RSGExport or "", - Exports.RSGInv or "" - +RSGExport, RSGInv = Exports.RSGExport or "", Exports.RSGInv or "" QBMenuExport = Exports.QBMenuExport or "" QBTargetExport, OXTargetExport = Exports.QBTargetExport or "", Exports.OXTargetExport or "" -------------------------------------------------------------- --- Debug: Print Found Exports -------------------------------------------------------------- --- Print a list of all exports that are currently started (if debugMode is enabled). -for _, v in pairs(Exports) do - if isStarted(v) then - debugPrint("^6Bridge^7: '^3"..v.."^7' export found") - end -end - -OxPlayer = nil -if isStarted(OXCoreExport) then - if not isServer() then - OxPlayer = Ox.GetPlayer() - end -end - -------------------------------------------------------------- --- Resource Variables for Items, Jobs, and Vehicles -------------------------------------------------------------- -local itemResource, jobResource, vehResource = "", "", "" - -------------------------------------------------------------- --- Loading Items -------------------------------------------------------------- --- Load and compile shared items from the detected inventory system. -if isStarted(OXInv) then - itemResource = OXInv - Items = exports[OXInv]:Items() - - -- Add weapons to Items from QBXCore if available - if isStarted(QBXExport) then - local tempWeapons = exports[QBExport]:GetCoreObject().Shared.Weapons - for k, v in pairs(tempWeapons) do - local tempWeaponInfo = exports[OXInv]:Items(v.name) - local weight = 0 - if tempWeaponInfo then - weight = tempWeaponInfo.weight - end - if not Items[v.name] then - Items[v.name] = { - name = v.name, - label = v.label, - type = "weapon", - ammotype = v.ammotype or "AMMO_PISTOL", - weight = weight, - image = v.image or (v.name..".png"), - description = v.label or "", - } - end - end - end - for k, v in pairs(Items) do - if v.client and v.client.image then - Items[k].image = (v.client.image):gsub("nui://"..OXInv.."/web/images/", "") - else - Items[k].image = k..".png" - end - Items[k].hunger = v.client and v.client.hunger or nil - Items[k].thirst = v.client and v.client.thirst or nil - end - -elseif isStarted(QBExport) then - itemResource = QBExport - Core = Core or exports[QBExport]:GetCoreObject() - Items = Core and Core.Shared.Items or nil - CreateThread(function() - while not Items or not next(Items) do - Items = exports[QBExport]:GetCoreObject().Shared.Items - Wait(1000) - end - end) - if isStarted(QBExport) and not isStarted(QBXExport) then - RegisterNetEvent('QBCore:Client:UpdateObject', function() - Core = Core or exports[QBExport]:GetCoreObject() - Items = Core and Core.Shared.Items or nil - end) - end - -elseif isStarted(ESXExport) then - itemResource = ESXExport - CreateThread(function() - if isServer() then - Items = ESX.GetItems() - while not createCallback do Wait(100) end - createCallback(getScript()..":getItems", function(source) - return Items - end) - end - if not isServer() then - Items = triggerCallback(getScript()..":getItems") - debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Items).." ^3Items^2 from ^7"..itemResource) - end - end) - -elseif isStarted(RSGExport) then - itemResource = RSGExport - Core = Core or exports[RSGExport]:GetCoreObject() - Items = Core and Core.Shared.Items or nil - RegisterNetEvent('RSGCore:Client:UpdateObject', function() - Core = Core or exports[RSGExport]:GetCoreObject() - Items = Core and Core.Shared.Items or nil - end) - -end - -if itemResource == nil then - print("^4ERROR^7: ^2No Item info detected ^7- ^2Check ^3starter^1.^2lua^7") -else - while not Items do Wait(100) end - debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Items).." ^3Items^2 from ^7"..itemResource) -end - - -------------------------------------------------------------- --- Loading Vehicles -------------------------------------------------------------- --- Compile vehicles from the detected frameworks into a unified table. if isStarted(QBXExport) or isStarted(QBExport) then - vehResource = QBExport Core = Core or exports[QBExport]:GetCoreObject() - Vehicles = Core and Core.Shared.Vehicles - if isStarted(QBExport) and not isStarted(QBXExport) then - RegisterNetEvent('QBCore:Client:UpdateObject', function() - Core = Core or exports[QBExport]:GetCoreObject() - Vehicles = Core and Core.Shared.Vehicles - end) - end +elseif isStarted(RSGExport) then + Core = Core or exports[RSGExport]:GetCoreObject() +end -elseif isStarted(OXCoreExport) then - vehResource = OXCoreExport - Vehicles = {} - for k, v in pairs(Ox.GetVehicleData()) do - Vehicles[k] = { model = k, hash = GetHashKey(k), price = v.price, name = v.name, brand = v.make } - end -elseif isStarted(ESXExport) then - vehResource = ESXExport +if IsDuplicityVersion() then CreateThread(function() - if isServer() then - createCallback(getScript()..":getVehiclesPrices", function(source) - return Vehicles - end) - while not MySQL do Wait(2000) print("^1Waiting for MySQL to exist") end - Vehicles = MySQL.query.await('SELECT model, price, name FROM vehicles') + local cache = nil + local timeout = GetGameTimer() + 5000 -- 5 seconds max wait + + -- Wait until jim_bridge is started and export is available + while not cache and GetGameTimer() < timeout do + if GetResourceState("jim_bridge"):find("start") then + local success, result = pcall(function() + return exports["jim_bridge"]:GetSharedData() + end) + if success and result then + cache = result + --print(json.encode(cache, {indent = true})) + end + end + Wait(100) end - if not isServer() then - --while not triggerCallback do print("waiting") Wait(100) end - local TempVehicles = triggerCallback(getScript()..":getVehiclesPrices") - for _, v in pairs(TempVehicles) do - Vehicles = Vehicles or {} + + if not cache then + print("^1ERROR^7: ^2jim_bridge export not available after timeout^7.") + return + end + + Items = cache.Items + Vehicles = cache.Vehicles + Jobs = cache.Jobs + Gangs = cache.Gangs + + debugPrint("^6Bridge^7: ^2Shared cache successfully loaded from export^7.") + end) +else + local hasCache = false + -- 🔹 Client Side: Request from server + _G.__jimBridgeDataCache = {} + + RegisterNetEvent("jim_bridge:receiveCache", function(data) + if not hasCache then + _G.__jimBridgeDataCache = data + hasCache = true + else + return + end + end) + + TriggerServerEvent("jim_bridge:requestCache") + + CreateThread(function() + while not _G.__jimBridgeDataCache or not next(_G.__jimBridgeDataCache) do Wait(50) end + local cache = _G.__jimBridgeDataCache + Items = cache.Items or {} + Vehicles = cache.Vehicles or {} + Jobs = cache.Jobs or {} + Gangs = cache.Gangs or {} + + if isStarted(ESXExport) then + for _, v in pairs(Vehicles) do Vehicles[v.model] = { model = v.model, - hash = GetHashKey(v.model), + hash = v.hash, price = v.price, name = v.name, brand = GetMakeNameFromVehicleModel(v.model):lower():gsub("^%l", string.upper) } end end - end) -elseif isStarted(RSGExport) then - vehResource = RSGExport - Core = Core or exports[RSGExport]:GetCoreObject() - Vehicles = Core and Core.Shared.Vehicles - RegisterNetEvent('RSGCore:Client:UpdateObject', function() - Core = Core or exports[RSGExport]:GetCoreObject() - Vehicles = Core and Core.Shared.Vehicles end) end -if vehResource == nil then - print("^4ERROR^7: ^2No Vehicle info detected ^7- ^2Check ^3starter^1.^2lua^7") -else - while not Vehicles do Wait(1000) end - debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..vehResource) -end - -------------------------------------------------------------- --- Loading Jobs and Gangs -------------------------------------------------------------- --- Compile jobs and gangs from the detected framework. -if isStarted(QBXExport) then - jobResource = QBXExport - Core = Core or exports[QBExport]:GetCoreObject() - Jobs, Gangs = exports[QBXExport]:GetJobs(), exports[QBXExport]:GetGangs() - -elseif isStarted(OXCoreExport) then - jobResource = OXCoreExport - CreateThread(function() - if isServer() then - Jobs = {} - createCallback(getScript()..":getOxGroups", function(source) - return Jobs - end) - local tempJobs = MySQL.query.await('SELECT * FROM `ox_groups`') - local tempGrades = MySQL.query.await('SELECT * FROM `ox_group_grades`') - -- Index all grades by group - local gradeMap = {} - for _, grade in pairs(tempGrades) do - gradeMap[grade.group] = gradeMap[grade.group] or {} - gradeMap[grade.group][grade.grade] = { - name = grade.label - } - end - - -- Process jobs and attach grades - for _, job in pairs(tempJobs) do - Jobs[job.name] = { - label = job.label, - grades = gradeMap[job.name] or {} - } - end - - -- Copy to Gangs - Gangs = Jobs - else - Jobs = triggerCallback(getScript()..":getOxGroups") - Gangs = Jobs - end - end) - -elseif isStarted(QBExport) then - jobResource = QBExport - Core = Core or exports[QBExport]:GetCoreObject() - Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs - if isStarted(QBExport) and not isStarted(QBXExport) then - RegisterNetEvent('QBCore:Client:UpdateObject', function() - Core = exports[QBExport]:GetCoreObject() - Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs - end) - end - -elseif isStarted(ESXExport) then - jobResource = ESXExport - if isServer() then - -- If server, create callback to get jobs - createCallback(getScript()..":getESXJobs", function(source) - return Jobs - end) - -- Populate jobs table with ESX.GetJobs() - Jobs = ESX.GetJobs() - --jsonPrint(Jobs) - --If retreived jobs is empty, wait for ESX to load - while countTable(Jobs) == 0 do - Jobs = ESX.GetJobs() - Wait(100) - end - -- Organise into a table the script can use - for Role, Grades in pairs(Jobs) do - - -- Check for "Boss" in name of grades - for grade, info in pairs(Grades.grades) do - --print(grade) - --jsonPrint(info) - - if info.label then - --print(info) - if info.label:find("boss") or info.label:find("Boss") then - --print("Found Boss label for:", Grades.label) - Jobs[Role].grades[grade].isBoss = true - goto continue - end - end - end - - -- If no roles with "boss" in the name, revert to max grade - - -- Count grades - local count = countTable(Grades.grades) - Jobs[Role].grades[tostring(count-1)].isBoss = true - --print(Grades.label.." Grade: "..count.." is Boss") - ::continue:: - end - -- ESX Default doesn't have gangs, so copy jobs to gangs - Gangs = Jobs - end - -- If client side, trigger callback to get jobs - if not isServer() then - Jobs = triggerCallback(getScript()..":getESXJobs") - Gangs = Jobs - end - -elseif isStarted(RSGExport) then - jobResource = RSGExport - Core = Core or exports[RSGExport]:GetCoreObject() - Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs - RegisterNetEvent('RSGCore:Client:UpdateObject', function() - Core = exports[RSGExport]:GetCoreObject() - Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs - end) -end - -if jobResource == nil then - print("^4ERROR^7: ^2No Job info detected ^7- ^2Check ^3starter^1.^2lua^7") -else - while not Jobs do Wait(1000) end - debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Jobs).." ^3Jobs^2 from ^7"..jobResource) - debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Gangs).." ^3Gangs^2 from ^7"..jobResource) -end \ No newline at end of file diff --git a/shared/crafting.lua b/shared/crafting.lua index aac352d..f2507f3 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -334,6 +334,10 @@ function makeItem(data) if isInventoryOpen() then print("^1Error^7: ^2Inventory is open, you tried to break things") crafted, crafting = false, false + stopTempCam() + ClearPedTasks(PlayerPedId()) + if canReturn then craftingMenu(data) end + CraftLock = false return end if crafting and progressBar({ @@ -353,10 +357,15 @@ function makeItem(data) Wait(200) end if isInventoryOpen() then - --print("^1Error^7: ^2Inventory is open, you tried to break things") - crafted, crafting, CraftLock = false, false, false + print("^1Error^7: ^2Inventory is open, you tried to break things") + crafted, crafting = false, false + stopTempCam() + ClearPedTasks(PlayerPedId()) + if canReturn then craftingMenu(data) end + CraftLock = false return end + if crafted then local craftProp = nil if prop then diff --git a/shared/helpers.lua b/shared/helpers.lua index d5cb6b7..e332138 100644 --- a/shared/helpers.lua +++ b/shared/helpers.lua @@ -56,6 +56,25 @@ function isServer() return IsDuplicityVersion() end +function checkExportExists(resource, export) + if not resource or not export then return false end + + local exists = false + local ok, result = pcall(function() + exists = type(exports[resource][export]) == "function" + end) + + if ok and exists then + debugPrint(("^3Export Check^7: ^5exports^7['^5%s^7']:^5%s^7() ^2Exists^7"):format(resource, export)) + return true + else + debugPrint(("^3Export Check^7: ^5exports^7['^5%s^7']:^5%s^7() ^1Doesn^7'^1t Exist^7"):format(resource, export)) + return false + end +end + + + ------------------------------------------------------------- -- Debugging and JSON Utilities ------------------------------------------------------------- diff --git a/shared/inventories.lua b/shared/inventories.lua index 2fa671f..9e850a2 100644 --- a/shared/inventories.lua +++ b/shared/inventories.lua @@ -176,39 +176,44 @@ function getPlayerInv(src) end function isInventoryOpen() - if isStarted(OXInv) then - return LocalPlayer.state.invBusy - elseif isStarted(QSInv) then - return exports[QSInv]:inInventory() + return IsNuiFocused() - elseif isStarted(OrigenInv) then - return exports[OrigenInv]:IsInventoryOpen() + --if isStarted(OXInv) then + -- return LocalPlayer.state.invBusy - elseif isStarted(CoreInv) then - return exports[CoreInv]:isInventoryOpen() + --elseif isStarted(QSInv) then + -- return exports[QSInv]:inInventory() - elseif isStarted(CodeMInv) then - return false - -- CodeM doesn't have a function to check if the inventory is open - -- No idea what it uses, so it just skips the check + --elseif isStarted(OrigenInv) then + -- return exports[OrigenInv]:IsInventoryOpen() - elseif isStarted(TgiannInv) then - return exports[TgiannInv]:IsInventoryActive() + --elseif isStarted(CoreInv) then + -- return exports[CoreInv]:isInventoryOpen() - elseif isStarted(QBInv) then - return LocalPlayer.state.inv_busy + --elseif isStarted(CodeMInv) then + -- return false + -- -- CodeM doesn't have a function to check if the inventory is open + -- -- No idea what it uses, so it just skips the check - elseif isStarted(PSInv) then - return LocalPlayer.state.inv_busy + --elseif isStarted(TgiannInv) then + -- return IsNuiFocused() - elseif ESX and isStarted(ESXExport) then - return false + --elseif isStarted(QBInv) then + -- return LocalPlayer.state.inv_busy + + --elseif isStarted(PSInv) then + -- return LocalPlayer.state.inv_busy + + --elseif ESX and isStarted(ESXExport) then + -- return false + + --elseif isStarted(RSGInv) then + -- return LocalPlayer.state.inv_busy + + --end - elseif isStarted(RSGInv) then - return LocalPlayer.state.inv_busy - end end ------------------------------------------------------------- diff --git a/shared/notify.lua b/shared/notify.lua index 4dc327c..9959106 100644 --- a/shared/notify.lua +++ b/shared/notify.lua @@ -50,18 +50,10 @@ function triggerNotify(title, message, type, src) TriggerClientEvent('ox_lib:notify', src, { title = title, description = message, type = type or "success" }) end elseif Config.System.Notify == "gta" then - if isStarted("jim-gtaui") then - if not src then - TriggerEvent("jim-gtaui:Notify", title, message, type) - else - TriggerClientEvent("jim-gtaui:Notify", src, title, message, type) - end + if not src then + exports.jim_bridge:Notify(title, message, type) else - if not src then - TriggerEvent(getScript()..":DisplayGTANotify", title, message) - else - TriggerClientEvent(getScript()..":DisplayGTANotify", src, title, message) - end + TriggerClientEvent("jim-bridge:Notify", src, title, message, type) end elseif Config.System.Notify == "esx" then if not src then @@ -116,25 +108,25 @@ end) --- ```lua --- TriggerEvent(getScript()..":DisplayGTANotify", "taxiname", "Taxi service has arrived.") --- ``` -RegisterNetEvent(getScript()..":DisplayGTANotify", function(title, text) - local iconTable = {} - if getScript() == "jim-npcservice" then - iconTable = { - [Loc[Config.Lan].notify["taxiname"]] = "CHAR_TAXI", - [Loc[Config.Lan].notify["limoname"]] = "CHAR_CASINO", - [Loc[Config.Lan].notify["ambiname"]] = "CHAR_CALL911", - [Loc[Config.Lan].notify["pilotname"]] = "CHAR_DEFAULT", - [Loc[Config.Lan].notify["planename"]] = "CHAR_BOATSITE2", - [Loc[Config.Lan].notify["heliname"]] = "CHAR_BOATSITE2", - } - end - - BeginTextCommandThefeedPost("STRING") - AddTextComponentSubstringKeyboardDisplay(text) - EndTextCommandThefeedPostMessagetext( - iconTable[title] or "CHAR_DEFAULT", - iconTable[title] or "CHAR_DEFAULT", - true, 1, title, nil, text - ) - EndTextCommandThefeedPostTicker(true, false) -end) \ No newline at end of file +--RegisterNetEvent(getScript()..":DisplayGTANotify", function(title, text) +-- local iconTable = {} +-- if getScript() == "jim-npcservice" then +-- iconTable = { +-- [Loc[Config.Lan].notify["taxiname"]] = "CHAR_TAXI", +-- [Loc[Config.Lan].notify["limoname"]] = "CHAR_CASINO", +-- [Loc[Config.Lan].notify["ambiname"]] = "CHAR_CALL911", +-- [Loc[Config.Lan].notify["pilotname"]] = "CHAR_DEFAULT", +-- [Loc[Config.Lan].notify["planename"]] = "CHAR_BOATSITE2", +-- [Loc[Config.Lan].notify["heliname"]] = "CHAR_BOATSITE2", +-- } +-- end +-- +-- BeginTextCommandThefeedPost("STRING") +-- AddTextComponentSubstringKeyboardDisplay(text) +-- EndTextCommandThefeedPostMessagetext( +-- iconTable[title] or "CHAR_DEFAULT", +-- iconTable[title] or "CHAR_DEFAULT", +-- true, 1, title, nil, text +-- ) +-- EndTextCommandThefeedPostTicker(true, false) +--end) \ No newline at end of file diff --git a/shared/phones.lua b/shared/phones.lua index df04b1c..3f9b1a1 100644 --- a/shared/phones.lua +++ b/shared/phones.lua @@ -73,6 +73,11 @@ function sendPhoneMail(data) TriggerServerEvent('qb-phone:server:sendNewMail', mailData) end, }, + { name = "npwd_qbx_mail", + send = function(mailData) + TriggerServerEvent('qb-phone:server:sendNewMail', mailData) + end, + }, { name = "jpr-phonesystem", send = function(mailData) TriggerServerEvent(getScript()..":jpr:SendMail", mailData) @@ -85,23 +90,125 @@ function sendPhoneMail(data) }, } - local activePhone = nil -- Check each phone system in order and use the first active one. for _, phone in ipairs(phoneSystems) do if isStarted(phone.name) then - activePhone = phone.name + debugPrint("^6Bridge^7[^3"..phone.name.."^7]: ^2Sending mail to player") phone.send(data) - break + return true end end - - if activePhone then - debugPrint("^6Bridge^7[^3"..activePhone.."^7]: ^2Sending mail to player") - else - print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found") - end + print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found") + return false end +function sendPhoneInvoice(data) + if not data.src then return end + -- Define each supported phone system and its corresponding mail-sending function. + local phoneSystems = { + { + name = "qb-phone", + send = function(mailData) + -- Defensive check for required fields + local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "src" } + for _, field in ipairs(required) do + if mailData[field] == nil then + print("^1[jim-payments] ERROR:^0 Missing field '" .. field .. "' in mailData") + return + end + end + + -- Safe insert with all parameters present + MySQL.Async.insert( + 'INSERT INTO phone_invoices (citizenid, amount, society, sender, sendercitizenid) VALUES (?, ?, ?, ?, ?)', + { + mailData.billedCitizenid, + mailData.amount, + mailData.job, + mailData.name, + mailData.billerCitizenid + }, + function(id) + if id then + TriggerClientEvent('qb-phone:client:AcceptorDenyInvoice', mailData.src, id, mailData.name, mailData.job, mailData.billerCitizenid, mailData.amount, GetInvokingResource()) + end + end + ) + + TriggerClientEvent('qb-phone:RefreshPhone', mailData.src) + end + }, + { + name = "codem-phone", + send = function(mailData) + -- Defensive check for required fields + local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "src" } + for _, field in ipairs(required) do + if mailData[field] == nil then + print("^1[jim-payments] ERROR:^0 Missing field '" .. field .. "' in mailData") + return + end + end + + -- Safe insert with all parameters present + MySQL.Async.insert( + 'INSERT INTO phone_invoices (citizenid, amount, society, sender, sendercitizenid) VALUES (?, ?, ?, ?, ?)', + { + mailData.billedCitizenid, + mailData.amount, + mailData.job, + mailData.name, + mailData.billerCitizenid + }, + function(id) + -- if id then + -- TriggerClientEvent('qb-phone:client:AcceptorDenyInvoice', mailData.src, id, mailData.name, mailData.job, mailData.billerCitizenid, mailData.amount, GetInvokingResource()) + -- end + end + ) + + + end + }, + { + name = "gks-phone", + send = function(mailData) + -- Defensive check for required fields + local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "label" } + for _, field in ipairs(required) do + if mailData[field] == nil then + print("^1[jim-payments] ERROR:^0 Missing field '" .. field .. "' in mailData") + return + end + end + + MySQL.Async.execute( + 'INSERT INTO gksphone_invoices (citizenid, amount, society, sender, sendercitizenid, label) VALUES (@citizenid, @amount, @society, @sender, @sendercitizenid, @label)', + { + ['@citizenid'] = mailData.billedCitizenid, + ['@amount'] = mailData.amount, + ['@society'] = mailData.job, + ['@sender'] = mailData.name, + ['@sendercitizenid'] = mailData.billerCitizenid, + ['@label'] = mailData.label + } + ) + end + } + } + + + -- Check each phone system in order and use the first active one. + for _, phone in ipairs(phoneSystems) do + if isStarted(phone.name) then + debugPrint("^6Bridge^7[^3"..phone.name.."^7]: ^2Sending mail to player^7", data.src) + phone.send(data) + return true + end + end + print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found") + return false +end ------------------------------------------------------------- -- Phone System Event Handlers ------------------------------------------------------------- diff --git a/shared/playerfunctions.lua b/shared/playerfunctions.lua index ab76ccf..5f84207 100644 --- a/shared/playerfunctions.lua +++ b/shared/playerfunctions.lua @@ -153,6 +153,18 @@ end -- Economy Event Handlers ------------------------------------------------------------- +--- BillPlayer +function billPlayer(data) + if not Config.System.Billing or Config.System.Billing == "jim" then + TriggerEvent("jim-payments:client:Charge", { + job = data.job, + gang = data.gang, + coords = data.coords.xyz, + img = data.img + }) + end +end + --- Charges a player by removing money from their account. --- --- @param cost number The amount to charge. diff --git a/shared/shops.lua b/shared/shops.lua index 5e686b6..7cd3edd 100644 --- a/shared/shops.lua +++ b/shared/shops.lua @@ -164,12 +164,16 @@ function openShop(data) elseif isStarted(OXInv) then exports[OXInv]:openInventory('shop', { type = data.shop }) - elseif isStarted(QSInv) or isStarted(CodeMInv) then + elseif isStarted(QSInv) then TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items) elseif isStarted(TgiannInv) then TriggerServerEvent(getScript()..':server:openServerShop', data.shop) + elseif isStarted(CodeMInv) then + --print(data.shop) + TriggerEvent("codem-inventory:openshop", data.shop) + elseif isStarted(QBInv) then if QBInvNew then TriggerServerEvent(getScript()..':server:openServerShop', data.shop) diff --git a/shared/skillcheck.lua b/shared/skillcheck.lua index 945d69a..f4d3c79 100644 --- a/shared/skillcheck.lua +++ b/shared/skillcheck.lua @@ -30,127 +30,10 @@ function skillCheck(data) result = false end elseif Config.System.skillCheck == "gta" then - loadTextureDict("timerbars") - local successes = 0 - local barsRequired = 3 + exports.jim_bridge:skillCheck() - for bar = 1, barsRequired do - debugPrint("^6Bridge^7: ^2Starting Bar ^3"..bar.."^7/^3"..barsRequired.."^7") - activeSkillCheck = true - local width, height = 0.2, 0.01 - local x, y = 0.5, 0.8 - - -- Random highlighted zone - local highlightSize = math.random(10, 20) / 100 - local highlightStart = math.random(10, 50) / 100 - local highlightEnd = highlightStart + highlightSize - local highlightAlpha = 0 - local cursorPos = 0.0 - local cursorSpeed = 0.025 - local movingRight = true - - while activeSkillCheck do - Wait(0) - makeInstructionalButtons({ - { keys = { 177 }, text = "Exit" }, - { keys = { 38 }, text = "Confirm" }, - }) - - createScaleBars(x, y, width, height) - - local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect - highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha - - -- Draw highlighted zone (success area) with pulsing effect - DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha ) - -- Draw moving cursor - DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) - - -- Move cursor - if movingRight then - cursorPos += cursorSpeed - if cursorPos >= 1.0 then movingRight = false end - else - cursorPos -= cursorSpeed - if cursorPos <= 0.0 then movingRight = true end - end - - if IsControlJustPressed(0, 177) then -- Backspace to cancel - local displayTime = GetGameTimer() + 2000 - PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) - while GetGameTimer() < displayTime do - Wait(0) - - createScaleBars(x, y, width, height) - - DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 228, 52, 52, 255) - DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) - - drawSuccessText(x, y, "Failed", 228, 52, 52) - end - return false - end - - -- Check for keypress (E) - if IsControlJustPressed(0, 38) then - activeSkillCheck = false - result = cursorPos >= highlightStart and cursorPos <= highlightEnd - if result then - PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) - else - PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) - end - local displayTime = GetGameTimer() + 2000 - while GetGameTimer() < displayTime do - Wait(0) - - createScaleBars(x, y, width, height) - - -- Draw highlighted zone - DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180) - - -- Draw stationary cursor at result position - DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) - -- Display result text - drawSuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52) - end - if result then - successes += 1 - else - return false - end - end - end - end - activeSkillCheck = false - debugPrint("^6Bridge^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7") - return successes == barsRequired else result = true end return result -end - -function drawSuccessText(x, y, text, r, g, b) - SetTextFont(8) - SetTextScale(0.45, 0.45) - SetTextColour(r, g, b, 255) - SetTextDropshadow(0, 0, 0, 0, 255) - SetTextEdge(2, 0, 0, 0, 150) - SetTextDropShadow() - SetTextOutline() - SetTextCentre(true) - SetTextEntry("STRING") - SetTextCentre(true) - SetTextEntry("STRING") - AddTextComponentString(text) - DrawText(x, y + 0.03) -end - -function createScaleBars(x, y, width, height) - -- Draw background box - DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255) - DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255) - -- Draw full bar (dark background) - DrawRect(x, y, width, height, 100, 100, 100, 255) end \ No newline at end of file diff --git a/shared/stashcontrol.lua b/shared/stashcontrol.lua index b94cd1e..a7f2edd 100644 --- a/shared/stashcontrol.lua +++ b/shared/stashcontrol.lua @@ -222,6 +222,60 @@ RegisterNetEvent(getScript()..":server:openServerStash", function(data) end end) +function clearStash(stashId) + if isStarted(QBInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..QBInv.."^2 Stash^7", stashId) + if QBInvNew then + exports[QBInv]:ClearStash(stashId) + else + MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { + ['stash'] = stashId, + ['items'] = json.encode({}) + }) + end + + elseif isStarted(OXInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..OXInv.."^2 Stash^7", stashId) + exports[OXInv]:ClearInventory(stashId) + + elseif isStarted(PSInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..PSInv.."^2 Stash^7", stashId) + if QBInvNew then + exports[QBInv]:ClearStash(stashId) + else + MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { + ['stash'] = stashId, + ['items'] = json.encode({}) + }) + end + + elseif isStarted(QSInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..QSInv.."^2 Stash^7", stashId) + exports[QSInv]:ClearOtherInventory('stash', stashId) + + elseif isStarted(CoreInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..CoreInv.."^2 Stash^7", stashId) + exports[CoreInv]:clearInventory("stash-"..stashId) + + elseif isStarted(CodeMInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..CodeMInv.."^2 Stash^7", stashId) + exports[CodeMInv]:ClearInventory(stashId) + + elseif isStarted(OrigenInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..OrigenInv.."^2 Stash^7", stashId) + exports[OrigenInv]:ClearInventory(stashId) + + elseif isStarted(TgiannInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..TgiannInv.."^2 Stash^7", stashId) + exports["tgiann-inventory"]:DeleteInventory("stash", stashId) + + elseif isStarted(RSGInv) then + debugPrint("^5Bridge^7: ^2Cleared ^3"..RSGInv.."^2 Stash^7", stashId) + exports[RSGInv]:ClearStash(stashId) + + end +end + ------------------------------------------------------------- -- Stash Retrieval Function diff --git a/shared/targets.lua b/shared/targets.lua index 8eba814..10f16c5 100644 --- a/shared/targets.lua +++ b/shared/targets.lua @@ -20,24 +20,7 @@ ------------------------------------------------------------- -- Utility Data & Tables ------------------------------------------------------------- ---- -local KEY_TABLE = { 38, 29, 47, 23, 45, } --- Mapping of key codes to human-readable key names. -local Keys = { - [322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5", - [167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10", - [243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5", - [159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=", - [177] = "BACKSPACE", [37] = "TAB", - [44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y", - [303] = "U", [199] = "P", - [39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS", - [34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G", - [74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT", - [20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N", - [244] = "M", [82] = ",", [81] = "." -} -- Tables for storing created targets for the fallback system and zone management. local TextTargets = {} -- For fallback DrawText3D targets. @@ -82,38 +65,9 @@ function createEntityTarget(entity, opts, dist) -- Fallback: Use DrawText3D if targeting systems are disabled or unavailable. if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - local entityCoords = GetEntityCoords(entity) - debugPrint("^6Bridge^7: ^2Creating new ^3Entity^2 target with DrawText for entity ^7"..entity) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - entityCoords) < 0.01 then - existingTarget = target - break - end - end + debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..OXTargetExport.." ^2for entity ^7"..entity) + exports.jim_bridge:createEntityTarget(entity, opts, dist) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[entity] = { - coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z), - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..OXTargetExport.." ^2for entity ^7"..entity) local options = {} @@ -129,10 +83,12 @@ function createEntityTarget(entity, opts, dist) } end exports[OXTargetExport]:addLocalEntity(entity, options) + elseif isStarted(QBTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..QBTargetExport.." ^2for entity ^7"..entity) local options = { options = opts, distance = dist } exports[QBTargetExport]:AddTargetEntity(entity, options) + end end @@ -192,37 +148,8 @@ end function createBoxTarget(data, opts, dist) if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6DrawText ^2 for zone ^7"..data[1]) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - data[2]) < 0.01 then - existingTarget = target - break - end - end + return exports.jim_bridge:createZoneTarget(data, opts, dist) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[data[1]] = { - coords = data[2], - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end - return data[1] elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..OXTargetExport.." ^2for zone ^7"..data[1]) local options = {} @@ -250,12 +177,14 @@ function createBoxTarget(data, opts, dist) }) boxTargets[#boxTargets + 1] = target return target + elseif isStarted(QBTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..QBTargetExport.." ^2for zone ^7"..data[1]) local options = { options = opts, distance = dist } local target = exports[QBTargetExport]:AddBoxZone(data[1], data[2], data[3], data[4], data[5], options) boxTargets[#boxTargets + 1] = target return data[1] + end end @@ -298,37 +227,8 @@ end function createCircleTarget(data, opts, dist) if Config.System.DontUseTarget then debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6DrawText ^2for zone ^7"..data[1]) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - data[2]) < 0.01 then - existingTarget = target - break - end - end + return exports.jim_bridge:createZoneTarget(data, opts, dist) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[data[1]] = { - coords = data[2], - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end - return data[1] elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6"..OXTargetExport.." ^2for zone ^7"..data[1]) local options = {} @@ -387,30 +287,8 @@ end ---``` function createModelTarget(models, opts, dist) if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - if type(models) ~= "table" then - models = { models } - end + return exports.jim_bridge:createModelTarget(models, opts, dist) - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - - local keyStr = "" - for i, m in ipairs(models) do - keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") - end - local targetKey = "model_" .. keyStr - - TextTargets[targetKey] = { - models = models, - buttontext = tempText, - options = opts, - dist = dist, - coords = vec3(0, 0, 0), - text = table.concat(tempText, "\n") - } elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Model^2 target with ^6"..OXTargetExport) local options = {} @@ -453,7 +331,7 @@ function removeEntityTarget(entity) exports[OXTargetExport]:removeLocalEntity(entity, nil) end if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - TextTargets[entity] = nil + exports.jim_bridge:removeEntityTarget(entity) end end @@ -474,7 +352,7 @@ function removeZoneTarget(target) exports[OXTargetExport]:removeZone(target, true) end if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - TextTargets[target] = nil + exports.jim_bridge:removeZoneTarget(target) end end @@ -494,7 +372,7 @@ function removeModelTarget(model) exports[OXTargetExport]:removeModel(model, nil) end if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - TextTargets[entity] = nil + exports.jim_bridge:removeZoneTarget(target) end end ------------------------------------------------------------- @@ -504,69 +382,105 @@ end -- If no targeting system is detected and this is a client script, use DrawText3D for targets. if (Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport))) and not isServer() then CreateThread(function() - local wait = 1000 while true do - local pedCoords = GetEntityCoords(PlayerPedId()) + local ped = PlayerPedId() + local pedCoords = GetEntityCoords(ped) local camCoords = GetGameplayCamCoord() local camRot = GetGameplayCamRot(2) local camForward = RotationToDirection(camRot) - local closestTarget, closestDist = nil, math.huge - local notificationShown = false + + local closestTarget = nil + local closestDist = math.huge local targetEntity = nil - -- Update model targets and determine the closest target. - for _, target in pairs(TextTargets) do + + -- Shallow copy for safety + local targetsCopy = {} + for k, v in pairs(TextTargets) do + targetsCopy[k] = v + end + + -- Detect models and update coords + for _, target in pairs(targetsCopy) do if target.models then - for _, model in ipairs(target.models) do - local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false) - if entity and entity ~= 0 then - target.coords = GetEntityCoords(entity) - targetEntity = entity - break + if not target.entity or not DoesEntityExist(target.entity) then + for _, model in ipairs(target.models) do + local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false) + if entity and entity ~= 0 then + target.entity = entity + target.coords = GetEntityCoords(entity) + break + end end + else + target.coords = GetEntityCoords(target.entity) end end + end - local dist = #(pedCoords - target.coords) - if dist <= target.dist then + -- Identify closest visible target + for _, target in pairs(targetsCopy) do + if target.coords then + local dist = #(pedCoords - target.coords) local vecToTarget = target.coords - camCoords local normVec = normalizeVector(vecToTarget) local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z - if dot > 0.5 and dist < closestDist then - closestDist = dist - closestTarget = target + local isFacing = dot > 0.5 + + if dist <= target.dist and isFacing then + if dist < closestDist then + closestDist = dist + closestTarget = target + targetEntity = target.entity + end end end end - -- Render targets, listen for key presses and display the help notification. - for key, target in pairs(TextTargets) do - if #(pedCoords - target.coords) <= target.dist then + -- Render + handle input + for _, target in pairs(targetsCopy) do + if target.coords and #(pedCoords - target.coords) <= target.dist then local isClosest = (target == closestTarget) - for i, opt in ipairs(target.options) do + + for _, opt in ipairs(target.options) do if IsControlJustPressed(0, opt.key) and isClosest then - if opt.onSelect then opt.onSelect(targetEntity) end - if opt.action then opt.action(targetEntity) end + local canInteract = (not target.canInteract or target.canInteract()) + local hasItem = (not opt.item or hasItem(opt.item)) + local hasJob = (not opt.job or hasJob(opt.job, nil)) + + if canInteract and hasItem and hasJob then + if opt.onSelect then opt.onSelect(targetEntity) end + if opt.action then opt.action(targetEntity) end + end end end - notificationShown = true - ShowFloatingHelpNotification(vec3(target.coords.x, target.coords.y, target.coords.z + 0.7), target.text) + -- Draw each eligible text line + local baseZ = target.coords.z + 1.0 + local lineHeight = -0.16 + local lineOffset = 0 + + for i, opt in ipairs(target.options) do + local canInteract = (not target.canInteract or target.canInteract()) + local hasItem = (not opt.item or hasItem(opt.item)) + local hasJob = (not opt.job or hasJob(opt.job, nil)) + + if canInteract and hasItem and hasJob then + local text = target.buttontext[i] + local zOffset = lineOffset * lineHeight + DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + zOffset), text, isClosest) + lineOffset += 1 + end + end end end - -- If no notification was drawn this frame, clear help messages. - if notificationShown then - wait = 0 - else - ClearAllHelpMessages() - wait = 1000 - end - Wait(wait) + Wait(0) end end) end + function ShowFloatingHelpNotification(coord, text, highlight) AddTextEntry("FloatingText", text) SetFloatingHelpTextWorldPosition(1, coord.x, coord.y, coord.z) diff --git a/shared/warmenu.lua b/shared/warmenu.lua new file mode 100644 index 0000000..901ec9a --- /dev/null +++ b/shared/warmenu.lua @@ -0,0 +1,694 @@ +WarMenu = { } +WarMenu.__index = WarMenu + +if not isServer() then + +-- Deprecated +WarMenu.debug = false +function WarMenu.SetDebugEnabled(enabled) +end +function WarMenu.IsDebugEnabled() + return false +end +--- + +local menus = { } +local keys = { down = 187, scrollDown = 242, up = 188, scrollUp = 241, left = 189, right = 190, select = 191, accept = 237, back = 194, cancel = 238 } + +local skipInputNextFrame = true +local optionCount = 0 + +local currentKey = nil +local currentMenu = nil + +local toolTipWidth = 0.153 + +local spriteWidth = 0.027 +local spriteHeight = spriteWidth * GetAspectRatio() + +local titleHeight = 0.101 +local titleYOffset = 0.021 +local titleFont = 1 +local titleScale = 1.0 + +local buttonHeight = 0.038 +local buttonFont = 0 +local buttonScale = 0.365 +local buttonTextXOffset = 0.005 +local buttonTextYOffset = 0.005 +local buttonSpriteXOffset = 0.002 +local buttonSpriteYOffset = 0.005 + +local defaultStyle = { + x = 0.0175, + y = 0.025, + width = 0.23, + maxOptionCountOnScreen = 10, + titleVisible = true, + titleColor = { 0, 0, 0, 255 }, + titleBackgroundColor = { 245, 127, 23, 255 }, + titleBackgroundSprite = nil, + subTitleColor = { 245, 127, 23, 255 }, + textColor = { 254, 254, 254, 255 }, + subTextColor = { 189, 189, 189, 255 }, + focusTextColor = { 0, 0, 0, 255 }, + focusColor = { 245, 245, 245, 255 }, + backgroundColor = { 0, 0, 0, 160 }, + subTitleBackgroundColor = { 0, 0, 0, 255 }, + buttonPressedSound = { name = 'SELECT', set = 'HUD_FRONTEND_DEFAULT_SOUNDSET' }, --https://pastebin.com/0neZdsZ5 +} + +local function IsNavigatedDown() + return IsControlJustReleased(2, keys.down) or IsControlJustReleased(2, keys.scrollDown) +end + +local function IsNavigatedUp() + return IsControlJustReleased(2, keys.up) or IsControlJustReleased(2, keys.scrollUp) +end + +local function IsSelectedPressed() + return IsControlJustReleased(2, keys.select) or IsControlJustReleased(2, keys.accept) +end + +local function IsBackPressed() + return IsControlJustReleased(2, keys.back) or IsControlJustReleased(2, keys.cancel) +end + +local function setMenuProperty(id, property, value) + if not id then + return + end + + local menu = menus[id] + if menu then + menu[property] = value + end +end + +local function setStyleProperty(id, property, value) + if not id then + return + end + + local menu = menus[id] + + if menu then + if not menu.overrideStyle then + menu.overrideStyle = { } + end + + menu.overrideStyle[property] = value + end +end + +local function getStyleProperty(property, menu) + menu = menu or currentMenu + + if menu.overrideStyle then + local value = menu.overrideStyle[property] + if value ~= nil then + return value + end + end + + return menu.style and menu.style[property] or defaultStyle[property] +end + +local function getTitleHeight() + return getStyleProperty('titleVisible') and titleHeight or 0 +end + +local function copyTable(t) + if type(t) ~= 'table' then + return t + end + + local result = { } + for k, v in pairs(t) do + result[k] = copyTable(v) + end + + return result +end + +local function setMenuVisible(id, visible, holdCurrentOption) + if currentMenu then + if visible then + if currentMenu.id == id then + return + end + else + if currentMenu.id ~= id then + return + end + end + end + + if visible then + local menu = menus[id] + + if not currentMenu then + menu.currentOption = 1 + else + if not holdCurrentOption then + menus[currentMenu.id].currentOption = 1 + end + end + + currentMenu = menu + skipInputNextFrame = true + + SetUserRadioControlEnabled(false) + HudWeaponWheelIgnoreControlInput(true) + else + HudWeaponWheelIgnoreControlInput(false) + SetUserRadioControlEnabled(true) + + currentMenu = nil + end +end + +local function setTextParams(font, color, scale, center, shadow, alignRight, wrapFrom, wrapTo) + SetTextFont(font) + SetTextColour(color[1], color[2], color[3], color[4] or 255) + SetTextScale(scale, scale) + + if shadow then + SetTextDropShadow() + end + + if center then + SetTextCentre(true) + elseif alignRight then + SetTextRightJustify(true) + end + + if not wrapFrom or not wrapTo then + wrapFrom = wrapFrom or getStyleProperty('x') + wrapTo = wrapTo or getStyleProperty('x') + getStyleProperty('width') - buttonTextXOffset + end + + SetTextWrap(wrapFrom, wrapTo) +end + +local function getLinesCount(text, x, y) + BeginTextCommandLineCount('TWOSTRINGS') + AddTextComponentString(tostring(text)) + return EndTextCommandGetLineCount(x, y) +end + +local function drawText(text, x, y) + BeginTextCommandDisplayText('TWOSTRINGS') + AddTextComponentString(tostring(text)) + EndTextCommandDisplayText(x, y) +end + +local function drawRect(x, y, width, height, color) + DrawRect(x, y, width, height, color[1], color[2], color[3], color[4] or 255) +end + +local function getCurrentIndex() + if currentMenu.currentOption <= getStyleProperty('maxOptionCountOnScreen') and optionCount <= getStyleProperty('maxOptionCountOnScreen') then + return optionCount + elseif optionCount > currentMenu.currentOption - getStyleProperty('maxOptionCountOnScreen') and optionCount <= currentMenu.currentOption then + return optionCount - (currentMenu.currentOption - getStyleProperty('maxOptionCountOnScreen')) + end + + return nil +end + +local function drawTitle() + if not getStyleProperty('titleVisible') then + return + end + + local x = getStyleProperty('x') + getStyleProperty('width') / 2 + local y = getStyleProperty('y') + titleHeight / 2 + + if getStyleProperty('titleBackgroundSprite') then + DrawSprite(getStyleProperty('titleBackgroundSprite').dict, getStyleProperty('titleBackgroundSprite').name, x, y, getStyleProperty('width'), titleHeight, 0., 255, 255, 255, 255) + else + drawRect(x, y, getStyleProperty('width'), titleHeight, getStyleProperty('titleBackgroundColor')) + end + + if currentMenu.title then + setTextParams(titleFont, getStyleProperty('titleColor'), titleScale, true) + drawText(currentMenu.title, x, y - titleHeight / 2 + titleYOffset) + end +end + +local function drawSubTitle() + local x = getStyleProperty('x') + getStyleProperty('width') / 2 + local y = getStyleProperty('y') + getTitleHeight() + buttonHeight / 2 + + drawRect(x, y, getStyleProperty('width'), buttonHeight, getStyleProperty('subTitleBackgroundColor')) + + setTextParams(buttonFont, getStyleProperty('subTitleColor'), buttonScale, false) + drawText(currentMenu.subTitle, getStyleProperty('x') + buttonTextXOffset, y - buttonHeight / 2 + buttonTextYOffset) + + if optionCount > getStyleProperty('maxOptionCountOnScreen') then + setTextParams(buttonFont, getStyleProperty('subTitleColor'), buttonScale, false, false, true) + drawText(tostring(currentMenu.currentOption)..' / '..tostring(optionCount), getStyleProperty('x') + getStyleProperty('width'), y - buttonHeight / 2 + buttonTextYOffset) + end +end + +local function drawButton(text, subText) + local currentIndex = getCurrentIndex() + if not currentIndex then + return + end + + local backgroundColor = nil + local textColor = nil + local subTextColor = nil + local shadow = false + + if currentMenu.currentOption == optionCount then + backgroundColor = getStyleProperty('focusColor') + textColor = getStyleProperty('focusTextColor') + subTextColor = getStyleProperty('focusTextColor') + else + backgroundColor = getStyleProperty('backgroundColor') + textColor = getStyleProperty('textColor') + subTextColor = getStyleProperty('subTextColor') + shadow = true + end + + local x = getStyleProperty('x') + getStyleProperty('width') / 2 + local y = getStyleProperty('y') + getTitleHeight() + buttonHeight + (buttonHeight * currentIndex) - buttonHeight / 2 + + drawRect(x, y, getStyleProperty('width'), buttonHeight, backgroundColor) + + setTextParams(buttonFont, textColor, buttonScale, false, shadow) + drawText(text, getStyleProperty('x') + buttonTextXOffset, y - (buttonHeight / 2) + buttonTextYOffset) + + if subText then + setTextParams(buttonFont, subTextColor, buttonScale, false, shadow, true) + drawText(subText, getStyleProperty('x') + buttonTextXOffset, y - buttonHeight / 2 + buttonTextYOffset) + end +end + +function WarMenu.CreateMenu(id, title, subTitle, style) + -- Default settings + local menu = { } + + -- Members + menu.id = id + menu.previousMenu = nil + menu.currentOption = 1 + menu.title = title + menu.subTitle = subTitle and string.upper(subTitle) or 'INTERACTION MENU' + + -- Style + if style then + menu.style = style + end + + menus[id] = menu +end + +function WarMenu.CreateSubMenu(id, parent, subTitle, style) + local parentMenu = menus[parent] + if not parentMenu then + return + end + + WarMenu.CreateMenu(id, parentMenu.title, subTitle and string.upper(subTitle) or parentMenu.subTitle) + + local menu = menus[id] + + menu.previousMenu = parent + + if parentMenu.overrideStyle then + menu.overrideStyle = copyTable(parentMenu.overrideStyle) + end + + if style then + menu.style = style + elseif parentMenu.style then + menu.style = copyTable(parentMenu.style) + end +end + +function WarMenu.CurrentMenu() + return currentMenu and currentMenu.id or nil +end + +function WarMenu.OpenMenu(id) + if id and menus[id] then + PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true) + setMenuVisible(id, true, true) + end +end + +function WarMenu.IsMenuOpened(id) + return currentMenu and currentMenu.id == id +end +WarMenu.Begin = WarMenu.IsMenuOpened + +function WarMenu.IsAnyMenuOpened() + return currentMenu ~= nil +end + +function WarMenu.IsMenuAboutToBeClosed() + return false +end + +function WarMenu.CloseMenu() + if currentMenu then + setMenuVisible(currentMenu.id, false) + optionCount = 0 + currentKey = nil + PlaySoundFrontend(-1, 'QUIT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true) + end +end + +function WarMenu.ToolTip(text, width, flipHorizontal) + if not currentMenu then + return + end + + local currentIndex = getCurrentIndex() + if not currentIndex then + return + end + + width = width or toolTipWidth + + local x = nil + if not flipHorizontal then + x = getStyleProperty('x') + getStyleProperty('width') + width / 2 + buttonTextXOffset + else + x = getStyleProperty('x') - width / 2 - buttonTextXOffset + end + + local textX = x - (width / 2) + buttonTextXOffset + setTextParams(buttonFont, getStyleProperty('textColor'), buttonScale, false, true, false, textX, textX + width - (buttonTextYOffset * 2)) + local linesCount = getLinesCount(text, textX, getStyleProperty('y')) + + local height = GetTextScaleHeight(buttonScale, buttonFont) * (linesCount + 1) + buttonTextYOffset + local y = getStyleProperty('y') + getTitleHeight() + (buttonHeight * currentIndex) + height / 2 + + drawRect(x, y, width, height, getStyleProperty('backgroundColor')) + + y = y - (height / 2) + buttonTextYOffset + drawText(text, textX, y) +end + +function WarMenu.Button(text, subText) + if not currentMenu then + return + end + + optionCount = optionCount + 1 + + drawButton(text, subText) + + local pressed = false + + if currentMenu.currentOption == optionCount then + if currentKey == keys.select then + pressed = true + PlaySoundFrontend(-1, getStyleProperty('buttonPressedSound').name, getStyleProperty('buttonPressedSound').set, true) + elseif currentKey == keys.left or currentKey == keys.right then + PlaySoundFrontend(-1, 'NAV_UP_DOWN', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true) + end + end + + return pressed +end + +function WarMenu.SpriteButton(text, dict, name, r, g, b, a) + if not currentMenu then + return + end + + local pressed = WarMenu.Button(text) + + local currentIndex = getCurrentIndex() + if not currentIndex then + return + end + + if not HasStreamedTextureDictLoaded(dict) then + RequestStreamedTextureDict(dict) + end + DrawSprite(dict, name, getStyleProperty('x') + getStyleProperty('width') - spriteWidth / 2 - buttonSpriteXOffset, getStyleProperty('y') + getTitleHeight() + buttonHeight + (buttonHeight * currentIndex) - spriteHeight / 2 + buttonSpriteYOffset, spriteWidth, spriteHeight, 0., r or 255, g or 255, b or 255, a or 255) + + return pressed +end + +function WarMenu.InputButton(text, windowTitleEntry, defaultText, maxLength, subText) + if not currentMenu then + return + end + + local pressed = WarMenu.Button(text, subText) + local inputText = nil + + if pressed then + DisplayOnscreenKeyboard(1, windowTitleEntry or 'FMMC_MPM_NA', '', defaultText or '', '', '', '', maxLength or 255) + + while true do + DisableAllControlActions(0) + + local status = UpdateOnscreenKeyboard() + if status == 2 then + break + elseif status == 1 then + inputText = GetOnscreenKeyboardResult() + break + end + + Citizen.Wait(0) + end + end + + return pressed, inputText +end + +function WarMenu.MenuButton(text, id, subText) + if not currentMenu then + return + end + + local pressed = WarMenu.Button(text, subText) + + if pressed then + currentMenu.currentOption = optionCount + setMenuVisible(currentMenu.id, false) + setMenuVisible(id, true, true) + end + + return pressed +end + +function WarMenu.CheckBox(text, checked, callback) + if not currentMenu then + return + end + + local name = nil + if currentMenu.currentOption == optionCount + 1 then + name = checked and 'shop_box_tickb' or 'shop_box_blankb' + else + name = checked and 'shop_box_tick' or 'shop_box_blank' + end + + local pressed = WarMenu.SpriteButton(text, 'commonmenu', name) + + if pressed then + checked = not checked + if callback then callback(checked) end + end + + return pressed +end + +function WarMenu.ComboBox(text, items, currentIndex, selectedIndex, callback) + if not currentMenu then + return + end + + local itemsCount = #items + local selectedItem = items[currentIndex] + local isCurrent = currentMenu.currentOption == optionCount + 1 + selectedIndex = selectedIndex or currentIndex + + if itemsCount > 1 and isCurrent then + selectedItem = '← '..tostring(selectedItem)..' →' + end + + local pressed = WarMenu.Button(text, selectedItem) + + if pressed then + selectedIndex = currentIndex + elseif isCurrent then + if currentKey == keys.left then + if currentIndex > 1 then currentIndex = currentIndex - 1 else currentIndex = itemsCount end + elseif currentKey == keys.right then + if currentIndex < itemsCount then currentIndex = currentIndex + 1 else currentIndex = 1 end + end + end + + if callback then callback(currentIndex, selectedIndex) end + return pressed, currentIndex +end + +function WarMenu.Display() + if currentMenu then + if not IsPauseMenuActive() then + ClearAllHelpMessages() + HudWeaponWheelIgnoreSelection() + DisablePlayerFiring(PlayerId(), true) + DisableControlAction(0, 25, true) + + drawTitle() + drawSubTitle() + + currentKey = nil + + if skipInputNextFrame then + skipInputNextFrame = false + else + if IsNavigatedDown() then + PlaySoundFrontend(-1, 'NAV_UP_DOWN', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true) + + if currentMenu.currentOption < optionCount then + currentMenu.currentOption = currentMenu.currentOption + 1 + else + currentMenu.currentOption = 1 + end + elseif IsNavigatedUp() then + PlaySoundFrontend(-1, 'NAV_UP_DOWN', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true) + + if currentMenu.currentOption > 1 then + currentMenu.currentOption = currentMenu.currentOption - 1 + else + currentMenu.currentOption = optionCount + end + elseif IsControlJustReleased(2, keys.left) then + currentKey = keys.left + elseif IsControlJustReleased(2, keys.right) then + currentKey = keys.right + elseif IsSelectedPressed() then + currentKey = keys.select + elseif IsBackPressed() then + if menus[currentMenu.previousMenu] then + setMenuVisible(currentMenu.previousMenu, true) + PlaySoundFrontend(-1, 'BACK', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true) + else + WarMenu.CloseMenu() + end + end + end + end + + optionCount = 0 + end +end +WarMenu.End = WarMenu.Display + +function WarMenu.CurrentOption() + if currentMenu and optionCount ~= 0 then + return currentMenu.currentOption + end + + return nil +end + +function WarMenu.IsItemHovered() + if not currentMenu or optionCount == 0 then + return false + end + + return currentMenu.currentOption == optionCount +end + +function WarMenu.IsItemSelected() + return currentKey == keys.select and WarMenu.IsItemHovered() +end + +function WarMenu.SetTitle(id, title) + setMenuProperty(id, 'title', title) +end +WarMenu.SetMenuTitle = WarMenu.SetTitle + +function WarMenu.SetSubTitle(id, text) + setMenuProperty(id, 'subTitle', string.upper(text)) +end +WarMenu.SetMenuSubTitle = WarMenu.SetSubTitle + +function WarMenu.SetMenuStyle(id, style) + setMenuProperty(id, 'style', style) +end + +function WarMenu.SetMenuTitleVisible(id, visible) + setStyleProperty(id, 'titleVisible', visible) +end + +function WarMenu.SetMenuX(id, x) + setStyleProperty(id, 'x', x) +end + +function WarMenu.SetMenuY(id, y) + setStyleProperty(id, 'y', y) +end + +function WarMenu.SetMenuWidth(id, width) + setStyleProperty(id, 'width', width) +end + +function WarMenu.SetMenuMaxOptionCountOnScreen(id, count) + setStyleProperty(id, 'maxOptionCountOnScreen', count) +end + +function WarMenu.SetTitleColor(id, r, g, b, a) + setStyleProperty(id, 'titleColor', { r, g, b, a }) +end +WarMenu.SetMenuTitleColor = WarMenu.SetTitleColor + +function WarMenu.SetMenuSubTitleColor(id, r, g, b, a) + setStyleProperty(id, 'subTitleColor', { r, g, b, a }) +end + +function WarMenu.SetMenuSubTitleBackgroundColor(id, r, g, b, a) + setStyleProperty(id, 'subTitleBackgroundColor', { r, g, b, a }) +end + +function WarMenu.SetTitleBackgroundColor(id, r, g, b, a) + setStyleProperty(id, 'titleBackgroundColor', { r, g, b, a }) +end +WarMenu.SetMenuTitleBackgroundColor = WarMenu.SetTitleBackgroundColor + +function WarMenu.SetTitleBackgroundSprite(id, dict, name) + RequestStreamedTextureDict(dict) + setStyleProperty(id, 'titleBackgroundSprite', { dict = dict, name = name }) +end +WarMenu.SetMenuTitleBackgroundSprite = WarMenu.SetTitleBackgroundSprite + +function WarMenu.SetMenuBackgroundColor(id, r, g, b, a) + setStyleProperty(id, 'backgroundColor', { r, g, b, a }) +end + +function WarMenu.SetMenuTextColor(id, r, g, b, a) + setStyleProperty(id, 'textColor', { r, g, b, a }) +end + +function WarMenu.SetMenuSubTextColor(id, r, g, b, a) + setStyleProperty(id, 'subTextColor', { r, g, b, a }) +end + +function WarMenu.SetMenuFocusColor(id, r, g, b, a) + setStyleProperty(id, 'focusColor', { r, g, b, a }) +end + +function WarMenu.SetMenuFocusTextColor(id, r, g, b, a) + setStyleProperty(id, 'focusTextColor', { r, g, b, a }) +end + +function WarMenu.SetMenuButtonPressedSound(id, name, set) + setStyleProperty(id, 'buttonPressedSound', { name = name, set = set }) +end + +end \ No newline at end of file diff --git a/starter.lua b/starter.lua index 9572e92..107a00e 100644 --- a/starter.lua +++ b/starter.lua @@ -175,6 +175,8 @@ for _, v in pairs({ -- This is a specific load order 'vehicles.lua', 'effects.lua', + --'warmenu.lua', + -- Do version check last '_scriptversioncheck.lua' }) do @@ -184,6 +186,20 @@ for _, v in pairs({ -- This is a specific load order local fileLoader = assert(load(LoadResourceFile('jim_bridge', ('shared/'..v)), ('@@jim_bridge/shared/'..v))) fileLoader() if debugMode then - print("^5Success^7: ^2loaded file^7: ^3"..(v):gsub("/", "^7/^3"):gsub("%.lua", "^7.lua").."^7") + print("^5CoreLoader^7: ^2loaded file^7: ^3"..(v):gsub("/", "^7/^3"):gsub("%.lua", "^7.lua").."^7") end -end \ No newline at end of file +end + +if isStarted(QBInv) then + if not checkExportExists(QBInv, "HasItem") then + print("^6Bridge^7: ^2Can^7'^2t ^2find new QBInv export^7, ^2forcing ^1QBInvNew ^2to ^1false^7") + QBInvNew = false + end +end + +if isStarted(PSInv) then + if not checkExportExists(PSInv, "HasItem") then + print("^6Bridge^7: ^2Can^7't ^2find new PSInv export^7, ^2forcing ^1QBInvNew ^2to ^1false^7") + QBInvNew = false + end +end diff --git a/ui_modules/notifications.lua b/ui_modules/notifications.lua new file mode 100644 index 0000000..4b54433 --- /dev/null +++ b/ui_modules/notifications.lua @@ -0,0 +1,120 @@ +local notifications = {} +local spacing = 60 -- vertical spacing between notifications +local activeDrawing = false -- flag for drawing loop state + +if not HasStreamedTextureDictLoaded("timerbars") then + while not HasStreamedTextureDictLoaded("timerbars") do + RequestStreamedTextureDict("timerbars") + Wait(5) + end +end + +local notifTypes = { + success = "✔️", + error = "❌", + warning = "⚠️", + police = "🚓", + ambulance = "🚑", +} + +function gtaNotify(title, message, emoji, src) + local notif = { + title = title, + message = message, + emoji = notifTypes[emoji] or "❔", + state = "enter", + startTime = GetGameTimer(), + progress = 0, + holdTime = 8000, + slideDuration = 100, + currentOffset = 0, + } + table.insert(notifications, notif) + + -- Activate drawing loop if not already running + -- This allows it to be silent until the first notifcation is called, then the loop starts + if not activeDrawing then + activeDrawing = true + StartDrawingLoop() + end +end + +-- Drawing loop as a separate controlled thread +function StartDrawingLoop() + CreateThread(function() + while #notifications > 0 do + local currentTime = GetGameTimer() + + -- Update notifications vertical offset + for i, notif in ipairs(notifications) do + local target = (#notifications - i) * spacing + notif.currentOffset += (target - notif.currentOffset) * 0.1 + end + + for i = #notifications, 1, -1 do + local notif = notifications[i] + + if notif.state == "enter" then + local elapsed = currentTime - notif.startTime + notif.progress = math.min(elapsed / notif.slideDuration, 1.0) + if notif.progress >= 1.0 then + notif.state = "hold" + notif.holdStart = currentTime + end + elseif notif.state == "hold" then + if currentTime - notif.holdStart >= notif.holdTime then + notif.state = "exit" + notif.exitStart = currentTime + end + elseif notif.state == "exit" then + local elapsed = currentTime - notif.exitStart + notif.progress = 1.0 - math.min(elapsed / notif.slideDuration, 1.0) + if notif.progress <= 0 then + table.remove(notifications, i) + goto continue + end + end + + local startX, targetX = 1.0, 0.8 + local posX = startX - (startX - targetX) * notif.progress + local posY = 0.05 + (notif.currentOffset / 1080) + + -- Background sprite + DrawSprite("timerbars", "all_black_bg", posX + 0.11, posY + 0.025, 0.2, 0.053, 0.0, 255, 255, 255, 255) + + -- Title text + local moveMessage = false + if not notif.title or notif.title == "" then + moveMessage = true + else + drawNotiText(8, 0.4, vec2(0.75, 0.975), notif.title, vec2(posX, posY)) + end + + -- Message text + drawNotiText(4, 0.3, vec2(0.75, 0.975), notif.message, vec2(posX, posY + (moveMessage and 0.015 or 0.03))) + + -- Emoji + drawNotiText(0, 0.3, vec2(0.75, 0.995), notif.emoji, vec2(posX, posY + 0.015)) + + ::continue:: + end + Wait(0) + end + activeDrawing = false -- No notifications left, pause drawing + end) +end + +function drawNotiText(font, scale, wrap, string, pos) + SetTextFont(font) + SetTextScale(scale, scale) + SetTextWrap(wrap.x, wrap.y) + SetTextJustification(2) + SetTextColour(255, 255, 255, 255) + SetTextOutline() + SetTextEntry("STRING") + AddTextComponentString(string) + DrawText(pos.x, pos.y) +end + +RegisterNetEvent("jim-bridge:Notify", gtaNotify) +exports("Notify", gtaNotify) \ No newline at end of file diff --git a/ui_modules/skillcheck.lua b/ui_modules/skillcheck.lua new file mode 100644 index 0000000..d2fbf30 --- /dev/null +++ b/ui_modules/skillcheck.lua @@ -0,0 +1,121 @@ +local activeSkillCheck = false + +function gtaSkillCheck() + if activeSkillCheck then return end + local result = false + local successes = 0 + local barsRequired = 3 + + for bar = 1, barsRequired do + activeSkillCheck = true + local width, height = 0.2, 0.01 + local x, y = 0.5, 0.8 + + -- Random highlighted zone + local highlightSize = math.random(10, 20) / 100 + local highlightStart = math.random(10, 50) / 100 + local highlightEnd = highlightStart + highlightSize + local highlightAlpha = 0 + local cursorPos = 0.0 + local cursorSpeed = 0.025 + local movingRight = true + + while activeSkillCheck do + Wait(0) + + createScaleBars(x, y, width, height) + + local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect + highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha + + -- Draw highlighted zone (success area) with pulsing effect + DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha ) + -- Draw moving cursor + DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) + + -- Move cursor + if movingRight then + cursorPos += cursorSpeed + if cursorPos >= 1.0 then movingRight = false end + else + cursorPos -= cursorSpeed + if cursorPos <= 0.0 then movingRight = true end + end + + if IsControlJustPressed(0, 177) then -- Backspace to cancel + local displayTime = GetGameTimer() + 2000 + PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) + while GetGameTimer() < displayTime do + Wait(0) + + createScaleBars(x, y, width, height) + + DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 228, 52, 52, 255) + DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) + + drawSuccessText(x, y, "Failed", 228, 52, 52) + end + return false + end + + -- Check for keypress (E) + if IsControlJustPressed(0, 38) then + activeSkillCheck = false + result = cursorPos >= highlightStart and cursorPos <= highlightEnd + if result then + PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + else + PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) + end + local displayTime = GetGameTimer() + 2000 + while GetGameTimer() < displayTime do + Wait(0) + + createScaleBars(x, y, width, height) + + -- Draw highlighted zone + DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180) + + -- Draw stationary cursor at result position + DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) + -- Display result text + drawSuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52) + end + if result then + successes += 1 + else + return false + end + end + end + end + activeSkillCheck = false + print("^5GTAUI^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7") + return successes == barsRequired +end + +function drawSuccessText(x, y, text, r, g, b) + SetTextFont(8) + SetTextScale(0.45, 0.45) + SetTextColour(r, g, b, 255) + SetTextDropshadow(0, 0, 0, 0, 255) + SetTextEdge(2, 0, 0, 0, 150) + SetTextDropShadow() + SetTextOutline() + SetTextCentre(true) + SetTextEntry("STRING") + SetTextCentre(true) + SetTextEntry("STRING") + AddTextComponentString(text) + DrawText(x, y + 0.03) +end + +function createScaleBars(x, y, width, height) + -- Draw background box + DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255) + DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255) + -- Draw full bar (dark background) + DrawRect(x, y, width, height, 100, 100, 100, 255) +end + +exports("skillCheck", gtaSkillCheck) \ No newline at end of file diff --git a/ui_modules/target.lua b/ui_modules/target.lua new file mode 100644 index 0000000..c54bae1 --- /dev/null +++ b/ui_modules/target.lua @@ -0,0 +1,318 @@ +-- Global Key Table, defined once. +--- +local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 } + +-- Mapping of key codes to human-readable key names. +local Keys = { + [322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5", + [167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10", + [243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5", + [159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=", + [177] = "BACKSPACE", [37] = "TAB", + [44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y", + [303] = "U", [199] = "P", + [39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS", + [34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G", + [74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT", + [20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N", + [244] = "M", [82] = ",", [81] = "." +} +-- Tables for storing created targets. +local TextTargets = {} -- For fallback DrawText3D targets. +local targetEntities = {} -- For entity targets. + +function createEntityTarget(entity, opts, dist) + startTargetLoop() + targetEntities[#targetEntities + 1] = entity + local entityCoords = GetEntityCoords(entity) + + local existingTarget = nil + for _, target in pairs(TextTargets) do + if #(target.coords - entityCoords) < 0.01 then + existingTarget = target + break + end + end + + if existingTarget then + for i = 1, #opts do + local key = KEY_TABLE[#existingTarget.options + i] + opts[i].key = key + existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label + existingTarget.options[#existingTarget.options + 1] = opts[i] + end + updateCachedText(existingTarget) + else + local tempText = {} + for i = 1, #opts do + opts[i].key = KEY_TABLE[i] + tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + end + TextTargets[entity] = { + coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z), + buttontext = tempText, + options = opts, + dist = dist, + text = table.concat(tempText, "\n") + } + end +end + +function createZoneTarget(data, opts, dist) + startTargetLoop() + local existingTarget = nil + for _, target in pairs(TextTargets) do + if #(target.coords - data[2]) < 0.01 then + existingTarget = target + break + end + end + + if existingTarget then + for i = 1, #opts do + local key = KEY_TABLE[#existingTarget.options + i] + opts[i].key = key + existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label + existingTarget.options[#existingTarget.options + 1] = opts[i] + end + updateCachedText(existingTarget) + else + local tempText = {} + for i = 1, #opts do + opts[i].key = KEY_TABLE[i] + tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + end + TextTargets[data[1]] = { + coords = data[2], + buttontext = tempText, + options = opts, + dist = dist, + text = table.concat(tempText, "\n") + } + end + return data[1] +end + +function createModelTarget(models, opts, dist) + startTargetLoop() + if type(models) ~= "table" then + models = { models } + end + + local tempText = {} + for i = 1, #opts do + opts[i].key = KEY_TABLE[i] + tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + end + + local keyStr = "" + for i, m in ipairs(models) do + keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") + end + local targetKey = "model_" .. keyStr + + TextTargets[targetKey] = { + models = models, + buttontext = tempText, + options = opts, + dist = dist, + coords = vec3(0, 0, 0), + text = table.concat(tempText, "\n") + } + + return targetKey +end + +function removeEntityTarget(entity) + TextTargets[entity] = nil +end + +function removeZoneTarget(target) + TextTargets[target] = nil +end + +function removeModelTarget(model) + TextTargets[model] = nil +end + +exports("createEntityTarget", createEntityTarget) +exports("createZoneTarget", createZoneTarget) +exports("createModelTarget", createModelTarget) + +exports("removeEntityTarget", removeEntityTarget) +exports("removeZoneTarget", removeZoneTarget) +exports("removeModelTarget", removeModelTarget) + + +------------------------------------------------------------- +-- Fallback: DrawText3D Targets (Experimental) +------------------------------------------------------------- +local started = false +function startTargetLoop() + if started then return end + Config = { + System = { + + } + } + started = true + local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua'))) + fileLoader() + -- Model Entity Refresher + CreateThread(function() + while true do + local pedCoords = GetEntityCoords(PlayerPedId()) + for _, target in pairs(TextTargets) do + if target.models then + for _, model in ipairs(target.models) do + local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false) + if entity and entity ~= 0 then + target.entity = entity + target.coords = GetEntityCoords(entity) + break + end + end + end + end + Wait(3000) -- Refresh every 3s + end + end) + + -- Main Target Loop + CreateThread(function() + while true do + local ped = PlayerPedId() + local pedCoords = GetEntityCoords(ped) + local camCoords = GetGameplayCamCoord() + local camRot = GetGameplayCamRot(2) + local camForward = RotationToDirection(camRot) + + local closestTarget, closestDist, targetEntity = nil, math.huge, nil + + for _, target in pairs(TextTargets) do + local coords = target.coords + if coords then + local dist = #(pedCoords - coords) + if dist <= target.dist then + local normVec = normalizeVector(coords - camCoords) + local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z + + if dot > 0.5 and dist < closestDist then + closestTarget = target + closestDist = dist + targetEntity = target.entity + end + end + end + end + + for _, target in pairs(TextTargets) do + if not target.coords then goto continue end + local dist = #(pedCoords - target.coords) + if dist > target.dist then goto continue end + local isClosest = (target == closestTarget) + + for i, opt in ipairs(target.options) do + if IsControlJustPressed(0, opt.key) and isClosest then + if (not target.canInteract or target.canInteract()) and + (not opt.item or hasItem(opt.item)) and + (not opt.job or hasJob(opt.job, nil)) then + if opt.onSelect then opt.onSelect(targetEntity) end + if opt.action then opt.action(targetEntity) end + end + end + end + + local baseZ, lineHeight = target.coords.z + 1.0, -0.16 + local lineOffset = 0 + + for i, opt in ipairs(target.options) do + if (not target.canInteract or target.canInteract()) and + (not opt.item or hasItem(opt.item)) and + (not opt.job or hasJob(opt.job, nil)) then + DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest) + lineOffset = lineOffset + 1 + end + end + + ::continue:: + end + + Wait(1) -- Throttled + end + end) +end + + +function DrawText3D(coord, text, highlight) + SetTextScale(0.30, 0.30) + SetTextFont(0) + SetTextProportional(1) + SetTextColour(255, 255, 255, 215) + SetTextEntry("STRING") + SetTextCentre(true) + + local totalLength = string.len(text) + local textMaxLength = 99 -- max 99 + local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text + AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text) + SetDrawOrigin(coord.x, coord.y, coord.z, 0) + DrawText(0.0, 0.0) + local count, length = GetLineCountAndMaxLength(text) + + local padding = 0.005 + local heightFactor = (count / 43) + padding + local weightFactor = (length / 150) + padding + + local height = (heightFactor / 2) - padding / 1 + local width = (weightFactor / 2) - padding / 1 + + DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150) + ClearDrawOrigin() +end + +--- Calculates the number of lines and the maximum line length from the given text. +--- +--- @param text string The text to analyze. +--- @return number, number The line count and maximum line length. +--- +--- @usage +--- ```lua +--- local count, maxLen = GetLineCountAndMaxLength("Hello World") +--- ``` +function GetLineCountAndMaxLength(text) + local lineCount, maxLength = 0, 0 + for line in text:gmatch("[^\n]+") do + lineCount += 1 + local lineLength = string.len(line) + if lineLength > maxLength then + maxLength = lineLength + end + end + if lineCount == 0 then lineCount = 1 end + return lineCount, maxLength +end + + +function RotationToDirection(rot) + local adjust = math.pi / 180 + return vec3( + -math.sin(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)), + math.cos(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)), + math.sin(adjust * rot.x) + ) +end + +function normalizeVector(vec) + local len = math.sqrt(vec.x^2 + vec.y^2 + vec.z^2) + if len ~= 0 then + return vec3(vec.x / len, vec.y / len, vec.z / len) + else + return vec3(0, 0, 0) + end +end + +-- Helper to update cached text. +function updateCachedText(target) + target.text = table.concat(target.buttontext, "\n") +end \ No newline at end of file diff --git a/version.txt b/version.txt index c9fddf0..c315cf0 100644 --- a/version.txt +++ b/version.txt @@ -1,12 +1,12 @@ -2.0.14 +2.0.15 -- Add getPlayer() fallbacks for if player isn't loaded -- Add fallback for sellMenu missing locales -- Change blip preview export to "jim-blipcontroller" -- Support for tgiann-bank -- Fixes for OX_Core COX version -- Fix QBOX weapons not being added to "Items" cache -- Remove Player.Offline from canCarry() as it was erroring -- Possible fix for QS_Inv stashes wiping on script start +- Fix old PSInv/QBInv stashes being wiped when it should be updating them +- Better breakout from opening inventory while crafting +- Unify isInventoryOpen() function to simply check for IsNuiFocused() +- Add clearStash() and doesItemExist() functions for future use +- Add warmenu.lua file for gta scaleform menus (loading is disabled by default) +- Move custom GTA native notify, skillcheck, target to separate files, can be used outside the script +- Complete refactor of framework shared info loading, now only does it once and then shares to scripts +- Added extra checks for ESX loading, should hopefully stop the coreloader line 317 error https://github.com/jimathy/jim_bridge \ No newline at end of file