From db9e7fea0acf6f1d99728f9bd6b4e6f9cec8b530 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Fri, 22 Aug 2025 21:18:51 +0100 Subject: [PATCH 01/13] fix stashName variable for stashRemoveItem() --- shared/itemcontrol.lua | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua index a12da77..896b5db 100644 --- a/shared/itemcontrol.lua +++ b/shared/itemcontrol.lua @@ -201,7 +201,7 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) for k, v in pairs(items) do - exports[QSInv]:RemoveItemIntoStash(stashName, k, v) + exports[QSInv]:RemoveItemIntoStash(stashName[1], k, v) debugPrint("^6Bridge^7: ^2Removing ^3"..QSInv.." ^2Stash item^7:", k, v) end end, @@ -291,7 +291,7 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) for k, v in pairs(items) do - exports[CoreInv]:removeItemExact(stashName, k, v) + exports[CoreInv]:removeItemExact(stashName[1], k, v) debugPrint("^6Bridge^7: ^2Removing ^3"..CoreInv.." ^2Stash item^7:", k, v) end end, @@ -381,7 +381,7 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) for k, v in pairs(items) do - exports[OrigenInv]:RemoveFromStash(stashName, k, v) + exports[OrigenInv]:RemoveFromStash(stashName[1], k, v) debugPrint("^6Bridge^7: ^2Removing ^3"..OrigenInv.." ^2Stash item^7:", k, v) end end, @@ -494,7 +494,7 @@ local InvFunc = { end end end - exports[CodeMInv]:UpdateStash(stashName, stashItems) + exports[CodeMInv]:UpdateStash(stashName[1], stashItems) end, registerStash = function(name, label, slots, weight, owner, coords) @@ -588,8 +588,8 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) for k, v in pairs(items) do - local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName, k) - exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName, k, v, itemData.slot, nil) + local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName[1], k) + exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName[1], k, v, itemData.slot, nil) debugPrint("^6Bridge^7: ^2Removing ^3"..TgiannInv.." ^2Stash item^7:", k, v) end end, @@ -743,7 +743,7 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) if not stashItems or not next(stashItems) then - stashItems = getStash(stashName) + stashItems = getStash(stashName[1]) end for k, v in pairs(items) do for l in pairs(stashItems) do @@ -757,9 +757,9 @@ local InvFunc = { end end end - debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3JPR^2 stash '^6"..stashName.."^7'") + debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3JPR^2 stash '^6"..stashName[1].."^7'") MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { - ['stash'] = stashName, + ['stash'] = stashName[1], ['items'] = json.encode(stashItems) }) end, @@ -924,12 +924,12 @@ local InvFunc = { function(stashItems, stashName, items) if checkExportExists(QBInv, "RemoveItem") then for k, v in pairs(items) do - exports[QBInv]:RemoveItem(stashName, k, v, false, 'crafting') + exports[QBInv]:RemoveItem(stashName[1], k, v, false, 'crafting') debugPrint("^6Bridge^7: ^2Removing ^3"..QBInv.." ^2Stash item^7:", k, v) end else if not stashItems or not next(stashItems) then - stashItems = getStash(stashName) + stashItems = getStash(stashName[1]) end for k, v in pairs(items) do for l in pairs(stashItems) do @@ -943,9 +943,9 @@ local InvFunc = { end end end - debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash '^6"..stashName.."^7'") + debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash '^6"..stashName[1].."^7'") MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { - ['stash'] = stashName, + ['stash'] = stashName[1], ['items'] = json.encode(stashItems) }) end @@ -1105,7 +1105,7 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) if not stashItems or not next(stashItems) then - stashItems = getStash(stashName) + stashItems = getStash(stashName[1]) end for k, v in pairs(items) do for l in pairs(stashItems) do @@ -1119,9 +1119,9 @@ local InvFunc = { end end end - debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3PS^2 stash ^7'^6"..stashName.."^7'") + debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3PS^2 stash ^7'^6"..stashName[1].."^7'") MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { - ['stash'] = stashName, + ['stash'] = stashName[1], ['items'] = json.encode(stashItems) }) end, @@ -1244,7 +1244,7 @@ local InvFunc = { stashRemoveItem = function(stashItems, stashName, items) for k, v in pairs(items) do - exports[RSGInv]:RemoveItem(stashName, k, v, false, 'crafting') + exports[RSGInv]:RemoveItem(stashName[1], k, v, false, 'crafting') debugPrint("^6Bridge^7: ^2Removing ^3"..RSGInv.." ^2Stash item^7:", k, v) end end, @@ -2280,7 +2280,7 @@ function stashRemoveItem(stashItems, stashName, items) for i = 1, #InvFunc do local inv = InvFunc[i] if isStarted(inv.invName) then - inv.stashRemoveItem(stashItems, stashName[1], items) + inv.stashRemoveItem(stashItems, stashName, items) return end end From 035ef264860bbff60e770a3a164ddfd1996dfec4 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 23 Aug 2025 23:34:15 +0100 Subject: [PATCH 02/13] Update `contextMenus.lua` to use new format --- shared/contextmenus.lua | 516 +++++++++++++++++++++------------------- 1 file changed, 270 insertions(+), 246 deletions(-) diff --git a/shared/contextmenus.lua b/shared/contextmenus.lua index fb59367..a24eaed 100644 --- a/shared/contextmenus.lua +++ b/shared/contextmenus.lua @@ -10,6 +10,263 @@ • esx (using ESX.UI.Menu) ]] +local contextFunc = { + ["ox"] = + function(Menu, data) + local index = nil + if data.onBack and not data.onSelected then + table.insert(Menu, 1, { + icon = "fas fa-circle-arrow-left", + title = "Return", + onSelect = data.onBack, + label = "Return", + }) + end + for k in pairs(Menu) do + if data.onSelected and Menu[k].arrow then + Menu[k].icon = "fas fa-angle-right" + end + -- If no title, use header or txt as title/label. + if not Menu[k].title then + if Menu[k].header ~= nil and Menu[k].header ~= "" then + Menu[k].title = Menu[k].header + Menu[k].label = Menu[k].header + if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end + else + Menu[k].title = Menu[k].txt + Menu[k].label = Menu[k].txt + end + end + -- Copy parameters from 'params' if available. + if Menu[k].params then + Menu[k].event = Menu[k].params.event + Menu[k].args = Menu[k].params.args or {} + end + if Menu[k].isMenuHeader then + Menu[k].readOnly = true + end + end + local menuID = 'Menu' + (data.onSelected and lib.registerMenu or lib.registerContext)({ + id = menuID, + title = data.header..br..br..(data.headertxt and data.headertxt or ""), + position = 'top-right', + options = Menu, + canClose = data.canClose and data.canClose or nil, + onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil, + onExit = data.onExit and data.onExit or nil, + onSelected = data.onSelected and (function(selected) index = selected end) or nil, + }, data.onSelected and (function(x, y, args) + if Menu[x].refresh then + if Menu[x].onSelect then + Menu[x].onSelect() + end + lib.showMenu(menuID, index) + else + if Menu[x].onSelect then + Menu[x].onSelect() + else + lib.showMenu(menuID, index) + end + end + end) or nil) + if data.onSelected then + lib.showMenu(menuID, 1) + else + lib.showContext(menuID) + end + end, + + ["qb"] = + function(Menu, data) + if data.onBack then + table.insert(Menu, 1, { + icon = "fas fa-circle-arrow-left", + header = " ", + txt = "Return", + params = { + isAction = true, + event = data.onBack, + }, + }) + elseif data.canClose then + table.insert(Menu, 1, { + icon = "fas fa-circle-xmark", + header = " ", + txt = "Close", + params = { + isAction = true, + event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end), + }, + }) + end + if data.header ~= nil then + local tempMenu = {} + for k, v in pairs(Menu) do tempMenu[k + 1] = v end + tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true } + Menu = tempMenu + end + for k in pairs(Menu) do + if not Menu[k].params or not Menu[k].params.event then + Menu[k].params = { + isAction = true, + event = Menu[k].onSelect or function() end, + } + end + if not Menu[k].header then Menu[k].header = " " end + if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end + Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable + end + exports[QBMenuExport]:openMenu(Menu) + end, + + ["gta"] = + function(Menu, data) + WarMenu.CreateMenu(tostring(Menu), data.header, " ", { + titleColor = { 222, 255, 255 }, + maxOptionCountOnScreen = 15, + width = 0.25, + x = 0.7, + }) + if WarMenu.IsAnyMenuOpened() then return end + WarMenu.OpenMenu(tostring(Menu)) + CreateThread(function() + local close = true + while true do + if WarMenu.Begin(tostring(Menu)) then + if data.onBack then + if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then + WarMenu.CloseMenu() + Wait(10) + data.onBack() + end + end + for k in pairs(Menu) do + local pressed = WarMenu.Button(Menu[k].header) + if not Menu[k].header then + Menu[k].header = Menu[k].txt + Menu[k].txt = nil + end + if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then + if Menu[k].disabled or Menu[k].isMenuHeader then + WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true) + else + WarMenu.ToolTip( + (Menu[k].blip and "~BLIP_".."8".."~ " or "").. + Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18, + true) + end + end + if pressed and not Menu[k].isMenuHeader then + WarMenu.CloseMenu() + close = false + Wait(10) + Menu[k].onSelect() + end + end + WarMenu.End() + else + return + end + if not WarMenu.IsAnyMenuOpened() and close then + stopTempCam(cam) + if data.onExit then data.onExit() end + end + Wait(0) + end + end) + end, + + ["esx"] = + function(Menu, data) + for k in pairs(Menu) do + Menu[k].label = Menu[k].header + Menu[k].name = "button"..k + end + if data.canClose then + table.insert(Menu, 1, { + icon = "fas fa-circle-xmark", + label = "Close", + name = "close", + onSelect = data.onExit, + }) + end + if data.onBack then + table.insert(Menu, 1, { + icon = "fas fa-circle-arrow-left", + label = "Return", + name = "return", + onSelect = data.onBack, + }) + end + ESX.UI.Menu.Open("default", getScript(), "Example_Menu", { + title = data.header, + align = 'top-right', + elements = Menu, + }, + function(menuData, menu) + for k in pairs(Menu) do + if menuData.current.name == Menu[k].name then + menu.close() + Wait(10) + Menu[k].onSelect() + end + end + end, + function(data, menu) + menu.close() + end) + end, + + ["lation"] = + function(Menu, data) + if data.onBack then + table.insert(Menu, 1, { + icon = "fas fa-circle-arrow-left", + onSelect = data.onBack, + header = "Return", + }) + end + + for k in pairs(Menu) do + if data.onSelected and Menu[k].arrow then + Menu[k].icon = "fas fa-angle-right" + end + -- If no title, use header or txt as title/label. + if not Menu[k].title then + if Menu[k].header ~= nil and Menu[k].header ~= "" then + Menu[k].title = Menu[k].header + Menu[k].label = Menu[k].header + if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end + else + Menu[k].title = Menu[k].txt + Menu[k].label = Menu[k].txt + end + end + -- Copy parameters from 'params' if available. + if Menu[k].params then + Menu[k].event = Menu[k].params.event + Menu[k].args = Menu[k].params.args or {} + end + if Menu[k].isMenuHeader then + Menu[k].readOnly = true + end + end + + exports.lation_ui:registerMenu({ + id = 'menu', + title = data.header, + onExit = data.onExit and data.onExit or nil, + subtitle = (data.headertxt and data.headertxt or ""), + options = Menu, + }) + + -- Show menu + exports.lation_ui:showMenu('menu') + end, +} + + --- Opens a menu using the configured menu system. --- --- This function translates code and creates menus in several different menu scripts, depending on the configured menu system specified in `Config.System.Menu`. @@ -47,255 +304,22 @@ --- }) --- ``` function openMenu(Menu, data) - if Config.System.Menu == "ox" then - local index = nil - if data.onBack and not data.onSelected then - table.insert(Menu, 1, { - icon = "fas fa-circle-arrow-left", - title = "Return", - onSelect = data.onBack, - label = "Return", - }) - end - for k in pairs(Menu) do - if data.onSelected and Menu[k].arrow then - Menu[k].icon = "fas fa-angle-right" - end - -- If no title, use header or txt as title/label. - if not Menu[k].title then - if Menu[k].header ~= nil and Menu[k].header ~= "" then - Menu[k].title = Menu[k].header - Menu[k].label = Menu[k].header - if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end - else - Menu[k].title = Menu[k].txt - Menu[k].label = Menu[k].txt - end - end - -- Copy parameters from 'params' if available. - if Menu[k].params then - Menu[k].event = Menu[k].params.event - Menu[k].args = Menu[k].params.args or {} - end - if Menu[k].isMenuHeader then - Menu[k].readOnly = true - end - end - local menuID = 'Menu' - (data.onSelected and lib.registerMenu or lib.registerContext)({ - id = menuID, - title = data.header..br..br..(data.headertxt and data.headertxt or ""), - position = 'top-right', - options = Menu, - canClose = data.canClose and data.canClose or nil, - onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil, - onExit = data.onExit and data.onExit or nil, - onSelected = data.onSelected and (function(selected) index = selected end) or nil, - }, data.onSelected and (function(x, y, args) - if Menu[x].refresh then - if Menu[x].onSelect then - Menu[x].onSelect() - end - lib.showMenu(menuID, index) - else - if Menu[x].onSelect then - Menu[x].onSelect() - else - lib.showMenu(menuID, index) - end - end - end) or nil) - if data.onSelected then - lib.showMenu(menuID, 1) - else - lib.showContext(menuID) - end - - elseif Config.System.Menu == "qb" then - if data.onBack then - table.insert(Menu, 1, { - icon = "fas fa-circle-arrow-left", - header = " ", - txt = "Return", - params = { - isAction = true, - event = data.onBack, - }, - }) - elseif data.canClose then - table.insert(Menu, 1, { - icon = "fas fa-circle-xmark", - header = " ", - txt = "Close", - params = { - isAction = true, - event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end), - }, - }) - end - if data.header ~= nil then - local tempMenu = {} - for k, v in pairs(Menu) do tempMenu[k + 1] = v end - tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true } - Menu = tempMenu - end - for k in pairs(Menu) do - if not Menu[k].params or not Menu[k].params.event then - Menu[k].params = { - isAction = true, - event = Menu[k].onSelect or function() end, - } - end - if not Menu[k].header then Menu[k].header = " " end - if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end - Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable - end - exports[QBMenuExport]:openMenu(Menu) - - elseif Config.System.Menu == "gta" then - WarMenu.CreateMenu(tostring(Menu), data.header, " ", { - titleColor = { 222, 255, 255 }, - maxOptionCountOnScreen = 15, - width = 0.25, - x = 0.7, - }) - if WarMenu.IsAnyMenuOpened() then return end - WarMenu.OpenMenu(tostring(Menu)) - CreateThread(function() - local close = true - while true do - if WarMenu.Begin(tostring(Menu)) then - if data.onBack then - if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then - WarMenu.CloseMenu() - Wait(10) - data.onBack() - end - end - for k in pairs(Menu) do - local pressed = WarMenu.Button(Menu[k].header) - if not Menu[k].header then - Menu[k].header = Menu[k].txt - Menu[k].txt = nil - end - if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then - if Menu[k].disabled or Menu[k].isMenuHeader then - WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true) - else - WarMenu.ToolTip( - (Menu[k].blip and "~BLIP_".."8".."~ " or "").. - Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18, - true) - end - end - if pressed and not Menu[k].isMenuHeader then - WarMenu.CloseMenu() - close = false - Wait(10) - Menu[k].onSelect() - end - end - WarMenu.End() - else - return - end - if not WarMenu.IsAnyMenuOpened() and close then - stopTempCam(cam) - if data.onExit then data.onExit() end - end - Wait(0) - end - end) - - elseif Config.System.Menu == "esx" then - for k in pairs(Menu) do - Menu[k].label = Menu[k].header - Menu[k].name = "button"..k - end - if data.canClose then - table.insert(Menu, 1, { - icon = "fas fa-circle-xmark", - label = "Close", - name = "close", - onSelect = data.onExit, - }) - end - if data.onBack then - table.insert(Menu, 1, { - icon = "fas fa-circle-arrow-left", - label = "Return", - name = "return", - onSelect = data.onBack, - }) - end - ESX.UI.Menu.Open("default", getScript(), "Example_Menu", { - title = data.header, - align = 'top-right', - elements = Menu, - }, - function(menuData, menu) - for k in pairs(Menu) do - if menuData.current.name == Menu[k].name then - menu.close() - Wait(10) - Menu[k].onSelect() - end - end - end, - function(data, menu) - menu.close() - end) - - elseif Config.System.Menu == "lation" then - if data.onBack then - table.insert(Menu, 1, { - icon = "fas fa-circle-arrow-left", - onSelect = data.onBack, - header = "Return", - }) - end - - for k in pairs(Menu) do - if data.onSelected and Menu[k].arrow then - Menu[k].icon = "fas fa-angle-right" - end - -- If no title, use header or txt as title/label. - if not Menu[k].title then - if Menu[k].header ~= nil and Menu[k].header ~= "" then - Menu[k].title = Menu[k].header - Menu[k].label = Menu[k].header - if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end - else - Menu[k].title = Menu[k].txt - Menu[k].label = Menu[k].txt - end - end - -- Copy parameters from 'params' if available. - if Menu[k].params then - Menu[k].event = Menu[k].params.event - Menu[k].args = Menu[k].params.args or {} - end - if Menu[k].isMenuHeader then - Menu[k].readOnly = true - end - end - - exports.lation_ui:registerMenu({ - id = 'menu', - title = data.header, - onExit = data.onExit and data.onExit or nil, - subtitle = (data.headertxt and data.headertxt or ""), - options = Menu, - }) - - -- Show menu - exports.lation_ui:showMenu('menu') - - end + contextFunc[Config.System.Menu](Menu, data) end +-- Line break conversion table, basically, to automate the linebreak in functions `br` +-- This is declared and automatically sets it for what menu you are using (depends if one is uding MD formatting or HTML formatting) +local lineBreakConversion = { + ["ox"] = "\n", + ["gta"] = "\n", + ["lation"] = "\n", + + ["qb"] = "
", + ["esx"] = "
", +} + --- A line break constant used for menu header formatting. -br = (Config.System.Menu == "ox" or Config.System.Menu == "gta" or Config.System.Menu == "lation") and "\n" or "
" +br = lineBreakConversion[Config.System.Menu] --- Checks if the current menu system is 'ox' or 'gta' for formatting purposes. --- @return boolean boolean True if using ox or gta menus, otherwise false. From 392e0601a84b052c1d04f82c1f20260e2e656ceb Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 23 Aug 2025 23:36:13 +0100 Subject: [PATCH 03/13] Update wrapperfunctions.lua This simply adds names for each "option" it receives, just to make it easier to work with. --- shared/wrapperfunctions.lua | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/shared/wrapperfunctions.lua b/shared/wrapperfunctions.lua index 314be1f..3218292 100644 --- a/shared/wrapperfunctions.lua +++ b/shared/wrapperfunctions.lua @@ -21,24 +21,56 @@ --- }) --- ``` function registerCommand(command, options) + -- Name the options + local optionTable = { + helpInfo = options[1] or nil, -- help info that pops up in chat box + subText = options[2] or nil, -- sub help info for each arg + argsRequired = options[3] or nil, -- command only works if you enter args + funct = options[4] or nil, -- function that will run when command is triggered + restriction = options[5] or nil -- admin group that can use the group or `nil` + } + local commandResource = "" if isStarted(OXLibExport) then commandResource = OXLibExport - lib.addCommand(command, { help = options[1], restricted = options[5] and "group."..options[5] or nil }, options[4]) + lib.addCommand(command, + { + help = optionTable.helpInfo, + params = optionTable.subText, + restricted = optionTable.restriction and "group."..optionTable.restriction or nil + }, + optionTable.funct) elseif isStarted(QBExport) and not isStarted(QBXExport) then commandResource = QBExport - Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil) + Core.Commands.Add(command, + optionTable.helpInfo, + optionTable.subText, + optionTable.argsRequired, + optionTable.funt, + optionTable.restriction or nil + ) elseif isStarted(RSGExport) then commandResource = RSGExport - Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil) + Core.Commands.Add(command, + optionTable.helpInfo, + optionTable.subText, + optionTable.argsRequired, + optionTable.funt, + optionTable.restriction or nil + ) elseif isStarted(ESXExport) then commandResource = ESXExport - ESX.RegisterCommand(command, options[5] or 'admin', function(xPlayer, args, showError) - options[4](xPlayer.source, args, showError) - end, false, { help = options[1] }) + ESX.RegisterCommand(command, + optionTable.restriction or 'admin', + function(xPlayer, args, showError) + optionTable.funct(xPlayer.source, args, showError) + end, + false, + { help = options[1] } + ) end if commandResource ~= "" then From 354002312ef98c8479e222dabfc3d6aa09a56405 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 23 Aug 2025 23:36:42 +0100 Subject: [PATCH 04/13] Make use of `getItemLabel()` in jim_bridge more --- shared/crafting.lua | 12 ++++++------ shared/itemcontrol.lua | 17 ++++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index 93da037..0aaa24d 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -115,12 +115,12 @@ function craftingMenu(data) local metaTable = {} -- Build ingredient details. for l, b in pairs(Recipes[i][tostring(k)]) do - local label = Items[l] and Items[l].label or "error - "..l + local label = getItemLabel(l) local hasItem = checkStashItem(data.stashName, { [l] = b }) local missingMark = not hasItem and " ❌" or " " settext = settext..(settext ~= "" and br or "").."[ x"..b.." ] - "..label..missingMark - metaTable[Items[l] and Items[l].label or "error - "..l] = b + metaTable[label] = b itemTable[l] = b end @@ -128,7 +128,7 @@ function craftingMenu(data) Wait(10) end disable = not checkStashItem(data.stashName, itemTable) - setheader = ((metadata and metadata.label) or (Items[tostring(k)] and Items[tostring(k)].label) or "error - "..tostring(k)) + setheader = ((metadata and metadata.label) or getItemLabel(k)) ..(Recipes[i]["amount"] > 1 and " x"..Recipes[i]["amount"] or "") local statusEmoji = disable and " " or not canCarryTable[k] and " 📦" or " ✔️" @@ -359,7 +359,7 @@ function makeItem(data) return end if crafting and progressBar({ - label = "Using "..b.." "..Items[l].label, + label = "Using "..b.." "..getItemLabel(l), time = 1000, cancel = true, dict = 'pickup_object', @@ -398,7 +398,7 @@ function makeItem(data) PlaySoundFromEntity(s.soundId, s.audioName, PlayerPedId(), s.audioRef, true, 0) end if crafting and progressBar({ - label = bartext..((metadata and metadata.label) or Items[data.item].label).." x"..craftAmount, + label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount, time = totalBartime, cancel = true, dict = animDict, @@ -437,7 +437,7 @@ function makeItem(data) -- Run the original loop for multiple progress bars for i = 1, craftAmount do if crafting and progressBar({ - label = bartext..((metadata and metadata.label) or Items[data.item].label), + label = bartext..((metadata and metadata.label) or getItemLabel(data.item)), time = bartime, cancel = true, dict = animDict, diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua index 896b5db..40dd7ff 100644 --- a/shared/itemcontrol.lua +++ b/shared/itemcontrol.lua @@ -1585,7 +1585,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount, end else - debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..Items[item].label.."("..item..") x"..(amount or 1)) + debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..getItemLabel(item).."("..item..") x"..(amount or 1)) end end else @@ -1610,7 +1610,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount, ESX.GetPlayerFromId(src).addInventoryItem(item, amountToAdd) end else - debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..Items[item].label.."("..item..") x"..(amount or 1)) + debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..getItemLabel(item).."("..item..") x"..(amount or 1)) end end end) @@ -1951,6 +1951,7 @@ end --- if doesItemExist(item) then print("item exists") end --- ``` function doesItemExist(item) + local item = type(item) == "string" and item or tostring(item) if not item or item == "" then return false end @@ -1965,16 +1966,18 @@ end function getItemLabel(item) + local item = type(item) == "string" and item or tostring(item) + if not item or item == "" then return "" end if not Items or not next(Items) then - return item.." (Missing)" + return item.." (Missing item error)" end if Items[item] ~= nil then return Items[item].label end - return item.." (Missing)" + return item.." (Missing item error)" end @@ -2476,7 +2479,7 @@ function sellMenu(data) Menu[#Menu + 1] = { isMenuHeader = not hasTable[k].hasItem, icon = invImg(k), - header = Items[k].label..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""), + header = getItemLabel(item)..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""), txt = (Loc and Loc[Config.Lan]) and Loc[Config.Lan].info["sell_all"]..v.." "..Loc[Config.Lan].info["sell_each"] or "Sell ALL at $"..v.." each", onSelect = function() @@ -2534,7 +2537,7 @@ end --- ``` function sellAnim(data, token) if not hasItem(data.item, 1) then - triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error") + triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..getItemLabel(data.item), "error") return end @@ -2596,7 +2599,7 @@ RegisterNetEvent(getScript()..":Sellitems", function(data, token) print((hasTable[data.item].count * data.price), data.price) fundPlayer((hasTable[data.item].count * data.price), "cash", src) else - triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error", src) + triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..getItemLabel(data.item), "error", src) end end) From a851f8823c3223277e56f17188fe8ebab8d2d54b Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Mon, 25 Aug 2025 16:34:37 +0100 Subject: [PATCH 05/13] Fix sell shops showing `nil` for items --- shared/itemcontrol.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua index 40dd7ff..2969cb1 100644 --- a/shared/itemcontrol.lua +++ b/shared/itemcontrol.lua @@ -2479,7 +2479,7 @@ function sellMenu(data) Menu[#Menu + 1] = { isMenuHeader = not hasTable[k].hasItem, icon = invImg(k), - header = getItemLabel(item)..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""), + header = getItemLabel(k)..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""), txt = (Loc and Loc[Config.Lan]) and Loc[Config.Lan].info["sell_all"]..v.." "..Loc[Config.Lan].info["sell_each"] or "Sell ALL at $"..v.." each", onSelect = function() From eafa881cf8827abbca2a34d36ba585aaebad4992 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Tue, 26 Aug 2025 13:24:36 +0100 Subject: [PATCH 06/13] Adjust frameworkCache.lua to be table based Reworked `frameworkCache.lua` to the new layout Fix possibility of invisible props/ peds/vehicles on spawn Start adding very basic and unfinished support for RedM VorpCore --- frameworkCache.lua | 503 ++++++++++++++++++++++----------------- shared/_authToken.lua | 5 +- shared/_loaders.lua | 9 + shared/callback.lua | 11 + shared/coreloader.lua | 5 + shared/helpers.lua | 1 + shared/make/makePed.lua | 4 +- shared/make/makeProp.lua | 4 +- shared/make/makeVeh.lua | 2 +- starter.lua | 7 +- 10 files changed, 331 insertions(+), 220 deletions(-) diff --git a/frameworkCache.lua b/frameworkCache.lua index 714ee2a..3b8bb8d 100644 --- a/frameworkCache.lua +++ b/frameworkCache.lua @@ -31,17 +31,18 @@ local Exports = { -- REDM RSGExport = "rsg-core", - RSGInv = "rsg-inventory" + RSGInv = "rsg-inventory", + + VorpExport = "vorp_core", + VorpInv = "vorp_inventory", + VorpMenu = "vorp_menu", } -- Prevent reloading if cache is already initialized -local cache = { - Items = {}, - Vehicles = {}, - Jobs = {}, - Gangs = {}, -} +local cache = { Items = {}, Vehicles = {}, Jobs = {}, Gangs = {}, } local cacheReady = false + +-- Timer info, for debugging more then anything local timers = {} local function startTimer(label) timers[label] = GetGameTimer() @@ -52,26 +53,57 @@ local function endTimer(label) timers[label] = "("..(timers[label] / 1000).."s)" end startTimer("Cache") startTimer("Items") startTimer("Vehicles") startTimer("Jobs") startTimer("InvWeight") startTimer("InvSlots") --- Helper function to check if resource exists in server (instead of if it is already started) + +-- Helper functions -- local function checkExists(resourceName) - return GetResourceState(resourceName):find("start") or GetResourceState(resourceName):find("stopped") + local state = GetResourceState(resourceName) + return state and (state:find("start") or state:find("stopped")) end --- Ensure oxmysql resource is loaded -if checkExists(Exports.OXCoreExport) or checkExists(Exports.ESXExport) then +local function waitStarted(resourceName) + while GetResourceState(resourceName) ~= "started" do Wait(100) end +end + +local function waitStartedOrStopped(resourceName) + local state = GetResourceState(resourceName) + while state ~= "started" and state ~= "stopped" do + Wait(100) + state = GetResourceState(resourceName) + end +end + +local function dupLowercaseWeapons(items) + for k, v in pairs(items or {}) do + if type(k) == "string" then + if k:find("WEAPON") then + items[k:lower()] = v + end + else + print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?") + print("^1Possible Issue found^7:") + print(json.encode(items[k], { indent = true })) + end + end +end + +-- Forceload libs -- + +-- Ensure oxmysql resource is loaded for jim_bridge internally +-- For some core's it needs to get data from the database +if checkExists(Exports.OXCoreExport) or checkExists(Exports.ESXExport) or checkExists(Exports.VorpExport) then local fileLoader = assert(load(LoadResourceFile("oxmysql", ('lib/MySQL.lua')), ('@@oxmysql/lib/MySQL.lua'))) fileLoader() end 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 + waitStartedOrStopped(Exports.OXCoreExport) 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 + waitStarted(Exports.ESXExport) local fileLoader = assert(load(LoadResourceFile(Exports.ESXExport, ('/imports.lua')), ('@@'..Exports.ESXExport..'/imports.lua'))) fileLoader() end @@ -89,225 +121,274 @@ end --------------------- ---- Load Items ----- --------------------- --- Items initialization based on detected inventory system -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 - local success, result = pcall(function() - return exports[Exports.OXInv]:Items() - end) - if success and result then - cache.Items = result - end - - -- Get Weapon info and duplicate them if they are uppercase - -- (duplicate incase anything checks for the uppercase version) - for k, v in pairs(cache.Items) do - if type(k) == "string" then - if k:find("WEAPON") then - cache.Items[k:lower()] = cache.Items[k] +local itemFunc = { + { script = Exports.OXInv, + cacheItem = function() + local success, result = pcall(function() + return exports[Exports.OXInv]:Items() + end) + if success and result then + cache.Items = result end - else - print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?") - print("^1Possible Issue found^7:") - print(json.encode(cache.Items[k], {indent = true})) - end - end - -elseif checkExists(Exports.TgiannInv) then - -- Wait for OX Inventory to start if it's not already started - while GetResourceState(Exports.TgiannInv) ~= "started" do Wait(100) end - itemResource = Exports.TgiannInv - - local success, result = pcall(function() - return exports[Exports.TgiannInv]:Items() - end) - if success and result then - cache.Items = result - end - - -- Get Weapon info and duplicate them if they are uppercase - -- (duplicate incase anything checks for the uppercase version) - for k, v in pairs(cache.Items) do - if type(k) == "string" then - if k:find("WEAPON") then - cache.Items[k:lower()] = cache.Items[k] + end, + }, + { script = Exports.TgiannInv, + cacheItem = function() + local success, result = pcall(function() + return exports[Exports.TgiannInv]:Items() + end) + if success and result then + cache.Items = result end - else - print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?") - print("^1Possible Issue found^7:") - print(json.encode(cache.Items[k], {indent = true})) - end + end, + }, + { script = Exports.QBXExport, + cacheItem = function() + cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items + -- If this is nil, they need to update to qbx_core 1.23.0+ + if not cache.Items then + -- if their inventory doesn't allow that (they refuse to update their butchered core replacement): + if GetResourceState(Exports.QSInv):find("start") then + itemResource = Exports.QSInv + cache.Items = exports[Exports.QSInv]:GetItemList() + + elseif GetResourceState(Exports.OrigenInv):find("start") then + itemResource = Exports.OrigenInv + cache.Items = exports[Exports.OrigenInv]:Items() + + elseif GetResourceState(Exports.CodeMInv):find("start") then + itemResource = Exports.CodeMInv + cache.Items = exports[Exports.CodeMInv]:GetItemList() + + elseif GetResourceState(Exports.CoreInv):find("start") then + itemResource = Exports.CoreInv + cache.Items = exports[Exports.CoreInv]:getItemsList() + + elseif GetResourceState(Exports.TgiannInv):find("start") then + itemResource = Exports.TgiannInv + cache.Items = exports[Exports.TgiannInv]:Items() + end + end + end, + }, + { script = Exports.QBExport, + cacheItem = function() + cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items + end, + }, + { script = Exports.ESXExport, + cacheItem = function() + if GetResourceState(Exports.QSInv):find("start") then + cache.Items = exports[Exports.QSInv]:GetItemList() + else + cache.Items = ESX.GetItems() + while not next(cache.Items) do + cache.Items = ESX.GetItems() + Wait(1000) + end + end + end, + }, + { script = Exports.RSGExport, + cacheItem = function() + cache.Items = exports[Exports.RSGExport]:GetCoreObject().Shared.Items + end, + }, + { script = Exports.VorpInv, + cacheItem = function() + local dbItems = MySQL.query.await('SELECT * FROM `items`') + local tempItems = {} + for i = 1, #dbItems do + local v = dbItems[i] + tempItems[v.item] = { + name = v.item, + label = v.label, + weight = v.weight, + info = v.metadata, + usable = v.usable, + type = v.type, + description = v.desc, + } + end + cache.Items = tempItems + end, + }, +} + +for i = 1, #itemFunc do + local data = itemFunc[i] + if checkExists(data.script) then + waitStarted(data.script) -- Wait for detected script to start fully + data.cacheItem() -- run tablized function for core/inv + dupLowercaseWeapons(cache.Items) -- make usable weapon item names for jim_bridge + itemResource = data.script -- Grab script name to announce later + endTimer("Items") -- end timer + break -- break loop so it doesn't keep checking end - -elseif checkExists(Exports.QBXExport) then - while GetResourceState(Exports.QBXExport) ~= "started" do Wait(100) end - itemResource = Exports.QBXExport - cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items - -- If this is nil, they need to update to qbx_core 1.23.0+ - if not cache.Items then - -- if their inventory doesn't allow that (they refuse to update their butchered core replacement): - if GetResourceState(Exports.QSInv):find("start") then - itemResource = Exports.QSInv - cache.Items = exports[Exports.QSInv]:GetItemList() - - elseif GetResourceState(Exports.OrigenInv):find("start") then - itemResource = Exports.OrigenInv - cache.Items = exports[Exports.OrigenInv]:Items() - - elseif GetResourceState(Exports.CodeMInv):find("start") then - itemResource = Exports.CodeMInv - cache.Items = exports[Exports.CodeMInv]:GetItemList() - - elseif GetResourceState(Exports.CoreInv):find("start") then - itemResource = Exports.CoreInv - cache.Items = exports[Exports.CoreInv]:getItemsList() - - elseif GetResourceState(Exports.TgiannInv):find("start") then - itemResource = Exports.TgiannInv - cache.Items = exports[Exports.TgiannInv]:Items() - end - end - -elseif checkExists(Exports.QBExport) then - while GetResourceState(Exports.QBExport) ~= "started" do Wait(100) end - itemResource = Exports.QBExport - cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items - -elseif checkExists(Exports.ESXExport) then - itemResource = Exports.ESXExport - if GetResourceState(Exports.QSInv):find("start") then - cache.Items = exports[Exports.QSInv]:GetItemList() - else - cache.Items = ESX.GetItems() - while not next(cache.Items) do - cache.Items = ESX.GetItems() - Wait(1000) - end - end - -elseif checkExists(Exports.RSGExport) then - while GetResourceState(Exports.RSGExport) ~= "started" do Wait(100) end - itemResource = Exports.RSGExport - cache.Items = exports[Exports.RSGExport]:GetCoreObject().Shared.Items end -endTimer("Items") --------------------- --- Load Vehicles --- --------------------- --- Vehicle loading depending on framework -if checkExists(Exports.QBXExport) then - vehResource = Exports.QBXExport - cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles +--- +local vehicleFunc = { -elseif checkExists(Exports.QBExport)then - vehResource = Exports.QBExport - cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles + { script = Exports.QBXExport, + cacheVehicle = function() + cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles + end, + }, + { script = Exports.QBExport, + cacheVehicle = function() + cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles + end, + }, + { script = Exports.OXCoreExport, + cacheVehicle = function() + cache.Vehicles = {} + for k, v in pairs(Ox.GetVehicleData()) do + cache.Vehicles[k] = { + model = k, hash = joaat(k), + price = v.price, + name = v.name, + brand = v.make + } + end + end, + }, + { script = Exports.ESXExport, + cacheVehicle = function() + while not MySQL do Wait(100) end + for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do + cache.Vehicles[v.model] = { + model = v.model, + hash = joaat(v.model), + price = v.price, + name = v.name, + } + end + end, + }, + { script = Exports.RSGExport, + cacheVehicle = function() + cache.Vehicles = exports[Exports.RSGExport]:GetCoreObject().Shared.Vehicles + end, + }, + { script = Exports.VorpExport, + cacheVehicle = function() + cache.Vehicles = { ["unkown"] = {} } + end, + }, +} -elseif checkExists(Exports.OXCoreExport) then - vehResource = Exports.OXCoreExport - cache.Vehicles = {} - for k, v in pairs(Ox.GetVehicleData()) do - cache.Vehicles[k] = { - model = k, hash = joaat(k), - price = v.price, - name = v.name, - brand = v.make - } +for i = 1, #vehicleFunc do + local data = vehicleFunc[i] + if checkExists(data.script) then + waitStarted(data.script) -- Wait for detected script to start fully + data.cacheVehicle() -- run tablized function for core + vehResource = data.script -- Grab script name to announce later + endTimer("Vehicles") -- end timer + break -- break loop so it doesn't keep checking end - -elseif checkExists(Exports.ESXExport) then - vehResource = Exports.ESXExport - while not MySQL do Wait(1000) end - for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do - cache.Vehicles[v.model] = { - model = v.model, - hash = joaat(v.model), - price = v.price, - name = v.name, - } - end - -elseif checkExists(Exports.RSGExport) then - vehResource = Exports.RSGExport - cache.Vehicles = exports[Exports.RSGExport]:GetCoreObject().Shared.Vehicles - end -endTimer("Vehicles") --------------------- ----- Load Jobs ----- --------------------- --- Jobs loading based on framework -if checkExists(Exports.QBXExport) then - jobResource = Exports.QBXExport - cache.Jobs, cache.Gangs = exports[Exports.QBXExport]:GetJobs(), exports[Exports.QBXExport]:GetGangs() -elseif checkExists(Exports.QBExport) then - jobResource = Exports.QBExport - cache.Jobs, cache.Gangs = exports[Exports.QBExport]:GetCoreObject().Shared.Jobs, exports[Exports.QBExport]:GetCoreObject().Shared.Gangs +local jobFunc = { -elseif checkExists(Exports.OXCoreExport) then - jobResource = Exports.OXCoreExport - 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 - cache.Jobs[job.name] = { - label = job.label, - grades = gradeMap[job.name] or {} - } - end - cache.Gangs = cache.Jobs - -elseif checkExists(Exports.ESXExport) then - jobResource = Exports.ESXExport - ESX = exports[Exports.ESXExport]:getSharedObject() - cache.Jobs = ESX.GetJobs() - while not next(cache.Jobs) do - Wait(100) - cache.Jobs = ESX.GetJobs() - end - for Role, Grades in pairs(cache.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 - cache.Jobs[Role].grades[grade].isBoss = true - goto continue + { script = Exports.QBXExport, + cacheJob = function() + cache.Jobs, cache.Gangs = exports[Exports.QBXExport]:GetJobs(), exports[Exports.QBXExport]:GetGangs() + end, + }, + { script = Exports.QBExport, + cacheJob = function() + Core = exports[Exports.QBExport]:GetCoreObject() + cache.Jobs, cache.Gangs = Core.Shared.Jobs, Core.Shared.Gangs + end, + }, + { script = Exports.OXCoreExport, + cacheJob = function() + while not MySQL do Wait(100) 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 - 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 + for _, job in pairs(tempJobs) do + cache.Jobs[job.name] = { + label = job.label, + grades = gradeMap[job.name] or {} + } end - end + cache.Gangs = cache.Jobs + end, + }, + { script = Exports.ESXExport, + cacheJob = function() + ESX = ESX or exports[Exports.ESXExport]:getSharedObject() + cache.Jobs = ESX.GetJobs() + while not next(cache.Jobs) do + Wait(100) + cache.Jobs = ESX.GetJobs() + end + for Role, Grades in pairs(cache.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 + cache.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 - cache.Jobs[Role].grades[tostring(highestGrade)].isBoss = true - end - ::continue:: + if highestGrade then + cache.Jobs[Role].grades[tostring(highestGrade)].isBoss = true + end + ::continue:: + end + cache.Gangs = cache.Jobs + end, + }, + { script = Exports.RSGExport, + cacheJob = function() + Core = exports[Exports.RSGExport]:GetCoreObject() + cache.Jobs, cache.Gangs = Core.Shared.Jobs, Core.Shared.Gangs + end, + }, + { script = Exports.VorpExport, + cacheJob = function() + cache.Jobs = { ["unkown"] = {} } + cache.Gangs = cache.Jobs + end, + }, +} + +for i = 1, #jobFunc do + local data = jobFunc[i] + if checkExists(data.script) then + waitStarted(data.script) -- Wait for detected script to start fully + data.cacheJob() -- run tablized function for core + jobResource = data.script -- Grab script name to announce later + endTimer("Jobs") -- end timer + break -- break loop so it doesn't keep checking end - cache.Gangs = cache.Jobs - -elseif checkExists(Exports.RSGExport) then - jobResource = Exports.RSGExport - cache.Jobs, cache.Gangs = exports[Exports.RSGExport]:GetCoreObject().Shared.Jobs, exports[Exports.RSGExport]:GetCoreObject().Shared.Gangs - end -endTimer("Jobs") + -- Fallback if nil or empty if cache.Items == nil or not next(cache.Items) then @@ -402,10 +483,8 @@ for script, data in pairs(invWeightTable) do if checkExists(script) then if script == Exports.QBInv and GetResourceState(Exports.JPRInv):find("start") then goto skip end - while GetResourceState(script) ~= "started" and GetResourceState(script) ~= "stopped" do - Wait(100) - print("Waiting for script start") - end + waitStartedOrStopped(script) + local attempts = data.fallback or { data } local lookup, used, err diff --git a/shared/_authToken.lua b/shared/_authToken.lua index 901aa32..3104d2e 100644 --- a/shared/_authToken.lua +++ b/shared/_authToken.lua @@ -12,7 +12,7 @@ if isServer() then local src = source local token = keyGen()..keyGen()..keyGen()..keyGen() -- Use a secure random generator here --debugPrint(GetInvokingResource()) - if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then + if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= Exports.QBExport and GetInvokingResource() ~= Exports.VorpExport then debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7") return "" end @@ -45,8 +45,9 @@ if isServer() then createCallback(getScript()..":callback:GetAuthEvent", function(source) local src = source + --debugPrint(GetInvokingResource()) - if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then + if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= Exports.QBExport and GetInvokingResource() ~= Exports.VorpExport then debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital callback was called from an external resource^7") return "" end diff --git a/shared/_loaders.lua b/shared/_loaders.lua index 823bba1..9644e7e 100644 --- a/shared/_loaders.lua +++ b/shared/_loaders.lua @@ -62,6 +62,9 @@ function onPlayerLoaded(func, onStart) elseif isStarted(RSGExport) then onPlayerFramework = RSGExport RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', tempFunc) + elseif isStarted(VorpExport) then + onPlayerFramework = VorpExport + RegisterNetEvent("vorp_core:Client:OnPlayerSpawned", tempFunc) end if onPlayerFramework ~= "" then @@ -174,6 +177,12 @@ function waitForLogin() break end end + elseif isStarted(VorpExport) then + -- For other frameworks, use LocalPlayer.state.isLoggedIn. + while not LocalPlayer.state.IsInSession and (GetGameTimer() - startTime) < timeout do + Wait(100) + end + loggedIn = LocalPlayer.state.IsInSession else -- For other frameworks, use LocalPlayer.state.isLoggedIn. while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do diff --git a/shared/callback.lua b/shared/callback.lua index 729e73d..b6ad9d6 100644 --- a/shared/callback.lua +++ b/shared/callback.lua @@ -37,6 +37,10 @@ function createCallback(callbackName, funct) debugPrint("^6Bridge^7: ^2Registering ^4"..QBExport.." ^3Callback^7:", callbackName) Core = Core or exports[QBExport]:GetCoreObject() Core.Functions.CreateCallback(callbackName, adaptedFunction) + elseif isStarted(VorpExport) then + debugPrint("^6Bridge^7: ^2Registering ^4"..VorpExport.." ^3Callback^7:", callbackName) + Core = Core or exports.vorp_core:GetCore() + Core.Callback.Register(callbackName, adaptedFunction) elseif isStarted(ESXExport) then debugPrint("^6Bridge^7: ^2Registering ^4"..ESXExport.." ^3Callback^7:", callbackName) ESX.RegisterServerCallback(callbackName, adaptedFunction) @@ -76,6 +80,13 @@ function triggerCallback(callbackName, ...) end, ...) result = Citizen.Await(p) Wait(10) + elseif isStarted(VorpExport) then + local p = promise.new() + Core.Callback.TriggerAwait(callbackName, function(cbResult) + p:resolve(cbResult) + end, ...) + result = Citizen.Await(p) + Wait(10) elseif isStarted(ESXExport) then local p = promise.new() ESX.TriggerServerCallback(callbackName, function(cbResult) diff --git a/shared/coreloader.lua b/shared/coreloader.lua index 2e69473..349b866 100644 --- a/shared/coreloader.lua +++ b/shared/coreloader.lua @@ -22,6 +22,9 @@ OXInv, QBInv, PSInv, QSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv, JPRInv = Exports.JPRInv or "" RSGExport, RSGInv = Exports.RSGExport or "", Exports.RSGInv or "" +VorpExport, VorpInv = Exports.VorpExport or "", Exports.VorpInv or "" + + QBMenuExport = Exports.QBMenuExport or "" QBTargetExport, OXTargetExport = Exports.QBTargetExport or "", Exports.OXTargetExport or "" @@ -29,6 +32,8 @@ if isStarted(QBXExport) or isStarted(QBExport) then Core = Core or exports[QBExport]:GetCoreObject() elseif isStarted(RSGExport) then Core = Core or exports[RSGExport]:GetCoreObject() +elseif isStarted(VorpExport) then + Core = Core or exports[VorpExport]:GetCore() end diff --git a/shared/helpers.lua b/shared/helpers.lua index a3f32f0..42ae696 100644 --- a/shared/helpers.lua +++ b/shared/helpers.lua @@ -180,6 +180,7 @@ function GetPrintTime() local hour, min, sec = os.date('%H'), os.date('%M'), os.date('%S') return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")" else + if gameName == "rdr3" then return "" end local _, _, _, hour, min, sec = GetLocalTime() return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")" end diff --git a/shared/make/makePed.lua b/shared/make/makePed.lua index 9ab670c..ea95d84 100644 --- a/shared/make/makePed.lua +++ b/shared/make/makePed.lua @@ -65,7 +65,6 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade) model = data.model loadModel(data.model) ped = CreatePed(0, model, coords.x, coords.y, coords.z - 1.03, coords.w, synced and synced or false, false) - SetEntityAlpha(ped, 0, false) -- Inheritance SetPedHeadBlendData(ped, data.custom.faceFather, data.custom.faceMother, data.custom.raceShape, data.custom.skinFather, data.custom.skinMother, data.custom.raceSkin, data.custom.faceMix or 0, data.custom.skinMix or 0, data.custom.raceMix or 0, false) @@ -145,7 +144,8 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade) end unloadModel(model) Peds[keyGen()..keyGen()] = ped - if fade ~= false then + if fade ~= false and gameName ~= "rdr3" then + SetEntityAlpha(ped, 0, false) CreateThread(function() fadeInEnt(ped) end) diff --git a/shared/make/makeProp.lua b/shared/make/makeProp.lua index 28ac20d..b2cd590 100644 --- a/shared/make/makeProp.lua +++ b/shared/make/makeProp.lua @@ -24,14 +24,14 @@ local distProps = {} function makeProp(data, freeze, synced, fade) loadModel(data.prop) local prop = CreateObject(data.prop, data.coords.x, data.coords.y, data.coords.z-1.03, synced and synced or false, synced and synced or false, false) - SetEntityAlpha(veh, 0, false) SetEntityHeading(prop, (data.coords.w or 0) + 180.0) FreezeEntityPosition(prop, freeze or false) debugPrint("^6Bridge^7: ^1Prop ^2Created^7: '^6"..prop.."^7' | ^2Hash^7: ^7'^6"..data.prop.."^7' | ^2Coord^7: "..formatCoord(data.coords)) SetModelAsNoLongerNeeded(data.prop) Props[keyGen()..keyGen()] = prop - if fade ~= false then + if fade ~= false and gameName ~= "rdr3" then + SetEntityAlpha(prop, 0, false) CreateThread(function() fadeInEnt(prop) end) diff --git a/shared/make/makeVeh.lua b/shared/make/makeVeh.lua index 80b272c..a4fb39d 100644 --- a/shared/make/makeVeh.lua +++ b/shared/make/makeVeh.lua @@ -30,7 +30,7 @@ function makeVeh(model, coords, synced, fade) debugPrint("^6Bridge^7: ^1Veh ^2Created^7: '^6"..veh.."^7' | ^2Hash^7: ^7'^6"..model.."^7' | ^2Coord^7: "..formatCoord(coords)) unloadModel(model) Vehicles[#Vehicles + 1] = veh - if fade ~= false then + if fade ~= false and gameName ~= "rdr3" then SetEntityAlpha(veh, 0, false) CreateThread(function() fadeInEnt(veh) diff --git a/starter.lua b/starter.lua index f990d99..6c14d1d 100644 --- a/starter.lua +++ b/starter.lua @@ -25,7 +25,12 @@ Exports = { -- REDM RSGExport = "rsg-core", - RSGInv = "rsg-inventory" + RSGInv = "rsg-inventory", + + VorpExport = "vorp_core", + VorpInv = "vorp_inventory", + VorpMenu = "vorp_menu", + } -- Required variables From 315634c173335e40103d8ec2f01d56b2107ea3f7 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Wed, 27 Aug 2025 19:40:11 +0100 Subject: [PATCH 07/13] add `supress_updates` variable to config This new `fxmanifest.lua` variable (when true) will stop checking for script updates People REALLY dont like being told when somethings fixed. This will stop the update notifications for both `jim_bridge` and the script that loads it. --- _versioncheck.lua | 125 +++++++++++++++------------ fxmanifest.lua | 5 +- shared/_scriptversioncheck.lua | 153 ++++++++++++++++++--------------- 3 files changed, 153 insertions(+), 130 deletions(-) diff --git a/_versioncheck.lua b/_versioncheck.lua index 2423e78..56d6217 100644 --- a/_versioncheck.lua +++ b/_versioncheck.lua @@ -1,67 +1,78 @@ -local function parseVersion(version) - local parts = {} - for num in version:gmatch("%d+") do - table.insert(parts, tonumber(num)) - end - return parts +local function readBoolMeta(key, default) + local val = GetResourceMetadata(GetCurrentResourceName(), key, 0) + if not val then return default end + val = tostring(val):lower() + return (val == 'true' or val == '1' or val == 'yes' or val == 'on') end -local function compareVersions(current, newest) - local currentParts = parseVersion(current) - local newestParts = parseVersion(newest) - for i = 1, math.max(#currentParts, #newestParts) do - local c = currentParts[i] or 0 - local n = newestParts[i] or 0 - if c < n then return -1 - elseif c > n then return 1 end +local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false) + +if not SUPPRESS_UPDATES then + local function parseVersion(version) + local parts = {} + for num in version:gmatch("%d+") do + table.insert(parts, tonumber(num)) + end + return parts end - return 0 -- equal -end -function CheckBridgeVersion() - if IsDuplicityVersion() then - CreateThread(function() - Wait(4000) - local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version') - --PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers) - PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers) - if not body then - print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)") - return - end + local function compareVersions(current, newest) + local currentParts = parseVersion(current) + local newestParts = parseVersion(newest) + for i = 1, math.max(#currentParts, #newestParts) do + local c = currentParts[i] or 0 + local n = newestParts[i] or 0 + if c < n then return -1 + elseif c > n then return 1 end + end + return 0 -- equal + end - local lines = {} - for line in body:gmatch("[^\r\n]+") do - table.insert(lines, line) - end - - local newestVersionRaw = lines[1] or "0.0.0" - local changelog = {} - for i = 2, #lines do - table.insert(changelog, lines[i]) - end - - local compareResult = compareVersions(currentVersionRaw, newestVersionRaw) - - if compareResult == 0 then - print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)") - elseif compareResult < 0 then - print("^1----------------------------------------------------------------------^7") - print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)") - for _, line in ipairs(changelog) do - print((line:find("http") and "^7" or "^5")..line) + function CheckBridgeVersion() + if IsDuplicityVersion() then + CreateThread(function() + Wait(4000) + local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version') + --PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers) + PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers) + if not body then + print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)") + return end - print("^1----------------------------------------------------------------------^7") - SetTimeout(3600000, function() - CheckBridgeVersion() - end) - else - print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)") - end + + local lines = {} + for line in body:gmatch("[^\r\n]+") do + table.insert(lines, line) + end + + local newestVersionRaw = lines[1] or "0.0.0" + local changelog = {} + for i = 2, #lines do + table.insert(changelog, lines[i]) + end + + local compareResult = compareVersions(currentVersionRaw, newestVersionRaw) + + if compareResult == 0 then + print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)") + elseif compareResult < 0 then + print("^1----------------------------------------------------------------------^7") + print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)") + for _, line in ipairs(changelog) do + print((line:find("http") and "^7" or "^5")..line) + end + print("^1----------------------------------------------------------------------^7") + SetTimeout(3600000, function() + CheckBridgeVersion() + end) + else + print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)") + end + end) end) - end) + end end -end -CheckBridgeVersion() \ No newline at end of file + CheckBridgeVersion() +end \ No newline at end of file diff --git a/fxmanifest.lua b/fxmanifest.lua index 2475595..0a3a719 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -7,7 +7,6 @@ rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aw games { 'gta5', 'rdr3' } lua54 'yes' - files { 'starter.lua', 'shared/*.lua', @@ -24,4 +23,6 @@ server_scripts { client_scripts { 'clientFrameworkCache.lua', 'ui_modules/*.lua', -} \ No newline at end of file +} + +suppress_updates 'true' -- set to 'true' to disable update pings \ No newline at end of file diff --git a/shared/_scriptversioncheck.lua b/shared/_scriptversioncheck.lua index 7562e26..8cab90f 100644 --- a/shared/_scriptversioncheck.lua +++ b/shared/_scriptversioncheck.lua @@ -1,100 +1,111 @@ -function parseVersion(version) - local parts = {} - for num in version:gmatch("%d+") do - table.insert(parts, tonumber(num)) +local function readBoolMeta(key, default) + local val = GetResourceMetadata("jim_bridge", key, 0) + if not val then return default end + val = tostring(val):lower() + return (val == 'true' or val == '1' or val == 'yes' or val == 'on') +end + +local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false) + +if not SUPPRESS_UPDATES then + function parseVersion(version) + local parts = {} + for num in version:gmatch("%d+") do + table.insert(parts, tonumber(num)) + end + return parts end - return parts -end -function compareVersions(current, newest) - local currentParts = parseVersion(current) - local newestParts = parseVersion(newest) - for i = 1, math.max(#currentParts, #newestParts) do - local c = currentParts[i] or 0 - local n = newestParts[i] or 0 - if c < n then return -1 - elseif c > n then return 1 end + function compareVersions(current, newest) + local currentParts = parseVersion(current) + local newestParts = parseVersion(newest) + for i = 1, math.max(#currentParts, #newestParts) do + local c = currentParts[i] or 0 + local n = newestParts[i] or 0 + if c < n then return -1 + elseif c > n then return 1 end + end + return 0 end - return 0 -end -function capitalize(str) - return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end)) -end + function capitalize(str) + return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end)) + end -local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or "" -local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or "" -local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or "" -local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or "" + local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or "" + local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or "" + local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or "" + local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or "" -print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7") + print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7") -function CheckVersion() - if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then - CreateThread(function() - Wait(4000) - local script = getScript() - local currentVersionRaw = GetResourceMetadata(script, 'version') + function CheckVersion() + if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then + CreateThread(function() + Wait(4000) + local script = getScript() + local currentVersionRaw = GetResourceMetadata(script, 'version') - PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers) - if not newestVersionRaw then - -- fallback - PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers) - if not fallbackVersionRaw then - print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)") - return - end + PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers) + if not newestVersionRaw then + -- fallback + PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers) + if not fallbackVersionRaw then + print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)") + return + end + local lines = {} + for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end + local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "") + local changelog = {} + for i = 2, #lines do table.insert(changelog, lines[i]) end + + local compareResult = compareVersions(currentVersionRaw, fallbackVersion) + if compareResult == 0 then + print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)") + elseif compareResult < 0 then + print("^1----------------------------------------------------------------------^7") + print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)") + if #changelog > 0 then + for _, line in ipairs(changelog) do + print((line:find("http") and "^7" or "^5")..line) + end + end + print("^1----------------------------------------------------------------------^7") + SetTimeout(1200000, function() CheckVersion() end) + else + print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)") + end + end) + else local lines = {} - for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end - local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "") + for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end + local newestVersion = (lines[1] or "0.0.0"):gsub("v", "") local changelog = {} for i = 2, #lines do table.insert(changelog, lines[i]) end - local compareResult = compareVersions(currentVersionRaw, fallbackVersion) + local compareResult = compareVersions(currentVersionRaw, newestVersion) if compareResult == 0 then print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)") elseif compareResult < 0 then print("^1----------------------------------------------------------------------^7") - print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)") + print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)") if #changelog > 0 then for _, line in ipairs(changelog) do print((line:find("http") and "^7" or "^5")..line) end end print("^1----------------------------------------------------------------------^7") - SetTimeout(1200000, function() CheckVersion() end) + SetTimeout(3600000, function() CheckVersion() end) else - print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)") + print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)") end - end) - else - local lines = {} - for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end - local newestVersion = (lines[1] or "0.0.0"):gsub("v", "") - local changelog = {} - for i = 2, #lines do table.insert(changelog, lines[i]) end - - local compareResult = compareVersions(currentVersionRaw, newestVersion) - if compareResult == 0 then - print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)") - elseif compareResult < 0 then - print("^1----------------------------------------------------------------------^7") - print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)") - if #changelog > 0 then - for _, line in ipairs(changelog) do - print((line:find("http") and "^7" or "^5")..line) - end - end - print("^1----------------------------------------------------------------------^7") - SetTimeout(3600000, function() CheckVersion() end) - else - print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)") end - end + end) end) - end) + end end -end -CheckVersion() + CheckVersion() +end \ No newline at end of file From cb49fa694928b4ecfb8bcc74f13d0d049b87251b Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Wed, 27 Aug 2025 19:57:56 +0100 Subject: [PATCH 08/13] Update fxmanifest.lua --- fxmanifest.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fxmanifest.lua b/fxmanifest.lua index 0a3a719..177743c 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -25,4 +25,4 @@ client_scripts { 'ui_modules/*.lua', } -suppress_updates 'true' -- set to 'true' to disable update pings \ No newline at end of file +suppress_updates 'false' -- set to 'true' to disable update pings \ No newline at end of file From 1e5d4800fc001daccef004e684fd5c74db0fc4a5 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Fri, 29 Aug 2025 20:39:30 +0100 Subject: [PATCH 09/13] debug print progressbar results --- shared/make/progressBars.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/make/progressBars.lua b/shared/make/progressBars.lua index 3c7b77a..0e2eb72 100644 --- a/shared/make/progressBars.lua +++ b/shared/make/progressBars.lua @@ -273,6 +273,7 @@ function progressBar(data) while result == nil do Wait(10) end + debugPrint("^5Debug^7: ^2ProgressBar result^7: ^3"..tostring(result).."^7") -- Cleanup FreezeEntityPosition(ped, false) From be5449b100db904412feef8bc717eec09f08de76 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 30 Aug 2025 12:15:09 +0100 Subject: [PATCH 10/13] Add `promise:new()` for qb/esx progressbars This stops them instantly returning `false` and actually waits for the results This fixes issues with players being unable to complete tasks determined by progressbars eg. installing items on a vehicle and it fails instantly --- shared/make/progressBars.lua | 406 ++++++++++++++++++----------------- 1 file changed, 214 insertions(+), 192 deletions(-) diff --git a/shared/make/progressBars.lua b/shared/make/progressBars.lua index 0e2eb72..4b98ced 100644 --- a/shared/make/progressBars.lua +++ b/shared/make/progressBars.lua @@ -4,220 +4,236 @@ local storedPID = nil local progressFunc = { ox = { - start = function(data) - local options = { - duration = debugMode and 1000 or data.time, - label = data.label, - position = data.position or "bottom", - useWhileDead = data.dead or false, - canCancel = data.cancel or true, - anim = { - dict = data.dict, - clip = data.anim, - flag = (data.flag == 8 and 32 or data.flag) or nil, - scenario = data.task - }, - disable = { - combat = data.combat or true, - move = data.disableMovement or false, - car = data.disableMovement or false, - mouse = data.mouse or false - }, - } - if data.prop and data.prop.model then - options.prop = { - model = data.prop.model, - pos = data.prop.pos or vec3(0, 0, 0), - rot = data.prop.rot or vec3(0, 0, 0), - bone = data.prop.bone or 0 + start = + function(data) + local options = { + duration = debugMode and 1000 or data.time, + label = data.label, + position = data.position or "bottom", + useWhileDead = data.dead or false, + canCancel = data.cancel or true, + anim = { + dict = data.dict, + clip = data.anim, + flag = (data.flag == 8 and 32 or data.flag) or nil, + scenario = data.task + }, + disable = { + combat = data.combat or true, + move = data.disableMovement or false, + car = data.disableMovement or false, + mouse = data.mouse or false + }, } - end - if data.propTwo and data.propTwo.model then - options.propTwo = { - model = data.propTwo.model, - pos = data.propTwo.pos or vec3(0, 0, 0), - rot = data.propTwo.rot or vec3(0, 0, 0), - bone = data.propTwo.bone or 0 - } - end - if data.progressType == "circle" then - if exports[OXLibExport]:progressCircle(options) then - return true - else - return false + if data.prop and data.prop.model then + options.prop = { + model = data.prop.model, + pos = data.prop.pos or vec3(0, 0, 0), + rot = data.prop.rot or vec3(0, 0, 0), + bone = data.prop.bone or 0 + } end - end - if not data.progressType or data.progressType == "bar" then - if exports[OXLibExport]:progressBar(options) then - return true - else - return false + if data.propTwo and data.propTwo.model then + options.propTwo = { + model = data.propTwo.model, + pos = data.propTwo.pos or vec3(0, 0, 0), + rot = data.propTwo.rot or vec3(0, 0, 0), + bone = data.propTwo.bone or 0 + } end - end - end, - stop = function() - exports[OXLibExport]:cancelProgress() - end, + if data.progressType == "circle" then + if exports[OXLibExport]:progressCircle(options) then + return true + else + return false + end + end + if not data.progressType or data.progressType == "bar" then + if exports[OXLibExport]:progressBar(options) then + return true + else + print("^1progressBar was not successful") + end + end + end, + stop = + function() + exports[OXLibExport]:cancelProgress() + end, }, qb = { - start = function(data) - Core.Functions.Progressbar("progbar", - data.label, - debugMode and 1000 or data.time, - data.dead or false, - data.cancel or true, - { - disableMovement = data.disableMovement or false, - disableCarMovement = data.disableMovement or false, - disableMouse = data.disableMouse or false, - disableCombat = data.disableCombat or true, - }, - { - animDict = data.dict, - anim = data.anim, - flags = data.flag or 32, - task = data.task - }, - {}, {}, - function() - return true - end, function() - return false - end, data.icon) + start = + function(data) + local p = promise.new() + Core.Functions.Progressbar("progbar", + data.label, + debugMode and 1000 or data.time, + data.dead or false, + data.cancel or true, + { + disableMovement = data.disableMovement or false, + disableCarMovement = data.disableMovement or false, + disableMouse = data.disableMouse or false, + disableCombat = data.disableCombat or true, + }, + { + animDict = data.dict, + anim = data.anim, + flags = data.flag or 32, + task = data.task + }, + {}, {}, + function() p:resolve(true) end, + function() p:resolve(false) end, + data.icon) + return Citizen.Await(p) + end, + stop = + function() + TriggerEvent("progressbar:client:cancel") end, - stop = function() - TriggerEvent("progressbar:client:cancel") - end, }, qs = { - start = function(data) - if exports['qs-interface']:ProgressBar({ - duration = debugMode and 1000 or data.time, - label = data.label, - position = 'bottom', - useWhileDead = data.dead or false, - canCancel = data.cancel or true, - disable = data.disableMovement or false, - anim = { - dict = data.dict, - clip = data.anim, - flag = data.flag or 32 - }, - prop = nil - }) then - return true - else - return false - end - end, - stop = function() - --?? - TriggerEvent("progressbar:client:cancel") - end, + start = + function(data) + if exports['qs-interface']:ProgressBar({ + duration = debugMode and 1000 or data.time, + label = data.label, + position = 'bottom', + useWhileDead = data.dead or false, + canCancel = data.cancel or true, + disable = data.disableMovement or false, + anim = { + dict = data.dict, + clip = data.anim, + flag = data.flag or 32 + }, + prop = nil + }) then + return true + else + return false + end + end, + stop = + function() + --?? + TriggerEvent("progressbar:client:cancel") + end, }, esx = { - start = function(data) - ESX.Progressbar(data.label, debugMode and 1000 or data.time, { - FreezePlayer = true, - animation = { - type = data.anim, - dict = data.dict, - scenario = data.task, - }, - onFinish = function() - return true - end, - onCancel = function() - return false - end - }) - end, - stop = function() - ESX.CancelProgressbar() - end, + start = + function(data) + local p = promise.new() + ESX.Progressbar(data.label, debugMode and 1000 or data.time, { + FreezePlayer = true, + animation = { + type = data.anim, + dict = data.dict, + scenario = data.task, + }, + onFinish = function() + p:resolve(true) + end, + onCancel = function() + p:resolve(false) + return false + end + }) + return Citizen.Await(p) + end, + stop = + function() + ESX.CancelProgressbar() + end, }, lation = { - start = function(data) - if exports.lation_ui:progressBar({ - label = data.label, - description = nil, - duration = debugMode and 1000 or data.time, - icon = data.icon, - useWhileDead = data.dead or false, - disable = { - combat = data.combat or true, - move = data.disableMovement or false, - car = data.disableMovement or false, - }, - anim = { - dict = data.dict, - clip = data.anim, - flag = data.flag - }, - prop = { - model = data.prop and data.prop.model, - pos = data.prop and (data.prop.pos or vec3(0, 0, 0)), - rot = data.prop and (data.prop.rot or vec3(0, 0, 0)), - bone = data.prop and (data.prop.bone or 0) - } - }) then - return true - else - return false - end - end, - stop = function() - exports.lation_ui:cancelProgress() - end, + start = + function(data) + if exports.lation_ui:progressBar({ + label = data.label, + description = nil, + duration = debugMode and 1000 or data.time, + icon = data.icon, + useWhileDead = data.dead or false, + disable = { + combat = data.combat or true, + move = data.disableMovement or false, + car = data.disableMovement or false, + }, + anim = { + dict = data.dict, + clip = data.anim, + flag = data.flag + }, + prop = { + model = data.prop and data.prop.model, + pos = data.prop and (data.prop.pos or vec3(0, 0, 0)), + rot = data.prop and (data.prop.rot or vec3(0, 0, 0)), + bone = data.prop and (data.prop.bone or 0) + } + }) then + return true + else + return false + end + end, + stop = + function() + exports.lation_ui:cancelProgress() + end, }, red = { - start = function(data) - if exports.jim_bridge:redProgressBar({ - label = data.label, - time = debugMode and 1000 or data.time, - dict = data.dict, - anim = data.anim, - flag = data.flag or 32, - task = data.task, - disableMovement = data.disableMovement or false, - cancel = data.cancel or true, - }) then - return true - else - return false - end - end, - stop = function() - exports.jim_bridge:stopProgressBar() - end, + start = + function(data) + if exports.jim_bridge:redProgressBar({ + label = data.label, + time = debugMode and 1000 or data.time, + dict = data.dict, + anim = data.anim, + flag = data.flag or 32, + task = data.task, + disableMovement = data.disableMovement or false, + cancel = data.cancel or true, + }) then + return true + else + return false + end + end, + stop = + function() + exports.jim_bridge:stopProgressBar() + end, }, gta = { - start = function(data) - - if exports.jim_bridge:gtaProgressBar({ - label = data.label, - time = debugMode and 1000 or data.time, - dict = data.dict, - anim = data.anim, - flag = data.flag or 32, - task = data.task, - disableMovement = data.disableMovement or false, - cancel = data.cancel or true, - }) then - return true - else - return false - end - end, - stop = function() - exports.jim_bridge:stopProgressBar() - end, + start = + function(data) + if exports.jim_bridge:gtaProgressBar({ + label = data.label, + time = debugMode and 1000 or data.time, + dict = data.dict, + anim = data.anim, + flag = data.flag or 32, + task = data.task, + disableMovement = data.disableMovement or false, + cancel = data.cancel or true, + }) then + return true + else + return false + end + end, + stop = + function() + exports.jim_bridge:stopProgressBar() + end, }, } @@ -254,7 +270,11 @@ local progressFunc = { --- cancel = true, --- }) --- ``` + +local progresssBarActive = false function progressBar(data) + if progresssBarActive then return else progresssBarActive = true end + local ped = PlayerPedId() if data.shared then debugPrint("^6Bridge^7: ^6Sharing progressBar to player^7: ^6"..data.shared.pid.."^7") @@ -294,6 +314,7 @@ function progressBar(data) TriggerServerEvent(getScript()..":clearAuthToken") currentToken = triggerCallback(AuthEvent) end + progresssBarActive = false return result end @@ -302,6 +323,7 @@ end --- This function cancels the progress bar based on the configured progress bar system, handling any necessary cleanup. function stopProgressBar() progressFunc[Config.System.ProgressBar].stop() + progresssBarActive = false end -- System to handle sending/sharing progress bars between players -- From 2924c611f7fcb4c28a729d0766ab7094a29a61f6 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 30 Aug 2025 12:50:10 +0100 Subject: [PATCH 11/13] Optimize built in progressbar math Instead of rendering segments and trying to work out how much they should fill, add simple lines to segment it instead, this should make it run a bit smoother as there is way less going on --- ui_modules/progressbar.lua | 158 +++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 87 deletions(-) diff --git a/ui_modules/progressbar.lua b/ui_modules/progressbar.lua index 65743aa..80a2947 100644 --- a/ui_modules/progressbar.lua +++ b/ui_modules/progressbar.lua @@ -53,27 +53,11 @@ function redProgressBar(data) Wait(0) local elapsed = GetGameTimer() local percentage = ((elapsed - (endTime - wait)) / wait) * 100 + if percentage < 0 then percentage = 0 end + if percentage > 100 then percentage = 100 end - -- Convert to segmented progress (assuming 5 segments here) - local segments = 1 -- Number of segments in the bar - local segmentProgress = {} - local progressPerSegment = 100 / segments - - for i = 1, segments do - local segmentStart = (i - 1) * progressPerSegment - local segmentEnd = i * progressPerSegment - if percentage >= segmentEnd then - segmentProgress[i] = 100 - elseif percentage <= segmentStart then - segmentProgress[i] = 0 - else - segmentProgress[i] = ((percentage - segmentStart) / progressPerSegment) * 100 - end - end - - percentage = percentage >= 100 and 100 or percentage -- Draw your segmented progress bar - ShowRedProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage)) + ShowRedProgressBar(percentage, data.label, ("%.0f%%"):format(percentage)) -- Controls to disable during progress if data.disableMouse then @@ -151,40 +135,48 @@ function redProgressBar(data) return result end -function ShowRedProgressBar(currentProg, title, level) +function ShowRedProgressBar(percentage, title, level) local loc = vec2(0.40, 0.90) local size = vec2(0.3, 0.03) + local tickCount = 10 -- visually split into 10 segments (9 inner lines) + local barHeight = size.y / 3.4 + local barWidth = 0.21 -- tuned to your existing layout + local barLeft = (loc.x - size.x / 4) + 0.075 + local barCenter = barLeft + barWidth / 2 + local lineW = 0.001 + local lineH = barHeight + 0.00 - -- Draw background box - DrawSprite("generic_textures", "inkroller_1a", loc.x+0.1, loc.y-0.01, 0.25, 0.07, 180.0, 0, 0, 0, 200) + -- Background plate + DrawSprite("generic_textures", "inkroller_1a", loc.x + 0.1, loc.y - 0.01, 0.25, 0.07, 180.0, 0, 0, 0, 200) + -- Title (left) SetTextFontForCurrentCommand(6) SetTextScale(0.35, 0.35) - SetTextColor(255, 255, 255, 255) - SetTextDropshadow(1, 0, 0, 0, 200) - BgDisplayText(title, loc.x - size.x / 4 + 0.074, loc.y - 0.034) + SetTextColor(255, 255, 255, 255) + SetTextDropshadow(1, 0, 0, 0, 200) + BgDisplayText(title, loc.x - size.x / 4 + 0.074, loc.y - 0.034) + -- Percentage (right) SetTextFontForCurrentCommand(1) SetTextScale(0.35, 0.35) - SetTextColor(255, 255, 255, 255) - SetTextDropshadow(1, 0, 0, 0, 200) - BgDisplayText(level, loc.x - size.x / 4 + 0.246, loc.y - 0.030) + SetTextColor(255, 255, 255, 255) + SetTextDropshadow(1, 0, 0, 0, 200) + BgDisplayText(level, loc.x - size.x / 4 + 0.246, loc.y - 0.030) - local segmentWidth = (size.x + 0.05) / (#currentProg * 2) -- Divide the total width by 18 (9 segments * 2 gaps for each) - local gap = segmentWidth / #currentProg -- Smaller gap between segments + -- Track (bg) + DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255) - for i = 1, #currentProg do - local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap) - local fillPercentage = currentProg[i] - local progressBarWidth = segmentWidth * (fillPercentage / 100) + -- Fill + local fillWidth = barWidth * (percentage / 100.0) + if fillWidth > 0.0 then + local fillCenter = barLeft + (fillWidth / 2.0) + DrawRect(fillCenter, loc.y, fillWidth, barHeight, 255, 0, 0, 200) -- red fill + end - -- Semi-transparent background for each segment - DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255) - - -- Filling progress for each segment - if progressBarWidth > 0 then - DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 255, 0, 0, 200) -- Blue progress - end + -- Tick lines (9 inner lines for 10 segments) + for i = 1, (tickCount - 1) do + local x = barLeft + (barWidth * (i / tickCount)) + DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120) end end @@ -213,29 +205,14 @@ function gtaProgressBar(data) local elapsed = GetGameTimer() local percentage = ((elapsed - (endTime - wait)) / wait) * 100 - -- Convert to segmented progress (assuming 5 segments here) - local segments = 1 -- Number of segments in the bar - local segmentProgress = {} - local progressPerSegment = 100 / segments + if percentage < 0 then percentage = 0 end + if percentage > 100 then percentage = 100 end - for i = 1, segments do - local segmentStart = (i - 1) * progressPerSegment - local segmentEnd = i * progressPerSegment - if percentage >= segmentEnd then - segmentProgress[i] = 100 - elseif percentage <= segmentStart then - segmentProgress[i] = 0 - else - segmentProgress[i] = ((percentage - segmentStart) / progressPerSegment) * 100 - end - end - - percentage = percentage >= 100 and 100 or percentage -- Draw your segmented progress bar - ShowGTAProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage)) + ShowGTAProgressBar(percentage, data.label, ("%.0f%%"):format(percentage)) -- Controls to disable during progress - DisablePlayerFiring(ped, true) + DisablePlayerFiring(PlayerId(), true) DisableControlAction(0, 25, true) -- Disable aim DisableControlAction(0, 21, true) -- Disable sprint DisableControlAction(0, 30, true) -- Disable move left/right @@ -246,13 +223,15 @@ function gtaProgressBar(data) inProgress = false end end + result = inProgress end) - -- Wait for completion or cancel - while GetGameTimer() < endTime and inProgress do - Wait(100) - end + -- Wait for completion or cancel + while result == nil do Wait(10) end + inProgress = false + + print(tostring(result)) -- Cleanup animations/tasks if data.dict then stopAnim(data.dict, data.anim, ped) @@ -261,10 +240,6 @@ function gtaProgressBar(data) ClearPedTasks(ped) end - result = inProgress - inProgress = false - - while result == nil do Wait(10) end -- Cleanup FreezeEntityPosition(ped, false) @@ -276,14 +251,22 @@ function gtaProgressBar(data) return result end -function ShowGTAProgressBar(currentProg, title, level) +function ShowGTAProgressBar(percentage, title, level) local loc = vec2(0.37, 0.90) local size = vec2(0.3, 0.03) + local tickCount = 10 + local barHeight = size.y / 3.4 + local barWidth = 0.19 + local barLeft = (loc.x - size.x / 4) + 0.075 + local barCenter = barLeft + barWidth / 2 + local lineW = 0.001 + local lineH = barHeight + 0.00 - -- Draw background box - DrawSprite("timerbars", "all_black_bg", loc.x +0.028, loc.y-0.01, 0.15, 0.07, 0.0, 255, 255, 255, 255) - DrawSprite("timerbars", "all_black_bg", loc.x +0.170, loc.y-0.01, 0.15, 0.07, 180.0, 255, 255, 255, 255) + -- Background plates + DrawSprite("timerbars", "all_black_bg", loc.x + 0.028, loc.y - 0.01, 0.15, 0.07, 0.0, 255, 255, 255, 255) + DrawSprite("timerbars", "all_black_bg", loc.x + 0.170, loc.y - 0.01, 0.15, 0.07, 180.0, 255, 255, 255, 255) + -- Title (left) SetTextFont(0) SetTextProportional(1) SetTextScale(0.35, 0.35) @@ -293,34 +276,35 @@ function ShowGTAProgressBar(currentProg, title, level) SetTextOutline() SetTextEntry("STRING") AddTextComponentString(title) - DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034) -- Adjust text position + DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034) + -- Percentage (right) SetTextFont(0) SetTextProportional(1) SetTextScale(0.35, 0.25) SetTextColour(255, 255, 255, 255) SetTextEntry("STRING") AddTextComponentString(level) - DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030) -- Right-aligned additional text + DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030) - local segmentWidth = (size.x + 0.05) / (#currentProg * 2) -- Divide the total width by 18 (9 segments * 2 gaps for each) - local gap = segmentWidth / #currentProg -- Smaller gap between segments + -- Track (bg) + DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255) - for i = 1, #currentProg do - local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap) - local fillPercentage = currentProg[i] - local progressBarWidth = segmentWidth * (fillPercentage / 100) + -- Fill + local fillWidth = barWidth * (percentage / 100.0) + if fillWidth > 0.0 then + local fillCenter = barLeft + (fillWidth / 2.0) + DrawRect(fillCenter, loc.y, fillWidth, barHeight, 93, 182, 229, 255) -- GTA blue + end - -- Semi-transparent background for each segment - DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255) - - -- Filling progress for each segment - if progressBarWidth > 0 then - DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 93, 182, 229, 255) -- Blue progress - end + -- Tick lines + for i = 1, (tickCount - 1) do + local x = barLeft + (barWidth * (i / tickCount)) + DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120) end end + function stopProgressBar() inProgress = false end function isProgressBar() return inProgress end From 39691a2254965b416a6cdd2eb90a88f87607a085 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 30 Aug 2025 13:19:01 +0100 Subject: [PATCH 12/13] enhance built in ui target code Now properly tracks scripts that added a new target, if that is restarted it created ones properly If an overriding target is sent, use that and remove old one --- ui_modules/target.lua | 311 +++++++++++++++++++++++++----------------- 1 file changed, 183 insertions(+), 128 deletions(-) diff --git a/ui_modules/target.lua b/ui_modules/target.lua index c54bae1..76e7b88 100644 --- a/ui_modules/target.lua +++ b/ui_modules/target.lua @@ -1,5 +1,4 @@ -- 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. @@ -17,122 +16,196 @@ local Keys = { [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 +-- ===== Ownership + indexes ===== +-- TextTargets: key -> target data (coords/entity/models/options/etc.) +local TextTargets = {} +-- targetEntities kept for parity (not strictly required) +local targetEntities = {} + +-- registry: key -> owner, owner -> set(keys) +local TargetRegistry = { byKey = {}, byResource = {} } + +local function getOwnerResource() + return GetInvokingResource() or GetCurrentResourceName() or "unknown" +end + +local function registerTarget(owner, key) + -- move key across owners if replacing + local prev = TargetRegistry.byKey[key] + if prev and TargetRegistry.byResource[prev] then + TargetRegistry.byResource[prev][key] = nil end + TargetRegistry.byKey[key] = owner + TargetRegistry.byResource[owner] = TargetRegistry.byResource[owner] or {} + TargetRegistry.byResource[owner][key] = true +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] +local function removeTargetKey(key, reason) + local owner = TargetRegistry.byKey[key] + if TextTargets[key] then + TextTargets[key] = nil + -- print("^6Bridge^7:^5 Target^7: ^2Removed target '%s'%s", key, reason and (" ("..reason..")") or "") + end + if owner then + if TargetRegistry.byResource[owner] then + TargetRegistry.byResource[owner][key] = nil 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") - } + TargetRegistry.byKey[key] = nil 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 +AddEventHandler("onResourceStop", function(res) + local owned = TargetRegistry.byResource[res] + if not owned then return end + local cnt = 0 + for key in pairs(owned) do + removeTargetKey(key, "resource stopped: "..res) + cnt = cnt + 1 end + TargetRegistry.byResource[res] = nil + -- print("^6Bridge^7:^5 Target^7: ^2Cleared "..cnt.." target(s) from '"..res.."'") +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] +-- ===== Helpers ===== +local function vecKey(v) + -- stable rounded coord string for entity dedupe when name isn't provided + return ("%.3f,%.3f,%.3f"):format(v.x, v.y, v.z) end -function createModelTarget(models, opts, dist) - startTargetLoop() - if type(models) ~= "table" then - models = { models } - end - +local function bakeButtons(opts) 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 + tempText[#tempText + 1] = + (" ~b~[~w~%s~b~] ~w~%s"):format(Keys[opts[i].key] or ("K"..opts[i].key), opts[i].label or ("Option "..i)) end + return tempText +end - local keyStr = "" - for i, m in ipairs(models) do - keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") - end - local targetKey = "model_" .. keyStr +-- Update cached text blob +local function updateCachedText(target) + target.text = table.concat(target.buttontext, "\n") +end - TextTargets[targetKey] = { - models = models, - buttontext = tempText, - options = opts, - dist = dist, - coords = vec3(0, 0, 0), - text = table.concat(tempText, "\n") +-- ===== Public API: Create targets ===== +-- ENTITY: createEntityTarget(entity, opts, dist, nameOpt?) +function createEntityTarget(entity, opts, dist, name) + startTargetLoop() + + if not entity or entity == 0 then return end + targetEntities[#targetEntities + 1] = entity + + local owner = getOwnerResource() + local coords = GetEntityCoords(entity) + local key = tostring(name) or ("entity@" .. vecKey(coords)) + + -- Always overwrite on same key + local buttontext = bakeButtons(opts) + TextTargets[key] = { + _key = key, + _type = "entity", + _owner = owner, + entity = entity, + coords = vec3(coords.x, coords.y, coords.z), + buttontext = buttontext, + options = opts, + dist = dist, } - - return targetKey + updateCachedText(TextTargets[key]) + registerTarget(owner, key) + -- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords)) + return key end -function removeEntityTarget(entity) - TextTargets[entity] = nil +-- ZONE: createZoneTarget(data, opts, dist) +-- Expect data[1] = id/name, data[2] = vec3 coords (as in your original) +function createZoneTarget(data, opts, dist) + startTargetLoop() + + local owner = getOwnerResource() + local zname = tostring(data[1] or ("zone@"..vecKey(data[2] or vec3(0,0,0)))) + local coords = data[2] + + local buttontext = bakeButtons(opts) + TextTargets[zname] = { + _key = zname, + _type = "zone", + _owner = owner, + coords = coords, + buttontext = buttontext, + options = opts, + dist = dist, + } + updateCachedText(TextTargets[zname]) + registerTarget(owner, zname) + -- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ZONE target '"..zname.."' by '"..owner.."' @ "..formatCoord(coords)) + return zname end -function removeZoneTarget(target) - TextTargets[target] = nil +-- MODEL: createModelTarget(models, opts, dist, nameOpt?) +function createModelTarget(models, opts, dist, name) + startTargetLoop() + + local owner = getOwnerResource() + if type(models) ~= "table" then models = { models } end + + local key + if name then + key = tostring(name) + else + local parts = {} + for i, m in ipairs(models) do parts[i] = tostring(m) end + key = "model_" .. table.concat(parts, "_") + end + + local buttontext = bakeButtons(opts) + TextTargets[key] = { + _key = key, + _type = "model", + _owner = owner, + models = models, + buttontext = buttontext, + options = opts, + dist = dist, + coords = vec3(0, 0, 0), -- will be updated by the refresher + } + updateCachedText(TextTargets[key]) + registerTarget(owner, key) + -- print("^6Bridge^7:^5 Target^7: ^2Added/Updated MODEL target '"..key.."' by '"..owner.."' (models: "..table.concat(models, ",")..")") + return key end -function removeModelTarget(model) - TextTargets[model] = nil +-- ===== Public API: Remove targets ===== +-- Entity removal accepts entity handle or key string. +function removeEntityTarget(entityOrKey) + local key = nil + if type(entityOrKey) == "string" then + key = entityOrKey + elseif type(entityOrKey) == "number" then + for k, t in pairs(TextTargets) do + if t._type == "entity" and t.entity == entityOrKey then key = k break end + end + if not key then + -- Fallback: try coord-key match + local c = GetEntityCoords(entityOrKey) + local guess = "entity@"..vecKey(c) + if TextTargets[guess] then key = guess end + end + end + if key then removeTargetKey(key, "removeEntityTarget") end +end + +function removeZoneTarget(key) + if not key then return end + removeTargetKey(key, "removeZoneTarget") +end + +-- For models, pass the returned key from createModelTarget (recommended). +function removeModelTarget(key) + if not key then return end + removeTargetKey(key, "removeModelTarget") end exports("createEntityTarget", createEntityTarget) @@ -143,22 +216,20 @@ 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 + + -- lazy include (unchanged from your original) + Config = { System = {} } local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua'))) fileLoader() - -- Model Entity Refresher + + -- Model Entity Refresher (kept) CreateThread(function() while true do local pedCoords = GetEntityCoords(PlayerPedId()) @@ -178,7 +249,7 @@ function startTargetLoop() end end) - -- Main Target Loop + -- Main Target Loop (unchanged logic, just uses new TextTargets entries) CreateThread(function() while true do local ped = PlayerPedId() @@ -215,10 +286,10 @@ function startTargetLoop() 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 + (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 + if opt.action then opt.action(targetEntity) end end end end @@ -228,8 +299,8 @@ function startTargetLoop() 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 + (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 @@ -238,12 +309,11 @@ function startTargetLoop() ::continue:: end - Wait(1) -- Throttled + Wait(1) end end) end - function DrawText3D(coord, text, highlight) SetTextScale(0.30, 0.30) SetTextFont(0) @@ -253,37 +323,28 @@ function DrawText3D(coord, text, highlight) 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) + local textMaxLength = 99 + local txt = totalLength > textMaxLength and text:sub(1, textMaxLength) or text + AddTextComponentString(highlight and txt:gsub("%~w~", "~y~") or txt) SetDrawOrigin(coord.x, coord.y, coord.z, 0) DrawText(0.0, 0.0) - local count, length = GetLineCountAndMaxLength(text) + local count, length = GetLineCountAndMaxLength(txt) 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 + 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 + lineCount = lineCount + 1 local lineLength = string.len(line) if lineLength > maxLength then maxLength = lineLength @@ -293,7 +354,6 @@ function GetLineCountAndMaxLength(text) return lineCount, maxLength end - function RotationToDirection(rot) local adjust = math.pi / 180 return vec3( @@ -311,8 +371,3 @@ function normalizeVector(vec) 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 From 8efec21408bb88fab22421b0acbdde4715720892 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Mon, 1 Sep 2025 10:25:11 +0100 Subject: [PATCH 13/13] Create `release.yml` Automate release zips and make them generate the folder name correctly This helps users download a correctly named script so they dont end up with a script with a version number in the title --- .gitattributes | 12 +++++ .github/workflows/release.yml | 88 +++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/release.yml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..430242a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# VCS / CI noise +.gitattributes export-ignore +.gitignore export-ignore +.github/** export-ignore +.gitmodules export-ignore + +# Dev tooling/config +.vscode/** export-ignore +*.code-workspace export-ignore +.editorconfig export-ignore +.eslintrc* export-ignore +.prettier* export-ignore \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b3756f4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Package & Release + +on: + push: + tags: + - "v*" + - "*.*.*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Derive vars + id: vars + shell: bash + run: | + echo "name=${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT # e.g. jim_bridge + echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT # strip leading v + + - name: Build zip with folder prefix + shell: bash + run: | + NAME='${{ steps.vars.outputs.name }}' + VER='${{ steps.vars.outputs.version }}' + git archive --format=zip --prefix="${NAME}/" -o "${NAME}-${VER}.zip" "$GITHUB_REF_NAME" + ls -lh "${NAME}-${VER}.zip" + + # --- Patch notes generation (commit titles + links) ------------------- + - name: Find previous tag (SemVer aware) + id: prev + shell: bash + run: | + CUR="${GITHUB_REF_NAME}" + # Sort tags by version desc, drop the current, take the next newest + PREV=$(git tag --sort=-v:refname | grep -v -x "$CUR" | head -n 1 || true) + echo "prev=$PREV" >> $GITHUB_OUTPUT + echo "Previous tag: ${PREV:-}" + + - name: Generate RELEASE_NOTES.md from commits + shell: bash + run: | + REPO="${{ github.repository }}" + CUR="${GITHUB_REF_NAME}" + PREV='${{ steps.prev.outputs.prev }}' + + if [ -n "$PREV" ]; then + RANGE="$PREV..$CUR" + HEADER="## Changes in $CUR" + else + # First release: include all commits + ROOT=$(git rev-list --max-parents=0 HEAD | tail -n 1) + RANGE="$ROOT..$CUR" + HEADER="## Changes" + fi + + { + echo "$HEADER" + echo + # Oldest → newest, subject only, skip merge commits + # Escape '[' and ']' so markdown doesn't break on rare titles + git log --reverse --no-merges --pretty=format:'- ['%h'] - %s' "$RANGE" \ + | perl -pe 's/([\[\]])/\\$1/g' + } > RELEASE_NOTES.md + + echo "--- RELEASE_NOTES.md ---" + cat RELEASE_NOTES.md + echo "------------------------" + + - name: Create / Update GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} + files: | + *.zip + body_path: RELEASE_NOTES.md # attach our generated notes + # If you wanted GitHub's auto notes instead, set: + # generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}