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,67 +1,78 @@
local function parseVersion(version) local function readBoolMeta(key, default)
local parts = {} local val = GetResourceMetadata(GetCurrentResourceName(), key, 0)
for num in version:gmatch("%d+") do if not val then return default end
table.insert(parts, tonumber(num)) val = tostring(val):lower()
end return (val == 'true' or val == '1' or val == 'yes' or val == 'on')
return parts
end end
local function compareVersions(current, newest) local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false)
local currentParts = parseVersion(current)
local newestParts = parseVersion(newest) if not SUPPRESS_UPDATES then
for i = 1, math.max(#currentParts, #newestParts) do local function parseVersion(version)
local c = currentParts[i] or 0 local parts = {}
local n = newestParts[i] or 0 for num in version:gmatch("%d+") do
if c < n then return -1 table.insert(parts, tonumber(num))
elseif c > n then return 1 end end
return parts
end end
return 0 -- equal
end
function CheckBridgeVersion() local function compareVersions(current, newest)
if IsDuplicityVersion() then local currentParts = parseVersion(current)
CreateThread(function() local newestParts = parseVersion(newest)
Wait(4000) for i = 1, math.max(#currentParts, #newestParts) do
local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version') local c = currentParts[i] or 0
--PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers) local n = newestParts[i] or 0
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers) if c < n then return -1
if not body then elseif c > n then return 1 end
print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)") end
return return 0 -- equal
end end
local lines = {} function CheckBridgeVersion()
for line in body:gmatch("[^\r\n]+") do if IsDuplicityVersion() then
table.insert(lines, line) CreateThread(function()
end Wait(4000)
local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version')
local newestVersionRaw = lines[1] or "0.0.0" --PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers)
local changelog = {} PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers)
for i = 2, #lines do if not body then
table.insert(changelog, lines[i]) print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)")
end return
local compareResult = compareVersions(currentVersionRaw, newestVersionRaw)
if compareResult == 0 then
print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end end
print("^1----------------------------------------------------------------------^7")
SetTimeout(3600000, function() local lines = {}
CheckBridgeVersion() for line in body:gmatch("[^\r\n]+") do
end) table.insert(lines, line)
else end
print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)")
end local newestVersionRaw = lines[1] or "0.0.0"
local changelog = {}
for i = 2, #lines do
table.insert(changelog, lines[i])
end
local compareResult = compareVersions(currentVersionRaw, newestVersionRaw)
if compareResult == 0 then
print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(3600000, function()
CheckBridgeVersion()
end)
else
print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)")
end
end)
end) end)
end) end
end end
end
CheckBridgeVersion() CheckBridgeVersion()
end

View File

@@ -31,17 +31,18 @@ local Exports = {
-- REDM -- REDM
RSGExport = "rsg-core", 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 -- Prevent reloading if cache is already initialized
local cache = { local cache = { Items = {}, Vehicles = {}, Jobs = {}, Gangs = {}, }
Items = {},
Vehicles = {},
Jobs = {},
Gangs = {},
}
local cacheReady = false local cacheReady = false
-- Timer info, for debugging more then anything
local timers = {} local timers = {}
local function startTimer(label) local function startTimer(label)
timers[label] = GetGameTimer() timers[label] = GetGameTimer()
@@ -52,26 +53,57 @@ local function endTimer(label)
timers[label] = "("..(timers[label] / 1000).."s)" timers[label] = "("..(timers[label] / 1000).."s)"
end end
startTimer("Cache") startTimer("Items") startTimer("Vehicles") startTimer("Jobs") startTimer("InvWeight") startTimer("InvSlots") 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) 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 end
-- Ensure oxmysql resource is loaded local function waitStarted(resourceName)
if checkExists(Exports.OXCoreExport) or checkExists(Exports.ESXExport) then 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'))) local fileLoader = assert(load(LoadResourceFile("oxmysql", ('lib/MySQL.lua')), ('@@oxmysql/lib/MySQL.lua')))
fileLoader() fileLoader()
end end
if checkExists(Exports.OXCoreExport) then if checkExists(Exports.OXCoreExport) then
-- Detected OX_Core in server, wait for it to be started if needed -- 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'))) local fileLoader = assert(load(LoadResourceFile(Exports.OXCoreExport, ('lib/init.lua')), ('@@'..Exports.OXCoreExport..'/lib/init.lua')))
fileLoader() fileLoader()
end end
if checkExists(Exports.ESXExport) then if checkExists(Exports.ESXExport) then
-- Detected ESX in server, wait for it to be started if needed -- 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'))) local fileLoader = assert(load(LoadResourceFile(Exports.ESXExport, ('/imports.lua')), ('@@'..Exports.ESXExport..'/imports.lua')))
fileLoader() fileLoader()
end end
@@ -89,225 +121,274 @@ end
--------------------- ---------------------
---- Load Items ----- ---- Load Items -----
--------------------- ---------------------
-- Items initialization based on detected inventory system
if checkExists(Exports.OXInv) then
-- Wait for OX Inventory to start if it's not already started
while GetResourceState(Exports.OXInv) ~= "started" do Wait(100) end
itemResource = Exports.OXInv
local success, result = pcall(function() local itemFunc = {
return exports[Exports.OXInv]:Items() { script = Exports.OXInv,
end) cacheItem = function()
if success and result then local success, result = pcall(function()
cache.Items = result return exports[Exports.OXInv]:Items()
end end)
if success and result then
-- Get Weapon info and duplicate them if they are uppercase cache.Items = result
-- (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 end
else end,
print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?") },
print("^1Possible Issue found^7:") { script = Exports.TgiannInv,
print(json.encode(cache.Items[k], {indent = true})) cacheItem = function()
end local success, result = pcall(function()
end return exports[Exports.TgiannInv]:Items()
end)
elseif checkExists(Exports.TgiannInv) then if success and result then
-- Wait for OX Inventory to start if it's not already started cache.Items = result
while GetResourceState(Exports.TgiannInv) ~= "started" do Wait(100) end
itemResource = Exports.TgiannInv
local success, result = pcall(function()
return exports[Exports.TgiannInv]:Items()
end)
if success and result then
cache.Items = result
end
-- Get Weapon info and duplicate them if they are uppercase
-- (duplicate incase anything checks for the uppercase version)
for k, v in pairs(cache.Items) do
if type(k) == "string" then
if k:find("WEAPON") then
cache.Items[k:lower()] = cache.Items[k]
end end
else end,
print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?") },
print("^1Possible Issue found^7:") { script = Exports.QBXExport,
print(json.encode(cache.Items[k], {indent = true})) cacheItem = function()
end cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
-- If this is nil, they need to update to qbx_core 1.23.0+
if not cache.Items then
-- if their inventory doesn't allow that (they refuse to update their butchered core replacement):
if GetResourceState(Exports.QSInv):find("start") then
itemResource = Exports.QSInv
cache.Items = exports[Exports.QSInv]:GetItemList()
elseif GetResourceState(Exports.OrigenInv):find("start") then
itemResource = Exports.OrigenInv
cache.Items = exports[Exports.OrigenInv]:Items()
elseif GetResourceState(Exports.CodeMInv):find("start") then
itemResource = Exports.CodeMInv
cache.Items = exports[Exports.CodeMInv]:GetItemList()
elseif GetResourceState(Exports.CoreInv):find("start") then
itemResource = Exports.CoreInv
cache.Items = exports[Exports.CoreInv]:getItemsList()
elseif GetResourceState(Exports.TgiannInv):find("start") then
itemResource = Exports.TgiannInv
cache.Items = exports[Exports.TgiannInv]:Items()
end
end
end,
},
{ script = Exports.QBExport,
cacheItem = function()
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
end,
},
{ script = Exports.ESXExport,
cacheItem = function()
if GetResourceState(Exports.QSInv):find("start") then
cache.Items = exports[Exports.QSInv]:GetItemList()
else
cache.Items = ESX.GetItems()
while not next(cache.Items) do
cache.Items = ESX.GetItems()
Wait(1000)
end
end
end,
},
{ script = Exports.RSGExport,
cacheItem = function()
cache.Items = exports[Exports.RSGExport]:GetCoreObject().Shared.Items
end,
},
{ script = Exports.VorpInv,
cacheItem = function()
local dbItems = MySQL.query.await('SELECT * FROM `items`')
local tempItems = {}
for i = 1, #dbItems do
local v = dbItems[i]
tempItems[v.item] = {
name = v.item,
label = v.label,
weight = v.weight,
info = v.metadata,
usable = v.usable,
type = v.type,
description = v.desc,
}
end
cache.Items = tempItems
end,
},
}
for i = 1, #itemFunc do
local data = itemFunc[i]
if checkExists(data.script) then
waitStarted(data.script) -- Wait for detected script to start fully
data.cacheItem() -- run tablized function for core/inv
dupLowercaseWeapons(cache.Items) -- make usable weapon item names for jim_bridge
itemResource = data.script -- Grab script name to announce later
endTimer("Items") -- end timer
break -- break loop so it doesn't keep checking
end end
elseif checkExists(Exports.QBXExport) then
while GetResourceState(Exports.QBXExport) ~= "started" do Wait(100) end
itemResource = Exports.QBXExport
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
-- If this is nil, they need to update to qbx_core 1.23.0+
if not cache.Items then
-- if their inventory doesn't allow that (they refuse to update their butchered core replacement):
if GetResourceState(Exports.QSInv):find("start") then
itemResource = Exports.QSInv
cache.Items = exports[Exports.QSInv]:GetItemList()
elseif GetResourceState(Exports.OrigenInv):find("start") then
itemResource = Exports.OrigenInv
cache.Items = exports[Exports.OrigenInv]:Items()
elseif GetResourceState(Exports.CodeMInv):find("start") then
itemResource = Exports.CodeMInv
cache.Items = exports[Exports.CodeMInv]:GetItemList()
elseif GetResourceState(Exports.CoreInv):find("start") then
itemResource = Exports.CoreInv
cache.Items = exports[Exports.CoreInv]:getItemsList()
elseif GetResourceState(Exports.TgiannInv):find("start") then
itemResource = Exports.TgiannInv
cache.Items = exports[Exports.TgiannInv]:Items()
end
end
elseif checkExists(Exports.QBExport) then
while GetResourceState(Exports.QBExport) ~= "started" do Wait(100) end
itemResource = Exports.QBExport
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
elseif checkExists(Exports.ESXExport) then
itemResource = Exports.ESXExport
if GetResourceState(Exports.QSInv):find("start") then
cache.Items = exports[Exports.QSInv]:GetItemList()
else
cache.Items = ESX.GetItems()
while not next(cache.Items) do
cache.Items = ESX.GetItems()
Wait(1000)
end
end
elseif checkExists(Exports.RSGExport) then
while GetResourceState(Exports.RSGExport) ~= "started" do Wait(100) end
itemResource = Exports.RSGExport
cache.Items = exports[Exports.RSGExport]:GetCoreObject().Shared.Items
end end
endTimer("Items")
--------------------- ---------------------
--- Load Vehicles --- --- Load Vehicles ---
--------------------- ---------------------
-- Vehicle loading depending on framework ---
if checkExists(Exports.QBXExport) then local vehicleFunc = {
vehResource = Exports.QBXExport
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
elseif checkExists(Exports.QBExport)then { script = Exports.QBXExport,
vehResource = Exports.QBExport cacheVehicle = function()
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
end,
},
{ script = Exports.QBExport,
cacheVehicle = function()
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
end,
},
{ script = Exports.OXCoreExport,
cacheVehicle = function()
cache.Vehicles = {}
for k, v in pairs(Ox.GetVehicleData()) do
cache.Vehicles[k] = {
model = k, hash = joaat(k),
price = v.price,
name = v.name,
brand = v.make
}
end
end,
},
{ script = Exports.ESXExport,
cacheVehicle = function()
while not MySQL do Wait(100) end
for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do
cache.Vehicles[v.model] = {
model = v.model,
hash = joaat(v.model),
price = v.price,
name = v.name,
}
end
end,
},
{ script = Exports.RSGExport,
cacheVehicle = function()
cache.Vehicles = exports[Exports.RSGExport]:GetCoreObject().Shared.Vehicles
end,
},
{ script = Exports.VorpExport,
cacheVehicle = function()
cache.Vehicles = { ["unkown"] = {} }
end,
},
}
elseif checkExists(Exports.OXCoreExport) then for i = 1, #vehicleFunc do
vehResource = Exports.OXCoreExport local data = vehicleFunc[i]
cache.Vehicles = {} if checkExists(data.script) then
for k, v in pairs(Ox.GetVehicleData()) do waitStarted(data.script) -- Wait for detected script to start fully
cache.Vehicles[k] = { data.cacheVehicle() -- run tablized function for core
model = k, hash = joaat(k), vehResource = data.script -- Grab script name to announce later
price = v.price, endTimer("Vehicles") -- end timer
name = v.name, break -- break loop so it doesn't keep checking
brand = v.make
}
end end
elseif checkExists(Exports.ESXExport) then
vehResource = Exports.ESXExport
while not MySQL do Wait(1000) end
for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do
cache.Vehicles[v.model] = {
model = v.model,
hash = joaat(v.model),
price = v.price,
name = v.name,
}
end
elseif checkExists(Exports.RSGExport) then
vehResource = Exports.RSGExport
cache.Vehicles = exports[Exports.RSGExport]:GetCoreObject().Shared.Vehicles
end end
endTimer("Vehicles")
--------------------- ---------------------
----- Load Jobs ----- ----- Load Jobs -----
--------------------- ---------------------
-- Jobs loading based on framework
if checkExists(Exports.QBXExport) then
jobResource = Exports.QBXExport
cache.Jobs, cache.Gangs = exports[Exports.QBXExport]:GetJobs(), exports[Exports.QBXExport]:GetGangs()
elseif checkExists(Exports.QBExport) then local jobFunc = {
jobResource = Exports.QBExport
cache.Jobs, cache.Gangs = exports[Exports.QBExport]:GetCoreObject().Shared.Jobs, exports[Exports.QBExport]:GetCoreObject().Shared.Gangs
elseif checkExists(Exports.OXCoreExport) then { script = Exports.QBXExport,
jobResource = Exports.OXCoreExport cacheJob = function()
while not MySQL do Wait(1000) end cache.Jobs, cache.Gangs = exports[Exports.QBXExport]:GetJobs(), exports[Exports.QBXExport]:GetGangs()
local tempJobs = MySQL.query.await('SELECT * FROM `ox_groups`') end,
local tempGrades = MySQL.query.await('SELECT * FROM `ox_group_grades`') },
local gradeMap = {} { script = Exports.QBExport,
for _, grade in pairs(tempGrades) do cacheJob = function()
gradeMap[grade.group] = gradeMap[grade.group] or {} Core = exports[Exports.QBExport]:GetCoreObject()
gradeMap[grade.group][grade.grade] = { name = grade.label } cache.Jobs, cache.Gangs = Core.Shared.Jobs, Core.Shared.Gangs
end end,
for _, job in pairs(tempJobs) do },
cache.Jobs[job.name] = { { script = Exports.OXCoreExport,
label = job.label, cacheJob = function()
grades = gradeMap[job.name] or {} while not MySQL do Wait(100) end
} local tempJobs = MySQL.query.await('SELECT * FROM `ox_groups`')
end local tempGrades = MySQL.query.await('SELECT * FROM `ox_group_grades`')
cache.Gangs = cache.Jobs local gradeMap = {}
for _, grade in pairs(tempGrades) do
elseif checkExists(Exports.ESXExport) then gradeMap[grade.group] = gradeMap[grade.group] or {}
jobResource = Exports.ESXExport gradeMap[grade.group][grade.grade] = { name = grade.label }
ESX = exports[Exports.ESXExport]:getSharedObject()
cache.Jobs = ESX.GetJobs()
while not next(cache.Jobs) do
Wait(100)
cache.Jobs = ESX.GetJobs()
end
for Role, Grades in pairs(cache.Jobs) do
-- Check for if user has added grades
if Grades.grades == nil or not next(Grades.grades) then
goto continue
end
for grade, info in pairs(Grades.grades) do
if info.label and info.label:find("[Bb]oss") then
cache.Jobs[Role].grades[grade].isBoss = true
goto continue
end end
end for _, job in pairs(tempJobs) do
local highestGrade = nil cache.Jobs[job.name] = {
for k in pairs(Grades.grades) do label = job.label,
local num = tonumber(k) grades = gradeMap[job.name] or {}
if num and (not highestGrade or num > highestGrade) then }
highestGrade = num
end end
end cache.Gangs = cache.Jobs
end,
},
{ script = Exports.ESXExport,
cacheJob = function()
ESX = ESX or exports[Exports.ESXExport]:getSharedObject()
cache.Jobs = ESX.GetJobs()
while not next(cache.Jobs) do
Wait(100)
cache.Jobs = ESX.GetJobs()
end
for Role, Grades in pairs(cache.Jobs) do
-- Check for if user has added grades
if Grades.grades == nil or not next(Grades.grades) then
goto continue
end
for grade, info in pairs(Grades.grades) do
if info.label and info.label:find("[Bb]oss") then
cache.Jobs[Role].grades[grade].isBoss = true
goto continue
end
end
local highestGrade = nil
for k in pairs(Grades.grades) do
local num = tonumber(k)
if num and (not highestGrade or num > highestGrade) then
highestGrade = num
end
end
if highestGrade then if highestGrade then
cache.Jobs[Role].grades[tostring(highestGrade)].isBoss = true cache.Jobs[Role].grades[tostring(highestGrade)].isBoss = true
end end
::continue:: ::continue::
end
cache.Gangs = cache.Jobs
end,
},
{ script = Exports.RSGExport,
cacheJob = function()
Core = exports[Exports.RSGExport]:GetCoreObject()
cache.Jobs, cache.Gangs = Core.Shared.Jobs, Core.Shared.Gangs
end,
},
{ script = Exports.VorpExport,
cacheJob = function()
cache.Jobs = { ["unkown"] = {} }
cache.Gangs = cache.Jobs
end,
},
}
for i = 1, #jobFunc do
local data = jobFunc[i]
if checkExists(data.script) then
waitStarted(data.script) -- Wait for detected script to start fully
data.cacheJob() -- run tablized function for core
jobResource = data.script -- Grab script name to announce later
endTimer("Jobs") -- end timer
break -- break loop so it doesn't keep checking
end end
cache.Gangs = cache.Jobs
elseif checkExists(Exports.RSGExport) then
jobResource = Exports.RSGExport
cache.Jobs, cache.Gangs = exports[Exports.RSGExport]:GetCoreObject().Shared.Jobs, exports[Exports.RSGExport]:GetCoreObject().Shared.Gangs
end end
endTimer("Jobs")
-- Fallback if nil or empty -- Fallback if nil or empty
if cache.Items == nil or not next(cache.Items) then 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 checkExists(script) then
if script == Exports.QBInv and GetResourceState(Exports.JPRInv):find("start") then goto skip end if script == Exports.QBInv and GetResourceState(Exports.JPRInv):find("start") then goto skip end
while GetResourceState(script) ~= "started" and GetResourceState(script) ~= "stopped" do waitStartedOrStopped(script)
Wait(100)
print("Waiting for script start")
end
local attempts = data.fallback or { data } local attempts = data.fallback or { data }
local lookup, used, err 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' } games { 'gta5', 'rdr3' }
lua54 'yes' lua54 'yes'
files { files {
'starter.lua', 'starter.lua',
'shared/*.lua', 'shared/*.lua',
@@ -24,4 +23,6 @@ server_scripts {
client_scripts { client_scripts {
'clientFrameworkCache.lua', 'clientFrameworkCache.lua',
'ui_modules/*.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 src = source
local token = keyGen()..keyGen()..keyGen()..keyGen() -- Use a secure random generator here local token = keyGen()..keyGen()..keyGen()..keyGen() -- Use a secure random generator here
--debugPrint(GetInvokingResource()) --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") debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
return "" return ""
end end
@@ -45,8 +45,9 @@ if isServer() then
createCallback(getScript()..":callback:GetAuthEvent", function(source) createCallback(getScript()..":callback:GetAuthEvent", function(source)
local src = 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") debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital callback was called from an external resource^7")
return "" return ""
end end

View File

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

View File

@@ -1,100 +1,111 @@
function parseVersion(version) local function readBoolMeta(key, default)
local parts = {} local val = GetResourceMetadata("jim_bridge", key, 0)
for num in version:gmatch("%d+") do if not val then return default end
table.insert(parts, tonumber(num)) 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
return parts
end
function compareVersions(current, newest) function compareVersions(current, newest)
local currentParts = parseVersion(current) local currentParts = parseVersion(current)
local newestParts = parseVersion(newest) local newestParts = parseVersion(newest)
for i = 1, math.max(#currentParts, #newestParts) do for i = 1, math.max(#currentParts, #newestParts) do
local c = currentParts[i] or 0 local c = currentParts[i] or 0
local n = newestParts[i] or 0 local n = newestParts[i] or 0
if c < n then return -1 if c < n then return -1
elseif c > n then return 1 end elseif c > n then return 1 end
end
return 0
end end
return 0
end
function capitalize(str) function capitalize(str)
return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end)) 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 scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or "" local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or "" local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) 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 if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
CreateThread(function() CreateThread(function()
Wait(4000) Wait(4000)
local script = getScript() local script = getScript()
local currentVersionRaw = GetResourceMetadata(script, 'version') local currentVersionRaw = GetResourceMetadata(script, 'version')
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers) PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers)
if not newestVersionRaw then if not newestVersionRaw then
-- fallback -- fallback
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers) PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers)
if not fallbackVersionRaw then if not fallbackVersionRaw then
print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)") print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)")
return return
end end
local lines = {}
for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, fallbackVersion)
if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)")
if #changelog > 0 then
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
end
end)
else
local lines = {} local lines = {}
for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "") local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {} local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, fallbackVersion) local compareResult = compareVersions(currentVersionRaw, newestVersion)
if compareResult == 0 then if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)") print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7") print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)") print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)")
if #changelog > 0 then if #changelog > 0 then
for _, line in ipairs(changelog) do for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line) print((line:find("http") and "^7" or "^5")..line)
end end
end end
print("^1----------------------------------------------------------------------^7") print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function() CheckVersion() end) SetTimeout(3600000, function() CheckVersion() end)
else else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)") print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
end end
end)
else
local lines = {}
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, newestVersion)
if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)")
if #changelog > 0 then
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(3600000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
end end
end end)
end) end)
end) end
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) debugPrint("^6Bridge^7: ^2Registering ^4"..QBExport.." ^3Callback^7:", callbackName)
Core = Core or exports[QBExport]:GetCoreObject() Core = Core or exports[QBExport]:GetCoreObject()
Core.Functions.CreateCallback(callbackName, adaptedFunction) 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 elseif isStarted(ESXExport) then
debugPrint("^6Bridge^7: ^2Registering ^4"..ESXExport.." ^3Callback^7:", callbackName) debugPrint("^6Bridge^7: ^2Registering ^4"..ESXExport.." ^3Callback^7:", callbackName)
ESX.RegisterServerCallback(callbackName, adaptedFunction) ESX.RegisterServerCallback(callbackName, adaptedFunction)
@@ -76,6 +80,13 @@ function triggerCallback(callbackName, ...)
end, ...) end, ...)
result = Citizen.Await(p) result = Citizen.Await(p)
Wait(10) 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 elseif isStarted(ESXExport) then
local p = promise.new() local p = promise.new()
ESX.TriggerServerCallback(callbackName, function(cbResult) ESX.TriggerServerCallback(callbackName, function(cbResult)

View File

@@ -10,6 +10,263 @@
• esx (using ESX.UI.Menu) • esx (using ESX.UI.Menu)
]] ]]
local contextFunc = {
["ox"] =
function(Menu, data)
local index = nil
if data.onBack and not data.onSelected then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
title = "Return",
onSelect = data.onBack,
label = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
local menuID = 'Menu'
(data.onSelected and lib.registerMenu or lib.registerContext)({
id = menuID,
title = data.header..br..br..(data.headertxt and data.headertxt or ""),
position = 'top-right',
options = Menu,
canClose = data.canClose and data.canClose or nil,
onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil,
onExit = data.onExit and data.onExit or nil,
onSelected = data.onSelected and (function(selected) index = selected end) or nil,
}, data.onSelected and (function(x, y, args)
if Menu[x].refresh then
if Menu[x].onSelect then
Menu[x].onSelect()
end
lib.showMenu(menuID, index)
else
if Menu[x].onSelect then
Menu[x].onSelect()
else
lib.showMenu(menuID, index)
end
end
end) or nil)
if data.onSelected then
lib.showMenu(menuID, 1)
else
lib.showContext(menuID)
end
end,
["qb"] =
function(Menu, data)
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
header = " ",
txt = "Return",
params = {
isAction = true,
event = data.onBack,
},
})
elseif data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
header = " ",
txt = "Close",
params = {
isAction = true,
event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end),
},
})
end
if data.header ~= nil then
local tempMenu = {}
for k, v in pairs(Menu) do tempMenu[k + 1] = v end
tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true }
Menu = tempMenu
end
for k in pairs(Menu) do
if not Menu[k].params or not Menu[k].params.event then
Menu[k].params = {
isAction = true,
event = Menu[k].onSelect or function() end,
}
end
if not Menu[k].header then Menu[k].header = " " end
if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end
Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable
end
exports[QBMenuExport]:openMenu(Menu)
end,
["gta"] =
function(Menu, data)
WarMenu.CreateMenu(tostring(Menu), data.header, " ", {
titleColor = { 222, 255, 255 },
maxOptionCountOnScreen = 15,
width = 0.25,
x = 0.7,
})
if WarMenu.IsAnyMenuOpened() then return end
WarMenu.OpenMenu(tostring(Menu))
CreateThread(function()
local close = true
while true do
if WarMenu.Begin(tostring(Menu)) then
if data.onBack then
if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then
WarMenu.CloseMenu()
Wait(10)
data.onBack()
end
end
for k in pairs(Menu) do
local pressed = WarMenu.Button(Menu[k].header)
if not Menu[k].header then
Menu[k].header = Menu[k].txt
Menu[k].txt = nil
end
if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then
if Menu[k].disabled or Menu[k].isMenuHeader then
WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true)
else
WarMenu.ToolTip(
(Menu[k].blip and "~BLIP_".."8".."~ " or "")..
Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18,
true)
end
end
if pressed and not Menu[k].isMenuHeader then
WarMenu.CloseMenu()
close = false
Wait(10)
Menu[k].onSelect()
end
end
WarMenu.End()
else
return
end
if not WarMenu.IsAnyMenuOpened() and close then
stopTempCam(cam)
if data.onExit then data.onExit() end
end
Wait(0)
end
end)
end,
["esx"] =
function(Menu, data)
for k in pairs(Menu) do
Menu[k].label = Menu[k].header
Menu[k].name = "button"..k
end
if data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
label = "Close",
name = "close",
onSelect = data.onExit,
})
end
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
label = "Return",
name = "return",
onSelect = data.onBack,
})
end
ESX.UI.Menu.Open("default", getScript(), "Example_Menu", {
title = data.header,
align = 'top-right',
elements = Menu,
},
function(menuData, menu)
for k in pairs(Menu) do
if menuData.current.name == Menu[k].name then
menu.close()
Wait(10)
Menu[k].onSelect()
end
end
end,
function(data, menu)
menu.close()
end)
end,
["lation"] =
function(Menu, data)
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
onSelect = data.onBack,
header = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
exports.lation_ui:registerMenu({
id = 'menu',
title = data.header,
onExit = data.onExit and data.onExit or nil,
subtitle = (data.headertxt and data.headertxt or ""),
options = Menu,
})
-- Show menu
exports.lation_ui:showMenu('menu')
end,
}
--- Opens a menu using the configured menu system. --- 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`. --- This function translates code and creates menus in several different menu scripts, depending on the configured menu system specified in `Config.System.Menu`.
@@ -47,255 +304,22 @@
--- }) --- })
--- ``` --- ```
function openMenu(Menu, data) function openMenu(Menu, data)
if Config.System.Menu == "ox" then contextFunc[Config.System.Menu](Menu, data)
local index = nil
if data.onBack and not data.onSelected then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
title = "Return",
onSelect = data.onBack,
label = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
local menuID = 'Menu'
(data.onSelected and lib.registerMenu or lib.registerContext)({
id = menuID,
title = data.header..br..br..(data.headertxt and data.headertxt or ""),
position = 'top-right',
options = Menu,
canClose = data.canClose and data.canClose or nil,
onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil,
onExit = data.onExit and data.onExit or nil,
onSelected = data.onSelected and (function(selected) index = selected end) or nil,
}, data.onSelected and (function(x, y, args)
if Menu[x].refresh then
if Menu[x].onSelect then
Menu[x].onSelect()
end
lib.showMenu(menuID, index)
else
if Menu[x].onSelect then
Menu[x].onSelect()
else
lib.showMenu(menuID, index)
end
end
end) or nil)
if data.onSelected then
lib.showMenu(menuID, 1)
else
lib.showContext(menuID)
end
elseif Config.System.Menu == "qb" then
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
header = " ",
txt = "Return",
params = {
isAction = true,
event = data.onBack,
},
})
elseif data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
header = " ",
txt = "Close",
params = {
isAction = true,
event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end),
},
})
end
if data.header ~= nil then
local tempMenu = {}
for k, v in pairs(Menu) do tempMenu[k + 1] = v end
tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true }
Menu = tempMenu
end
for k in pairs(Menu) do
if not Menu[k].params or not Menu[k].params.event then
Menu[k].params = {
isAction = true,
event = Menu[k].onSelect or function() end,
}
end
if not Menu[k].header then Menu[k].header = " " end
if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end
Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable
end
exports[QBMenuExport]:openMenu(Menu)
elseif Config.System.Menu == "gta" then
WarMenu.CreateMenu(tostring(Menu), data.header, " ", {
titleColor = { 222, 255, 255 },
maxOptionCountOnScreen = 15,
width = 0.25,
x = 0.7,
})
if WarMenu.IsAnyMenuOpened() then return end
WarMenu.OpenMenu(tostring(Menu))
CreateThread(function()
local close = true
while true do
if WarMenu.Begin(tostring(Menu)) then
if data.onBack then
if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then
WarMenu.CloseMenu()
Wait(10)
data.onBack()
end
end
for k in pairs(Menu) do
local pressed = WarMenu.Button(Menu[k].header)
if not Menu[k].header then
Menu[k].header = Menu[k].txt
Menu[k].txt = nil
end
if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then
if Menu[k].disabled or Menu[k].isMenuHeader then
WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true)
else
WarMenu.ToolTip(
(Menu[k].blip and "~BLIP_".."8".."~ " or "")..
Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18,
true)
end
end
if pressed and not Menu[k].isMenuHeader then
WarMenu.CloseMenu()
close = false
Wait(10)
Menu[k].onSelect()
end
end
WarMenu.End()
else
return
end
if not WarMenu.IsAnyMenuOpened() and close then
stopTempCam(cam)
if data.onExit then data.onExit() end
end
Wait(0)
end
end)
elseif Config.System.Menu == "esx" then
for k in pairs(Menu) do
Menu[k].label = Menu[k].header
Menu[k].name = "button"..k
end
if data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
label = "Close",
name = "close",
onSelect = data.onExit,
})
end
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
label = "Return",
name = "return",
onSelect = data.onBack,
})
end
ESX.UI.Menu.Open("default", getScript(), "Example_Menu", {
title = data.header,
align = 'top-right',
elements = Menu,
},
function(menuData, menu)
for k in pairs(Menu) do
if menuData.current.name == Menu[k].name then
menu.close()
Wait(10)
Menu[k].onSelect()
end
end
end,
function(data, menu)
menu.close()
end)
elseif Config.System.Menu == "lation" then
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
onSelect = data.onBack,
header = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
exports.lation_ui:registerMenu({
id = 'menu',
title = data.header,
onExit = data.onExit and data.onExit or nil,
subtitle = (data.headertxt and data.headertxt or ""),
options = Menu,
})
-- Show menu
exports.lation_ui:showMenu('menu')
end
end 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. --- 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. --- 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. --- @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 "" Exports.JPRInv or ""
RSGExport, RSGInv = Exports.RSGExport or "", Exports.RSGInv or "" RSGExport, RSGInv = Exports.RSGExport or "", Exports.RSGInv or ""
VorpExport, VorpInv = Exports.VorpExport or "", Exports.VorpInv or ""
QBMenuExport = Exports.QBMenuExport or "" QBMenuExport = Exports.QBMenuExport or ""
QBTargetExport, OXTargetExport = Exports.QBTargetExport or "", Exports.OXTargetExport 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() Core = Core or exports[QBExport]:GetCoreObject()
elseif isStarted(RSGExport) then elseif isStarted(RSGExport) then
Core = Core or exports[RSGExport]:GetCoreObject() Core = Core or exports[RSGExport]:GetCoreObject()
elseif isStarted(VorpExport) then
Core = Core or exports[VorpExport]:GetCore()
end end

View File

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

View File

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

View File

@@ -201,7 +201,7 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
for k, v in pairs(items) do 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) debugPrint("^6Bridge^7: ^2Removing ^3"..QSInv.." ^2Stash item^7:", k, v)
end end
end, end,
@@ -291,7 +291,7 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
for k, v in pairs(items) do 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) debugPrint("^6Bridge^7: ^2Removing ^3"..CoreInv.." ^2Stash item^7:", k, v)
end end
end, end,
@@ -381,7 +381,7 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
for k, v in pairs(items) do 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) debugPrint("^6Bridge^7: ^2Removing ^3"..OrigenInv.." ^2Stash item^7:", k, v)
end end
end, end,
@@ -494,7 +494,7 @@ local InvFunc = {
end end
end end
end end
exports[CodeMInv]:UpdateStash(stashName, stashItems) exports[CodeMInv]:UpdateStash(stashName[1], stashItems)
end, end,
registerStash = registerStash =
function(name, label, slots, weight, owner, coords) function(name, label, slots, weight, owner, coords)
@@ -588,8 +588,8 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
for k, v in pairs(items) do for k, v in pairs(items) do
local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName, k) local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName[1], k)
exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName, k, v, itemData.slot, nil) exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName[1], k, v, itemData.slot, nil)
debugPrint("^6Bridge^7: ^2Removing ^3"..TgiannInv.." ^2Stash item^7:", k, v) debugPrint("^6Bridge^7: ^2Removing ^3"..TgiannInv.." ^2Stash item^7:", k, v)
end end
end, end,
@@ -743,7 +743,7 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
if not stashItems or not next(stashItems) then if not stashItems or not next(stashItems) then
stashItems = getStash(stashName) stashItems = getStash(stashName[1])
end end
for k, v in pairs(items) do for k, v in pairs(items) do
for l in pairs(stashItems) do for l in pairs(stashItems) do
@@ -757,9 +757,9 @@ local InvFunc = {
end end
end 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', { 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) ['items'] = json.encode(stashItems)
}) })
end, end,
@@ -924,12 +924,12 @@ local InvFunc = {
function(stashItems, stashName, items) function(stashItems, stashName, items)
if checkExportExists(QBInv, "RemoveItem") then if checkExportExists(QBInv, "RemoveItem") then
for k, v in pairs(items) do 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) debugPrint("^6Bridge^7: ^2Removing ^3"..QBInv.." ^2Stash item^7:", k, v)
end end
else else
if not stashItems or not next(stashItems) then if not stashItems or not next(stashItems) then
stashItems = getStash(stashName) stashItems = getStash(stashName[1])
end end
for k, v in pairs(items) do for k, v in pairs(items) do
for l in pairs(stashItems) do for l in pairs(stashItems) do
@@ -943,9 +943,9 @@ local InvFunc = {
end end
end 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', { 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) ['items'] = json.encode(stashItems)
}) })
end end
@@ -1105,7 +1105,7 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
if not stashItems or not next(stashItems) then if not stashItems or not next(stashItems) then
stashItems = getStash(stashName) stashItems = getStash(stashName[1])
end end
for k, v in pairs(items) do for k, v in pairs(items) do
for l in pairs(stashItems) do for l in pairs(stashItems) do
@@ -1119,9 +1119,9 @@ local InvFunc = {
end end
end 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', { 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) ['items'] = json.encode(stashItems)
}) })
end, end,
@@ -1244,7 +1244,7 @@ local InvFunc = {
stashRemoveItem = stashRemoveItem =
function(stashItems, stashName, items) function(stashItems, stashName, items)
for k, v in pairs(items) do 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) debugPrint("^6Bridge^7: ^2Removing ^3"..RSGInv.." ^2Stash item^7:", k, v)
end end
end, end,
@@ -1585,7 +1585,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount,
end end
else 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 end
else else
@@ -1610,7 +1610,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount,
ESX.GetPlayerFromId(src).addInventoryItem(item, amountToAdd) ESX.GetPlayerFromId(src).addInventoryItem(item, amountToAdd)
end end
else 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 end
end) end)
@@ -1951,6 +1951,7 @@ end
--- if doesItemExist(item) then print("item exists") end --- if doesItemExist(item) then print("item exists") end
--- ``` --- ```
function doesItemExist(item) function doesItemExist(item)
local item = type(item) == "string" and item or tostring(item)
if not item or item == "" then if not item or item == "" then
return false return false
end end
@@ -1965,16 +1966,18 @@ end
function getItemLabel(item) function getItemLabel(item)
local item = type(item) == "string" and item or tostring(item)
if not item or item == "" then if not item or item == "" then
return "" return ""
end end
if not Items or not next(Items) then if not Items or not next(Items) then
return item.." (Missing)" return item.." (Missing item error)"
end end
if Items[item] ~= nil then if Items[item] ~= nil then
return Items[item].label return Items[item].label
end end
return item.." (Missing)" return item.." (Missing item error)"
end end
@@ -2280,7 +2283,7 @@ function stashRemoveItem(stashItems, stashName, items)
for i = 1, #InvFunc do for i = 1, #InvFunc do
local inv = InvFunc[i] local inv = InvFunc[i]
if isStarted(inv.invName) then if isStarted(inv.invName) then
inv.stashRemoveItem(stashItems, stashName[1], items) inv.stashRemoveItem(stashItems, stashName, items)
return return
end end
end end
@@ -2476,7 +2479,7 @@ function sellMenu(data)
Menu[#Menu + 1] = { Menu[#Menu + 1] = {
isMenuHeader = not hasTable[k].hasItem, isMenuHeader = not hasTable[k].hasItem,
icon = invImg(k), 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"] 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", or "Sell ALL at $"..v.." each",
onSelect = function() onSelect = function()
@@ -2534,7 +2537,7 @@ end
--- ``` --- ```
function sellAnim(data, token) function sellAnim(data, token)
if not hasItem(data.item, 1) then 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 return
end end
@@ -2596,7 +2599,7 @@ RegisterNetEvent(getScript()..":Sellitems", function(data, token)
print((hasTable[data.item].count * data.price), data.price) print((hasTable[data.item].count * data.price), data.price)
fundPlayer((hasTable[data.item].count * data.price), "cash", src) fundPlayer((hasTable[data.item].count * data.price), "cash", src)
else 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
end) end)

View File

@@ -65,7 +65,6 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
model = data.model model = data.model
loadModel(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) 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 -- 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) 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 end
unloadModel(model) unloadModel(model)
Peds[keyGen()..keyGen()] = ped Peds[keyGen()..keyGen()] = ped
if fade ~= false then if fade ~= false and gameName ~= "rdr3" then
SetEntityAlpha(ped, 0, false)
CreateThread(function() CreateThread(function()
fadeInEnt(ped) fadeInEnt(ped)
end) end)

View File

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

View File

@@ -4,220 +4,236 @@ local storedPID = nil
local progressFunc = { local progressFunc = {
ox = { ox = {
start = function(data) start =
local options = { function(data)
duration = debugMode and 1000 or data.time, local options = {
label = data.label, duration = debugMode and 1000 or data.time,
position = data.position or "bottom", label = data.label,
useWhileDead = data.dead or false, position = data.position or "bottom",
canCancel = data.cancel or true, useWhileDead = data.dead or false,
anim = { canCancel = data.cancel or true,
dict = data.dict, anim = {
clip = data.anim, dict = data.dict,
flag = (data.flag == 8 and 32 or data.flag) or nil, clip = data.anim,
scenario = data.task flag = (data.flag == 8 and 32 or data.flag) or nil,
}, scenario = data.task
disable = { },
combat = data.combat or true, disable = {
move = data.disableMovement or false, combat = data.combat or true,
car = data.disableMovement or false, move = data.disableMovement or false,
mouse = data.mouse or false car = data.disableMovement or false,
}, mouse = data.mouse or false
} },
if data.prop and data.prop.model then
options.prop = {
model = data.prop.model,
pos = data.prop.pos or vec3(0, 0, 0),
rot = data.prop.rot or vec3(0, 0, 0),
bone = data.prop.bone or 0
} }
end if data.prop and data.prop.model then
if data.propTwo and data.propTwo.model then options.prop = {
options.propTwo = { model = data.prop.model,
model = data.propTwo.model, pos = data.prop.pos or vec3(0, 0, 0),
pos = data.propTwo.pos or vec3(0, 0, 0), rot = data.prop.rot or vec3(0, 0, 0),
rot = data.propTwo.rot or vec3(0, 0, 0), bone = data.prop.bone or 0
bone = data.propTwo.bone or 0 }
}
end
if data.progressType == "circle" then
if exports[OXLibExport]:progressCircle(options) then
return true
else
return false
end end
end if data.propTwo and data.propTwo.model then
if not data.progressType or data.progressType == "bar" then options.propTwo = {
if exports[OXLibExport]:progressBar(options) then model = data.propTwo.model,
return true pos = data.propTwo.pos or vec3(0, 0, 0),
else rot = data.propTwo.rot or vec3(0, 0, 0),
return false bone = data.propTwo.bone or 0
}
end end
end if data.progressType == "circle" then
end, if exports[OXLibExport]:progressCircle(options) then
stop = function() return true
exports[OXLibExport]:cancelProgress() else
end, return false
end
end
if not data.progressType or data.progressType == "bar" then
if exports[OXLibExport]:progressBar(options) then
return true
else
print("^1progressBar was not successful")
end
end
end,
stop =
function()
exports[OXLibExport]:cancelProgress()
end,
}, },
qb = { qb = {
start = function(data) start =
Core.Functions.Progressbar("progbar", function(data)
data.label, local p = promise.new()
debugMode and 1000 or data.time, Core.Functions.Progressbar("progbar",
data.dead or false, data.label,
data.cancel or true, debugMode and 1000 or data.time,
{ data.dead or false,
disableMovement = data.disableMovement or false, data.cancel or true,
disableCarMovement = data.disableMovement or false, {
disableMouse = data.disableMouse or false, disableMovement = data.disableMovement or false,
disableCombat = data.disableCombat or true, disableCarMovement = data.disableMovement or false,
}, disableMouse = data.disableMouse or false,
{ disableCombat = data.disableCombat or true,
animDict = data.dict, },
anim = data.anim, {
flags = data.flag or 32, animDict = data.dict,
task = data.task anim = data.anim,
}, flags = data.flag or 32,
{}, {}, task = data.task
function() },
return true {}, {},
end, function() function() p:resolve(true) end,
return false function() p:resolve(false) end,
end, data.icon) data.icon)
return Citizen.Await(p)
end,
stop =
function()
TriggerEvent("progressbar:client:cancel")
end, end,
stop = function()
TriggerEvent("progressbar:client:cancel")
end,
}, },
qs = { qs = {
start = function(data) start =
if exports['qs-interface']:ProgressBar({ function(data)
duration = debugMode and 1000 or data.time, if exports['qs-interface']:ProgressBar({
label = data.label, duration = debugMode and 1000 or data.time,
position = 'bottom', label = data.label,
useWhileDead = data.dead or false, position = 'bottom',
canCancel = data.cancel or true, useWhileDead = data.dead or false,
disable = data.disableMovement or false, canCancel = data.cancel or true,
anim = { disable = data.disableMovement or false,
dict = data.dict, anim = {
clip = data.anim, dict = data.dict,
flag = data.flag or 32 clip = data.anim,
}, flag = data.flag or 32
prop = nil },
}) then prop = nil
return true }) then
else return true
return false else
end return false
end, end
stop = function() end,
--?? stop =
TriggerEvent("progressbar:client:cancel") function()
end, --??
TriggerEvent("progressbar:client:cancel")
end,
}, },
esx = { esx = {
start = function(data) start =
ESX.Progressbar(data.label, debugMode and 1000 or data.time, { function(data)
FreezePlayer = true, local p = promise.new()
animation = { ESX.Progressbar(data.label, debugMode and 1000 or data.time, {
type = data.anim, FreezePlayer = true,
dict = data.dict, animation = {
scenario = data.task, type = data.anim,
}, dict = data.dict,
onFinish = function() scenario = data.task,
return true },
end, onFinish = function()
onCancel = function() p:resolve(true)
return false end,
end onCancel = function()
}) p:resolve(false)
end, return false
stop = function() end
ESX.CancelProgressbar() })
end, return Citizen.Await(p)
end,
stop =
function()
ESX.CancelProgressbar()
end,
}, },
lation = { lation = {
start = function(data) start =
if exports.lation_ui:progressBar({ function(data)
label = data.label, if exports.lation_ui:progressBar({
description = nil, label = data.label,
duration = debugMode and 1000 or data.time, description = nil,
icon = data.icon, duration = debugMode and 1000 or data.time,
useWhileDead = data.dead or false, icon = data.icon,
disable = { useWhileDead = data.dead or false,
combat = data.combat or true, disable = {
move = data.disableMovement or false, combat = data.combat or true,
car = data.disableMovement or false, move = data.disableMovement or false,
}, car = data.disableMovement or false,
anim = { },
dict = data.dict, anim = {
clip = data.anim, dict = data.dict,
flag = data.flag clip = data.anim,
}, flag = data.flag
prop = { },
model = data.prop and data.prop.model, prop = {
pos = data.prop and (data.prop.pos or vec3(0, 0, 0)), model = data.prop and data.prop.model,
rot = data.prop and (data.prop.rot or vec3(0, 0, 0)), pos = data.prop and (data.prop.pos or vec3(0, 0, 0)),
bone = data.prop and (data.prop.bone or 0) rot = data.prop and (data.prop.rot or vec3(0, 0, 0)),
} bone = data.prop and (data.prop.bone or 0)
}) then }
return true }) then
else return true
return false else
end return false
end, end
stop = function() end,
exports.lation_ui:cancelProgress() stop =
end, function()
exports.lation_ui:cancelProgress()
end,
}, },
red = { red = {
start = function(data) start =
if exports.jim_bridge:redProgressBar({ function(data)
label = data.label, if exports.jim_bridge:redProgressBar({
time = debugMode and 1000 or data.time, label = data.label,
dict = data.dict, time = debugMode and 1000 or data.time,
anim = data.anim, dict = data.dict,
flag = data.flag or 32, anim = data.anim,
task = data.task, flag = data.flag or 32,
disableMovement = data.disableMovement or false, task = data.task,
cancel = data.cancel or true, disableMovement = data.disableMovement or false,
}) then cancel = data.cancel or true,
return true }) then
else return true
return false else
end return false
end, end
stop = function() end,
exports.jim_bridge:stopProgressBar() stop =
end, function()
exports.jim_bridge:stopProgressBar()
end,
}, },
gta = { gta = {
start = function(data) start =
function(data)
if exports.jim_bridge:gtaProgressBar({ if exports.jim_bridge:gtaProgressBar({
label = data.label, label = data.label,
time = debugMode and 1000 or data.time, time = debugMode and 1000 or data.time,
dict = data.dict, dict = data.dict,
anim = data.anim, anim = data.anim,
flag = data.flag or 32, flag = data.flag or 32,
task = data.task, task = data.task,
disableMovement = data.disableMovement or false, disableMovement = data.disableMovement or false,
cancel = data.cancel or true, cancel = data.cancel or true,
}) then }) then
return true return true
else else
return false return false
end end
end, end,
stop = function() stop =
exports.jim_bridge:stopProgressBar() function()
end, exports.jim_bridge:stopProgressBar()
end,
}, },
} }
@@ -254,7 +270,11 @@ local progressFunc = {
--- cancel = true, --- cancel = true,
--- }) --- })
--- ``` --- ```
local progresssBarActive = false
function progressBar(data) function progressBar(data)
if progresssBarActive then return else progresssBarActive = true end
local ped = PlayerPedId() local ped = PlayerPedId()
if data.shared then if data.shared then
debugPrint("^6Bridge^7: ^6Sharing progressBar to player^7: ^6"..data.shared.pid.."^7") debugPrint("^6Bridge^7: ^6Sharing progressBar to player^7: ^6"..data.shared.pid.."^7")
@@ -273,6 +293,7 @@ function progressBar(data)
while result == nil do while result == nil do
Wait(10) Wait(10)
end end
debugPrint("^5Debug^7: ^2ProgressBar result^7: ^3"..tostring(result).."^7")
-- Cleanup -- Cleanup
FreezeEntityPosition(ped, false) FreezeEntityPosition(ped, false)
@@ -293,6 +314,7 @@ function progressBar(data)
TriggerServerEvent(getScript()..":clearAuthToken") TriggerServerEvent(getScript()..":clearAuthToken")
currentToken = triggerCallback(AuthEvent) currentToken = triggerCallback(AuthEvent)
end end
progresssBarActive = false
return result return result
end end
@@ -301,6 +323,7 @@ end
--- This function cancels the progress bar based on the configured progress bar system, handling any necessary cleanup. --- This function cancels the progress bar based on the configured progress bar system, handling any necessary cleanup.
function stopProgressBar() function stopProgressBar()
progressFunc[Config.System.ProgressBar].stop() progressFunc[Config.System.ProgressBar].stop()
progresssBarActive = false
end end
-- System to handle sending/sharing progress bars between players -- -- System to handle sending/sharing progress bars between players --

View File

@@ -21,24 +21,56 @@
--- }) --- })
--- ``` --- ```
function registerCommand(command, options) 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 = "" local commandResource = ""
if isStarted(OXLibExport) then if isStarted(OXLibExport) then
commandResource = OXLibExport 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 elseif isStarted(QBExport) and not isStarted(QBXExport) then
commandResource = QBExport 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 elseif isStarted(RSGExport) then
commandResource = RSGExport 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 elseif isStarted(ESXExport) then
commandResource = ESXExport commandResource = ESXExport
ESX.RegisterCommand(command, options[5] or 'admin', function(xPlayer, args, showError) ESX.RegisterCommand(command,
options[4](xPlayer.source, args, showError) optionTable.restriction or 'admin',
end, false, { help = options[1] }) function(xPlayer, args, showError)
optionTable.funct(xPlayer.source, args, showError)
end,
false,
{ help = options[1] }
)
end end
if commandResource ~= "" then if commandResource ~= "" then

View File

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

View File

@@ -53,27 +53,11 @@ function redProgressBar(data)
Wait(0) Wait(0)
local elapsed = GetGameTimer() local elapsed = GetGameTimer()
local percentage = ((elapsed - (endTime - wait)) / wait) * 100 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 -- Draw your segmented progress bar
ShowRedProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage)) ShowRedProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
-- Controls to disable during progress -- Controls to disable during progress
if data.disableMouse then if data.disableMouse then
@@ -151,40 +135,48 @@ function redProgressBar(data)
return result return result
end end
function ShowRedProgressBar(currentProg, title, level) function ShowRedProgressBar(percentage, title, level)
local loc = vec2(0.40, 0.90) local loc = vec2(0.40, 0.90)
local size = vec2(0.3, 0.03) 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 -- 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) 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) SetTextFontForCurrentCommand(6)
SetTextScale(0.35, 0.35) SetTextScale(0.35, 0.35)
SetTextColor(255, 255, 255, 255) SetTextColor(255, 255, 255, 255)
SetTextDropshadow(1, 0, 0, 0, 200) SetTextDropshadow(1, 0, 0, 0, 200)
BgDisplayText(title, loc.x - size.x / 4 + 0.074, loc.y - 0.034) BgDisplayText(title, loc.x - size.x / 4 + 0.074, loc.y - 0.034)
-- Percentage (right)
SetTextFontForCurrentCommand(1) SetTextFontForCurrentCommand(1)
SetTextScale(0.35, 0.35) SetTextScale(0.35, 0.35)
SetTextColor(255, 255, 255, 255) SetTextColor(255, 255, 255, 255)
SetTextDropshadow(1, 0, 0, 0, 200) SetTextDropshadow(1, 0, 0, 0, 200)
BgDisplayText(level, loc.x - size.x / 4 + 0.246, loc.y - 0.030) 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) -- Track (bg)
local gap = segmentWidth / #currentProg -- Smaller gap between segments DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
for i = 1, #currentProg do -- Fill
local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap) local fillWidth = barWidth * (percentage / 100.0)
local fillPercentage = currentProg[i] if fillWidth > 0.0 then
local progressBarWidth = segmentWidth * (fillPercentage / 100) local fillCenter = barLeft + (fillWidth / 2.0)
DrawRect(fillCenter, loc.y, fillWidth, barHeight, 255, 0, 0, 200) -- red fill
end
-- Semi-transparent background for each segment -- Tick lines (9 inner lines for 10 segments)
DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255) for i = 1, (tickCount - 1) do
local x = barLeft + (barWidth * (i / tickCount))
-- Filling progress for each segment DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
if progressBarWidth > 0 then
DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 255, 0, 0, 200) -- Blue progress
end
end end
end end
@@ -213,29 +205,14 @@ function gtaProgressBar(data)
local elapsed = GetGameTimer() local elapsed = GetGameTimer()
local percentage = ((elapsed - (endTime - wait)) / wait) * 100 local percentage = ((elapsed - (endTime - wait)) / wait) * 100
-- Convert to segmented progress (assuming 5 segments here) if percentage < 0 then percentage = 0 end
local segments = 1 -- Number of segments in the bar if percentage > 100 then percentage = 100 end
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 -- Draw your segmented progress bar
ShowGTAProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage)) ShowGTAProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
-- Controls to disable during progress -- Controls to disable during progress
DisablePlayerFiring(ped, true) DisablePlayerFiring(PlayerId(), true)
DisableControlAction(0, 25, true) -- Disable aim DisableControlAction(0, 25, true) -- Disable aim
DisableControlAction(0, 21, true) -- Disable sprint DisableControlAction(0, 21, true) -- Disable sprint
DisableControlAction(0, 30, true) -- Disable move left/right DisableControlAction(0, 30, true) -- Disable move left/right
@@ -246,13 +223,15 @@ function gtaProgressBar(data)
inProgress = false inProgress = false
end end
end end
result = inProgress
end) 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 -- Cleanup animations/tasks
if data.dict then if data.dict then
stopAnim(data.dict, data.anim, ped) stopAnim(data.dict, data.anim, ped)
@@ -261,10 +240,6 @@ function gtaProgressBar(data)
ClearPedTasks(ped) ClearPedTasks(ped)
end end
result = inProgress
inProgress = false
while result == nil do Wait(10) end
-- Cleanup -- Cleanup
FreezeEntityPosition(ped, false) FreezeEntityPosition(ped, false)
@@ -276,14 +251,22 @@ function gtaProgressBar(data)
return result return result
end end
function ShowGTAProgressBar(currentProg, title, level) function ShowGTAProgressBar(percentage, title, level)
local loc = vec2(0.37, 0.90) local loc = vec2(0.37, 0.90)
local size = vec2(0.3, 0.03) 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 -- 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.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) 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) SetTextFont(0)
SetTextProportional(1) SetTextProportional(1)
SetTextScale(0.35, 0.35) SetTextScale(0.35, 0.35)
@@ -293,34 +276,35 @@ function ShowGTAProgressBar(currentProg, title, level)
SetTextOutline() SetTextOutline()
SetTextEntry("STRING") SetTextEntry("STRING")
AddTextComponentString(title) 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) SetTextFont(0)
SetTextProportional(1) SetTextProportional(1)
SetTextScale(0.35, 0.25) SetTextScale(0.35, 0.25)
SetTextColour(255, 255, 255, 255) SetTextColour(255, 255, 255, 255)
SetTextEntry("STRING") SetTextEntry("STRING")
AddTextComponentString(level) 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) -- Track (bg)
local gap = segmentWidth / #currentProg -- Smaller gap between segments DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
for i = 1, #currentProg do -- Fill
local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap) local fillWidth = barWidth * (percentage / 100.0)
local fillPercentage = currentProg[i] if fillWidth > 0.0 then
local progressBarWidth = segmentWidth * (fillPercentage / 100) local fillCenter = barLeft + (fillWidth / 2.0)
DrawRect(fillCenter, loc.y, fillWidth, barHeight, 93, 182, 229, 255) -- GTA blue
end
-- Semi-transparent background for each segment -- Tick lines
DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255) for i = 1, (tickCount - 1) do
local x = barLeft + (barWidth * (i / tickCount))
-- Filling progress for each segment DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
if progressBarWidth > 0 then
DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 93, 182, 229, 255) -- Blue progress
end
end end
end end
function stopProgressBar() inProgress = false end function stopProgressBar() inProgress = false end
function isProgressBar() return inProgress end function isProgressBar() return inProgress end

View File

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