Compare commits

..

1 Commits

Author SHA1 Message Date
Jim Shield
d5af0334d5 Version Bump
`2.0.19` - `2.0.20`
2025-07-06 19:14:50 +01:00
50 changed files with 4719 additions and 7885 deletions

16
.gitattributes vendored
View File

@@ -1,16 +0,0 @@
# VCS / CI noise
/.gitattributes export-ignore
/.gitignore export-ignore
/.gitmodules export-ignore
/.github export-ignore
# Dev tooling/config
/.vscode export-ignore
*.code-workspace export-ignore
/.editorconfig export-ignore
/.eslintrc* export-ignore
/.prettier* export-ignore
# Omit release-only helpers
/server/restaurantchecker.lua export-ignore
/server/installitems.lua export-ignore

View File

@@ -1,20 +1,17 @@
name: Discord Commit Notifier
on:
push:
branches: ['*']
branches:
- '*' # triggers on all branches
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Validate event JSON (sanity check)
run: jq -e . "$GITHUB_EVENT_PATH" >/dev/null
- name: Send commit payload to Discord Bot
env:
URL: http://${{ secrets.DISCORDBOT }}:3000/github-commits
shell: bash
run: |
curl -sS --fail-with-body -X POST "$URL" \
-H 'Content-Type: application/json' \
--data-binary "@$GITHUB_EVENT_PATH"
- name: Discord Commits
uses: Sniddl/discord-commits@v1.6
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
template: "avatar-with-link"
include-extras: true

View File

@@ -1,71 +0,0 @@
name: Discord Release Notifier
on:
# Manual or UI-published releases
release:
types: [published]
# Releases created by your tag-driven workflow
workflow_run:
workflows: ["Package & Release"] # must match the 'name:' in release.yml
types: [completed]
permissions:
contents: read
jobs:
# 1) Handle manual / UI / non-workflow-created releases
notify-from-release:
if: github.event_name == 'release' && github.event.action == 'published'
runs-on: ubuntu-latest
steps:
- name: Send release payload to Discord Bot
env:
URL: http://${{ secrets.DISCORDBOT }}:3000/github-releases
run: |
curl -sS -X POST "$URL" \
-H 'Content-Type: application/json' \
--data-binary "@$GITHUB_EVENT_PATH"
# 2) Handle releases created by your 'Package & Release' workflow
# (which wont trigger `on: release` when using GITHUB_TOKEN)
notify-from-workflow-run:
if: >
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Fetch release JSON by tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
TAG="${{ github.event.workflow_run.head_branch }}"
# head_branch is the tag name for a tag push workflow_run
curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$OWNER/$REPO/releases/tags/$TAG" \
> release.json
- name: Wrap as 'release' event payload & notify bot
env:
URL: http://${{ secrets.DISCORDBOT }}:3000/github-releases
run: |
set -euo pipefail
jq -n --slurpfile rel release.json \
--arg repo "$GITHUB_REPOSITORY" \
--arg repo_url "https://github.com/$GITHUB_REPOSITORY" \
--arg action "published" '
{ action: $action,
repository: { full_name: $repo, html_url: $repo_url },
release: $rel[0],
sender: { login: "github-actions[bot]" } }' > payload.json
curl -sS -X POST "$URL" \
-H 'Content-Type: application/json' \
--data-binary "@payload.json"

View File

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

View File

@@ -1,78 +1,67 @@
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')
local function parseVersion(version)
local parts = {}
for num in version:gmatch("%d+") do
table.insert(parts, tonumber(num))
end
return parts
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
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 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
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
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
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
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) (^1Expect Errors^7)")
end
end)
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, 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
CheckBridgeVersion()
end
CheckBridgeVersion()

View File

@@ -21,12 +21,12 @@ This module provides access and control over player metadata, which is useful fo
print("Player stress level:", stress)
```
- **setPlayerMetadata(player, key, value)**
- **SetMetadata(player, key, value)**
⚠️ Server side only
- Updates or assigns a value to a specific metadata key for a player.
- The function updates the player's metadata using the active core export.
- **Example:**
```lua
setPlayerMetadata(player, "stress", 0)
SetMetadata(player, "stress", 0)
```

View File

@@ -1,6 +1,6 @@
### wrapperfunctions.lua
Provides wrapper compatibility functions for command and inventory stash systems across different frameworks (OX, QB, ESX, etc).
Provides wrapper compatibility functions for command and inventory stash systems across different frameworks (OX, QB, ESX, QS, etc).
- **registerCommand(command, options)**
@@ -23,7 +23,7 @@ Provides wrapper compatibility functions for command and inventory stash systems
- **registerStash(name, label, slots?, weight?, owner?, coords?)**
⚠️ Server Side Only
- Registers a stash using OX, or Origen inventory systems.
- Registers a stash using OX, QS, or Origen inventory systems.
- **Example:**
```lua
registerStash(

View File

@@ -15,6 +15,7 @@ local Exports = {
OXInv = "ox_inventory",
QBInv = "qb-inventory",
PSInv = "ps-inventory",
QSInv = "qs-inventory",
CoreInv = "core_inventory",
CodeMInv = "codem-inventory",
OrigenInv = "origen_inventory",
@@ -30,18 +31,17 @@ local Exports = {
-- REDM
RSGExport = "rsg-core",
RSGInv = "rsg-inventory",
VorpExport = "vorp_core",
VorpInv = "vorp_inventory",
VorpMenu = "vorp_menu",
RSGInv = "rsg-inventory"
}
-- 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()
@@ -49,60 +49,27 @@ end
local function endTimer(label)
timers[label] = GetGameTimer() - (timers[label] or GetGameTimer())
timers[label] = "("..(timers[label] / 1000).."s)"
timers[label] = timers[label] / 1000
end
startTimer("Cache") startTimer("Items") startTimer("Vehicles") startTimer("Jobs") startTimer("InvWeight") startTimer("InvSlots")
-- Helper functions --
startTimer("Cache")
-- Helper function to check if resource exists in server (instead of if it is already started)
local function checkExists(resourceName)
local state = GetResourceState(resourceName)
return state and (state:find("start") or state:find("stopped"))
return GetResourceState(resourceName):find("start") or GetResourceState(resourceName):find("stopped")
end
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
-- Ensure oxmysql resource is loaded
local fileLoader = assert(load(LoadResourceFile("oxmysql", ('lib/MySQL.lua')), ('@@oxmysql/lib/MySQL.lua')))
fileLoader()
if checkExists(Exports.OXCoreExport) then
-- Detected OX_Core in server, wait for it to be started if needed
waitStartedOrStopped(Exports.OXCoreExport)
while GetResourceState(Exports.OXCoreExport) ~= "started" do Wait(100) end
local fileLoader = assert(load(LoadResourceFile(Exports.OXCoreExport, ('lib/init.lua')), ('@@'..Exports.OXCoreExport..'/lib/init.lua')))
fileLoader()
end
if checkExists(Exports.ESXExport) then
-- Detected ESX in server, wait for it to be started if needed
waitStarted(Exports.ESXExport)
while GetResourceState(Exports.ESXExport) ~= "started" do Wait(100) end
local fileLoader = assert(load(LoadResourceFile(Exports.ESXExport, ('/imports.lua')), ('@@'..Exports.ESXExport..'/imports.lua')))
fileLoader()
end
@@ -120,267 +87,202 @@ end
---------------------
---- Load Items -----
---------------------
-- Items initialization based on detected inventory system
startTimer("Items")
if checkExists(Exports.OXInv) then
-- Wait for OX Inventory to start if it's not already started
while GetResourceState(Exports.OXInv) ~= "started" do Wait(100) end
itemResource = Exports.OXInv
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
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
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.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()
cache.Items = ESX.GetItems()
while not next(cache.Items) do
cache.Items = ESX.GetItems()
Wait(1000)
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
local success, result = pcall(function()
return exports[Exports.OXInv]:Items()
end)
if success and result then
cache.Items = result
end
-- Get Weapon info and duplicate them if they are uppercase
-- (duplicate incase anything checks for the uppercase version)
for k, v in pairs(cache.Items) do
if type(k) == "string" then
if k:find("WEAPON") then
cache.Items[k:lower()] = cache.Items[k]
end
else
print("^1ERROR^7: ^1Possible table inside a table, check your items.lua^7?")
print("^1Possible Issue found^7:")
print(json.encode(Items[k], {indent = true}))
end
end
elseif checkExists(Exports.QBXExport) then
while GetResourceState(Exports.QBXExport) ~= "started" do Wait(100) end
itemResource = Exports.QBXExport
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
-- If this is nil, they need to update to qbx_core 1.23.0+
if not cache.Items then
-- if their inventory doesn't allow that (they refuse to update their butchered core replacement):
if GetResourceState(Exports.QSInv):find("start") then
itemResource = Exports.QSInv
cache.Items = exports[Exports.QSInv]:GetItemList()
elseif GetResourceState(Exports.OrigenInv):find("start") then
itemResource = Exports.OrigenInv
cache.Items = exports[Exports.OrigenInv]:Items()
elseif GetResourceState(Exports.CodeMInv):find("start") then
itemResource = Exports.CodeMInv
cache.Items = exports[Exports.CodeMInv]:GetItemList()
elseif GetResourceState(Exports.CoreInv):find("start") then
itemResource = Exports.CoreInv
cache.Items = exports[Exports.CoreInv]:getItemsList()
elseif GetResourceState(Exports.TgiannInv):find("start") then
itemResource = Exports.TgiannInv
cache.Items = exports[Exports.TgiannInv]:Items()
end
end
elseif checkExists(Exports.QBExport) then
while GetResourceState(Exports.QBExport) ~= "started" do Wait(100) end
itemResource = Exports.QBExport
cache.Items = exports[Exports.QBExport]:GetCoreObject().Shared.Items
elseif checkExists(Exports.ESXExport) then
itemResource = Exports.ESXExport
if GetResourceState(Exports.QSInv):find("start") then
cache.Items = exports[Exports.QSInv]:GetItemList()
else
cache.Items = ESX.GetItems()
while not next(cache.Items) do
cache.Items = ESX.GetItems()
Wait(1000)
end
end
elseif checkExists(Exports.RSGExport) then
while GetResourceState(Exports.RSGExport) ~= "started" do Wait(100) end
itemResource = Exports.RSGExport
cache.Items = exports[Exports.RSGExport]:GetCoreObject().Shared.Items
end
endTimer("Items")
---------------------
--- Load Vehicles ---
---------------------
---
local vehicleFunc = {
startTimer("Vehicles")
-- Vehicle loading depending on framework
if checkExists(Exports.QBXExport) then
vehResource = Exports.QBXExport
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.QBExport)then
vehResource = Exports.QBExport
cache.Vehicles = exports[Exports.QBExport]:GetCoreObject().Shared.Vehicles
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
elseif checkExists(Exports.OXCoreExport) then
vehResource = Exports.OXCoreExport
cache.Vehicles = {}
for k, v in pairs(Ox.GetVehicleData()) do
cache.Vehicles[k] = {
model = k, hash = GetHashKey(k),
price = v.price,
name = v.name,
brand = v.make
}
end
elseif checkExists(Exports.ESXExport) then
vehResource = Exports.ESXExport
while not MySQL do Wait(1000) end
for _, v in pairs(MySQL.query.await('SELECT model, price, name FROM vehicles')) do
cache.Vehicles[v.model] = {
model = v.model,
hash = GetHashKey(v.model),
price = v.price,
name = v.name,
}
end
elseif checkExists(Exports.RSGExport) then
vehResource = Exports.RSGExport
cache.Vehicles = exports[Exports.RSGExport]:GetCoreObject().Shared.Vehicles
end
endTimer("Vehicles")
---------------------
----- Load Jobs -----
---------------------
startTimer("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()
local jobFunc = {
elseif checkExists(Exports.QBExport) then
jobResource = Exports.QBExport
cache.Jobs, cache.Gangs = exports[Exports.QBExport]:GetCoreObject().Shared.Jobs, exports[Exports.QBExport]:GetCoreObject().Shared.Gangs
{ 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
for _, job in pairs(tempJobs) do
cache.Jobs[job.name] = {
label = job.label,
grades = gradeMap[job.name] or {}
}
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::
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
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
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
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::
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
@@ -406,6 +308,8 @@ end
-- Forcefully load the the specified config file from inventory scripts
-- This allows to get information required for certain functions that need to detect how much space is left in a players inventory
-- This is born from too many tickets of me needing to explain that they need to change "InventoryWeight" to match their inv setting
startTimer("InvWeight")
startTimer("InvSlots")
local function getInventoryConfig(resource, data)
if data.convars then
return function(path)
@@ -419,17 +323,12 @@ local function getInventoryConfig(resource, data)
if not content then return nil, "Failed to load file" end
local env = {
GetConvar = GetConvar,
vector3 = vector3,
vector4 = vector4,
Citizen = Citizen,
GetResourceState = GetResourceState,
exports = exports,
DependencyCheck = function() return nil end,
GetConvar = GetConvar, vector3 = vector3, Citizen = Citizen,
GetResourceState = GetResourceState, exports = exports,
DependencyCheck = DependencyCheck or function() return nil end,
}
local fn, err = load(content, '@'..data.file, 't', env)
if not fn then return nil, "Failed to compile config: " .. err end
if not pcall(fn) then return nil, "Error executing config file" end
@@ -460,6 +359,7 @@ local invWeightTable = {
},
[Exports.JPRInv] = { file = "configs/main_config.lua", path = { "MaxInventoryWeight" }, slotPath = { "MaxInventorySlots" } },
[Exports.PSInv] = { file = "config.lua", path = { "MaxInventoryWeight" }, slotPath = { "MaxInventorySlots" } },
[Exports.QSInv] = { file = "config/config.lua", path = { "InventoryWeight", "weight" }, slotPath = { "InventoryWeight", "slots" } },
[Exports.TgiannInv] = { file = "configs/config.lua", path = { "slotsMaxWeights", "player", "maxWeight" }, slotPath = { "slotsMaxWeights", "player", "slots" } },
[Exports.CodeMInv] = { file = "config/config.lua", path = { "MaxWeight" }, slotPath = { "MaxSlots" } },
[Exports.RSGInv] = { file = "config/config.lua", path = { "MaxWeight" }, slotPath = { "MaxSlots" } },
@@ -472,8 +372,6 @@ 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
waitStartedOrStopped(script)
local attempts = data.fallback or { data }
local lookup, used, err
@@ -521,17 +419,17 @@ CreateThread(function()
if type(v) ~= "number" then for count in pairs(v) do counts[k] += 1 end end
end
if cache.InventoryWeight then
print("^6FrameWorkCache^7: ^4"..invResource.."^2 InventoryWeight^7: ^3"..cache.InventoryWeight.."^7 (^3"..(cache.InventoryWeight / 1000).."kg^7) ^7"..timers["InvWeight"])
print("^6FrameWorkCache^7: ^4"..invResource.."^2 InventoryWeight^7: ^3"..cache.InventoryWeight.."^7 (^3"..(cache.InventoryWeight / 1000).."kg^7) ^7("..timers["InvWeight"].."s)")
end
if cache.InventorySlots then
print("^6FrameWorkCache^7: ^4"..invResource.."^2 InventorySlots^7: ^3"..cache.InventorySlots.." ^7"..timers["InvSlots"])
print("^6FrameWorkCache^7: ^4"..invResource.."^2 InventorySlots^7: ^3"..cache.InventorySlots.." ^7("..timers["InvSlots"].."s)")
end
print("^6FrameworkCache^7: ^4"..itemResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Items).."^2 Items ^7"..timers["Items"])
print("^6FrameworkCache^7: ^4"..vehResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Vehicles).."^2 Vehicles ^7"..timers["Vehicles"])
print("^6FrameworkCache^7: ^4"..jobResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Jobs).."^2 Jobs ^7"..timers["Jobs"])
print("^6FrameworkCache^7: ^4"..jobResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Gangs).."^2 Gangs ^7"..timers["Jobs"])
print("^6FrameworkCache^7: ^4"..itemResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Items).."^2 Items ^7("..timers["Items"].."s)")
print("^6FrameworkCache^7: ^4"..vehResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Vehicles).."^2 Vehicles ^7("..timers["Vehicles"].."s)")
print("^6FrameworkCache^7: ^4"..jobResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Jobs).."^2 Jobs ^7("..timers["Jobs"].."s)")
print("^6FrameworkCache^7: ^4"..jobResource:gsub("-", "^7-^4"):gsub("_", "^7_^4").."^2 Loaded ^3"..tostring(counts.Gangs).."^2 Gangs ^7("..timers["Jobs"].."s)")
endTimer("Cache")
print("^6FrameworkCache^7: ^2Cache Ready ^7"..timers["Cache"])
print("^6FrameworkCache^7: ^2Cache Ready ^7("..timers["Cache"].."s)")
cacheReady = true
end)

View File

@@ -1,21 +1,18 @@
name "Jim_Bridge"
author "Jimathy"
version "2.1.09"
version "2.0.20"
description "Framework Bridge By Jimathy"
fx_version "cerulean"
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
games { 'gta5', 'rdr3' }
lua54 'yes'
files {
'starter.lua',
'shared/*.lua',
'shared/make/*.lua',
'shared/auth/*.lua',
'shared/make/*.lua',
'shared/modules/*.lua',
'shared/scaleforms/*.lua',
'shared/wrappers/*.lua',
}
-- Version checker
@@ -27,6 +24,4 @@ server_scripts {
client_scripts {
'clientFrameworkCache.lua',
'ui_modules/*.lua',
}
suppress_updates 'false' -- set to 'true' to disable update pings
}

View File

@@ -1,28 +1,18 @@
-------------------------------------------------------------
-- Exploit Auth System
-------------------------------------------------------------
forceDisableExplotProtection = false -- dangerous, this allows exploits
AuthEvent = nil
currentToken = nil
if isServer() then
local excludeRes = {
[Exports.QBExport] = true,
[Exports.ESXExport] = true,
[Exports.VorpExport] = true,
}
local AuthEvent = getScript()..":"..keyGen()..keyGen()..keyGen()..keyGen()..":"..keyGen()..keyGen()..keyGen()..keyGen()
validTokens = validTokens or {}
createCallback(AuthEvent, function(source)
local src = source
local token = keyGen()..keyGen()..keyGen()..keyGen() -- Use a secure random generator here
local invokingRes = GetInvokingResource()
debugPrint(invokingRes)
if invokingRes and invokingRes ~= getScript() and not excludeRes[invokingRes] then
--debugPrint(GetInvokingResource())
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
return ""
end
@@ -55,22 +45,21 @@ if isServer() then
createCallback(getScript()..":callback:GetAuthEvent", function(source)
local src = source
local invokingRes = GetInvokingResource()
if invokingRes and invokingRes ~= getScript() and not excludeRes[invokingRes] then
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital callback was called from an external resource^7")
return ""
end
if authCooldown[src] then
debugPrint("^1Auth^7: ^3Cooldown active^7 for Player ^1"..src.."^7, ignoring additional auth request")
return AuthEvent
return ""
end
debugPrint("^1Auth^7: ^2Player Source^7: "..src.." ^2requested ^3AuthEvent^7", AuthEvent)
authCooldown[src] = true
SetTimeout(60000, function() -- 1 minute cooldown
SetTimeout(5000, function() -- 5 second cooldown
authCooldown[src] = nil
end)
@@ -91,8 +80,6 @@ if isServer() then
-- Multiuse function to check if the generated client token is valid
function checkToken(src, token, genType, name)
if forceDisableExplotProtection == true then return true end
if token == nil then
debugPrint("^1Auth^7: ^1No token recieved^7")
if genType == "stash" then
@@ -128,40 +115,4 @@ else
debugPrint("^1Auth^7: ^2Clearing Auth Event^7")
TriggerServerEvent(getScript()..":clearAuthEventRequest")
end, true)
end
function distExploitCheck(table, src)
if forceDisableExplotProtection == true then return true end
if not table then
print("^1Error^7: ^1This wasn^7'^1t reigstered correctly or this is an exploit attempt^1")
return false
end
local maxDist = 10.0
local nearestDist = 999999.0
local nearestCoords = nil
local ped = src and GetPlayerPed(src) or PlayerPedId()
local srcCoords = GetEntityCoords(ped)
for i = 1, #table do
local dist2d = #(table[i].xy - srcCoords.xy)
-- Track the nearestDist
if dist2d < nearestDist then
nearestDist = dist2d
nearestCoords = table[i]
end
if dist2d <= maxDist then
debugPrint("^1Found location^7: ^3"..nearestDist.." ^7away from player - "..formatCoord(nearestCoords))
return true
end
end
print(src and ("^1Src ^3"..src.." ") or "", "^1Tried to open a registered shop/stash from over the distance limit^7")
print("^1Nearest Possible Location^7: ^3"..nearestDist.." ^7away from player - "..formatCoord(nearestCoords))
return false
end

View File

@@ -7,152 +7,6 @@
• Wait for the player to be logged in before proceeding.
]]
local onLoadLast = {} -- keyed by tostring(func)
local function _debouncedRun(func)
local key = tostring(func)
local now = GetGameTimer()
local last = onLoadLast[key] or -1e12
if (now - last) < 5000 then
debugPrint(("^6Bridge^7 ^3onPlayerLoaded^7 skipped — cooldown %dms remaining"):format(5000 - (now - last)))
return
end
onLoadLast[key] = now
CreateThread(function()
Wait(2000) -- keep your small delay
debugPrint("^6Bridge^7: ^2Executing onPlayerLoaded callback")
func()
end)
end
local frameworkLoadFunc = {
{ framework = Exports.QBXExport,
onPlayerLoaded =
function(func)
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', func)
end,
onPlayerUnload =
function(func)
RegisterNetEvent('QBCore:Client:OnPlayerUnload', func)
end,
waitforLogin =
function(timeout)
local startTime = GetGameTimer()
while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do
Wait(100)
end
return LocalPlayer.state.isLoggedIn
end,
},
{ framework = Exports.QBExport,
onPlayerLoaded =
function(func)
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', func)
end,
onPlayerUnload =
function(func)
RegisterNetEvent('QBCore:Client:OnPlayerUnload', func)
end,
waitforLogin =
function(timeout)
local startTime = GetGameTimer()
while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do
Wait(100)
end
return LocalPlayer.state.isLoggedIn
end,
},
{ framework = Exports.ESXExport,
onPlayerLoaded =
function(func)
RegisterNetEvent("esx:playerLoaded", function()
-- make sure shared has loaded (because sql)
if waitForSharedLoad() then func() end
end)
end,
onPlayerUnload =
function(func)
RegisterNetEvent("esx:onPlayerLogout", func)
end,
waitforLogin =
function(timeout)
local startTime = GetGameTimer()
while (GetGameTimer() - startTime) < timeout do
local playerData = ESX.GetPlayerData()
if playerData and playerData.job then
return true
end
Wait(100)
end
end,
},
{ framework = Exports.OXCoreExport,
onPlayerLoaded =
function(func)
RegisterNetEvent('ox:playerLoaded', func)
end,
onPlayerUnload =
function(func)
RegisterNetEvent('ox:playerLogout', func)
end,
waitforLogin =
function(timeout)
if OxPlayer["stateId"] then
return true
end
while not OxPlayer["stateId"] do
Wait(1000)
if OxPlayer.get["stateId"] then
return true
end
end
end,
},
{ framework = Exports.RSGExport,
onPlayerLoaded =
function(func)
RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', func)
end,
onPlayerUnload =
function(func)
RegisterNetEvent('RSGCore:Client:OnPlayerUnload', func)
end,
waitforLogin =
function(timeout)
local startTime = GetGameTimer()
while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do
Wait(100)
end
return LocalPlayer.state.isLoggedIn
end,
},
{ framework = Exports.VorpExport,
onPlayerLoaded =
function(func)
RegisterNetEvent('vorp_core:Client:OnPlayerSpawned', func)
end,
onPlayerUnload =
function(func)
--??
end,
waitforLogin =
function(timeout)
local startTime = GetGameTimer()
while not LocalPlayer.state.IsInSession and (GetGameTimer() - startTime) < timeout do
Wait(100)
end
return LocalPlayer.state.IsInSession
end,
},
}
-------------------------------------------------------------
-- Player Loaded and Unloaded Events
-------------------------------------------------------------
@@ -170,28 +24,52 @@ local frameworkLoadFunc = {
--- end, true)
--- ```
function onPlayerLoaded(func, onStart)
local onPlayerFramework = ""
local loaded = false
if onStart then
onResourceStart(function()
if not waitForLogin() then return end
debugPrint("^6Bridge^7: ^3onResourceStart^7()^2 routed through ^3onPlayerLoaded^7()")
_debouncedRun(func)
loaded = true
debugPrint("^6Bridge^7: ^3onResourceStart^7()^2 executed through ^3onPlayerLoaded^7()")
Wait(2000)
func()
end, true)
end
local handler = function()
_debouncedRun(func)
end
if not loaded then
local tempFunc = function()
Wait(2000)
debugPrint("^6Bridge^7: ^2Executing onPlayerLoaded")
func()
end
for i = 1, #frameworkLoadFunc do
local data = frameworkLoadFunc[i]
jsonPrint(data)
if isStarted(data.framework) then
debugPrint("^6Bridge^7: ^2Registering ^3"..data.framework.." ^5onPlayerLoaded^7()")
data.onPlayerLoaded(handler)
return
if isStarted(QBExport) or isStarted(QBXExport) then
onPlayerFramework = QBExport
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', tempFunc)
elseif isStarted(ESXExport) then
onPlayerFramework = ESXExport
RegisterNetEvent('esx:playerLoaded', function()
if waitForSharedLoad() then
tempFunc()
end
end
)
elseif isStarted(OXCoreExport) then
onPlayerFramework = OXCoreExport
RegisterNetEvent('ox:playerLoaded', tempFunc)
elseif isStarted(RSGExport) then
onPlayerFramework = RSGExport
RegisterNetEvent('RSGCore:Client:OnPlayerLoaded', tempFunc)
end
if onPlayerFramework ~= "" then
debugPrint("^6Bridge^7: ^2Registering ^3onPlayerLoaded^7()^2 with ^3" .. onPlayerFramework.."^7")
else
print("^4ERROR^7: No supported core detected for onPlayerLoaded - Check starter.lua")
end
end
print("^1ERROR^7: ^1No supported core detected for onPlayerLoaded - Check starter.lua^7")
end
--- Executes a function when the player character is unloaded.
@@ -204,15 +82,15 @@ end
--- end)
--- ```
function onPlayerUnload(func)
for i = 1, #frameworkLoadFunc do
local data = frameworkLoadFunc[i]
if isStarted(data.framework) then
debugPrint("^6Bridge^7: ^2Registering ^3"..data.framework.." ^5onPlayerUnload^7()")
data.onPlayerUnload(func)
return
end
end
print("^1ERROR^7: ^1No supported core detected for onPlayerUnload - Check starter.lua^7")
debugPrint("^6Bridge^7: ^2Registering ^3onPlayerUnload^7()")
RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() func() end)
RegisterNetEvent('ox:playerLogout', function() func() end)
RegisterNetEvent('RSGCore:Client:OnPlayerUnload', function() func() end)
RegisterNetEvent('esx:onPlayerLogout', function() func() end)
end
-------------------------------------------------------------
@@ -239,7 +117,8 @@ function onResourceStart(func, thisScript)
debugPrint("^6Bridge^7: ^2Shared Load Detected^7.")
hasPrinted = true
end
func(resourceName)
if isStarted(ESXExport) then Wait(10000) end
func()
end
end
end)
@@ -258,7 +137,7 @@ function onResourceStop(func, thisScript)
debugPrint("^6Bridge^7: ^2Registering ^3onResourceStop^7()")
AddEventHandler('onResourceStop', function(resourceName)
if getScript() == resourceName and (thisScript or true) then
func(resourceName)
func()
end
end)
end
@@ -271,30 +150,58 @@ end
--- @usage
--- waitForLogin()
function waitForLogin()
for i = 1, #frameworkLoadFunc do
local data = frameworkLoadFunc[i]
if isStarted(data.framework) then
debugPrint("^6Bridge^7: ^2Waiting for ^3"..data.framework.."^2 player login^7.")
local result = data.waitforLogin(10000)
if result == true then
debugPrint("^6Bridge^7: ^3"..data.framework.."^2 Player Login Detected^7.")
else
print("^4Error^7: ^2Timeout reached while waiting for player login^7.")
local timeout = 10000 -- 10 seconds in milliseconds
local startTime = GetGameTimer()
local loggedIn = false
if isStarted(ESXExport) then
while (GetGameTimer() - startTime) < timeout do
local playerData = ESX.GetPlayerData()
if playerData and playerData.job then
loggedIn = true
break
end
return result
Wait(100)
end
elseif isStarted(OXCoreExport) then
if OxPlayer["stateId"] then
loggedIn = true
end
while not OxPlayer["stateId"] do
Wait(1000)
debugPrint("Waiting for stateId to class as logged in")
if OxPlayer.get["stateId"] then
loggedIn = true
break
end
end
else
-- For other frameworks, use LocalPlayer.state.isLoggedIn.
while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do
Wait(100)
end
loggedIn = LocalPlayer.state.isLoggedIn
end
if not loggedIn then
print("^4Error^7: ^2Timeout reached while waiting for player login^7.")
return false
else
debugPrint("^6Bridge^7: ^2Player Login Detected^7.")
return true
end
end
local messageShown = false
function waitForSharedLoad()
local timeout = GetGameTimer() + 900000 -- 15 minutes max wait (had to up this from 2 minutes because of slow servers)
local timeout = 100000 -- 10 seconds in milliseconds
local startTime = GetGameTimer()
local loaded = true
local loop = 0
while ((not Jobs or not next(Jobs)) or
(not Items or not next(Items)) or
(not Vehicles or not next(Vehicles))
) and (timeout and GetGameTimer() < timeout) do
) and ((GetGameTimer() - startTime) < timeout) do
if loop >= 3 and not messageShown then
if (not Jobs or not next(Jobs)) then
print("^4Debug^7: ^2Waiting for ^7Jobs^2 to be loaded")
@@ -307,16 +214,19 @@ function waitForSharedLoad()
end
messageShown = true
end
--print((GetGameTimer() - startTime) < timeout)
Wait(1000)
if Jobs and Items and Vehicles then
--print("^6Bridge^7: ^2Jobs, Items, and Vehicles Loaded^7.")
loaded = true
break
end
loop += 1
end
if Jobs and Items and Vehicles then
debugPrint("^6Bridge^7: ^2Jobs, Items, and Vehicles Loaded^7.")
return true
else
if not loaded then
print("^4Error^7: ^1Timeout reached while waiting for shared load^7.")
return false
else
return true
end
end

View File

@@ -1,111 +1,100 @@
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')
function parseVersion(version)
local parts = {}
for num in version:gmatch("%d+") do
table.insert(parts, tonumber(num))
end
return parts
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
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
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
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 newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
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, newestVersion)
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"..newestVersion.."^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(3600000, function() CheckVersion() end)
SetTimeout(1200000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
end
end
end)
end)
end
end
end)
else
local lines = {}
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
CheckVersion()
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(1200000, 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
CheckVersion()

View File

@@ -1,30 +1,3 @@
local CALLBACK_RETRIES = 0
-- Internal: await a callback with a timeout
local function awaitWithTimeout(registerFn, timeoutMs)
local p = promise.new()
local finished = false
registerFn(function(result)
if finished then return end
finished = true
p:resolve(result)
end)
-- timeout watchdog
CreateThread(function()
Wait(5000)
if not finished then
finished = true
p:reject('timeout')
end
end)
local ok, res = pcall(function() return Citizen.Await(p) end)
if ok then return res, nil end
return nil, res
end
--- Registers a callback function with the appropriate framework.
---
--- This function checks which framework is started (e.g., OX, QB, ESX) and registers the callback accordingly.
@@ -64,10 +37,6 @@ 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)
@@ -86,7 +55,6 @@ end
---@param ... any Additional arguments to pass to the callback.
---
---@return any any The result returned by the callback function.
---@return string string The error/success message returned by the callback function.
---
---@usage
--- ```lua
@@ -97,54 +65,25 @@ end
--- print(result)
--- ```
function triggerCallback(callbackName, ...)
local result = nil
debugPrint("^6Bridge^7: ^2Triggering ^3Callback^7:", callbackName)
local args = {...}
if isStarted(OXLibExport) then
local ok, res = pcall(function()
return lib.callback.await(callbackName, false, table.unpack(args))
end)
if ok then return res, "nil" end
return nil, tostring(res)
end
local attempts = 0
local lastErr
repeat
attempts = attempts + 1
if isStarted(QBExport) then
local res, err = awaitWithTimeout(function(cb)
Core.Functions.TriggerCallback(callbackName, cb, table.unpack(args))
end, 5000)
if res ~= nil then return res, "nil" end
lastErr = err
debugPrint(("^6Bridge^7: ^3Callback^7 %s ^1failed^7 (QB) attempt %d: %s"):format(callbackName, attempts, tostring(err)))
elseif isStarted(VorpExport) then
local res, err = awaitWithTimeout(function(cb)
Core.Callback.TriggerAwait(callbackName, cb, table.unpack(args))
end, 5000)
if res ~= nil then return res, "nil" end
lastErr = err
debugPrint(("^6Bridge^7: ^3Callback^7 %s ^1failed^7 (Vorp) attempt %d: %s"):format(callbackName, attempts, tostring(err)))
elseif isStarted(ESXExport) then
local res, err = awaitWithTimeout(function(cb)
ESX.TriggerServerCallback(callbackName, cb, table.unpack(args))
end, 5000)
if res ~= nil then return res, "nil" end
lastErr = err
debugPrint(("^6Bridge^7: ^3Callback^7 %s ^1failed^7 (ESX) attempt %d: %s"):format(callbackName, attempts, tostring(err)))
else
print("^6Bridge^7: ^1ERROR^7: ^3Can't find any script to trigger callback with^7:", callbackName)
return nil, "no_framework"
end
result = lib.callback.await(callbackName, false, ...)
elseif isStarted(QBExport) then
local p = promise.new()
Core.Functions.TriggerCallback(callbackName, function(cbResult)
p:resolve(cbResult)
end, ...)
result = Citizen.Await(p)
Wait(10)
until attempts > (1 + CALLBACK_RETRIES)
return nil, lastErr or "timeout"
end
elseif isStarted(ESXExport) then
local p = promise.new()
ESX.TriggerServerCallback(callbackName, function(cbResult)
p:resolve(cbResult)
end, ...)
result = Citizen.Await(p)
else
print("^6Bridge^7: ^1ERROR^7: ^3Can't find any script to trigger callback with", callbackName)
end
return result
end

View File

@@ -10,263 +10,6 @@
• 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`.
@@ -304,22 +47,255 @@ local contextFunc = {
--- })
--- ```
function openMenu(Menu, data)
contextFunc[Config.System.Menu](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
end
-- Line break conversion table, basically, to automate the linebreak in functions `br`
-- This is declared and automatically sets it for what menu you are using (depends if one is uding MD formatting or HTML formatting)
local lineBreakConversion = {
["ox"] = "\n",
["gta"] = "\n",
["lation"] = "\n",
["qb"] = "<br>",
["esx"] = "<br>",
}
--- A line break constant used for menu header formatting.
br = lineBreakConversion[Config.System.Menu]
br = (Config.System.Menu == "ox" or Config.System.Menu == "gta" or Config.System.Menu == "lation") and "\n" or "<br>"
--- Checks if the current menu system is 'ox' or 'gta' for formatting purposes.
--- @return boolean boolean True if using ox or gta menus, otherwise false.

View File

@@ -10,10 +10,11 @@ OXLibExport, QBXExport, QBExport, ESXExport, OXCoreExport =
Exports.ESXExport or "",
Exports.OXCoreExport or ""
OXInv, QBInv, PSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv, JPRInv =
OXInv, QBInv, PSInv, QSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv, JPRInv =
Exports.OXInv or "",
Exports.QBInv or "",
Exports.PSInv or "",
Exports.QSInv or "",
Exports.CoreInv or "",
Exports.CodeMInv or "",
Exports.OrigenInv or "",
@@ -21,9 +22,6 @@ OXInv, QBInv, PSInv, 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 ""
@@ -31,17 +29,15 @@ 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
if IsDuplicityVersion() then
local cache = nil
local timeout = GetGameTimer() + 900000 -- 15 minutes max wait (had to up this from 2 minutes because of slow servers)
local timeout = GetGameTimer() + 120000 -- 2 minutes max wait (had to up this from 5 seconds because of slow servers)
-- Wait until jim_bridge is started and export is available
while not cache and (timeout and GetGameTimer() < timeout) do
while not cache and GetGameTimer() < timeout do
if GetResourceState("jim_bridge"):find("start") then
local success, result = pcall(function()
return exports["jim_bridge"]:GetSharedData()
@@ -53,7 +49,7 @@ if IsDuplicityVersion() then
Wait(100)
end
if timeout and not cache then
if not cache then
print("^1ERROR^7: ^2jim_bridge export not available after timeout^7.")
return
end

View File

@@ -17,8 +17,7 @@ local excludeKeys = {
amount = true, metadata = true, description = true, info = true,
job = true, gang = true, oneUse = true, slot = true,
blueprintRef = true, craftingLevel = true, craftedItems = true,
hasCrafted = true, exp = true, anim = true, time = true, id = true,
ingredients = true,
hasCrafted = true, exp = true, anim = true, time = true,
}
-------------------------------------------------------------
@@ -41,14 +40,9 @@ local excludeKeys = {
--- craftable = {
--- Header = "Weapon Crafting",
--- Recipes = {
--- weapon_pistol = {
--- id = 1,
--- ingredients = {
--- steel = 5, plastic = 5,
--- },
--- info = {
--- amount = 1,
--- },
--- [1] = {
--- ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 },
--- amount = 1,
--- },
--- -- More recipes...
--- },
@@ -62,10 +56,9 @@ local excludeKeys = {
--- job = "mechanic",
--- onBack = function() print("Returning to previous menu") end,
--- })
--- ```
function craftingMenu(data)
if CraftLock then return end
local data = cloneTable(data)
-- Job or gang check; exit if not authorized.
if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
@@ -79,49 +72,16 @@ function craftingMenu(data)
-- Normalize stash name.
data.stashName = data.stashTable or data.stashName
-- Wrapper to convert old style crafting recipes to be handled by the menu properly
--if data.craftable.Recipes[1] then -- assume old style crafting table
-- local compatTable = {}
-- local id = 0
-- for k, v in ipairs(data.craftable.Recipes) do
-- local Recipe = v
-- for l, b in pairs(Recipe) do
-- if doesItemExist(l) then
-- id += 1
-- compatTable[l] = {
-- ingredients = b,
-- id = id,
-- info = {
-- amount = Recipe.amount or 1,
-- metadata = Recipe.metadata or nil,
-- job = Recipe.job or nil,
-- gang = Recipe.gang or nil,
-- hasCrafted = Recipe.hasCrafted or nil,
-- },
-- }
-- end
-- end
-- end
-- data.craftable.Recipes = compatTable
--end
-- Convert to array
--local RecipesArray = {}
--for k, v in pairs(data.craftable.Recipes) do
-- RecipesArray[v.id] = { [k] = v }
--end
local Menu = {}
local Recipes = cloneTable(data.craftable.Recipes)
local Recipes = data.craftable.Recipes
local craftedItems = {}
local tempCarryTable = {}
-- Build a temporary table of all required ingredients (default quantity is 1).
-- Build a table of all required ingredients (default quantity is 1).
for i = 1, #Recipes do
for k, v in pairs(Recipes[i]) do
if not excludeKeys[k] then
if not Recipes[i].amount then Recipes[i].amount = 1 end
tempCarryTable[k] = math.max(tempCarryTable[k] or 0, Recipes[i].amount)
for k in pairs(Recipes[i]) do
if k ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" then
tempCarryTable[k] = Recipes[i].amount or 1
end
end
end
@@ -129,98 +89,81 @@ function craftingMenu(data)
-- Check if the player can carry the required items (server callback).
local canCarryTable = triggerCallback(getScript()..':server:canCarry', tempCarryTable)
local usingStash = data.stashName ~= nil and data.stashName ~= ""
local usingStash = data.stashName ~= nil
Menu[#Menu+1] = {
icon = usingStash and "fas fa-boxes-stacked" or "fas fa-person",
header = "Material Source: "..(usingStash and "Job Stash" or "Player Inventory"),
disabled = true,
}
-- Process each recipe to create menu entries.
for i = 1, #Recipes do
local menuId = #Menu+1
local item = ""
local Recipe = {}
for k, v in pairs(Recipes[i]) do
if not Recipes[i]["amount"] then Recipes[i]["amount"] = 1 end
for k, _ in pairs(Recipes[i]) do
if not excludeKeys[k] then
item = k
Recipe = Recipes[i]
Recipe.amount = Recipe.amount or 1
break
end
end
-- Job Check
local hasGroup = true
if Recipe.job then
for l, b in pairs(Recipe.job) do
hasGroup = hasJob(l, nil, b)
if hasGroup then goto skipcheck end
end
end
if Recipe.gang then
for l, b in pairs(Recipe.gang) do
hasGroup = hasJob(l, nil, b)
if hasGroup then goto skipcheck end
end
end
::skipcheck::
-- if has group requirement, continue
if hasGroup then
local setheader, settext, disable, metadata = "", "", false, (Recipe.metadata or Recipe.info or nil)
local itemTable = {}
local metaTable = {}
-- Build ingredient details.
for l, b in pairs(Recipe[item]) do
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[label] = b
itemTable[l] = b
end
-- Make sure "canCarryTable" exists
while not canCarryTable do Wait(10) end
disable = not checkStashItem(data.stashName, itemTable)
setheader = ((metadata and metadata.label) or getItemLabel(item))
..(Recipe.amount > 1 and " x"..Recipe.amount or "")
local statusEmoji = disable and " " or not canCarryTable[item] and " 📦" or " ✔️"
local isNew = (Recipe.hasCrafted ~= nil and craftedItems[item] == nil) and "" or ""
setheader = isNew .. setheader .. statusEmoji
-- Build menu option using info
Menu[menuId] = {
arrow = isOx() and (not disable and canCarryTable[item]),
isMenuHeader = disable or not canCarryTable[item],
icon = invImg((metadata and metadata.image) or item),
image = invImg((metadata and metadata.image) or item),
header = setheader,
txt = settext or nil,
metadata = metaTable,
onSelect = (not disable and canCarryTable[item]) and function()
local transdata = {
item = item,
craft = Recipe,
craftable = data.craftable,
coords = data.coords,
amount = Recipe.amount,
stashName = data.stashName,
onBack = data.onBack,
metadata = metadata,
}
if Config.Crafting.MultiCraft then
multiCraft(transdata)
else
makeItem(transdata)
local hasjob = true
if Recipes[i].job then
for l, b in pairs(Recipes[i].job) do
hasjob = hasJob(l, nil, b)
if hasjob then break end
end
end or nil,
}
end
if hasjob then
local setheader, settext, disable, metadata = "", "", false, (Recipes[i]["metadata"] or Recipes[i]["info"] or nil)
local itemTable = {}
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 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
itemTable[l] = b
end
while not canCarryTable do Wait(0) 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))
..(Recipes[i]["amount"] > 1 and " x"..Recipes[i]["amount"] or "")
local statusEmoji = disable and " " or not canCarryTable[k] and " 📦" or " ✔️"
local isNew = (Recipes[i]["hasCrafted"] ~= nil and craftedItems[k] == nil) and "" or ""
setheader = isNew .. setheader .. statusEmoji
Menu[#Menu + 1] = {
arrow = isOx() and (not disable and canCarryTable[k]),
isMenuHeader = disable or not canCarryTable[k],
icon = invImg((metadata and metadata.image) or tostring(k)),
image = invImg((metadata and metadata.image) or tostring(k)),
header = setheader,
txt = settext or nil,
metadata = metaTable,
onSelect = (not disable and canCarryTable[k]) and function()
local transdata = {
item = k,
craft = data.craftable.Recipes[i],
craftable = data.craftable,
coords = data.coords,
stashName = data.stashName,
onBack = data.onBack,
metadata = metadata,
}
if Config.Crafting.MultiCraft then
multiCraft(transdata)
else
makeItem(transdata)
end
end or nil,
}
end
end
--Wait(0)
end
end
-- open context menu
openMenu(Menu, {
header = data.craftable.Header,
headertxt = data.craftable.Headertxt,
@@ -377,11 +320,9 @@ end
--- })
--- ```
function makeItem(origData)
Config.Crafting.SingleProgress = true -- Set false to use individual progress bars per craft
local data = cloneTable(origData)
local Ped = PlayerPedId()
function makeItem(data)
if CraftLock then return end
CraftLock = true
data.stashName = data.stashTable or data.stashName
@@ -395,142 +336,146 @@ function makeItem(origData)
local craftAmount = (data.amount and data.amount ~= 1) and data.amount or 1
local metadata = data.metadata or nil
local prop = data.craftable.Anims and data.craftable.Anims.prop or nil
local canReturn = true
local crafted, crafting = true, true
local cam = createCam(Ped, data.coords.xyz)
startCam(cam, 5000)
local cam = createTempCam(PlayerPedId(), data.coords)
startTempCam(cam)
-- Calculate total bartime if SingleProgress is enabled
local totalBartime = (bartime * craftAmount)
local totalBartime = bartime * craftAmount
local craftProp = nil
if prop then
craftProp = makeProp({ prop = prop.model, coords = GetEntityCoords(PlayerPedId()), true, true })
AttachEntityToEntity(craftProp, Ped, GetPedBoneIndex(Ped, prop.bone), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, 1, true)
end
if data.sound then
local s = data.sound
PlaySoundFromEntity(s.soundId, s.audioName, Ped, s.audioRef, true, 0)
end
if not Config.Crafting.SingleProgress then -- if SingleProgress is disabled, dont do ingredient progressbars
-- Run ingredient check and usage separately first
for i = 1, craftAmount do
for k, v in pairs(data.craft[data.item]) do
if isInventoryOpen() then
print("^1Error^7: ^2Inventory is open, you tried to break things")
stopCam(100)
ClearPedTasks(Ped)
if canReturn then craftingMenu(origData) end
CraftLock = false
return
-- Run ingredient check and usage separately first
for i = 1, craftAmount do
for k, v in pairs(data.craft) do
if not excludeKeys[k] and type(v) == "table" then
for l, b in pairs(v) do
if isInventoryOpen() then
print("^1Error^7: ^2Inventory is open, you tried to break things")
stopTempCam()
ClearPedTasks(PlayerPedId())
if canReturn then craftingMenu(data) end
CraftLock = false
return
end
if crafting and progressBar({
label = "Using "..b.." "..Items[l].label,
time = 1000,
cancel = true,
dict = 'pickup_object',
anim = "putdown_low",
flag = 49,
icon = l,
}) then
TriggerEvent((isStarted(QBInv) and QBInvNew and "qb-" or "")..'inventory:client:ItemBox', Items[l], "use", b)
else
crafted, crafting = false, false
break
end
Wait(200)
end
if crafting and progressBar({
label = "Using "..v.." "..getItemLabel(k),
time = 800,
cancel = true,
dict = 'pickup_object',
anim = "putdown_low",
flag = 49,
icon = k,
disableMovement = true,
}) then
TriggerEvent((isStarted(QBInv) and QBInvNew and "qb-" or "")..'inventory:client:ItemBox', Items[k], "use", v)
else
crafted, crafting = false, false
break
end
Wait(200)
end
if not crafted then
goto finishEarly
end
if crafting and progressBar({
label = bartext..((metadata and metadata.label) or getItemLabel(data.item)),
time = bartime,
cancel = true,
dict = animDict,
anim = anim,
flag = 49,
icon = data.item,
disableMovement = true,
request = true,
}) then
TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken)
currentToken = nil
if data.craft.hasCrafted ~= nil then
data.craftable.craftedItems[data.item] = true
triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems)
end
if data.craft.exp ~= nil then
craftingLevel += data.craft.exp.give
triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel)
end
if data.craftable.Recipes[1].oneUse == true then
removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
local breakId = GetSoundId()
PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
canReturn = false
end
else
break
end
end
else
end
if not crafted then
stopTempCam()
ClearPedTasks(PlayerPedId())
if canReturn then craftingMenu(data) end
CraftLock = false
return
end
-- Handle SingleProgress option
if Config.Crafting.SingleProgress then
local craftProp = nil
if prop then
craftProp = makeProp({ prop = prop.model, coords = vec4(0, 0, 0, 0), true, true })
AttachEntityToEntity(craftProp, PlayerPedId(), GetPedBoneIndex(PlayerPedId(), prop.bone), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, 1, true)
end
if data.sound then
local s = data.sound
PlaySoundFromEntity(s.soundId, s.audioName, PlayerPedId(), s.audioRef, true, 0)
end
if crafting and progressBar({
label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount,
label = bartext..((metadata and metadata.label) or Items[data.item].label).." x"..craftAmount,
time = totalBartime,
cancel = true,
dict = animDict,
anim = anim,
flag = 49,
icon = data.item,
disableMovement = true,
request = true,
}) then
data.craft.amount *= craftAmount
for k, v in pairs(data.craft[data.item]) do
-- multiply igredient requirement in sent crafting table for removal
data.craft[data.item][k] = (v * craftAmount)
end
data.craft.amount = craftAmount
TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken)
currentToken = nil -- clear client cached token
-- handle metadata and experience in a single go
if data.craft.hasCrafted ~= nil then
if data.craft["hasCrafted"] ~= nil then
data.craftable.craftedItems[data.item] = true
triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems)
triggerCallback(getScript()..":server:SetMetadata", "craftedItems", data.craftable.craftedItems)
end
if data.craft.exp ~= nil then
if data.craft["exp"] ~= nil then
craftingLevel += data.craft["exp"].give * craftAmount
triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel)
triggerCallback(getScript()..":server:SetMetadata", "craftingLevel", craftingLevel)
end
if data.craftable.Recipes[1].oneUse == true then
removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
local breakId = GetSoundId()
PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
canReturn = false
end
if data.sound then
StopSound(data.sound.soundId)
end
if data.requiredItemfunc then
data.requiredItemfunc()
end
end
if craftProp then destroyProp(craftProp) end
else
-- 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),
time = bartime,
cancel = true,
dict = animDict,
anim = anim,
flag = 49,
icon = data.item,
request = true,
}) then
TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken)
currentToken = nil
if data.craft["hasCrafted"] ~= nil then
data.craftable.craftedItems[data.item] = true
triggerCallback(getScript()..":server:SetMetadata", "craftedItems", data.craftable.craftedItems)
end
if data.craft["exp"] ~= nil then
craftingLevel += data.craft["exp"].give
triggerCallback(getScript()..":server:SetMetadata", "craftingLevel", craftingLevel)
end
if data.craftable.Recipes[1].oneUse == true then
removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
local breakId = GetSoundId()
PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
canReturn = false
end
if data.requiredItemfunc then
data.requiredItemfunc()
end
else
break
end
--if data.craft.Recipes[1].oneUse == true then
-- removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
-- local breakId = GetSoundId()
-- PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
-- canReturn = false
--end
end
end
::finishEarly::
if craftProp then destroyProp(craftProp) end
if data.sound then
StopSound(data.sound.soundId)
end
if data.requiredItemfunc then
data.requiredItemfunc()
end
--Wait(500)
stopCam(100)
Wait(500)
stopTempCam()
CraftLock = false
if canReturn then craftingMenu(origData) end
ClearPedTasks(Ped)
if canReturn then craftingMenu(data) end
ClearPedTasks(PlayerPedId())
end
@@ -560,13 +505,14 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
return
end
local hasItems, hasTable = hasItem(ItemMake, 1, src)
if stashName then
local itemRemove = {}
if type(stashName) == "table" then
for _, name in pairs(stashName) do
stashItems = getStash(name)
for k, v in pairs(craftable[ItemMake]) do
for k, v in pairs(craftable[ItemMake] or {}) do
for _, b in pairs(stashItems or {}) do
if k == b.name then
itemRemove[k] = v
@@ -576,7 +522,7 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
end
else
stashItems = getStash(stashName)
for k, v in pairs(craftable[ItemMake]) do
for k, v in pairs(craftable[ItemMake] or {}) do
for _, b in pairs(stashItems or {}) do
if k == b.name then
itemRemove[k] = v
@@ -586,8 +532,8 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
end
stashRemoveItem(stashItems, stashName, itemRemove)
else
if craftable[ItemMake] then
for k, v in pairs(craftable[ItemMake]) do
if craftable then
for k, v in pairs(craftable[ItemMake] or {}) do
removeItem(tostring(k), v, src)
end
end
@@ -596,5 +542,4 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
-- Optionally, add experience here:
-- for example:
-- if isStarted("core_skills") then exports["core_skills"]:AddExperience(src, 2) end
end)
end)

View File

@@ -5,114 +5,6 @@
various frameworks: QB, OX, GTA, and ESX.
]]
-- Text system handlers
local textHandlers = {
qb = {
show =
function(text, image)
if image then
text = '<img src="'..(radarTable[image] or "")..'" style="width:12px;height:12px">'..text
end
exports[QBExport]:DrawText(text, 'left')
end,
hide =
function()
exports[QBExport]:HideText()
end,
},
ox = {
show =
function(input, image, oxStyleTable)
local inputCount = #input
for i = 1, inputCount do
input[i] = input[i] .. (i ~= inputCount and " \n" or "")
end
lib.showTextUI(table.concat(input), {
icon = (image and radarTable[image] or image) or nil,
position = 'left-center',
style = oxStyleTable
})
end,
hide =
function()
lib.hideTextUI()
end,
},
lation = {
show =
function(input, image)
local inputCount = #input
for i = 1, inputCount do
input[i] = input[i] .. (i ~= inputCount and " \n" or "")
end
exports.lation_ui:showText({
description = table.concat(input),
keybind = nil,
icon = (image and radarTable[image] or image) or nil,
iconColor = '#3B82F6',
position = 'left-center'
})
end,
hide =
function()
exports.lation_ui:hideText()
end,
},
esx = {
show =
function(text, image)
if image then
text = '<img src="'..(radarTable[image] or "")..'" style="width:12px;height:12px">'..text
end
ESX.TextUI(text, nil)
end,
hide =
function()
ESX.HideUI()
end,
},
gta = {
show =
function(input, image, style)
local text = ""
for i = 1, #input do
if input[i] ~= "" then
text = text .. input[i] .. "\n~s~"
end
end
if image then
text = "~BLIP_" .. image .. "~ " .. text
end
DisplayHelpMsg(text:gsub("%:", ":~" .. (style or "g") .. "~"))
end,
hide =
function()
ClearAllHelpMessages()
end,
},
red = {
show =
function(input)
local text = ""
for i = 1, #input do
if input[i] ~= "" then
text = text .. input[i] .. "\n~q~"
end
end
TriggerEvent("jim-redui:DrawText", text)
end,
hide =
function()
TriggerEvent("jim-redui:HideText")
end,
}
}
--- Displays text on the screen using the configured draw text system.
---
--- Depending on Config.System.drawText, this function will use different methods to
@@ -128,33 +20,73 @@ local textHandlers = {
--- drawText("img_link", { "Test line 1", "Test Line 2" }, "~g~")
--- ```
function drawText(image, input, style, oxStyleTable)
local text = ""
if not radarTable then radarTable = {} end
local systemType = Config.System.drawText
local handler = textHandlers[systemType]
if not handler then return end
if systemType == "qb" or systemType == "esx" then
-- Concatenate lines for QB/ESX system with HTML line breaks.
local text = ""
if Config.System.drawText == "qb" then
-- Concatenate lines for QB system with HTML line breaks.
for i = 1, #input do
text = text .. input[i] .. "</span>" .. (input[i + 1] and "<br>" or "")
text = text..input[i].."</span>"..(input[i + 1] and "<br>" or "")
end
text = text:gsub("%:", ":<span style='color:yellow'>")
handler.show(text, image)
if image then
text = '<img src="'..(radarTable[image] or "")..'" style="width:12px;height:12px">'..text
end
exports[QBExport]:DrawText(text, 'left')
elseif systemType == "ox" then
handler.show(input, image, oxStyleTable)
elseif Config.System.drawText == "ox" then
-- Append newline spacing to each input line.
local inputnum = countTable(input)
for k, v in pairs(input) do
input[k] = v..(inputnum ~= k and " \n" or "")
end
lib.showTextUI(table.concat(input), { icon = (image and radarTable[image] or image) or nil, position = 'left-center', style = oxStyleTable })
elseif systemType == "lation" then
handler.show(input, image)
elseif Config.System.drawText == "lation" then
local inputnum = countTable(input)
for k, v in pairs(input) do
input[k] = v..(inputnum ~= k and " \n" or "")
end
exports.lation_ui:showText({
--title = " ",
description = table.concat(input),
keybind = nil,
icon = (image and radarTable[image] or image) or nil,
iconColor = '#3B82F6',
position = 'center-left'
})
elseif systemType == "gta" then
handler.show(input, image, style)
elseif Config.System.drawText == "gta" then
-- Concatenate input lines and apply GTA style formatting.
for i = 1, #input do
if input[i] ~= "" then
text = text..input[i].."\n~s~"
end
end
if image then
text = "~BLIP_"..image.."~ "..text
end
DisplayHelpMsg(text:gsub("%:", ":~"..(style or "g").."~"))
elseif Config.System.drawText == "esx" then
-- ESX-based text UI uses similar HTML formatting as QB.
for i = 1, #input do
text = text..input[i].."</span>"..(input[i + 1] and "<br>" or "")
end
text = text:gsub("%:", ":<span style='color:yellow'>")
if image then
text = '<img src="'..(radarTable[image] or "")..'" style="width:12px;height:12px">'..text
end
ESX.TextUI(text, nil)
elseif Config.System.drawText == "red" then
-- Concatenate input lines and apply GTA style formatting.
for i = 1, #input do
if input[i] ~= "" then
text = text..input[i].."\n~q~"
end
end
TriggerEvent("jim-redui:DrawText", text)
elseif systemType == "red" then
handler.show(input)
end
end
@@ -167,8 +99,17 @@ end
--- hideText()
--- ```
function hideText()
local handler = textHandlers[Config.System.drawText]
if handler and handler.hide then
handler.hide()
if Config.System.drawText == "qb" then
exports[QBExport]:HideText()
elseif Config.System.drawText == "ox" then
lib.hideTextUI()
elseif Config.System.drawText == "lation" then
exports.lation_ui:hideText()
elseif Config.System.drawText == "gta" then
ClearAllHelpMessages()
elseif Config.System.drawText == "esx" then
ESX.HideUI()
elseif Config.System.drawText == "red" then
TriggerEvent("jim-redui:HideText")
end
end

View File

@@ -7,10 +7,7 @@ function AlienEffect()
Wait(math.random(5000, 8000))
local Ped = PlayerPedId()
local animDict = "MOVE_M@DRUNK@VERYDRUNK"
RequestAnimSet(animDict)
while not HasAnimSetLoaded(animDict) do
Wait(100)
end
loadAnimDict(animDict)
SetPedCanRagdoll(Ped, true)
ShakeGameplayCam('DRUNK_SHAKE', 2.80)
SetTimecycleModifier("Drunk")

View File

@@ -56,14 +56,6 @@ function isServer()
return IsDuplicityVersion()
end
--- Allows the user to check if an export exists before trying to call it
--- @return boolean boolean True if it exists
---@usage
--- ```lua
--- if checkExportExists("qb-inventory", "OpenInventory") then
--- exports["qb-inventory"]:OpenInventory()
--- end
--- ```
function checkExportExists(resource, export)
if not resource or not export then return false end
@@ -81,6 +73,8 @@ function checkExportExists(resource, export)
end
end
-------------------------------------------------------------
-- Debugging and JSON Utilities
-------------------------------------------------------------
@@ -188,7 +182,6 @@ 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
@@ -233,9 +226,9 @@ end
function cv(amount)
local formatted = tostring(amount or "0")
while true do
local newFormatted, count = formatted:gsub("^(-?%d+)(%d%d%d)", '%1,%2')
if count == 0 then break end
formatted = newFormatted
formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
if k == 0 then break end
Wait(0)
end
return formatted
end
@@ -250,14 +243,13 @@ end
--- ```
function formatCoord(coord)
local vecType = type(coord):gsub("tor", "")
local parts = {}
if coord.x then parts[#parts + 1] = string.format("^6%.1f", coord.x) end
if coord.y then parts[#parts + 1] = string.format("^6%.1f", coord.y) end
if coord.z then parts[#parts + 1] = string.format("^6%.1f", coord.z) end
if coord.w then parts[#parts + 1] = string.format("^6%.1f", coord.w) end
return string.format("^5%s^7(%s^7)", vecType, table.concat(parts, "^7, "))
local components = {
[1] = coord.x and ("^6"..string.format("%.1f", coord.x)) or "",
[2] = coord.y and ("^7, ^6"..string.format("%.1f", coord.y)) or "",
[3] = coord.z and ("^7, ^6"..string.format("%.1f", coord.z)) or "",
[4] = coord.w and ("^7, ^6"..string.format("%.1f", coord.w)) or "",
}
return "^5"..vecType.."^7("..components[1]..components[2]..components[3]..components[4].."^7)"
end
--- Calculates the center point of a list of coordinates.
@@ -269,17 +261,13 @@ end
--- print("Center of Zones:", center)
--- ```
function getCenterOfZones(tbl)
local count = #tbl
if count == 0 then return vector3(0, 0, 0) end
local totalX, totalY, totalZ = 0, 0, 0
for i = 1, count do
local coord = tbl[i]
for _, coord in ipairs(tbl) do
totalX = totalX + coord.x
totalY = totalY + coord.y
totalZ = totalZ + coord.z
end
local count = #tbl
return vector3(totalX / count, totalY / count, totalZ / count)
end
@@ -292,9 +280,9 @@ end
--- print("Number of keys:", count)
--- ```
function countTable(tbl)
local count = 0
for _ in pairs(tbl) do count = count + 1 end
return count
local i = 0
for _ in pairs(tbl) do i += 1 end
return i
end
--- Returns an iterator over a table's keys in sorted order.
@@ -311,19 +299,7 @@ function pairsByKeys(t)
print("^1Error^7: ^3Nil ^2table recieved for ^3pairsByKeys^7(), ^2setting to ^7{} ^2to prevent break^7")
t = {}
end
local keys = {}
for key in pairs(t) do
keys[#keys + 1] = key
end
table.sort(keys)
local index = 0
return function()
index = index + 1
local key = keys[index]
return key, t[key]
end
local a = {} for n in pairs(t) do a[#a+1] = n end table.sort(a) local i = 0 local iter = function() i += 1 if a[i] == nil then return nil else return a[i], t[a[i]] end end return iter
end
--- Creates a new table with consecutive numerical indices sorted by the 'id' field.
@@ -337,18 +313,15 @@ end
--- end
--- ```
function createConsecutiveTable(originalTable)
local entries = {}
for _, entry in pairs(originalTable) do
entries[#entries + 1] = entry
local sortedEntries = {}
for _, entry in pairs(originalTable) do table.insert(sortedEntries, entry) end
table.sort(sortedEntries, function(a, b) return a.id < b.id end)
local newTable = {}
for newIndex, entry in ipairs(sortedEntries) do
entry.id = newIndex
newTable[newIndex] = entry
end
table.sort(entries, function(a, b) return a.id < b.id end)
for index, entry in ipairs(entries) do
entry.id = index
end
return entries
return newTable
end
--- Concatenates a table of strings into a single string separated by newlines.
@@ -360,7 +333,11 @@ end
--- print(combinedText)
--- ```
function concatenateText(tbl)
return table.concat(tbl, "\n")
local result = ""
for i = 1, #tbl do
result = result..tbl[i]..(i < #tbl and "\n" or "")
end
return result
end
--- Converts a rotation (degrees) to a direction vector.
@@ -372,14 +349,11 @@ end
--- print(direction)
--- ```
function RotationToDirection(rot)
local radianConvert = math.pi / 180
local rotX, rotZ = radianConvert * rot.x, radianConvert * rot.z
local cosX = math.abs(math.cos(rotX))
local adjust = math.pi / 180
return vec3(
-math.sin(rotZ) * cosX,
math.cos(rotZ) * cosX,
math.sin(rotX)
-math.sin(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)),
math.cos(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)),
math.sin(adjust * rot.x)
)
end
@@ -392,9 +366,11 @@ end
--- print(bar)
--- ```
function basicBar(percentage)
local perc = math.ceil(percentage)
local total = 10
local filled = math.floor(math.min(math.max(percentage, 0), 100) / 100 * total)
return string.rep("", filled) .. string.rep("", total - filled)
local filled = math.floor((perc / 100) * total)
local empty = total - filled
return string.rep("", filled)..string.rep("", empty)
end
--- Normalizes a 3D vector.
@@ -406,8 +382,12 @@ end
--- print(normalizedVec)
--- ```
function normalizeVector(vec)
local length = math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z)
return length > 0 and vec3(vec.x / length, vec.y / length, vec.z / length) or vec3(0, 0, 0)
local len = math.sqrt(vec.x^2 + vec.y^2 + vec.z^2)
if len ~= 0 then
return vec3(vec.x / len, vec.y / len, vec.z / len)
else
return vec3(0, 0, 0)
end
end
-------------------------------------------------------------
@@ -423,15 +403,16 @@ end
--- drawLine(vector3(100, 200, 300), vector3(150, 250, 350), vector4(255, 0, 0, 255))
--- ```
function drawLine(startCoords, endCoords, col)
if not debugMode then return end
local col = col or vec4(255, 255, 255, 150)
CreateThread(function()
for i = 100, 0, -1 do
DrawLine(startCoords.x, startCoords.y, startCoords.z, endCoords.x, endCoords.y, endCoords.z, col.x, col.y, col.z, col.w)
Wait(0)
end
end)
if debugMode then
CreateThread(function()
local count = 1000
while count >= 0 do
DrawLine(startCoords.x, startCoords.y, startCoords.z, endCoords.x, endCoords.y, endCoords.z, col.x, col.y, col.z, col.w)
count -= 10
Wait(0)
end
end)
end
end
--- Draws a sphere at the specified coordinates (for debugging).
@@ -442,15 +423,16 @@ end
--- drawSphere(vector3(100, 200, 300), vector4(0, 255, 0, 255))
--- ```
function drawSphere(coords, col)
if not debugMode then return end
local col = col or vec4(255, 255, 255, 150)
CreateThread(function()
for i = 100, 0, -1 do
DrawSphere(coords.x, coords.y, coords.z, 0.5, col.x, col.y, col.z, col.w)
Wait(10)
end
end)
if debugMode then
CreateThread(function()
local count = 1000
while count >= 0 do
DrawSphere(coords.x, coords.y, coords.z, 0.5, col.x, col.y, col.z, col.w)
count -= 1
Wait(10)
end
end)
end
end
--- Performs a raycast between two coordinates and returns the results.
@@ -468,15 +450,15 @@ end
--- end
--- ```
function PerformRaycast(startCoords, endCoords, entity, flags)
drawLine(startCoords, endCoords, vec4(0, 0, 255, 255))
local shapeTest = StartExpensiveSynchronousShapeTestLosProbe(
startCoords.x, startCoords.y, startCoords.z,
endCoords.x, endCoords.y, endCoords.z,
flags or 4294967295, entity, 4
drawLine(startCoords, endCoords, vec4(0,0,255,255))
local val1, val2, val3, val4, val5, val6 = GetShapeTestResult(
StartExpensiveSynchronousShapeTestLosProbe(
startCoords.x, startCoords.y, startCoords.z,
endCoords.x, endCoords.y, endCoords.z,
flags or 4294967295, entity, 0
)
)
return GetShapeTestResult(shapeTest)
return val1, val2, val3, val4, val5, val6
end
--- Adjusts the Z-coordinate of a position to the ground level.
@@ -488,34 +470,16 @@ end
--- print("Ground Position:", groundCoords)
--- ```
function adjustForGround(coords)
local foundGround, groundZ = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z + 1.0)
local foundGround, zPos = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z + 1.0)
if foundGround then
return coords.w and vec4(coords.x, coords.y, groundZ, coords.w) or vec3(coords.x, coords.y, groundZ)
if coords.w then
return vec4(coords.x, coords.y, zPos, coords.w)
else
return vec3(coords.x, coords.y, zPos)
end
else
return coords
end
return coords
end
local function waitForNetworkEntity(netId, converter, timeout)
timeout = timeout or 100
while not NetworkDoesNetworkIdExist(netId) and timeout > 0 do
timeout = timeout - 1
Wait(10)
end
if not NetworkDoesNetworkIdExist(netId) then return 0 end
local entity = converter(netId)
timeout = 100
while not DoesEntityExist(entity) and entity ~= 0 and timeout > 0 do
timeout = timeout - 1
Wait(10)
end
return DoesEntityExist(entity) and entity or 0
end
--- Ensures a network vehicle exists from its network ID.
@@ -529,7 +493,21 @@ end
--- end
--- ```
function ensureNetToVeh(vehNetID)
return waitForNetworkEntity(vehNetID, NetToVeh)
--debugPrint("^6Bridge^7: ^3ensureNetToVeh^7: ^2Requesting NetworkDoesNetworkIdExist^7(^6"..vehNetID.."^7)")
local timeout = 100
while not NetworkDoesNetworkIdExist(vehNetID) and timeout > 0 do
timeout -= 1
Wait(10)
end
if not NetworkDoesNetworkIdExist(vehNetID) then return 0 end
timeout = 100
local vehicle = NetToVeh(vehNetID)
while not DoesEntityExist(vehicle) and vehicle ~= 0 and timeout > 0 do
timeout -= 1
Wait(10)
end
if not DoesEntityExist(vehicle) then return 0 end
return vehicle
end
--- Ensures a network entity exists from its network ID.
@@ -543,7 +521,21 @@ end
--- end
--- ```
function ensureNetToEnt(entNetID)
return waitForNetworkEntity(entNetID, NetworkGetEntityFromNetworkId)
--debugPrint("^6Bridge^7: ^3ensureNetToEnt^7: ^2Requesting NetworkDoesNetworkIdExist^7(^6"..entNetID.."^7)")
local timeout = 100
while not NetworkDoesNetworkIdExist(entNetID) and timeout > 0 do
timeout -= 1
Wait(10)
end
if not NetworkDoesNetworkIdExist(entNetID) then return 0 end
timeout = 100
local entity = NetworkGetEntityFromNetworkId(entNetID)
while not DoesEntityExist(entity) and entity ~= 0 and timeout > 0 do
timeout -= 1
Wait(10)
end
if not DoesEntityExist(entity) then return 0 end
return entity
end
function sendLog(text)
@@ -588,96 +580,11 @@ RegisterNetEvent(getScript()..":server:sendlog", sendServerLog)
-- local hungerAmount = GetRandomTiming(hunger)
-- print(hungerAmount) -- number between 10, 20
function GetRandomTiming(tbl)
return type(tbl) == "table" and math.random(tbl[1], tbl[2]) or tbl
end
-------------------------------------------------------------
-- Player Movement
-------------------------------------------------------------
--- Instantly turns an entity to face a target (entity or coordinates) without animation.
---
--- @param ent number|nil The Ped to turn (defaults to player's Ped if nil).
--- @param ent2 number|vector3|nil The target entity or coordinates to face.
---
--- @usage
--- ```lua
--- instantLookEnt(nil, vector3(200.0, 300.0, 40.0))
--- instantLookEnt(ped1, ped2)
--- ```
function instantLookEnt(ent, ent2)
local ped = ent or PlayerPedId()
local p1 = GetEntityCoords(ped, true)
local p2 = type(ent2) == "vector3" and ent2 or GetEntityCoords(ent2, true)
local dx = p2.x - p1.x
local dy = p2.y - p1.y
local heading = GetHeadingFromVector_2d(dx, dy)
debugPrint("^6Bridge^7: ^1Forced ^2Turning Player to^7: '^6"..formatCoord(p2).."^7'")
SetEntityHeading(ped, heading)
end
--- Makes the player look towards a specific target with an animated turn.
---
--- If the player is not already facing the target (entity or coordinates), a turning animation is triggered.
---
--- @param entity number|vector3|vector4|nil The target to look at.
---
--- @usage
--- ```lua
--- lookEnt(vector3(200.0, 300.0, 40.0))
--- lookEnt(pedEntity)
--- ```
function lookEnt(entity)
local ped = PlayerPedId()
if entity then
if type(entity) == "vector3" or type(entity) == "vector4" then
if not IsPedHeadingTowardsPosition(ped, entity.xyz, 30.0) then
TaskTurnPedToFaceCoord(ped, entity.xyz, 1500)
debugPrint("^6Bridge^7: ^2Turning Player to^7: '^6"..formatCoord(entity).."^7'")
Wait(1500)
end
-- Make players head face the coord for 5 seconds
TaskLookAtCoord(ped, entity.x, entity.y, entity.z, 5000, 1, 1)
else
if DoesEntityExist(entity) then
local entCoords = GetEntityCoords(entity)
if not IsPedHeadingTowardsPosition(ped, entCoords, 30.0) then
TaskTurnPedToFaceCoord(ped, entCoords, 1500)
debugPrint("^6Bridge^7: ^2Turning Player to^7: '^6"..entity.."^7' - '"..formatCoord(entCoords).."^7'")
Wait(1500)
end
-- Make players head face the coord for 5 seconds
TaskLookAtEntity(ped, entity, 5000, 1, 1)
end
end
end
end
-- Function to clone tables, to use when referencing tables that need to be hard set
function cloneTable(obj, opts, seen)
if type(obj) ~= "table" then return obj end
seen = seen or {}
if seen[obj] then return seen[obj] end
local copy = {}
seen[obj] = copy
-- Copy entries
for k, v in pairs(obj) do
local k2 = (opts and opts.copy_keys) and cloneTable(k, opts, seen) or k
copy[k2] = cloneTable(v, opts, seen)
end
-- Preserve metatable unless told not to
if not (opts and opts.strip_meta) then
local mt = getmetatable(obj)
if mt ~= nil then setmetatable(copy, mt) end
end
return copy
if type(tbl) == "table" then
return math.random(tbl[1], tbl[2])
else
return tbl
end
end
-------------------------------------------------------------
@@ -913,9 +820,9 @@ local materials = {
--- local matHash, matName = GetGroundMaterialAtPosition(vector3(100,200,300))
--- print("Material:", matName)
--- ```
function GetGroundMaterialAtPosition(coords, Ped)
local endCoords = vec3(coords.x, coords.y, coords.z - 1.1)
local rayHandle = StartShapeTestCapsule(coords.x, coords.y, coords.z, endCoords.x, endCoords.y, endCoords.z, 1.0, 1, Ped or PlayerPedId(), 7)
function GetGroundMaterialAtPosition(coords)
local endX, endY, endZ = coords.x, coords.y, coords.z - 1.0
local rayHandle = StartShapeTestCapsule(coords.x, coords.y, coords.z, endX, endY, endZ, 1.0, 1, playerPed, 7)
local _, hit, _, _, materialHash, _ = GetShapeTestResultIncludingMaterial(rayHandle)
local materialName = "Unknown"
for k, v in pairs(materials) do
@@ -961,18 +868,4 @@ end
function GetEntityForwardVector(entity)
local heading = math.rad(GetEntityHeading(entity) + 90)
return vec3(math.cos(heading), math.sin(heading), 0.0)
end
function adjustMinMaxZ(coord, table)
local table = table
local adMinZ, adMaxZ = false, false
if table.minZ > (coord.z + 0.1) then
adMinZ = true
table.minZ = (coord.z - 1.05)
end
if table.maxZ < (coord.z - 0.1) then
adMinZ = true
table.maxZ = (coord.z + 0.80)
end
return table.minZ, table.maxZ, adMinZ, adMaxZ
end

View File

@@ -64,7 +64,6 @@ function createInput(title, opts)
label = (opts[i].label or opts[i].text)..(opts[i].txt and " - "..opts[i].txt or ""),
isRequired = opts[i].isRequired,
name = opts[i].name,
default = opts[i].default,
options = opts[i].options,
}
end

246
shared/inventories.lua Normal file
View File

@@ -0,0 +1,246 @@
-------------------------------------------------------------
-- Item Availability & Inventory Retrieval
-------------------------------------------------------------
---
--- Locks or unlocks the player's inventory.
--- Freezes/unfreezes the player's position, sets inventory busy state, and toggles hotbar usage.
---
--- @param toggle boolean True to lock inventory; false to unlock.
---
--- @usage
--- ```lua
--- lockInv(true) -- Lock inventory.
--- lockInv(false) -- Unlock inventory.
--- ```
function lockInv(toggle)
FreezeEntityPosition(PlayerPedId(), toggle)
LocalPlayer.state:set("inv_busy", toggle, true)
LocalPlayer.state:set("invBusy", toggle, true)
TriggerEvent('inventory:client:busy:status', toggle)
TriggerEvent('canUseInventoryAndHotbar:toggle', not toggle)
end
--- Checks if a player has the specified items in their inventory.
---
--- Verifies whether the required quantities are present. Returns a boolean and a table of details.
---
--- @param items string|table A single item name or table with required amounts.
--- @param amount number The required quantity (default 1).
--- @param src number|nil Player source ID (defaults to caller).
--- @return boolean boolean True if all items are available; otherwise, false.
--- @return table|nil table Table detailing counts for each item.
---
---@usage
--- ```lua
--- local hasAll, details = hasItem({"health_potion", "mana_potion"}, 2, playerId)
--- if hasAll then
--- -- Proceed with action
--- else
--- -- Inform the player about missing items
--- end
--- ```
function hasItem(items, amount, src)
local amount = amount and amount or 1
local grabInv, foundInv = getPlayerInv(src)
if type(items) ~= "table" then items = { [items] = amount and amount or 1, } end
if grabInv then
local hasTable = {}
for item, amt in pairs(items) do
if not Items[item] then print("^4ERROR^7: ^2Script can't find ingredient item in Shared Items - ^1"..item.."^7") end
local count = 0
for _, itemData in pairs(grabInv) do
--jsonPrint(itemData)
if itemData and itemData.name == item then
count += (itemData.amount or itemData.count or 1)
end
end
foundInv = foundInv:gsub("%-", "^7-^6"):gsub("%_", "^7_^6")
local foundMessage = "^6Bridge^7: ^3hasItem^7[^6"..foundInv.."^7]: "..tostring(item).." ^3"..count.."^7/^3"..amt
if count >= amt then foundMessage = foundMessage.." ^5FOUND^7" else foundMessage = foundMessage .." ^1NOT FOUND^7" end
debugPrint(foundMessage)
hasTable[item] = { hasItem = count >= amt, count = count }
end
for k, v in pairs(hasTable) do
if not v.hasItem then
return false, hasTable
end
end
return true, hasTable
end
-- if can't find inventory, return false
print("^1Error^7: ^1Can't find players inventory for some reason")
return false, {}
end
--- Retrieves a player's inventory based on the active inventory system.
---
--- @param src number|nil The player source ID (if nil, retrieves current player's inventory).
--- @return table|nil table The inventory items.
--- @return string|nil string The name of the inventory system.
---
---@usage
--- ```lua
--- local inventory, system = getPlayerInv(playerId)
--- if inventory then
--- -- Process inventory
--- end
--- ```
function getPlayerInv(src)
local grabInv = nil
local foundInv = ""
if isStarted(OXInv) then
foundInv = OXInv
if src then
grabInv = exports[OXInv]:GetInventoryItems(src)
else
grabInv = exports[OXInv]:GetPlayerItems()
end
elseif isStarted(QSInv) then
foundInv = QSInv
if src then
grabInv = exports[QSInv]:GetInventory(src)
else
grabInv = exports[QSInv]:getUserInventory()
end
elseif isStarted(OrigenInv) then
foundInv = OrigenInv
if src then
grabInv = exports[OrigenInv]:getInventory(src)
else
grabInv = exports[OrigenInv]:getInventory()
end
elseif isStarted(CoreInv) then
foundInv = CoreInv
if src then
grabInv = exports[CoreInv]:getInventory(src)
else
grabInv = exports[CoreInv]:getInventory()
end
elseif isStarted(CodeMInv) then
foundInv = CodeMInv
if src then
grabInv = exports[CodeMInv]:GetInventory(getPlayer(src).citizenId, src)
else
grabInv = exports[CodeMInv]:getUserInventory()
end
elseif isStarted(TgiannInv) then
foundInv = TgiannInv
if src then
grabInv = exports[TgiannInv]:GetPlayerItems(src)
else
grabInv = exports[TgiannInv]:GetPlayerItems()
end
elseif isStarted(JPRInv) then
foundInv = JPRInv
if src then
grabInv = Core.Functions.GetPlayer(src).PlayerData.items
else
grabInv = Core.Functions.GetPlayerData().items
end
elseif isStarted(QBInv) then
foundInv = QBInv
if src then
grabInv = Core.Functions.GetPlayer(src).PlayerData.items
else
grabInv = Core.Functions.GetPlayerData().items
end
elseif isStarted(PSInv) then
foundInv = PSInv
if src then
grabInv = Core.Functions.GetPlayer(src).PlayerData.items
else
grabInv = Core.Functions.GetPlayerData().items
end
elseif ESX and isStarted(ESXExport) then
foundInv = ESX
if src then
local xPlayer = ESX.GetPlayerFromId(src)
grabInv = xPlayer.inventory
else
local xPlayer = ESX.GetPlayerData() -- Client side, if available
grabInv = xPlayer.inventory
end
elseif isStarted(RSGInv) then
foundInv = RSGInv
if src then
grabInv = Core.Functions.GetPlayer(src).PlayerData.items
else
grabInv = Core.Functions.GetPlayerData().items
end
else
print("^4ERROR^7: ^2No Inventory detected ^7- ^2Check ^3starter^1.^2lua^7")
end
return grabInv, foundInv
end
function isInventoryOpen()
return IsNuiFocused()
end
-------------------------------------------------------------
-- Item Image Retrieval
-------------------------------------------------------------
--- Retrieves the NUI link for an item's image from the active inventory system.
---
--- @param item string The item name.
--- @return string string A `nui://` link to the item's image, or an empty string if not found.
---
--- @usage
--- ```lua
--- local imageLink = invImg("health_potion")
--- if imageLink ~= "" then print(imageLink) end
--- ```
function invImg(item)
local imgLink = ""
if item ~= "" and Items[item] then
if isStarted(OXInv) then
imgLink = "nui://"..OXInv.."/web/images/"..(Items[item].image or "")
elseif isStarted(QSInv) then
imgLink = "nui://"..QSInv.."/html/images/"..(Items[item].image or "")
elseif isStarted(CoreInv) then
imgLink = "nui://"..CoreInv.."/html/img/"..(Items[item].image or "")
elseif isStarted(CodeMInv) then
imgLink = "nui://"..CodeMInv.."/html/itemimages/"..(Items[item].image or "")
elseif isStarted(OrigenInv) then
imgLink = "nui://"..OrigenInv.."/html/img/"..(Items[item].image or "")
elseif isStarted(JPRInv) then
imgLink = "nui://"..JPRInv.."/html/images/"..(Items[item].image or "")
elseif isStarted(QBInv) then
imgLink = "nui://"..QBInv.."/html/images/"..(Items[item].image or "")
elseif isStarted(PSInv) then
imgLink = "nui://"..PSInv.."/html/images/"..(Items[item].image or "")
elseif isStarted(TgiannInv) then
imgLink = "nui://inventory_images/images/"..(Items[item].image or "")
elseif isStarted(RSGInv) then
imgLink = "nui://"..RSGInv.."/html/images/"..(Items[item].image or "")
else
print("^4ERROR^7: ^2No Inventory detected for invImg - Check starter.lua")
end
end
return imgLink
end

View File

@@ -73,8 +73,8 @@ if not isServer() then
--- local isPlayerAnimal = isAnimal()
--- local isSpecificPedAnimal = isAnimal(somePedEntity)
--- ```
function isPedAnimal(Ped)
local PedModel = GetEntityModel(Ped or PlayerPedId())
function isPedAnimal(ped)
local PedModel = GetEntityModel(ped or PlayerPedId())
for _, animalCategory in pairs(AnimalPeds) do
for animalModelHash, _ in pairs(animalCategory) do
if PedModel == animalModelHash then
@@ -105,8 +105,8 @@ if not isServer() then
--- print("Driver is a cat!")
--- end
--- ```
function isCat(Ped)
local PedModel = GetEntityModel(Ped or PlayerPedId())
function isCat(ped)
local PedModel = GetEntityModel(ped or PlayerPedId())
for modelHash, _ in pairs(AnimalPeds.CatPeds) do
if PedModel == modelHash then
debugPrint("^4isAnimal^7: ^2Ped is ^4Cat")
@@ -148,8 +148,8 @@ if not isServer() then
--- end
--- end
--- ```
function isDog(Ped)
local PedModel = GetEntityModel(Ped or PlayerPedId())
function isDog(ped)
local PedModel = GetEntityModel(ped or PlayerPedId())
for modelHash, _ in pairs(AnimalPeds.BigDogs) do
if PedModel == modelHash then
debugPrint("^4isAnimal^7: ^2Ped is ^4Dog")
@@ -199,8 +199,8 @@ if not isServer() then
--- local getAnim = getAnimalAnims(ped)
--- playAnim(getAnim.sitDict, getAnim.sitAnim, -1, 1)
--- ```
function getAnimalAnims(Ped)
local model = GetEntityModel(Ped)
function getAnimalAnims(ped)
local model = GetEntityModel(ped)
local animalTable = {}
for _, animalCategory in pairs(AnimalPeds) do
for k, v in pairs(animalCategory) do
@@ -284,4 +284,4 @@ AnimalPeds = {
[`a_c_rhesus`] = { deathAnim = "dead", deathDict = "dead_a", exitAnim = "get_up@sat_on_floor@to_stand", exitDict = "getup_0" },
[`ft-capmonkey2`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,73 +9,6 @@
and teleporting via doors.
]]
local bossMenuFunc = {
{ name = "qb-management",
openBossMenu = function(isGang, group)
if isGang then
TriggerEvent("qb-gangmenu:client:OpenMenu")
else
TriggerEvent("qb-bossmenu:client:OpenMenu")
end
end,
},
{ name = "qbx_management",
openBossMenu = function(isGang, group)
if isGang then
exports["qbx_management"]:OpenBossMenu("gang")
else
exports["qbx_management"]:OpenBossMenu("job")
end
end,
},
{ name = "esx_society",
openBossMenu = function(isGang, group)
TriggerServerEvent(getScript()..":registerESXSociety", isGang, group)
TriggerEvent('esx_society:openBossMenu', group, function() end, { wash = false })
end,
},
{ name = "tss-bossmenu",
openBossMenu = function(isGang, group)
TriggerEvent("tss-bossmenu:client:OpenMenu")
end,
},
{ name = "okokBossMenu",
openBossMenu = function(isGang, group)
ExecuteCommand('openbossmenu')
end,
},
}
function openBossMenu(isGang, group)
for i = 1, #bossMenuFunc do
local bossmenu = bossMenuFunc[i]
if isStarted(bossmenu.name) then
bossmenu.openBossMenu(isGang, group)
return
end
end
end
-- Register ESX Society account in the server
RegisterNetEvent(getScript()..":registerESXSociety", function(group)
local checkExist = exports.esx_society:GetSociety(group)
if checkExist == nil then
exports.esx_society:registerSociety(
group,
Gangs[group] and Gangs[group].label or Jobs[group] and Jobs[group].label,
group,
group,
group,
{type = "public"}
)
end
end)
-------------------------------------------------------------
-- Global Duty Status
-------------------------------------------------------------
@@ -167,57 +100,20 @@ end
--- toggleDuty() -- Player receives a notification of their new duty status.
--- ```
function toggleDuty()
local dutyFunc = {
{ framework = QBExport,
func = function()
TriggerServerEvent("QBCore:ToggleDuty")
Wait(100)
onDuty = getPlayer().onDuty
end
},
{ framework = QBXExport,
func = function()
TriggerServerEvent("QBCore:ToggleDuty")
Wait(100)
onDuty = getPlayer().onDuty
end
},
{ framework = RSGExport,
func = function()
TriggerServerEvent("RSGCore:ToggleDuty")
Wait(100)
onDuty = getPlayer().onDuty
end
},
{ framework = ESXExport,
func = function()
local tempJob = ESX.GetPlayerData().job
tempJob.onDuty = not onDuty
ESX.SetPlayerData("job", tempJob)
onDuty = getPlayer().onDuty
if onDuty then
triggerNotify(nil, "Now on duty", "success")
else
triggerNotify(nil, "Now off duty", "success")
end
end
},
}
for i = 1, #dutyFunc do
local framework = dutyFunc[i]
if isStarted(framework.framework) then
framework.func()
return
end
end
-- fallback
onDuty = not onDuty
if onDuty then
triggerNotify(nil, "Now on duty", "success")
if isStarted(QBExport) or isStarted(QBXExport) then
TriggerServerEvent("QBCore:ToggleDuty")
Wait(100)
onDuty = getPlayer().onDuty
elseif isStarted(RSGExport) then
TriggerServerEvent("RSGCore:ToggleDuty")
Wait(100)
onDuty = getPlayer().onDuty
else
triggerNotify(nil, "Now off duty", "success")
if onDuty then
triggerNotify(nil, "Now on duty", "success")
else
triggerNotify(nil, "Now off duty", "success")
end
end
end
@@ -334,4 +230,20 @@ function useDoor(data)
SetEntityHeading(PlayerPedId(), data.telecoords.w)
DoScreenFadeIn(1000)
Wait(100)
end
function openBossMenu(isGang, group)
if isStarted("qb-management") then
if isGang then
TriggerEvent("qb-gangmenu:client:OpenMenu")
else
TriggerEvent("qb-bossmenu:client:OpenMenu")
end
elseif isStarted("qbx_management") then
exports["qbx_management"]:OpenBossMenu(isGang and "gang" or "job")
elseif isStarted("esx_society") then
TriggerEvent('esx_society:openBossMenu', group, function() end, { wash = false })
end
end

View File

@@ -1,5 +1,3 @@
local camCache = {}
--- Creates a temporary camera at a specified position, pointing towards given coordinates.
--
-- This function creates a camera at a position relative to an entity or at a specified position and orients it to look at the target coordinates.
@@ -9,137 +7,58 @@ local camCache = {}
-- If `ent` is an entity, the camera position is calculated as an offset from the entity's position using `GetOffsetFromEntityInWorldCoords`.
-- If `ent` is a `vector3`, it is used directly as the camera's position.
--
---@param coords vector3|entityId The target `vector3` coordinates that the camera will point at.
---@param coords vector3 The target `vector3` coordinates that the camera will point at.
--
---@return camID number The handle of the created camera, or `nil` if the camera was not created (e.g., if `Config.Crafting.craftCam` is `false`).
---@return cam camID The handle of the created camera, or `nil` if the camera was not created (e.g., if `Config.Crafting.craftCam` is `false`).
--
---@usage
-- ```lua
-- local cam = createTempCam(entity, targetCoords)
-- ```
function createTempCam(ent, coords)
local camID = nil
if (Config.Crafting and Config.Crafting.craftCam) or Config.System.enableCam then
-- if not ent or coords are provided, make a basic camera to control later
if not ent and not coords then
camID = CreateCam("DEFAULT_SCRIPTED_CAMERA", true)
camCache[#camCache+1] = camID
return camID
local cam = nil
if Config.Crafting.craftCam then
if debugMode then
triggerNotify(nil, "ModCam Created", "success")
end
local camCoords = nil
local pointCoords = nil
if type(ent) ~= "vector3" then
camCoords = GetOffsetFromEntityInWorldCoords(ent, 1.0, -0.3, 0.8)
else
camCoords = ent
end
-- if received a vector3 or vector4 use those coords for origin point, otherwrise get offset from entity
local camCoords = type(ent) ~= "number" and ent or GetOffsetFromEntityInWorldCoords(ent, 1.0, -0.3, 0.8)
cam = CreateCamWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z + 0.5, 1.0, 0.0, 0.0, 60.00, false, 0)
-- Create the camera
camID = CreateCamWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z + 0.5, 1.0, 0.0, 0.0, 60.00, false, 0)
camCache[#camCache+1] = camID
debugPrint("^6Bridge^7: ^2Custom Camera Created", camID)
--if type(coords) == "number" then
-- SetCamCoord(camID, GetCamCoord(camID) + vec3(0, 0, 1.0))
--end
if coords then
camLookAt(camID, coords)
if type(coords) == "number" then
SetCamCoord(cam, GetCamCoord(cam) + vec3(0, 0, 1.0))
PointCamAtEntity(cam, coords)
else
PointCamAtCoord(cam, coords)
end
end
return camID
return cam
end
local cacheCameraEffect = {}
local cachePrevCam = nil
--- Activates and starts rendering the temporary camera.
--
-- This function sets the specified camera as active and begins rendering it with a smooth transition.
-- The camera is only activated if `Config.Crafting.craftCam` is enabled in the configuration.
--
---@param cam number The handle of the camera to activate and render.
---@param renderTime number The handle of the camera to activate and render.
---@param loadScene boolean The handle of the camera to activate and render.
---@param filter table The filter or postFx to render when starting the camera.
---@param cam camID The handle of the camera to activate and render.
--
---@usage
-- ```lua
-- startTempCam(camID, 1000, true, { postFx = "HeistCelebEnd" })
-- startTempCam(cam)
-- ```
function startTempCam(cam, renderTime, loadScene, filter, switchCam)
if cam and DoesCamExist(cam) then
debugPrint("Starting camera")
-- if moving from a cached previous cam, or you've sent a camre id for it to switch from, interpolate to it
if switchCam or cachePrevCam then
SetCamActiveWithInterp(cam, cachePrevCam, renderTime or 1000, 0, 0)
SetCamActive(cachePrevCam, false) -- Set previous cam inactive (shouldn't be needed but just in case)
else
SetCamActive(cam, true)
end
cachePrevCam = cam
-- Clear previous filters
if cacheCameraEffect.postFx then
AnimpostfxStop(cacheCameraEffect.postFx)
cacheCameraEffect.postFx = nil
end
if cacheCameraEffect.timecycle then
ClearTimecycleModifier()
cacheCameraEffect.timecycle = nil
end
-- Apply filters (timecycle) here as requested
if filter and filter.modifier then
SetTimecycleModifier(filter.modifier)
SetTimecycleModifierStrength((filter.strength or 1.0) + 0.0)
cacheCameraEffect.timecycle = true
end
if filter and filter.postFx then
AnimpostfxPlay(filter.postFx, 0, filter.loop and true or false)
cacheCameraEffect.postFx = filter.postFx
end
if loadScene then
loadLocation(cam, nil, 100.0)
end
RenderScriptCams(true, true, renderTime or 1000, true, true)
function startTempCam(cam)
if Config.Crafting.craftCam then
SetCamActive(cam, true)
RenderScriptCams(true, true, 1000, true, true)
end
end
-- Follow cam or coords
function camLookAt(cam, entCoords)
if cam and DoesCamExist(cam) and entCoords then
if type(entCoords) ~= "number" then
PointCamAtCoord(cam, entCoords.xyz)
else
PointCamAtEntity(cam, entCoords)
end
end
end
function loadLocation(cam, pos, radius)
local pos = pos or GetCamCoord(cam)
SetFocusPosAndVel(pos.x, pos.y, pos.z, 0.0, 0.0, 0.0)
RequestCollisionAtCoord(pos.x, pos.y, pos.z)
NewLoadSceneStart(pos.x, pos.y, pos.z, 0.0, 0.0, 0.0, radius or 100.0, 0)
local t0 = GetGameTimer()
while not IsNewLoadSceneLoaded() and (GetGameTimer() - t0) < 2000 do
RequestCollisionAtCoord(pos.x, pos.y, pos.z)
Wait(0)
end
NewLoadSceneStop()
end
function clearLoadLocation()
ClearFocus()
NewLoadSceneStop()
end
--- Deactivates the temporary camera and stops rendering.
--
-- This function waits for one second, then stops rendering script cameras and destroys all cameras.
@@ -151,34 +70,12 @@ end
-- ```lua
-- stopTempCam()
-- ```
function stopTempCam(renderTime)
CreateThread(function()
Wait(1000)
cachePrevCam = nil
-- Clear previous filters
if cacheCameraEffect.postFx then
AnimpostfxStop(cacheCameraEffect.postFx)
cacheCameraEffect.postFx = nil
end
if cacheCameraEffect.timecycle then
ClearTimecycleModifier()
cacheCameraEffect.timecycle = nil
end
RenderScriptCams(false, true, renderTime or 500, true, true)
clearLoadLocation()
Wait(renderTime or 500)
for i = 1, #camCache do
DestroyCam(camCache[i], true)
end
camCache = {}
end)
end
function createCam(...) return createTempCam(...) end
function startCam(...) return startTempCam(...) end
function stopCam(...) return stopTempCam(...) end
function stopTempCam()
if Config.Crafting.craftCam then
CreateThread(function()
Wait(1000)
RenderScriptCams(false, true, 500, true, true)
DestroyAllCams()
end)
end
end

View File

@@ -187,7 +187,7 @@ end
--- ```
function playAnim(animDict, animName, duration, flag, ped, speed)
loadAnimDict(animDict)
debugPrint("^6Bridge^7: ^3playAnim^7() ^2Triggered^7: ", animDict, animName, flag)
debugPrint("^6Bridge^7: ^3playAnim^7() ^2Triggered^7: ", animDict, animName)
TaskPlayAnim(ped and ped or PlayerPedId(), animDict, animName, speed or 8.0, speed or -8.0, duration or 30000, flag or 50, 1, false, false, false)
end
@@ -238,25 +238,4 @@ function playGameSound(audioBank, soundSet, soundRef, coords, synced, range)
PlaySoundFromEntity(soundId, soundRef, coords, soundSet, synced, 1.0)
end
ReleaseScriptAudioBank(audioBank)
end
-- Experimental, add fade in for spawned entities
function fadeInEnt(ent, duration)
if not DoesEntityExist(ent) then return end
duration = duration or 500 -- in ms
local fadeSteps = 20
local stepTime = duration / fadeSteps
SetEntityAlpha(ent, 0, false)
SetEntityVisible(ent, true, false)
SetEntityLocallyInvisible(ent) -- sometimes helps prevent "pop-in" in early frames
for i = 1, fadeSteps do
Wait(stepTime)
local newAlpha = math.floor((i / fadeSteps) * 255)
SetEntityAlpha(ent, newAlpha, false)
end
-- Restore full visibility
ResetEntityAlpha(ent)
end

View File

@@ -133,19 +133,16 @@ function makeEntityBlip(data)
AddTextComponentString(tostring(data.name))
EndTextCommandSetBlipName(blip)
-- Handle preview image if certain resources are running
if isStarted("jim-blipcontroller") then
if isStarted("fs_smallresources") or isStarted("blip-info") or isStarted("blipinfo") then
if data.preview then
local txname = string.gsub(tostring(data.name..'preview'..string.gsub(data.coords.z, "%.", "")), "[ ()~]", "")
local txname = string.gsub(tostring(data.name..'preview'), "[ ()~]", "")
if data.preview:find("http") or data.preview:find("nui") then
createDui(txname, data.preview, vec2(512, 256), scriptTxd)
else
CreateRuntimeTextureFromImage(scriptTxd, txname, data.preview)
end
exports["jim-blipcontroller"]:ShowBlipInfo(blip, {
title = data.name,
dict = getScript()..'scriptTxd',
tex = txname,
})
exports["fs_smallresources"]:SetBlipInfoImage(blip, getScript()..'previewTxd', txname)
exports["fs_smallresources"]:SetBlipInfoTitle(blip, data.name, false)
end
end
end

View File

@@ -1,6 +1,5 @@
--- A table to keep track of all created Peds.
local Peds = {}
local distPeds = {}
--- Creates a distance-based Ped (pedestrian) that spawns when the player enters a specified area.
--
@@ -19,20 +18,18 @@ local distPeds = {}
-- ```lua
-- makeDistPed(pedData, pedCoords, true, false, 'WORLD_HUMAN_STAND_IMPATIENT', nil, true)
-- ```
function makeDistPed(data, coords, freeze, collision, scenario, anim, synced, func)
function makeDistPed(data, coords, freeze, collision, scenario, anim, synced)
local zoneCoords = type(data) == "table" and data.coords or coords
local randName = keyGen()..keyGen()
distPeds[#distPeds+1] = createCirclePoly({
createCirclePoly({
name = randName,
coords = vec3(zoneCoords.x, zoneCoords.y, zoneCoords.z - 1.03),
radius = 50.0,
onEnter = function()
Peds[randName] = makePed(data, coords, freeze, collision, scenario, anim, synced)
if func then func(Peds[randName]) end
end,
onExit = function()
DeletePed(Peds[randName])
Peds[randName] = nil
end,
debug = debugMode,
})
@@ -58,13 +55,14 @@ end
-- ```lua
-- local ped = makePed(pedData, pedCoords, true, false, nil, {'animDict', 'animName'}, true)
-- ```
function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
function makePed(data, coords, freeze, collision, scenario, anim, synced)
local ped = nil
local model = nil
if type(data) == "table" then
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)
-- 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)
@@ -118,7 +116,6 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
loadModel(model)
if gameName == "rdr3" then
ped = CreatePed(model, coords.x, coords.y, coords.z - 1.03, coords.w, synced or false, false)
SetEntityAlpha(ped, 0, false)
SetEntityVisible(ped, 1) -- SetEntityVisible
SetEntityAlpha(ped, 255, false) -- SetEntityAlpha
SetRandomOutfitVariation(ped, true) -- Invisible without
@@ -144,12 +141,6 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
end
unloadModel(model)
Peds[keyGen()..keyGen()] = ped
if fade ~= false and gameName ~= "rdr3" then
SetEntityAlpha(ped, 0, false)
CreateThread(function()
fadeInEnt(ped)
end)
end
return ped
end
@@ -247,19 +238,9 @@ function GenerateRandomPedData(data)
return newTable
end
onPlayerUnload(function()
for k in pairs(Peds) do
DeletePed(Peds[k])
end
for i = 1, #distPeds do
removeZoneTarget(distPeds[i])
end
distPeds = {}
end)
--- Cleans up all created Peds when the resource stops.
onResourceStop(function()
for k in pairs(Peds) do
DeletePed(Peds[k])
end
end, true)
end, true)

View File

@@ -1,5 +1,4 @@
local Props = {}
local distProps = {}
--- Creates a prop (object) in the game world at specified coordinates.
---
@@ -21,7 +20,7 @@ local distProps = {}
--- }
--- local prop = makeProp(propData, true, false)
--- ```
function makeProp(data, freeze, synced, fade)
function makeProp(data, freeze, synced)
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)
SetEntityHeading(prop, (data.coords.w or 0) + 180.0)
@@ -30,12 +29,6 @@ function makeProp(data, freeze, synced, fade)
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 and gameName ~= "rdr3" then
SetEntityAlpha(prop, 0, false)
CreateThread(function()
fadeInEnt(prop)
end)
end
return prop
end
@@ -58,21 +51,17 @@ end
--- }
--- makeDistProp(propData, true, false)
--- ```
function makeDistProp(data, freeze, synced, range, func)
function makeDistProp(data, freeze, synced, range)
local name = keyGen()..keyGen()
distProps[#distProps + 1] = createCirclePoly({
createCirclePoly({
name = name,
coords = vec3(data.coords.x, data.coords.y, data.coords.z - 1.03),
radius = range or 50.0,
onEnter = function()
Props[name] = makeProp(data, freeze, synced)
if func then
func(Props[name])
end
end,
onExit = function()
destroyProp(Props[name])
Props[name] = nil
end,
debug = debugMode,
})
@@ -97,16 +86,6 @@ function destroyProp(entity)
end
end
onPlayerUnload(function()
for k in pairs(Props) do
DeleteObject(Props[k])
end
for i = 1, #distProps do
removeZoneTarget(distProps[i])
end
distProps = {}
end)
--- Cleans up all created props when the resource stops.
onResourceStop(function()
for k in pairs(Props) do

View File

@@ -13,29 +13,23 @@ local Vehicles = {}
--- ```lua
--- local vehicle = makeVeh('adder', vector4(123.4, 567.8, 90.1, 180.0))
--- ```
function makeVeh(model, coords, synced, fade)
function makeVeh(model, coords)
loadModel(model)
local veh = CreateVehicle(model, coords.x, coords.y, coords.z, coords.w, synced ~= false, false)
local veh = CreateVehicle(model, coords.x, coords.y, coords.z, coords.w, true, false)
SetVehicleHasBeenOwnedByPlayer(veh, true)
if gameName ~= "rdr3" then
SetNetworkIdCanMigrate(NetworkGetNetworkIdFromEntity(veh), true)
SetNetworkIdCanMigrate(NetworkGetNetworkIdFromEntity(veh), true)
Wait(100)
SetVehicleNeedsToBeHotwired(veh, false)
SetVehRadioStation(veh, 'OFF')
SetVehicleFuelLevel(veh, 100.0)
SetVehicleModKit(veh, 0)
end
SetVehicleOnGroundProperly(veh)
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 and gameName ~= "rdr3" then
SetEntityAlpha(veh, 0, false)
CreateThread(function()
fadeInEnt(veh)
end)
end
return veh
end
@@ -58,7 +52,7 @@ function makeDistVehicle(data, radius, onEnter, onExit)
coords = vec3(data.coords.x, data.coords.y, data.coords.z),
radius = radius,
onEnter = function()
vehicle = makeVeh(data.model, data.coords, false)
vehicle = makeVeh(data.model, data.coords)
if onEnter then
debugPrint("^6Bridge^7: ^4makeDistVehicle ^3onEnter^7() ^2running^7")
onEnter(vehicle)

View File

@@ -1,259 +1,6 @@
local inProgress = false
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
}
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
end
end
if not data.progressType or data.progressType == "bar" then
if exports[OXLibExport]:progressBar(options) then
return true
else
return false
end
end
end,
stop =
function()
exports[OXLibExport]:cancelProgress()
end,
},
qb = {
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,
},
esx = {
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,
},
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,
},
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,
},
["17mov"] = {
start =
function(data)
if exports["17mov_Hud"]:StartProgress({
duration = debugMode and 1000 or data.time,
label = data.label,
useWhileDead = data.dead or false,
canCancel = data.cancel or true,
controlDisables = {
disableMovement = data.disableMovement or false,
disableCarMovement = data.disableMovement or false,
disableMouse = data.mouse or false,
disableCombat = data.combat or true,
},
animation = {
animDict = data.dict,
anim = data.anim,
flags = (data.flag == 8 and 32 or data.flag) or nil,
task = data.task,
},
prop = data.prop and {
model = data.prop.model,
bone = data.prop.bone or 0,
coords = data.prop.pos or vec3(0, 0, 0),
rotation = data.prop.rot or vec3(0, 0, 0),
} or nil,
propTwo = data.propTwo and {
model = data.propTwo.model,
bone = data.propTwo.bone or 0,
coords = data.propTwo.pos or vec3(0, 0, 0),
rotation = data.propTwo.rot or vec3(0, 0, 0),
} or nil,
}, nil, nil) then
return true
else
return false
end
end,
stop =
function()
exports["17mov_Hud"]:StopProgress()
end,
},
}
--- Displays a progress bar using the configured progress bar system.
---
--- This function handles displaying a progress bar to the player using the specified progress bar system (e.g., ox, qb, esx, gta).
@@ -287,11 +34,7 @@ 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")
@@ -300,17 +43,164 @@ function progressBar(data)
end
local result = nil
if data.cam then startTempCam(data.cam) end
if Config.System.ProgressBar == "ox" then
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
}
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
result = true
else
result = false
end
end
if not data.progressType or data.progressType == "bar" then
if exports[OXLibExport]:progressBar(options) then
result = true
else
result = false
end
end
elseif Config.System.ProgressBar == "qb" then
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()
result = true
end, function()
result = false
end, data.icon)
elseif Config.System.ProgressBar == "esx" then
ESX.Progressbar(data.label, debugMode and 1000 or data.time, {
FreezePlayer = true,
animation = {
type = data.anim,
dict = data.dict,
scenario = data.task,
},
onFinish = function()
result = true
end,
onCancel = function()
result = false
end
})
elseif Config.System.ProgressBar == "lation" then
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,
},
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
result = true
else
result = true
end
elseif Config.System.ProgressBar == "red" then
-- Currently only uses jim-redui if you choose this option
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
result = true
else
result = false
end
elseif Config.System.ProgressBar == "gta" then
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
result = true
else
result = false
end
if progressFunc[Config.System.ProgressBar].start(data) then
result = true
else
result = false
end
while result == nil do
Wait(10)
end
debugPrint("^5Debug^7: ^2ProgressBar result^7: ^3"..tostring(result).."^7")
-- Cleanup
FreezeEntityPosition(ped, false)
@@ -331,7 +221,6 @@ function progressBar(data)
TriggerServerEvent(getScript()..":clearAuthToken")
currentToken = triggerCallback(AuthEvent)
end
progresssBarActive = false
return result
end
@@ -339,8 +228,15 @@ 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
if Config.System.ProgressBar == "ox" then
exports[OXLibExport]:cancelProgress()
elseif Config.System.ProgressBar == "qb" then
TriggerEvent("progressbar:client:cancel")
elseif Config.System.ProgressBar == "lation" then
exports.lation_ui:cancelProgress()
elseif Config.System.ProgressBar == "gta" or Config.System.ProgressBar == "red" then
exports["jim_bridge"]:stopProgressBar()
end
end
-- System to handle sending/sharing progress bars between players --

View File

@@ -1,255 +0,0 @@
if not isServer() then
local RopeCache = {
nextId = 1,
byId = {},
byHandle = {},
}
function loadRopeTextures()
RopeLoadTextures()
local tries = 0
while not RopeAreTexturesLoaded() and tries < 200 do
Wait(0)
tries += 1
end
return RopeAreTexturesLoaded()
end
function maybeUnloadTextures()
local anyLeft = false
local all = GetAllRopes()
if all and type(all) == "table" then
for _, _ in ipairs(all) do anyLeft = true break end
end
if not anyLeft then
RopeUnloadTextures()
end
end
function registerRope(ropeHandle, startCoords, endCoords, opts)
if not ropeHandle or ropeHandle == 0 then return nil end
local id = RopeCache.nextId
RopeCache.nextId = id + 1
local rec = {
handle = ropeHandle,
startCoords = startCoords,
endCoords = endCoords,
type = opts.ropeType,
rate = opts.lengthChangeRate,
breakable = opts.breakable,
createdAt = GetGameTimer()
}
RopeCache.byId[id] = rec
RopeCache.byHandle[ropeHandle] = id
debugPrint("^5Debug^7: 2Registered Rope^7'^3"..id.."^7' - '^3"..ropeHandle.."^7'")
return id
end
function resolve(id)
if type(id) ~= "number" then return nil, "rope id must be a number" end
local rec = RopeCache.byId[id]
if not rec then return nil, "rope id not found" end
if not DoesRopeExist(rec.handle) then
-- stale; clean mapping
RopeCache.byHandle[rec.handle] = nil
RopeCache.byId[id] = nil
return nil, "rope no longer exists"
end
return rec
end
--- Create a rope pinned between two coordinates.
--- @param startCoords vector3|table start coord
--- @param endCoords vector3|table end coord
--- @param opts table|nil { ropeType=0..7, breakable=false, collision=true, lockFromFront=false, lengthChangeRate=1.0, timeMultiplier=1.0, slack=0.0, preset=nil }
--- @return integer ropeId
function ropeCreateLine(startCoords, endCoords, opts)
startCoords = type(startCoords) == "number" and GetEntityCoords(startCoords) or startCoords
endCoords = type(endCoords) == "number" and GetEntityCoords(endCoords) or endCoords
opts = opts or {}
local length = #(startCoords - endCoords)
local mid = (startCoords + endCoords) * 0.5
local ropeType = opts.ropeType or 1 -- 0..7 (see ropedata.xml types)
local initLength = length + (opts.slack or 0.0)
local maxLength = initLength -- allow droop equal to init
local minLength = 0.0
local lengthChangeRate = opts.lengthChangeRate or 1.0
local collisionOn = (opts.collision ~= false)
local lockFromFront = (opts.lockFromFront == true)
local timeMultiplier = opts.timeMultiplier or 1.0
local breakable = (opts.breakable == true)
local rope = AddRope(mid.x, mid.y, mid.z, 0.0, 0.0, 0.0, maxLength, ropeType, initLength, minLength, lengthChangeRate, false, collisionOn, lockFromFront, timeMultiplier, breakable, 0)
if not rope or rope == 0 or not DoesRopeExist(rope) then
debugPrint("^1AddRope failed")
return nil
end
-- Optional preset (e.g., "ropeFamily3") if provided.
if opts.preset then
-- Best-effort; if invalid it just won't change.
pcall(function() LoadRopeData(rope, tostring(opts.preset)) end)
end
ActivatePhysics(rope)
-- Pin both ends to the given coords.
local count = GetRopeVertexCount(rope)
if count and count >= 2 then
PinRopeVertex(rope, 0, startCoords.x, startCoords.y, startCoords.z)
PinRopeVertex(rope, count - 1, endCoords.x, endCoords.y, endCoords.z)
end
local id = registerRope(rope, startCoords, endCoords, {
ropeType = ropeType,
lengthChangeRate = lengthChangeRate,
breakable = breakable
})
return id
end
--- Delete and unregister a rope.
--- @param id integer
function ropeDelete(id)
local rec, err = resolve(id)
if not rec then return false, err end
local rope = rec.handle
if DoesRopeExist(rope) then
DeleteRope(rope)
end
RopeCache.byHandle[rope] = nil
RopeCache.byId[id] = nil
debugPrint(("deleted rope id=%s"):format(id))
maybeUnloadTextures()
return true
end
--- Force rope to a specific length (instant snap).
--- @param id integer
--- @param newLength number
function ropeSetLength(id, newLength)
local rec, err = resolve(id)
if not rec then return false, err end
RopeForceLength(rec.handle, newLength)
return true
end
--- Smoothly wind the rope (shorten over time).
--- @param id integer
function ropeStartWinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StartRopeWinding(rec.handle)
return true
end
--- Stop winding (if winding).
function ropeStopWinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StopRopeWinding(rec.handle)
return true
end
--- Smoothly unwind the rope from the front (lengthen over time).
function ropeStartUnwinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StartRopeUnwindingFront(rec.handle)
return true
end
--- Stop unwinding (if unwinding).
function ropeStopUnwinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StopRopeUnwindingFront(rec.handle)
return true
end
--- Change the wind speed (length change rate).
--- @param id integer
--- @param rate number (units per sec; try 0.5..5.0)
function ropeSetRate(id, rate)
local rec, err = resolve(id)
if not rec then return false, err end
SetRopeLengthChangeRate(rec.handle, rate)
rec.rate = rate
return true
end
--- Move end endpoint to a new coordinate (re-pins the vertex).
--- @param id integer
--- @param pos vector3|table
function ropeMoveEnd(id, pos)
local rec, err = resolve(id)
if not rec then return false, err end
pos = type(pos) == "number" and GetEntityCoords(pos) or pos
local rope = rec.handle
local count = GetRopeVertexCount(rope)
if not count or count < 2 then return false, "invalid vertex count" end
PinRopeVertex(rope, count - 1, pos.x, pos.y, pos.z)
rec.endCoord = pos
return true
end
--- Move start endpoint to a new coordinate (re-pins the vertex).
--- @param id integer
--- @param pos vector3|table
function ropeMoveStart(id, pos)
local rec, err = resolve(id)
if not rec then return false, err end
pos = type(pos) == "number" and GetEntityCoords(pos) or pos
local rope = rec.handle
local count = GetRopeVertexCount(rope)
if not count or count < 2 then return false, "invalid vertex count" end
PinRopeVertex(rope, 0, pos.x, pos.y, pos.z)
rec.startCoord = pos
return true
end
--- Re-pin both endpoints in one call.
function ropeSetEnds(ropeId, startCoord, endCoord)
startCoord = type(startCoord) == "number" and GetEntityCoords(startCoord) or startCoord
endCoord = type(endCoord) == "number" and GetEntityCoords(endCoord) or endCoord
local rec, err = resolve(ropeId)
if not rec then return false, err end
local rope = rec.handle
local count = GetRopeVertexCount(rope)
if not count or count < 2 then
return false, "invalid vertex count"
end
PinRopeVertex(rope, 0, startCoord.x, startCoord.y, startCoord.z)
PinRopeVertex(rope, count - 1, endCoord.x, endCoord.y, endCoord.z)
rec.startCoord = startCoord
rec.endCoord = endCoord
return true
end
function ropeExists(id)
local rec = RopeCache.byId[id]
return rec and DoesRopeExist(rec.handle) or false
end
onResourceStart(function()
loadRopeTextures()
end)
onResourceStop(function()
for id, rec in pairs(RopeCache.byId) do
if rec.handle and DoesRopeExist(rec.handle) then
DeleteRope(rec.handle)
end
RopeCache.byHandle[rec.handle] = nil
RopeCache.byId[id] = nil
end
maybeUnloadTextures()
end)
end

View File

@@ -6,102 +6,6 @@
for getting and setting metadata.
]]
local metaDataFunc = {
{ framework = QBXExport,
GetPlayer =
function(src)
if src then
return exports[QBXExport]:GetPlayer(src)
end
return exports[QBXExport]:GetPlayerData()
end,
GetPlayerMetadata =
function(Player, dataToCheck)
return Player.PlayerData.metadata[dataToCheck]
end,
SetPlayerMetadata =
function(Player, key, value)
return Player.Functions.SetMetaData(key, value)
end,
},
{ framework = QBExport,
GetPlayer =
function(src)
if src then
return exports[QBExport]:GetCoreObject().Functions.GetPlayer(src)
end
local info = nil
Core.Functions.GetPlayerData(function(PlayerData) info = PlayerData end)
return info
end,
GetPlayerMetadata =
function(Player, dataToCheck)
return Player.PlayerData.metadata[dataToCheck]
end,
SetPlayerMetadata =
function(Player, key, value)
return Player.Functions.SetMetaData(key, value)
end,
},
{ framework = ESXExport,
GetPlayer =
function(src)
if src then
return ESX.GetPlayerFromId(src)
end
return ESX.GetPlayerData()
end,
GetPlayerMetadata =
function(Player, dataToCheck)
return Player.getMeta(dataToCheck)
end,
SetPlayerMetadata =
function(Player, key, value)
return Player.set(key, value)
end,
},
{ framework = OXCoreExport,
GetPlayer =
function(src)
if src then
return exports[OXCoreExport]:GetPlayer(src)
end
return {}
end,
GetPlayerMetadata =
function(Player, dataToCheck)
return Player.get(dataToCheck)
end,
SetPlayerMetadata =
function(Player, key, value)
return Player.set(key, value)
end,
},
{ framework = RSGExport,
GetPlayer =
function(src)
if src then
return exports[RSGExport]:GetCoreObject().Functions.GetPlayer(src)
end
local info = nil
Core.Functions.GetPlayerData(function(PlayerData) info = PlayerData end)
return info
end,
GetPlayerMetadata = function(Player, dataToCheck)
return Player.PlayerData.metadata[dataToCheck]
end,
SetPlayerMetadata = function(Player, key, value)
return Player.Functions.SetMetaData(key, value)
end,
},
}
-------------------------------------------------------------
-- Player Retrieval
-------------------------------------------------------------
@@ -116,13 +20,28 @@ local metaDataFunc = {
--- local player = GetPlayer(playerId)
--- ```
function GetPlayer(source)
for i = 1, #metaDataFunc do
local framework = metaDataFunc[i]
if isStarted(framework.framework) then
return framework.GetPlayer(source)
end
if isStarted(QBExport) then
debugPrint("^6Bridge^7: ^3GetPlayer^7() QBExport")
return exports[QBExport]:GetCoreObject().Functions.GetPlayer(source)
elseif isStarted(QBXExport) then
debugPrint("^6Bridge^7: ^3GetPlayer^7() QBOXExport")
return exports[QBXExport]:GetCoreObject().Functions.GetPlayer(source)
elseif isStarted(ESXExport) then
debugPrint("^6Bridge^7: ^3GetPlayer^7() ESXExport")
return ESX.GetPlayerFromId(source)
elseif isStarted(OXCoreExport) then
debugPrint("^6Bridge^7: ^3GetPlayer^7() OXCoreExport")
return exports[OXCoreExport]:GetPlayer(source)
elseif isStarted(RSGExport) then
debugPrint("^6Bridge^7: ^3GetPlayer^7() RSGExport")
return exports[RSGExport]:GetCoreObject().Functions.GetPlayer(source)
end
return {}
return nil
end
-------------------------------------------------------------
@@ -139,22 +58,29 @@ end
---
--- @usage
--- ```lua
--- local myMeta = getPlayerMetadata(player, "myKey")
--- local myMeta = GetMetadata(player, "myKey")
--- ```
function getPlayerMetadata(player, key)
-- Assume this is client side and callback to server to get the data
function GetMetadata(player, key)
if not player then
debugPrint("^6Bridge^7: ^3GetMetadata^7() calling server: "..key)
return triggerCallback(getScript()..":server:GetMetadata", key)
else
-- else grab server metadata about the player
for i = 1, #metaDataFunc do
local framework = metaDataFunc[i]
if isStarted(framework.framework) then
if isStarted(QBExport) or isStarted(QBXExport) then
debugPrint("^6Bridge^7: ^3GetMetadata^7() QBExport/QBXExport", key)
return player.PlayerData.metadata[key]
elseif isStarted(ESXExport) then
debugPrint("^6Bridge^7: ^3GetMetadata^7() ESXExport", key)
return player.getMeta(key)
elseif isStarted(OXCoreExport) then
debugPrint("^6Bridge^7: ^3GetMetadata^7() OXCoreExport", key)
return player.get(key)
elseif isStarted(RSGExport) then
debugPrint("^6Bridge^7: ^3GetMetadata^7() RSGExport", key)
return player.PlayerData.metadata[key]
debugPrint("^6Bridge^7: ^3GetMetadata^7() ^3"..framework.framework.."^7", key)
return framework.GetPlayerMetadata(player, key)
end
end
end
return nil
@@ -172,11 +98,11 @@ createCallback(getScript()..":server:GetMetadata", function(source, key)
if type(key) == "table" then
local Metadata = {}
for _, k in ipairs(key) do
Metadata[k] = getPlayerMetadata(player, k)
Metadata[k] = GetMetadata(player, k)
end
return Metadata
elseif type(key) == "string" then
return getPlayerMetadata(player, key)
return GetMetadata(player, key)
end
end)
@@ -194,26 +120,38 @@ end)
---
--- @usage
--- ```lua
--- setPlayerMetadata(player, "myKey", "newValue")
--- SetMetadata(player, "myKey", "newValue")
--- ```
function setPlayerMetadata(player, key, value)
debugPrint("^6Bridge^7: ^3SetMetadata^7() setting metadata^7...")
for i = 1, #metaDataFunc do
local framework = metaDataFunc[i]
if isStarted(framework.framework) then
debugPrint("^6Bridge^7: ^3SetMetadata^7() ^3"..framework.framework.."^7", key, value)
return framework.GetPlayerMetadata(player, key)
end
function SetMetadata(player, key, value)
debugPrint("^6Bridge^7: ^3SetMetadata^7() setting metadata for key: "..key)
if isStarted(QBExport) or isStarted(QBXExport) then
debugPrint("^6Bridge^7: ^3SetMetadata^7() using QBExport/QBXExport")
player.Functions.SetMetaData(key, value)
elseif isStarted(ESXExport) then
debugPrint("^6Bridge^7: ^3SetMetadata^7() using ESXExport")
player.setMeta(key, value)
elseif isStarted(OXCoreExport) then
debugPrint("^6Bridge^7: ^3SetMetadata^7() using OXCoreExport")
player.set(key, value)
elseif isStarted(RSGExport) then
debugPrint("^6Bridge^7: ^3SetMetadata^7() using RSGExport")
player.Functions.SetMetaData(key, value)
end
debugPrint("^6Bridge^7: ^1Error setting metadata^7, ^1framework not supported^7?")
end
-- Register a server callback for setting metadata.
createCallback(getScript()..":server:setPlayerMetadata", function(source, key, value)
local src = source
debugPrint("SetMetadata callback triggered for source:", src, "key:", key, "value:", value)
local player = GetPlayer(src)
setPlayerMetadata(player, key, value)
createCallback(getScript()..":server:SetMetadata", function(source, key, value)
debugPrint("SetMetadata callback triggered for source:", source, "key:", key, "value:", value)
local player = GetPlayer(source)
--[[if not player then
print("Error setting metadata: player not found for source "..tostring(source))
return false
end]]
SetMetadata(player, key, value)
print("Metadata set successfully.", key)
return true
end)

View File

@@ -8,90 +8,10 @@
• okok
• qb
• ox
• red (default)
• gta (default)
• lation
• esx
]]
local notifyFunc = {
okok = {
client =
function(title, message, type)
TriggerEvent('okokNotify:Alert', title, message, 6000, type)
end,
server =
function(title, message, type, src)
TriggerClientEvent('okokNotify:Alert', src, title, message, 6000, type)
end,
},
qb = {
client =
function(title, message, type)
TriggerEvent("QBCore:Notify", message, type)
end,
server =
function(title, message, type, src)
TriggerClientEvent("QBCore:Notify", src, message, type)
end,
},
ox = {
client =
function(title, message, type)
exports.ox_lib:notify({ title = title, description = message, type = type or "success" })
end,
server =
function(title, message, type, src)
TriggerClientEvent('ox_lib:notify', src, { title = title, description = message, type = type or "success" })
end,
},
esx = {
client =
function(title, message, type)
exports["esx_notify"]:Notify(type, 4000, message)
end,
server =
function(title, message, type, src)
TriggerClientEvent(getScript()..":DisplayESXNotify", src, type, message)
end,
},
lation = {
client =
function(title, message, type)
exports.lation_ui:notify({ title = title, message = message, type = type or "success", })
end,
server =
function(title, message, type, src)
TriggerClientEvent("lation_ui:notify", src, { title = title, message = message, type = type or "success", })
end,
},
gta = {
client =
function(title, message, type)
exports.jim_bridge:Notify(title, message, type)
end,
server =
function(title, message, type, src)
TriggerClientEvent("jim-bridge:Notify", src, title, message, type)
end,
},
red = {
client =
function(title, message, type)
TriggerEvent("jim-redui:Notify", title, message, type)
end,
server =
function(title, message, type, src)
TriggerClientEvent("jim-redui:Notify", src, title, message, type)
end,
},
}
--- Displays notifications to the player using the configured notification system.
---
--- Supports multiple notification systems based on Config.System.Notify. Can be triggered from both
@@ -111,15 +31,60 @@ local notifyFunc = {
--- triggerNotify("Alert", "You have been warned for misconduct.", "error", playerId)
--- ```
function triggerNotify(title, message, type, src)
if not Config or not Config.System or not Config.System.Notify then
print("Notify triggered but not set up")
return
end
if not Config.System or not Config.System.Notify then debugPrint("Notify triggered but not set up") return end
if Config.System.Notify == "okok" then
if not src then
TriggerEvent('okokNotify:Alert', title, message, 6000, type)
else
TriggerClientEvent('okokNotify:Alert', src, title, message, 6000, type)
end
elseif Config.System.Notify == "qb" then
if not src then
TriggerEvent("QBCore:Notify", message, type)
else
TriggerClientEvent("QBCore:Notify", src, message, type)
end
elseif Config.System.Notify == "ox" then
if not src then
TriggerEvent('ox_lib:notify', { title = title, description = message, type = type or "success" })
else
TriggerClientEvent('ox_lib:notify', src, { title = title, description = message, type = type or "success" })
end
elseif Config.System.Notify == "gta" then
if not src then
exports.jim_bridge:Notify(title, message, type)
else
TriggerClientEvent("jim-bridge:Notify", src, title, message, type)
end
elseif Config.System.Notify == "esx" then
if not src then
exports["esx_notify"]:Notify(type, 4000, message)
else
TriggerClientEvent(getScript()..":DisplayESXNotify", src, type, message)
end
elseif Config.System.Notify == "lation" then
if not src then
exports.lation_ui:notify({
title = title,
message = message,
type = type or "success",
})
else
TriggerClientEvent("lation_ui:notify", src, {
title = title,
message = message,
type = type or "success",
})
end
if src then
notifyFunc[Config.System.Notify].server(title, message, type, src)
else
notifyFunc[Config.System.Notify].client(title, message, type)
elseif Config.System.Notify == "red" then
if isStarted("jim-redui") then
if not src then
TriggerEvent("jim-redui:Notify", title, message, type)
else
TriggerClientEvent("jim-redui:Notify", src, title, message, type)
end
end
end
end
@@ -132,6 +97,7 @@ end
--- Listens for DisplayESXNotify events and triggers the ESX notification on the client.
---
--- @param type string The notification type.
--- @param title string The notification title.
--- @param text string The notification message.
---
--- @usage

View File

@@ -5,74 +5,110 @@
Supported systems include:
- gksphone
- yflip-phone
- qs-smartphone
- qs-smartphone-pro
- roadphone
- lb-phone
- qb-phone
- jpr-phonesystem
]]
local phoneFunc = {
{ phone = "gksphone",
sendMail = function(mailData)
exports["gksphone"]:SendNewMail(mailData)
end,
sendInvoice =
function(mailData)
-- Defensive check for required fields
local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "label" }
for _, field in ipairs(required) do
if mailData[field] == nil then
print("^1[jim-payments] ERROR:^0 Missing field '" .. field .. "' in mailData")
return
end
end
MySQL.Async.execute(
'INSERT INTO gksphone_invoices (citizenid, amount, society, sender, sendercitizenid, label) VALUES (@citizenid, @amount, @society, @sender, @sendercitizenid, @label)',
{
['@citizenid'] = mailData.billedCitizenid,
['@amount'] = mailData.amount,
['@society'] = mailData.job,
['@sender'] = mailData.name,
['@sendercitizenid'] = mailData.billerCitizenid,
['@label'] = mailData.label
}
)
--- Sends a phone mail using the detected phone system.
--- The function iterates through a prioritized list of supported phone systems.
--- Once an active system is found (via `isStarted`), the corresponding mail function is executed.
---
--- @param data table A table containing the mail data.
--- - subject (string): The email subject.
--- - sender (string): The sender identifier.
--- - message (string): The email body content.
--- - actions (table|nil): Optional action buttons for the email.
--- @usage
--- sendPhoneMail({
--- subject = "Welcome!",
--- sender = "Admin",
--- message = "Thank you for joining our server.",
--- actions = {
--- { label = "Reply", action = replyFunction }
--- }
--- })
function sendPhoneMail(data)
-- Define each supported phone system and its corresponding mail-sending function.
local phoneSystems = {
{ name = "gksphone",
send = function(mailData)
exports["gksphone"]:SendNewMail(mailData)
end,
},
{ phone = "yflip-phone",
sendMail =
function(mailData)
},
{ name = "yflip-phone",
send = function(mailData)
TriggerServerEvent(getScript()..":yflip:SendMail", mailData)
end,
},
{ phone = "roadphone",
sendMail =
function(mailData)
},
{ name = "qs-smartphone",
send = function(mailData)
TriggerServerEvent('qs-smartphone:server:sendNewMail', mailData)
end,
},
{ name = "qs-smartphone-pro",
send = function(mailData)
TriggerServerEvent('phone:sendNewMail', mailData)
end,
},
{ name = "roadphone",
send = function(mailData)
-- Convert HTML line breaks to newlines for roadphone.
mailData.message = mailData.message:gsub("%<br>", "\n")
exports["roadphone"]:sendMail(mailData)
end,
},
{ phone = "lb-phone",
sendMail =
function(mailData)
},
{ name = "lb-phone",
send = function(mailData)
-- Convert HTML line breaks to newlines for lb-phone.
mailData.message = mailData.message:gsub("%<br>", "\n")
TriggerServerEvent(getScript()..":lbphone:SendMail", mailData)
end,
},
{ phone = "qb-phone",
sendMail =
function(mailData)
},
{ name = "qb-phone",
send = function(mailData)
TriggerServerEvent('qb-phone:server:sendNewMail', mailData)
end,
sendInvoice =
function(mailData)
},
{ name = "npwd_qbx_mail",
send = function(mailData)
TriggerServerEvent('qb-phone:server:sendNewMail', mailData)
end,
},
{ name = "jpr-phonesystem",
send = function(mailData)
TriggerServerEvent(getScript()..":jpr:SendMail", mailData)
end,
},
{ name = "ef-phone",
send = function(mailData)
TriggerServerEvent('qb-phone:server:sendNewMail', mailData)
end,
},
}
-- Check each phone system in order and use the first active one.
for _, phone in ipairs(phoneSystems) do
if isStarted(phone.name) then
debugPrint("^6Bridge^7[^3"..phone.name.."^7]: ^2Sending mail to player")
phone.send(data)
return true
end
end
print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found")
return false
end
function sendPhoneInvoice(data)
if not data.src then return end
-- Define each supported phone system and its corresponding mail-sending function.
local phoneSystems = {
{
name = "qb-phone",
send = function(mailData)
-- Defensive check for required fields
local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "src" }
for _, field in ipairs(required) do
@@ -98,76 +134,75 @@ local phoneFunc = {
end
end
)
TriggerClientEvent('qb-phone:RefreshPhone', mailData.src)
end,
},
end
},
{
name = "codem-phone",
send = function(mailData)
-- Defensive check for required fields
local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "src" }
for _, field in ipairs(required) do
if mailData[field] == nil then
print("^1[jim-payments] ERROR:^0 Missing field '" .. field .. "' in mailData")
return
end
end
{ phone = "npwd_qbx_mail",
sendMail =
function(mailData)
TriggerServerEvent('qb-phone:server:sendNewMail', mailData)
end,
},
{ phone = "jpr-phonesystem",
sendMail =
function(mailData)
TriggerServerEvent(getScript()..":jpr:SendMail", mailData)
end,
},
{ phone = "ef-phone",
sendMail =
function(mailData)
TriggerServerEvent('qb-phone:server:sendNewMail', mailData)
end,
},
}
-- Safe insert with all parameters present
MySQL.Async.insert(
'INSERT INTO phone_invoices (citizenid, amount, society, sender, sendercitizenid) VALUES (?, ?, ?, ?, ?)',
{
mailData.billedCitizenid,
mailData.amount,
mailData.job,
mailData.name,
mailData.billerCitizenid
},
function(id)
-- if id then
-- TriggerClientEvent('qb-phone:client:AcceptorDenyInvoice', mailData.src, id, mailData.name, mailData.job, mailData.billerCitizenid, mailData.amount, GetInvokingResource())
-- end
end
)
--- Sends a phone mail using the detected phone system.
--- The function iterates through a prioritized list of supported phone systems.
--- Once an active system is found (via `isStarted`), the corresponding mail function is executed.
---
--- @param data table A table containing the mail data.
--- - subject (string): The email subject.
--- - sender (string): The sender identifier.
--- - message (string): The email body content.
--- - actions (table|nil): Optional action buttons for the email.
--- @usage
--- sendPhoneMail({
--- subject = "Welcome!",
--- sender = "Admin",
--- message = "Thank you for joining our server.",
--- actions = {
--- { label = "Reply", action = replyFunction }
--- }
--- })
function sendPhoneMail(mailData)
-- Check each phone system in order and use the first active one.
for i = 1, #phoneFunc do
local script = phoneFunc[i]
if isStarted(script.phone) and script.sendMail then
debugPrint("^6Bridge^7[^3"..script.phone.."^7]: ^2Sending mail to player")
script.sendMail(mailData)
return true
end
end
print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found")
return false
end
end
},
{
name = "gks-phone",
send = function(mailData)
-- Defensive check for required fields
local required = { "billedCitizenid", "amount", "job", "name", "billerCitizenid", "label" }
for _, field in ipairs(required) do
if mailData[field] == nil then
print("^1[jim-payments] ERROR:^0 Missing field '" .. field .. "' in mailData")
return
end
end
MySQL.Async.execute(
'INSERT INTO gksphone_invoices (citizenid, amount, society, sender, sendercitizenid, label) VALUES (@citizenid, @amount, @society, @sender, @sendercitizenid, @label)',
{
['@citizenid'] = mailData.billedCitizenid,
['@amount'] = mailData.amount,
['@society'] = mailData.job,
['@sender'] = mailData.name,
['@sendercitizenid'] = mailData.billerCitizenid,
['@label'] = mailData.label
}
)
end
}
}
function sendPhoneInvoice(data)
if not data.src then return end
-- Check each phone system in order and use the first active one.
for i = 1, #phoneFunc do
local script = phoneFunc[i]
if isStarted(script.phone) and script.sendInvoice then
debugPrint("^6Bridge^7[^3"..script.phone.."^7]: ^2Sending mail to player^7", data.src)
script.sendInvoice(data)
for _, phone in ipairs(phoneSystems) do
if isStarted(phone.name) then
debugPrint("^6Bridge^7[^3"..phone.name.."^7]: ^2Sending mail to player^7", data.src)
phone.send(data)
return true
end
end
@@ -240,6 +275,4 @@ RegisterNetEvent(getScript()..":jpr:SendMail", function(data)
Destinatario = Player.PlayerData.citizenid, -- Recipient identifier
Event = {}, -- Optional event details
})
end)
end)

File diff suppressed because it is too large Load Diff

View File

@@ -11,68 +11,6 @@
• removePolyZone(Location) - Removes a created zone.
]]
local polyCreation = {
{
polyZoneScript = OXLibExport,
createPoly = function(data)
data.minZ = data.minZ or -20.0
data.maxZ = data.maxZ or 1000.0
data.thickness = ((data.maxZ / 2) - (data.minZ / 2)) * 2
local mid = data.maxZ - ((data.maxZ / 2) - (data.minZ / 2))
for i = 1, #data.points do
data.points[i] = vec3(data.points[i].x, data.points[i].y, mid)
end
return lib.zones.poly(data)
end,
createCircle = function(data)
return lib.zones.sphere(data)
end,
removeZone = function(Location)
Location:remove()
end,
},
{
polyZoneScript = "PolyZone",
createPoly = function(data)
local zone = PolyZone:Create(data.points, {
name = data.name,
minZ = data.minZ or nil,
maxZ = data.maxZ or nil,
debugPoly = data.debug
})
zone:onPlayerInOut(function(isPointInside)
if isPointInside then
data.onEnter()
else
data.onExit()
end
end)
return zone
end,
createCircle = function(data)
local zone = CircleZone:Create(data.coords, data.radius, {
name = data.name,
debugPoly = data.debug
})
zone:onPlayerInOut(function(isPointInside)
if isPointInside then
data.onEnter()
else
data.onExit()
end
end)
return zone
end,
removeZone = function(Location)
Location:destroy()
end,
}
}
-------------------------------------------------------------
-- Polygonal Zone Creation
-------------------------------------------------------------
@@ -103,16 +41,26 @@ local polyCreation = {
---})
---```
function createPoly(data)
for i = 1, #polyCreation do
local script = polyCreation[i]
if isStarted(script.polyZoneScript) then
debugPrint("^6Bridge^7: ^2Creating new poly with ^7'^4"..script.polyZoneScript.."^7': "..data.name)
return script.createPoly(data)
local Location = nil
if isStarted(OXLibExport) then
debugPrint("^6Bridge^7: ^2Creating new poly with ^7'^4"..OXLibExport.."^7': "..data.name)
-- Convert 2D points to 3D with a fixed Z coordinate (e.g., 12.0)
for i = 1, #data.points do
data.points[i] = vec3(data.points[i].x, data.points[i].y, 12.0)
end
end
data.thickness = 1000 -- Set a default thickness value
Location = lib.zones.poly(data)
elseif isStarted("PolyZone") then
debugPrint("^6Bridge^7: ^2Creating new poly with ^7'^4PolyZone^7': "..data.name)
Location = PolyZone:Create(data.points, { name = data.name, debugPoly = data.debug })
Location:onPlayerInOut(function(isPointInside)
if isPointInside then data.onEnter() else data.onExit() end
print("^4ERROR^7: ^2No PolyZone creation script detected ^7")
return nil
end)
else
print("^4ERROR^7: ^2No PolyZone creation script detected ^7- ^2Check ^3exports^1.^2lua^7")
end
return Location
end
-------------------------------------------------------------
@@ -144,17 +92,25 @@ end
--- })
--- ```
function createCirclePoly(data)
for i = 1, #polyCreation do
local script = polyCreation[i]
if isStarted(script.polyZoneScript) then
debugPrint("^6Bridge^7: ^2Creating new ^3Cricle^2 poly with ^7"..script.polyZoneScript.." "..data.name)
debugPrint("^6Bridge^7: ^2Zone Stats - Coords: "..formatCoord(data.coords).." Radius: "..data.radius)
return script.createCircle(data)
end
local Location = nil
if isStarted(OXLibExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Cricle^2 poly with ^7"..OXLibExport.." "..data.name)
Location = lib.zones.sphere(data)
elseif isStarted("PolyZone") then
debugPrint("^6Bridge^7: ^2Creating new ^3Cricle^2 poly with ^7PolyZone ".. data.name)
Location = CircleZone:Create(data.coords, data.radius, { name = data.name, debugPoly = debugMode })
Location:onPlayerInOut(function(isPointInside)
if isPointInside then
data.onEnter()
else
data.onExit()
end
end)
else
print("^4ERROR^7: ^2No PolyZone creation script detected ^7- ^2Check ^3starter^1.^2lua^7")
end
print("^4ERROR^7: ^2No PolyZone creation script detected ^7")
return nil
debugPrint("^6Bridge^7: ^2Zone Stats - Coords: "..formatCoord(data.coords).." Radius: "..data.radius)
return Location
end
-------------------------------------------------------------
@@ -174,12 +130,11 @@ end
--- removePolyZone(zone)
--- ```
function removePolyZone(Location)
for i = 1, #polyCreation do
local script = polyCreation[i]
if isStarted(script.polyZoneScript) then
debugPrint("^6Bridge^7: ^2Removing ^3"..script.polyZoneScript.." ^2Zone^7")
script.removeZone(Location)
break
end
if isStarted(OXLibExport) then
debugPrint("^6Bridge^7: ^2Removing ^2poly with ^7"..OXLibExport)
Location:remove()
elseif isStarted("PolyZone") then
debugPrint("^6Bridge^7: ^2poly with ^7PolyZone")
Location:destroy()
end
end

266
shared/shops.lua Normal file
View File

@@ -0,0 +1,266 @@
-------------------------------------------------------------
-- Selling Menu and Animation
-------------------------------------------------------------
--- Opens a selling menu with available items and prices.
---
--- @param data table Contains selling menu data:
--- - sellTable (`table`) Table with Header and Items (item names and prices).
--- - ped (optional) (`number`) Ped entity involved.
--- - onBack (optional) (`function`) Callback for returning.
--- @usage
--- ```lua
--- sellMenu({
--- sellTable = {
--- Header = "Sell Items",
--- Items = {
--- ["gold_ring"] = 100,
--- ["diamond"] = 500,
--- },
--- },
--- ped = pedEntity,
--- onBack = function() print("Returning to previous menu") end,
--- })
--- ```
function sellMenu(data)
local origData = data
local Menu = {}
if data.sellTable.Items then
local itemList = {}
for k, v in pairs(data.sellTable.Items) do itemList[k] = 1 end
local _, hasTable = hasItem(itemList)
for k, v in pairsByKeys(data.sellTable.Items) do
if Items[k] then
Menu[#Menu + 1] = {
isMenuHeader = not hasTable[k].hasItem,
icon = invImg(k),
header = Items[k].label..(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()
sellAnim({ item = k, price = v, ped = data.ped, onBack = function() sellMenu(data) end })
end,
}
end
end
else
for k, v in pairsByKeys(data.sellTable) do
if type(v) == "table" then
Menu[#Menu + 1] = {
arrow = true,
header = k,
txt = "Amount of items: "..countTable(v.Items),
onSelect = function()
v.onBack = function()
sellMenu(origData)
end
v.sellTable = data.sellTable[k]
sellMenu(v)
end,
}
end
end
end
openMenu(Menu, {
header = data.sellTable.Header or "Amount of items: "..countTable(data.sellTable.Items or data.sellTable),
headertxt = data.sellTable.Header and "Amount of items: "..countTable(data.sellTable.Items or data.sellTable),
canClose = true,
onBack = data.onBack,
})
end
--- Plays the selling animation and processes the sale transaction.
---
--- Checks if the player has the item, plays animations, triggers the server event for selling,
--- and then calls the onBack callback if provided.
---
--- @param data table Contains:
--- `- item: The item to sell.
--- `- price: Price per item.
--- `- ped (optional): Ped entity involved.
--- `- onBack (optional): Callback to call on completion.
---@usage
--- ```lua
--- sellAnim({
--- item = "gold_ring",
--- price = 100,
--- ped = pedEntity,
--- onBack = function() sellMenu(data) end,
--- })
--- ```
function sellAnim(data)
if not hasItem(data.item, 1) then
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error")
return
end
-- Remove any attached clipboard objects.
for _, obj in pairs(GetGamePool('CObject')) do
for _, model in pairs({ `p_cs_clipboard` }) do
if GetEntityModel(obj) == model and IsEntityAttachedToEntity(data.ped, obj) then
DeleteObject(obj)
DetachEntity(obj, 0, 0)
SetEntityAsMissionEntity(obj, true, true)
Wait(100)
DeleteEntity(obj)
end
end
end
TriggerServerEvent(getScript().."Sellitems", data)
lookEnt(data.ped)
local dict = "mp_common"
playAnim(dict, "givetake2_a", 0.3, 48)
playAnim(dict, "givetake2_b", 0.3, 48, data.ped)
Wait(2000)
StopAnimTask(PlayerPedId(), dict, "givetake2_a", 0.5)
StopAnimTask(data.ped, dict, "givetake2_b", 0.5)
if data.onBack then
data.onBack()
end
end
--- Server event handler for processing item sales.
--- Removes sold items from inventory and funds the player based on the sale.
RegisterNetEvent(getScript().."Sellitems", function(data)
local src = source
local hasItems, hasTable = hasItem(data.item, 1, src)
if hasItems then
removeItem(data.item, hasTable[data.item].count, src)
fundPlayer((hasTable[data.item].count * data.price), "cash", src)
else
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error", src)
end
end)
-------------------------------------------------------------
-- Shop Interface
-------------------------------------------------------------
--- Opens a shop interface for the player.
---
--- Checks job/gang restrictions, then uses the active inventory system to open the shop.
--- @param data table Contains:
--- - shop (`string`) The shop identifier.
--- - items (`table`) The items available in the shop.
--- - coords (`vector3`) where the shop is located.
--- - job/gang (optional) (`string`) Job or gang requirements.
---@usage
--- ```lua
--- openShop({
--- shop = "weapon_shop",
--- items = weaponShopItems,
--- coords = vector3(100.0, 200.0, 300.0),
--- job = "police",
--- })
--- ```
function openShop(data)
jsonPrint(data)
if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
if Config.General.JimShops then
TriggerServerEvent("jim-shops:ShopOpen", "shop", data.items.label, data.items)
elseif isStarted(OXInv) then
exports[OXInv]:openInventory('shop', { type = data.shop })
elseif isStarted(QSInv) then
TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
elseif isStarted(TgiannInv) then
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
elseif isStarted(CodeMInv) then
--print(data.shop)
TriggerEvent("codem-inventory:openshop", data.shop)
elseif isStarted(QBInv) then
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
elseif isStarted(PSInv) then
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
elseif isStarted(RSGInv) then
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
end
lookEnt(data.coords)
end
RegisterNetEvent(getScript()..':server:openServerShop', function(data)
if isStarted(QBInv) and checkExportExists(QBInv, "OpenShop") then
exports[QBInv]:OpenShop(source, data)
end
if isStarted(PSInv) and checkExportExists(PSInv, "OpenShop") then
exports[PSInv]:OpenShop(source, data)
end
if isStarted(TgiannInv) then
exports[TgiannInv]:OpenShop(source, data)
end
if isStarted(RSGInv) then
exports[RSGInv]:OpenShop(source, data)
end
end)
--- Registers a shop with the active inventory system.
--- Supports either OXInv or QBInv (with QBInvNew flag).
---
--- @param name string Unique shop identifier.
--- @param label string Display name for the shop.
--- @param items table List of available shop items.
--- @param society string|nil (Optional) Society identifier for shared shops.
--- @usage
--- ```lua
--- registerShop("weaponShop", "Weapon Shop", weaponItems, "society_weapons")
--- ```
function registerShop(name, label, items, society)
local shopResource = ""
if isStarted(OXInv) then
shopResource = OXInv
exports[OXInv]:RegisterShop(name, {
name = label,
inventory = items,
society = society,
})
elseif isStarted(TgiannInv) then
shopResource = TgiannInv
exports[TgiannInv]:RegisterShop(name, items)
elseif isStarted(QBInv) and checkExportExists(QBInv, "CreateShop") then
shopResource = QBInv
exports[QBInv]:CreateShop({
name = name,
label = label,
slots = #items,
items = items,
society = society,
})
elseif isStarted(PSInv) and checkExportExists(PSInv, "CreateShop") then
shopResource = PSInv
exports[PSInv]:CreateShop({
name = name,
label = label,
slots = #items,
items = items,
society = society,
})
elseif isStarted(RSGInv) then
shopResource = RSGInv
exports[RSGInv]:CreateShop({
name = name,
label = label,
slots = #items,
items = items,
society = society,
})
end
if shopResource ~= "" then
debugPrint("^6Bridge^7: ^2Registering ^5"..shopResource.." ^3Store^7:", name, "^4Label^7: "..label)
else
debugPrint("^1ERROR^7: ^1Couldn't find supported inventory to register shop^7:", name)
end
end

View File

@@ -1,73 +1,58 @@
local skillCheckFunc = {
qb = {
start = function(data)
local Skillbar = exports["qb-minigames"]:Skillbar()
if Skillbar then
return true
else
return false
end
end,
},
ox = {
start = function(data)
local Skillbar = exports[OXLibExport]:skillCheck(
{
"easy",
"easy",
"easy"
},
{
"1",
"2",
"3",
"4"
}
)
if Skillbar then
return true
else
return false
end
end,
},
gta = {
start = function(data)
local Skillbar = exports.jim_bridge:skillCheck()
if Skillbar then
return true
else
return false
end
end,
},
lation = {
start = function(data)
local Skillbar = exports.lation_ui:skillCheck("",
{
"easy",
"easy",
"easy"
},
{
"1",
"2",
"3",
"4"
})
if Skillbar then
return true
else
return false
end
end,
},
}
local activeSkillCheck = false
function skillCheck(data)
if Config.System.skillCheck then
return skillCheckFunc[Config.System.skillCheck].start(data)
local result = false
if Config.System.skillCheck == "qb" then
local Skillbar = exports["qb-minigames"]:Skillbar()
if Skillbar then
result = true
else
result = false
end
elseif Config.System.skillCheck == "ox" then
local Skillbar = exports[OXLibExport]:skillCheck(
{
"easy",
"easy",
"easy"
},
{
"1",
"2",
"3",
"4"
})
if Skillbar then
result = true
else
result = false
end
elseif Config.System.skillCheck == "gta" then
exports.jim_bridge:skillCheck()
elseif Config.System.skillCheck == "lation" then
local Skillbar = exports.lation_ui:skillCheck("",
{
"easy",
"easy",
"easy"
},
{
"1",
"2",
"3",
"4"
})
if Skillbar then
result = true
else
result = false
end
else
result = true
end
return true
return result
end

View File

@@ -11,205 +11,6 @@
• Tgiann-bank
]]
local societyFunc = {
{ bankName = "Renewed-Banking",
getAccount =
function(society)
return exports["Renewed-Banking"]:getAccountMoney(society)
end,
chargeSociety =
function(society, amount)
exports['Renewed-Banking']:removeAccountMoney(society, amount)
end,
fundSociety =
function(society, amount)
exports['Renewed-Banking']:addAccountMoney(society, amount)
end,
},
{ bankName = "fd_banking",
getAccount =
function(society)
return exports["fd_banking"]:GetAccount(society)
end,
chargeSociety =
function(society, amount)
exports["fd_banking"]:RemoveMoney(society, amount)
end,
fundSociety =
function(society, amount)
exports["fd_banking"]:AddMoney(society, amount)
end,
},
{ bankName = "okokBanking",
getAccount =
function(society)
return exports['okokBanking']:GetAccount(society)
end,
chargeSociety =
function(society, amount)
exports['okokBanking']:RemoveMoney(society, amount)
end,
fundSociety =
function(society, amount)
exports['okokBanking']:AddMoney(society, amount)
end,
},
{ bankName = "tgiann-bank",
getAccount =
function(society)
if Jobs[society] then
return exports["tgiann-bank"]:GetJobAccountBalance(society)
else
return exports["tgiann-bank"]:GetGangAccountBalance(society)
end
end,
chargeSociety =
function(society, amount)
if Jobs[society] then
exports["tgiann-bank"]:RemoveJobMoney(society, amount)
else
exports["tgiann-bank"]:RemoveGangMoney(society, amount)
end
end,
fundSociety =
function(society, amount)
if Jobs[society] then
exports["tgiann-bank"]:AddJobMoney(society, amount)
else
exports["tgiann-bank"]:AddGangMoney(society, amount)
end
end,
},
{ bankName = "qb-banking",
getAccount =
function(society)
if not exports["qb-banking"]:GetAccount(society) then
if Jobs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateJobAccount(society, 0)
Wait(150)
elseif Gangs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateGangAccount(society, 0)
Wait(150)
end
end
return exports["qb-banking"]:GetAccountBalance(society)
end,
chargeSociety =
function(society, amount)
if not exports["qb-banking"]:GetAccount(society) then
if Jobs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateJobAccount(society, 0) Wait(150) -- make new account if return "null"
elseif Gangs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateGangAccount(society, 0) Wait(150) -- make new account if return "null"
end
end
exports["qb-banking"]:RemoveMoney(society, amount)
end,
fundSociety =
function(society, amount)
if not exports["qb-banking"]:GetAccount(society) then
if Jobs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateJobAccount(society, 0)
Wait(150)
elseif Gangs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateGangAccount(society, 0)
Wait(150)
end
end
exports["qb-banking"]:AddMoney(society, amount)
end,
},
{ bankName = "esx_society",
getAccount =
function(society)
local checkExist = exports.esx_society:GetSociety(society)
if checkExist == nil then
print("^6Bridge^7: ^2Making new bank account in ^7'^3esx_society^7' ^2for ^7'^3"..society.."^7'")
exports.esx_society:registerSociety(
society,
Gangs[society] and Gangs[society].label or Jobs[society] and Jobs[society].label,
society,
society,
society,
{type = "public"}
)
end
local p = promise.new()
TriggerEvent('esx_addonaccount:getSharedAccount', checkExist.account, function(account)
p:resolve(account.money)
end)
local newAmount = Citizen.Await(p)
return newAmount
end,
chargeSociety =
function(society, amount)
local checkExist = exports.esx_society:GetSociety(society)
if checkExist == nil then
print("^6Bridge^7: ^2Making new bank account in ^7'^3esx_society^7' ^2for ^7'^3"..society.."^7'")
exports.esx_society:registerSociety(
society,
Gangs[society] and Gangs[society].label or Jobs[society] and Jobs[society].label,
society,
society,
society,
{type = "public"}
)
end
TriggerEvent('esx_addonaccount:getSharedAccount', checkExist.account, function(account)
if amount > 0 and account.money >= amount then
account.removeMoney(amount)
end
end)
end,
fundSociety =
function(society, amount)
local checkExist = exports.esx_society:GetSociety(society)
if checkExist == nil then
print("^6Bridge^7: ^2Making new bank account in ^7'^3esx_society^7' ^2for ^7'^3"..society.."^7'")
exports.esx_society:registerSociety(
society,
Gangs[society] and Gangs[society].label or Jobs[society] and Jobs[society].label,
society,
society,
society,
{type = "public"}
)
end
TriggerEvent('esx_addonaccount:getSharedAccount', checkExist.account, function(account)
account.addMoney(amount)
end)
end,
},
{ bankName = "wasabi_banking",
getAccount =
function(society)
return exports['wasabi_banking']:GetAccountBalance(society, 'society')
end,
chargeSociety =
function(society, amount)
exports['wasabi_banking']:RemoveMoney('society', society, amount)
end,
fundSociety =
function(society, amount)
exports['wasabi_banking']:AddMoney('society', society, amount)
end,
},
}
--- Retrieves the current balance of a society's bank account.
--- @param society string The identifier of the society.
--- @return number number The current account balance.
@@ -219,19 +20,56 @@ local societyFunc = {
--- print("Police account balance: $"..balance)
--- ```
function getSocietyAccount(society)
local amount = 0
local bankScript, amount = "", 0
if society == nil or society == "none" then return amount end
for i = 1, #societyFunc do
local script = societyFunc[i]
if isStarted(script.bankName) then
local amount = script.getAccount(society)
debugPrint("^6Bridge^7: ^3"..script.bankName.."^7: ^2Retrieved account ^7'^6"..society.."^7' ($"..tostring(amount)..")")
return amount
if isStarted("qb-banking") then
bankScript = "qb-banking"
if not exports["qb-banking"]:GetAccount(society) then
if Jobs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateJobAccount(society, 0)
Wait(150)
elseif Gangs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateGangAccount(society, 0)
Wait(150)
end
end
amount = exports["qb-banking"]:GetAccountBalance(society)
--elseif isStarted("esx_society") then
-- bankScript = "esx_society"
-- -- Since esx_society does not have a native client export for retrieving money,
-- -- we use a server callback to get the final amount.
-- amount = triggerCallback(getScript()..":getESXSocietyAccount", society) or 0
elseif isStarted("Renewed-Banking") then
bankScript = "Renewed-Banking"
amount = exports["Renewed-Banking"]:getAccountMoney(society)
elseif isStarted("fd_banking") then
bankScript = "fd_banking"
amount = exports["fd_banking"]:GetAccount(society)
elseif isStarted("okokBanking") then
bankScript = "okokBanking"
amount = exports['okokBanking']:GetAccount(society)
elseif isStarted("tgiann-bank") then
bankScript = "tgiann-bank"
if Jobs[society] then
amount = exports["tgiann-bank"]:GetJobAccountBalance(society)
else
amount = exports["tgiann-bank"]:GetGangAccountBalance(society)
end
end
print("^1Error^7: ^3GetSocietyAccount^7: ^2No supported banking script found")
if bankScript == "" then
print("^1Error^7: ^3GetSocietyAccount^7: ^2No supported banking script found")
else
debugPrint("^6Bridge^7: ^3"..bankScript.."^7: ^2Retrieved account ^7'^6"..society.."^7' ($"..amount..")")
end
return amount
end
@@ -244,18 +82,52 @@ end
--- chargeSociety("police", 1000)
--- ```
function chargeSociety(society, amount)
local bankScript, newAmount = "", 0
for i = 1, #societyFunc do
local script = societyFunc[i]
if isStarted(script.bankName) then
script.chargeSociety(society, amount)
local newAmount = getSocietyAccount(society)
debugPrint("^6Bridge^7: ^3"..script.bankName.."^7: ^2Removing $"..amount.." from account '^6"..society.."^7' ($"..tostring(newAmount)..")")
return
if isStarted("qb-banking") then
bankScript = "qb-banking"
if not exports["qb-banking"]:GetAccount(society) then
if Jobs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateJobAccount(society, 0) Wait(150) -- make new account if return "null"
elseif Gangs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateGangAccount(society, 0) Wait(150) -- make new account if return "null"
end
end
exports["qb-banking"]:RemoveMoney(society, amount)
--elseif isStarted("esx_society") then
-- bankScript = "esx_society"
-- TriggerEvent("esx_society:withdrawMoney", society, amount)
elseif isStarted("Renewed-Banking") then
bankScript = "Renewed-Banking"
exports['Renewed-Banking']:removeAccountMoney(society, amount)
elseif isStarted("fd_banking") then
bankScript = "fd_banking"
exports["fd_banking"]:RemoveMoney(society, amount)
elseif isStarted("okokBanking") then
bankScript = "okokBanking"
exports['okokBanking']:RemoveMoney(society, amount)
elseif isStarted("tgiann-bank") then
bankScript = "tgiann-bank"
if Jobs[society] then
exports["tgiann-bank"]:RemoveJobMoney(society, amount)
else
exports["tgiann-bank"]:RemoveGangMoney(society, amount)
end
end
print("^1Error^7: ^3ChargeSociety^7: ^2No supported banking script found")
if bankScript == "" then
print("^1Error^7: ^3ChargeSociety^7: ^2No supported banking script found")
else
newAmount = getSocietyAccount(society)
debugPrint("^6Bridge^7: ^3"..bankScript.."^7: ^2Removing $"..amount.." from account '^6"..society.."^7' ($"..newAmount..")")
end
end
--- Adds funds to a society's bank account.
@@ -266,26 +138,66 @@ end
--- fundSociety("police", 500)
--- ```
function fundSociety(society, amount)
local bankScript, newAmount, success = "", 0, false
for i = 1, #societyFunc do
local script = societyFunc[i]
if isStarted(script.bankName) then
script.fundSociety(society, amount)
local newAmount = getSocietyAccount(society)
debugPrint("^6Bridge^7: ^3"..script.bankName.."^7: ^2Adding ^7$"..amount.." ^2to account ^7'^6"..society.."^7' ($"..tostring(newAmount)..")")
return
if isStarted("qb-banking") then
bankScript = "qb-banking"
if not exports["qb-banking"]:GetAccount(society) then
if Jobs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateJobAccount(society, 0)
Wait(150)
elseif Gangs[society] then
print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
exports["qb-banking"]:CreateGangAccount(society, 0)
Wait(150)
end
end
exports["qb-banking"]:AddMoney(society, amount)
--elseif isStarted("esx_society") then
-- bankScript = "esx_society"
-- -- Use the esx_society event to deposit money.
-- TriggerServerEvent('esx_society:depositMoney', society, amount)
-- -- Use callback to return the updated balance.
-- newAmount = triggerCallback(getScript()..":getESXSocietyAccount", society) or 0
elseif isStarted("Renewed-Banking") then
bankScript = "Renewed-Banking"
exports['Renewed-Banking']:addAccountMoney(society, amount)
newAmount = exports["Renewed-Banking"]:getAccountMoney(society)
elseif isStarted("fd_banking") then
bankScript = "fd_banking"
exports["fd_banking"]:AddMoney(society, amount)
elseif isStarted("okokBanking") then
bankScript = "okokBanking"
exports['okokBanking']:AddMoney(society, amount)
elseif isStarted("tgiann-bank") then
bankScript = "tgiann-bank"
if Jobs[society] then
exports["tgiann-bank"]:AddJobMoney(society, amount)
else
exports["tgiann-bank"]:AddGangMoney(society, amount)
end
end
print("^1Error^7: ^3FundSociety^7: ^2No supported banking script found")
if bankScript == "" then
print("^1Error^7: ^3FundSociety^7: ^2No supported banking script found")
else
newAmount = getSocietyAccount(society)
debugPrint("^6Bridge^7: ^3"..bankScript.."^7: ^2Adding ^7$"..amount.." ^2to account ^7'^6"..society.."^7' ($"..newAmount..")")
end
end
if isServer() then
createCallback(getScript()..":server:getAccount", function(source, society)
if society then
local getMoney = getSocietyAccount(society)
return getMoney
end
return 0
-- other
if isStarted("esx_society") then
createCallback(getScript()..":getESXSocietyAccount", function(source, society)
-- Example query adjust table/field names to match your esx_society implementation.
local result = MySQL.scalar.await('SELECT money FROM society_money WHERE society = ?', { society })
return result or 0
end)
end

700
shared/stashcontrol.lua Normal file
View File

@@ -0,0 +1,700 @@
--[[
Stash Management Module
-------------------------
This module handles stash-related operations including:
• Retrieving stash items (from server or local cache).
• Checking for required items in stashes.
• Opening stashes using different inventory systems.
• Removing items from stashes.
• Checking if a stash has specific items.
]]
-- Global variable to hold the current stash (used in callbacks).
local stash
-- If running on the server, create a callback to retrieve stash items.
if isServer() then
createCallback(getScript()..':server:GetStashItems', function(source, stashName)
stash = getStash(stashName)
return stash
end)
end
-- Local cache for stashes.
local stashCache = {}
--- Retrieves (or updates) a local stash cache entry with a timeout.
--- When the cache is empty or expired, it triggers a server callback to update the items.
---
--- @param stashName string The name of the stash.
--- @param stop boolean (Optional) If true, clears the entire stash cache.
--- @return boolean True if items exist in cache (and recheck is skipped), false otherwise.
---
--- @usage
--- ```lua
--- local cached = GetStashTimeout("playerStash")
--- ```
function GetStashTimeout(stashName, stop)
if stop then
stashCache = {}
return
end
-- Retrieve cache for this stash, or initialize if not present.
stash = stashCache[stashName]
if not stash then
debugPrint("^6Bridge^7: ^2Local Stash ^7'^3"..stashName.."^7'^2 cache ^1not ^2found^7, ^2need to grab from server^7")
stashCache[stashName] = { items = {}, timeout = 0 }
stash = stashCache[stashName]
else
debugPrint("^6Bridge^7: ^2Local Stash for ^7'^3"..stashName.."^7'^2 cache found^7")
end
-- If there are already items in cache, skip recheck.
if countTable(stashCache[stashName].items) > 0 then
debugPrint("^6Bridge^7: '^3"..stashName.."^7' ^2Items found in local cache, skipping server recheck")
return true
end
-- If timeout has expired, update the stash items from the server.
if stashCache[stashName].timeout <= 0 then
stashCache[stashName].items = triggerCallback(getScript()..':server:GetStashItems', stashName)
stashCache[stashName].timeout = 10000 -- Timeout in milliseconds.
CreateThread(function()
while stashCache[stashName] and stashCache[stashName].timeout > 0 do
stashCache[stashName].timeout -= 1000
Wait(1000)
end
debugPrint("^6Bridge^7: ^2Local Stash ^7'^3"..stashName.."^7'^2 cache timed out^7, ^3Clearing^7")
stashCache[stashName] = nil
end)
end
return false
end
--- Checks if the specified stashes have the required items.
---
--- If multiple stashes are provided (as a table), it iterates over each until all required items are found.
---
--- @param stashes string|table Either a single stash name or a table of stash names.
--- @param itemTable table A table where keys are item names and values are the required amounts.
--- @return boolean, string|nil `boolean, string` true and the stash name if found, otherwise false and nil.
---
--- @usage
--- ```lua
--- local found, stashName = checkStashItem({"playerStash", "storageStash"}, { iron = 2, wood = 5 })
--- ```
function checkStashItem(stashes, itemTable)
if not stashes then
return hasItem(itemTable), nil
end
if type(stashes) == "table" then
debugPrint("^6Bridge^7: ^2Checking multiple stashes for ingredients^7")
-- Iterate over each provided stash name.
for _, name in pairs(stashes) do
GetStashTimeout(name)
if stashhasItem(stashCache[name].items, itemTable, nil) then
return true, name
end
end
else
debugPrint("^6Bridge^7: ^2Checking "..(stashes and " '^3"..stashes.."^7'" or "").." ingredients")
GetStashTimeout(stashes)
return stashhasItem(stashCache[stashes].items, itemTable), stashes
end
return false, nil
end
-------------------------------------------------------------
-- Stash Opening Functions
-------------------------------------------------------------
--- Opens a stash using the active inventory system.
---
--- Checks for job or gang restrictions before opening the stash.
---
--- @param data table A table containing stash data:
--- - stash (string): The stash identifier.
--- - label (string): Display label.
--- - maxWeight (number|nil): Maximum weight (default 600000).
--- - slots (number|nil): Number of slots (default 40).
--- - stashOptions (table|nil): Additional options for the stash.
--- - job/gang (string|nil): Restriction for access.
--- - coords (vector3): Coordinates to "look" at.
---
--- @usage
--- ```lua
--- openStash({ stash = "playerStash", label = "Player Stash", coords = vector3(100, 200, 30) })
--- ```
function openStash(data)
if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
if isStarted(OXInv) then
exports[OXInv]:openInventory('stash', data.stash)
elseif isStarted(CoreInv) then
TriggerServerEvent('core_inventory:server:openInventory', data.stash, 'stash')
elseif isStarted(CodeMInv) then
TriggerServerEvent('codem-inventory:server:openstash', data.stash, data.slots, data.maxWeight, data.label)
elseif isStarted(OrigenInv) then
exports[OrigenInv]:openInventory('stash', data.stash, { label = data.label })
elseif isStarted(TgiannInv) then
TriggerServerEvent(getScript()..':server:openServerStash', {
stashName = data.stash,
label = data.label,
maxweight = data.maxWeight or 600000,
slots = data.slots or 40
})
elseif isStarted(JPRInv) then
if QBInvNew then
TriggerServerEvent(getScript()..':server:openServerStash', {
stashName = data.stash,
label = data.label,
maxweight = data.maxWeight or 600000,
slots = data.slots or 40
})
else
TriggerEvent("inventory:client:SetCurrentStash", data.stash)
TriggerServerEvent("inventory:server:OpenInventory", "stash", data.stash, {
slots = data.slots or 50,
maxWeight = data.maxWeight or 600000
})
end
elseif isStarted(QBInv) then
if QBInvNew then
TriggerServerEvent(getScript()..':server:openServerStash', {
stashName = data.stash,
label = data.label,
maxweight = data.maxWeight or 600000,
slots = data.slots or 40
})
else
TriggerEvent("inventory:client:SetCurrentStash", data.stash)
TriggerServerEvent("inventory:server:OpenInventory", "stash", data.stash, {
slots = data.slots or 50,
maxWeight = data.maxWeight or 600000
})
end
elseif isStarted(PSInv) then
if QBInvNew then
TriggerServerEvent(getScript()..':server:openServerStash', {
stashName = data.stash,
label = data.label,
maxweight = data.maxWeight or 600000,
slots = data.slots or 40
})
else
TriggerEvent("ps-inventory:client:SetCurrentStash", data.stash)
TriggerEvent("inventory:client:SetCurrentStash", data.stash)
TriggerServerEvent("inventory:server:OpenInventory", "stash", data.stash, {
slots = data.slots or 50,
maxWeight = data.maxWeight or 600000
})
end
elseif isStarted(RSGInv) then
TriggerServerEvent(getScript()..':server:openServerStash', {
stashName = data.stash,
label = data.label,
maxweight = data.maxWeight or 600000,
slots = data.slots or 40
})
else
--Fallback to these commands
TriggerEvent("inventory:client:SetCurrentStash", data.stash)
TriggerServerEvent("inventory:server:OpenInventory", "stash", data.stash, {
slots = data.slots or 50,
maxWeight = data.maxWeight or 600000
})
end
lookEnt(data.coords)
end
-- Wrapper function for opening stash from the server.
-- Messy but not much else I can do about it.
RegisterNetEvent(getScript()..":server:openServerStash", function(data)
local src = source
if isStarted(TgiannInv) then
exports[TgiannInv]:OpenInventory(source, 'stash', data.stashName, data)
end
if isStarted(JPRInv) then
exports[JPRInv]:OpenInventory(source, data.stashName, data)
end
if isStarted(QBInv) then
exports[QBInv]:OpenInventory(source, data.stashName, data)
end
if isStarted(PSInv) then
exports[PSInv]:OpenInventory(source, data.stashName, data)
end
if isStarted(RSGInv) then
exports[RSGInv]:OpenInventory(source, data.stashName, data)
end
end)
function clearStash(stashId)
if isStarted(JPRInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..JPRInv.."^2 Stash^7", stashId)
MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
['stash'] = stashId,
['items'] = json.encode({})
})
elseif isStarted(QBInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..QBInv.."^2 Stash^7", stashId)
if QBInvNew then
exports[QBInv]:ClearStash(stashId)
else
MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
['stash'] = stashId,
['items'] = json.encode({})
})
end
elseif isStarted(OXInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..OXInv.."^2 Stash^7", stashId)
exports[OXInv]:ClearInventory(stashId)
elseif isStarted(PSInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..PSInv.."^2 Stash^7", stashId)
MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
['stash'] = stashId,
['items'] = json.encode({})
})
elseif isStarted(QSInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..QSInv.."^2 Stash^7", stashId)
exports[QSInv]:ClearOtherInventory('stash', stashId)
elseif isStarted(CoreInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..CoreInv.."^2 Stash^7", stashId)
exports[CoreInv]:clearInventory("stash-"..stashId)
elseif isStarted(CodeMInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..CodeMInv.."^2 Stash^7", stashId)
exports[CodeMInv]:ClearInventory(stashId)
elseif isStarted(OrigenInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..OrigenInv.."^2 Stash^7", stashId)
exports[OrigenInv]:ClearInventory(stashId)
elseif isStarted(TgiannInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..TgiannInv.."^2 Stash^7", stashId)
exports["tgiann-inventory"]:DeleteInventory("stash", stashId)
elseif isStarted(RSGInv) then
debugPrint("^5Bridge^7: ^2Clearing ^3"..RSGInv.."^2 Stash^7", stashId)
exports[RSGInv]:ClearStash(stashId)
end
end
-------------------------------------------------------------
-- Stash Retrieval Function
-------------------------------------------------------------
--- Retrieves stash items from the active inventory system.
---
--- This function converts the raw stash items into a standardized table using the global Items lookup.
---
--- @param stashName string The identifier for the stash.
--- @return stashTable table A table of items from the stash.
---
--- @usage
--- ```lua
--- local items = getStash("playerStash")
--- ```
function getStash(stashName)
local stashResource = ""
if type(stashName) ~= "string" then
print("^6Bridge^7: ^2Stash name was not a string ^3"..stashName.."^7(^3"..type(stashName).."^7)")
return {}
end
local stashItems, items = {}, {}
if isStarted(OXInv) then
stashResource = OXInv
local stash = exports[OXInv]:Inventory(stashName)
-- Add fallback if ox can't find the stash and returns a boolean
stashItems = type(stash) == "table" and stash.items or {}
elseif isStarted(QSInv) then
stashResource = QSInv
stashItems = exports[QSInv]:GetStashItems(stashName)
elseif isStarted(CoreInv) then
stashResource = CoreInv
stashItems = exports[CoreInv]:getInventory(stashName)
elseif isStarted(CodeMInv) then
stashResource = CodeMInv
stashItems = exports[CodeMInv]:GetStashItems(stashName)
elseif isStarted(OrigenInv) then
stashResource = OrigenInv
stashItems = exports[OrigenInv]:getInventory(stashName)
elseif isStarted(TgiannInv) then
stashResource = TgiannInv
stashItems = exports[TgiannInv]:GetSecondaryInventoryItems("stash", stashName)
elseif isStarted(PSInv) then
stashResource = PSInv
local result = MySQL.scalar.await('SELECT items FROM stashitems WHERE stash = ?', { stashName })
if result then stashItems = json.decode(result) end
elseif isStarted(JPRInv) then
stashResource = JPRInv
local result = MySQL.scalar.await('SELECT items FROM stashitems WHERE stash = ?', { stashName })
if result then stashItems = json.decode(result) end
elseif isStarted(QBInv) then
stashResource = QBInv
if QBInvNew then
local result = exports[QBInv]:GetInventory(stashName) or {}
stashItems = result.items or {}
else
local result = MySQL.scalar.await("SELECT items FROM stashitems WHERE stash = ?", { stashName })
if result then stashItems = json.decode(result) end
end
elseif isStarted(RSGInv) then
stashResource = RSGInv
stashItems = exports[RSGInv]:GetInventory(stashName)
end
debugPrint("^6Bridge^7: ^2Retrieving ^3Stash^2 with ^7"..stashResource)
if stashItems then
for _, item in pairs(stashItems) do
local itemInfo = Items[item.name:lower()]
if itemInfo then
local indexNum = #items + 1 -- Fallback index if slot is missing.
items[(item.slot or indexNum)] = {
name = itemInfo.name or nil,
amount = tonumber(item.amount) or tonumber(item.count),
info = item.info or "",
label = itemInfo.label or nil,
description = itemInfo.description or "",
weight = itemInfo.weight or nil,
type = itemInfo.type or nil,
unique = itemInfo.unique or nil,
useable = itemInfo.useable or nil,
image = itemInfo.image or nil,
slot = (item.slot and item.slot) or indexNum,
metadata = (item.metadata and item.metadata) or nil,
}
end
end
debugPrint("^6Bridge^7: ^3GetStashItems^7: ^2Stash information for '^6"..stashName.."^7' retrieved")
end
jsonPrint(items)
return items
end
-------------------------------------------------------------
-- Stash Item Removal Function
-------------------------------------------------------------
--- Removes items from a stash using the active inventory system.
---
--- Iterates over the provided items and adjusts the stash contents accordingly.
---
--- @param stashItems table The current stash items.
--- @param stashName string|table The stash identifier (or table of identifiers).
--- @param items table A table of items to remove (keys are item names, values are amounts).
---
--- @usage
--- ```lua
--- stashRemoveItem(currentItems, "playerStash", { iron = 2, wood = 5 })
--- ```
function stashRemoveItem(stashItems, stashName, items)
if type(stashName) ~= "table" then
stashName = { stashName }
end
if isStarted(OXInv) then
for k, v in pairs(items) do
for _, name in pairs(stashName) do
local success = exports[OXInv]:RemoveItem(name, k, v)
if success then
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..OXInv, k, v)
break
end
end
end
elseif isStarted(QSInv) then
for k, v in pairs(items) do
exports[QSInv]:RemoveItemIntoStash(stashName[1], k, v)
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..QSInv, k, v)
end
elseif isStarted(CoreInv) then
for k, v in pairs(items) do
exports[CoreInv]:removeItemExact(stashName[1], k, v)
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..CoreInv, k, v)
end
elseif isStarted(CodeMInv) then
for k, v in pairs(items) do
for l in pairs(stashItems) do
if stashItems[l].name == k then
if (stashItems[l].amount - v) <= 0 then
debugPrint("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
stashItems[l] = nil
else
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..CodeMInv, k, v)
stashItems[l].amount -= v
end
end
end
end
exports[CodeMInv]:UpdateStash(stashName[1], stashItems)
debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3CodeM^2 stash ^7'^6"..stashName.."^7'")
elseif isStarted(OrigenInv) then
for k, v in pairs(items) do
exports[OrigenInv]:RemoveFromStash(stashName[1], k, v)
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..OrigenInv, k, v)
end
elseif isStarted(TgiannInv) then
for k, v in pairs(items) do
local itemData = exports[TgiannInv]:GetItemByNameFromSecondaryInventory("stash", stashName[1], k)
exports[TgiannInv]:RemoveItemFromSecondaryInventory("stash", stashName[1], k, v, itemData.slot, nil)
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..TgiannInv, k, v)
end
elseif isStarted(JPRInv) then
if not stashItems or not next(stashItems) then
stashItems = getStash(stashName[1])
end
for k, v in pairs(items) do
for l in pairs(stashItems) do
if stashItems[l].name == k then
if (stashItems[l].amount - v) <= 0 then
debugPrint("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
stashItems[l] = nil
else
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with "..JPRInv, k, v)
stashItems[l].amount -= v
end
end
end
end
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[1],
['items'] = json.encode(stashItems)
})
elseif isStarted(QBInv) then
if QBInvNew then
for k, v in pairs(items) do
exports[QBInv]:RemoveItem(stashName[1], k, v, false, 'crafting')
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..QBInv, k, v)
end
else
if not stashItems or not next(stashItems) then
stashItems = getStash(stashName[1])
end
for k, v in pairs(items) do
for l in pairs(stashItems) do
if stashItems[l].name == k then
if (stashItems[l].amount - v) <= 0 then
debugPrint("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
stashItems[l] = nil
else
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with "..QBInv, k, v)
stashItems[l].amount -= v
end
end
end
end
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[1],
['items'] = json.encode(stashItems)
})
end
elseif isStarted(PSInv) then
if not stashItems or not next(stashItems) then
stashItems = getStash(stashName[1])
end
for k, v in pairs(items) do
for l in pairs(stashItems) do
if stashItems[l].name == k then
if (stashItems[l].amount - v) <= 0 then
debugPrint("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
stashItems[l] = nil
else
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..PSInv, k, v)
stashItems[l].amount -= v
end
end
end
end
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[1],
['items'] = json.encode(stashItems)
})
elseif isStarted(RSGInv) then
for k, v in pairs(items) do
exports[RSGInv]:RemoveItem(stashName[1], k, v, false, 'crafting')
debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..RSGInv, k, v)
end
else
print("^4ERROR^7: ^2No Inventory detected ^7- ^2Check ^3starter^1.^2lua^7")
end
end
RegisterNetEvent(getScript()..":server:stashRemoveItem", stashRemoveItem)
-------------------------------------------------------------
-- Stash Item Availability Check
-------------------------------------------------------------
--- Checks whether a stash has the required amount of specific items.
---
--- It iterates through the provided items and tallies available quantities.
---
--- @param stashItems table The items available in the stash.
--- @param items string|table The item name or table of required items (key: item, value: amount).
--- @param amount number (Optional) The required amount (if a single item is provided).
--- @return boolean, table (`boolean, string`) Returns true (and a table with counts) if all items are available; false otherwise.
---
--- @usage
--- ```lua
--- local hasAll, details = stashhasItem(currentStashItems, { iron = 2, wood = 5 })
--- ```
function stashhasItem(stashItems, items, amount)
local invs = { OXInv, QSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv, JPRInv, QBInv, PSInv, RSGInv }
local foundInv = ""
for _, inv in ipairs(invs) do
if isStarted(inv) then
foundInv = inv:gsub("%-", "^7-^6"):gsub("%_", "^7_^6")
break
end
end
-- Ensure items is a table.
if type(items) ~= "table" then items = { [items] = amount and amount or 1, } end
local hasTable = {}
for item, requiredAmount in pairs(items) do
local count = 0
for _, itemData in pairs(stashItems) do
if itemData and (itemData.name == item) then
count += (itemData.amount or 1)
end
end
local debugMsg = string.format("^6Bridge^7: ^3stashHasItem^7[^6%s^7]: %s '%s' ^3%d^7/^3%d^7", foundInv, (count >= requiredAmount and "^5FOUND^7" or "^1NOT FOUND^7"), item, count, requiredAmount)
debugPrint(debugMsg)
hasTable[item] = { hasItem = (count >= requiredAmount), count = count }
end
for k, v in pairs(hasTable) do if v.hasItem == false then return false, hasTable end end
return true, hasTable
end
--- Registers a stash with the active inventory system.
--- Supports either OXInv or QSInv.
---
--- @param name string Unique stash identifier.
--- @param label string Display name for the stash.
--- @param slots number|nil (Optional) Number of slots (default 50).
--- @param weight number|nil (Optional) Maximum weight (default 4000000).
--- @param owner string|nil (Optional) Owner identifier for personal stashes.
--- @param coords table|nil (Optional) Coordinates for the stash location.
--- @usage
--- ```lua
--- registerStash(
--- "playerStash",
--- "Player Stash",
--- 100,
--- 8000000,
--- "player123",
--- { x = 100.0, y = 200.0, z = 30.0 }
--- )
--- ```
function registerStash(name, label, slots, weight, owner, coords)
local stashResource = ""
if isStarted(OXInv) then
stashResource = OXInv
exports[OXInv]:RegisterStash(name, label, slots or 50, weight or 4000000, owner or nil)
elseif isStarted(OrigenInv) then
stashResource = OrigenInv
exports["origen_inventory"]:registerStash(name, label, slots or 50, weight or 4000000)
elseif isStarted(TgiannInv) then
stashResource = TgiannInv
exports[TgiannInv]:RegisterStash(name, label, slots or 50, weight or 4000000)
end
if stashResource ~= "" then
debugPrint("^6Bridge^7: ^2Registering ^4"..stashResource.." ^3Stash^7:", name, "^4Label^7: "..label)
else
debugPrint("^1ERROR^7: ^1Couldn't find supported inventory to register stash^7:", name)
end
end
if isServer() then
--- Registers an event to create an OX stash from the server.
--- When triggered, it calls registerStash with the provided parameters.
---
--- @event server:makeOXStash
--- @param name string Unique stash identifier.
--- @param label string Display name for the stash.
--- @param slots number|nil (Optional) Number of slots.
--- @param weight number|nil (Optional) Maximum weight.
--- @param owner string|nil (Optional) Owner identifier.
--- @param coords table|nil (Optional) Stash coordinates.
--- @usage
--- ```lua
--- TriggerEvent(getScript()..":server:makeOXStash", name, label, slots, weight, owner, coords, token)
--- ```
RegisterNetEvent(getScript()..":server:makeOXStash", function(name, label, slots, weight, owner, coords, token)
local src = source or nil
if src then
if not checkToken(src, token, "stash", name) then
return
end
end
registerStash(name, label, slots, weight, owner, coords)
end)
end
RegisterNetEvent(getScript()..":openGrabBox", function(data)
if isStarted(OXInv) then
return
end
local id = ""
if data.metadata then
id = data.metadata.id
elseif data.info then
id = data.info.id
end
openStash({
stash = id,
})
end)

View File

@@ -17,206 +17,17 @@
the module uses DrawText3D prompts. This is experimental and may not work as expected.
]]
local targetFunc = {
{ targetName = OXTargetExport,
entityTarget =
function(entity, opts, dist)
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
return exports[OXTargetExport]:addLocalEntity(entity, options)
end,
boxTarget =
function(data, opts, dist)
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].groups or opts[i].job or opts[i].gang,
onSelect = opts[i].onSelect or opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
data[5].maxZ = data[5].maxZ or (data[2].z + 0.80)
data[5].minZ = data[5].minZ or data[2].z - 1.05
local thickness = ((data[5].maxZ / 2) - (data[5].minZ / 2)) * 2
local mid = data[5].maxZ - ((data[5].maxZ / 2) - (data[5].minZ / 2))
data[2] = vec3(data[2].x, data[2].y, mid) -- force the coord to middle of the minZ and maxZ
local target = exports[OXTargetExport]:addBoxZone({
coords = data[2],
size = vec3(data[4], data[3], thickness), -- size uses the math to determine how high it needs to be
rotation = data[5].heading,
debug = data[5].debugPoly,
options = options
})
return target
end,
circleTarget =
function(data, opts, dist)
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].onSelect or opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
local target = exports[OXTargetExport]:addSphereZone({
coords = data[2],
radius = data[3],
debug = data[4].debugPoly,
options = options
})
return target
end,
modelTarget =
function(models, opts, dist)
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
return exports[OXTargetExport]:addModel(models, options)
end,
removeTargetEntity =
function(entity)
exports[OXTargetExport]:removeLocalEntity(entity, nil)
end,
removeTargetZone =
function(target)
exports[OXTargetExport]:removeZone(target, true)
end,
removeTargetModel =
function(model)
exports[OXTargetExport]:removeModel(model, nil)
end,
},
{ targetName = QBTargetExport,
entityTarget =
function(entity, opts, dist)
local options = { options = opts, distance = dist }
return exports[QBTargetExport]:AddTargetEntity(entity, options)
end,
boxTarget =
function(data, opts, dist)
local options = { options = opts, distance = dist }
local target = exports[QBTargetExport]:AddBoxZone(data[1], data[2], data[3], data[4], data[5], options)
return data[1]
end,
circleTarget =
function(data, opts, dist)
local options = { options = opts, distance = dist }
local target = exports[QBTargetExport]:AddCircleZone(data[1], data[2], data[3], data[4], options)
return data[1]
end,
modelTarget =
function(models, opts, dist)
local options = { options = opts, distance = dist }
return exports[QBTargetExport]:AddTargetModel(models, options)
end,
removeTargetEntity =
function(entity)
exports[QBTargetExport]:RemoveTargetEntity(entity)
end,
removeTargetZone =
function(target)
exports[QBTargetExport]:RemoveZone(target)
end,
removeTargetModel =
function(model)
exports[QBTargetExport]:RemoveTargetModel(model, "Test")
end,
},
{ targetName = "jim_bridge",
entityTarget =
function(entity, opts, dist)
return exports.jim_bridge:createEntityTarget(entity, opts, dist)
end,
boxTarget =
function(data, opts, dist)
return exports.jim_bridge:createZoneTarget(data, opts, dist)
end,
circleTarget =
function(data, opts, dist)
return exports.jim_bridge:createZoneTarget(data, opts, dist)
end,
modelTarget =
function(models, opts, dist)
return exports.jim_bridge:createModelTarget(models, opts, dist)
end,
removeTargetEntity =
function(entity)
exports.jim_bridge:removeEntityTarget(entity)
end,
removeTargetZone =
function(target)
exports.jim_bridge:removeZoneTarget(target)
end,
removeTargetModel =
function(model)
exports.jim_bridge:removeZoneTarget(model)
end,
},
}
-------------------------------------------------------------
-- Utility Data & Tables
-------------------------------------------------------------
-- Tables for storing created targets for the fallback system and zone management.
local TextTargets = {} -- For fallback DrawText3D targets.
local targetEntities = {} -- For entity targets.
local boxTargets = {} -- For box-shaped zone targets.
local circleTargets = {} -- For circular zone targets.
local modelTargets = {}
-------------------------------------------------------------
-- Entity Target Creation
-------------------------------------------------------------
@@ -252,22 +63,33 @@ function createEntityTarget(entity, opts, dist)
-- Store the target entity for later cleanup.
targetEntities[#targetEntities + 1] = entity
-- if force target off, use jim_bridge built in target functions
if Config.System.DontUseTarget then
-- Fallback: Use DrawText3D if targeting systems are disabled or unavailable.
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..OXTargetExport.." ^2for entity ^7"..entity)
exports.jim_bridge:createEntityTarget(entity, opts, dist)
debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6jim_bridge ^2for entity ^7"..entity)
return
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..script.targetName.." ^2for entity ^7"..entity)
return script.entityTarget(entity, opts, dist)
elseif isStarted(OXTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..OXTargetExport.." ^2for entity ^7"..entity)
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
end
exports[OXTargetExport]:addLocalEntity(entity, options)
elseif isStarted(QBTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..QBTargetExport.." ^2for entity ^7"..entity)
local options = { options = opts, distance = dist }
exports[QBTargetExport]:AddTargetEntity(entity, options)
end
end
-------------------------------------------------------------
@@ -324,75 +146,47 @@ end
---}, 2.0)
---```
function createBoxTarget(data, opts, dist)
-- if force target off, use jim_bridge built in target functions
if Config.System.DontUseTarget then
debugPrint("^6Bridge^7: ^2Creating new ^3Box ^2target with ^6jim_bridge ^7"..data[1])
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6DrawText ^2 for zone ^7"..data[1])
return exports.jim_bridge:createZoneTarget(data, opts, dist)
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
debugPrint("^6Bridge^7: ^2Creating new ^3Box ^2target with ^6"..script.targetName.." ^7"..data[1])
if data[5].minZ or data[5].maxZ then
local adMinZ, adMaxZ
data[5].minZ, data[5].maxZ, adMinZ, adMaxZ = adjustMinMaxZ(data[2], { minZ = data[5].minZ, maxZ = data[5].maxZ })
if adMinZ or adMaxZ then
print("^5Debug^7: ^2Auto adjusted ^7'^4"..data[1].."^7' ^2minZ and maxZ because ^1they weren't set correctly ^2remove or fix them for this target")
end
end
local target = script.boxTarget(data, opts, dist)
boxTargets[#boxTargets + 1] = target
return target
elseif isStarted(OXTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..OXTargetExport.." ^2for zone ^7"..data[1])
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].onSelect or opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
end
return nil
end
function createPropTarget(data, opts, dist)
-- Audo create a location based on a coord and a prop models dimensions
local width, depth, _ = GetPropDimensions(data[3])
local min, max = GetModelDimensions(data[3])
local coordAdjustment = data[2] - vec4(0, 0, 1.03, 0)
local newData = {
[1] = data[1],
[2] = coordAdjustment.xyz,
[3] = width + 0.1,
[4] = depth + 0.1,
[5] = {
name = data[1],
heading = coordAdjustment.w - 90.0,
debugPoly = debugMode,
minZ = coordAdjustment.z + (min.z) - 0.1,
maxZ = coordAdjustment.z + (max.z) + 0.1,
}
}
if data[3] then -- If spawning a prop, make entity target instead (ignores depth and width stuff)
makeDistProp({ prop = data[3], coords = data[2] }, true, false)
end
-- if force target off, use jim_bridge built in target functions
if Config.System.DontUseTarget then
debugPrint("^6Bridge^7: ^2Creating new ^3Box ^2target with ^6jim_bridge ^7"..data[1])
return exports.jim_bridge:createZoneTarget(newData, opts, dist)
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
debugPrint("^6Bridge^7: ^2Creating new ^3Box ^2target with ^6"..script.targetName.." ^7"..data[1])
local target = script.boxTarget(newData, opts, dist)
boxTargets[#boxTargets + 1] = target
return target
if not data[5].useZ then
local z = data[2].z + math.abs(data[5].maxZ - data[5].minZ) / 2
data[2] = vec3(data[2].x, data[2].y, z)
end
end
return nil
end
local target = exports[OXTargetExport]:addBoxZone({
coords = data[2],
size = vec3(data[4], data[3], (data[5].useZ or not data[5].maxZ) and data[2].z or math.abs(data[5].maxZ - data[5].minZ)),
rotation = data[5].heading,
debug = data[5].debugPoly,
options = options
})
boxTargets[#boxTargets + 1] = target
return target
elseif isStarted(QBTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..QBTargetExport.." ^2for zone ^7"..data[1])
local options = { options = opts, distance = dist }
local target = exports[QBTargetExport]:AddBoxZone(data[1], data[2], data[3], data[4], data[5], options)
boxTargets[#boxTargets + 1] = target
return data[1]
end
end
-------------------------------------------------------------
-- Circle Zone Target Creation
@@ -431,25 +225,39 @@ end
--- }, 2.0)
--- ```
function createCircleTarget(data, opts, dist)
-- if force target off, use jim_bridge built in target functions
if Config.System.DontUseTarget then
debugPrint("^6Bridge^7: ^2Creating new ^3Sphere ^2target with ^6jim_bridge ^7"..data[1])
debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6DrawText ^2for zone ^7"..data[1])
return exports.jim_bridge:createZoneTarget(data, opts, dist)
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
debugPrint("^6Bridge^7: ^2Creating new ^3Sphere ^2target with ^6"..script.targetName.." ^7"..data[1])
local target = script.circleTarget(data, opts, dist)
circleTargets[#circleTargets + 1] = target
return target
elseif isStarted(OXTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6"..OXTargetExport.." ^2for zone ^7"..data[1])
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].onSelect or opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
local target = exports[OXTargetExport]:addSphereZone({
coords = data[2],
radius = data[3],
debug = data[4].debugPoly,
options = options
})
circleTargets[#circleTargets + 1] = target
return target
elseif isStarted(QBTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6"..QBTargetExport.." ^2for zone ^7"..data[1])
local options = { options = opts, distance = dist }
local target = exports[QBTargetExport]:AddCircleZone(data[1], data[2], data[3], data[4], options)
circleTargets[#circleTargets + 1] = target
return data[1]
end
return nil
end
-------------------------------------------------------------
@@ -478,24 +286,29 @@ end
---}, 2.0)
---```
function createModelTarget(models, opts, dist)
-- if force target off, use jim_bridge built in target functions
if Config.System.DontUseTarget then
debugPrint("^6Bridge^7: ^2Creating new ^3Model ^2target with ^6jim_bridge^7")
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
return exports.jim_bridge:createModelTarget(models, opts, dist)
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
debugPrint("^6Bridge^7: ^2Creating new ^3Model ^2target with ^6"..script.targetName.."^7")
local target = script.modelTarget(models, opts, dist)
modelTargets[#modelTargets + 1] = target
return target
elseif isStarted(OXTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Model^2 target with ^6"..OXTargetExport)
local options = {}
for i = 1, #opts do
options[i] = {
icon = opts[i].icon,
label = opts[i].label,
items = opts[i].item or nil,
groups = opts[i].job or opts[i].gang,
onSelect = opts[i].action,
distance = dist,
canInteract = opts[i].canInteract or nil,
}
end
exports[OXTargetExport]:addModel(models, options)
elseif isStarted(QBTargetExport) then
debugPrint("^6Bridge^7: ^2Creating new ^3Model^2 target with ^6"..QBTargetExport)
local options = { options = opts, distance = dist }
exports[QBTargetExport]:AddTargetModel(models, options)
end
end
-------------------------------------------------------------
@@ -511,20 +324,15 @@ end
--- removeEntityTarget(entityId)
--- ```
function removeEntityTarget(entity)
if Config.System.DontUseTarget then
if isStarted(QBTargetExport) then
exports[QBTargetExport]:RemoveTargetEntity(entity)
end
if isStarted(OXTargetExport) then
exports[OXTargetExport]:removeLocalEntity(entity, nil)
end
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
exports.jim_bridge:removeEntityTarget(entity)
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
script.removeTargetEntity(entity)
break
end
end
end
--- Removes a previously created zone target.
@@ -537,18 +345,14 @@ end
--- removeZoneTarget(targetObject)
--- ```
function removeZoneTarget(target)
if Config.System.DontUseTarget then
exports.jim_bridge:removeZoneTarget(target)
if isStarted(QBTargetExport) then
exports[QBTargetExport]:RemoveZone(target)
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
script.removeTargetZone(target)
break
end
if isStarted(OXTargetExport) then
exports[OXTargetExport]:removeZone(target, true)
end
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
exports.jim_bridge:removeZoneTarget(target)
end
end
@@ -561,20 +365,121 @@ end
--- removeModelTarget(model)
--- ```
function removeModelTarget(model)
if Config.System.DontUseTarget then
exports.jim_bridge:removeZoneTarget(model)
if isStarted(QBTargetExport) then
exports[QBTargetExport]:RemoveTargetModel(model, "Test")
end
-- Check for target script and use that
for i = 1, #targetFunc do
local script = targetFunc[i]
if isStarted(script.targetName) then
script.removeTargetModel(model)
break
end
if isStarted(OXTargetExport) then
exports[OXTargetExport]:removeModel(model, nil)
end
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
exports.jim_bridge:removeZoneTarget(target)
end
end
-------------------------------------------------------------
-- Fallback: DrawText3D Targets (Experimental)
-------------------------------------------------------------
-- If no targeting system is detected and this is a client script, use DrawText3D for targets.
if (Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport))) and not isServer() then
CreateThread(function()
while true do
local ped = PlayerPedId()
local pedCoords = GetEntityCoords(ped)
local camCoords = GetGameplayCamCoord()
local camRot = GetGameplayCamRot(2)
local camForward = RotationToDirection(camRot)
local closestTarget = nil
local closestDist = math.huge
local targetEntity = nil
-- Shallow copy for safety
local targetsCopy = {}
for k, v in pairs(TextTargets) do
targetsCopy[k] = v
end
-- Detect models and update coords
for _, target in pairs(targetsCopy) do
if target.models then
if not target.entity or not DoesEntityExist(target.entity) then
for _, model in ipairs(target.models) do
local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false)
if entity and entity ~= 0 then
target.entity = entity
target.coords = GetEntityCoords(entity)
break
end
end
else
target.coords = GetEntityCoords(target.entity)
end
end
end
-- Identify closest visible target
for _, target in pairs(targetsCopy) do
if target.coords then
local dist = #(pedCoords - target.coords)
local vecToTarget = target.coords - camCoords
local normVec = normalizeVector(vecToTarget)
local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z
local isFacing = dot > 0.5
if dist <= target.dist and isFacing then
if dist < closestDist then
closestDist = dist
closestTarget = target
targetEntity = target.entity
end
end
end
end
-- Render + handle input
for _, target in pairs(targetsCopy) do
if target.coords and #(pedCoords - target.coords) <= target.dist then
local isClosest = (target == closestTarget)
for _, opt in ipairs(target.options) do
if IsControlJustPressed(0, opt.key) and isClosest then
local canInteract = (not target.canInteract or target.canInteract())
local hasItem = (not opt.item or hasItem(opt.item))
local hasJob = (not opt.job or hasJob(opt.job, nil))
if canInteract and hasItem and hasJob then
if opt.onSelect then opt.onSelect(targetEntity) end
if opt.action then opt.action(targetEntity) end
end
end
end
-- Draw each eligible text line
local baseZ = target.coords.z + 1.0
local lineHeight = -0.16
local lineOffset = 0
for i, opt in ipairs(target.options) do
local canInteract = (not target.canInteract or target.canInteract())
local hasItem = (not opt.item or hasItem(opt.item))
local hasJob = (not opt.job or hasJob(opt.job, nil))
if canInteract and hasItem and hasJob then
local text = target.buttontext[i]
local zOffset = lineOffset * lineHeight
DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + zOffset), text, isClosest)
lineOffset += 1
end
end
end
end
Wait(0)
end
end)
end
function ShowFloatingHelpNotification(coord, text, highlight)
AddTextEntry("FloatingText", text)
@@ -584,11 +489,16 @@ function ShowFloatingHelpNotification(coord, text, highlight)
EndTextCommandDisplayHelp(2, false, false, -1)
end
function updateCachedText(target)
target.text = table.concat(target.buttontext, "\n")
end
-------------------------------------------------------------
-- Cleanup on Resource Stop
-------------------------------------------------------------
local function CleanupTargets()
-- When the current resource stops, remove all targets.
onResourceStop(function()
-- Remove entity targets.
for i = 1, #targetEntities do
if isStarted(OXTargetExport) then
@@ -602,7 +512,7 @@ local function CleanupTargets()
if isStarted(OXTargetExport) then
exports[OXTargetExport]:removeZone(boxTargets[i], true)
elseif isStarted(QBTargetExport) then
exports[QBTargetExport]:RemoveZone(boxTargets[i])
exports[QBTargetExport]:RemoveZone(boxTargets[i].name)
end
end
-- Remove circle zone targets.
@@ -610,16 +520,7 @@ local function CleanupTargets()
if isStarted(OXTargetExport) then
exports[OXTargetExport]:removeZone(circleTargets[i], true)
elseif isStarted(QBTargetExport) then
exports[QBTargetExport]:RemoveZone(circleTargets[i])
exports[QBTargetExport]:RemoveZone(circleTargets[i].name)
end
end
end
onPlayerUnload(function()
CleanupTargets()
end)
-- When the current resource stops, remove all targets.
onResourceStop(function()
CleanupTargets()
end, true)

View File

@@ -57,7 +57,7 @@ function searchCar(vehicle)
}
if Vehicles then
for k, v in pairs(Vehicles) do
if tonumber(v.hash) == model or joaat(v.hash) == model or joaat(v.model) == model then
if tonumber(v.hash) == model or GetHashKey(v.hash) == model or GetHashKey(v.model) == model then
debugPrint("^6Bridge^7: ^2Vehicle info found in^7 ^4Vehicles^7 ^2table^7: ^6"..(v.hash and v.hash or v.model).. " ^7(^6"..Vehicles[k].name.."^7)")
carInfo.name = Vehicles[k].name.." "..Vehicles[k].brand
carInfo.price = Vehicles[k].price
@@ -105,28 +105,16 @@ end
function getVehicleProperties(vehicle)
if not vehicle then return nil end
local propertyFunc = {
{ framework = OXLibExport,
func = function(vehicle)
return lib.getVehicleProperties(vehicle)
end,
},
{ framework = QBExport,
func = function(vehicle)
return Core.Functions.GetVehicleProperties(vehicle)
end,
},
}
for i = 1, #propertyFunc do
local prop = propertyFunc[i]
if isStarted(prop.framework) then
local properties = prop.func(vehicle)
debugPrint("^6Bridge^7: ^2Getting Vehicle Properties ^7[^6"..prop.framework.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..properties.model.."^7] - [^3"..properties.plate.."^7]")
return properties
end
end
local properties = {}
if isStarted(QBExport) and not isStarted(QBXExport) then
properties = Core.Functions.GetVehicleProperties(vehicle)
debugPrint("^6Bridge^7: ^2Getting Vehicle Properties ^7[^6"..QBExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..properties.model.."^7] - [^3"..properties.plate.."^7]")
return nil
elseif isStarted(OXLibExport) then
properties = lib.getVehicleProperties(vehicle)
debugPrint("^6Bridge^7: ^2Getting Vehicle Properties ^7[^6"..OXLibExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..properties.model.."^7] - [^3"..properties.plate.."^7]")
end
return properties
end
--- Sets the properties of a given vehicle if changes are detected.
@@ -240,7 +228,7 @@ function pushVehicle(entity)
if entity ~= 0 and DoesEntityExist(entity) then
-- Request network control if not already controlled.
if not NetworkHasControlOfEntity(entity) then
debugPrint("^6Bridge^7: ^3pushEnt^7: ^2Requesting network control of vehicle^7.")
debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Requesting network control of vehicle^7.")
NetworkRequestControlOfEntity(entity)
local timeout = 2000
while timeout > 0 and not NetworkHasControlOfEntity(entity) do
@@ -248,13 +236,13 @@ function pushVehicle(entity)
timeout = timeout - 100
end
if NetworkHasControlOfEntity(entity) then
debugPrint("^6Bridge^7: ^3pushEnt^7: ^2Network now has control of the entity^7.")
debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Network now has control of the entity^7.")
end
end
-- Set as mission entity if not already set.
if not IsEntityAMissionEntity(entity) then
debugPrint("^6Bridge^7: ^3pushEnt^7: ^2Setting vehicle as a ^7'^2mission^7' ^2entity^7.")
debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Setting vehicle as a ^7'^2mission^7' ^2entity^7.")
SetEntityAsMissionEntity(entity, true, true)
local timeout = 2000
while timeout > 0 and not IsEntityAMissionEntity(entity) do
@@ -262,23 +250,18 @@ function pushVehicle(entity)
timeout = timeout - 100
end
if IsEntityAMissionEntity(entity) then
debugPrint("^6Bridge^7: ^3pushEnt^7: ^2Vehicle is a ^7'^2mission^7'^2 entity^7.")
debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Vehicle is a ^7'^2mission^7'^2 entity^7.")
end
end
end
end
-- add entitty named version
function pushEnt(...) pushVehicle(...) end
--- Finds the closest vehicle to the specified coordinates.
--- The function uses different APIs based on whether a source is provided.
---
--- @param coords table|vector3 (Optional) The reference coordinates. If nil, uses the player's position.
--- @param src boolean (Optional) If true, uses GetPlayerPed(source) and GetAllVehicles.
--- @return number closestVehicle The closest vehicle entity and its distance.
--- @return number closestDistance The distance of the closest vehicle.
--- @return number|number closestVehicle|closestDistance The closest vehicle entity and its distance.
---
--- @usage
--- ```lua
@@ -288,11 +271,9 @@ function getClosestVehicle(coords, src)
local ped, vehicles, closestDistance, closestVehicle
if src then
-- if checking server side cache src's ped and use server native
ped = GetPlayerPed(src)
vehicles = GetAllVehicles()
else
-- if checking client side cache local ped and use client native
ped = PlayerPedId()
vehicles = GetGamePool('CVehicle')
end
@@ -309,7 +290,7 @@ function getClosestVehicle(coords, src)
for i = 1, #vehicles, 1 do
local vehicleCoords = GetEntityCoords(vehicles[i])
local distance = #(vehicleCoords - coords.xyz)
local distance = #(vehicleCoords - coords)
if closestDistance == -1 or distance < closestDistance then
closestDistance = distance
@@ -318,62 +299,4 @@ function getClosestVehicle(coords, src)
end
return closestVehicle, closestDistance
end
--- Checks whether a vehicle is owned or not using the plate as reference
---
--- @param plate string The plate of the vehicle to check
--- @return boolean whether vehicle is owned
---
--- @usage
--- ```lua
--- local plate = "ABCD1234"
--- local isVehicleOwned = isVehicleOwned(plate)
--- ```
local vehiclesOwned = {}
function isVehicleOwned(plate)
-- If already checked, cache it to reduce database calls
if vehiclesOwned[plate] == true then
return true
else
-- Find frameworks vehicle table and search sql for if vehicle plate is owned
local sqlTable = "player_vehicles"
local vehDatabase = {
{ framework = ESXExport,
sqlTable = "owned_vehicles"
},
{ framework = QBExport,
sqlTable = "player_vehicles"
},
{ framework = QBXExport,
sqlTable = "player_vehicles"
},
{ framework = OXCoreExport,
sqlTable = "owned_vehicles"
},
}
for i = 1, #vehDatabase do
local framework = vehDatabase[i]
if isStarted(framework.framework) then
sqlTable = framework.sqlTable
break
end
end
local result = MySQL.query.await("SELECT 1 from "..sqlTable.." WHERE plate = ?", { plate })
if json.encode(result) ~= "[]" then
vehiclesOwned[plate] = true -- Cache ownership for later checks
return true
else
return false
end
end
end

View File

@@ -1,3 +1,4 @@
--- Registers a command with the active command system.
--- This function supports multiple command systems (OXLib, qb-core, ESX Legacy).
---
@@ -14,62 +15,30 @@
--- registerCommand("greet", {
--- "Greets the player",
--- { name = "name", help = "Name of the player to greet" },
--- nil,
--- function(source, args) print("Hello, "..args[1].."!") end,
--- nil,
--- "admin"
--- })
--- ```
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 = optionTable.helpInfo,
params = optionTable.subText,
restricted = optionTable.restriction and "group."..optionTable.restriction or nil
},
optionTable.funct)
lib.addCommand(command, { help = options[1], restricted = options[5] and "group."..options[5] or nil }, options[4])
elseif isStarted(QBExport) and not isStarted(QBXExport) then
commandResource = QBExport
Core.Commands.Add(command,
optionTable.helpInfo,
optionTable.subText,
optionTable.argsRequired,
optionTable.funct,
optionTable.restriction or nil
)
Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil)
elseif isStarted(RSGExport) then
commandResource = RSGExport
Core.Commands.Add(command,
optionTable.helpInfo,
optionTable.subText,
optionTable.argsRequired,
optionTable.funct,
optionTable.restriction or nil
)
Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil)
elseif isStarted(ESXExport) then
commandResource = ESXExport
ESX.RegisterCommand(command,
optionTable.restriction or 'admin',
function(xPlayer, args, showError)
optionTable.funct(xPlayer.source, args, showError)
end,
false,
{ help = options[1] }
)
ESX.RegisterCommand(command, options[5] or 'admin', function(xPlayer, args, showError)
options[4](xPlayer.source, args, showError)
end, false, { help = options[1] })
end
if commandResource ~= "" then

View File

@@ -9,6 +9,7 @@ Exports = {
OXInv = "ox_inventory",
QBInv = "qb-inventory",
PSInv = "ps-inventory",
QSInv = "qs-inventory",
CoreInv = "core_inventory",
CodeMInv = "codem-inventory",
OrigenInv = "origen_inventory",
@@ -24,60 +25,19 @@ Exports = {
-- REDM
RSGExport = "rsg-core",
RSGInv = "rsg-inventory",
VorpExport = "vorp_core",
VorpInv = "vorp_inventory",
VorpMenu = "vorp_menu",
RSGInv = "rsg-inventory"
}
-- Required variables
debugMode = false
debugMode = Config.System.Debug
QBInvNew = true
InventoryWeight = 120000
-- Corruptted transfer file Checks -- Not Ready Yet ⚠️
--CreateThread(function()
-- local res = GetCurrentResourceName()
-- local ok, val = pcall(function()
-- return exports[res]:ping_encrypted()
-- end)
--
-- if not ok or val ~= "ok" then
-- print("\n^6===============================================================================")
-- print(("\n^1Corruption Warning^7: ^1[^7%s^1] Encrypted files failed to initialize.^7\n"):format(res))
-- print("^3Common causes^7:")
-- print(" • Possible corrupt files")
-- print(" • Damaged or partial upload (FTP/zip/unzip issue).")
-- print(" • Outdated artifact version\n")
-- print("^2How To Fix^7:")
-- print(" • Update your server artifact version")
-- print(" • Re-download asset from Keymaster")
-- print(" • Use WinSCP to upload")
-- print(" • Avoid editing any encrypted files")
-- print(" • Restart the server")
-- print("\n^6===============================================================================^7\n")
-- end
--end)
-- Missing/Broken Config file check
if not Config or not Config.System then
print("\n^6=================================================================================")
print("^6=================================================================================\n")
print(" ^1ERROR^7: jim_bridge ^1can't find the ^7Config^1 table in ^7"..GetCurrentResourceName())
print(" ^1It is either missing, or there is an error inside it stopping it from loading^7\n")
print("^6=================================================================================")
print("^6=================================================================================^7\n")
end
-- [[ Server.Cfg Convar Check ]]--
-- Check server convars for hard set defaults, otherwise it uses per script configurations
if Config and Config.System then
debugMode = Config.System.Debug
if Config.System.Debug then
if GetConvar("jim_DisableDebug", "false") == "true" then
debugMode = false
@@ -86,6 +46,7 @@ if Config and Config.System then
Config.System.EventDebug = false
end
end
Config.System.Menu = GetConvar("jim_menuScript", Config.System.Menu)
Config.System.Notify = GetConvar("jim_notifyScript", Config.System.Notify or "gta")
Config.System.ProgressBar = GetConvar("jim_progressBarScript", Config.System.ProgressBar or "gta")
@@ -188,6 +149,7 @@ for _, v in pairs({ -- This is a specific load order
'wrapperfunctions.lua',
'polyZone.lua',
'inventories.lua',
'itemcontrol.lua',
'playerfunctions.lua',
'metaHandlers.lua',
@@ -205,13 +167,14 @@ for _, v in pairs({ -- This is a specific load order
-- Crafting / Shops / Stashes
'crafting.lua',
'shops.lua',
'stashcontrol.lua',
-- Kind of "other"
'isAnimal.lua',
'scaleEntity.lua',
'vehicles.lua',
'effects.lua',
'make/ropeControl.lua',
--'warmenu.lua',
'_authToken.lua',

View File

@@ -1,289 +0,0 @@
local alcoholCount = 0
local drugCount = 0
local purgeTimer = 300000
local minDrugCount = 3
local maxDrugCount = 7
local minAlcoholCount = 2
local midAlcoholCount = 4
local maxAlcoholCount = 7
local alcoholEffectData = {
levels = {
["min"] = {
effect = "DrugsMichaelAliensFightIn",
movement = "move_m@drunk@slightlydrunk",
camShake = 0.5,
canStumble = false,
},
["mid"] = {
effect = "DrugsMichaelAliensFightIn",
movement = "move_m@drunk@moderatedrunk",
canRagdoll = true,
camShake = 0.5,
canStumble = true,
stumbleChance = 0.8,
drunkDriving = true,
},
["max"] = {
effect = "DrugsMichaelAliensFightIn",
movement = "move_m@drunk@verydrunk",
canRagdoll = true,
camShake = 2.8,
canStumble = true,
stumbleChance = 0.6,
drunkDriving = true,
},
},
}
local drugEffectData = {
levels = {
["min"] = {
--effect = "SwitchHUDTrevorIn",
--movement = "move_m@drunk@slightlydrunk",
},
["max"] = {
effect = "SwitchHUDTrevorIn",
movement = "move_m@drunk@slightlydrunk",
camShake = 0.2,
},
}
}
-- Alcohol Thread
function addAlocholCount(count, canOD)
alcoholCount = alcoholCount + count
--print("Alcohol count increased, current amount:", alcoholCount)
CreateThread(function()
runAlcoholThread()
end)
if alcoholCount >= maxAlcoholCount then
-- max
startAlcoholEffect(alcoholEffectData.levels["max"], "max")
if canOD then
SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) - math.random(10, 15))
end
elseif alcoholCount < maxAlcoholCount and alcoholCount >= midAlcoholCount then
-- mid
startAlcoholEffect(alcoholEffectData.levels["mid"], "mid")
TriggerEvent("evidence:client:SetStatus", "heavyalcohol", 200)
if canOD then
SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) - math.random(5, 10))
end
elseif alcoholCount >= 1 then
-- min
TriggerEvent("evidence:client:SetStatus", "alcohol", 200)
startAlcoholEffect(alcoholEffectData.levels["min"], "min")
end
end
function getAlcoholCount()
return alcoholCount
end
function removeAlcoholCount(count)
alcoholCount = alcoholCount - count
if alcoholCount < minAlcoholCount then
clearCurrentAlcoholEffect()
end
--print("Alcohol count decreased, current amount:", alcoholCount)
end
local alcoholThreadRun = false
function runAlcoholThread()
if alcoholThreadRun == true then
return
else
alcoholThreadRun = true
end
--print("Alcohol Thread triggered")
while alcoholCount > 0 do
Wait(purgeTimer)
removeAlcoholCount(1)
if alcoholCount <= 0 then
clearCurrentAlcoholEffect()
alcoholThreadRun = false
break
end
end
end
exports("getAlcoholCount",getAlcoholCount)
exports("addAlocholCount", addAlocholCount)
exports("removeAlcoholCount", removeAlcoholCount)
-- EFFECT Management
local currentAlcoholEffect = ""
local alcoholEffect = ""
function clearCurrentAlcoholEffect()
local Ped = PlayerPedId()
if alcoholEffect ~= "" then
AnimpostfxStop(alcoholEffect)
ResetPedMovementClipset(Ped, 0.0)
ClearTimecycleModifier()
ResetScenarioTypesEnabled()
SetPedIsDrunk(Ped, false)
SetPedMotionBlur(Ped, false)
end
currentAlcoholEffect = ""
alcoholEffect = ""
end
function startAlcoholEffect(data, level)
local Ped = PlayerPedId()
if currentAlcoholEffect == level then
return
else
clearCurrentAlcoholEffect()
currentAlcoholEffect = level
end
-- Start effect using sent data
if alcoholEffect ~= data.effect then
alcoholEffect = data.effect
AnimpostfxPlay(data.effect, 0, true)
if data.movement then
RequestAnimSet(data.movement)
while not HasAnimSetLoaded(data.movement) do
Wait(100)
end
SetPedMovementClipset(Ped, data.movement, 3.0)
end
SetPedCanRagdoll(Ped, true)
if data.camShake then
ShakeGameplayCam("DRUNK_SHAKE", data.camShake)
end
SetTimecycleModifier("Drunk")
SetPedMotionBlur(Ped, true)
SetPedIsDrunk(Ped, true)
if data.drivingEffect or data.canStumble then
CreateThread(function()
local effectCheck = level
local lastStumbleTime = GetGameTimer() + 1200
local lastDriveTime = GetGameTimer() + 6000
while effectCheck == currentAlcoholEffect do
if data.camStumble and GetGameTimer() > lastStumbleTime and math.random() > (data.stumbleChance or 0.8) then
lastStumbleTime = GetGameTimer() + 1200
SetPedToRagdoll(Ped, 5000, 5000, 0, true, true, false)
end
if data.driving and GetGameTimer() > lastDriveTime then
local inVeh, veh, seat = GetSeatPedIsIn()
if inVeh then
lastDriveTime = GetGameTimer() + 6000
if seat == -1 then
if math.random() < 0.62 then
StartVehicleHorn(veh, 1000, "NORMAL", false)
end
end
end
end
Wait(1000)
end
--print("Effect loop broken")
end)
end
end
end
-- Helper
function GetSeatPedIsIn()
local inVeh, veh, seat = false, 0, 0
local veh = GetVehiclePedIsIn(PlayerPedId(), false)
if veh ~= 0 then
if GetPedInVehicleSeat(veh, -1) == PlayerPedId() then
inVeh = true
seat = -1
end
end
return inVeh, veh, seat
end
-- Drug thread
function addDrugCount(count, canOD)
drugCount = drugCount + count
--print("Drug count increased, current amount:", drugCount)
if drugCount >= minDrugCount and drugCount <= maxDrugCount then
startDrugEffect(drugEffectData.levels["min"], "min")
elseif drugCount > maxDrugCount then
if canOD then
SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) - math.random(10, 15))
end
startDrugEffect(drugEffectData.levels["max"], "max")
end
end
function getDrugCount()
return drugCount
end
function removeDrugCount(count)
--print("Drug count decreased, current amount:", drugCount)
drugCount = drugCount - count
end
local drugThreadRun = false
function runDrugThread()
if drugThreadRun == true then
return
else
drugThreadRun = true
end
--print("Drug Thread triggered")
while drugCount > 0 do
Wait(purgeTimer)
removeDrugCount(1)
if drugCount <= 0 then
drugThreadRun = false
break
end
end
end
exports("getDrugCount", getDrugCount)
exports("addDrugCount", addDrugCount)
exports("removeDrugCount", removeDrugCount)
-- EFFECT Management
local currentDrugEffect = ""
local drugEffect = ""
function startDrugEffect(data, level)
local Ped = PlayerPedId()
if currentDrugEffect == level then
return
else
clearCurrentAlcoholEffect()
currentDrugEffect = level
end
-- Start effect using sent data
if drugEffect ~= data.effect then
drugEffect = data.effect
AnimpostfxPlay(data.effect, 0, true)
if data.movement then
RequestAnimSet(data.movement)
while not HasAnimSetLoaded(data.movement) do
Wait(100)
end
SetPedMovementClipset(Ped, data.movement, 3.0)
end
SetPedCanRagdoll(Ped, true)
if data.camShake then
ShakeGameplayCam("DRUNK_SHAKE", data.camShake)
end
SetTimecycleModifier("Drunk")
SetPedMotionBlur(Ped, true)
SetPedIsDrunk(Ped, true)
end
end

View File

@@ -22,7 +22,7 @@ end
local function stopAnim(animDict, animName, ped)
StopAnimTask(ped or PlayerPedId(), animDict, animName, 0.5)
StopAnimTask(ped or PlayerPedId(), animName, animDict, 0.5)
RemoveAnimDict(animDict)
unloadAnimDict(animDict)
end
function redProgressBar(data)
@@ -53,11 +53,27 @@ 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(percentage, data.label, ("%.0f%%"):format(percentage))
ShowRedProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage))
-- Controls to disable during progress
if data.disableMouse then
@@ -135,48 +151,40 @@ function redProgressBar(data)
return result
end
function ShowRedProgressBar(percentage, title, level)
function ShowRedProgressBar(currentProg, 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
-- 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)
-- 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)
-- 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)
-- Track (bg)
DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
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
-- 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
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)
-- 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)
-- 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
end
end
@@ -205,14 +213,29 @@ function gtaProgressBar(data)
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
ShowGTAProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
ShowGTAProgressBar(segmentProgress, data.label, ("%.0f%%"):format(percentage))
-- Controls to disable during progress
DisablePlayerFiring(PlayerId(), true)
DisablePlayerFiring(ped, true)
DisableControlAction(0, 25, true) -- Disable aim
DisableControlAction(0, 21, true) -- Disable sprint
DisableControlAction(0, 30, true) -- Disable move left/right
@@ -223,13 +246,12 @@ function gtaProgressBar(data)
inProgress = false
end
end
result = inProgress
end)
-- Wait for completion or cancel
while result == nil do Wait(10) end
inProgress = false
while GetGameTimer() < endTime and inProgress do
Wait(100)
end
-- Cleanup animations/tasks
if data.dict then
@@ -239,6 +261,11 @@ function gtaProgressBar(data)
ClearPedTasks(ped)
end
result = inProgress
inProgress = false
while result == nil do Wait(10) end
-- Cleanup
FreezeEntityPosition(ped, false)
@@ -249,22 +276,14 @@ function gtaProgressBar(data)
return result
end
function ShowGTAProgressBar(percentage, title, level)
function ShowGTAProgressBar(currentProg, 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
-- 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)
-- 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)
-- Title (left)
SetTextFont(0)
SetTextProportional(1)
SetTextScale(0.35, 0.35)
@@ -274,35 +293,34 @@ function ShowGTAProgressBar(percentage, title, level)
SetTextOutline()
SetTextEntry("STRING")
AddTextComponentString(title)
DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034)
DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034) -- Adjust text position
-- 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)
DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030) -- Right-aligned additional text
-- Track (bg)
DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
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
-- 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
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)
-- Tick lines
for i = 1, (tickCount - 1) do
local x = barLeft + (barWidth * (i / tickCount))
DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
-- 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
end
end
function stopProgressBar() inProgress = false end
function isProgressBar() return inProgress end

View File

@@ -53,7 +53,7 @@ function gtaSkillCheck()
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 228, 52, 52, 255)
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
drawGTASuccessText(x, y, "Failed", 228, 52, 52)
drawSuccessText(x, y, "Failed", 228, 52, 52)
end
return false
end

View File

@@ -1,4 +1,5 @@
-- 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.
@@ -16,196 +17,122 @@ 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)
-- ===== 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
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
local existingTarget = nil
for _, target in pairs(TextTargets) do
if #(target.coords - entityCoords) < 0.01 then
existingTarget = target
break
end
TargetRegistry.byKey[key] = nil
end
if existingTarget then
for i = 1, #opts do
local key = KEY_TABLE[#existingTarget.options + i]
opts[i].key = key
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
existingTarget.options[#existingTarget.options + 1] = opts[i]
end
updateCachedText(existingTarget)
else
local tempText = {}
for i = 1, #opts do
opts[i].key = KEY_TABLE[i]
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
end
TextTargets[entity] = {
coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z),
buttontext = tempText,
options = opts,
dist = dist,
text = table.concat(tempText, "\n")
}
end
end
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
function createZoneTarget(data, opts, dist)
startTargetLoop()
local existingTarget = nil
for _, target in pairs(TextTargets) do
if #(target.coords - data[2]) < 0.01 then
existingTarget = target
break
end
end
TargetRegistry.byResource[res] = nil
-- print("^6Bridge^7:^5 Target^7: ^2Cleared "..cnt.." target(s) from '"..res.."'")
end)
-- ===== Helpers =====
local function vecKey(v)
-- stable rounded coord string for entity dedupe when name isn't provided
return ("%.3f,%.3f,%.3f"):format(v.x, v.y, v.z)
if existingTarget then
for i = 1, #opts do
local key = KEY_TABLE[#existingTarget.options + i]
opts[i].key = key
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
existingTarget.options[#existingTarget.options + 1] = opts[i]
end
updateCachedText(existingTarget)
else
local tempText = {}
for i = 1, #opts do
opts[i].key = KEY_TABLE[i]
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
end
TextTargets[data[1]] = {
coords = data[2],
buttontext = tempText,
options = opts,
dist = dist,
text = table.concat(tempText, "\n")
}
end
return data[1]
end
local function bakeButtons(opts)
function createModelTarget(models, opts, dist)
startTargetLoop()
if type(models) ~= "table" then
models = { models }
end
local tempText = {}
for i = 1, #opts do
opts[i].key = KEY_TABLE[i]
tempText[#tempText + 1] =
(" ~b~[~w~%s~b~] ~w~%s"):format(Keys[opts[i].key] or ("K"..opts[i].key), opts[i].label or ("Option "..i))
end
return tempText
end
-- Update cached text blob
local function updateCachedText(target)
target.text = table.concat(target.buttontext, "\n")
end
-- ===== Public API: Create targets =====
-- ENTITY: createEntityTarget(entity, opts, dist, nameOpt?)
function createEntityTarget(entity, opts, dist, name)
startTargetLoop()
if not entity or entity == 0 then return end
targetEntities[#targetEntities + 1] = entity
local owner = getOwnerResource()
local coords = GetEntityCoords(entity)
local key = tostring(name) or ("entity@" .. vecKey(coords))
-- Always overwrite on same key
local buttontext = bakeButtons(opts)
TextTargets[key] = {
_key = key,
_type = "entity",
_owner = owner,
entity = entity,
coords = vec3(coords.x, coords.y, coords.z),
buttontext = buttontext,
options = opts,
dist = dist,
}
updateCachedText(TextTargets[key])
registerTarget(owner, key)
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords))
return key
end
-- ZONE: createZoneTarget(data, opts, dist)
-- Expect data[1] = id/name, data[2] = vec3 coords (as in your original)
function createZoneTarget(data, opts, dist)
startTargetLoop()
local 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
-- 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, "_")
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
end
local buttontext = bakeButtons(opts)
TextTargets[key] = {
_key = key,
_type = "model",
_owner = owner,
local keyStr = ""
for i, m in ipairs(models) do
keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "")
end
local targetKey = "model_" .. keyStr
TextTargets[targetKey] = {
models = models,
buttontext = buttontext,
options = opts,
dist = dist,
coords = vec3(0, 0, 0), -- will be updated by the refresher
buttontext = tempText,
options = opts,
dist = dist,
coords = vec3(0, 0, 0),
text = table.concat(tempText, "\n")
}
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
return targetKey
end
-- ===== 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
function removeEntityTarget(entity)
TextTargets[entity] = nil
end
function removeZoneTarget(key)
if not key then return end
removeTargetKey(key, "removeZoneTarget")
function removeZoneTarget(target)
TextTargets[target] = nil
end
-- For models, pass the returned key from createModelTarget (recommended).
function removeModelTarget(key)
if not key then return end
removeTargetKey(key, "removeModelTarget")
function removeModelTarget(model)
TextTargets[model] = nil
end
exports("createEntityTarget", createEntityTarget)
@@ -216,20 +143,22 @@ exports("removeEntityTarget", removeEntityTarget)
exports("removeZoneTarget", removeZoneTarget)
exports("removeModelTarget", removeModelTarget)
-------------------------------------------------------------
-- Fallback: DrawText3D Targets (Experimental)
-------------------------------------------------------------
local started = false
function startTargetLoop()
if started then return end
started = true
Config = {
System = {
-- lazy include (unchanged from your original)
Config = { System = {} }
}
}
started = true
local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua')))
fileLoader()
-- Model Entity Refresher (kept)
-- Model Entity Refresher
CreateThread(function()
while true do
local pedCoords = GetEntityCoords(PlayerPedId())
@@ -249,7 +178,7 @@ function startTargetLoop()
end
end)
-- Main Target Loop (unchanged logic, just uses new TextTargets entries)
-- Main Target Loop
CreateThread(function()
while true do
local ped = PlayerPedId()
@@ -286,10 +215,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
@@ -299,8 +228,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
@@ -309,11 +238,12 @@ function startTargetLoop()
::continue::
end
Wait(1)
Wait(1) -- Throttled
end
end)
end
function DrawText3D(coord, text, highlight)
SetTextScale(0.30, 0.30)
SetTextFont(0)
@@ -323,28 +253,37 @@ function DrawText3D(coord, text, highlight)
SetTextCentre(true)
local totalLength = string.len(text)
local textMaxLength = 99
local txt = totalLength > textMaxLength and text:sub(1, textMaxLength) or text
AddTextComponentString(highlight and txt:gsub("%~w~", "~y~") or txt)
local textMaxLength = 99 -- max 99
local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text
AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text)
SetDrawOrigin(coord.x, coord.y, coord.z, 0)
DrawText(0.0, 0.0)
local count, length = GetLineCountAndMaxLength(txt)
local count, length = GetLineCountAndMaxLength(text)
local padding = 0.005
local heightFactor = (count / 43) + padding
local weightFactor = (length / 150) + padding
local height = (heightFactor / 2) - padding / 1
local width = (weightFactor / 2) - padding / 1
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 = lineCount + 1
lineCount += 1
local lineLength = string.len(line)
if lineLength > maxLength then
maxLength = lineLength
@@ -354,6 +293,7 @@ function GetLineCountAndMaxLength(text)
return lineCount, maxLength
end
function RotationToDirection(rot)
local adjust = math.pi / 180
return vec3(
@@ -371,3 +311,8 @@ 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

View File

@@ -1,14 +1,14 @@
2.1.09
2.0.20
- Add 100 wait for stopCam to reduce slow movement
- Add support for rcore_clothing in openClothing() - Thanks desypher
- Add hunger and thirst calls to getPlayer() - Thanks oosayeroo
- Add support for tgg-billing - Thanks GhostInLowGear
- Fix hasJob() checking players who haven't logged in yet
- Add exploit possibility for removeItem with minus numbers
- Fix movement being allowed when crafting
- Make distExploitCheck() more verbose
- CodeM slot fix, force to number (because numbers are apparently strings)
- ESX change to check statebag for hunger and thirst info
- Add Full support for lation_ui
- Add "Open Wheel" to searchCar()
- Increase timeout for cache timer from 5 seconds to 2 minutes
- Fix GTA progressbar erroring on playAnim()
- Add Igredient location indicator when crafting
- Fix JPRInv checks breaking early when caching framework info
- Enhance doesItemExist() and add getItemLabel() function
- Add Support for old qb-inventory config loading
- Fix typo in cancarry when using Tgiann Inv
- Add support for single progressBar when crafting
https://github.com/jimathy/jim_bridge/releases/latest
https://github.com/jimathy/jim_bridge