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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,100 +1,111 @@
function parseVersion(version)
local parts = {}
for num in version:gmatch("%d+") do
table.insert(parts, tonumber(num))
local function readBoolMeta(key, default)
local val = GetResourceMetadata("jim_bridge", key, 0)
if not val then return default end
val = tostring(val):lower()
return (val == 'true' or val == '1' or val == 'yes' or val == 'on')
end
local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false)
if not SUPPRESS_UPDATES then
function parseVersion(version)
local parts = {}
for num in version:gmatch("%d+") do
table.insert(parts, tonumber(num))
end
return parts
end
return parts
end
function compareVersions(current, newest)
local currentParts = parseVersion(current)
local newestParts = parseVersion(newest)
for i = 1, math.max(#currentParts, #newestParts) do
local c = currentParts[i] or 0
local n = newestParts[i] or 0
if c < n then return -1
elseif c > n then return 1 end
function compareVersions(current, newest)
local currentParts = parseVersion(current)
local newestParts = parseVersion(newest)
for i = 1, math.max(#currentParts, #newestParts) do
local c = currentParts[i] or 0
local n = newestParts[i] or 0
if c < n then return -1
elseif c > n then return 1 end
end
return 0
end
return 0
end
function capitalize(str)
return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end))
end
function capitalize(str)
return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end))
end
local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or ""
local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or ""
print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7")
print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7")
function CheckVersion()
if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
CreateThread(function()
Wait(4000)
local script = getScript()
local currentVersionRaw = GetResourceMetadata(script, 'version')
function CheckVersion()
if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
CreateThread(function()
Wait(4000)
local script = getScript()
local currentVersionRaw = GetResourceMetadata(script, 'version')
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers)
if not newestVersionRaw then
-- fallback
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers)
if not fallbackVersionRaw then
print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)")
return
end
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers)
if not newestVersionRaw then
-- fallback
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers)
if not fallbackVersionRaw then
print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)")
return
end
local lines = {}
for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, fallbackVersion)
if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)")
if #changelog > 0 then
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
end
end)
else
local lines = {}
for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "")
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, fallbackVersion)
local compareResult = compareVersions(currentVersionRaw, newestVersion)
if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)")
if #changelog > 0 then
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function() CheckVersion() end)
SetTimeout(3600000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
end
end)
else
local lines = {}
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, newestVersion)
if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)")
if #changelog > 0 then
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(3600000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
end
end
end)
end)
end)
end
end
end
CheckVersion()
CheckVersion()
end

View File

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

View File

@@ -10,6 +10,263 @@
• esx (using ESX.UI.Menu)
]]
local contextFunc = {
["ox"] =
function(Menu, data)
local index = nil
if data.onBack and not data.onSelected then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
title = "Return",
onSelect = data.onBack,
label = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
local menuID = 'Menu'
(data.onSelected and lib.registerMenu or lib.registerContext)({
id = menuID,
title = data.header..br..br..(data.headertxt and data.headertxt or ""),
position = 'top-right',
options = Menu,
canClose = data.canClose and data.canClose or nil,
onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil,
onExit = data.onExit and data.onExit or nil,
onSelected = data.onSelected and (function(selected) index = selected end) or nil,
}, data.onSelected and (function(x, y, args)
if Menu[x].refresh then
if Menu[x].onSelect then
Menu[x].onSelect()
end
lib.showMenu(menuID, index)
else
if Menu[x].onSelect then
Menu[x].onSelect()
else
lib.showMenu(menuID, index)
end
end
end) or nil)
if data.onSelected then
lib.showMenu(menuID, 1)
else
lib.showContext(menuID)
end
end,
["qb"] =
function(Menu, data)
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
header = " ",
txt = "Return",
params = {
isAction = true,
event = data.onBack,
},
})
elseif data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
header = " ",
txt = "Close",
params = {
isAction = true,
event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end),
},
})
end
if data.header ~= nil then
local tempMenu = {}
for k, v in pairs(Menu) do tempMenu[k + 1] = v end
tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true }
Menu = tempMenu
end
for k in pairs(Menu) do
if not Menu[k].params or not Menu[k].params.event then
Menu[k].params = {
isAction = true,
event = Menu[k].onSelect or function() end,
}
end
if not Menu[k].header then Menu[k].header = " " end
if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end
Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable
end
exports[QBMenuExport]:openMenu(Menu)
end,
["gta"] =
function(Menu, data)
WarMenu.CreateMenu(tostring(Menu), data.header, " ", {
titleColor = { 222, 255, 255 },
maxOptionCountOnScreen = 15,
width = 0.25,
x = 0.7,
})
if WarMenu.IsAnyMenuOpened() then return end
WarMenu.OpenMenu(tostring(Menu))
CreateThread(function()
local close = true
while true do
if WarMenu.Begin(tostring(Menu)) then
if data.onBack then
if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then
WarMenu.CloseMenu()
Wait(10)
data.onBack()
end
end
for k in pairs(Menu) do
local pressed = WarMenu.Button(Menu[k].header)
if not Menu[k].header then
Menu[k].header = Menu[k].txt
Menu[k].txt = nil
end
if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then
if Menu[k].disabled or Menu[k].isMenuHeader then
WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true)
else
WarMenu.ToolTip(
(Menu[k].blip and "~BLIP_".."8".."~ " or "")..
Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18,
true)
end
end
if pressed and not Menu[k].isMenuHeader then
WarMenu.CloseMenu()
close = false
Wait(10)
Menu[k].onSelect()
end
end
WarMenu.End()
else
return
end
if not WarMenu.IsAnyMenuOpened() and close then
stopTempCam(cam)
if data.onExit then data.onExit() end
end
Wait(0)
end
end)
end,
["esx"] =
function(Menu, data)
for k in pairs(Menu) do
Menu[k].label = Menu[k].header
Menu[k].name = "button"..k
end
if data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
label = "Close",
name = "close",
onSelect = data.onExit,
})
end
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
label = "Return",
name = "return",
onSelect = data.onBack,
})
end
ESX.UI.Menu.Open("default", getScript(), "Example_Menu", {
title = data.header,
align = 'top-right',
elements = Menu,
},
function(menuData, menu)
for k in pairs(Menu) do
if menuData.current.name == Menu[k].name then
menu.close()
Wait(10)
Menu[k].onSelect()
end
end
end,
function(data, menu)
menu.close()
end)
end,
["lation"] =
function(Menu, data)
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
onSelect = data.onBack,
header = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
exports.lation_ui:registerMenu({
id = 'menu',
title = data.header,
onExit = data.onExit and data.onExit or nil,
subtitle = (data.headertxt and data.headertxt or ""),
options = Menu,
})
-- Show menu
exports.lation_ui:showMenu('menu')
end,
}
--- Opens a menu using the configured menu system.
---
--- This function translates code and creates menus in several different menu scripts, depending on the configured menu system specified in `Config.System.Menu`.
@@ -47,255 +304,22 @@
--- })
--- ```
function openMenu(Menu, data)
if Config.System.Menu == "ox" then
local index = nil
if data.onBack and not data.onSelected then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
title = "Return",
onSelect = data.onBack,
label = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
local menuID = 'Menu'
(data.onSelected and lib.registerMenu or lib.registerContext)({
id = menuID,
title = data.header..br..br..(data.headertxt and data.headertxt or ""),
position = 'top-right',
options = Menu,
canClose = data.canClose and data.canClose or nil,
onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil,
onExit = data.onExit and data.onExit or nil,
onSelected = data.onSelected and (function(selected) index = selected end) or nil,
}, data.onSelected and (function(x, y, args)
if Menu[x].refresh then
if Menu[x].onSelect then
Menu[x].onSelect()
end
lib.showMenu(menuID, index)
else
if Menu[x].onSelect then
Menu[x].onSelect()
else
lib.showMenu(menuID, index)
end
end
end) or nil)
if data.onSelected then
lib.showMenu(menuID, 1)
else
lib.showContext(menuID)
end
elseif Config.System.Menu == "qb" then
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
header = " ",
txt = "Return",
params = {
isAction = true,
event = data.onBack,
},
})
elseif data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
header = " ",
txt = "Close",
params = {
isAction = true,
event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end),
},
})
end
if data.header ~= nil then
local tempMenu = {}
for k, v in pairs(Menu) do tempMenu[k + 1] = v end
tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true }
Menu = tempMenu
end
for k in pairs(Menu) do
if not Menu[k].params or not Menu[k].params.event then
Menu[k].params = {
isAction = true,
event = Menu[k].onSelect or function() end,
}
end
if not Menu[k].header then Menu[k].header = " " end
if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end
Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable
end
exports[QBMenuExport]:openMenu(Menu)
elseif Config.System.Menu == "gta" then
WarMenu.CreateMenu(tostring(Menu), data.header, " ", {
titleColor = { 222, 255, 255 },
maxOptionCountOnScreen = 15,
width = 0.25,
x = 0.7,
})
if WarMenu.IsAnyMenuOpened() then return end
WarMenu.OpenMenu(tostring(Menu))
CreateThread(function()
local close = true
while true do
if WarMenu.Begin(tostring(Menu)) then
if data.onBack then
if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then
WarMenu.CloseMenu()
Wait(10)
data.onBack()
end
end
for k in pairs(Menu) do
local pressed = WarMenu.Button(Menu[k].header)
if not Menu[k].header then
Menu[k].header = Menu[k].txt
Menu[k].txt = nil
end
if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then
if Menu[k].disabled or Menu[k].isMenuHeader then
WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true)
else
WarMenu.ToolTip(
(Menu[k].blip and "~BLIP_".."8".."~ " or "")..
Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18,
true)
end
end
if pressed and not Menu[k].isMenuHeader then
WarMenu.CloseMenu()
close = false
Wait(10)
Menu[k].onSelect()
end
end
WarMenu.End()
else
return
end
if not WarMenu.IsAnyMenuOpened() and close then
stopTempCam(cam)
if data.onExit then data.onExit() end
end
Wait(0)
end
end)
elseif Config.System.Menu == "esx" then
for k in pairs(Menu) do
Menu[k].label = Menu[k].header
Menu[k].name = "button"..k
end
if data.canClose then
table.insert(Menu, 1, {
icon = "fas fa-circle-xmark",
label = "Close",
name = "close",
onSelect = data.onExit,
})
end
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
label = "Return",
name = "return",
onSelect = data.onBack,
})
end
ESX.UI.Menu.Open("default", getScript(), "Example_Menu", {
title = data.header,
align = 'top-right',
elements = Menu,
},
function(menuData, menu)
for k in pairs(Menu) do
if menuData.current.name == Menu[k].name then
menu.close()
Wait(10)
Menu[k].onSelect()
end
end
end,
function(data, menu)
menu.close()
end)
elseif Config.System.Menu == "lation" then
if data.onBack then
table.insert(Menu, 1, {
icon = "fas fa-circle-arrow-left",
onSelect = data.onBack,
header = "Return",
})
end
for k in pairs(Menu) do
if data.onSelected and Menu[k].arrow then
Menu[k].icon = "fas fa-angle-right"
end
-- If no title, use header or txt as title/label.
if not Menu[k].title then
if Menu[k].header ~= nil and Menu[k].header ~= "" then
Menu[k].title = Menu[k].header
Menu[k].label = Menu[k].header
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
else
Menu[k].title = Menu[k].txt
Menu[k].label = Menu[k].txt
end
end
-- Copy parameters from 'params' if available.
if Menu[k].params then
Menu[k].event = Menu[k].params.event
Menu[k].args = Menu[k].params.args or {}
end
if Menu[k].isMenuHeader then
Menu[k].readOnly = true
end
end
exports.lation_ui:registerMenu({
id = 'menu',
title = data.header,
onExit = data.onExit and data.onExit or nil,
subtitle = (data.headertxt and data.headertxt or ""),
options = Menu,
})
-- Show menu
exports.lation_ui:showMenu('menu')
end
contextFunc[Config.System.Menu](Menu, data)
end
-- Line break conversion table, basically, to automate the linebreak in functions `br`
-- This is declared and automatically sets it for what menu you are using (depends if one is uding MD formatting or HTML formatting)
local lineBreakConversion = {
["ox"] = "\n",
["gta"] = "\n",
["lation"] = "\n",
["qb"] = "<br>",
["esx"] = "<br>",
}
--- A line break constant used for menu header formatting.
br = (Config.System.Menu == "ox" or Config.System.Menu == "gta" or Config.System.Menu == "lation") and "\n" or "<br>"
br = lineBreakConversion[Config.System.Menu]
--- Checks if the current menu system is 'ox' or 'gta' for formatting purposes.
--- @return boolean boolean True if using ox or gta menus, otherwise false.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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