This commit is contained in:
oosayeroo
2025-09-03 11:18:57 +01:00
22 changed files with 1395 additions and 1040 deletions

12
.gitattributes vendored Normal file
View File

@@ -0,0 +1,12 @@
# VCS / CI noise
.gitattributes export-ignore
.gitignore export-ignore
.github/** export-ignore
.gitmodules export-ignore
# Dev tooling/config
.vscode/** export-ignore
*.code-workspace export-ignore
.editorconfig export-ignore
.eslintrc* export-ignore
.prettier* export-ignore

88
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Package & Release
on:
push:
tags:
- "v*"
- "*.*.*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Derive vars
id: vars
shell: bash
run: |
echo "name=${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT # e.g. jim_bridge
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT # strip leading v
- name: Build zip with folder prefix
shell: bash
run: |
NAME='${{ steps.vars.outputs.name }}'
VER='${{ steps.vars.outputs.version }}'
git archive --format=zip --prefix="${NAME}/" -o "${NAME}-${VER}.zip" "$GITHUB_REF_NAME"
ls -lh "${NAME}-${VER}.zip"
# --- Patch notes generation (commit titles + links) -------------------
- name: Find previous tag (SemVer aware)
id: prev
shell: bash
run: |
CUR="${GITHUB_REF_NAME}"
# Sort tags by version desc, drop the current, take the next newest
PREV=$(git tag --sort=-v:refname | grep -v -x "$CUR" | head -n 1 || true)
echo "prev=$PREV" >> $GITHUB_OUTPUT
echo "Previous tag: ${PREV:-<none>}"
- name: Generate RELEASE_NOTES.md from commits
shell: bash
run: |
REPO="${{ github.repository }}"
CUR="${GITHUB_REF_NAME}"
PREV='${{ steps.prev.outputs.prev }}'
if [ -n "$PREV" ]; then
RANGE="$PREV..$CUR"
HEADER="## Changes in $CUR"
else
# First release: include all commits
ROOT=$(git rev-list --max-parents=0 HEAD | tail -n 1)
RANGE="$ROOT..$CUR"
HEADER="## Changes"
fi
{
echo "$HEADER"
echo
# Oldest → newest, subject only, skip merge commits
# Escape '[' and ']' so markdown doesn't break on rare titles
git log --reverse --no-merges --pretty=format:'- ['%h'] - %s' "$RANGE" \
| perl -pe 's/([\[\]])/\\$1/g'
} > RELEASE_NOTES.md
echo "--- RELEASE_NOTES.md ---"
cat RELEASE_NOTES.md
echo "------------------------"
- name: Create / Update GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
files: |
*.zip
body_path: RELEASE_NOTES.md # attach our generated notes
# If you wanted GitHub's auto notes instead, set:
# generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,12 +1,22 @@
local function parseVersion(version)
local function readBoolMeta(key, default)
local val = GetResourceMetadata(GetCurrentResourceName(), key, 0)
if not val then return default end
val = tostring(val):lower()
return (val == 'true' or val == '1' or val == 'yes' or val == 'on')
end
local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false)
if not SUPPRESS_UPDATES then
local function parseVersion(version)
local parts = {}
for num in version:gmatch("%d+") do
table.insert(parts, tonumber(num))
end
return parts
end
end
local function compareVersions(current, newest)
local function compareVersions(current, newest)
local currentParts = parseVersion(current)
local newestParts = parseVersion(newest)
for i = 1, math.max(#currentParts, #newestParts) do
@@ -16,9 +26,9 @@ local function compareVersions(current, newest)
elseif c > n then return 1 end
end
return 0 -- equal
end
end
function CheckBridgeVersion()
function CheckBridgeVersion()
if IsDuplicityVersion() then
CreateThread(function()
Wait(4000)
@@ -61,7 +71,8 @@ function CheckBridgeVersion()
end)
end)
end
end
CheckBridgeVersion()
end
CheckBridgeVersion()

View File

@@ -31,17 +31,18 @@ local Exports = {
-- REDM
RSGExport = "rsg-core",
RSGInv = "rsg-inventory"
RSGInv = "rsg-inventory",
VorpExport = "vorp_core",
VorpInv = "vorp_inventory",
VorpMenu = "vorp_menu",
}
-- Prevent reloading if cache is already initialized
local cache = {
Items = {},
Vehicles = {},
Jobs = {},
Gangs = {},
}
local cache = { Items = {}, Vehicles = {}, Jobs = {}, Gangs = {}, }
local cacheReady = false
-- Timer info, for debugging more then anything
local timers = {}
local function startTimer(label)
timers[label] = GetGameTimer()
@@ -52,26 +53,57 @@ local function endTimer(label)
timers[label] = "("..(timers[label] / 1000).."s)"
end
startTimer("Cache") startTimer("Items") startTimer("Vehicles") startTimer("Jobs") startTimer("InvWeight") startTimer("InvSlots")
-- Helper function to check if resource exists in server (instead of if it is already started)
-- Helper functions --
local function checkExists(resourceName)
return GetResourceState(resourceName):find("start") or GetResourceState(resourceName):find("stopped")
local state = GetResourceState(resourceName)
return state and (state:find("start") or state:find("stopped"))
end
-- Ensure oxmysql resource is loaded
if checkExists(Exports.OXCoreExport) or checkExists(Exports.ESXExport) then
local function waitStarted(resourceName)
while GetResourceState(resourceName) ~= "started" do Wait(100) end
end
local function waitStartedOrStopped(resourceName)
local state = GetResourceState(resourceName)
while state ~= "started" and state ~= "stopped" do
Wait(100)
state = GetResourceState(resourceName)
end
end
local function dupLowercaseWeapons(items)
for k, v in pairs(items or {}) do
if type(k) == "string" then
if k:find("WEAPON") then
items[k:lower()] = v
end
else
print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?")
print("^1Possible Issue found^7:")
print(json.encode(items[k], { indent = true }))
end
end
end
-- Forceload libs --
-- Ensure oxmysql resource is loaded for jim_bridge internally
-- For some core's it needs to get data from the database
if checkExists(Exports.OXCoreExport) or checkExists(Exports.ESXExport) or checkExists(Exports.VorpExport) then
local fileLoader = assert(load(LoadResourceFile("oxmysql", ('lib/MySQL.lua')), ('@@oxmysql/lib/MySQL.lua')))
fileLoader()
end
if checkExists(Exports.OXCoreExport) then
-- Detected OX_Core in server, wait for it to be started if needed
while GetResourceState(Exports.OXCoreExport) ~= "started" do Wait(100) end
waitStartedOrStopped(Exports.OXCoreExport)
local fileLoader = assert(load(LoadResourceFile(Exports.OXCoreExport, ('lib/init.lua')), ('@@'..Exports.OXCoreExport..'/lib/init.lua')))
fileLoader()
end
if checkExists(Exports.ESXExport) then
-- Detected ESX in server, wait for it to be started if needed
while GetResourceState(Exports.ESXExport) ~= "started" do Wait(100) end
waitStarted(Exports.ESXExport)
local fileLoader = assert(load(LoadResourceFile(Exports.ESXExport, ('/imports.lua')), ('@@'..Exports.ESXExport..'/imports.lua')))
fileLoader()
end
@@ -89,62 +121,30 @@ end
---------------------
---- Load Items -----
---------------------
-- Items initialization based on detected inventory system
if checkExists(Exports.OXInv) then
-- Wait for OX Inventory to start if it's not already started
while GetResourceState(Exports.OXInv) ~= "started" do Wait(100) end
itemResource = Exports.OXInv
local itemFunc = {
{ script = Exports.OXInv,
cacheItem = function()
local success, result = pcall(function()
return exports[Exports.OXInv]:Items()
end)
if success and result then
cache.Items = result
end
-- Get Weapon info and duplicate them if they are uppercase
-- (duplicate incase anything checks for the uppercase version)
for k, v in pairs(cache.Items) do
if type(k) == "string" then
if k:find("WEAPON") then
cache.Items[k:lower()] = cache.Items[k]
end
else
print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?")
print("^1Possible Issue found^7:")
print(json.encode(cache.Items[k], {indent = true}))
end
end
elseif checkExists(Exports.TgiannInv) then
-- Wait for OX Inventory to start if it's not already started
while GetResourceState(Exports.TgiannInv) ~= "started" do Wait(100) end
itemResource = Exports.TgiannInv
end,
},
{ script = Exports.TgiannInv,
cacheItem = function()
local success, result = pcall(function()
return exports[Exports.TgiannInv]:Items()
end)
if success and result then
cache.Items = result
end
-- Get Weapon info and duplicate them if they are uppercase
-- (duplicate incase anything checks for the uppercase version)
for k, v in pairs(cache.Items) do
if type(k) == "string" then
if k:find("WEAPON") then
cache.Items[k:lower()] = cache.Items[k]
end
else
print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?")
print("^1Possible Issue found^7:")
print(json.encode(cache.Items[k], {indent = true}))
end
end
elseif checkExists(Exports.QBXExport) then
while GetResourceState(Exports.QBXExport) ~= "started" do Wait(100) end
itemResource = Exports.QBXExport
end,
},
{ script = Exports.QBXExport,
cacheItem = function()
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
-- If this is nil, they need to update to qbx_core 1.23.0+
if not cache.Items then
@@ -170,14 +170,15 @@ elseif checkExists(Exports.QBXExport) then
cache.Items = exports[Exports.TgiannInv]:Items()
end
end
elseif checkExists(Exports.QBExport) then
while GetResourceState(Exports.QBExport) ~= "started" do Wait(100) end
itemResource = Exports.QBExport
end,
},
{ script = Exports.QBExport,
cacheItem = function()
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
elseif checkExists(Exports.ESXExport) then
itemResource = Exports.ESXExport
end,
},
{ script = Exports.ESXExport,
cacheItem = function()
if GetResourceState(Exports.QSInv):find("start") then
cache.Items = exports[Exports.QSInv]:GetItemList()
else
@@ -187,28 +188,64 @@ elseif checkExists(Exports.ESXExport) then
Wait(1000)
end
end
elseif checkExists(Exports.RSGExport) then
while GetResourceState(Exports.RSGExport) ~= "started" do Wait(100) end
itemResource = Exports.RSGExport
end,
},
{ script = Exports.RSGExport,
cacheItem = function()
cache.Items = exports[Exports.RSGExport]:GetCoreObject().Shared.Items
end,
},
{ script = Exports.VorpInv,
cacheItem = function()
local dbItems = MySQL.query.await('SELECT * FROM `items`')
local tempItems = {}
for i = 1, #dbItems do
local v = dbItems[i]
tempItems[v.item] = {
name = v.item,
label = v.label,
weight = v.weight,
info = v.metadata,
usable = v.usable,
type = v.type,
description = v.desc,
}
end
cache.Items = tempItems
end,
},
}
for i = 1, #itemFunc do
local data = itemFunc[i]
if checkExists(data.script) then
waitStarted(data.script) -- Wait for detected script to start fully
data.cacheItem() -- run tablized function for core/inv
dupLowercaseWeapons(cache.Items) -- make usable weapon item names for jim_bridge
itemResource = data.script -- Grab script name to announce later
endTimer("Items") -- end timer
break -- break loop so it doesn't keep checking
end
end
endTimer("Items")
---------------------
--- Load Vehicles ---
---------------------
-- Vehicle loading depending on framework
if checkExists(Exports.QBXExport) then
vehResource = Exports.QBXExport
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
---
local vehicleFunc = {
elseif checkExists(Exports.QBExport)then
vehResource = Exports.QBExport
{ script = Exports.QBXExport,
cacheVehicle = function()
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
elseif checkExists(Exports.OXCoreExport) then
vehResource = Exports.OXCoreExport
end,
},
{ script = Exports.QBExport,
cacheVehicle = function()
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
end,
},
{ script = Exports.OXCoreExport,
cacheVehicle = function()
cache.Vehicles = {}
for k, v in pairs(Ox.GetVehicleData()) do
cache.Vehicles[k] = {
@@ -218,10 +255,11 @@ elseif checkExists(Exports.OXCoreExport) then
brand = v.make
}
end
elseif checkExists(Exports.ESXExport) then
vehResource = Exports.ESXExport
while not MySQL do Wait(1000) end
end,
},
{ script = Exports.ESXExport,
cacheVehicle = function()
while not MySQL do Wait(100) end
for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do
cache.Vehicles[v.model] = {
model = v.model,
@@ -230,29 +268,51 @@ elseif checkExists(Exports.ESXExport) then
name = v.name,
}
end
elseif checkExists(Exports.RSGExport) then
vehResource = Exports.RSGExport
end,
},
{ script = Exports.RSGExport,
cacheVehicle = function()
cache.Vehicles = exports[Exports.RSGExport]:GetCoreObject().Shared.Vehicles
end,
},
{ script = Exports.VorpExport,
cacheVehicle = function()
cache.Vehicles = { ["unkown"] = {} }
end,
},
}
for i = 1, #vehicleFunc do
local data = vehicleFunc[i]
if checkExists(data.script) then
waitStarted(data.script) -- Wait for detected script to start fully
data.cacheVehicle() -- run tablized function for core
vehResource = data.script -- Grab script name to announce later
endTimer("Vehicles") -- end timer
break -- break loop so it doesn't keep checking
end
end
endTimer("Vehicles")
---------------------
----- Load Jobs -----
---------------------
-- Jobs loading based on framework
if checkExists(Exports.QBXExport) then
jobResource = Exports.QBXExport
local jobFunc = {
{ script = Exports.QBXExport,
cacheJob = function()
cache.Jobs, cache.Gangs = exports[Exports.QBXExport]:GetJobs(), exports[Exports.QBXExport]:GetGangs()
elseif checkExists(Exports.QBExport) then
jobResource = Exports.QBExport
cache.Jobs, cache.Gangs = exports[Exports.QBExport]:GetCoreObject().Shared.Jobs, exports[Exports.QBExport]:GetCoreObject().Shared.Gangs
elseif checkExists(Exports.OXCoreExport) then
jobResource = Exports.OXCoreExport
while not MySQL do Wait(1000) end
end,
},
{ script = Exports.QBExport,
cacheJob = function()
Core = exports[Exports.QBExport]:GetCoreObject()
cache.Jobs, cache.Gangs = Core.Shared.Jobs, Core.Shared.Gangs
end,
},
{ script = Exports.OXCoreExport,
cacheJob = function()
while not MySQL do Wait(100) end
local tempJobs = MySQL.query.await('SELECT * FROM `ox_groups`')
local tempGrades = MySQL.query.await('SELECT * FROM `ox_group_grades`')
local gradeMap = {}
@@ -267,10 +327,11 @@ elseif checkExists(Exports.OXCoreExport) then
}
end
cache.Gangs = cache.Jobs
elseif checkExists(Exports.ESXExport) then
jobResource = Exports.ESXExport
ESX = exports[Exports.ESXExport]:getSharedObject()
end,
},
{ script = Exports.ESXExport,
cacheJob = function()
ESX = ESX or exports[Exports.ESXExport]:getSharedObject()
cache.Jobs = ESX.GetJobs()
while not next(cache.Jobs) do
Wait(100)
@@ -301,13 +362,33 @@ elseif checkExists(Exports.ESXExport) then
::continue::
end
cache.Gangs = cache.Jobs
end,
},
{ script = Exports.RSGExport,
cacheJob = function()
Core = exports[Exports.RSGExport]:GetCoreObject()
cache.Jobs, cache.Gangs = Core.Shared.Jobs, Core.Shared.Gangs
end,
},
{ script = Exports.VorpExport,
cacheJob = function()
cache.Jobs = { ["unkown"] = {} }
cache.Gangs = cache.Jobs
end,
},
}
elseif checkExists(Exports.RSGExport) then
jobResource = Exports.RSGExport
cache.Jobs, cache.Gangs = exports[Exports.RSGExport]:GetCoreObject().Shared.Jobs, exports[Exports.RSGExport]:GetCoreObject().Shared.Gangs
for i = 1, #jobFunc do
local data = jobFunc[i]
if checkExists(data.script) then
waitStarted(data.script) -- Wait for detected script to start fully
data.cacheJob() -- run tablized function for core
jobResource = data.script -- Grab script name to announce later
endTimer("Jobs") -- end timer
break -- break loop so it doesn't keep checking
end
end
endTimer("Jobs")
-- Fallback if nil or empty
if cache.Items == nil or not next(cache.Items) then
@@ -402,10 +483,8 @@ for script, data in pairs(invWeightTable) do
if checkExists(script) then
if script == Exports.QBInv and GetResourceState(Exports.JPRInv):find("start") then goto skip end
while GetResourceState(script) ~= "started" and GetResourceState(script) ~= "stopped" do
Wait(100)
print("Waiting for script start")
end
waitStartedOrStopped(script)
local attempts = data.fallback or { data }
local lookup, used, err

View File

@@ -7,7 +7,6 @@ rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aw
games { 'gta5', 'rdr3' }
lua54 'yes'
files {
'starter.lua',
'shared/*.lua',
@@ -25,3 +24,5 @@ client_scripts {
'clientFrameworkCache.lua',
'ui_modules/*.lua',
}
suppress_updates 'false' -- set to 'true' to disable update pings

View File

@@ -12,7 +12,7 @@ if isServer() then
local src = source
local token = keyGen()..keyGen()..keyGen()..keyGen() -- Use a secure random generator here
--debugPrint(GetInvokingResource())
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= Exports.QBExport and GetInvokingResource() ~= Exports.VorpExport then
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
return ""
end
@@ -45,8 +45,9 @@ if isServer() then
createCallback(getScript()..":callback:GetAuthEvent", function(source)
local src = source
--debugPrint(GetInvokingResource())
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= Exports.QBExport and GetInvokingResource() ~= Exports.VorpExport then
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital callback was called from an external resource^7")
return ""
end

View File

@@ -62,6 +62,9 @@ function onPlayerLoaded(func, onStart)
elseif isStarted(RSGExport) then
onPlayerFramework = RSGExport
RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', tempFunc)
elseif isStarted(VorpExport) then
onPlayerFramework = VorpExport
RegisterNetEvent("vorp_core:Client:OnPlayerSpawned", tempFunc)
end
if onPlayerFramework ~= "" then
@@ -174,6 +177,12 @@ function waitForLogin()
break
end
end
elseif isStarted(VorpExport) then
-- For other frameworks, use LocalPlayer.state.isLoggedIn.
while not LocalPlayer.state.IsInSession and (GetGameTimer() - startTime) < timeout do
Wait(100)
end
loggedIn = LocalPlayer.state.IsInSession
else
-- For other frameworks, use LocalPlayer.state.isLoggedIn.
while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do

View File

@@ -1,12 +1,22 @@
function parseVersion(version)
local function readBoolMeta(key, default)
local val = GetResourceMetadata("jim_bridge", key, 0)
if not val then return default end
val = tostring(val):lower()
return (val == 'true' or val == '1' or val == 'yes' or val == 'on')
end
local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false)
if not SUPPRESS_UPDATES then
function parseVersion(version)
local parts = {}
for num in version:gmatch("%d+") do
table.insert(parts, tonumber(num))
end
return parts
end
end
function compareVersions(current, newest)
function compareVersions(current, newest)
local currentParts = parseVersion(current)
local newestParts = parseVersion(newest)
for i = 1, math.max(#currentParts, #newestParts) do
@@ -16,20 +26,20 @@ function compareVersions(current, newest)
elseif c > n then return 1 end
end
return 0
end
end
function capitalize(str)
function capitalize(str)
return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end))
end
end
local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or ""
local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or ""
print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7")
print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7")
function CheckVersion()
function CheckVersion()
if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
CreateThread(function()
Wait(4000)
@@ -95,6 +105,7 @@ function CheckVersion()
end)
end)
end
end
end
CheckVersion()
CheckVersion()
end

View File

@@ -37,6 +37,10 @@ function createCallback(callbackName, funct)
debugPrint("^6Bridge^7: ^2Registering ^4"..QBExport.." ^3Callback^7:", callbackName)
Core = Core or exports[QBExport]:GetCoreObject()
Core.Functions.CreateCallback(callbackName, adaptedFunction)
elseif isStarted(VorpExport) then
debugPrint("^6Bridge^7: ^2Registering ^4"..VorpExport.." ^3Callback^7:", callbackName)
Core = Core or exports.vorp_core:GetCore()
Core.Callback.Register(callbackName, adaptedFunction)
elseif isStarted(ESXExport) then
debugPrint("^6Bridge^7: ^2Registering ^4"..ESXExport.." ^3Callback^7:", callbackName)
ESX.RegisterServerCallback(callbackName, adaptedFunction)
@@ -76,6 +80,13 @@ function triggerCallback(callbackName, ...)
end, ...)
result = Citizen.Await(p)
Wait(10)
elseif isStarted(VorpExport) then
local p = promise.new()
Core.Callback.TriggerAwait(callbackName, function(cbResult)
p:resolve(cbResult)
end, ...)
result = Citizen.Await(p)
Wait(10)
elseif isStarted(ESXExport) then
local p = promise.new()
ESX.TriggerServerCallback(callbackName, function(cbResult)

View File

@@ -10,44 +10,9 @@
• esx (using ESX.UI.Menu)
]]
--- Opens a menu using the configured menu system.
---
--- This function translates code and creates menus in several different menu scripts, depending on the configured menu system specified in `Config.System.Menu`.
---
---@param Menu table A table containing the menu options to display.
--- Each menu item can include:
--- - header (`string`): The text to display for the menu item.
--- - txt (`string`, optional): Additional text or description.
--- - icon (`string`, optional): Icon to display with the menu item.
--- - onSelect (`function`, optional): Function to execute when the menu item is selected.
--- - arrow (`boolean`, optional): Whether to display an arrow next to the item (for certain menus).
--- - params (`table`, optional): Additional parameters, such as events and arguments.
--- - isMenuHeader (`boolean`, optional): Marks the item as a header.
--- - disabled (`boolean`, optional): Disables the menu item if `true`.
---
---@param data table A table containing configuration data for the menu.
--- - header (`string`): The header/title of the menu.
--- - headertxt (`string`, optional): Additional header text.
--- - onBack (`function`, optional): Function to call when the "Return" option is selected.
--- - onExit (`function`, optional): Function to call when the menu is exited.
--- - onSelected (`function`, optional): Function to call when a menu item is selected (for certain menu systems).
--- - canClose (`boolean`, optional): Whether the menu can be closed by the user.
---
---@usage
--- ```lua
--- openMenu({
--- { header = "Option 1", txt = "Description 1", onSelect = function() print("Option 1 selected") end },
--- { header = "Option 2", txt = "Description 2", onSelect = function() print("Option 2 selected") end },
--- }, {
--- header = "Main Menu",
--- headertxt = "Select an option",
--- onBack = function() print("Return selected") end,
--- onExit = function() print("Menu closed") end,
--- canClose = true,
--- })
--- ```
function openMenu(Menu, data)
if Config.System.Menu == "ox" then
local contextFunc = {
["ox"] =
function(Menu, data)
local index = nil
if data.onBack and not data.onSelected then
table.insert(Menu, 1, {
@@ -110,8 +75,10 @@ function openMenu(Menu, data)
else
lib.showContext(menuID)
end
end,
elseif Config.System.Menu == "qb" then
["qb"] =
function(Menu, data)
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
@@ -151,8 +118,10 @@ function openMenu(Menu, data)
Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable
end
exports[QBMenuExport]:openMenu(Menu)
end,
elseif Config.System.Menu == "gta" then
["gta"] =
function(Menu, data)
WarMenu.CreateMenu(tostring(Menu), data.header, " ", {
titleColor = { 222, 255, 255 },
maxOptionCountOnScreen = 15,
@@ -206,8 +175,10 @@ function openMenu(Menu, data)
Wait(0)
end
end)
end,
elseif Config.System.Menu == "esx" then
["esx"] =
function(Menu, data)
for k in pairs(Menu) do
Menu[k].label = Menu[k].header
Menu[k].name = "button"..k
@@ -245,8 +216,10 @@ function openMenu(Menu, data)
function(data, menu)
menu.close()
end)
end,
elseif Config.System.Menu == "lation" then
["lation"] =
function(Menu, data)
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
@@ -290,12 +263,63 @@ function openMenu(Menu, data)
-- Show menu
exports.lation_ui:showMenu('menu')
end,
}
end
--- Opens a menu using the configured menu system.
---
--- This function translates code and creates menus in several different menu scripts, depending on the configured menu system specified in `Config.System.Menu`.
---
---@param Menu table A table containing the menu options to display.
--- Each menu item can include:
--- - header (`string`): The text to display for the menu item.
--- - txt (`string`, optional): Additional text or description.
--- - icon (`string`, optional): Icon to display with the menu item.
--- - onSelect (`function`, optional): Function to execute when the menu item is selected.
--- - arrow (`boolean`, optional): Whether to display an arrow next to the item (for certain menus).
--- - params (`table`, optional): Additional parameters, such as events and arguments.
--- - isMenuHeader (`boolean`, optional): Marks the item as a header.
--- - disabled (`boolean`, optional): Disables the menu item if `true`.
---
---@param data table A table containing configuration data for the menu.
--- - header (`string`): The header/title of the menu.
--- - headertxt (`string`, optional): Additional header text.
--- - onBack (`function`, optional): Function to call when the "Return" option is selected.
--- - onExit (`function`, optional): Function to call when the menu is exited.
--- - onSelected (`function`, optional): Function to call when a menu item is selected (for certain menu systems).
--- - canClose (`boolean`, optional): Whether the menu can be closed by the user.
---
---@usage
--- ```lua
--- openMenu({
--- { header = "Option 1", txt = "Description 1", onSelect = function() print("Option 1 selected") end },
--- { header = "Option 2", txt = "Description 2", onSelect = function() print("Option 2 selected") end },
--- }, {
--- header = "Main Menu",
--- headertxt = "Select an option",
--- onBack = function() print("Return selected") end,
--- onExit = function() print("Menu closed") end,
--- canClose = true,
--- })
--- ```
function openMenu(Menu, data)
contextFunc[Config.System.Menu](Menu, data)
end
-- Line break conversion table, basically, to automate the linebreak in functions `br`
-- This is declared and automatically sets it for what menu you are using (depends if one is uding MD formatting or HTML formatting)
local lineBreakConversion = {
["ox"] = "\n",
["gta"] = "\n",
["lation"] = "\n",
["qb"] = "<br>",
["esx"] = "<br>",
}
--- A line break constant used for menu header formatting.
br = (Config.System.Menu == "ox" or Config.System.Menu == "gta" or Config.System.Menu == "lation") and "\n" or "<br>"
br = lineBreakConversion[Config.System.Menu]
--- Checks if the current menu system is 'ox' or 'gta' for formatting purposes.
--- @return boolean boolean True if using ox or gta menus, otherwise false.

View File

@@ -22,6 +22,9 @@ OXInv, QBInv, PSInv, QSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv, JPRInv =
Exports.JPRInv or ""
RSGExport, RSGInv = Exports.RSGExport or "", Exports.RSGInv or ""
VorpExport, VorpInv = Exports.VorpExport or "", Exports.VorpInv or ""
QBMenuExport = Exports.QBMenuExport or ""
QBTargetExport, OXTargetExport = Exports.QBTargetExport or "", Exports.OXTargetExport or ""
@@ -29,6 +32,8 @@ if isStarted(QBXExport) or isStarted(QBExport) then
Core = Core or exports[QBExport]:GetCoreObject()
elseif isStarted(RSGExport) then
Core = Core or exports[RSGExport]:GetCoreObject()
elseif isStarted(VorpExport) then
Core = Core or exports[VorpExport]:GetCore()
end

View File

@@ -115,12 +115,12 @@ function craftingMenu(data)
local metaTable = {}
-- Build ingredient details.
for l, b in pairs(Recipes[i][tostring(k)]) do
local label = Items[l] and Items[l].label or "error - "..l
local label = getItemLabel(l)
local hasItem = checkStashItem(data.stashName, { [l] = b })
local missingMark = not hasItem and "" or " "
settext = settext..(settext ~= "" and br or "").."[ x"..b.." ] - "..label..missingMark
metaTable[Items[l] and Items[l].label or "error - "..l] = b
metaTable[label] = b
itemTable[l] = b
end
@@ -128,7 +128,7 @@ function craftingMenu(data)
Wait(10)
end
disable = not checkStashItem(data.stashName, itemTable)
setheader = ((metadata and metadata.label) or (Items[tostring(k)] and Items[tostring(k)].label) or "error - "..tostring(k))
setheader = ((metadata and metadata.label) or getItemLabel(k))
..(Recipes[i]["amount"] > 1 and " x"..Recipes[i]["amount"] or "")
local statusEmoji = disable and " " or not canCarryTable[k] and " 📦" or " ✔️"
@@ -359,7 +359,7 @@ function makeItem(data)
return
end
if crafting and progressBar({
label = "Using "..b.." "..Items[l].label,
label = "Using "..b.." "..getItemLabel(l),
time = 1000,
cancel = true,
dict = 'pickup_object',
@@ -398,7 +398,7 @@ function makeItem(data)
PlaySoundFromEntity(s.soundId, s.audioName, PlayerPedId(), s.audioRef, true, 0)
end
if crafting and progressBar({
label = bartext..((metadata and metadata.label) or Items[data.item].label).." x"..craftAmount,
label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount,
time = totalBartime,
cancel = true,
dict = animDict,
@@ -437,7 +437,7 @@ function makeItem(data)
-- Run the original loop for multiple progress bars
for i = 1, craftAmount do
if crafting and progressBar({
label = bartext..((metadata and metadata.label) or Items[data.item].label),
label = bartext..((metadata and metadata.label) or getItemLabel(data.item)),
time = bartime,
cancel = true,
dict = animDict,

View File

@@ -180,6 +180,7 @@ function GetPrintTime()
local hour, min, sec = os.date('%H'), os.date('%M'), os.date('%S')
return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")"
else
if gameName == "rdr3" then return "" end
local _, _, _, hour, min, sec = GetLocalTime()
return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")"
end

View File

@@ -201,7 +201,7 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
for k, v in pairs(items) do
exports[QSInv]:RemoveItemIntoStash(stashName, k, v)
exports[QSInv]:RemoveItemIntoStash(stashName[1], k, v)
debugPrint("^6Bridge^7: ^2Removing ^3"..QSInv.." ^2Stash item^7:", k, v)
end
end,
@@ -291,7 +291,7 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
for k, v in pairs(items) do
exports[CoreInv]:removeItemExact(stashName, k, v)
exports[CoreInv]:removeItemExact(stashName[1], k, v)
debugPrint("^6Bridge^7: ^2Removing ^3"..CoreInv.." ^2Stash item^7:", k, v)
end
end,
@@ -381,7 +381,7 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
for k, v in pairs(items) do
exports[OrigenInv]:RemoveFromStash(stashName, k, v)
exports[OrigenInv]:RemoveFromStash(stashName[1], k, v)
debugPrint("^6Bridge^7: ^2Removing ^3"..OrigenInv.." ^2Stash item^7:", k, v)
end
end,
@@ -494,7 +494,7 @@ local InvFunc = {
end
end
end
exports[CodeMInv]:UpdateStash(stashName, stashItems)
exports[CodeMInv]:UpdateStash(stashName[1], stashItems)
end,
registerStash =
function(name, label, slots, weight, owner, coords)
@@ -588,8 +588,8 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
for k, v in pairs(items) do
local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName, k)
exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName, k, v, itemData.slot, nil)
local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName[1], k)
exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName[1], k, v, itemData.slot, nil)
debugPrint("^6Bridge^7: ^2Removing ^3"..TgiannInv.." ^2Stash item^7:", k, v)
end
end,
@@ -743,7 +743,7 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
if not stashItems or not next(stashItems) then
stashItems = getStash(stashName)
stashItems = getStash(stashName[1])
end
for k, v in pairs(items) do
for l in pairs(stashItems) do
@@ -757,9 +757,9 @@ local InvFunc = {
end
end
end
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3JPR^2 stash '^6"..stashName.."^7'")
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3JPR^2 stash '^6"..stashName[1].."^7'")
MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
['stash'] = stashName,
['stash'] = stashName[1],
['items'] = json.encode(stashItems)
})
end,
@@ -924,12 +924,12 @@ local InvFunc = {
function(stashItems, stashName, items)
if checkExportExists(QBInv, "RemoveItem") then
for k, v in pairs(items) do
exports[QBInv]:RemoveItem(stashName, k, v, false, 'crafting')
exports[QBInv]:RemoveItem(stashName[1], k, v, false, 'crafting')
debugPrint("^6Bridge^7: ^2Removing ^3"..QBInv.." ^2Stash item^7:", k, v)
end
else
if not stashItems or not next(stashItems) then
stashItems = getStash(stashName)
stashItems = getStash(stashName[1])
end
for k, v in pairs(items) do
for l in pairs(stashItems) do
@@ -943,9 +943,9 @@ local InvFunc = {
end
end
end
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash '^6"..stashName.."^7'")
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash '^6"..stashName[1].."^7'")
MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
['stash'] = stashName,
['stash'] = stashName[1],
['items'] = json.encode(stashItems)
})
end
@@ -1105,7 +1105,7 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
if not stashItems or not next(stashItems) then
stashItems = getStash(stashName)
stashItems = getStash(stashName[1])
end
for k, v in pairs(items) do
for l in pairs(stashItems) do
@@ -1119,9 +1119,9 @@ local InvFunc = {
end
end
end
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3PS^2 stash ^7'^6"..stashName.."^7'")
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3PS^2 stash ^7'^6"..stashName[1].."^7'")
MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
['stash'] = stashName,
['stash'] = stashName[1],
['items'] = json.encode(stashItems)
})
end,
@@ -1244,7 +1244,7 @@ local InvFunc = {
stashRemoveItem =
function(stashItems, stashName, items)
for k, v in pairs(items) do
exports[RSGInv]:RemoveItem(stashName, k, v, false, 'crafting')
exports[RSGInv]:RemoveItem(stashName[1], k, v, false, 'crafting')
debugPrint("^6Bridge^7: ^2Removing ^3"..RSGInv.." ^2Stash item^7:", k, v)
end
end,
@@ -1585,7 +1585,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount,
end
else
debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..Items[item].label.."("..item..") x"..(amount or 1))
debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..getItemLabel(item).."("..item..") x"..(amount or 1))
end
end
else
@@ -1610,7 +1610,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount,
ESX.GetPlayerFromId(src).addInventoryItem(item, amountToAdd)
end
else
debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..Items[item].label.."("..item..") x"..(amount or 1))
debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..getItemLabel(item).."("..item..") x"..(amount or 1))
end
end
end)
@@ -1951,6 +1951,7 @@ end
--- if doesItemExist(item) then print("item exists") end
--- ```
function doesItemExist(item)
local item = type(item) == "string" and item or tostring(item)
if not item or item == "" then
return false
end
@@ -1965,16 +1966,18 @@ end
function getItemLabel(item)
local item = type(item) == "string" and item or tostring(item)
if not item or item == "" then
return ""
end
if not Items or not next(Items) then
return item.." (Missing)"
return item.." (Missing item error)"
end
if Items[item] ~= nil then
return Items[item].label
end
return item.." (Missing)"
return item.." (Missing item error)"
end
@@ -2280,7 +2283,7 @@ function stashRemoveItem(stashItems, stashName, items)
for i = 1, #InvFunc do
local inv = InvFunc[i]
if isStarted(inv.invName) then
inv.stashRemoveItem(stashItems, stashName[1], items)
inv.stashRemoveItem(stashItems, stashName, items)
return
end
end
@@ -2476,7 +2479,7 @@ function sellMenu(data)
Menu[#Menu + 1] = {
isMenuHeader = not hasTable[k].hasItem,
icon = invImg(k),
header = Items[k].label..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""),
header = getItemLabel(k)..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""),
txt = (Loc and Loc[Config.Lan]) and Loc[Config.Lan].info["sell_all"]..v.." "..Loc[Config.Lan].info["sell_each"]
or "Sell ALL at $"..v.." each",
onSelect = function()
@@ -2534,7 +2537,7 @@ end
--- ```
function sellAnim(data, token)
if not hasItem(data.item, 1) then
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error")
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..getItemLabel(data.item), "error")
return
end
@@ -2596,7 +2599,7 @@ RegisterNetEvent(getScript()..":Sellitems", function(data, token)
print((hasTable[data.item].count * data.price), data.price)
fundPlayer((hasTable[data.item].count * data.price), "cash", src)
else
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error", src)
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..getItemLabel(data.item), "error", src)
end
end)

View File

@@ -65,7 +65,6 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
model = data.model
loadModel(data.model)
ped = CreatePed(0, model, coords.x, coords.y, coords.z - 1.03, coords.w, synced and synced or false, false)
SetEntityAlpha(ped, 0, false)
-- Inheritance
SetPedHeadBlendData(ped, data.custom.faceFather, data.custom.faceMother, data.custom.raceShape, data.custom.skinFather, data.custom.skinMother, data.custom.raceSkin, data.custom.faceMix or 0, data.custom.skinMix or 0, data.custom.raceMix or 0, false)
@@ -145,7 +144,8 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
end
unloadModel(model)
Peds[keyGen()..keyGen()] = ped
if fade ~= false then
if fade ~= false and gameName ~= "rdr3" then
SetEntityAlpha(ped, 0, false)
CreateThread(function()
fadeInEnt(ped)
end)

View File

@@ -24,14 +24,14 @@ local distProps = {}
function makeProp(data, freeze, synced, fade)
loadModel(data.prop)
local prop = CreateObject(data.prop, data.coords.x, data.coords.y, data.coords.z-1.03, synced and synced or false, synced and synced or false, false)
SetEntityAlpha(veh, 0, false)
SetEntityHeading(prop, (data.coords.w or 0) + 180.0)
FreezeEntityPosition(prop, freeze or false)
debugPrint("^6Bridge^7: ^1Prop ^2Created^7: '^6"..prop.."^7' | ^2Hash^7: ^7'^6"..data.prop.."^7' | ^2Coord^7: "..formatCoord(data.coords))
SetModelAsNoLongerNeeded(data.prop)
Props[keyGen()..keyGen()] = prop
if fade ~= false then
if fade ~= false and gameName ~= "rdr3" then
SetEntityAlpha(prop, 0, false)
CreateThread(function()
fadeInEnt(prop)
end)

View File

@@ -30,7 +30,7 @@ function makeVeh(model, coords, synced, fade)
debugPrint("^6Bridge^7: ^1Veh ^2Created^7: '^6"..veh.."^7' | ^2Hash^7: ^7'^6"..model.."^7' | ^2Coord^7: "..formatCoord(coords))
unloadModel(model)
Vehicles[#Vehicles + 1] = veh
if fade ~= false then
if fade ~= false and gameName ~= "rdr3" then
SetEntityAlpha(veh, 0, false)
CreateThread(function()
fadeInEnt(veh)

View File

@@ -4,7 +4,8 @@ local storedPID = nil
local progressFunc = {
ox = {
start = function(data)
start =
function(data)
local options = {
duration = debugMode and 1000 or data.time,
label = data.label,
@@ -51,17 +52,20 @@ local progressFunc = {
if exports[OXLibExport]:progressBar(options) then
return true
else
return false
print("^1progressBar was not successful")
end
end
end,
stop = function()
stop =
function()
exports[OXLibExport]:cancelProgress()
end,
},
qb = {
start = function(data)
start =
function(data)
local p = promise.new()
Core.Functions.Progressbar("progbar",
data.label,
debugMode and 1000 or data.time,
@@ -80,19 +84,20 @@ local progressFunc = {
task = data.task
},
{}, {},
function()
return true
end, function()
return false
end, data.icon)
function() p:resolve(true) end,
function() p:resolve(false) end,
data.icon)
return Citizen.Await(p)
end,
stop = function()
stop =
function()
TriggerEvent("progressbar:client:cancel")
end,
},
qs = {
start = function(data)
start =
function(data)
if exports['qs-interface']:ProgressBar({
duration = debugMode and 1000 or data.time,
label = data.label,
@@ -112,14 +117,17 @@ local progressFunc = {
return false
end
end,
stop = function()
stop =
function()
--??
TriggerEvent("progressbar:client:cancel")
end,
},
esx = {
start = function(data)
start =
function(data)
local p = promise.new()
ESX.Progressbar(data.label, debugMode and 1000 or data.time, {
FreezePlayer = true,
animation = {
@@ -128,20 +136,24 @@ local progressFunc = {
scenario = data.task,
},
onFinish = function()
return true
p:resolve(true)
end,
onCancel = function()
p:resolve(false)
return false
end
})
return Citizen.Await(p)
end,
stop = function()
stop =
function()
ESX.CancelProgressbar()
end,
},
lation = {
start = function(data)
start =
function(data)
if exports.lation_ui:progressBar({
label = data.label,
description = nil,
@@ -170,13 +182,15 @@ local progressFunc = {
return false
end
end,
stop = function()
stop =
function()
exports.lation_ui:cancelProgress()
end,
},
red = {
start = function(data)
start =
function(data)
if exports.jim_bridge:redProgressBar({
label = data.label,
time = debugMode and 1000 or data.time,
@@ -192,14 +206,15 @@ local progressFunc = {
return false
end
end,
stop = function()
stop =
function()
exports.jim_bridge:stopProgressBar()
end,
},
gta = {
start = function(data)
start =
function(data)
if exports.jim_bridge:gtaProgressBar({
label = data.label,
time = debugMode and 1000 or data.time,
@@ -215,7 +230,8 @@ local progressFunc = {
return false
end
end,
stop = function()
stop =
function()
exports.jim_bridge:stopProgressBar()
end,
},
@@ -254,7 +270,11 @@ local progressFunc = {
--- cancel = true,
--- })
--- ```
local progresssBarActive = false
function progressBar(data)
if progresssBarActive then return else progresssBarActive = true end
local ped = PlayerPedId()
if data.shared then
debugPrint("^6Bridge^7: ^6Sharing progressBar to player^7: ^6"..data.shared.pid.."^7")
@@ -273,6 +293,7 @@ function progressBar(data)
while result == nil do
Wait(10)
end
debugPrint("^5Debug^7: ^2ProgressBar result^7: ^3"..tostring(result).."^7")
-- Cleanup
FreezeEntityPosition(ped, false)
@@ -293,6 +314,7 @@ function progressBar(data)
TriggerServerEvent(getScript()..":clearAuthToken")
currentToken = triggerCallback(AuthEvent)
end
progresssBarActive = false
return result
end
@@ -301,6 +323,7 @@ end
--- This function cancels the progress bar based on the configured progress bar system, handling any necessary cleanup.
function stopProgressBar()
progressFunc[Config.System.ProgressBar].stop()
progresssBarActive = false
end
-- System to handle sending/sharing progress bars between players --

View File

@@ -21,24 +21,56 @@
--- })
--- ```
function registerCommand(command, options)
-- Name the options
local optionTable = {
helpInfo = options[1] or nil, -- help info that pops up in chat box
subText = options[2] or nil, -- sub help info for each arg
argsRequired = options[3] or nil, -- command only works if you enter args
funct = options[4] or nil, -- function that will run when command is triggered
restriction = options[5] or nil -- admin group that can use the group or `nil`
}
local commandResource = ""
if isStarted(OXLibExport) then
commandResource = OXLibExport
lib.addCommand(command, { help = options[1], restricted = options[5] and "group."..options[5] or nil }, options[3])
lib.addCommand(command,
{
help = optionTable.helpInfo,
params = optionTable.subText,
restricted = optionTable.restriction and "group."..optionTable.restriction or nil
},
optionTable.funct)
elseif isStarted(QBExport) and not isStarted(QBXExport) then
commandResource = QBExport
Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil)
Core.Commands.Add(command,
optionTable.helpInfo,
optionTable.subText,
optionTable.argsRequired,
optionTable.funt,
optionTable.restriction or nil
)
elseif isStarted(RSGExport) then
commandResource = RSGExport
Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil)
Core.Commands.Add(command,
optionTable.helpInfo,
optionTable.subText,
optionTable.argsRequired,
optionTable.funt,
optionTable.restriction or nil
)
elseif isStarted(ESXExport) then
commandResource = ESXExport
ESX.RegisterCommand(command, options[5] or 'admin', function(xPlayer, args, showError)
options[4](xPlayer.source, args, showError)
end, false, { help = options[1] })
ESX.RegisterCommand(command,
optionTable.restriction or 'admin',
function(xPlayer, args, showError)
optionTable.funct(xPlayer.source, args, showError)
end,
false,
{ help = options[1] }
)
end
if commandResource ~= "" then

View File

@@ -25,7 +25,12 @@ Exports = {
-- REDM
RSGExport = "rsg-core",
RSGInv = "rsg-inventory"
RSGInv = "rsg-inventory",
VorpExport = "vorp_core",
VorpInv = "vorp_inventory",
VorpMenu = "vorp_menu",
}
-- Required variables

View File

@@ -53,27 +53,11 @@ function redProgressBar(data)
Wait(0)
local elapsed = GetGameTimer()
local percentage = ((elapsed - (endTime - wait)) / wait) * 100
if percentage < 0 then percentage = 0 end
if percentage > 100 then percentage = 100 end
-- Convert to segmented progress (assuming 5 segments here)
local segments = 1 -- Number of segments in the bar
local segmentProgress = {}
local progressPerSegment = 100 / segments
for i = 1, segments do
local segmentStart = (i - 1) * progressPerSegment
local segmentEnd = i * progressPerSegment
if percentage >= segmentEnd then
segmentProgress[i] = 100
elseif percentage <= segmentStart then
segmentProgress[i] = 0
else
segmentProgress[i] = ((percentage - segmentStart) / progressPerSegment) * 100
end
end
percentage = percentage >= 100 and 100 or percentage
-- Draw your segmented progress bar
ShowRedProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage))
ShowRedProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
-- Controls to disable during progress
if data.disableMouse then
@@ -151,40 +135,48 @@ function redProgressBar(data)
return result
end
function ShowRedProgressBar(currentProg, title, level)
function ShowRedProgressBar(percentage, title, level)
local loc = vec2(0.40, 0.90)
local size = vec2(0.3, 0.03)
local tickCount = 10 -- visually split into 10 segments (9 inner lines)
local barHeight = size.y / 3.4
local barWidth = 0.21 -- tuned to your existing layout
local barLeft = (loc.x - size.x / 4) + 0.075
local barCenter = barLeft + barWidth / 2
local lineW = 0.001
local lineH = barHeight + 0.00
-- Draw background box
DrawSprite("generic_textures", "inkroller_1a", loc.x+0.1, loc.y-0.01, 0.25, 0.07, 180.0, 0, 0, 0, 200)
-- Background plate
DrawSprite("generic_textures", "inkroller_1a", loc.x + 0.1, loc.y - 0.01, 0.25, 0.07, 180.0, 0, 0, 0, 200)
-- Title (left)
SetTextFontForCurrentCommand(6)
SetTextScale(0.35, 0.35)
SetTextColor(255, 255, 255, 255)
SetTextDropshadow(1, 0, 0, 0, 200)
BgDisplayText(title, loc.x - size.x / 4 + 0.074, loc.y - 0.034)
-- Percentage (right)
SetTextFontForCurrentCommand(1)
SetTextScale(0.35, 0.35)
SetTextColor(255, 255, 255, 255)
SetTextDropshadow(1, 0, 0, 0, 200)
BgDisplayText(level, loc.x - size.x / 4 + 0.246, loc.y - 0.030)
local segmentWidth = (size.x + 0.05) / (#currentProg * 2) -- Divide the total width by 18 (9 segments * 2 gaps for each)
local gap = segmentWidth / #currentProg -- Smaller gap between segments
-- Track (bg)
DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
for i = 1, #currentProg do
local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap)
local fillPercentage = currentProg[i]
local progressBarWidth = segmentWidth * (fillPercentage / 100)
-- Semi-transparent background for each segment
DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255)
-- Filling progress for each segment
if progressBarWidth > 0 then
DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 255, 0, 0, 200) -- Blue progress
-- Fill
local fillWidth = barWidth * (percentage / 100.0)
if fillWidth > 0.0 then
local fillCenter = barLeft + (fillWidth / 2.0)
DrawRect(fillCenter, loc.y, fillWidth, barHeight, 255, 0, 0, 200) -- red fill
end
-- Tick lines (9 inner lines for 10 segments)
for i = 1, (tickCount - 1) do
local x = barLeft + (barWidth * (i / tickCount))
DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
end
end
@@ -213,29 +205,14 @@ function gtaProgressBar(data)
local elapsed = GetGameTimer()
local percentage = ((elapsed - (endTime - wait)) / wait) * 100
-- Convert to segmented progress (assuming 5 segments here)
local segments = 1 -- Number of segments in the bar
local segmentProgress = {}
local progressPerSegment = 100 / segments
if percentage < 0 then percentage = 0 end
if percentage > 100 then percentage = 100 end
for i = 1, segments do
local segmentStart = (i - 1) * progressPerSegment
local segmentEnd = i * progressPerSegment
if percentage >= segmentEnd then
segmentProgress[i] = 100
elseif percentage <= segmentStart then
segmentProgress[i] = 0
else
segmentProgress[i] = ((percentage - segmentStart) / progressPerSegment) * 100
end
end
percentage = percentage >= 100 and 100 or percentage
-- Draw your segmented progress bar
ShowGTAProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage))
ShowGTAProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
-- Controls to disable during progress
DisablePlayerFiring(ped, true)
DisablePlayerFiring(PlayerId(), true)
DisableControlAction(0, 25, true) -- Disable aim
DisableControlAction(0, 21, true) -- Disable sprint
DisableControlAction(0, 30, true) -- Disable move left/right
@@ -246,13 +223,15 @@ function gtaProgressBar(data)
inProgress = false
end
end
result = inProgress
end)
-- Wait for completion or cancel
while GetGameTimer() < endTime and inProgress do
Wait(100)
end
-- Wait for completion or cancel
while result == nil do Wait(10) end
inProgress = false
print(tostring(result))
-- Cleanup animations/tasks
if data.dict then
stopAnim(data.dict, data.anim, ped)
@@ -261,10 +240,6 @@ function gtaProgressBar(data)
ClearPedTasks(ped)
end
result = inProgress
inProgress = false
while result == nil do Wait(10) end
-- Cleanup
FreezeEntityPosition(ped, false)
@@ -276,14 +251,22 @@ function gtaProgressBar(data)
return result
end
function ShowGTAProgressBar(currentProg, title, level)
function ShowGTAProgressBar(percentage, title, level)
local loc = vec2(0.37, 0.90)
local size = vec2(0.3, 0.03)
local tickCount = 10
local barHeight = size.y / 3.4
local barWidth = 0.19
local barLeft = (loc.x - size.x / 4) + 0.075
local barCenter = barLeft + barWidth / 2
local lineW = 0.001
local lineH = barHeight + 0.00
-- Draw background box
DrawSprite("timerbars", "all_black_bg", loc.x +0.028, loc.y-0.01, 0.15, 0.07, 0.0, 255, 255, 255, 255)
DrawSprite("timerbars", "all_black_bg", loc.x +0.170, loc.y-0.01, 0.15, 0.07, 180.0, 255, 255, 255, 255)
-- Background plates
DrawSprite("timerbars", "all_black_bg", loc.x + 0.028, loc.y - 0.01, 0.15, 0.07, 0.0, 255, 255, 255, 255)
DrawSprite("timerbars", "all_black_bg", loc.x + 0.170, loc.y - 0.01, 0.15, 0.07, 180.0, 255, 255, 255, 255)
-- Title (left)
SetTextFont(0)
SetTextProportional(1)
SetTextScale(0.35, 0.35)
@@ -293,34 +276,35 @@ function ShowGTAProgressBar(currentProg, title, level)
SetTextOutline()
SetTextEntry("STRING")
AddTextComponentString(title)
DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034) -- Adjust text position
DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034)
-- Percentage (right)
SetTextFont(0)
SetTextProportional(1)
SetTextScale(0.35, 0.25)
SetTextColour(255, 255, 255, 255)
SetTextEntry("STRING")
AddTextComponentString(level)
DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030) -- Right-aligned additional text
DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030)
local segmentWidth = (size.x + 0.05) / (#currentProg * 2) -- Divide the total width by 18 (9 segments * 2 gaps for each)
local gap = segmentWidth / #currentProg -- Smaller gap between segments
-- Track (bg)
DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
for i = 1, #currentProg do
local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap)
local fillPercentage = currentProg[i]
local progressBarWidth = segmentWidth * (fillPercentage / 100)
-- Semi-transparent background for each segment
DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255)
-- Filling progress for each segment
if progressBarWidth > 0 then
DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 93, 182, 229, 255) -- Blue progress
-- Fill
local fillWidth = barWidth * (percentage / 100.0)
if fillWidth > 0.0 then
local fillCenter = barLeft + (fillWidth / 2.0)
DrawRect(fillCenter, loc.y, fillWidth, barHeight, 93, 182, 229, 255) -- GTA blue
end
-- Tick lines
for i = 1, (tickCount - 1) do
local x = barLeft + (barWidth * (i / tickCount))
DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
end
end
function stopProgressBar() inProgress = false end
function isProgressBar() return inProgress end

View File

@@ -1,5 +1,4 @@
-- Global Key Table, defined once.
---
local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 }
-- Mapping of key codes to human-readable key names.
@@ -17,122 +16,196 @@ local Keys = {
[20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N",
[244] = "M", [82] = ",", [81] = "."
}
-- Tables for storing created targets.
local TextTargets = {} -- For fallback DrawText3D targets.
local targetEntities = {} -- For entity targets.
function createEntityTarget(entity, opts, dist)
startTargetLoop()
targetEntities[#targetEntities + 1] = entity
local entityCoords = GetEntityCoords(entity)
local existingTarget = nil
for _, target in pairs(TextTargets) do
if #(target.coords - entityCoords) < 0.01 then
existingTarget = target
break
end
end
-- ===== Ownership + indexes =====
-- TextTargets: key -> target data (coords/entity/models/options/etc.)
local TextTargets = {}
-- targetEntities kept for parity (not strictly required)
local targetEntities = {}
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]
-- registry: key -> owner, owner -> set(keys)
local TargetRegistry = { byKey = {}, byResource = {} }
local function getOwnerResource()
return GetInvokingResource() or GetCurrentResourceName() or "unknown"
end
local function registerTarget(owner, key)
-- move key across owners if replacing
local prev = TargetRegistry.byKey[key]
if prev and TargetRegistry.byResource[prev] then
TargetRegistry.byResource[prev][key] = nil
end
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
TargetRegistry.byKey[key] = owner
TargetRegistry.byResource[owner] = TargetRegistry.byResource[owner] or {}
TargetRegistry.byResource[owner][key] = true
end
local function removeTargetKey(key, reason)
local owner = TargetRegistry.byKey[key]
if TextTargets[key] then
TextTargets[key] = nil
-- print("^6Bridge^7:^5 Target^7: ^2Removed target '%s'%s", key, reason and (" ("..reason..")") or "")
end
TextTargets[entity] = {
coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z),
buttontext = tempText,
options = opts,
dist = dist,
text = table.concat(tempText, "\n")
}
if owner then
if TargetRegistry.byResource[owner] then
TargetRegistry.byResource[owner][key] = nil
end
TargetRegistry.byKey[key] = nil
end
end
AddEventHandler("onResourceStop", function(res)
local owned = TargetRegistry.byResource[res]
if not owned then return end
local cnt = 0
for key in pairs(owned) do
removeTargetKey(key, "resource stopped: "..res)
cnt = cnt + 1
end
TargetRegistry.byResource[res] = nil
-- print("^6Bridge^7:^5 Target^7: ^2Cleared "..cnt.." target(s) from '"..res.."'")
end)
-- ===== Helpers =====
local function vecKey(v)
-- stable rounded coord string for entity dedupe when name isn't provided
return ("%.3f,%.3f,%.3f"):format(v.x, v.y, v.z)
end
local function bakeButtons(opts)
local tempText = {}
for i = 1, #opts do
opts[i].key = KEY_TABLE[i]
tempText[#tempText + 1] =
(" ~b~[~w~%s~b~] ~w~%s"):format(Keys[opts[i].key] or ("K"..opts[i].key), opts[i].label or ("Option "..i))
end
return tempText
end
-- Update cached text blob
local function updateCachedText(target)
target.text = table.concat(target.buttontext, "\n")
end
-- ===== Public API: Create targets =====
-- ENTITY: createEntityTarget(entity, opts, dist, nameOpt?)
function createEntityTarget(entity, opts, dist, name)
startTargetLoop()
if not entity or entity == 0 then return end
targetEntities[#targetEntities + 1] = entity
local owner = getOwnerResource()
local coords = GetEntityCoords(entity)
local key = tostring(name) or ("entity@" .. vecKey(coords))
-- Always overwrite on same key
local buttontext = bakeButtons(opts)
TextTargets[key] = {
_key = key,
_type = "entity",
_owner = owner,
entity = entity,
coords = vec3(coords.x, coords.y, coords.z),
buttontext = buttontext,
options = opts,
dist = dist,
}
updateCachedText(TextTargets[key])
registerTarget(owner, key)
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords))
return key
end
-- ZONE: createZoneTarget(data, opts, dist)
-- Expect data[1] = id/name, data[2] = vec3 coords (as in your original)
function createZoneTarget(data, opts, dist)
startTargetLoop()
local 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,
local owner = getOwnerResource()
local zname = tostring(data[1] or ("zone@"..vecKey(data[2] or vec3(0,0,0))))
local coords = data[2]
local buttontext = bakeButtons(opts)
TextTargets[zname] = {
_key = zname,
_type = "zone",
_owner = owner,
coords = coords,
buttontext = buttontext,
options = opts,
dist = dist,
text = table.concat(tempText, "\n")
}
end
return data[1]
updateCachedText(TextTargets[zname])
registerTarget(owner, zname)
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ZONE target '"..zname.."' by '"..owner.."' @ "..formatCoord(coords))
return zname
end
function createModelTarget(models, opts, dist)
-- MODEL: createModelTarget(models, opts, dist, nameOpt?)
function createModelTarget(models, opts, dist, name)
startTargetLoop()
if type(models) ~= "table" then
models = { models }
local owner = getOwnerResource()
if type(models) ~= "table" then models = { models } end
local key
if name then
key = tostring(name)
else
local parts = {}
for i, m in ipairs(models) do parts[i] = tostring(m) end
key = "model_" .. table.concat(parts, "_")
end
local 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] = {
local buttontext = bakeButtons(opts)
TextTargets[key] = {
_key = key,
_type = "model",
_owner = owner,
models = models,
buttontext = tempText,
buttontext = buttontext,
options = opts,
dist = dist,
coords = vec3(0, 0, 0),
text = table.concat(tempText, "\n")
coords = vec3(0, 0, 0), -- will be updated by the refresher
}
return targetKey
updateCachedText(TextTargets[key])
registerTarget(owner, key)
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated MODEL target '"..key.."' by '"..owner.."' (models: "..table.concat(models, ",")..")")
return key
end
function removeEntityTarget(entity)
TextTargets[entity] = nil
-- ===== Public API: Remove targets =====
-- Entity removal accepts entity handle or key string.
function removeEntityTarget(entityOrKey)
local key = nil
if type(entityOrKey) == "string" then
key = entityOrKey
elseif type(entityOrKey) == "number" then
for k, t in pairs(TextTargets) do
if t._type == "entity" and t.entity == entityOrKey then key = k break end
end
if not key then
-- Fallback: try coord-key match
local c = GetEntityCoords(entityOrKey)
local guess = "entity@"..vecKey(c)
if TextTargets[guess] then key = guess end
end
end
if key then removeTargetKey(key, "removeEntityTarget") end
end
function removeZoneTarget(target)
TextTargets[target] = nil
function removeZoneTarget(key)
if not key then return end
removeTargetKey(key, "removeZoneTarget")
end
function removeModelTarget(model)
TextTargets[model] = nil
-- For models, pass the returned key from createModelTarget (recommended).
function removeModelTarget(key)
if not key then return end
removeTargetKey(key, "removeModelTarget")
end
exports("createEntityTarget", createEntityTarget)
@@ -143,22 +216,20 @@ exports("removeEntityTarget", removeEntityTarget)
exports("removeZoneTarget", removeZoneTarget)
exports("removeModelTarget", removeModelTarget)
-------------------------------------------------------------
-- Fallback: DrawText3D Targets (Experimental)
-------------------------------------------------------------
local started = false
function startTargetLoop()
if started then return end
Config = {
System = {
}
}
started = true
-- lazy include (unchanged from your original)
Config = { System = {} }
local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua')))
fileLoader()
-- Model Entity Refresher
-- Model Entity Refresher (kept)
CreateThread(function()
while true do
local pedCoords = GetEntityCoords(PlayerPedId())
@@ -178,7 +249,7 @@ function startTargetLoop()
end
end)
-- Main Target Loop
-- Main Target Loop (unchanged logic, just uses new TextTargets entries)
CreateThread(function()
while true do
local ped = PlayerPedId()
@@ -238,12 +309,11 @@ function startTargetLoop()
::continue::
end
Wait(1) -- Throttled
Wait(1)
end
end)
end
function DrawText3D(coord, text, highlight)
SetTextScale(0.30, 0.30)
SetTextFont(0)
@@ -253,12 +323,12 @@ function DrawText3D(coord, text, highlight)
SetTextCentre(true)
local totalLength = string.len(text)
local textMaxLength = 99 -- max 99
local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text
AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text)
local textMaxLength = 99
local txt = totalLength > textMaxLength and text:sub(1, textMaxLength) or text
AddTextComponentString(highlight and txt:gsub("%~w~", "~y~") or txt)
SetDrawOrigin(coord.x, coord.y, coord.z, 0)
DrawText(0.0, 0.0)
local count, length = GetLineCountAndMaxLength(text)
local count, length = GetLineCountAndMaxLength(txt)
local padding = 0.005
local heightFactor = (count / 43) + padding
@@ -271,19 +341,10 @@ function DrawText3D(coord, text, highlight)
ClearDrawOrigin()
end
--- Calculates the number of lines and the maximum line length from the given text.
---
--- @param text string The text to analyze.
--- @return number, number The line count and maximum line length.
---
--- @usage
--- ```lua
--- local count, maxLen = GetLineCountAndMaxLength("Hello World")
--- ```
function GetLineCountAndMaxLength(text)
local lineCount, maxLength = 0, 0
for line in text:gmatch("[^\n]+") do
lineCount += 1
lineCount = lineCount + 1
local lineLength = string.len(line)
if lineLength > maxLength then
maxLength = lineLength
@@ -293,7 +354,6 @@ function GetLineCountAndMaxLength(text)
return lineCount, maxLength
end
function RotationToDirection(rot)
local adjust = math.pi / 180
return vec3(
@@ -311,8 +371,3 @@ function normalizeVector(vec)
return vec3(0, 0, 0)
end
end
-- Helper to update cached text.
function updateCachedText(target)
target.text = table.concat(target.buttontext, "\n")
end