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