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