mirror of
https://github.com/jimathy/jim_bridge.git
synced 2026-08-17 05:56:02 +01:00
Compare commits
262 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e320bcb99 | ||
|
|
54f96f7e77 | ||
|
|
dffed24fb3 | ||
|
|
aae2bd5021 | ||
|
|
732b07c957 | ||
|
|
8e8010f837 | ||
|
|
e05815cfbd | ||
|
|
8594ba12ec | ||
|
|
9f914bd497 | ||
|
|
ad47836232 | ||
|
|
ae4637f9c4 | ||
|
|
a13284d0e0 | ||
|
|
98f8944a79 | ||
|
|
13b0f796ff | ||
|
|
b282d3393f | ||
|
|
180cc7b2a2 | ||
|
|
beb67b79a5 | ||
|
|
c359bc4342 | ||
|
|
2404fff002 | ||
|
|
03ca409bf3 | ||
|
|
9a78b057dd | ||
|
|
45f390c177 | ||
|
|
803c612cbb | ||
|
|
63bcb86ac1 | ||
|
|
0bc3c0f9b4 | ||
|
|
672dd7684b | ||
|
|
c040390d1f | ||
|
|
26b1f36029 | ||
|
|
331dc94e2f | ||
|
|
d1dab48acc | ||
|
|
7e595df143 | ||
|
|
d791a2e09f | ||
|
|
7738bb4fca | ||
|
|
3ad65810c3 | ||
|
|
45eeaf8f0d | ||
|
|
37f7eb1116 | ||
|
|
145e3aa156 | ||
|
|
bb176addbf | ||
|
|
178040348e | ||
|
|
ee15f3b076 | ||
|
|
6919f038ee | ||
|
|
47029da6ed | ||
|
|
e1d292d1bf | ||
|
|
f08638812c | ||
|
|
de5424c506 | ||
|
|
b46d734442 | ||
|
|
92ad96e435 | ||
|
|
6166314cbf | ||
|
|
9fa8f58cfb | ||
|
|
ed997ad8a2 | ||
|
|
20cea95783 | ||
|
|
0dc5a45b5d | ||
|
|
54b1280bef | ||
|
|
3ac65d4630 | ||
|
|
ed49876c69 | ||
|
|
69d0f07bb6 | ||
|
|
b975f318a0 | ||
|
|
ce1df2c205 | ||
|
|
75d8146e24 | ||
|
|
268c95f29a | ||
|
|
91dd069e5a | ||
|
|
f96d96f26e | ||
|
|
ae231b6101 | ||
|
|
c709159ea4 | ||
|
|
c8089109c6 | ||
|
|
807cfe1a2a | ||
|
|
420e035e2b | ||
|
|
a0377df713 | ||
|
|
301b069312 | ||
|
|
6c09e55fd4 | ||
|
|
d8467f0b02 | ||
|
|
a9832da7d7 | ||
|
|
2942907963 | ||
|
|
02ca13bcbd | ||
|
|
833bc675ce | ||
|
|
ac9a7df57b | ||
|
|
657bc3935c | ||
|
|
10085a3c23 | ||
|
|
0d3888565c | ||
|
|
8533bbff90 | ||
|
|
42630cc3c4 | ||
|
|
188fb81bdc | ||
|
|
cbc3a52643 | ||
|
|
bda18260b3 | ||
|
|
32dd938eef | ||
|
|
2826bc95c7 | ||
|
|
b35817740b | ||
|
|
fffef0c228 | ||
|
|
0caeca4824 | ||
|
|
b5536dbb6b | ||
|
|
26f8140ee8 | ||
|
|
6e4d9bc1d7 | ||
|
|
610f492762 | ||
|
|
f88cdb4f9d | ||
|
|
463358b53a | ||
|
|
3b038fb700 | ||
|
|
8efec21408 | ||
|
|
39691a2254 | ||
|
|
2924c611f7 | ||
|
|
be5449b100 | ||
|
|
1e5d4800fc | ||
|
|
cb49fa6949 | ||
|
|
315634c173 | ||
|
|
eafa881cf8 | ||
|
|
a851f8823c | ||
|
|
354002312e | ||
|
|
392e0601a8 | ||
|
|
035ef26486 | ||
|
|
db9e7fea0a | ||
|
|
7077b7d831 | ||
|
|
bd94b14dad | ||
|
|
8b0e4d803f | ||
|
|
5f81bfa899 | ||
|
|
85626d3356 | ||
|
|
ceeb9021d9 | ||
|
|
09887f3a9d | ||
|
|
26c341c6ec | ||
|
|
bfca698457 | ||
|
|
c13d39ca05 | ||
|
|
0bcf1b7ed4 | ||
|
|
d7622f5143 | ||
|
|
d1d992f807 | ||
|
|
16f77f442c | ||
|
|
3b959b8567 | ||
|
|
345a81aaf7 | ||
|
|
06ef5cb53b | ||
|
|
748e5e2201 | ||
|
|
981563df6a | ||
|
|
b4df8deac6 | ||
|
|
e3da31970e | ||
|
|
6fb06da0e4 | ||
|
|
945cd52787 | ||
|
|
a880c2dadb | ||
|
|
76855d3246 | ||
|
|
d6c4619717 | ||
|
|
f799e6b2d6 | ||
|
|
2acd506504 | ||
|
|
6cfb776eb4 | ||
|
|
f09e34b1b5 | ||
|
|
1146639d6f | ||
|
|
a9ac2e8897 | ||
|
|
1a1960f5a8 | ||
|
|
996b135973 | ||
|
|
9337238832 | ||
|
|
ee049a953c | ||
|
|
adfe3e8e08 | ||
|
|
71c9c99db7 | ||
|
|
dd66083f45 | ||
|
|
9ddd6422bd | ||
|
|
7d24c274cd | ||
|
|
3911824fc6 | ||
|
|
9729d2fef1 | ||
|
|
3cb4005889 | ||
|
|
bf759ebaf8 | ||
|
|
3d0eb3df65 | ||
|
|
fe09d0f773 | ||
|
|
880d2b0233 | ||
|
|
5865d2eeb2 | ||
|
|
528735abd1 | ||
|
|
d4df995c71 | ||
|
|
3b13fa663e | ||
|
|
e973c64ffd | ||
|
|
6ecf10c084 | ||
|
|
79e765fbb0 | ||
|
|
d4a08d5e2b | ||
|
|
eeb3f40018 | ||
|
|
e6b4c1d13c | ||
|
|
6ecbc3392a | ||
|
|
7f10c394de | ||
|
|
fae099fd93 | ||
|
|
64d43b54e9 | ||
|
|
d4abdbc93f | ||
|
|
b595453efe | ||
|
|
d255034c02 | ||
|
|
d6fd2cc28c | ||
|
|
7c47de5dc0 | ||
|
|
c1ab2b3505 | ||
|
|
1665ab41de | ||
|
|
6d397b59bf | ||
|
|
9dfbcbe878 | ||
|
|
ca0970b650 | ||
|
|
02218a4902 | ||
|
|
a900044306 | ||
|
|
7382a534a0 | ||
|
|
3b76b15bb4 | ||
|
|
f2eda0fef6 | ||
|
|
61c1cce814 | ||
|
|
0f1660e3ed | ||
|
|
e5b55e49b7 | ||
|
|
1ff01526bc | ||
|
|
1ed227080b | ||
|
|
b07c089946 | ||
|
|
d603067c77 | ||
|
|
5a97b8f83a | ||
|
|
d0a09871c7 | ||
|
|
e87aa8d74d | ||
|
|
e204e6153d | ||
|
|
5c69f8dd28 | ||
|
|
f3fa309dbf | ||
|
|
9d74e7cb46 | ||
|
|
fea72bba2e | ||
|
|
b364f14260 | ||
|
|
8077ec4f46 | ||
|
|
e5277574df | ||
|
|
e0563dd4ce | ||
|
|
1987cf3df3 | ||
|
|
9f5145c3b2 | ||
|
|
5921cf8adc | ||
|
|
f154b45076 | ||
|
|
2c41272c36 | ||
|
|
57d4beafed | ||
|
|
bdb526bd1e | ||
|
|
bb9dbe248f | ||
|
|
caf2e7fbed | ||
|
|
d0b8fb16d6 | ||
|
|
2e57e66095 | ||
|
|
05fc856e86 | ||
|
|
74e6a679e2 | ||
|
|
17db5aa31a | ||
|
|
387c97de71 | ||
|
|
de45491c8c | ||
|
|
4da4cbfc15 | ||
|
|
9fbf2f2211 | ||
|
|
41b2965743 | ||
|
|
a02c11d3cc | ||
|
|
c888aa5ecf | ||
|
|
30de9a2d9c | ||
|
|
804327f3aa | ||
|
|
ba59418b6d | ||
|
|
f39678002f | ||
|
|
3e402cae3d | ||
|
|
b4aa1b3eda | ||
|
|
0f9bab00ef | ||
|
|
9d8706e8c6 | ||
|
|
6ba1e14888 | ||
|
|
0454f8c0a9 | ||
|
|
e17d8f2b93 | ||
|
|
b44dfa91e5 | ||
|
|
51d4c88694 | ||
|
|
f5f502875e | ||
|
|
9c769f4e8e | ||
|
|
87cdd5502e | ||
|
|
68d713a8dc | ||
|
|
dd34f8aab1 | ||
|
|
4a3fb8db4d | ||
|
|
59733de08e | ||
|
|
2b7a1c2e6d | ||
|
|
5db9e88bb9 | ||
|
|
a5b1f750dd | ||
|
|
4c514b459e | ||
|
|
32b99d14e1 | ||
|
|
de8f7c050c | ||
|
|
9c4c92e696 | ||
|
|
0cedd85afb | ||
|
|
1bb125ed31 | ||
|
|
72778235b8 | ||
|
|
76c34b8c03 | ||
|
|
3ca2da2990 | ||
|
|
d3035efd5c | ||
|
|
64a74a8928 | ||
|
|
e0fcf23dba | ||
|
|
ad1ce5f9ea |
16
.gitattributes
vendored
Normal file
16
.gitattributes
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
21
.github/workflows/notify-discord.yml
vendored
21
.github/workflows/notify-discord.yml
vendored
@@ -1,17 +1,20 @@
|
||||
name: Discord Commit Notifier
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*' # triggers on all branches
|
||||
branches: ['*']
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Commits
|
||||
uses: Sniddl/discord-commits@v1.6
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
template: "avatar-with-link"
|
||||
include-extras: true
|
||||
- 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"
|
||||
|
||||
71
.github/workflows/notify-release.yml
vendored
Normal file
71
.github/workflows/notify-release.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
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 won’t 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"
|
||||
88
.github/workflows/release.yml
vendored
Normal file
88
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: Package & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (full history)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Derive vars
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
echo "name=${GITHUB_REPOSITORY##*/}" >> $GITHUB_OUTPUT # e.g. jim_bridge
|
||||
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT # strip leading v
|
||||
|
||||
- name: Build zip with folder prefix
|
||||
shell: bash
|
||||
run: |
|
||||
NAME='${{ steps.vars.outputs.name }}'
|
||||
VER='${{ steps.vars.outputs.version }}'
|
||||
git archive --format=zip --prefix="${NAME}/" -o "${NAME}-${VER}.zip" "$GITHUB_REF_NAME"
|
||||
ls -lh "${NAME}-${VER}.zip"
|
||||
|
||||
# --- Patch notes generation (commit titles + links) -------------------
|
||||
- name: Find previous tag (SemVer aware)
|
||||
id: prev
|
||||
shell: bash
|
||||
run: |
|
||||
CUR="${GITHUB_REF_NAME}"
|
||||
# Sort tags by version desc, drop the current, take the next newest
|
||||
PREV=$(git tag --sort=-v:refname | grep -v -x "$CUR" | head -n 1 || true)
|
||||
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||
echo "Previous tag: ${PREV:-<none>}"
|
||||
|
||||
- name: Generate RELEASE_NOTES.md from commits
|
||||
shell: bash
|
||||
run: |
|
||||
REPO="${{ github.repository }}"
|
||||
CUR="${GITHUB_REF_NAME}"
|
||||
PREV='${{ steps.prev.outputs.prev }}'
|
||||
|
||||
if [ -n "$PREV" ]; then
|
||||
RANGE="$PREV..$CUR"
|
||||
HEADER="## Changes in $CUR"
|
||||
else
|
||||
# First release: include all commits
|
||||
ROOT=$(git rev-list --max-parents=0 HEAD | tail -n 1)
|
||||
RANGE="$ROOT..$CUR"
|
||||
HEADER="## Changes"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "$HEADER"
|
||||
echo
|
||||
# Oldest → newest, subject only, skip merge commits
|
||||
# Escape '[' and ']' so markdown doesn't break on rare titles
|
||||
git log --reverse --no-merges --pretty=format:'- ['%h'] - %s' "$RANGE" \
|
||||
| perl -pe 's/([\[\]])/\\$1/g'
|
||||
} > RELEASE_NOTES.md
|
||||
|
||||
echo "--- RELEASE_NOTES.md ---"
|
||||
cat RELEASE_NOTES.md
|
||||
echo "------------------------"
|
||||
|
||||
- name: Create / Update GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
files: |
|
||||
*.zip
|
||||
body_path: RELEASE_NOTES.md # attach our generated notes
|
||||
# If you wanted GitHub's auto notes instead, set:
|
||||
# generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
10
README.md
10
README.md
@@ -53,11 +53,11 @@ setr jim_DisableDebug true
|
||||
setr jim_DisableEventDebug true
|
||||
|
||||
# Force the default setting for what framework scripts should be used
|
||||
setr jim_menuScript qb # qb, ox, gta, jim
|
||||
setr jim_notifyScript gta # qb, ox, gta, esx, okok, red
|
||||
setr jim_drawTextScript qb # qb, ox, gta, esx
|
||||
setr jim_progressBarScript qb # qb, ox, gta, esx
|
||||
setr jim_skillCheckScript qb # qb, ox, gta
|
||||
setr jim_menuScript qb # qb, ox, gta, lation
|
||||
setr jim_notifyScript gta # qb, ox, gta, esx, okok, red, lation
|
||||
setr jim_drawTextScript qb # qb, ox, gta, esx, lation
|
||||
setr jim_progressBarScript qb # qb, ox, gta, esx, lation
|
||||
setr jim_skillCheckScript qb # qb, ox, gta, lation
|
||||
setr jim_dontUseTarget false # Set to true to disable target systems and use draw text 3d
|
||||
```
|
||||
|
||||
|
||||
@@ -1,67 +1,78 @@
|
||||
local function parseVersion(version)
|
||||
local parts = {}
|
||||
for num in version:gmatch("%d+") do
|
||||
table.insert(parts, tonumber(num))
|
||||
end
|
||||
return parts
|
||||
local function readBoolMeta(key, default)
|
||||
local val = GetResourceMetadata(GetCurrentResourceName(), key, 0)
|
||||
if not val then return default end
|
||||
val = tostring(val):lower()
|
||||
return (val == 'true' or val == '1' or val == 'yes' or val == 'on')
|
||||
end
|
||||
|
||||
local function compareVersions(current, newest)
|
||||
local currentParts = parseVersion(current)
|
||||
local newestParts = parseVersion(newest)
|
||||
for i = 1, math.max(#currentParts, #newestParts) do
|
||||
local c = currentParts[i] or 0
|
||||
local n = newestParts[i] or 0
|
||||
if c < n then return -1
|
||||
elseif c > n then return 1 end
|
||||
local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false)
|
||||
|
||||
if not SUPPRESS_UPDATES then
|
||||
local function parseVersion(version)
|
||||
local parts = {}
|
||||
for num in version:gmatch("%d+") do
|
||||
table.insert(parts, tonumber(num))
|
||||
end
|
||||
return parts
|
||||
end
|
||||
return 0 -- equal
|
||||
end
|
||||
|
||||
function CheckBridgeVersion()
|
||||
if IsDuplicityVersion() then
|
||||
CreateThread(function()
|
||||
Wait(4000)
|
||||
local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version')
|
||||
--PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers)
|
||||
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers)
|
||||
if not body then
|
||||
print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)")
|
||||
return
|
||||
end
|
||||
local function compareVersions(current, newest)
|
||||
local currentParts = parseVersion(current)
|
||||
local newestParts = parseVersion(newest)
|
||||
for i = 1, math.max(#currentParts, #newestParts) do
|
||||
local c = currentParts[i] or 0
|
||||
local n = newestParts[i] or 0
|
||||
if c < n then return -1
|
||||
elseif c > n then return 1 end
|
||||
end
|
||||
return 0 -- equal
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in body:gmatch("[^\r\n]+") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
local newestVersionRaw = lines[1] or "0.0.0"
|
||||
local changelog = {}
|
||||
for i = 2, #lines do
|
||||
table.insert(changelog, lines[i])
|
||||
end
|
||||
|
||||
local compareResult = compareVersions(currentVersionRaw, newestVersionRaw)
|
||||
|
||||
if compareResult == 0 then
|
||||
print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)")
|
||||
elseif compareResult < 0 then
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
|
||||
for _, line in ipairs(changelog) do
|
||||
print((line:find("http") and "^7" or "^5")..line)
|
||||
function CheckBridgeVersion()
|
||||
if IsDuplicityVersion() then
|
||||
CreateThread(function()
|
||||
Wait(4000)
|
||||
local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version')
|
||||
--PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers)
|
||||
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers)
|
||||
if not body then
|
||||
print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)")
|
||||
return
|
||||
end
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
SetTimeout(1200000, function()
|
||||
CheckBridgeVersion()
|
||||
end)
|
||||
else
|
||||
print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)")
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in body:gmatch("[^\r\n]+") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
local newestVersionRaw = lines[1] or "0.0.0"
|
||||
local changelog = {}
|
||||
for i = 2, #lines do
|
||||
table.insert(changelog, lines[i])
|
||||
end
|
||||
|
||||
local compareResult = compareVersions(currentVersionRaw, newestVersionRaw)
|
||||
|
||||
if compareResult == 0 then
|
||||
print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)")
|
||||
elseif compareResult < 0 then
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
|
||||
for _, line in ipairs(changelog) do
|
||||
print((line:find("http") and "^7" or "^5")..line)
|
||||
end
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
SetTimeout(3600000, function()
|
||||
CheckBridgeVersion()
|
||||
end)
|
||||
else
|
||||
print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7) (^1Expect Errors^7)")
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
CheckBridgeVersion()
|
||||
CheckBridgeVersion()
|
||||
end
|
||||
32
clientFrameworkCache.lua
Normal file
32
clientFrameworkCache.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
if _G.__bridge_cache_loaded then return end
|
||||
_G.__bridge_cache_loaded = true
|
||||
|
||||
_G.BridgeCache = {
|
||||
hasCache = false,
|
||||
data = nil,
|
||||
queue = {},
|
||||
}
|
||||
|
||||
RegisterNetEvent("jim_bridge:receiveCache", function(data)
|
||||
if _G.BridgeCache.hasCache then return end
|
||||
_G.BridgeCache.data = data
|
||||
_G.BridgeCache.hasCache = true
|
||||
|
||||
for _, cb in ipairs(_G.BridgeCache.queue) do
|
||||
cb(data)
|
||||
end
|
||||
_G.BridgeCache.queue = nil
|
||||
end)
|
||||
|
||||
-- Only send the request ONCE per client session
|
||||
TriggerServerEvent("jim_bridge:requestCache")
|
||||
|
||||
-- Exported async function
|
||||
function GetBridgeCache(callback)
|
||||
if _G.BridgeCache.hasCache then
|
||||
callback(_G.BridgeCache.data)
|
||||
else
|
||||
table.insert(_G.BridgeCache.queue, callback)
|
||||
end
|
||||
end
|
||||
exports("GetBridgeCache", GetBridgeCache)
|
||||
@@ -21,12 +21,12 @@ This module provides access and control over player metadata, which is useful fo
|
||||
print("Player stress level:", stress)
|
||||
```
|
||||
|
||||
- **SetMetadata(player, key, value)**
|
||||
- **setPlayerMetadata(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
|
||||
SetMetadata(player, "stress", 0)
|
||||
setPlayerMetadata(player, "stress", 0)
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
### wrapperfunctions.lua
|
||||
|
||||
Provides wrapper compatibility functions for command and inventory stash systems across different frameworks (OX, QB, ESX, QS, etc).
|
||||
Provides wrapper compatibility functions for command and inventory stash systems across different frameworks (OX, QB, ESX, 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, QS, or Origen inventory systems.
|
||||
- Registers a stash using OX, or Origen inventory systems.
|
||||
- **Example:**
|
||||
```lua
|
||||
registerStash(
|
||||
|
||||
556
frameworkCache.lua
Normal file
556
frameworkCache.lua
Normal file
@@ -0,0 +1,556 @@
|
||||
--[[
|
||||
Cached Resource Initialization Module
|
||||
--------------------------------------
|
||||
Initializes shared resource data (Items, Vehicles, Jobs, Gangs) once and caches it
|
||||
to improve performance and reduce redundant data fetching across scripts.
|
||||
]]
|
||||
|
||||
-- Define resource names used in different frameworks
|
||||
local Exports = {
|
||||
QBExport = "qb-core",
|
||||
QBXExport = "qbx_core",
|
||||
ESXExport = "es_extended",
|
||||
OXCoreExport = "ox_core",
|
||||
|
||||
OXInv = "ox_inventory",
|
||||
QBInv = "qb-inventory",
|
||||
PSInv = "ps-inventory",
|
||||
CoreInv = "core_inventory",
|
||||
CodeMInv = "codem-inventory",
|
||||
OrigenInv = "origen_inventory",
|
||||
TgiannInv = "tgiann-inventory",
|
||||
JPRInv = "jpr-inventory",
|
||||
|
||||
OXLibExport = "ox_lib",
|
||||
|
||||
QBMenuExport = "qb-menu",
|
||||
|
||||
QBTargetExport = "qb-target",
|
||||
OXTargetExport = "ox_target",
|
||||
|
||||
-- REDM
|
||||
RSGExport = "rsg-core",
|
||||
RSGInv = "rsg-inventory",
|
||||
|
||||
VorpExport = "vorp_core",
|
||||
VorpInv = "vorp_inventory",
|
||||
VorpMenu = "vorp_menu",
|
||||
}
|
||||
|
||||
-- Prevent reloading if cache is already initialized
|
||||
local cache = { Items = {}, Vehicles = {}, Jobs = {}, Gangs = {}, }
|
||||
local cacheReady = false
|
||||
|
||||
-- Timer info, for debugging more then anything
|
||||
local timers = {}
|
||||
local function startTimer(label)
|
||||
timers[label] = GetGameTimer()
|
||||
end
|
||||
|
||||
local function endTimer(label)
|
||||
timers[label] = GetGameTimer() - (timers[label] or GetGameTimer())
|
||||
timers[label] = "("..(timers[label] / 1000).."s)"
|
||||
end
|
||||
startTimer("Cache") startTimer("Items") startTimer("Vehicles") startTimer("Jobs") startTimer("InvWeight") startTimer("InvSlots")
|
||||
|
||||
-- Helper functions --
|
||||
local function checkExists(resourceName)
|
||||
local state = GetResourceState(resourceName)
|
||||
return state and (state:find("start") or state: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
|
||||
|
||||
if checkExists(Exports.OXCoreExport) then
|
||||
-- Detected OX_Core in server, wait for it to be started if needed
|
||||
waitStartedOrStopped(Exports.OXCoreExport)
|
||||
local fileLoader = assert(load(LoadResourceFile(Exports.OXCoreExport, ('lib/init.lua')), ('@@'..Exports.OXCoreExport..'/lib/init.lua')))
|
||||
fileLoader()
|
||||
end
|
||||
if checkExists(Exports.ESXExport) then
|
||||
-- Detected ESX in server, wait for it to be started if needed
|
||||
waitStarted(Exports.ESXExport)
|
||||
local fileLoader = assert(load(LoadResourceFile(Exports.ESXExport, ('/imports.lua')), ('@@'..Exports.ESXExport..'/imports.lua')))
|
||||
fileLoader()
|
||||
end
|
||||
|
||||
-- Initialize variables for caching
|
||||
local itemResource, jobResource, vehResource = "N/A", "N/A", "N/A"
|
||||
|
||||
-- Print just to announce it knows the exports/scripts exist in the server
|
||||
for _, v in pairs(Exports) do
|
||||
if checkExists(v) then
|
||||
print("^6Bridge^7: '^3"..v.."^7' detected")
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
---- Load Items -----
|
||||
---------------------
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
--- Load Vehicles ---
|
||||
---------------------
|
||||
---
|
||||
local vehicleFunc = {
|
||||
|
||||
{ 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,
|
||||
},
|
||||
}
|
||||
|
||||
for i = 1, #vehicleFunc do
|
||||
local data = vehicleFunc[i]
|
||||
if checkExists(data.script) then
|
||||
waitStarted(data.script) -- Wait for detected script to start fully
|
||||
data.cacheVehicle() -- run tablized function for core
|
||||
vehResource = data.script -- Grab script name to announce later
|
||||
endTimer("Vehicles") -- end timer
|
||||
break -- break loop so it doesn't keep checking
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
----- Load Jobs -----
|
||||
---------------------
|
||||
|
||||
local jobFunc = {
|
||||
|
||||
{ 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
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Fallback if nil or empty
|
||||
if cache.Items == nil or not next(cache.Items) then
|
||||
print("^1--------------------------------------------^7")
|
||||
print("^1ERROR^7: ^1Can NOT find "..itemResource.." ^7Items ^1list^7, ^1possible error in that file or is it empty^7?")
|
||||
print("^1--------------------------------------------^7")
|
||||
cache.Items = {} -- Fallback to an empty table
|
||||
end
|
||||
if cache.Vehicles == nil or not next(cache.Vehicles) then
|
||||
print("^1--------------------------------------------^7")
|
||||
print("^1ERROR^7: ^1Can NOT find "..vehResource.." ^7Vehicles ^1list^7, ^1possible error in that file or is it empty^7?")
|
||||
print("^1--------------------------------------------^7")
|
||||
cache.Vehicles = {} -- Fallback to an empty table
|
||||
end
|
||||
if cache.Jobs == nil or not next(cache.Jobs) then
|
||||
print("^1--------------------------------------------^7")
|
||||
print("^1ERROR^7: ^1Can NOT find "..jobResource.." ^7job ^1list^7, ^1possible error in that file or is it empty^7?")
|
||||
print("^1--------------------------------------------^7")
|
||||
cache.Jobs = {} -- Fallback to an empty table
|
||||
end
|
||||
|
||||
-- Auto Detection of Inventory Weight -- **EXPERIMENTAL**
|
||||
-- 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
|
||||
local function getInventoryConfig(resource, data)
|
||||
if data.convars then
|
||||
return function(path)
|
||||
local key = path[1]
|
||||
local convar = key == "MaxWeight" and data.convars.weight or key == "MaxSlots" and data.convars.slots
|
||||
return convar and GetConvarInt(convar.key, convar.default) or nil, "Unsupported convar path: " .. table.concat(path, ".")
|
||||
end
|
||||
end
|
||||
|
||||
local content = LoadResourceFile(resource, data.file)
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
local cfg = env.Config or env.config
|
||||
if not cfg then return nil, "Config table not found" end
|
||||
|
||||
return function(path)
|
||||
local ref = cfg
|
||||
for _, k in ipairs(path) do
|
||||
if type(ref) ~= "table" then return nil, "Path invalid at: " .. tostring(k) end
|
||||
ref = ref[k]
|
||||
end
|
||||
return ref
|
||||
end
|
||||
end
|
||||
|
||||
-- Inventory table
|
||||
local invWeightTable = {
|
||||
[Exports.OXInv] = { convars = {
|
||||
weight = { key = "inventory:weight", default = 30000 },
|
||||
slots = { key = "inventory:slots", default = 40 }
|
||||
}},
|
||||
[Exports.QBInv] = {
|
||||
fallback = {
|
||||
{ file = "config.lua", path = { "MaxInventoryWeight" }, slotPath = { "MaxInventorySlots" } }, -- old version
|
||||
{ file = "config/config.lua", path = { "MaxWeight" }, slotPath = { "MaxSlots" } }, -- new version
|
||||
}
|
||||
},
|
||||
[Exports.JPRInv] = { file = "configs/main_config.lua", path = { "MaxInventoryWeight" }, slotPath = { "MaxInventorySlots" } },
|
||||
[Exports.PSInv] = { file = "config.lua", path = { "MaxInventoryWeight" }, slotPath = { "MaxInventorySlots" } },
|
||||
[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" } },
|
||||
--[Exports.OrigenInv] = { file = "config.lua", path = { "MaxWeight" } },
|
||||
}
|
||||
|
||||
-- Run config detection
|
||||
local invResource = ""
|
||||
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
|
||||
|
||||
for _, option in ipairs(attempts) do
|
||||
local try, e = getInventoryConfig(script, option)
|
||||
if try then
|
||||
lookup = try
|
||||
used = option
|
||||
break
|
||||
end
|
||||
err = e
|
||||
end
|
||||
|
||||
if not lookup then
|
||||
print(("^1ERROR^7: ^1Config loader failed from ^5%s^7: ^1%s^7"):format(script, err or "unknown"))
|
||||
break
|
||||
end
|
||||
|
||||
local function resolve(label, path)
|
||||
local val, perr = lookup(path)
|
||||
if val then
|
||||
cache["Inventory"..label] = val
|
||||
return
|
||||
end
|
||||
local warnType = label == "Weight" and "^1ERROR" or "^3WARNING"
|
||||
print(("%s^7: ^1Failed to get ^7Inventory%s ^1from ^5%s^7: ^1%s^7"):format(warnType, label, script, perr or "unknown"))
|
||||
end
|
||||
|
||||
resolve("Weight", used.path or { "MaxWeight" })
|
||||
resolve("Slots", used.slotPath or { "MaxSlots" })
|
||||
|
||||
invResource = script:gsub("-", "^7-^4"):gsub("_", "^7_^4")
|
||||
break
|
||||
end
|
||||
::skip::
|
||||
end
|
||||
endTimer("InvWeight")
|
||||
endTimer("InvSlots")
|
||||
|
||||
CreateThread(function()
|
||||
local counts = {
|
||||
Items = 0, Vehicles = 0, Jobs = 0, Gangs = 0,
|
||||
}
|
||||
for k, v in pairs(cache) do
|
||||
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"])
|
||||
end
|
||||
if cache.InventorySlots then
|
||||
print("^6FrameWorkCache^7: ^4"..invResource.."^2 InventorySlots^7: ^3"..cache.InventorySlots.." ^7"..timers["InvSlots"])
|
||||
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"])
|
||||
endTimer("Cache")
|
||||
print("^6FrameworkCache^7: ^2Cache Ready ^7"..timers["Cache"])
|
||||
cacheReady = true
|
||||
end)
|
||||
|
||||
RegisterNetEvent("jim_bridge:requestCache", function()
|
||||
local src = source
|
||||
TriggerClientEvent("jim_bridge:receiveCache", src, cache)
|
||||
end)
|
||||
|
||||
exports("GetSharedData", function()
|
||||
-- Wait indefinitely (with periodic checks) until cacheReady is true
|
||||
local timeout = GetGameTimer() + 20000 -- 20 second failsafe timeout
|
||||
while not cacheReady and GetGameTimer() < timeout do
|
||||
Wait(100)
|
||||
end
|
||||
|
||||
if not cacheReady then
|
||||
print("^1ERROR^7: jim_bridge cache timed out waiting for data.")
|
||||
return nil -- Signal clearly if cache never became ready
|
||||
end
|
||||
|
||||
return cache
|
||||
end)
|
||||
@@ -1,13 +1,12 @@
|
||||
name "Jim_Bridge"
|
||||
author "Jimathy"
|
||||
version "2.0.14"
|
||||
version "2.1.08"
|
||||
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',
|
||||
@@ -16,4 +15,14 @@ files {
|
||||
}
|
||||
|
||||
-- Version checker
|
||||
server_scripts { '_versioncheck.lua' }
|
||||
server_scripts {
|
||||
'frameworkCache.lua',
|
||||
'_versioncheck.lua',
|
||||
}
|
||||
|
||||
client_scripts {
|
||||
'clientFrameworkCache.lua',
|
||||
'ui_modules/*.lua',
|
||||
}
|
||||
|
||||
suppress_updates 'false' -- set to 'true' to disable update pings
|
||||
157
shared/_authToken.lua
Normal file
157
shared/_authToken.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-------------------------------------------------------------
|
||||
-- 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("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
|
||||
return ""
|
||||
end
|
||||
debugPrint("^1Auth^7:^2 Player Source^7: "..src.." ^2requested new token^7:", token)
|
||||
validTokens[src] = token
|
||||
timeOutAuth(validTokens[src], src) -- Give script 10 seconds, then clear token
|
||||
return token
|
||||
end)
|
||||
|
||||
function timeOutAuth(token, src)
|
||||
local token = token
|
||||
SetTimeout(10000, function()
|
||||
if token == validTokens[src] then
|
||||
print("^1--------------------------------------------^7")
|
||||
print("^7Clearing token for player ^1"..src.."^7", token)
|
||||
print("^7This shouldn't happen unless a token has been called by a player or script and it hasn't been used")
|
||||
print("^1--------------------------------------------^7")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
RegisterNetEvent(getScript()..":clearAuthToken", function()
|
||||
local src = source
|
||||
debugPrint("^1Auth^7: ^2Manually removing token for Player Source^7:", src, validTokens[src])
|
||||
validTokens[src] = nil
|
||||
end)
|
||||
|
||||
receivedEvent = {}
|
||||
authCooldown = {}
|
||||
|
||||
createCallback(getScript()..":callback:GetAuthEvent", function(source)
|
||||
local src = source
|
||||
local invokingRes = GetInvokingResource()
|
||||
|
||||
if invokingRes and invokingRes ~= getScript() and not excludeRes[invokingRes] 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
|
||||
end
|
||||
|
||||
debugPrint("^1Auth^7: ^2Player Source^7: "..src.." ^2requested ^3AuthEvent^7", AuthEvent)
|
||||
|
||||
authCooldown[src] = true
|
||||
SetTimeout(60000, function() -- 1 minute cooldown
|
||||
authCooldown[src] = nil
|
||||
end)
|
||||
|
||||
if not receivedEvent[src] then
|
||||
receivedEvent[src] = true
|
||||
return AuthEvent
|
||||
else
|
||||
print("^1Auth^7: ^1Player ^7"..src.." ^1tried to request auth token more than once^7")
|
||||
return ""
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNetEvent(getScript()..":clearAuthEventRequest", function()
|
||||
local src = source
|
||||
debugPrint("^1Auth^7: ^2Manually clearing Auth Event for Player Source^7:", src, AuthEvent)
|
||||
receivedEvent[src] = nil
|
||||
end)
|
||||
|
||||
-- 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
|
||||
dupeWarn(src, name, "^1Auth Error^7: ^3"..src.." ^1create a stash ^7"..name.." ^1without an auth token^7")
|
||||
elseif genType == "item" then
|
||||
dupeWarn(src, name, "^1Auth Error^7: ^3"..src.." ^1attempted to spawn an item ^7"..name.." ^1without an auth token^7")
|
||||
end
|
||||
return false
|
||||
else
|
||||
debugPrint("^1Auth^7: ^2Auth token received^7, ^2checking against server cache^7..")
|
||||
if token ~= validTokens[src] then
|
||||
debugPrint("^1Auth^7: ^1Tokens don't match! ^7", token, validTokens[src])
|
||||
if genType == "stash" then
|
||||
dupeWarn(src, name, "^1Auth Error^7: ^3"..src.." ^1create a stash ^7"..name.." ^1with an incorrect auth token^7")
|
||||
elseif genType == "item" then
|
||||
dupeWarn(src, name, "^1Auth Error^7: ^3"..src.." ^1attempted to spawn an item ^7"..name.." ^1with an incorrect auth token^7")
|
||||
end
|
||||
return false
|
||||
else
|
||||
debugPrint("^1Auth^7: ^2Client and Server Auth tokens match^7!", token, validTokens[src])
|
||||
validTokens[src] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
onPlayerLoaded(function()
|
||||
debugPrint("^1Auth^7: ^2Requesting ^3Auth Event^7")
|
||||
AuthEvent = triggerCallback(getScript()..":callback:GetAuthEvent")
|
||||
end, true)
|
||||
|
||||
onPlayerUnload(function()
|
||||
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 ped = src and GetPlayerPed(src) or PlayerPedId()
|
||||
local srcCoords = GetEntityCoords(ped)
|
||||
local allow = false
|
||||
|
||||
for i = 1, #table do
|
||||
if #(table[i].xy - srcCoords.xy) <= 10 then
|
||||
return true
|
||||
else
|
||||
allow = false
|
||||
end
|
||||
end
|
||||
|
||||
if not allow then
|
||||
print(src and ("^1Src ^3"..src.." ") or "", "^1Tried to open a registered shop/stash from over the distance limit^7")
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
@@ -7,6 +7,152 @@
|
||||
• 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
|
||||
-------------------------------------------------------------
|
||||
@@ -24,53 +170,28 @@
|
||||
--- end, true)
|
||||
--- ```
|
||||
function onPlayerLoaded(func, onStart)
|
||||
local onPlayerFramework = ""
|
||||
local loaded = false
|
||||
|
||||
if onStart then
|
||||
onResourceStart(function()
|
||||
if not waitForLogin() then return end
|
||||
|
||||
loaded = true
|
||||
debugPrint("^6Bridge^7: ^3onResourceStart^7()^2 executed through ^3onPlayerLoaded^7()")
|
||||
Wait(2000)
|
||||
func()
|
||||
debugPrint("^6Bridge^7: ^3onResourceStart^7()^2 routed through ^3onPlayerLoaded^7()")
|
||||
_debouncedRun(func)
|
||||
end, true)
|
||||
end
|
||||
|
||||
if not loaded then
|
||||
local tempFunc = function()
|
||||
Wait(2000)
|
||||
debugPrint("^6Bridge^7: ^2Executing onPlayerLoaded")
|
||||
func()
|
||||
end
|
||||
local handler = function()
|
||||
_debouncedRun(func)
|
||||
end
|
||||
|
||||
if isStarted(QBExport) or isStarted(QBXExport) then
|
||||
onPlayerFramework = QBExport
|
||||
AddEventHandler('QBCore:Client:OnPlayerLoaded', tempFunc)
|
||||
elseif isStarted(ESXExport) then
|
||||
onPlayerFramework = ESXExport
|
||||
AddEventHandler('esx:playerLoaded', function()
|
||||
if waitForSharedLoad() then
|
||||
if isStarted(ESXExport) then Wait(11000) end
|
||||
tempFunc()
|
||||
end
|
||||
end
|
||||
)
|
||||
elseif isStarted(OXCoreExport) then
|
||||
onPlayerFramework = OXCoreExport
|
||||
AddEventHandler('ox:playerLoaded', tempFunc)
|
||||
elseif isStarted(RSGExport) then
|
||||
onPlayerFramework = RSGExport
|
||||
AddEventHandler('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")
|
||||
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
|
||||
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.
|
||||
@@ -83,12 +204,15 @@ end
|
||||
--- end)
|
||||
--- ```
|
||||
function onPlayerUnload(func)
|
||||
AddEventHandler('QBCore:Client:OnPlayerUnload', function() func() end)
|
||||
AddEventHandler('ox:playerLogout', function() func() end)
|
||||
AddEventHandler('RSGCore:Client:OnPlayerUnload', function() func() end)
|
||||
|
||||
--AddEventHandler('esx:playerLogout', function() func() end)
|
||||
-- ^ Only server side for now, need a way to send it to client if not already available
|
||||
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")
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -105,14 +229,17 @@ end
|
||||
--- -- Initialization code on resource start.
|
||||
--- end, true)
|
||||
--- ```
|
||||
local hasPrinted = false
|
||||
function onResourceStart(func, thisScript)
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3onResourceStart^7()")
|
||||
AddEventHandler('onResourceStart', function(resourceName)
|
||||
if getScript() == resourceName and (thisScript or true) then
|
||||
if waitForSharedLoad() then
|
||||
debugPrint("^6Bridge^7: ^2Shared Load Detected^7.")
|
||||
if isStarted(ESXExport) then Wait(10000) end
|
||||
func()
|
||||
if not hasPrinted then
|
||||
debugPrint("^6Bridge^7: ^2Shared Load Detected^7.")
|
||||
hasPrinted = true
|
||||
end
|
||||
func(resourceName)
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -131,7 +258,7 @@ function onResourceStop(func, thisScript)
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3onResourceStop^7()")
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if getScript() == resourceName and (thisScript or true) then
|
||||
func()
|
||||
func(resourceName)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -144,66 +271,52 @@ end
|
||||
--- @usage
|
||||
--- waitForLogin()
|
||||
function waitForLogin()
|
||||
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
|
||||
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.")
|
||||
end
|
||||
Wait(100)
|
||||
return result
|
||||
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 = 100000 -- 10 seconds in milliseconds
|
||||
local startTime = GetGameTimer()
|
||||
local loaded = true
|
||||
while ((not Jobs or not next(Jobs)) and (not Items or not next(Items)) and (not Vehicles or not next(Vehicles))) and (GetGameTimer() - startTime) < timeout do
|
||||
print((GetGameTimer() - startTime) < timeout)
|
||||
Wait(1000)
|
||||
debugPrint("Waiting for Jobs, Items, and Vehicles to be loaded")
|
||||
if Jobs and Items and Vehicles then
|
||||
print("^6Bridge^7: ^2Jobs, Items, and Vehicles Loaded^7.")
|
||||
loaded = true
|
||||
break
|
||||
local timeout = GetGameTimer() + 900000 -- 15 minutes max wait (had to up this from 2 minutes because of slow servers)
|
||||
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
|
||||
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")
|
||||
end
|
||||
if (not Items or not next(Items)) then
|
||||
print("^4Debug^7: ^2Waiting for ^7Items^2 to be loaded")
|
||||
end
|
||||
if (not Vehicles or not next(Vehicles)) then
|
||||
print("^4Debug^7: ^2Waiting for ^7Vehicles^2 to be loaded")
|
||||
end
|
||||
messageShown = true
|
||||
end
|
||||
Wait(1000)
|
||||
|
||||
loop += 1
|
||||
end
|
||||
if not loaded then
|
||||
print("^4Error^7: ^2Timeout reached while waiting for shared load^7.")
|
||||
return false
|
||||
else
|
||||
|
||||
if Jobs and Items and Vehicles then
|
||||
debugPrint("^6Bridge^7: ^2Jobs, Items, and Vehicles Loaded^7.")
|
||||
return true
|
||||
else
|
||||
print("^4Error^7: ^1Timeout reached while waiting for shared load^7.")
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -1,100 +1,111 @@
|
||||
function parseVersion(version)
|
||||
local parts = {}
|
||||
for num in version:gmatch("%d+") do
|
||||
table.insert(parts, tonumber(num))
|
||||
local function readBoolMeta(key, default)
|
||||
local val = GetResourceMetadata("jim_bridge", key, 0)
|
||||
if not val then return default end
|
||||
val = tostring(val):lower()
|
||||
return (val == 'true' or val == '1' or val == 'yes' or val == 'on')
|
||||
end
|
||||
|
||||
local SUPPRESS_UPDATES = readBoolMeta('suppress_updates', false)
|
||||
|
||||
if not SUPPRESS_UPDATES then
|
||||
function parseVersion(version)
|
||||
local parts = {}
|
||||
for num in version:gmatch("%d+") do
|
||||
table.insert(parts, tonumber(num))
|
||||
end
|
||||
return parts
|
||||
end
|
||||
return parts
|
||||
end
|
||||
|
||||
function compareVersions(current, newest)
|
||||
local currentParts = parseVersion(current)
|
||||
local newestParts = parseVersion(newest)
|
||||
for i = 1, math.max(#currentParts, #newestParts) do
|
||||
local c = currentParts[i] or 0
|
||||
local n = newestParts[i] or 0
|
||||
if c < n then return -1
|
||||
elseif c > n then return 1 end
|
||||
function compareVersions(current, newest)
|
||||
local currentParts = parseVersion(current)
|
||||
local newestParts = parseVersion(newest)
|
||||
for i = 1, math.max(#currentParts, #newestParts) do
|
||||
local c = currentParts[i] or 0
|
||||
local n = newestParts[i] or 0
|
||||
if c < n then return -1
|
||||
elseif c > n then return 1 end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function capitalize(str)
|
||||
return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end))
|
||||
end
|
||||
function capitalize(str)
|
||||
return (str:gsub("^%l", string.upper):gsub("%-%l", function(letter) return "-" .. string.upper(letter:sub(2)) end))
|
||||
end
|
||||
|
||||
local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
|
||||
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
|
||||
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
|
||||
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or ""
|
||||
local scriptName = ("^2"..capitalize(getScript()):gsub("%-", "^7-^2"):gsub("%_", "^7_^2")) or ""
|
||||
local scriptVersion = ("^5"..GetResourceMetadata(getScript(), 'version', nil):gsub("%.", "^7.^5")) or ""
|
||||
local scriptDescription = ("^2"..GetResourceMetadata(getScript(), 'description', nil)) or ""
|
||||
local scriptAuthor = ("^2by ^4"..GetResourceMetadata(getScript(), 'author', nil):gsub("%and", "^7and^4")) or ""
|
||||
|
||||
print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7")
|
||||
print(scriptName.." ^7v"..scriptVersion.."^7 - "..scriptDescription.." "..scriptAuthor.."^7")
|
||||
|
||||
function CheckVersion()
|
||||
if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
|
||||
CreateThread(function()
|
||||
Wait(4000)
|
||||
local script = getScript()
|
||||
local currentVersionRaw = GetResourceMetadata(script, 'version')
|
||||
function CheckVersion()
|
||||
if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
|
||||
CreateThread(function()
|
||||
Wait(4000)
|
||||
local script = getScript()
|
||||
local currentVersionRaw = GetResourceMetadata(script, 'version')
|
||||
|
||||
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers)
|
||||
if not newestVersionRaw then
|
||||
-- fallback
|
||||
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers)
|
||||
if not fallbackVersionRaw then
|
||||
print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)")
|
||||
return
|
||||
end
|
||||
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers)
|
||||
if not newestVersionRaw then
|
||||
-- fallback
|
||||
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..script..'/master/version.txt', function(err, fallbackVersionRaw, headers)
|
||||
if not fallbackVersionRaw then
|
||||
print("^1Currently unable to run a version check for ^7'^3"..script.."^7' (^3"..currentVersionRaw.."^7)")
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
|
||||
local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "")
|
||||
local changelog = {}
|
||||
for i = 2, #lines do table.insert(changelog, lines[i]) end
|
||||
|
||||
local compareResult = compareVersions(currentVersionRaw, fallbackVersion)
|
||||
if compareResult == 0 then
|
||||
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
|
||||
elseif compareResult < 0 then
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)")
|
||||
if #changelog > 0 then
|
||||
for _, line in ipairs(changelog) do
|
||||
print((line:find("http") and "^7" or "^5")..line)
|
||||
end
|
||||
end
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
SetTimeout(1200000, function() CheckVersion() end)
|
||||
else
|
||||
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
|
||||
end
|
||||
end)
|
||||
else
|
||||
local lines = {}
|
||||
for line in fallbackVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
|
||||
local fallbackVersion = (lines[1] or "0.0.0"):gsub("v", "")
|
||||
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
|
||||
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
|
||||
local changelog = {}
|
||||
for i = 2, #lines do table.insert(changelog, lines[i]) end
|
||||
|
||||
local compareResult = compareVersions(currentVersionRaw, fallbackVersion)
|
||||
local compareResult = compareVersions(currentVersionRaw, newestVersion)
|
||||
if compareResult == 0 then
|
||||
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
|
||||
elseif compareResult < 0 then
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)")
|
||||
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)")
|
||||
if #changelog > 0 then
|
||||
for _, line in ipairs(changelog) do
|
||||
print((line:find("http") and "^7" or "^5")..line)
|
||||
end
|
||||
end
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
SetTimeout(1200000, function() CheckVersion() end)
|
||||
SetTimeout(3600000, function() CheckVersion() end)
|
||||
else
|
||||
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
|
||||
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
|
||||
end
|
||||
end)
|
||||
else
|
||||
local lines = {}
|
||||
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
|
||||
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
|
||||
local changelog = {}
|
||||
for i = 2, #lines do table.insert(changelog, lines[i]) end
|
||||
|
||||
local compareResult = compareVersions(currentVersionRaw, newestVersion)
|
||||
if compareResult == 0 then
|
||||
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
|
||||
elseif compareResult < 0 then
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersion.."^7)")
|
||||
if #changelog > 0 then
|
||||
for _, line in ipairs(changelog) do
|
||||
print((line:find("http") and "^7" or "^5")..line)
|
||||
end
|
||||
end
|
||||
print("^1----------------------------------------------------------------------^7")
|
||||
SetTimeout(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
|
||||
end
|
||||
end
|
||||
|
||||
CheckVersion()
|
||||
CheckVersion()
|
||||
end
|
||||
@@ -1,3 +1,30 @@
|
||||
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.
|
||||
@@ -24,8 +51,8 @@
|
||||
--- ```
|
||||
function createCallback(callbackName, funct)
|
||||
if isServer() then
|
||||
debugPrint("^6Bridge^7: ^3Registering callback^7:", callbackName)
|
||||
if isStarted(OXLibExport) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^4"..OXLibExport.." ^3Callback^7:", callbackName)
|
||||
lib.callback.register(callbackName, funct)
|
||||
else
|
||||
local adaptedFunction = function(source, cb, ...)
|
||||
@@ -34,12 +61,18 @@ function createCallback(callbackName, funct)
|
||||
end
|
||||
|
||||
if isStarted(QBExport) then
|
||||
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)
|
||||
else
|
||||
print("^6Bridge^7: ^1ERROR^7: ^3Can't find any script to register callback with", callbackName)
|
||||
print("^1ERROR^7: ^1Can't find any supported framework to register callback with^7: "..callbackName)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -53,6 +86,7 @@ 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
|
||||
@@ -63,25 +97,54 @@ end
|
||||
--- print(result)
|
||||
--- ```
|
||||
function triggerCallback(callbackName, ...)
|
||||
local result = nil
|
||||
debugPrint("^6Bridge^7: ^3Triggering callback^7:", callbackName)
|
||||
debugPrint("^6Bridge^7: ^2Triggering ^3Callback^7:", callbackName)
|
||||
local args = {...}
|
||||
|
||||
if isStarted(OXLibExport) then
|
||||
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)
|
||||
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)
|
||||
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
|
||||
return result
|
||||
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
|
||||
|
||||
Wait(10)
|
||||
until attempts > (1 + CALLBACK_RETRIES)
|
||||
|
||||
return nil, lastErr or "timeout"
|
||||
end
|
||||
|
||||
@@ -10,6 +10,263 @@
|
||||
• esx (using ESX.UI.Menu)
|
||||
]]
|
||||
|
||||
local contextFunc = {
|
||||
["ox"] =
|
||||
function(Menu, data)
|
||||
local index = nil
|
||||
if data.onBack and not data.onSelected then
|
||||
table.insert(Menu, 1, {
|
||||
icon = "fas fa-circle-arrow-left",
|
||||
title = "Return",
|
||||
onSelect = data.onBack,
|
||||
label = "Return",
|
||||
})
|
||||
end
|
||||
for k in pairs(Menu) do
|
||||
if data.onSelected and Menu[k].arrow then
|
||||
Menu[k].icon = "fas fa-angle-right"
|
||||
end
|
||||
-- If no title, use header or txt as title/label.
|
||||
if not Menu[k].title then
|
||||
if Menu[k].header ~= nil and Menu[k].header ~= "" then
|
||||
Menu[k].title = Menu[k].header
|
||||
Menu[k].label = Menu[k].header
|
||||
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
|
||||
else
|
||||
Menu[k].title = Menu[k].txt
|
||||
Menu[k].label = Menu[k].txt
|
||||
end
|
||||
end
|
||||
-- Copy parameters from 'params' if available.
|
||||
if Menu[k].params then
|
||||
Menu[k].event = Menu[k].params.event
|
||||
Menu[k].args = Menu[k].params.args or {}
|
||||
end
|
||||
if Menu[k].isMenuHeader then
|
||||
Menu[k].readOnly = true
|
||||
end
|
||||
end
|
||||
local menuID = 'Menu'
|
||||
(data.onSelected and lib.registerMenu or lib.registerContext)({
|
||||
id = menuID,
|
||||
title = data.header..br..br..(data.headertxt and data.headertxt or ""),
|
||||
position = 'top-right',
|
||||
options = Menu,
|
||||
canClose = data.canClose and data.canClose or nil,
|
||||
onClose = (data.onBack and data.onBack) or (data.onExit and data.onExit) or nil,
|
||||
onExit = data.onExit and data.onExit or nil,
|
||||
onSelected = data.onSelected and (function(selected) index = selected end) or nil,
|
||||
}, data.onSelected and (function(x, y, args)
|
||||
if Menu[x].refresh then
|
||||
if Menu[x].onSelect then
|
||||
Menu[x].onSelect()
|
||||
end
|
||||
lib.showMenu(menuID, index)
|
||||
else
|
||||
if Menu[x].onSelect then
|
||||
Menu[x].onSelect()
|
||||
else
|
||||
lib.showMenu(menuID, index)
|
||||
end
|
||||
end
|
||||
end) or nil)
|
||||
if data.onSelected then
|
||||
lib.showMenu(menuID, 1)
|
||||
else
|
||||
lib.showContext(menuID)
|
||||
end
|
||||
end,
|
||||
|
||||
["qb"] =
|
||||
function(Menu, data)
|
||||
if data.onBack then
|
||||
table.insert(Menu, 1, {
|
||||
icon = "fas fa-circle-arrow-left",
|
||||
header = " ",
|
||||
txt = "Return",
|
||||
params = {
|
||||
isAction = true,
|
||||
event = data.onBack,
|
||||
},
|
||||
})
|
||||
elseif data.canClose then
|
||||
table.insert(Menu, 1, {
|
||||
icon = "fas fa-circle-xmark",
|
||||
header = " ",
|
||||
txt = "Close",
|
||||
params = {
|
||||
isAction = true,
|
||||
event = data.onExit and data.onExit or (function() exports[QBMenuExport]:closeMenu() end),
|
||||
},
|
||||
})
|
||||
end
|
||||
if data.header ~= nil then
|
||||
local tempMenu = {}
|
||||
for k, v in pairs(Menu) do tempMenu[k + 1] = v end
|
||||
tempMenu[1] = { header = data.header, txt = data.headertxt or "", isMenuHeader = true }
|
||||
Menu = tempMenu
|
||||
end
|
||||
for k in pairs(Menu) do
|
||||
if not Menu[k].params or not Menu[k].params.event then
|
||||
Menu[k].params = {
|
||||
isAction = true,
|
||||
event = Menu[k].onSelect or function() end,
|
||||
}
|
||||
end
|
||||
if not Menu[k].header then Menu[k].header = " " end
|
||||
if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end
|
||||
Menu[k].isMenuHeader = Menu[k].isMenuHeader or Menu[k].disable
|
||||
end
|
||||
exports[QBMenuExport]:openMenu(Menu)
|
||||
end,
|
||||
|
||||
["gta"] =
|
||||
function(Menu, data)
|
||||
WarMenu.CreateMenu(tostring(Menu), data.header, " ", {
|
||||
titleColor = { 222, 255, 255 },
|
||||
maxOptionCountOnScreen = 15,
|
||||
width = 0.25,
|
||||
x = 0.7,
|
||||
})
|
||||
if WarMenu.IsAnyMenuOpened() then return end
|
||||
WarMenu.OpenMenu(tostring(Menu))
|
||||
CreateThread(function()
|
||||
local close = true
|
||||
while true do
|
||||
if WarMenu.Begin(tostring(Menu)) then
|
||||
if data.onBack then
|
||||
if WarMenu.SpriteButton("Return", 'commonmenu', "arrowleft", 127, 127, 127) then
|
||||
WarMenu.CloseMenu()
|
||||
Wait(10)
|
||||
data.onBack()
|
||||
end
|
||||
end
|
||||
for k in pairs(Menu) do
|
||||
local pressed = WarMenu.Button(Menu[k].header)
|
||||
if not Menu[k].header then
|
||||
Menu[k].header = Menu[k].txt
|
||||
Menu[k].txt = nil
|
||||
end
|
||||
if Menu[k].txt and Menu[k].txt ~= "" and WarMenu.IsItemHovered() then
|
||||
if Menu[k].disabled or Menu[k].isMenuHeader then
|
||||
WarMenu.ToolTip("~r~"..Menu[k].txt, 0.18, true)
|
||||
else
|
||||
WarMenu.ToolTip(
|
||||
(Menu[k].blip and "~BLIP_".."8".."~ " or "")..
|
||||
Menu[k].txt:gsub("%:", ":~g~"):gsub("%\n", "\n~s~"), 0.18,
|
||||
true)
|
||||
end
|
||||
end
|
||||
if pressed and not Menu[k].isMenuHeader then
|
||||
WarMenu.CloseMenu()
|
||||
close = false
|
||||
Wait(10)
|
||||
Menu[k].onSelect()
|
||||
end
|
||||
end
|
||||
WarMenu.End()
|
||||
else
|
||||
return
|
||||
end
|
||||
if not WarMenu.IsAnyMenuOpened() and close then
|
||||
stopTempCam(cam)
|
||||
if data.onExit then data.onExit() end
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
|
||||
["esx"] =
|
||||
function(Menu, data)
|
||||
for k in pairs(Menu) do
|
||||
Menu[k].label = Menu[k].header
|
||||
Menu[k].name = "button"..k
|
||||
end
|
||||
if data.canClose then
|
||||
table.insert(Menu, 1, {
|
||||
icon = "fas fa-circle-xmark",
|
||||
label = "Close",
|
||||
name = "close",
|
||||
onSelect = data.onExit,
|
||||
})
|
||||
end
|
||||
if data.onBack then
|
||||
table.insert(Menu, 1, {
|
||||
icon = "fas fa-circle-arrow-left",
|
||||
label = "Return",
|
||||
name = "return",
|
||||
onSelect = data.onBack,
|
||||
})
|
||||
end
|
||||
ESX.UI.Menu.Open("default", getScript(), "Example_Menu", {
|
||||
title = data.header,
|
||||
align = 'top-right',
|
||||
elements = Menu,
|
||||
},
|
||||
function(menuData, menu)
|
||||
for k in pairs(Menu) do
|
||||
if menuData.current.name == Menu[k].name then
|
||||
menu.close()
|
||||
Wait(10)
|
||||
Menu[k].onSelect()
|
||||
end
|
||||
end
|
||||
end,
|
||||
function(data, menu)
|
||||
menu.close()
|
||||
end)
|
||||
end,
|
||||
|
||||
["lation"] =
|
||||
function(Menu, data)
|
||||
if data.onBack then
|
||||
table.insert(Menu, 1, {
|
||||
icon = "fas fa-circle-arrow-left",
|
||||
onSelect = data.onBack,
|
||||
header = "Return",
|
||||
})
|
||||
end
|
||||
|
||||
for k in pairs(Menu) do
|
||||
if data.onSelected and Menu[k].arrow then
|
||||
Menu[k].icon = "fas fa-angle-right"
|
||||
end
|
||||
-- If no title, use header or txt as title/label.
|
||||
if not Menu[k].title then
|
||||
if Menu[k].header ~= nil and Menu[k].header ~= "" then
|
||||
Menu[k].title = Menu[k].header
|
||||
Menu[k].label = Menu[k].header
|
||||
if Menu[k].txt then Menu[k].description = Menu[k].txt else Menu[k].description = "" end
|
||||
else
|
||||
Menu[k].title = Menu[k].txt
|
||||
Menu[k].label = Menu[k].txt
|
||||
end
|
||||
end
|
||||
-- Copy parameters from 'params' if available.
|
||||
if Menu[k].params then
|
||||
Menu[k].event = Menu[k].params.event
|
||||
Menu[k].args = Menu[k].params.args or {}
|
||||
end
|
||||
if Menu[k].isMenuHeader then
|
||||
Menu[k].readOnly = true
|
||||
end
|
||||
end
|
||||
|
||||
exports.lation_ui:registerMenu({
|
||||
id = 'menu',
|
||||
title = data.header,
|
||||
onExit = data.onExit and data.onExit or nil,
|
||||
subtitle = (data.headertxt and data.headertxt or ""),
|
||||
options = Menu,
|
||||
})
|
||||
|
||||
-- Show menu
|
||||
exports.lation_ui:showMenu('menu')
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
--- Opens a menu using the configured menu system.
|
||||
---
|
||||
--- This function translates code and creates menus in several different menu scripts, depending on the configured menu system specified in `Config.System.Menu`.
|
||||
@@ -47,249 +304,22 @@
|
||||
--- })
|
||||
--- ```
|
||||
function openMenu(Menu, data)
|
||||
if Config.System.Menu == "jim" 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
|
||||
TriggerEvent("jim_bridge:client:openMenu", Menu)
|
||||
|
||||
elseif 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, data.headertxt or " ", {
|
||||
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
|
||||
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)
|
||||
end
|
||||
contextFunc[Config.System.Menu](Menu, data)
|
||||
end
|
||||
|
||||
-- Line break conversion table, basically, to automate the linebreak in functions `br`
|
||||
-- This is declared and automatically sets it for what menu you are using (depends if one is uding MD formatting or HTML formatting)
|
||||
local lineBreakConversion = {
|
||||
["ox"] = "\n",
|
||||
["gta"] = "\n",
|
||||
["lation"] = "\n",
|
||||
|
||||
["qb"] = "<br>",
|
||||
["esx"] = "<br>",
|
||||
}
|
||||
|
||||
--- A line break constant used for menu header formatting.
|
||||
br = (Config.System.Menu == "ox" or Config.System.Menu == "gta") and "\n" or "<br>"
|
||||
br = lineBreakConversion[Config.System.Menu]
|
||||
|
||||
--- Checks if the current menu system is 'ox' or 'gta' for formatting purposes.
|
||||
--- @return boolean boolean True if using ox or gta menus, otherwise false.
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
--[[
|
||||
Resource Initialization Module
|
||||
--------------------------------
|
||||
This module initializes and loads shared data (Items, Vehicles, Jobs, Gangs) from the
|
||||
various frameworks/inventory systems (OX, QB, ESX, etc.). It also corrects export names,
|
||||
caches framework exports into simple variables, and prints debug information if enabled.
|
||||
]]
|
||||
Items, Vehicles, Jobs, Gangs = nil, nil, nil, nil
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Global Variable Initialization
|
||||
-------------------------------------------------------------
|
||||
Items, Vehicles, Jobs, Gangs, Core = {}, nil, nil, nil, nil
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Correct QB Inventory Export
|
||||
-------------------------------------------------------------
|
||||
-- Correct ps-invetory to ls-inventory if somehow you still have that
|
||||
-- Shared Exports Initialization
|
||||
Exports.PSInv = isStarted("lj-inventory") and "lj-inventory" or Exports.PSInv
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Framework Exports and Inventory Identifiers
|
||||
-------------------------------------------------------------
|
||||
OXLibExport, QBXExport, QBExport, ESXExport, OXCoreExport =
|
||||
Exports.OXLibExport or "",
|
||||
Exports.QBXExport or "",
|
||||
@@ -27,320 +10,99 @@ OXLibExport, QBXExport, QBExport, ESXExport, OXCoreExport =
|
||||
Exports.ESXExport or "",
|
||||
Exports.OXCoreExport or ""
|
||||
|
||||
OXInv, QBInv, PSInv, QSInv, CoreInv, CodeMInv, OrigenInv, TgiannInv =
|
||||
OXInv, QBInv, PSInv, 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 "",
|
||||
Exports.TgiannInv or ""
|
||||
Exports.TgiannInv or "",
|
||||
Exports.JPRInv or ""
|
||||
|
||||
RSGExport, RSGInv = Exports.RSGExport or "", Exports.RSGInv or ""
|
||||
VorpExport, VorpInv = Exports.VorpExport or "", Exports.VorpInv or ""
|
||||
|
||||
RSGExport, RSGInv =
|
||||
Exports.RSGExport or "",
|
||||
Exports.RSGInv or ""
|
||||
|
||||
QBMenuExport = Exports.QBMenuExport or ""
|
||||
QBTargetExport, OXTargetExport = Exports.QBTargetExport or "", Exports.OXTargetExport or ""
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Debug: Print Found Exports
|
||||
-------------------------------------------------------------
|
||||
-- Print a list of all exports that are currently started (if debugMode is enabled).
|
||||
for _, v in pairs(Exports) do
|
||||
if isStarted(v) then
|
||||
debugPrint("^6Bridge^7: '^3"..v.."^7' export found")
|
||||
end
|
||||
end
|
||||
|
||||
OxPlayer = nil
|
||||
if isStarted(OXCoreExport) then
|
||||
if not isServer() then
|
||||
OxPlayer = Ox.GetPlayer()
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Resource Variables for Items, Jobs, and Vehicles
|
||||
-------------------------------------------------------------
|
||||
local itemResource, jobResource, vehResource = "", "", ""
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Loading Items
|
||||
-------------------------------------------------------------
|
||||
-- Load and compile shared items from the detected inventory system.
|
||||
if isStarted(OXInv) then
|
||||
itemResource = OXInv
|
||||
Items = exports[OXInv]:Items()
|
||||
|
||||
-- Add weapons to Items from QBXCore if available
|
||||
if isStarted(QBXExport) then
|
||||
local tempWeapons = exports[QBExport]:GetCoreObject().Shared.Weapons
|
||||
for k, v in pairs(tempWeapons) do
|
||||
local tempWeaponInfo = exports[OXInv]:Items(v.name)
|
||||
local weight = 0
|
||||
if tempWeaponInfo then
|
||||
weight = tempWeaponInfo.weight
|
||||
end
|
||||
if not Items[v.name] then
|
||||
Items[v.name] = {
|
||||
name = v.name,
|
||||
label = v.label,
|
||||
type = "weapon",
|
||||
ammotype = v.ammotype or "AMMO_PISTOL",
|
||||
weight = weight,
|
||||
image = v.image or (v.name..".png"),
|
||||
description = v.label or "",
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
for k, v in pairs(Items) do
|
||||
if v.client and v.client.image then
|
||||
Items[k].image = (v.client.image):gsub("nui://"..OXInv.."/web/images/", "")
|
||||
else
|
||||
Items[k].image = k..".png"
|
||||
end
|
||||
Items[k].hunger = v.client and v.client.hunger or nil
|
||||
Items[k].thirst = v.client and v.client.thirst or nil
|
||||
end
|
||||
|
||||
elseif isStarted(QBExport) then
|
||||
itemResource = QBExport
|
||||
Core = Core or exports[QBExport]:GetCoreObject()
|
||||
Items = Core and Core.Shared.Items or nil
|
||||
CreateThread(function()
|
||||
while not Items or not next(Items) do
|
||||
Items = exports[QBExport]:GetCoreObject().Shared.Items
|
||||
Wait(1000)
|
||||
end
|
||||
end)
|
||||
if isStarted(QBExport) and not isStarted(QBXExport) then
|
||||
RegisterNetEvent('QBCore:Client:UpdateObject', function()
|
||||
Core = Core or exports[QBExport]:GetCoreObject()
|
||||
Items = Core and Core.Shared.Items or nil
|
||||
end)
|
||||
end
|
||||
|
||||
elseif isStarted(ESXExport) then
|
||||
itemResource = ESXExport
|
||||
CreateThread(function()
|
||||
if isServer() then
|
||||
Items = ESX.GetItems()
|
||||
while not createCallback do Wait(100) end
|
||||
createCallback(getScript()..":getItems", function(source)
|
||||
return Items
|
||||
end)
|
||||
end
|
||||
if not isServer() then
|
||||
Items = triggerCallback(getScript()..":getItems")
|
||||
debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Items).." ^3Items^2 from ^7"..itemResource)
|
||||
end
|
||||
end)
|
||||
|
||||
elseif isStarted(RSGExport) then
|
||||
itemResource = RSGExport
|
||||
Core = Core or exports[RSGExport]:GetCoreObject()
|
||||
Items = Core and Core.Shared.Items or nil
|
||||
RegisterNetEvent('RSGCore:Client:UpdateObject', function()
|
||||
Core = Core or exports[RSGExport]:GetCoreObject()
|
||||
Items = Core and Core.Shared.Items or nil
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
if itemResource == nil then
|
||||
print("^4ERROR^7: ^2No Item info detected ^7- ^2Check ^3starter^1.^2lua^7")
|
||||
else
|
||||
while not Items do Wait(100) end
|
||||
debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Items).." ^3Items^2 from ^7"..itemResource)
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Loading Vehicles
|
||||
-------------------------------------------------------------
|
||||
-- Compile vehicles from the detected frameworks into a unified table.
|
||||
if isStarted(QBXExport) or isStarted(QBExport) then
|
||||
vehResource = QBExport
|
||||
Core = Core or exports[QBExport]:GetCoreObject()
|
||||
Vehicles = Core and Core.Shared.Vehicles
|
||||
if isStarted(QBExport) and not isStarted(QBXExport) then
|
||||
RegisterNetEvent('QBCore:Client:UpdateObject', function()
|
||||
Core = Core or exports[QBExport]:GetCoreObject()
|
||||
Vehicles = Core and Core.Shared.Vehicles
|
||||
end)
|
||||
end
|
||||
|
||||
elseif isStarted(OXCoreExport) then
|
||||
vehResource = OXCoreExport
|
||||
Vehicles = {}
|
||||
for k, v in pairs(Ox.GetVehicleData()) do
|
||||
Vehicles[k] = { model = k, hash = GetHashKey(k), price = v.price, name = v.name, brand = v.make }
|
||||
end
|
||||
|
||||
elseif isStarted(ESXExport) then
|
||||
vehResource = ESXExport
|
||||
CreateThread(function()
|
||||
if isServer() then
|
||||
createCallback(getScript()..":getVehiclesPrices", function(source)
|
||||
return Vehicles
|
||||
end)
|
||||
while not MySQL do Wait(2000) print("^1Waiting for MySQL to exist") end
|
||||
Vehicles = MySQL.query.await('SELECT model, price, name FROM vehicles')
|
||||
end
|
||||
if not isServer() then
|
||||
--while not triggerCallback do print("waiting") Wait(100) end
|
||||
local TempVehicles = triggerCallback(getScript()..":getVehiclesPrices")
|
||||
for _, v in pairs(TempVehicles) do
|
||||
Vehicles = Vehicles or {}
|
||||
Vehicles[v.model] = {
|
||||
model = v.model,
|
||||
hash = GetHashKey(v.model),
|
||||
price = v.price,
|
||||
name = v.name,
|
||||
brand = GetMakeNameFromVehicleModel(v.model):lower():gsub("^%l", string.upper)
|
||||
}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
elseif isStarted(RSGExport) then
|
||||
vehResource = RSGExport
|
||||
Core = Core or exports[RSGExport]:GetCoreObject()
|
||||
Vehicles = Core and Core.Shared.Vehicles
|
||||
RegisterNetEvent('RSGCore:Client:UpdateObject', function()
|
||||
Core = Core or exports[RSGExport]:GetCoreObject()
|
||||
Vehicles = Core and Core.Shared.Vehicles
|
||||
end)
|
||||
end
|
||||
if vehResource == nil then
|
||||
print("^4ERROR^7: ^2No Vehicle info detected ^7- ^2Check ^3starter^1.^2lua^7")
|
||||
else
|
||||
while not Vehicles do Wait(1000) end
|
||||
debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..vehResource)
|
||||
elseif isStarted(VorpExport) then
|
||||
Core = Core or exports[VorpExport]:GetCore()
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Loading Jobs and Gangs
|
||||
-------------------------------------------------------------
|
||||
-- Compile jobs and gangs from the detected framework.
|
||||
if isStarted(QBXExport) then
|
||||
jobResource = QBXExport
|
||||
Core = Core or exports[QBExport]:GetCoreObject()
|
||||
Jobs, Gangs = exports[QBXExport]:GetJobs(), exports[QBXExport]:GetGangs()
|
||||
|
||||
elseif isStarted(OXCoreExport) then
|
||||
jobResource = OXCoreExport
|
||||
CreateThread(function()
|
||||
if isServer() then
|
||||
Jobs = {}
|
||||
createCallback(getScript()..":getOxGroups", function(source)
|
||||
return Jobs
|
||||
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)
|
||||
|
||||
-- Wait until jim_bridge is started and export is available
|
||||
while not cache and (timeout and GetGameTimer() < timeout) do
|
||||
if GetResourceState("jim_bridge"):find("start") then
|
||||
local success, result = pcall(function()
|
||||
return exports["jim_bridge"]:GetSharedData()
|
||||
end)
|
||||
local tempJobs = MySQL.query.await('SELECT * FROM `ox_groups`')
|
||||
local tempGrades = MySQL.query.await('SELECT * FROM `ox_group_grades`')
|
||||
-- Index all grades by group
|
||||
local gradeMap = {}
|
||||
for _, grade in pairs(tempGrades) do
|
||||
gradeMap[grade.group] = gradeMap[grade.group] or {}
|
||||
gradeMap[grade.group][grade.grade] = {
|
||||
name = grade.label
|
||||
}
|
||||
if success and result then
|
||||
cache = result
|
||||
end
|
||||
|
||||
-- Process jobs and attach grades
|
||||
for _, job in pairs(tempJobs) do
|
||||
Jobs[job.name] = {
|
||||
label = job.label,
|
||||
grades = gradeMap[job.name] or {}
|
||||
}
|
||||
end
|
||||
|
||||
-- Copy to Gangs
|
||||
Gangs = Jobs
|
||||
else
|
||||
Jobs = triggerCallback(getScript()..":getOxGroups")
|
||||
Gangs = Jobs
|
||||
end
|
||||
end)
|
||||
|
||||
elseif isStarted(QBExport) then
|
||||
jobResource = QBExport
|
||||
Core = Core or exports[QBExport]:GetCoreObject()
|
||||
Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
|
||||
if isStarted(QBExport) and not isStarted(QBXExport) then
|
||||
RegisterNetEvent('QBCore:Client:UpdateObject', function()
|
||||
Core = exports[QBExport]:GetCoreObject()
|
||||
Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
|
||||
end)
|
||||
Wait(100)
|
||||
end
|
||||
|
||||
elseif isStarted(ESXExport) then
|
||||
jobResource = ESXExport
|
||||
if isServer() then
|
||||
-- If server, create callback to get jobs
|
||||
createCallback(getScript()..":getESXJobs", function(source)
|
||||
return Jobs
|
||||
end)
|
||||
-- Populate jobs table with ESX.GetJobs()
|
||||
Jobs = ESX.GetJobs()
|
||||
--jsonPrint(Jobs)
|
||||
--If retreived jobs is empty, wait for ESX to load
|
||||
while countTable(Jobs) == 0 do
|
||||
Jobs = ESX.GetJobs()
|
||||
Wait(100)
|
||||
end
|
||||
-- Organise into a table the script can use
|
||||
for Role, Grades in pairs(Jobs) do
|
||||
if timeout and not cache then
|
||||
print("^1ERROR^7: ^2jim_bridge export not available after timeout^7.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for "Boss" in name of grades
|
||||
for grade, info in pairs(Grades.grades) do
|
||||
--print(grade)
|
||||
--jsonPrint(info)
|
||||
Items = cache.Items
|
||||
Vehicles = cache.Vehicles
|
||||
Jobs = cache.Jobs
|
||||
Gangs = cache.Gangs
|
||||
InventoryWeight = cache.InventoryWeight or InventoryWeight
|
||||
InventorySlots = cache.InventorySlots or 40
|
||||
|
||||
if info.label then
|
||||
--print(info)
|
||||
if info.label:find("boss") or info.label:find("Boss") then
|
||||
--print("Found Boss label for:", Grades.label)
|
||||
Jobs[Role].grades[grade].isBoss = true
|
||||
goto continue
|
||||
end
|
||||
debugPrint("^6Bridge^7: ^2Shared cache successfully loaded from export^7.")
|
||||
--print(countTable(Items), countTable(Vehicles), countTable(Jobs))
|
||||
else
|
||||
-- Shared bridge cache - prevent duplicate listeners
|
||||
exports["jim_bridge"]:GetBridgeCache(function(cache)
|
||||
Items = cache.Items or {}
|
||||
Vehicles = cache.Vehicles or {}
|
||||
Jobs = cache.Jobs or {}
|
||||
Gangs = cache.Gangs or {}
|
||||
InventoryWeight = cache.InventoryWeight or InventoryWeight
|
||||
InventorySlots = cache.InventorySlots or 50
|
||||
|
||||
CreateThread(function()
|
||||
if isStarted(ESXExport) then
|
||||
for _, v in pairs(Vehicles) do
|
||||
Vehicles[v.model] = {
|
||||
model = v.model,
|
||||
hash = v.hash,
|
||||
price = v.price,
|
||||
name = v.name,
|
||||
brand = GetMakeNameFromVehicleModel(v.model):lower():gsub("^%l", string.upper)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- If no roles with "boss" in the name, revert to max grade
|
||||
if isStarted(OXInv) then
|
||||
for k, v in pairs(Items) do
|
||||
local tempInfo = exports[OXInv]:Items(k)
|
||||
Items[k].image = k..".png" -- Set default image
|
||||
if tempInfo and tempInfo.client then -- if client info, check for hard set image
|
||||
Items[k].image = (tempInfo.client.image) and tempInfo.client.image:gsub("nui://"..OXInv.."/web/images/", "") or Items[k].image
|
||||
Items[k].hunger = tempInfo.client.hunger
|
||||
Items[k].thirst = tempInfo.client.thirst
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Count grades
|
||||
local count = countTable(Grades.grades)
|
||||
Jobs[Role].grades[tostring(count-1)].isBoss = true
|
||||
--print(Grades.label.." Grade: "..count.." is Boss")
|
||||
::continue::
|
||||
end
|
||||
-- ESX Default doesn't have gangs, so copy jobs to gangs
|
||||
Gangs = Jobs
|
||||
end
|
||||
-- If client side, trigger callback to get jobs
|
||||
if not isServer() then
|
||||
Jobs = triggerCallback(getScript()..":getESXJobs")
|
||||
Gangs = Jobs
|
||||
end
|
||||
|
||||
elseif isStarted(RSGExport) then
|
||||
jobResource = RSGExport
|
||||
Core = Core or exports[RSGExport]:GetCoreObject()
|
||||
Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
|
||||
RegisterNetEvent('RSGCore:Client:UpdateObject', function()
|
||||
Core = exports[RSGExport]:GetCoreObject()
|
||||
Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
|
||||
debugPrint("^6Bridge^7: ^2Shared cache successfully loaded from export^7.")
|
||||
end)
|
||||
end
|
||||
|
||||
if jobResource == nil then
|
||||
print("^4ERROR^7: ^2No Job info detected ^7- ^2Check ^3starter^1.^2lua^7")
|
||||
else
|
||||
while not Jobs do Wait(1000) end
|
||||
debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Jobs).." ^3Jobs^2 from ^7"..jobResource)
|
||||
debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Gangs).." ^3Gangs^2 from ^7"..jobResource)
|
||||
end
|
||||
@@ -17,7 +17,8 @@ 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,
|
||||
hasCrafted = true, exp = true, anim = true, time = true, id = true,
|
||||
ingredients = true,
|
||||
}
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -40,9 +41,14 @@ local excludeKeys = {
|
||||
--- craftable = {
|
||||
--- Header = "Weapon Crafting",
|
||||
--- Recipes = {
|
||||
--- [1] = {
|
||||
--- ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 },
|
||||
--- amount = 1,
|
||||
--- weapon_pistol = {
|
||||
--- id = 1,
|
||||
--- ingredients = {
|
||||
--- steel = 5, plastic = 5,
|
||||
--- },
|
||||
--- info = {
|
||||
--- amount = 1,
|
||||
--- },
|
||||
--- },
|
||||
--- -- More recipes...
|
||||
--- },
|
||||
@@ -56,9 +62,10 @@ 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
|
||||
|
||||
@@ -72,86 +79,148 @@ function craftingMenu(data)
|
||||
-- Normalize stash name.
|
||||
data.stashName = data.stashTable or data.stashName
|
||||
|
||||
local Menu = {}
|
||||
local Recipes = data.craftable.Recipes
|
||||
local craftedItems = {}
|
||||
local tempCarryTable = {}
|
||||
-- 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
|
||||
|
||||
-- Build a table of all required ingredients (default quantity is 1).
|
||||
-- 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 craftedItems = {}
|
||||
|
||||
local tempCarryTable = {}
|
||||
-- Build a temporary table of all required ingredients (default quantity is 1).
|
||||
for i = 1, #Recipes do
|
||||
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
|
||||
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] = tempCarryTable[k] and (tempCarryTable[k] < Recipes[i].amount) or Recipes[i].amount
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if the player can carry the required items (server callback).
|
||||
local canCarryTable = triggerCallback(getScript()..':server:canCarry', tempCarryTable)
|
||||
-- Process each recipe to create menu entries.
|
||||
|
||||
local usingStash = data.stashName ~= nil and data.stashName ~= ""
|
||||
|
||||
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,
|
||||
}
|
||||
for i = 1, #Recipes do
|
||||
if not Recipes[i]["amount"] then Recipes[i]["amount"] = 1 end
|
||||
for k, _ in pairs(Recipes[i]) do
|
||||
local menuId = #Menu+1
|
||||
local item = ""
|
||||
local Recipe = {}
|
||||
for k, v in pairs(Recipes[i]) do
|
||||
if not excludeKeys[k] then
|
||||
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
|
||||
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
|
||||
settext = settext..(settext ~= "" and br or "")..(Items[l] and Items[l].label or "error - "..l)..(b > 1 and " x"..b or "")
|
||||
metaTable[Items[l] and Items[l].label or "error - "..l] = b
|
||||
itemTable[l] = b
|
||||
--Wait(0)
|
||||
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 = 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 = (isStarted(QBMenuExport) or disable) and 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
|
||||
item = k
|
||||
Recipe = Recipes[i]
|
||||
Recipe.amount = Recipe.amount or 1
|
||||
break
|
||||
end
|
||||
--Wait(0)
|
||||
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)
|
||||
end
|
||||
end or nil,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- open context menu
|
||||
openMenu(Menu, {
|
||||
header = data.craftable.Header,
|
||||
headertxt = data.craftable.Headertxt,
|
||||
@@ -234,7 +303,7 @@ function multiCraft(data)
|
||||
})
|
||||
|
||||
local dialog = createInput(data.craftable.Header..(Config.System.Menu == "qb" and ": "..br.."How many to craft? "..br.."Max: "..carryMax or ""), {
|
||||
((Config.System.Menu == "ox") and {
|
||||
((Config.System.Menu == "ox" or Config.System.Menu == "lation") and {
|
||||
type = "slider",
|
||||
label = "How many to craft? "..br.."Max: "..carryMax,
|
||||
required = true,
|
||||
@@ -307,7 +376,12 @@ end
|
||||
--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
|
||||
--- })
|
||||
--- ```
|
||||
function makeItem(data)
|
||||
|
||||
function makeItem(origData)
|
||||
|
||||
local data = cloneTable(origData)
|
||||
|
||||
local Ped = PlayerPedId()
|
||||
if CraftLock then return end
|
||||
CraftLock = true
|
||||
data.stashName = data.stashTable or data.stashName
|
||||
@@ -321,113 +395,142 @@ function makeItem(data)
|
||||
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 = createTempCam(PlayerPedId(), data.coords)
|
||||
startTempCam(cam)
|
||||
for i = 1, craftAmount do
|
||||
for k, v in pairs(data.craft) do
|
||||
if not excludeKeys[k] then
|
||||
if 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")
|
||||
crafted, crafting = false, 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 isInventoryOpen() then
|
||||
--print("^1Error^7: ^2Inventory is open, you tried to break things")
|
||||
crafted, crafting, CraftLock = false, false, false
|
||||
return
|
||||
end
|
||||
if crafted 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 isInventoryOpen() then
|
||||
--print("^1Error^7: ^2Inventory is open, you tried to break things")
|
||||
crafted, crafting = false, false
|
||||
crafted, crafting, CraftLock = false, false, false
|
||||
return
|
||||
end
|
||||
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 -- clear client cached token
|
||||
CreateThread(function()
|
||||
if data.craft["hasCrafted"] ~= nil then
|
||||
debugPrint("hasCrafted Found, marking '"..data.item.."' as crafted for player")
|
||||
data.craftable.craftedItems[data.item] = true
|
||||
triggerCallback(getScript()..":server:SetMetadata", "craftedItems", data.craftable.craftedItems)
|
||||
end
|
||||
Wait(100)
|
||||
if data.craft["exp"] ~= nil then
|
||||
craftingLevel += data.craft["exp"].give
|
||||
jsonPrint(data.craft["exp"])
|
||||
debugPrint("exp found, giving exp for '"..data.item.."'")
|
||||
triggerCallback(getScript()..":server:SetMetadata", "craftingLevel", craftingLevel)
|
||||
end
|
||||
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
|
||||
else
|
||||
crafting = false
|
||||
break
|
||||
end
|
||||
if craftProp then destroyProp(craftProp) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Wait(500)
|
||||
local cam = createCam(Ped, data.coords.xyz)
|
||||
startCam(cam, 5000)
|
||||
|
||||
-- Calculate total bartime if SingleProgress is enabled
|
||||
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
|
||||
stopTempCam()
|
||||
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(0)
|
||||
ClearPedTasks(Ped)
|
||||
if canReturn then craftingMenu(origData) end
|
||||
CraftLock = false
|
||||
return
|
||||
end
|
||||
if crafting and progressBar({
|
||||
label = "Using "..v.." "..getItemLabel(k),
|
||||
time = 800,
|
||||
cancel = true,
|
||||
dict = 'pickup_object',
|
||||
anim = "putdown_low",
|
||||
flag = 49,
|
||||
icon = k,
|
||||
}) 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,
|
||||
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
|
||||
if crafting and progressBar({
|
||||
label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount,
|
||||
time = totalBartime,
|
||||
cancel = true,
|
||||
dict = animDict,
|
||||
anim = anim,
|
||||
flag = 49,
|
||||
icon = data.item,
|
||||
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
|
||||
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
|
||||
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 * craftAmount
|
||||
triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel)
|
||||
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(0)
|
||||
CraftLock = false
|
||||
if canReturn then craftingMenu(data) end
|
||||
ClearPedTasks(PlayerPedId())
|
||||
if canReturn then craftingMenu(origData) end
|
||||
ClearPedTasks(Ped)
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Server Event Handler: Crafted Item
|
||||
-------------------------------------------------------------
|
||||
@@ -444,9 +547,9 @@ end
|
||||
--- @usage
|
||||
RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, stashName, metadata, token)
|
||||
local src = source
|
||||
debugPrint(GetInvokingResource())
|
||||
-- debugPrint("^6Bridge^7: ^4"..getScript().."^7:^4Crafting^7:^4GetItem ^2invoked by^7: "..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")
|
||||
debugPrint("^1ERROR^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -454,14 +557,13 @@ 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] or {}) do
|
||||
for k, v in pairs(craftable[ItemMake]) do
|
||||
for _, b in pairs(stashItems or {}) do
|
||||
if k == b.name then
|
||||
itemRemove[k] = v
|
||||
@@ -471,7 +573,7 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
|
||||
end
|
||||
else
|
||||
stashItems = getStash(stashName)
|
||||
for k, v in pairs(craftable[ItemMake] or {}) do
|
||||
for k, v in pairs(craftable[ItemMake]) do
|
||||
for _, b in pairs(stashItems or {}) do
|
||||
if k == b.name then
|
||||
itemRemove[k] = v
|
||||
@@ -481,8 +583,8 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
|
||||
end
|
||||
stashRemoveItem(stashItems, stashName, itemRemove)
|
||||
else
|
||||
if craftable then
|
||||
for k, v in pairs(craftable[ItemMake] or {}) do
|
||||
if craftable[ItemMake] then
|
||||
for k, v in pairs(craftable[ItemMake]) do
|
||||
removeItem(tostring(k), v, src)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,114 @@
|
||||
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
|
||||
@@ -20,58 +128,33 @@
|
||||
--- drawText("img_link", { "Test line 1", "Test Line 2" }, "~g~")
|
||||
--- ```
|
||||
function drawText(image, input, style, oxStyleTable)
|
||||
local text = ""
|
||||
if not radarTable then radarTable = {} end
|
||||
if Config.System.drawText == "qb" then
|
||||
-- Concatenate lines for QB system with HTML line breaks.
|
||||
|
||||
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 = ""
|
||||
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'>")
|
||||
if image then
|
||||
text = '<img src="'..(radarTable[image] or "")..'" style="width:12px;height:12px">'..text
|
||||
end
|
||||
exports[QBExport]:DrawText(text, 'left')
|
||||
handler.show(text, image)
|
||||
|
||||
elseif Config.System.drawText == "ox" then
|
||||
-- Append newline spacing to each input line.
|
||||
for k, v in pairs(input) do
|
||||
input[k] = v.." \n"
|
||||
end
|
||||
lib.showTextUI(table.concat(input), { icon = (image and radarTable[image] or image) or nil, position = 'left-center', style = oxStyleTable })
|
||||
elseif systemType == "ox" then
|
||||
handler.show(input, image, oxStyleTable)
|
||||
|
||||
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 systemType == "lation" then
|
||||
handler.show(input, image)
|
||||
|
||||
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 == "gta" then
|
||||
handler.show(input, image, style)
|
||||
|
||||
elseif systemType == "red" then
|
||||
handler.show(input)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,15 +167,8 @@ end
|
||||
--- hideText()
|
||||
--- ```
|
||||
function hideText()
|
||||
if Config.System.drawText == "qb" then
|
||||
exports[QBExport]:HideText()
|
||||
elseif Config.System.drawText == "ox" then
|
||||
lib.hideTextUI()
|
||||
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")
|
||||
local handler = textHandlers[Config.System.drawText]
|
||||
if handler and handler.hide then
|
||||
handler.hide()
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,10 @@ function AlienEffect()
|
||||
Wait(math.random(5000, 8000))
|
||||
local Ped = PlayerPedId()
|
||||
local animDict = "MOVE_M@DRUNK@VERYDRUNK"
|
||||
loadAnimDict(animDict)
|
||||
RequestAnimSet(animDict)
|
||||
while not HasAnimSetLoaded(animDict) do
|
||||
Wait(100)
|
||||
end
|
||||
SetPedCanRagdoll(Ped, true)
|
||||
ShakeGameplayCam('DRUNK_SHAKE', 2.80)
|
||||
SetTimecycleModifier("Drunk")
|
||||
|
||||
@@ -56,6 +56,31 @@ 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
|
||||
|
||||
local exists = false
|
||||
local ok, result = pcall(function()
|
||||
exists = type(exports[resource][export]) == "function"
|
||||
end)
|
||||
|
||||
if ok and exists then
|
||||
debugPrint(("^3Export Check^7: ^5exports^7['^5%s^7']:^5%s^7() ^2Exists^7"):format(resource, export))
|
||||
return true
|
||||
else
|
||||
debugPrint(("^3Export Check^7: ^5exports^7['^5%s^7']:^5%s^7() ^1Doesn^7'^1t Exist^7"):format(resource, export))
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Debugging and JSON Utilities
|
||||
-------------------------------------------------------------
|
||||
@@ -163,6 +188,7 @@ function GetPrintTime()
|
||||
local hour, min, sec = os.date('%H'), os.date('%M'), os.date('%S')
|
||||
return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")"
|
||||
else
|
||||
if gameName == "rdr3" then return "" end
|
||||
local _, _, _, hour, min, sec = GetLocalTime()
|
||||
return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")"
|
||||
end
|
||||
@@ -207,9 +233,9 @@ end
|
||||
function cv(amount)
|
||||
local formatted = tostring(amount or "0")
|
||||
while true do
|
||||
formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
|
||||
if k == 0 then break end
|
||||
Wait(0)
|
||||
local newFormatted, count = formatted:gsub("^(-?%d+)(%d%d%d)", '%1,%2')
|
||||
if count == 0 then break end
|
||||
formatted = newFormatted
|
||||
end
|
||||
return formatted
|
||||
end
|
||||
@@ -224,13 +250,14 @@ end
|
||||
--- ```
|
||||
function formatCoord(coord)
|
||||
local vecType = type(coord):gsub("tor", "")
|
||||
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)"
|
||||
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, "))
|
||||
end
|
||||
|
||||
--- Calculates the center point of a list of coordinates.
|
||||
@@ -242,13 +269,17 @@ 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 _, coord in ipairs(tbl) do
|
||||
for i = 1, count do
|
||||
local coord = tbl[i]
|
||||
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
|
||||
|
||||
@@ -261,9 +292,9 @@ end
|
||||
--- print("Number of keys:", count)
|
||||
--- ```
|
||||
function countTable(tbl)
|
||||
local i = 0
|
||||
for _ in pairs(tbl) do i += 1 end
|
||||
return i
|
||||
local count = 0
|
||||
for _ in pairs(tbl) do count = count + 1 end
|
||||
return count
|
||||
end
|
||||
|
||||
--- Returns an iterator over a table's keys in sorted order.
|
||||
@@ -280,7 +311,19 @@ function pairsByKeys(t)
|
||||
print("^1Error^7: ^3Nil ^2table recieved for ^3pairsByKeys^7(), ^2setting to ^7{} ^2to prevent break^7")
|
||||
t = {}
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
--- Creates a new table with consecutive numerical indices sorted by the 'id' field.
|
||||
@@ -294,15 +337,18 @@ end
|
||||
--- end
|
||||
--- ```
|
||||
function createConsecutiveTable(originalTable)
|
||||
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
|
||||
local entries = {}
|
||||
for _, entry in pairs(originalTable) do
|
||||
entries[#entries + 1] = entry
|
||||
end
|
||||
return newTable
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
--- Concatenates a table of strings into a single string separated by newlines.
|
||||
@@ -314,11 +360,7 @@ end
|
||||
--- print(combinedText)
|
||||
--- ```
|
||||
function concatenateText(tbl)
|
||||
local result = ""
|
||||
for i = 1, #tbl do
|
||||
result = result..tbl[i]..(i < #tbl and "\n" or "")
|
||||
end
|
||||
return result
|
||||
return table.concat(tbl, "\n")
|
||||
end
|
||||
|
||||
--- Converts a rotation (degrees) to a direction vector.
|
||||
@@ -330,11 +372,14 @@ end
|
||||
--- print(direction)
|
||||
--- ```
|
||||
function RotationToDirection(rot)
|
||||
local adjust = math.pi / 180
|
||||
local radianConvert = math.pi / 180
|
||||
local rotX, rotZ = radianConvert * rot.x, radianConvert * rot.z
|
||||
local cosX = math.abs(math.cos(rotX))
|
||||
|
||||
return vec3(
|
||||
-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)
|
||||
-math.sin(rotZ) * cosX,
|
||||
math.cos(rotZ) * cosX,
|
||||
math.sin(rotX)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -347,11 +392,9 @@ end
|
||||
--- print(bar)
|
||||
--- ```
|
||||
function basicBar(percentage)
|
||||
local perc = math.ceil(percentage)
|
||||
local total = 10
|
||||
local filled = math.floor((perc / 100) * total)
|
||||
local empty = total - filled
|
||||
return string.rep("█", filled)..string.rep("░", empty)
|
||||
local filled = math.floor(math.min(math.max(percentage, 0), 100) / 100 * total)
|
||||
return string.rep("█", filled) .. string.rep("░", total - filled)
|
||||
end
|
||||
|
||||
--- Normalizes a 3D vector.
|
||||
@@ -363,12 +406,8 @@ end
|
||||
--- print(normalizedVec)
|
||||
--- ```
|
||||
function normalizeVector(vec)
|
||||
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
|
||||
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)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -384,16 +423,15 @@ end
|
||||
--- drawLine(vector3(100, 200, 300), vector3(150, 250, 350), vector4(255, 0, 0, 255))
|
||||
--- ```
|
||||
function drawLine(startCoords, endCoords, col)
|
||||
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
|
||||
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)
|
||||
end
|
||||
|
||||
--- Draws a sphere at the specified coordinates (for debugging).
|
||||
@@ -404,16 +442,15 @@ end
|
||||
--- drawSphere(vector3(100, 200, 300), vector4(0, 255, 0, 255))
|
||||
--- ```
|
||||
function drawSphere(coords, col)
|
||||
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
|
||||
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)
|
||||
end
|
||||
|
||||
--- Performs a raycast between two coordinates and returns the results.
|
||||
@@ -431,15 +468,15 @@ end
|
||||
--- end
|
||||
--- ```
|
||||
function PerformRaycast(startCoords, endCoords, entity, flags)
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
return val1, val2, val3, val4, val5, val6
|
||||
|
||||
return GetShapeTestResult(shapeTest)
|
||||
end
|
||||
|
||||
--- Adjusts the Z-coordinate of a position to the ground level.
|
||||
@@ -451,16 +488,34 @@ end
|
||||
--- print("Ground Position:", groundCoords)
|
||||
--- ```
|
||||
function adjustForGround(coords)
|
||||
local foundGround, zPos = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z + 1.0)
|
||||
local foundGround, groundZ = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z + 1.0)
|
||||
|
||||
if foundGround then
|
||||
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
|
||||
return coords.w and vec4(coords.x, coords.y, groundZ, coords.w) or vec3(coords.x, coords.y, groundZ)
|
||||
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.
|
||||
@@ -474,21 +529,7 @@ end
|
||||
--- end
|
||||
--- ```
|
||||
function ensureNetToVeh(vehNetID)
|
||||
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
|
||||
return waitForNetworkEntity(vehNetID, NetToVeh)
|
||||
end
|
||||
|
||||
--- Ensures a network entity exists from its network ID.
|
||||
@@ -502,21 +543,7 @@ end
|
||||
--- end
|
||||
--- ```
|
||||
function ensureNetToEnt(entNetID)
|
||||
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
|
||||
return waitForNetworkEntity(entNetID, NetworkGetEntityFromNetworkId)
|
||||
end
|
||||
|
||||
function sendLog(text)
|
||||
@@ -561,11 +588,96 @@ RegisterNetEvent(getScript()..":server:sendlog", sendServerLog)
|
||||
-- local hungerAmount = GetRandomTiming(hunger)
|
||||
-- print(hungerAmount) -- number between 10, 20
|
||||
function GetRandomTiming(tbl)
|
||||
if type(tbl) == "table" then
|
||||
return math.random(tbl[1], tbl[2])
|
||||
else
|
||||
return tbl
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -801,9 +913,9 @@ local materials = {
|
||||
--- local matHash, matName = GetGroundMaterialAtPosition(vector3(100,200,300))
|
||||
--- print("Material:", matName)
|
||||
--- ```
|
||||
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)
|
||||
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)
|
||||
local _, hit, _, _, materialHash, _ = GetShapeTestResultIncludingMaterial(rayHandle)
|
||||
local materialName = "Unknown"
|
||||
for k, v in pairs(materials) do
|
||||
@@ -849,4 +961,18 @@ 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
|
||||
@@ -138,6 +138,98 @@ function createInput(title, opts)
|
||||
)
|
||||
return dialog
|
||||
|
||||
elseif Config.System.Menu == "lation" then
|
||||
|
||||
for i in pairs(opts) do
|
||||
currentNum += 1
|
||||
if opts[i] == nil then currentNum -= 1 goto skip end
|
||||
if opts[i].type == "radio" then
|
||||
for k in pairs(opts[i].options) do
|
||||
opts[i].options[k].label = opts[i].options[k].text
|
||||
end
|
||||
options[currentNum] = {
|
||||
type = "select",
|
||||
required = opts[i].isRequired,
|
||||
label = opts[i].label or opts[i].text,
|
||||
name = opts[i].name,
|
||||
default = opts[i].default or opts[i].options[1].value,
|
||||
options = opts[i].options,
|
||||
}
|
||||
end
|
||||
if opts[i].type == "number" then
|
||||
options[currentNum] = {
|
||||
type = opts[i].type,
|
||||
label = opts[i].label or opts[i].text,
|
||||
description = opts[i].txt and " - "..opts[i].txt or "",
|
||||
required = opts[i].isRequired,
|
||||
name = opts[i].name,
|
||||
options = opts[i].options,
|
||||
}
|
||||
end
|
||||
if opts[i].type == "text" then
|
||||
options[currentNum] = {
|
||||
type = "input",
|
||||
label = opts[i].label or opts[i].text,
|
||||
description = (opts[i].txt and " - "..opts[i].txt or ""),
|
||||
placeholder = opts[i].default,
|
||||
required = opts[i].isRequired,
|
||||
}
|
||||
end
|
||||
if opts[i].type == "select" then
|
||||
options[currentNum] = {
|
||||
type = opts[i].type,
|
||||
label = opts[i].label or opts[i].text,
|
||||
description = opts[i].txt and " - "..opts[i].txt or "",
|
||||
required = opts[i].isRequired,
|
||||
name = opts[i].name,
|
||||
options = opts[i].options,
|
||||
min = opts[i].min,
|
||||
max = opts[i].max,
|
||||
default = opts[i].default,
|
||||
}
|
||||
end
|
||||
|
||||
if opts[i].type == "checkbox" then
|
||||
for k in pairs(opts[i].options) do
|
||||
if options[currentNum] then currentNum += 1 end
|
||||
options[currentNum] = {
|
||||
type = opts[i].type,
|
||||
label = opts[i].options[k].text..(opts[i].txt and " - "..opts[i].txt or ""),
|
||||
name = opts[i].options[k].value,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if opts[i].type == "color" then
|
||||
options[currentNum] = {
|
||||
type = opts[i].type,
|
||||
label = opts[i].label,
|
||||
required = opts[i].isRequired,
|
||||
format = opts[i].format,
|
||||
default = opts[i].default,
|
||||
}
|
||||
end
|
||||
|
||||
if opts[i].type == "slider" then
|
||||
options[currentNum] = {
|
||||
type = opts[i].type,
|
||||
label = opts[i].label,
|
||||
required = opts[i].required,
|
||||
min = opts[i].min,
|
||||
max = opts[i].max,
|
||||
default = opts[i].default,
|
||||
}
|
||||
end
|
||||
::skip::
|
||||
end
|
||||
|
||||
local dialog = exports.lation_ui:input({
|
||||
title = title,
|
||||
submitText = "Accept",
|
||||
options = options
|
||||
})
|
||||
return dialog
|
||||
|
||||
elseif Config.System.Menu == "gta" then
|
||||
WarMenu.CreateMenu(tostring(opts),
|
||||
title,
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
-------------------------------------------------------------
|
||||
-- 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(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()
|
||||
if isStarted(OXInv) then
|
||||
return LocalPlayer.state.invBusy
|
||||
|
||||
elseif isStarted(QSInv) then
|
||||
return exports[QSInv]:inInventory()
|
||||
|
||||
elseif isStarted(OrigenInv) then
|
||||
return exports[OrigenInv]:IsInventoryOpen()
|
||||
|
||||
elseif isStarted(CoreInv) then
|
||||
return exports[CoreInv]:isInventoryOpen()
|
||||
|
||||
elseif isStarted(CodeMInv) then
|
||||
return false
|
||||
-- CodeM doesn't have a function to check if the inventory is open
|
||||
-- No idea what it uses, so it just skips the check
|
||||
|
||||
elseif isStarted(TgiannInv) then
|
||||
return exports[TgiannInv]:IsInventoryActive()
|
||||
|
||||
elseif isStarted(QBInv) then
|
||||
return LocalPlayer.state.inv_busy
|
||||
|
||||
elseif isStarted(PSInv) then
|
||||
return LocalPlayer.state.inv_busy
|
||||
|
||||
elseif ESX and isStarted(ESXExport) then
|
||||
return false
|
||||
|
||||
elseif isStarted(RSGInv) then
|
||||
return LocalPlayer.state.inv_busy
|
||||
|
||||
end
|
||||
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(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
|
||||
@@ -5,7 +5,7 @@
|
||||
or other type (e.g., coyote). It uses predefined model hashes stored in the AnimalPeds table.
|
||||
|
||||
Global Flags:
|
||||
- isCat, isDog, isBigDog, isSmallDog, isCoyote, isAnimal: Booleans to track the player's
|
||||
- isCat, isPedDog, isBigDog, isSmallDog, isCoyote, isAnimal: Booleans to track the player's
|
||||
current animal classification.
|
||||
|
||||
When running client-side (not on the server), the module checks the player's Ped after they load.
|
||||
@@ -26,13 +26,13 @@
|
||||
]]
|
||||
|
||||
-- Global animal classification flags.
|
||||
isCat, isDog, isBigDog, isSmallDog, isCoyote, isAnimal = false, false, false, false, false, false
|
||||
isPedCat, isPedDog, isBigDog, isSmallDog, isCoyote, isAnimal = false, false, false, false, false, false
|
||||
|
||||
if not isServer() then
|
||||
onPlayerLoaded(function()
|
||||
Wait(2000)
|
||||
Wait(1000)
|
||||
-- Reset classification flags
|
||||
isCat, isDog, isBigDog, isSmallDog, isCoyote = false, false, false, false, false
|
||||
isPedCat, isPedDog, isBigDog, isSmallDog, isCoyote = false, false, false, false, false
|
||||
-- Check if the player's Ped is an animal.
|
||||
isPedAnimal()
|
||||
if isAnimal then
|
||||
@@ -41,18 +41,18 @@ if not isServer() then
|
||||
|
||||
-- Determine if the Ped is a cat:
|
||||
-- Also treat 'ft-raccoon' as a cat unless it is 'ft-sphynx'
|
||||
isCat = (isCat(ped) or pedModel == `ft-raccoon`) and (pedModel ~= `ft-sphynx`)
|
||||
isPedCat = (isCat(ped) == true or pedModel == `ft-raccoon`) and (pedModel ~= `ft-sphynx`)
|
||||
|
||||
-- Determine if the Ped is a dog and whether it's big:
|
||||
isDog, isBigDog = isDog(ped)
|
||||
isPedDog, isBigDog = isDog(ped)
|
||||
isSmallDog = not isBigDog
|
||||
if isDog and pedModel == `a_c_coyote` then isDog = false end
|
||||
if isPedDog and pedModel == `a_c_coyote` then isPedDog = false end
|
||||
|
||||
-- Determine if the Ped is a coyote (special case):
|
||||
isCoyote = (pedModel == `ft-sphynx` or pedModel == `a_c_coyote`)
|
||||
|
||||
-- Special override: if model is 'ft-capmonkey2', treat as a dog.
|
||||
if pedModel == `ft-capmonkey2` then isDog = true end
|
||||
if pedModel == `ft-capmonkey2` then isPedDog = true end
|
||||
end
|
||||
end, true)
|
||||
|
||||
@@ -73,13 +73,13 @@ 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
|
||||
isAnimal = true
|
||||
debugPrint("^6Bridge^7: ^2Ped is Animal")
|
||||
debugPrint("^4isAnimal^7: ^2Ped is ^4Animal")
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -105,10 +105,11 @@ 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")
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -126,8 +127,8 @@ if not isServer() then
|
||||
---
|
||||
---@usage
|
||||
--- ```lua
|
||||
--- local isDog, isBigDog = isDog()
|
||||
--- if isDog then
|
||||
--- local isPedDog, isBigDog = isDog()
|
||||
--- if isPedDog then
|
||||
--- if isBigDog then
|
||||
--- print("Player is a big dog!")
|
||||
--- else
|
||||
@@ -147,15 +148,17 @@ 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")
|
||||
return true, true
|
||||
end
|
||||
end
|
||||
for modelHash, _ in pairs(AnimalPeds.SmallDogs) do
|
||||
if PedModel == modelHash then
|
||||
debugPrint("^4isAnimal^7: ^2Ped is ^4Small Dog")
|
||||
return true, false
|
||||
end
|
||||
end
|
||||
@@ -196,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
|
||||
@@ -281,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
@@ -9,6 +9,73 @@
|
||||
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
|
||||
-------------------------------------------------------------
|
||||
@@ -100,21 +167,58 @@ end
|
||||
--- toggleDuty() -- Player receives a notification of their new duty status.
|
||||
--- ```
|
||||
function toggleDuty()
|
||||
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
|
||||
if onDuty then
|
||||
triggerNotify(nil, "Now on duty", "success")
|
||||
else
|
||||
triggerNotify(nil, "Now off duty", "success")
|
||||
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")
|
||||
else
|
||||
triggerNotify(nil, "Now off duty", "success")
|
||||
end
|
||||
end
|
||||
|
||||
-- Framework specific functions to keep duty status synced
|
||||
@@ -230,4 +334,4 @@ function useDoor(data)
|
||||
SetEntityHeading(PlayerPedId(), data.telecoords.w)
|
||||
DoScreenFadeIn(1000)
|
||||
Wait(100)
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
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.
|
||||
@@ -7,58 +9,137 @@
|
||||
-- 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 The target `vector3` coordinates that the camera will point at.
|
||||
---@param coords vector3|entityId The target `vector3` coordinates that the camera will point at.
|
||||
--
|
||||
---@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`).
|
||||
---@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`).
|
||||
--
|
||||
---@usage
|
||||
-- ```lua
|
||||
-- local cam = createTempCam(entity, targetCoords)
|
||||
-- ```
|
||||
function createTempCam(ent, coords)
|
||||
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
|
||||
local camID = nil
|
||||
if 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
|
||||
end
|
||||
|
||||
cam = CreateCamWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z + 0.5, 1.0, 0.0, 0.0, 60.00, false, 0)
|
||||
-- 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)
|
||||
|
||||
if type(coords) == "number" then
|
||||
SetCamCoord(cam, GetCamCoord(cam) + vec3(0, 0, 1.0))
|
||||
PointCamAtEntity(cam, coords)
|
||||
else
|
||||
PointCamAtCoord(cam, coords)
|
||||
-- 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)
|
||||
end
|
||||
|
||||
end
|
||||
return cam
|
||||
return camID
|
||||
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 camID The handle of the camera to activate and render.
|
||||
---@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.
|
||||
--
|
||||
---@usage
|
||||
-- ```lua
|
||||
-- startTempCam(cam)
|
||||
-- startTempCam(camID, 1000, true, { postFx = "HeistCelebEnd" })
|
||||
-- ```
|
||||
function startTempCam(cam)
|
||||
if Config.Crafting.craftCam then
|
||||
SetCamActive(cam, true)
|
||||
RenderScriptCams(true, true, 1000, true, true)
|
||||
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)
|
||||
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.
|
||||
@@ -70,12 +151,34 @@ end
|
||||
-- ```lua
|
||||
-- stopTempCam()
|
||||
-- ```
|
||||
function stopTempCam()
|
||||
if Config.Crafting.craftCam then
|
||||
CreateThread(function()
|
||||
Wait(1000)
|
||||
RenderScriptCams(false, true, 500, true, true)
|
||||
DestroyAllCams()
|
||||
end)
|
||||
end
|
||||
end
|
||||
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 0)
|
||||
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
|
||||
@@ -187,7 +187,7 @@ end
|
||||
--- ```
|
||||
function playAnim(animDict, animName, duration, flag, ped, speed)
|
||||
loadAnimDict(animDict)
|
||||
debugPrint("Attempting to make player play anim", animDict, animName)
|
||||
debugPrint("^6Bridge^7: ^3playAnim^7() ^2Triggered^7: ", animDict, animName, flag)
|
||||
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
|
||||
|
||||
@@ -204,7 +204,7 @@ end
|
||||
--- stopAnim('amb@world_human_hang_out_street@male_c@base', 'base', PlayerPedId())
|
||||
--- ```
|
||||
function stopAnim(animDict, animName, ped)
|
||||
debugPrint("Stopping anim for "..(ped or PlayerPedId()))
|
||||
debugPrint("^6Bridge^7: ^3stopAnim^7() ^2Triggered^7: ", animDict, animName)
|
||||
StopAnimTask(ped or PlayerPedId(), animDict, animName, 0.5)
|
||||
StopAnimTask(ped or PlayerPedId(), animName, animDict, 0.5)
|
||||
unloadAnimDict(animDict)
|
||||
@@ -238,4 +238,25 @@ 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
|
||||
@@ -133,16 +133,19 @@ function makeEntityBlip(data)
|
||||
AddTextComponentString(tostring(data.name))
|
||||
EndTextCommandSetBlipName(blip)
|
||||
-- Handle preview image if certain resources are running
|
||||
if isStarted("fs_smallresources") or isStarted("blip-info") or isStarted("blipinfo") then
|
||||
if isStarted("jim-blipcontroller") then
|
||||
if data.preview then
|
||||
local txname = string.gsub(tostring(data.name..'preview'), "[ ()~]", "")
|
||||
local txname = string.gsub(tostring(data.name..'preview'..string.gsub(data.coords.z, "%.", "")), "[ ()~]", "")
|
||||
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["fs_smallresources"]:SetBlipInfoImage(blip, getScript()..'previewTxd', txname)
|
||||
exports["fs_smallresources"]:SetBlipInfoTitle(blip, data.name, false)
|
||||
exports["jim-blipcontroller"]:ShowBlipInfo(blip, {
|
||||
title = data.name,
|
||||
dict = getScript()..'scriptTxd',
|
||||
tex = txname,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
--- 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.
|
||||
--
|
||||
@@ -18,18 +19,20 @@ local Peds = {}
|
||||
-- ```lua
|
||||
-- makeDistPed(pedData, pedCoords, true, false, 'WORLD_HUMAN_STAND_IMPATIENT', nil, true)
|
||||
-- ```
|
||||
function makeDistPed(data, coords, freeze, collision, scenario, anim, synced)
|
||||
function makeDistPed(data, coords, freeze, collision, scenario, anim, synced, func)
|
||||
local zoneCoords = type(data) == "table" and data.coords or coords
|
||||
local randName = keyGen()..keyGen()
|
||||
createCirclePoly({
|
||||
distPeds[#distPeds+1] = 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,
|
||||
})
|
||||
@@ -55,14 +58,13 @@ end
|
||||
-- ```lua
|
||||
-- local ped = makePed(pedData, pedCoords, true, false, nil, {'animDict', 'animName'}, true)
|
||||
-- ```
|
||||
function makePed(data, coords, freeze, collision, scenario, anim, synced)
|
||||
function makePed(data, coords, freeze, collision, scenario, anim, synced, fade)
|
||||
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)
|
||||
|
||||
@@ -116,6 +118,7 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced)
|
||||
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
|
||||
@@ -141,6 +144,12 @@ function makePed(data, coords, freeze, collision, scenario, anim, synced)
|
||||
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
|
||||
|
||||
@@ -238,9 +247,19 @@ 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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local Props = {}
|
||||
local distProps = {}
|
||||
|
||||
--- Creates a prop (object) in the game world at specified coordinates.
|
||||
---
|
||||
@@ -20,7 +21,7 @@ local Props = {}
|
||||
--- }
|
||||
--- local prop = makeProp(propData, true, false)
|
||||
--- ```
|
||||
function makeProp(data, freeze, synced)
|
||||
function makeProp(data, freeze, synced, fade)
|
||||
loadModel(data.prop)
|
||||
local prop = CreateObject(data.prop, data.coords.x, data.coords.y, data.coords.z-1.03, synced and synced or false, synced and synced or false, false)
|
||||
SetEntityHeading(prop, (data.coords.w or 0) + 180.0)
|
||||
@@ -29,6 +30,12 @@ function makeProp(data, freeze, synced)
|
||||
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
|
||||
|
||||
@@ -51,17 +58,21 @@ end
|
||||
--- }
|
||||
--- makeDistProp(propData, true, false)
|
||||
--- ```
|
||||
function makeDistProp(data, freeze, synced, range)
|
||||
function makeDistProp(data, freeze, synced, range, func)
|
||||
local name = keyGen()..keyGen()
|
||||
createCirclePoly({
|
||||
distProps[#distProps + 1] = 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,
|
||||
})
|
||||
@@ -86,6 +97,16 @@ 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
|
||||
|
||||
@@ -13,23 +13,29 @@ local Vehicles = {}
|
||||
--- ```lua
|
||||
--- local vehicle = makeVeh('adder', vector4(123.4, 567.8, 90.1, 180.0))
|
||||
--- ```
|
||||
function makeVeh(model, coords)
|
||||
function makeVeh(model, coords, synced, fade)
|
||||
loadModel(model)
|
||||
local veh = CreateVehicle(model, coords.x, coords.y, coords.z, coords.w, true, false)
|
||||
local veh = CreateVehicle(model, coords.x, coords.y, coords.z, coords.w, synced ~= false, 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
|
||||
|
||||
@@ -52,16 +58,16 @@ 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)
|
||||
vehicle = makeVeh(data.model, data.coords, false)
|
||||
if onEnter then
|
||||
debugPrint("makeDistVehicle onEnter running")
|
||||
debugPrint("^6Bridge^7: ^4makeDistVehicle ^3onEnter^7() ^2running^7")
|
||||
onEnter(vehicle)
|
||||
end
|
||||
end,
|
||||
onExit = function()
|
||||
deleteVehicle(vehicle)
|
||||
if onExit then
|
||||
debugPrint("makeDistVehicle onExit running")
|
||||
debugPrint("^6Bridge^7: ^4makeDistVehicle ^3onExit^7() ^2running^7")
|
||||
onExit(vehicle)
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -1,4 +1,258 @@
|
||||
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.
|
||||
---
|
||||
@@ -33,7 +287,11 @@ local inProgress = false
|
||||
--- 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")
|
||||
@@ -42,144 +300,17 @@ function progressBar(data)
|
||||
end
|
||||
local result = nil
|
||||
if data.cam then startTempCam(data.cam) end
|
||||
if Config.System.ProgressBar == "ox" then
|
||||
if exports[OXLibExport]:progressBar({
|
||||
duration = debugMode and 1000 or data.time,
|
||||
label = data.label,
|
||||
useWhileDead = data.dead or false,
|
||||
canCancel = data.cancel and 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 = true
|
||||
},
|
||||
}) then
|
||||
result = true
|
||||
else
|
||||
result = false
|
||||
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 = true, disableCarMovement = true, disableMouse = false, disableCombat = 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 == "red" then
|
||||
-- Currently only uses jim-redui if you choose this option
|
||||
if exports["jim-redui"]:progressBar({
|
||||
label = data.label,
|
||||
time = debugMode and 1000 or data.time,
|
||||
dict = data.dict,
|
||||
anim = data.anim,
|
||||
flag = data.flag or 32,
|
||||
task = data.task,
|
||||
cancel = true,
|
||||
}) then
|
||||
result = true
|
||||
else
|
||||
result = false
|
||||
end
|
||||
|
||||
elseif Config.System.ProgressBar == "gta" then
|
||||
loadTextureDict("timerbars")
|
||||
if inProgress then return false end
|
||||
inProgress = true
|
||||
local wait = debugMode and 1000 or data.time
|
||||
local endTime = GetGameTimer() + wait
|
||||
local ped = PlayerPedId()
|
||||
|
||||
-- Setup Animation/Task if specified
|
||||
if data.dict then
|
||||
playAnim(data.dict, data.anim, -1, data.flag or 32)
|
||||
elseif data.task then
|
||||
TaskStartScenarioInPlace(ped, data.task, -1, true)
|
||||
end
|
||||
|
||||
-- Progress bar rendering loop
|
||||
CreateThread(function()
|
||||
while GetGameTimer() < endTime and inProgress do
|
||||
Wait(0)
|
||||
local elapsed = GetGameTimer()
|
||||
local percentage = ((elapsed - (endTime - wait)) / wait) * 100
|
||||
|
||||
-- Convert to segmented progress (assuming 5 segments here)
|
||||
local segments = 5 -- 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(segmentProgress, data.label, ("%.0f%%"):format(percentage))
|
||||
|
||||
-- Controls to disable during progress
|
||||
DisablePlayerFiring(ped, true)
|
||||
DisableControlAction(0, 25, true) -- Disable aim
|
||||
DisableControlAction(0, 21, true) -- Disable sprint
|
||||
DisableControlAction(0, 30, true) -- Disable move left/right
|
||||
DisableControlAction(0, 31, true) -- Disable move forward/back
|
||||
DisableControlAction(0, 36, true) -- Disable stealth
|
||||
|
||||
if data.cancel and (IsControlJustReleased(0, 202) or IsControlJustReleased(0, 177) or IsControlJustReleased(0, 73)) then
|
||||
inProgress = false
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Wait for completion or cancel
|
||||
while GetGameTimer() < endTime and inProgress do
|
||||
Wait(100)
|
||||
end
|
||||
|
||||
-- Cleanup animations/tasks
|
||||
if data.dict then stopAnim(data.dict, data.anim, ped) end
|
||||
ClearPedTasks(ped)
|
||||
|
||||
result = inProgress
|
||||
inProgress = false
|
||||
if progressFunc[Config.System.ProgressBar].start(data) then
|
||||
result = true
|
||||
else
|
||||
result = false
|
||||
end
|
||||
|
||||
while result == nil do Wait(10) end
|
||||
while result == nil do
|
||||
Wait(10)
|
||||
end
|
||||
debugPrint("^5Debug^7: ^2ProgressBar result^7: ^3"..tostring(result).."^7")
|
||||
|
||||
-- Cleanup
|
||||
FreezeEntityPosition(ped, false)
|
||||
@@ -200,71 +331,21 @@ function progressBar(data)
|
||||
TriggerServerEvent(getScript()..":clearAuthToken")
|
||||
currentToken = triggerCallback(AuthEvent)
|
||||
end
|
||||
progresssBarActive = false
|
||||
return result
|
||||
end
|
||||
|
||||
function ShowGTAProgressBar(currentProg, title, level)
|
||||
local loc = vec2(0.37, 0.90)
|
||||
local size = vec2(0.3, 0.03)
|
||||
|
||||
-- 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)
|
||||
|
||||
SetTextFont(0)
|
||||
SetTextProportional(1)
|
||||
SetTextScale(0.35, 0.35)
|
||||
SetTextColour(255, 255, 255, 255)
|
||||
SetTextDropshadow(0, 0, 0, 0, 255)
|
||||
SetTextDropShadow()
|
||||
SetTextOutline()
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(title)
|
||||
DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034) -- Adjust text position
|
||||
|
||||
SetTextFont(0)
|
||||
SetTextProportional(1)
|
||||
SetTextScale(0.35, 0.25)
|
||||
SetTextColour(255, 255, 255, 255)
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(level)
|
||||
DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030) -- Right-aligned additional text
|
||||
|
||||
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
|
||||
|
||||
for i = 1, #currentProg do
|
||||
local segmentX = (loc.x - size.x / 4 ) + 0.075 + (i - 1) * (segmentWidth + gap)
|
||||
local fillPercentage = currentProg[i]
|
||||
local progressBarWidth = segmentWidth * (fillPercentage / 100)
|
||||
|
||||
-- Semi-transparent background for each segment
|
||||
DrawRect(segmentX + segmentWidth / 2, loc.y, segmentWidth, size.y / 3.4, 100, 100, 100, 255)
|
||||
|
||||
-- Filling progress for each segment
|
||||
if progressBarWidth > 0 then
|
||||
DrawRect(segmentX + progressBarWidth / 2, loc.y, progressBarWidth, size.y / 3.4, 93, 182, 229, 255) -- Blue progress
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Stops the current progress bar.
|
||||
---
|
||||
--- This function cancels the progress bar based on the configured progress bar system, handling any necessary cleanup.
|
||||
function stopProgressBar()
|
||||
if Config.System.ProgressBar == "ox" then
|
||||
exports[OXLibExport]:cancelProgress()
|
||||
elseif Config.System.ProgressBar == "qb" then
|
||||
TriggerEvent("progressbar:client:cancel")
|
||||
elseif Config.System.ProgressBar == "gta" then
|
||||
inProgress = false
|
||||
end
|
||||
progressFunc[Config.System.ProgressBar].stop()
|
||||
progresssBarActive = false
|
||||
end
|
||||
|
||||
-- System to handle sending/sharing progress bars between players --
|
||||
-- For example, healing someone --
|
||||
|
||||
local storedPID = nil
|
||||
|
||||
--- Server event handler for starting a shared progress bar.
|
||||
--- This event is triggered when a player wants to start a progress bar on another player.
|
||||
|
||||
255
shared/make/ropeControl.lua
Normal file
255
shared/make/ropeControl.lua
Normal file
@@ -0,0 +1,255 @@
|
||||
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
|
||||
@@ -6,6 +6,102 @@
|
||||
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
|
||||
-------------------------------------------------------------
|
||||
@@ -20,26 +116,11 @@
|
||||
--- local player = GetPlayer(playerId)
|
||||
--- ```
|
||||
function GetPlayer(source)
|
||||
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)
|
||||
|
||||
for i = 1, #metaDataFunc do
|
||||
local framework = metaDataFunc[i]
|
||||
if isStarted(framework.framework) then
|
||||
return framework.GetPlayer(source)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -58,29 +139,22 @@ end
|
||||
---
|
||||
--- @usage
|
||||
--- ```lua
|
||||
--- local myMeta = GetMetadata(player, "myKey")
|
||||
--- local myMeta = getPlayerMetadata(player, "myKey")
|
||||
--- ```
|
||||
function GetMetadata(player, key)
|
||||
function getPlayerMetadata(player, key)
|
||||
-- Assume this is client side and callback to server to get the data
|
||||
if not player then
|
||||
debugPrint("^6Bridge^7: ^3GetMetadata^7() calling server: "..key)
|
||||
return triggerCallback(getScript()..":server:GetMetadata", key)
|
||||
else
|
||||
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]
|
||||
-- else grab server metadata about the player
|
||||
for i = 1, #metaDataFunc do
|
||||
local framework = metaDataFunc[i]
|
||||
if isStarted(framework.framework) then
|
||||
|
||||
debugPrint("^6Bridge^7: ^3GetMetadata^7() ^3"..framework.framework.."^7", key)
|
||||
return framework.GetPlayerMetadata(player, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
@@ -98,11 +172,11 @@ createCallback(getScript()..":server:GetMetadata", function(source, key)
|
||||
if type(key) == "table" then
|
||||
local Metadata = {}
|
||||
for _, k in ipairs(key) do
|
||||
Metadata[k] = GetMetadata(player, k)
|
||||
Metadata[k] = getPlayerMetadata(player, k)
|
||||
end
|
||||
return Metadata
|
||||
elseif type(key) == "string" then
|
||||
return GetMetadata(player, key)
|
||||
return getPlayerMetadata(player, key)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -120,38 +194,26 @@ end)
|
||||
---
|
||||
--- @usage
|
||||
--- ```lua
|
||||
--- SetMetadata(player, "myKey", "newValue")
|
||||
--- setPlayerMetadata(player, "myKey", "newValue")
|
||||
--- ```
|
||||
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)
|
||||
|
||||
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
|
||||
end
|
||||
debugPrint("^6Bridge^7: ^1Error setting metadata^7, ^1framework not supported^7?")
|
||||
end
|
||||
|
||||
-- Register a server callback for setting metadata.
|
||||
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)
|
||||
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)
|
||||
print("Metadata set successfully.", key)
|
||||
return true
|
||||
end)
|
||||
@@ -8,10 +8,90 @@
|
||||
• 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
|
||||
@@ -31,53 +111,12 @@
|
||||
--- triggerNotify("Alert", "You have been warned for misconduct.", "error", playerId)
|
||||
--- ```
|
||||
function triggerNotify(title, message, type, src)
|
||||
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 isStarted("jim-gtaui") then
|
||||
if not src then
|
||||
TriggerEvent("jim-gtaui:Notify", title, message, type)
|
||||
else
|
||||
TriggerClientEvent("jim-gtaui:Notify", src, title, message, type)
|
||||
end
|
||||
else
|
||||
if not src then
|
||||
TriggerEvent(getScript()..":DisplayGTANotify", title, message)
|
||||
else
|
||||
TriggerClientEvent(getScript()..":DisplayGTANotify", src, title, message)
|
||||
end
|
||||
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
|
||||
if not Config.System?.Notify then debugPrint("Notify triggered but not set up") return end
|
||||
|
||||
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
|
||||
if src then
|
||||
notifyFunc[Config.System.Notify].server(title, message, type, src)
|
||||
else
|
||||
notifyFunc[Config.System.Notify].client(title, message, type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -90,7 +129,6 @@ 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
|
||||
@@ -116,25 +154,25 @@ end)
|
||||
--- ```lua
|
||||
--- TriggerEvent(getScript()..":DisplayGTANotify", "taxiname", "Taxi service has arrived.")
|
||||
--- ```
|
||||
RegisterNetEvent(getScript()..":DisplayGTANotify", function(title, text)
|
||||
local iconTable = {}
|
||||
if getScript() == "jim-npcservice" then
|
||||
iconTable = {
|
||||
[Loc[Config.Lan].notify["taxiname"]] = "CHAR_TAXI",
|
||||
[Loc[Config.Lan].notify["limoname"]] = "CHAR_CASINO",
|
||||
[Loc[Config.Lan].notify["ambiname"]] = "CHAR_CALL911",
|
||||
[Loc[Config.Lan].notify["pilotname"]] = "CHAR_DEFAULT",
|
||||
[Loc[Config.Lan].notify["planename"]] = "CHAR_BOATSITE2",
|
||||
[Loc[Config.Lan].notify["heliname"]] = "CHAR_BOATSITE2",
|
||||
}
|
||||
end
|
||||
|
||||
BeginTextCommandThefeedPost("STRING")
|
||||
AddTextComponentSubstringKeyboardDisplay(text)
|
||||
EndTextCommandThefeedPostMessagetext(
|
||||
iconTable[title] or "CHAR_DEFAULT",
|
||||
iconTable[title] or "CHAR_DEFAULT",
|
||||
true, 1, title, nil, text
|
||||
)
|
||||
EndTextCommandThefeedPostTicker(true, false)
|
||||
end)
|
||||
--RegisterNetEvent(getScript()..":DisplayGTANotify", function(title, text)
|
||||
-- local iconTable = {}
|
||||
-- if getScript() == "jim-npcservice" then
|
||||
-- iconTable = {
|
||||
-- [Loc[Config.Lan].notify["taxiname"]] = "CHAR_TAXI",
|
||||
-- [Loc[Config.Lan].notify["limoname"]] = "CHAR_CASINO",
|
||||
-- [Loc[Config.Lan].notify["ambiname"]] = "CHAR_CALL911",
|
||||
-- [Loc[Config.Lan].notify["pilotname"]] = "CHAR_DEFAULT",
|
||||
-- [Loc[Config.Lan].notify["planename"]] = "CHAR_BOATSITE2",
|
||||
-- [Loc[Config.Lan].notify["heliname"]] = "CHAR_BOATSITE2",
|
||||
-- }
|
||||
-- end
|
||||
--
|
||||
-- BeginTextCommandThefeedPost("STRING")
|
||||
-- AddTextComponentSubstringKeyboardDisplay(text)
|
||||
-- EndTextCommandThefeedPostMessagetext(
|
||||
-- iconTable[title] or "CHAR_DEFAULT",
|
||||
-- iconTable[title] or "CHAR_DEFAULT",
|
||||
-- true, 1, title, nil, text
|
||||
-- )
|
||||
-- EndTextCommandThefeedPostTicker(true, false)
|
||||
--end)
|
||||
@@ -5,14 +5,127 @@
|
||||
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
|
||||
}
|
||||
)
|
||||
end,
|
||||
},
|
||||
|
||||
{ phone = "yflip-phone",
|
||||
sendMail =
|
||||
function(mailData)
|
||||
TriggerServerEvent(getScript()..":yflip:SendMail", mailData)
|
||||
end,
|
||||
},
|
||||
|
||||
{ phone = "roadphone",
|
||||
sendMail =
|
||||
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)
|
||||
-- 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)
|
||||
TriggerServerEvent('qb-phone:server:sendNewMail', mailData)
|
||||
end,
|
||||
sendInvoice =
|
||||
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
|
||||
|
||||
-- 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
|
||||
)
|
||||
TriggerClientEvent('qb-phone:RefreshPhone', mailData.src)
|
||||
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,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
--- 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.
|
||||
@@ -31,77 +144,36 @@
|
||||
--- { 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,
|
||||
},
|
||||
{ name = "yflip-phone",
|
||||
send = function(mailData)
|
||||
TriggerServerEvent(getScript()..":yflip:SendMail", mailData)
|
||||
end,
|
||||
},
|
||||
{ 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,
|
||||
},
|
||||
{ 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,
|
||||
},
|
||||
{ name = "qb-phone",
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
local activePhone = nil
|
||||
function sendPhoneMail(mailData)
|
||||
-- Check each phone system in order and use the first active one.
|
||||
for _, phone in ipairs(phoneSystems) do
|
||||
if isStarted(phone.name) then
|
||||
activePhone = phone.name
|
||||
phone.send(data)
|
||||
break
|
||||
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
|
||||
|
||||
if activePhone then
|
||||
debugPrint("^6Bridge^7[^3"..activePhone.."^7]: ^2Sending mail to player")
|
||||
else
|
||||
print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found")
|
||||
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
|
||||
|
||||
-- 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)
|
||||
return true
|
||||
end
|
||||
end
|
||||
print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7- ^2No supported phone found")
|
||||
return false
|
||||
end
|
||||
-------------------------------------------------------------
|
||||
-- Phone System Event Handlers
|
||||
-------------------------------------------------------------
|
||||
@@ -168,4 +240,6 @@ 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
@@ -11,6 +11,68 @@
|
||||
• 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
|
||||
-------------------------------------------------------------
|
||||
@@ -41,26 +103,16 @@
|
||||
---})
|
||||
---```
|
||||
function 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)
|
||||
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)
|
||||
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
|
||||
|
||||
end)
|
||||
else
|
||||
print("^4ERROR^7: ^2No PolyZone creation script detected ^7- ^2Check ^3exports^1.^2lua^7")
|
||||
end
|
||||
return Location
|
||||
|
||||
print("^4ERROR^7: ^2No PolyZone creation script detected ^7")
|
||||
return nil
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -92,25 +144,17 @@ end
|
||||
--- })
|
||||
--- ```
|
||||
function createCirclePoly(data)
|
||||
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")
|
||||
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
|
||||
end
|
||||
debugPrint("^6Bridge^7: ^2Zone Stats - Coords: "..formatCoord(data.coords).." Radius: "..data.radius)
|
||||
return Location
|
||||
|
||||
print("^4ERROR^7: ^2No PolyZone creation script detected ^7")
|
||||
return nil
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -130,11 +174,12 @@ end
|
||||
--- removePolyZone(zone)
|
||||
--- ```
|
||||
function removePolyZone(Location)
|
||||
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()
|
||||
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
|
||||
end
|
||||
end
|
||||
262
shared/shops.lua
262
shared/shops.lua
@@ -1,262 +0,0 @@
|
||||
-------------------------------------------------------------
|
||||
-- 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) or isStarted(CodeMInv) then
|
||||
TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
|
||||
|
||||
elseif isStarted(TgiannInv) then
|
||||
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
|
||||
|
||||
elseif isStarted(QBInv) then
|
||||
if QBInvNew then
|
||||
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
|
||||
else
|
||||
TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
|
||||
end
|
||||
|
||||
elseif isStarted(PSInv) then
|
||||
if QBInvNew then
|
||||
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
|
||||
else
|
||||
TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
|
||||
end
|
||||
|
||||
elseif isStarted(RSGInv) then
|
||||
TriggerServerEvent(getScript()..':server:openServerShop', data.shop)
|
||||
end
|
||||
lookEnt(data.coords)
|
||||
end
|
||||
|
||||
RegisterNetEvent(getScript()..':server:openServerShop', function(data)
|
||||
if isStarted(QBInv) then
|
||||
exports[QBInv]:OpenShop(source, data)
|
||||
end
|
||||
if isStarted(PSInv) 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)
|
||||
if isStarted(OXInv) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3OX ^2Store^7:", name, label)
|
||||
exports[OXInv]:RegisterShop(name, {
|
||||
name = label,
|
||||
inventory = items,
|
||||
society = society,
|
||||
})
|
||||
|
||||
elseif isStarted(TgiannInv) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3Tgiann ^2Store^7:", name, label)
|
||||
exports[TgiannInv]:RegisterShop(name, items)
|
||||
|
||||
elseif isStarted(QBInv) and QBInvNew then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3QB ^2Store^7:", name, label)
|
||||
exports[QBInv]:CreateShop({
|
||||
name = name,
|
||||
label = label,
|
||||
slots = #items,
|
||||
items = items,
|
||||
society = society,
|
||||
})
|
||||
|
||||
elseif isStarted(PSInv) and QBInvNew then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3PS ^2Store^7:", name, label)
|
||||
exports[PSInv]:CreateShop({
|
||||
name = name,
|
||||
label = label,
|
||||
slots = #items,
|
||||
items = items,
|
||||
society = society,
|
||||
})
|
||||
|
||||
elseif isStarted(RSGInv) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3RSG ^2Store^7:", name, label)
|
||||
exports[RSGInv]:CreateShop({
|
||||
name = name,
|
||||
label = label,
|
||||
slots = #items,
|
||||
items = items,
|
||||
society = society,
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -1,156 +1,73 @@
|
||||
local activeSkillCheck = false
|
||||
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,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
function skillCheck(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
|
||||
loadTextureDict("timerbars")
|
||||
local successes = 0
|
||||
local barsRequired = 3
|
||||
|
||||
for bar = 1, barsRequired do
|
||||
debugPrint("^6Bridge^7: ^2Starting Bar ^3"..bar.."^7/^3"..barsRequired.."^7")
|
||||
activeSkillCheck = true
|
||||
local width, height = 0.2, 0.01
|
||||
local x, y = 0.5, 0.8
|
||||
|
||||
-- Random highlighted zone
|
||||
local highlightSize = math.random(10, 20) / 100
|
||||
local highlightStart = math.random(10, 50) / 100
|
||||
local highlightEnd = highlightStart + highlightSize
|
||||
local highlightAlpha = 0
|
||||
local cursorPos = 0.0
|
||||
local cursorSpeed = 0.025
|
||||
local movingRight = true
|
||||
|
||||
while activeSkillCheck do
|
||||
Wait(0)
|
||||
makeInstructionalButtons({
|
||||
{ keys = { 177 }, text = "Exit" },
|
||||
{ keys = { 38 }, text = "Confirm" },
|
||||
})
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect
|
||||
highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha
|
||||
|
||||
-- Draw highlighted zone (success area) with pulsing effect
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha )
|
||||
-- Draw moving cursor
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
|
||||
-- Move cursor
|
||||
if movingRight then
|
||||
cursorPos += cursorSpeed
|
||||
if cursorPos >= 1.0 then movingRight = false end
|
||||
else
|
||||
cursorPos -= cursorSpeed
|
||||
if cursorPos <= 0.0 then movingRight = true end
|
||||
end
|
||||
|
||||
if IsControlJustPressed(0, 177) then -- Backspace to cancel
|
||||
local displayTime = GetGameTimer() + 2000
|
||||
PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1)
|
||||
while GetGameTimer() < displayTime do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
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)
|
||||
|
||||
drawSuccessText(x, y, "Failed", 228, 52, 52)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check for keypress (E)
|
||||
if IsControlJustPressed(0, 38) then
|
||||
activeSkillCheck = false
|
||||
result = cursorPos >= highlightStart and cursorPos <= highlightEnd
|
||||
if result then
|
||||
PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true)
|
||||
else
|
||||
PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1)
|
||||
end
|
||||
local displayTime = GetGameTimer() + 2000
|
||||
while GetGameTimer() < displayTime do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
-- Draw highlighted zone
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180)
|
||||
|
||||
-- Draw stationary cursor at result position
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
-- Display result text
|
||||
drawSuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52)
|
||||
end
|
||||
if result then
|
||||
successes += 1
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
activeSkillCheck = false
|
||||
debugPrint("^6Bridge^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7")
|
||||
return successes == barsRequired
|
||||
else
|
||||
result = true
|
||||
if Config.System.skillCheck then
|
||||
return skillCheckFunc[Config.System.skillCheck].start(data)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function drawSuccessText(x, y, text, r, g, b)
|
||||
SetTextFont(8)
|
||||
SetTextScale(0.45, 0.45)
|
||||
SetTextColour(r, g, b, 255)
|
||||
SetTextDropshadow(0, 0, 0, 0, 255)
|
||||
SetTextEdge(2, 0, 0, 0, 150)
|
||||
SetTextDropShadow()
|
||||
SetTextOutline()
|
||||
SetTextCentre(true)
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(true)
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(text)
|
||||
DrawText(x, y + 0.03)
|
||||
end
|
||||
|
||||
function createScaleBars(x, y, width, height)
|
||||
-- Draw background box
|
||||
DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255)
|
||||
DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255)
|
||||
-- Draw full bar (dark background)
|
||||
DrawRect(x, y, width, height, 100, 100, 100, 255)
|
||||
return true
|
||||
end
|
||||
@@ -11,6 +11,190 @@
|
||||
• 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,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
--- 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.
|
||||
@@ -20,56 +204,19 @@
|
||||
--- print("Police account balance: $"..balance)
|
||||
--- ```
|
||||
function getSocietyAccount(society)
|
||||
local bankScript, amount = "", 0
|
||||
local amount = 0
|
||||
if society == nil or society == "none" then return amount end
|
||||
|
||||
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)
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
print("^1Error^7: ^3GetSocietyAccount^7: ^2No supported banking script found")
|
||||
|
||||
return amount
|
||||
end
|
||||
@@ -82,52 +229,18 @@ end
|
||||
--- chargeSociety("police", 1000)
|
||||
--- ```
|
||||
function chargeSociety(society, amount)
|
||||
local bankScript, newAmount = "", 0
|
||||
|
||||
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)
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
print("^1Error^7: ^3ChargeSociety^7: ^2No supported banking script found")
|
||||
end
|
||||
|
||||
--- Adds funds to a society's bank account.
|
||||
@@ -138,66 +251,26 @@ end
|
||||
--- fundSociety("police", 500)
|
||||
--- ```
|
||||
function fundSociety(society, amount)
|
||||
local bankScript, newAmount, success = "", 0, false
|
||||
|
||||
|
||||
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)
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
print("^1Error^7: ^3FundSociety^7: ^2No supported banking script found")
|
||||
end
|
||||
|
||||
|
||||
-- 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
|
||||
if isServer() then
|
||||
createCallback(getScript()..":server:getAccount", function(source, society)
|
||||
if society then
|
||||
local getMoney = getSocietyAccount(society)
|
||||
return getMoney
|
||||
end
|
||||
return 0
|
||||
end)
|
||||
end
|
||||
@@ -1,591 +0,0 @@
|
||||
--[[
|
||||
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(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("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(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)
|
||||
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- 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(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(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
|
||||
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
|
||||
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, 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)
|
||||
if isStarted(OXInv) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3OX ^2Stash^7:", name, label, owner or nil)
|
||||
exports[OXInv]:RegisterStash(name, label, slots or 50, weight or 4000000, owner or nil)
|
||||
|
||||
--elseif isStarted(QSInv) then
|
||||
-- debugPrint("^6Bridge^7: ^2Registering ^3QS ^2Stash^7:", name, label)
|
||||
-- exports[QSInv]:RegisterStash(name, label, slots or 50, weight or 4000000)
|
||||
|
||||
--elseif isStarted(CoreInv) then
|
||||
-- debugPrint("^6Bridge^7: ^2Registering ^3CoreInv ^2Stash^7:", name, label)
|
||||
-- exports[CoreInv]:openHolder(nil, name, 'stash', nil, nil, false, nil)
|
||||
|
||||
elseif isStarted(OrigenInv) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3OrigenInv ^2Stash^7:", name, label)
|
||||
exports["origen_inventory"]:registerStash(name, label, slots or 50, weight or 4000000)
|
||||
|
||||
elseif isStarted(TgiannInv) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3TgiannInv ^2Stash^7:", name, label)
|
||||
exports[TgiannInv]:RegisterStash(name, label, slots or 50, weight or 4000000)
|
||||
|
||||
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)
|
||||
@@ -17,34 +17,206 @@
|
||||
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
|
||||
-------------------------------------------------------------
|
||||
---
|
||||
local KEY_TABLE = { 38, 29, 47, 23, 45, }
|
||||
|
||||
-- Mapping of key codes to human-readable key names.
|
||||
local Keys = {
|
||||
[322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5",
|
||||
[167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10",
|
||||
[243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5",
|
||||
[159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=",
|
||||
[177] = "BACKSPACE", [37] = "TAB",
|
||||
[44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y",
|
||||
[303] = "U", [199] = "P",
|
||||
[39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS",
|
||||
[34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G",
|
||||
[74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT",
|
||||
[20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N",
|
||||
[244] = "M", [82] = ",", [81] = "."
|
||||
}
|
||||
|
||||
-- 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
|
||||
-------------------------------------------------------------
|
||||
@@ -80,60 +252,22 @@ function createEntityTarget(entity, opts, dist)
|
||||
-- Store the target entity for later cleanup.
|
||||
targetEntities[#targetEntities + 1] = entity
|
||||
|
||||
-- Fallback: Use DrawText3D if targeting systems are disabled or unavailable.
|
||||
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
|
||||
local entityCoords = GetEntityCoords(entity)
|
||||
debugPrint("^6Bridge^7: ^2Creating new ^3Entity^2 target with DrawText for entity ^7"..entity)
|
||||
local existingTarget = nil
|
||||
for _, target in pairs(TextTargets) do
|
||||
if #(target.coords - entityCoords) < 0.01 then
|
||||
existingTarget = target
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if existingTarget then
|
||||
for i = 1, #opts do
|
||||
local key = KEY_TABLE[#existingTarget.options + i]
|
||||
opts[i].key = key
|
||||
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
|
||||
existingTarget.options[#existingTarget.options + 1] = opts[i]
|
||||
end
|
||||
updateCachedText(existingTarget)
|
||||
else
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
TextTargets[entity] = {
|
||||
coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z),
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
end
|
||||
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
|
||||
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)
|
||||
-- if force target off, use jim_bridge built in target functions
|
||||
if Config.System.DontUseTarget then
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -190,75 +324,76 @@ end
|
||||
---}, 2.0)
|
||||
---```
|
||||
function createBoxTarget(data, opts, dist)
|
||||
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])
|
||||
local existingTarget = nil
|
||||
for _, target in pairs(TextTargets) do
|
||||
if #(target.coords - data[2]) < 0.01 then
|
||||
existingTarget = target
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if existingTarget then
|
||||
for i = 1, #opts do
|
||||
local key = KEY_TABLE[#existingTarget.options + i]
|
||||
opts[i].key = key
|
||||
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
|
||||
existingTarget.options[#existingTarget.options + 1] = opts[i]
|
||||
end
|
||||
updateCachedText(existingTarget)
|
||||
else
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
TextTargets[data[1]] = {
|
||||
coords = data[2],
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
end
|
||||
return data[1]
|
||||
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
|
||||
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
|
||||
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]
|
||||
-- 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(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
|
||||
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
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Circle Zone Target Creation
|
||||
-------------------------------------------------------------
|
||||
@@ -296,68 +431,25 @@ 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 ^3Circle ^2target with ^6DrawText ^2for zone ^7"..data[1])
|
||||
local existingTarget = nil
|
||||
for _, target in pairs(TextTargets) do
|
||||
if #(target.coords - data[2]) < 0.01 then
|
||||
existingTarget = target
|
||||
break
|
||||
debugPrint("^6Bridge^7: ^2Creating new ^3Sphere ^2target with ^6jim_bridge ^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
|
||||
end
|
||||
end
|
||||
|
||||
if existingTarget then
|
||||
for i = 1, #opts do
|
||||
local key = KEY_TABLE[#existingTarget.options + i]
|
||||
opts[i].key = key
|
||||
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
|
||||
existingTarget.options[#existingTarget.options + 1] = opts[i]
|
||||
end
|
||||
updateCachedText(existingTarget)
|
||||
else
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
TextTargets[data[1]] = {
|
||||
coords = data[2],
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
end
|
||||
return data[1]
|
||||
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
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -386,51 +478,24 @@ end
|
||||
---}, 2.0)
|
||||
---```
|
||||
function createModelTarget(models, opts, dist)
|
||||
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
|
||||
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~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
|
||||
local keyStr = ""
|
||||
for i, m in ipairs(models) do
|
||||
keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "")
|
||||
end
|
||||
local targetKey = "model_" .. keyStr
|
||||
|
||||
TextTargets[targetKey] = {
|
||||
models = models,
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
coords = vec3(0, 0, 0),
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
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)
|
||||
-- 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")
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-------------------------------------------------------------
|
||||
@@ -446,15 +511,20 @@ end
|
||||
--- removeEntityTarget(entityId)
|
||||
--- ```
|
||||
function removeEntityTarget(entity)
|
||||
if isStarted(QBTargetExport) then
|
||||
exports[QBTargetExport]:RemoveTargetEntity(entity)
|
||||
|
||||
if Config.System.DontUseTarget then
|
||||
exports.jim_bridge:removeEntityTarget(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
|
||||
TextTargets[entity] = nil
|
||||
|
||||
-- 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.
|
||||
@@ -467,14 +537,18 @@ end
|
||||
--- removeZoneTarget(targetObject)
|
||||
--- ```
|
||||
function removeZoneTarget(target)
|
||||
if isStarted(QBTargetExport) then
|
||||
exports[QBTargetExport]:RemoveZone(target)
|
||||
|
||||
if Config.System.DontUseTarget then
|
||||
exports.jim_bridge:removeZoneTarget(target)
|
||||
end
|
||||
if isStarted(OXTargetExport) then
|
||||
exports[OXTargetExport]:removeZone(target, true)
|
||||
end
|
||||
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
|
||||
TextTargets[target] = nil
|
||||
|
||||
-- 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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -487,84 +561,19 @@ end
|
||||
--- removeModelTarget(model)
|
||||
--- ```
|
||||
function removeModelTarget(model)
|
||||
if isStarted(QBTargetExport) then
|
||||
exports[QBTargetExport]:RemoveTargetModel(model, "Test")
|
||||
|
||||
if Config.System.DontUseTarget then
|
||||
exports.jim_bridge:removeZoneTarget(model)
|
||||
end
|
||||
if isStarted(OXTargetExport) then
|
||||
exports[OXTargetExport]:removeModel(model, nil)
|
||||
end
|
||||
if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
|
||||
TextTargets[entity] = nil
|
||||
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()
|
||||
local wait = 1000
|
||||
while true do
|
||||
local pedCoords = GetEntityCoords(PlayerPedId())
|
||||
local camCoords = GetGameplayCamCoord()
|
||||
local camRot = GetGameplayCamRot(2)
|
||||
local camForward = RotationToDirection(camRot)
|
||||
local closestTarget, closestDist = nil, math.huge
|
||||
local notificationShown = false
|
||||
local targetEntity = nil
|
||||
-- Update model targets and determine the closest target.
|
||||
for _, target in pairs(TextTargets) do
|
||||
if target.models 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.coords = GetEntityCoords(entity)
|
||||
targetEntity = entity
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local dist = #(pedCoords - target.coords)
|
||||
if dist <= target.dist then
|
||||
local vecToTarget = target.coords - camCoords
|
||||
local normVec = normalizeVector(vecToTarget)
|
||||
local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z
|
||||
if dot > 0.5 and dist < closestDist then
|
||||
closestDist = dist
|
||||
closestTarget = target
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Render targets, listen for key presses and display the help notification.
|
||||
for key, target in pairs(TextTargets) do
|
||||
if #(pedCoords - target.coords) <= target.dist then
|
||||
local isClosest = (target == closestTarget)
|
||||
for i, opt in ipairs(target.options) do
|
||||
if IsControlJustPressed(0, opt.key) and isClosest then
|
||||
if opt.onSelect then opt.onSelect(targetEntity) end
|
||||
if opt.action then opt.action(targetEntity) end
|
||||
end
|
||||
end
|
||||
|
||||
notificationShown = true
|
||||
ShowFloatingHelpNotification(vec3(target.coords.x, target.coords.y, target.coords.z + 0.7), target.text)
|
||||
end
|
||||
end
|
||||
|
||||
-- If no notification was drawn this frame, clear help messages.
|
||||
if notificationShown then
|
||||
wait = 0
|
||||
else
|
||||
ClearAllHelpMessages()
|
||||
wait = 1000
|
||||
end
|
||||
|
||||
Wait(wait)
|
||||
-- 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
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function ShowFloatingHelpNotification(coord, text, highlight)
|
||||
@@ -575,16 +584,11 @@ 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
|
||||
-------------------------------------------------------------
|
||||
|
||||
-- When the current resource stops, remove all targets.
|
||||
onResourceStop(function()
|
||||
local function CleanupTargets()
|
||||
-- Remove entity targets.
|
||||
for i = 1, #targetEntities do
|
||||
if isStarted(OXTargetExport) then
|
||||
@@ -598,7 +602,7 @@ onResourceStop(function()
|
||||
if isStarted(OXTargetExport) then
|
||||
exports[OXTargetExport]:removeZone(boxTargets[i], true)
|
||||
elseif isStarted(QBTargetExport) then
|
||||
exports[QBTargetExport]:RemoveZone(boxTargets[i].name)
|
||||
exports[QBTargetExport]:RemoveZone(boxTargets[i])
|
||||
end
|
||||
end
|
||||
-- Remove circle zone targets.
|
||||
@@ -606,7 +610,16 @@ onResourceStop(function()
|
||||
if isStarted(OXTargetExport) then
|
||||
exports[OXTargetExport]:removeZone(circleTargets[i], true)
|
||||
elseif isStarted(QBTargetExport) then
|
||||
exports[QBTargetExport]:RemoveZone(circleTargets[i].name)
|
||||
exports[QBTargetExport]:RemoveZone(circleTargets[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
onPlayerUnload(function()
|
||||
CleanupTargets()
|
||||
end)
|
||||
|
||||
-- When the current resource stops, remove all targets.
|
||||
onResourceStop(function()
|
||||
CleanupTargets()
|
||||
end, true)
|
||||
@@ -53,10 +53,11 @@ function searchCar(vehicle)
|
||||
"Military", --20
|
||||
"Commercial", --21
|
||||
"Trains", --22
|
||||
"Open Wheel", --23
|
||||
}
|
||||
if Vehicles then
|
||||
for k, v in pairs(Vehicles) do
|
||||
if tonumber(v.hash) == model or GetHashKey(v.hash) == model or GetHashKey(v.model) == model then
|
||||
if tonumber(v.hash) == model or joaat(v.hash) == model or joaat(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
|
||||
@@ -104,16 +105,28 @@ end
|
||||
function getVehicleProperties(vehicle)
|
||||
if not vehicle then return nil 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]")
|
||||
|
||||
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]")
|
||||
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
|
||||
return properties
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Sets the properties of a given vehicle if changes are detected.
|
||||
@@ -227,7 +240,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: ^3pushVehicle^7: ^2Requesting network control of vehicle^7.")
|
||||
debugPrint("^6Bridge^7: ^3pushEnt^7: ^2Requesting network control of vehicle^7.")
|
||||
NetworkRequestControlOfEntity(entity)
|
||||
local timeout = 2000
|
||||
while timeout > 0 and not NetworkHasControlOfEntity(entity) do
|
||||
@@ -235,13 +248,13 @@ function pushVehicle(entity)
|
||||
timeout = timeout - 100
|
||||
end
|
||||
if NetworkHasControlOfEntity(entity) then
|
||||
debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Network now has control of the entity^7.")
|
||||
debugPrint("^6Bridge^7: ^3pushEnt^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: ^3pushVehicle^7: ^2Setting vehicle as a ^7'^2mission^7' ^2entity^7.")
|
||||
debugPrint("^6Bridge^7: ^3pushEnt^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
|
||||
@@ -249,18 +262,23 @@ function pushVehicle(entity)
|
||||
timeout = timeout - 100
|
||||
end
|
||||
if IsEntityAMissionEntity(entity) then
|
||||
debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Vehicle is a ^7'^2mission^7'^2 entity^7.")
|
||||
debugPrint("^6Bridge^7: ^3pushEnt^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|number closestVehicle|closestDistance The closest vehicle entity and its distance.
|
||||
--- @return number closestVehicle The closest vehicle entity and its distance.
|
||||
--- @return number closestDistance The distance of the closest vehicle.
|
||||
---
|
||||
--- @usage
|
||||
--- ```lua
|
||||
@@ -270,9 +288,11 @@ 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
|
||||
@@ -289,7 +309,7 @@ function getClosestVehicle(coords, src)
|
||||
|
||||
for i = 1, #vehicles, 1 do
|
||||
local vehicleCoords = GetEntityCoords(vehicles[i])
|
||||
local distance = #(vehicleCoords - coords)
|
||||
local distance = #(vehicleCoords - coords.xyz)
|
||||
|
||||
if closestDistance == -1 or distance < closestDistance then
|
||||
closestDistance = distance
|
||||
@@ -298,4 +318,62 @@ 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
|
||||
694
shared/warmenu.lua
Normal file
694
shared/warmenu.lua
Normal file
@@ -0,0 +1,694 @@
|
||||
WarMenu = { }
|
||||
WarMenu.__index = WarMenu
|
||||
|
||||
if not isServer() then
|
||||
|
||||
-- Deprecated
|
||||
WarMenu.debug = false
|
||||
function WarMenu.SetDebugEnabled(enabled)
|
||||
end
|
||||
function WarMenu.IsDebugEnabled()
|
||||
return false
|
||||
end
|
||||
---
|
||||
|
||||
local menus = { }
|
||||
local keys = { down = 187, scrollDown = 242, up = 188, scrollUp = 241, left = 189, right = 190, select = 191, accept = 237, back = 194, cancel = 238 }
|
||||
|
||||
local skipInputNextFrame = true
|
||||
local optionCount = 0
|
||||
|
||||
local currentKey = nil
|
||||
local currentMenu = nil
|
||||
|
||||
local toolTipWidth = 0.153
|
||||
|
||||
local spriteWidth = 0.027
|
||||
local spriteHeight = spriteWidth * GetAspectRatio()
|
||||
|
||||
local titleHeight = 0.101
|
||||
local titleYOffset = 0.021
|
||||
local titleFont = 1
|
||||
local titleScale = 1.0
|
||||
|
||||
local buttonHeight = 0.038
|
||||
local buttonFont = 0
|
||||
local buttonScale = 0.365
|
||||
local buttonTextXOffset = 0.005
|
||||
local buttonTextYOffset = 0.005
|
||||
local buttonSpriteXOffset = 0.002
|
||||
local buttonSpriteYOffset = 0.005
|
||||
|
||||
local defaultStyle = {
|
||||
x = 0.0175,
|
||||
y = 0.025,
|
||||
width = 0.23,
|
||||
maxOptionCountOnScreen = 10,
|
||||
titleVisible = true,
|
||||
titleColor = { 0, 0, 0, 255 },
|
||||
titleBackgroundColor = { 245, 127, 23, 255 },
|
||||
titleBackgroundSprite = nil,
|
||||
subTitleColor = { 245, 127, 23, 255 },
|
||||
textColor = { 254, 254, 254, 255 },
|
||||
subTextColor = { 189, 189, 189, 255 },
|
||||
focusTextColor = { 0, 0, 0, 255 },
|
||||
focusColor = { 245, 245, 245, 255 },
|
||||
backgroundColor = { 0, 0, 0, 160 },
|
||||
subTitleBackgroundColor = { 0, 0, 0, 255 },
|
||||
buttonPressedSound = { name = 'SELECT', set = 'HUD_FRONTEND_DEFAULT_SOUNDSET' }, --https://pastebin.com/0neZdsZ5
|
||||
}
|
||||
|
||||
local function IsNavigatedDown()
|
||||
return IsControlJustReleased(2, keys.down) or IsControlJustReleased(2, keys.scrollDown)
|
||||
end
|
||||
|
||||
local function IsNavigatedUp()
|
||||
return IsControlJustReleased(2, keys.up) or IsControlJustReleased(2, keys.scrollUp)
|
||||
end
|
||||
|
||||
local function IsSelectedPressed()
|
||||
return IsControlJustReleased(2, keys.select) or IsControlJustReleased(2, keys.accept)
|
||||
end
|
||||
|
||||
local function IsBackPressed()
|
||||
return IsControlJustReleased(2, keys.back) or IsControlJustReleased(2, keys.cancel)
|
||||
end
|
||||
|
||||
local function setMenuProperty(id, property, value)
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
|
||||
local menu = menus[id]
|
||||
if menu then
|
||||
menu[property] = value
|
||||
end
|
||||
end
|
||||
|
||||
local function setStyleProperty(id, property, value)
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
|
||||
local menu = menus[id]
|
||||
|
||||
if menu then
|
||||
if not menu.overrideStyle then
|
||||
menu.overrideStyle = { }
|
||||
end
|
||||
|
||||
menu.overrideStyle[property] = value
|
||||
end
|
||||
end
|
||||
|
||||
local function getStyleProperty(property, menu)
|
||||
menu = menu or currentMenu
|
||||
|
||||
if menu.overrideStyle then
|
||||
local value = menu.overrideStyle[property]
|
||||
if value ~= nil then
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
return menu.style and menu.style[property] or defaultStyle[property]
|
||||
end
|
||||
|
||||
local function getTitleHeight()
|
||||
return getStyleProperty('titleVisible') and titleHeight or 0
|
||||
end
|
||||
|
||||
local function copyTable(t)
|
||||
if type(t) ~= 'table' then
|
||||
return t
|
||||
end
|
||||
|
||||
local result = { }
|
||||
for k, v in pairs(t) do
|
||||
result[k] = copyTable(v)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function setMenuVisible(id, visible, holdCurrentOption)
|
||||
if currentMenu then
|
||||
if visible then
|
||||
if currentMenu.id == id then
|
||||
return
|
||||
end
|
||||
else
|
||||
if currentMenu.id ~= id then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if visible then
|
||||
local menu = menus[id]
|
||||
|
||||
if not currentMenu then
|
||||
menu.currentOption = 1
|
||||
else
|
||||
if not holdCurrentOption then
|
||||
menus[currentMenu.id].currentOption = 1
|
||||
end
|
||||
end
|
||||
|
||||
currentMenu = menu
|
||||
skipInputNextFrame = true
|
||||
|
||||
SetUserRadioControlEnabled(false)
|
||||
HudWeaponWheelIgnoreControlInput(true)
|
||||
else
|
||||
HudWeaponWheelIgnoreControlInput(false)
|
||||
SetUserRadioControlEnabled(true)
|
||||
|
||||
currentMenu = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function setTextParams(font, color, scale, center, shadow, alignRight, wrapFrom, wrapTo)
|
||||
SetTextFont(font)
|
||||
SetTextColour(color[1], color[2], color[3], color[4] or 255)
|
||||
SetTextScale(scale, scale)
|
||||
|
||||
if shadow then
|
||||
SetTextDropShadow()
|
||||
end
|
||||
|
||||
if center then
|
||||
SetTextCentre(true)
|
||||
elseif alignRight then
|
||||
SetTextRightJustify(true)
|
||||
end
|
||||
|
||||
if not wrapFrom or not wrapTo then
|
||||
wrapFrom = wrapFrom or getStyleProperty('x')
|
||||
wrapTo = wrapTo or getStyleProperty('x') + getStyleProperty('width') - buttonTextXOffset
|
||||
end
|
||||
|
||||
SetTextWrap(wrapFrom, wrapTo)
|
||||
end
|
||||
|
||||
local function getLinesCount(text, x, y)
|
||||
BeginTextCommandLineCount('TWOSTRINGS')
|
||||
AddTextComponentString(tostring(text))
|
||||
return EndTextCommandGetLineCount(x, y)
|
||||
end
|
||||
|
||||
local function drawText(text, x, y)
|
||||
BeginTextCommandDisplayText('TWOSTRINGS')
|
||||
AddTextComponentString(tostring(text))
|
||||
EndTextCommandDisplayText(x, y)
|
||||
end
|
||||
|
||||
local function drawRect(x, y, width, height, color)
|
||||
DrawRect(x, y, width, height, color[1], color[2], color[3], color[4] or 255)
|
||||
end
|
||||
|
||||
local function getCurrentIndex()
|
||||
if currentMenu.currentOption <= getStyleProperty('maxOptionCountOnScreen') and optionCount <= getStyleProperty('maxOptionCountOnScreen') then
|
||||
return optionCount
|
||||
elseif optionCount > currentMenu.currentOption - getStyleProperty('maxOptionCountOnScreen') and optionCount <= currentMenu.currentOption then
|
||||
return optionCount - (currentMenu.currentOption - getStyleProperty('maxOptionCountOnScreen'))
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function drawTitle()
|
||||
if not getStyleProperty('titleVisible') then
|
||||
return
|
||||
end
|
||||
|
||||
local x = getStyleProperty('x') + getStyleProperty('width') / 2
|
||||
local y = getStyleProperty('y') + titleHeight / 2
|
||||
|
||||
if getStyleProperty('titleBackgroundSprite') then
|
||||
DrawSprite(getStyleProperty('titleBackgroundSprite').dict, getStyleProperty('titleBackgroundSprite').name, x, y, getStyleProperty('width'), titleHeight, 0., 255, 255, 255, 255)
|
||||
else
|
||||
drawRect(x, y, getStyleProperty('width'), titleHeight, getStyleProperty('titleBackgroundColor'))
|
||||
end
|
||||
|
||||
if currentMenu.title then
|
||||
setTextParams(titleFont, getStyleProperty('titleColor'), titleScale, true)
|
||||
drawText(currentMenu.title, x, y - titleHeight / 2 + titleYOffset)
|
||||
end
|
||||
end
|
||||
|
||||
local function drawSubTitle()
|
||||
local x = getStyleProperty('x') + getStyleProperty('width') / 2
|
||||
local y = getStyleProperty('y') + getTitleHeight() + buttonHeight / 2
|
||||
|
||||
drawRect(x, y, getStyleProperty('width'), buttonHeight, getStyleProperty('subTitleBackgroundColor'))
|
||||
|
||||
setTextParams(buttonFont, getStyleProperty('subTitleColor'), buttonScale, false)
|
||||
drawText(currentMenu.subTitle, getStyleProperty('x') + buttonTextXOffset, y - buttonHeight / 2 + buttonTextYOffset)
|
||||
|
||||
if optionCount > getStyleProperty('maxOptionCountOnScreen') then
|
||||
setTextParams(buttonFont, getStyleProperty('subTitleColor'), buttonScale, false, false, true)
|
||||
drawText(tostring(currentMenu.currentOption)..' / '..tostring(optionCount), getStyleProperty('x') + getStyleProperty('width'), y - buttonHeight / 2 + buttonTextYOffset)
|
||||
end
|
||||
end
|
||||
|
||||
local function drawButton(text, subText)
|
||||
local currentIndex = getCurrentIndex()
|
||||
if not currentIndex then
|
||||
return
|
||||
end
|
||||
|
||||
local backgroundColor = nil
|
||||
local textColor = nil
|
||||
local subTextColor = nil
|
||||
local shadow = false
|
||||
|
||||
if currentMenu.currentOption == optionCount then
|
||||
backgroundColor = getStyleProperty('focusColor')
|
||||
textColor = getStyleProperty('focusTextColor')
|
||||
subTextColor = getStyleProperty('focusTextColor')
|
||||
else
|
||||
backgroundColor = getStyleProperty('backgroundColor')
|
||||
textColor = getStyleProperty('textColor')
|
||||
subTextColor = getStyleProperty('subTextColor')
|
||||
shadow = true
|
||||
end
|
||||
|
||||
local x = getStyleProperty('x') + getStyleProperty('width') / 2
|
||||
local y = getStyleProperty('y') + getTitleHeight() + buttonHeight + (buttonHeight * currentIndex) - buttonHeight / 2
|
||||
|
||||
drawRect(x, y, getStyleProperty('width'), buttonHeight, backgroundColor)
|
||||
|
||||
setTextParams(buttonFont, textColor, buttonScale, false, shadow)
|
||||
drawText(text, getStyleProperty('x') + buttonTextXOffset, y - (buttonHeight / 2) + buttonTextYOffset)
|
||||
|
||||
if subText then
|
||||
setTextParams(buttonFont, subTextColor, buttonScale, false, shadow, true)
|
||||
drawText(subText, getStyleProperty('x') + buttonTextXOffset, y - buttonHeight / 2 + buttonTextYOffset)
|
||||
end
|
||||
end
|
||||
|
||||
function WarMenu.CreateMenu(id, title, subTitle, style)
|
||||
-- Default settings
|
||||
local menu = { }
|
||||
|
||||
-- Members
|
||||
menu.id = id
|
||||
menu.previousMenu = nil
|
||||
menu.currentOption = 1
|
||||
menu.title = title
|
||||
menu.subTitle = subTitle and string.upper(subTitle) or 'INTERACTION MENU'
|
||||
|
||||
-- Style
|
||||
if style then
|
||||
menu.style = style
|
||||
end
|
||||
|
||||
menus[id] = menu
|
||||
end
|
||||
|
||||
function WarMenu.CreateSubMenu(id, parent, subTitle, style)
|
||||
local parentMenu = menus[parent]
|
||||
if not parentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
WarMenu.CreateMenu(id, parentMenu.title, subTitle and string.upper(subTitle) or parentMenu.subTitle)
|
||||
|
||||
local menu = menus[id]
|
||||
|
||||
menu.previousMenu = parent
|
||||
|
||||
if parentMenu.overrideStyle then
|
||||
menu.overrideStyle = copyTable(parentMenu.overrideStyle)
|
||||
end
|
||||
|
||||
if style then
|
||||
menu.style = style
|
||||
elseif parentMenu.style then
|
||||
menu.style = copyTable(parentMenu.style)
|
||||
end
|
||||
end
|
||||
|
||||
function WarMenu.CurrentMenu()
|
||||
return currentMenu and currentMenu.id or nil
|
||||
end
|
||||
|
||||
function WarMenu.OpenMenu(id)
|
||||
if id and menus[id] then
|
||||
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
|
||||
setMenuVisible(id, true, true)
|
||||
end
|
||||
end
|
||||
|
||||
function WarMenu.IsMenuOpened(id)
|
||||
return currentMenu and currentMenu.id == id
|
||||
end
|
||||
WarMenu.Begin = WarMenu.IsMenuOpened
|
||||
|
||||
function WarMenu.IsAnyMenuOpened()
|
||||
return currentMenu ~= nil
|
||||
end
|
||||
|
||||
function WarMenu.IsMenuAboutToBeClosed()
|
||||
return false
|
||||
end
|
||||
|
||||
function WarMenu.CloseMenu()
|
||||
if currentMenu then
|
||||
setMenuVisible(currentMenu.id, false)
|
||||
optionCount = 0
|
||||
currentKey = nil
|
||||
PlaySoundFrontend(-1, 'QUIT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
|
||||
end
|
||||
end
|
||||
|
||||
function WarMenu.ToolTip(text, width, flipHorizontal)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
local currentIndex = getCurrentIndex()
|
||||
if not currentIndex then
|
||||
return
|
||||
end
|
||||
|
||||
width = width or toolTipWidth
|
||||
|
||||
local x = nil
|
||||
if not flipHorizontal then
|
||||
x = getStyleProperty('x') + getStyleProperty('width') + width / 2 + buttonTextXOffset
|
||||
else
|
||||
x = getStyleProperty('x') - width / 2 - buttonTextXOffset
|
||||
end
|
||||
|
||||
local textX = x - (width / 2) + buttonTextXOffset
|
||||
setTextParams(buttonFont, getStyleProperty('textColor'), buttonScale, false, true, false, textX, textX + width - (buttonTextYOffset * 2))
|
||||
local linesCount = getLinesCount(text, textX, getStyleProperty('y'))
|
||||
|
||||
local height = GetTextScaleHeight(buttonScale, buttonFont) * (linesCount + 1) + buttonTextYOffset
|
||||
local y = getStyleProperty('y') + getTitleHeight() + (buttonHeight * currentIndex) + height / 2
|
||||
|
||||
drawRect(x, y, width, height, getStyleProperty('backgroundColor'))
|
||||
|
||||
y = y - (height / 2) + buttonTextYOffset
|
||||
drawText(text, textX, y)
|
||||
end
|
||||
|
||||
function WarMenu.Button(text, subText)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
optionCount = optionCount + 1
|
||||
|
||||
drawButton(text, subText)
|
||||
|
||||
local pressed = false
|
||||
|
||||
if currentMenu.currentOption == optionCount then
|
||||
if currentKey == keys.select then
|
||||
pressed = true
|
||||
PlaySoundFrontend(-1, getStyleProperty('buttonPressedSound').name, getStyleProperty('buttonPressedSound').set, true)
|
||||
elseif currentKey == keys.left or currentKey == keys.right then
|
||||
PlaySoundFrontend(-1, 'NAV_UP_DOWN', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
|
||||
end
|
||||
end
|
||||
|
||||
return pressed
|
||||
end
|
||||
|
||||
function WarMenu.SpriteButton(text, dict, name, r, g, b, a)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
local pressed = WarMenu.Button(text)
|
||||
|
||||
local currentIndex = getCurrentIndex()
|
||||
if not currentIndex then
|
||||
return
|
||||
end
|
||||
|
||||
if not HasStreamedTextureDictLoaded(dict) then
|
||||
RequestStreamedTextureDict(dict)
|
||||
end
|
||||
DrawSprite(dict, name, getStyleProperty('x') + getStyleProperty('width') - spriteWidth / 2 - buttonSpriteXOffset, getStyleProperty('y') + getTitleHeight() + buttonHeight + (buttonHeight * currentIndex) - spriteHeight / 2 + buttonSpriteYOffset, spriteWidth, spriteHeight, 0., r or 255, g or 255, b or 255, a or 255)
|
||||
|
||||
return pressed
|
||||
end
|
||||
|
||||
function WarMenu.InputButton(text, windowTitleEntry, defaultText, maxLength, subText)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
local pressed = WarMenu.Button(text, subText)
|
||||
local inputText = nil
|
||||
|
||||
if pressed then
|
||||
DisplayOnscreenKeyboard(1, windowTitleEntry or 'FMMC_MPM_NA', '', defaultText or '', '', '', '', maxLength or 255)
|
||||
|
||||
while true do
|
||||
DisableAllControlActions(0)
|
||||
|
||||
local status = UpdateOnscreenKeyboard()
|
||||
if status == 2 then
|
||||
break
|
||||
elseif status == 1 then
|
||||
inputText = GetOnscreenKeyboardResult()
|
||||
break
|
||||
end
|
||||
|
||||
Citizen.Wait(0)
|
||||
end
|
||||
end
|
||||
|
||||
return pressed, inputText
|
||||
end
|
||||
|
||||
function WarMenu.MenuButton(text, id, subText)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
local pressed = WarMenu.Button(text, subText)
|
||||
|
||||
if pressed then
|
||||
currentMenu.currentOption = optionCount
|
||||
setMenuVisible(currentMenu.id, false)
|
||||
setMenuVisible(id, true, true)
|
||||
end
|
||||
|
||||
return pressed
|
||||
end
|
||||
|
||||
function WarMenu.CheckBox(text, checked, callback)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
local name = nil
|
||||
if currentMenu.currentOption == optionCount + 1 then
|
||||
name = checked and 'shop_box_tickb' or 'shop_box_blankb'
|
||||
else
|
||||
name = checked and 'shop_box_tick' or 'shop_box_blank'
|
||||
end
|
||||
|
||||
local pressed = WarMenu.SpriteButton(text, 'commonmenu', name)
|
||||
|
||||
if pressed then
|
||||
checked = not checked
|
||||
if callback then callback(checked) end
|
||||
end
|
||||
|
||||
return pressed
|
||||
end
|
||||
|
||||
function WarMenu.ComboBox(text, items, currentIndex, selectedIndex, callback)
|
||||
if not currentMenu then
|
||||
return
|
||||
end
|
||||
|
||||
local itemsCount = #items
|
||||
local selectedItem = items[currentIndex]
|
||||
local isCurrent = currentMenu.currentOption == optionCount + 1
|
||||
selectedIndex = selectedIndex or currentIndex
|
||||
|
||||
if itemsCount > 1 and isCurrent then
|
||||
selectedItem = '← '..tostring(selectedItem)..' →'
|
||||
end
|
||||
|
||||
local pressed = WarMenu.Button(text, selectedItem)
|
||||
|
||||
if pressed then
|
||||
selectedIndex = currentIndex
|
||||
elseif isCurrent then
|
||||
if currentKey == keys.left then
|
||||
if currentIndex > 1 then currentIndex = currentIndex - 1 else currentIndex = itemsCount end
|
||||
elseif currentKey == keys.right then
|
||||
if currentIndex < itemsCount then currentIndex = currentIndex + 1 else currentIndex = 1 end
|
||||
end
|
||||
end
|
||||
|
||||
if callback then callback(currentIndex, selectedIndex) end
|
||||
return pressed, currentIndex
|
||||
end
|
||||
|
||||
function WarMenu.Display()
|
||||
if currentMenu then
|
||||
if not IsPauseMenuActive() then
|
||||
ClearAllHelpMessages()
|
||||
HudWeaponWheelIgnoreSelection()
|
||||
DisablePlayerFiring(PlayerId(), true)
|
||||
DisableControlAction(0, 25, true)
|
||||
|
||||
drawTitle()
|
||||
drawSubTitle()
|
||||
|
||||
currentKey = nil
|
||||
|
||||
if skipInputNextFrame then
|
||||
skipInputNextFrame = false
|
||||
else
|
||||
if IsNavigatedDown() then
|
||||
PlaySoundFrontend(-1, 'NAV_UP_DOWN', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
|
||||
|
||||
if currentMenu.currentOption < optionCount then
|
||||
currentMenu.currentOption = currentMenu.currentOption + 1
|
||||
else
|
||||
currentMenu.currentOption = 1
|
||||
end
|
||||
elseif IsNavigatedUp() then
|
||||
PlaySoundFrontend(-1, 'NAV_UP_DOWN', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
|
||||
|
||||
if currentMenu.currentOption > 1 then
|
||||
currentMenu.currentOption = currentMenu.currentOption - 1
|
||||
else
|
||||
currentMenu.currentOption = optionCount
|
||||
end
|
||||
elseif IsControlJustReleased(2, keys.left) then
|
||||
currentKey = keys.left
|
||||
elseif IsControlJustReleased(2, keys.right) then
|
||||
currentKey = keys.right
|
||||
elseif IsSelectedPressed() then
|
||||
currentKey = keys.select
|
||||
elseif IsBackPressed() then
|
||||
if menus[currentMenu.previousMenu] then
|
||||
setMenuVisible(currentMenu.previousMenu, true)
|
||||
PlaySoundFrontend(-1, 'BACK', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
|
||||
else
|
||||
WarMenu.CloseMenu()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
optionCount = 0
|
||||
end
|
||||
end
|
||||
WarMenu.End = WarMenu.Display
|
||||
|
||||
function WarMenu.CurrentOption()
|
||||
if currentMenu and optionCount ~= 0 then
|
||||
return currentMenu.currentOption
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function WarMenu.IsItemHovered()
|
||||
if not currentMenu or optionCount == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
return currentMenu.currentOption == optionCount
|
||||
end
|
||||
|
||||
function WarMenu.IsItemSelected()
|
||||
return currentKey == keys.select and WarMenu.IsItemHovered()
|
||||
end
|
||||
|
||||
function WarMenu.SetTitle(id, title)
|
||||
setMenuProperty(id, 'title', title)
|
||||
end
|
||||
WarMenu.SetMenuTitle = WarMenu.SetTitle
|
||||
|
||||
function WarMenu.SetSubTitle(id, text)
|
||||
setMenuProperty(id, 'subTitle', string.upper(text))
|
||||
end
|
||||
WarMenu.SetMenuSubTitle = WarMenu.SetSubTitle
|
||||
|
||||
function WarMenu.SetMenuStyle(id, style)
|
||||
setMenuProperty(id, 'style', style)
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuTitleVisible(id, visible)
|
||||
setStyleProperty(id, 'titleVisible', visible)
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuX(id, x)
|
||||
setStyleProperty(id, 'x', x)
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuY(id, y)
|
||||
setStyleProperty(id, 'y', y)
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuWidth(id, width)
|
||||
setStyleProperty(id, 'width', width)
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuMaxOptionCountOnScreen(id, count)
|
||||
setStyleProperty(id, 'maxOptionCountOnScreen', count)
|
||||
end
|
||||
|
||||
function WarMenu.SetTitleColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'titleColor', { r, g, b, a })
|
||||
end
|
||||
WarMenu.SetMenuTitleColor = WarMenu.SetTitleColor
|
||||
|
||||
function WarMenu.SetMenuSubTitleColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'subTitleColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuSubTitleBackgroundColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'subTitleBackgroundColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetTitleBackgroundColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'titleBackgroundColor', { r, g, b, a })
|
||||
end
|
||||
WarMenu.SetMenuTitleBackgroundColor = WarMenu.SetTitleBackgroundColor
|
||||
|
||||
function WarMenu.SetTitleBackgroundSprite(id, dict, name)
|
||||
RequestStreamedTextureDict(dict)
|
||||
setStyleProperty(id, 'titleBackgroundSprite', { dict = dict, name = name })
|
||||
end
|
||||
WarMenu.SetMenuTitleBackgroundSprite = WarMenu.SetTitleBackgroundSprite
|
||||
|
||||
function WarMenu.SetMenuBackgroundColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'backgroundColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuTextColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'textColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuSubTextColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'subTextColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuFocusColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'focusColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuFocusTextColor(id, r, g, b, a)
|
||||
setStyleProperty(id, 'focusTextColor', { r, g, b, a })
|
||||
end
|
||||
|
||||
function WarMenu.SetMenuButtonPressedSound(id, name, set)
|
||||
setStyleProperty(id, 'buttonPressedSound', { name = name, set = set })
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
--- Registers a command with the active command system.
|
||||
--- This function supports multiple command systems (OXLib, qb-core, ESX Legacy).
|
||||
---
|
||||
@@ -15,30 +14,67 @@
|
||||
--- registerCommand("greet", {
|
||||
--- "Greets the player",
|
||||
--- { name = "name", help = "Name of the player to greet" },
|
||||
--- function(source, args) print("Hello, "..args[1].."!") end,
|
||||
--- nil,
|
||||
--- function(source, args) print("Hello, "..args[1].."!") end,
|
||||
--- "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
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3Command^2 with ^7"..OXLibExport, command)
|
||||
lib.addCommand(command, { help = options[1], restricted = options[5] and "group."..options[5] or nil }, options[4])
|
||||
commandResource = OXLibExport
|
||||
lib.addCommand(command,
|
||||
{
|
||||
help = optionTable.helpInfo,
|
||||
params = optionTable.subText,
|
||||
restricted = optionTable.restriction and "group."..optionTable.restriction or nil
|
||||
},
|
||||
optionTable.funct)
|
||||
|
||||
elseif isStarted(QBExport) and not isStarted(QBXExport) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3Command^2 with ^7"..QBExport, command)
|
||||
Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil)
|
||||
commandResource = QBExport
|
||||
Core.Commands.Add(command,
|
||||
optionTable.helpInfo,
|
||||
optionTable.subText,
|
||||
optionTable.argsRequired,
|
||||
optionTable.funct,
|
||||
optionTable.restriction or nil
|
||||
)
|
||||
|
||||
elseif isStarted(RSGExport) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3Command^2 with ^7"..RSGExport, command)
|
||||
Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] or nil)
|
||||
commandResource = RSGExport
|
||||
Core.Commands.Add(command,
|
||||
optionTable.helpInfo,
|
||||
optionTable.subText,
|
||||
optionTable.argsRequired,
|
||||
optionTable.funct,
|
||||
optionTable.restriction or nil
|
||||
)
|
||||
|
||||
elseif isStarted(ESXExport) then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3Command^2 with ^7ESX Legacy", command)
|
||||
ESX.RegisterCommand(command, options[5] or 'admin', function(xPlayer, args, showError)
|
||||
options[4](xPlayer.source, args, showError)
|
||||
end, false, { help = options[1] })
|
||||
commandResource = ESXExport
|
||||
ESX.RegisterCommand(command,
|
||||
optionTable.restriction or 'admin',
|
||||
function(xPlayer, args, showError)
|
||||
optionTable.funct(xPlayer.source, args, showError)
|
||||
end,
|
||||
false,
|
||||
{ help = options[1] }
|
||||
)
|
||||
|
||||
end
|
||||
if commandResource ~= "" then
|
||||
debugPrint("^6Bridge^7: ^2Registering ^3Command^2 with ^4"..commandResource.."^7:", "^7/"..command)
|
||||
else
|
||||
print("^1ERROR^7: ^1Couldn't find supported framework to register command^7:", "/"..command)
|
||||
end
|
||||
end
|
||||
100
starter.lua
100
starter.lua
@@ -9,11 +9,11 @@ Exports = {
|
||||
OXInv = "ox_inventory",
|
||||
QBInv = "qb-inventory",
|
||||
PSInv = "ps-inventory",
|
||||
QSInv = "qs-inventory",
|
||||
CoreInv = "core_inventory",
|
||||
CodeMInv = "codem-inventory",
|
||||
OrigenInv = "origen_inventory",
|
||||
TgiannInv = "tgiann-inventory",
|
||||
JPRInv = "jpr-inventory",
|
||||
|
||||
OXLibExport = "ox_lib",
|
||||
|
||||
@@ -24,19 +24,60 @@ Exports = {
|
||||
|
||||
-- REDM
|
||||
RSGExport = "rsg-core",
|
||||
RSGInv = "rsg-inventory"
|
||||
RSGInv = "rsg-inventory",
|
||||
|
||||
VorpExport = "vorp_core",
|
||||
VorpInv = "vorp_inventory",
|
||||
VorpMenu = "vorp_menu",
|
||||
|
||||
}
|
||||
|
||||
-- Required variables
|
||||
debugMode = Config.System.Debug
|
||||
debugMode = false
|
||||
|
||||
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
|
||||
@@ -45,7 +86,6 @@ 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")
|
||||
@@ -148,7 +188,6 @@ for _, v in pairs({ -- This is a specific load order
|
||||
|
||||
'wrapperfunctions.lua',
|
||||
'polyZone.lua',
|
||||
'inventories.lua',
|
||||
'itemcontrol.lua',
|
||||
'playerfunctions.lua',
|
||||
'metaHandlers.lua',
|
||||
@@ -166,15 +205,16 @@ 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',
|
||||
-- Do version check last
|
||||
'_scriptversioncheck.lua'
|
||||
}) do
|
||||
@@ -184,6 +224,50 @@ for _, v in pairs({ -- This is a specific load order
|
||||
local fileLoader = assert(load(LoadResourceFile('jim_bridge', ('shared/'..v)), ('@@jim_bridge/shared/'..v)))
|
||||
fileLoader()
|
||||
if debugMode then
|
||||
print("^5Success^7: ^2loaded file^7: ^3"..(v):gsub("/", "^7/^3"):gsub("%.lua", "^7.lua").."^7")
|
||||
print("^5CoreLoader^7: ^2loaded file^7: ^3"..(v):gsub("/", "^7/^3"):gsub("%.lua", "^7.lua").."^7")
|
||||
end
|
||||
end
|
||||
|
||||
-- Auto Detection of Old/New QBInv/PSInv versions --
|
||||
|
||||
if isStarted(QBInv) then
|
||||
if isServer() then
|
||||
if not checkExportExists(QBInv, "CreateShop") then
|
||||
print("^6Bridge^7: ^2Can^7'^2t ^2find new ^4"..QBInv.."^2 export^7, ^2forcing ^1QBInvNew ^2to ^1false^7")
|
||||
QBInvNew = false
|
||||
end
|
||||
else
|
||||
if checkExportExists(QBInv, "GetTrunkSize") then
|
||||
print("^6Bridge^7: ^2Found old ^4"..QBInv.."^3 export^7, ^2forcing ^1QBInvNew ^2to ^1false^7")
|
||||
QBInvNew = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if isStarted(PSInv) then
|
||||
if isServer() then
|
||||
if not checkExportExists(PSInv, "CreateShop") then
|
||||
print("^6Bridge^7: ^2Can^7'^2t ^2find new ^4"..PSInv.."^2 export^7, ^2forcing ^1QBInvNew ^2to ^1false^7")
|
||||
QBInvNew = false
|
||||
end
|
||||
else
|
||||
local function versionCompare(v1, v2)
|
||||
local split = function(v) local t = {} for s in v:gmatch("[^.]+") do t[#t+1] = tonumber(s) end return t end
|
||||
local a, b = split(v1), split(v2)
|
||||
for i = 1, math.max(#a, #b) do
|
||||
local ai, bi = a[i] or 0, b[i] or 0
|
||||
if ai > bi then return true
|
||||
elseif ai < bi then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if versionCompare(GetResourceMetadata(PSInv, "version", 0), "1.0.6") then
|
||||
print("^6Bridge^7: ^4"..PSInv.."^2 Version above ^31.0.6^7, ^2forcing ^1QBInvNew ^2to ^1true^7")
|
||||
QBInvNew = true
|
||||
else
|
||||
print("^6Bridge^7: ^4"..PSInv.."^2 Version ^1below ^31.0.6^7, ^2forcing ^1QBInvNew ^2to ^1false^7")
|
||||
QBInvNew = false
|
||||
end
|
||||
end
|
||||
end
|
||||
289
ui_modules/alcohol.lua
Normal file
289
ui_modules/alcohol.lua
Normal file
@@ -0,0 +1,289 @@
|
||||
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
|
||||
266
ui_modules/notifications.lua
Normal file
266
ui_modules/notifications.lua
Normal file
@@ -0,0 +1,266 @@
|
||||
local notifications = {}
|
||||
local spacing = 60 -- vertical spacing between notifications
|
||||
local activeDrawing = false -- flag for drawing loop state
|
||||
|
||||
local function loadTextureDict(dict)
|
||||
if not HasStreamedTextureDictLoaded(dict) then
|
||||
while not HasStreamedTextureDictLoaded(dict) do RequestStreamedTextureDict(dict) Wait(5) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- GTA Section ---
|
||||
|
||||
local notifTypes = {
|
||||
success = "✔️",
|
||||
error = "❌",
|
||||
warning = "⚠️",
|
||||
police = "🚓",
|
||||
ambulance = "🚑",
|
||||
}
|
||||
|
||||
function gtaNotify(title, message, emoji, src)
|
||||
local notif = {
|
||||
title = title,
|
||||
message = message,
|
||||
emoji = notifTypes[emoji] or "❔",
|
||||
state = "enter",
|
||||
startTime = GetGameTimer(),
|
||||
progress = 0,
|
||||
holdTime = 8000,
|
||||
slideDuration = 100,
|
||||
currentOffset = 0,
|
||||
}
|
||||
table.insert(notifications, notif)
|
||||
|
||||
-- Activate drawing loop if not already running
|
||||
-- This allows it to be silent until the first notifcation is called, then the loop starts
|
||||
if not activeDrawing then
|
||||
activeDrawing = true
|
||||
StartGTADrawingLoop()
|
||||
end
|
||||
end
|
||||
|
||||
-- Drawing loop as a separate controlled thread
|
||||
function StartGTADrawingLoop()
|
||||
--loadTextureDict("timerbars")
|
||||
CreateThread(function()
|
||||
while #notifications > 0 do
|
||||
local currentTime = GetGameTimer()
|
||||
|
||||
-- Update notifications vertical offset
|
||||
for i, notif in ipairs(notifications) do
|
||||
local target = (#notifications - i) * spacing
|
||||
notif.currentOffset += (target - notif.currentOffset) * 0.1
|
||||
end
|
||||
|
||||
for i = #notifications, 1, -1 do
|
||||
local notif = notifications[i]
|
||||
|
||||
if notif.state == "enter" then
|
||||
local elapsed = currentTime - notif.startTime
|
||||
notif.progress = math.min(elapsed / notif.slideDuration, 1.0)
|
||||
if notif.progress >= 1.0 then
|
||||
notif.state = "hold"
|
||||
notif.holdStart = currentTime
|
||||
end
|
||||
elseif notif.state == "hold" then
|
||||
if currentTime - notif.holdStart >= notif.holdTime then
|
||||
notif.state = "exit"
|
||||
notif.exitStart = currentTime
|
||||
end
|
||||
elseif notif.state == "exit" then
|
||||
local elapsed = currentTime - notif.exitStart
|
||||
notif.progress = 1.0 - math.min(elapsed / notif.slideDuration, 1.0)
|
||||
if notif.progress <= 0 then
|
||||
table.remove(notifications, i)
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
local startX, targetX = 1.0, 0.8
|
||||
local posX = startX - (startX - targetX) * notif.progress
|
||||
local posY = 0.05 + (notif.currentOffset / 1080)
|
||||
|
||||
-- Background sprite
|
||||
DrawSprite("timerbars", "all_black_bg", posX + 0.11, posY + 0.025, 0.2, 0.053, 0.0, 255, 255, 255, 255)
|
||||
|
||||
-- Title text
|
||||
local moveMessage = false
|
||||
if not notif.title or notif.title == "" then
|
||||
moveMessage = true
|
||||
else
|
||||
drawGTANotiText(8, 0.4, vec2(0.75, 0.975), notif.title, vec2(posX, posY))
|
||||
end
|
||||
|
||||
-- Message text
|
||||
drawGTANotiText(4, 0.3, vec2(0.75, 0.975), notif.message, vec2(posX, posY + (moveMessage and 0.015 or 0.03)))
|
||||
|
||||
-- Emoji
|
||||
drawGTANotiText(0, 0.3, vec2(0.75, 0.995), notif.emoji, vec2(posX, posY + 0.015))
|
||||
|
||||
::continue::
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
activeDrawing = false -- No notifications left, pause drawing
|
||||
end)
|
||||
end
|
||||
|
||||
function drawGTANotiText(font, scale, wrap, string, pos)
|
||||
SetTextFont(font)
|
||||
SetTextScale(scale, scale)
|
||||
SetTextWrap(wrap.x, wrap.y)
|
||||
SetTextJustification(2)
|
||||
SetTextColour(255, 255, 255, 255)
|
||||
SetTextOutline()
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(string)
|
||||
DrawText(pos.x, pos.y)
|
||||
end
|
||||
|
||||
|
||||
------------------
|
||||
-- REDM SECTION --
|
||||
------------------
|
||||
---
|
||||
---
|
||||
local REDMnotifTypes = {
|
||||
success = vec3(0, 150, 0),
|
||||
error = vec3(150, 0, 0),
|
||||
warning = vec3(0, 150, 150),
|
||||
police = vec3(0, 0, 150),
|
||||
ambulance = vec3(0, 0, 150),
|
||||
}
|
||||
|
||||
function redNotify(title, message, style)
|
||||
local notif = {
|
||||
title = title and CreateVarString(10, "LITERAL_STRING", title) or nil,
|
||||
message = message and CreateVarString(10, "LITERAL_STRING", message) or nil,
|
||||
state = "enter",
|
||||
startTime = GetGameTimer(),
|
||||
style = REDMnotifTypes[style] or vec3(0, 0, 0),
|
||||
progress = 0,
|
||||
holdTime = 8000,
|
||||
slideDuration = 100,
|
||||
currentOffset = 0,
|
||||
}
|
||||
table.insert(notifications, notif)
|
||||
|
||||
-- Activate drawing loop if not already running
|
||||
if not activeDrawing then
|
||||
activeDrawing = true
|
||||
StartREDMDrawingLoop()
|
||||
end
|
||||
end
|
||||
|
||||
-- Drawing loop as a separate controlled thread
|
||||
function StartREDMDrawingLoop()
|
||||
CreateThread(function()
|
||||
loadTextureDict("generic_textures")
|
||||
while #notifications > 0 do
|
||||
local currentTime = GetGameTimer()
|
||||
|
||||
-- Update notifications vertical offset
|
||||
for i, notif in ipairs(notifications) do
|
||||
local target = (#notifications - i) * spacing
|
||||
notif.currentOffset += (target - notif.currentOffset) * 0.1
|
||||
end
|
||||
|
||||
for i = #notifications, 1, -1 do
|
||||
local notif = notifications[i]
|
||||
|
||||
if notif.state == "enter" then
|
||||
local elapsed = currentTime - notif.startTime
|
||||
notif.progress = math.min(elapsed / notif.slideDuration, 1.0)
|
||||
if notif.progress >= 1.0 then
|
||||
notif.state = "hold"
|
||||
notif.holdStart = currentTime
|
||||
end
|
||||
elseif notif.state == "hold" then
|
||||
if currentTime - notif.holdStart >= notif.holdTime then
|
||||
notif.state = "exit"
|
||||
notif.exitStart = currentTime
|
||||
end
|
||||
elseif notif.state == "exit" then
|
||||
local elapsed = currentTime - notif.exitStart
|
||||
notif.progress = 1.0 - math.min(elapsed / notif.slideDuration, 1.0)
|
||||
if notif.progress <= 0 then
|
||||
table.remove(notifications, i)
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
local startX, targetX = 1.0, 0.8
|
||||
local posX = startX - (startX - targetX) * notif.progress
|
||||
local posY = 0.05 + (notif.currentOffset / 1080)
|
||||
|
||||
-- Background sprite
|
||||
DrawSprite("generic_textures", "inkroller_1a", posX + 0.09, posY + 0.025, 0.2, 0.053, 180.0, notif.style.x, notif.style.y, notif.style.z, 150)
|
||||
|
||||
-- Title text
|
||||
local moveMessage = false
|
||||
if not notif.title or notif.title == "" then
|
||||
moveMessage = true
|
||||
else
|
||||
drawREDMNotiText(1, 0.4, vec2(0.75, 0.975), notif.title, vec2(posX, posY))
|
||||
end
|
||||
|
||||
-- Message text
|
||||
drawREDMNotiText(6, 0.3, vec2(0.75, 0.975), notif.message, vec2(posX, posY + (moveMessage and 0.0145 or 0.025)))
|
||||
|
||||
::continue::
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
activeDrawing = false -- No notifications left, pause drawing
|
||||
end)
|
||||
end
|
||||
|
||||
function drawREDMNotiText(font, scale, wrap, string, pos)
|
||||
SetTextFontForCurrentCommand(font)
|
||||
SetTextScale(scale, scale)
|
||||
SetTextColor(255, 255, 255, 255)
|
||||
SetTextWrap(wrap.x, wrap.y)
|
||||
SetTextJustification(2)
|
||||
SetTextDropshadow(1, 0, 0, 0, 200)
|
||||
DisplayText(string, pos.x, pos.y)
|
||||
end
|
||||
|
||||
-- Exports --
|
||||
-- Example:
|
||||
-- TriggerEvent("jim_bridge:Notify", nil, "Short test message", "success")
|
||||
--
|
||||
-- exports["jim_bridge"]:Notify(nil, "Short test message", "success")
|
||||
--
|
||||
|
||||
RegisterNetEvent("jim_bridge:RedNotify", redNotify)
|
||||
RegisterNetEvent("jim_bridge:GTANotify", gtaNotify)
|
||||
|
||||
RegisterNetEvent("jim_bridge:Notify", function(...)
|
||||
if GetCurrentGameName() == "rdr3" then
|
||||
redNotify(...)
|
||||
else
|
||||
gtaNotify(...)
|
||||
end
|
||||
end)
|
||||
|
||||
function Notify(...)
|
||||
if GetCurrentGameName() == "rdr3" then
|
||||
redNotify(...)
|
||||
else
|
||||
gtaNotify(...)
|
||||
end
|
||||
end
|
||||
|
||||
exports("Notify", Notify)
|
||||
|
||||
-- Example testing notifications (can remove after testing)
|
||||
--CreateThread(function()
|
||||
-- TriggerEvent("jim_bridge:Notify", nil, "Short test message", "success")
|
||||
-- Wait(3000)
|
||||
-- TriggerEvent("jim_bridge:Notify", "Error!", "This is a longer example message to show stacking.", "error")
|
||||
-- Wait(3000)
|
||||
-- TriggerEvent("jim_bridge:Notify", "Test!", "This notification has to 'type' set.", nil )
|
||||
-- Wait(4000)
|
||||
--end)
|
||||
345
ui_modules/progressbar.lua
Normal file
345
ui_modules/progressbar.lua
Normal file
@@ -0,0 +1,345 @@
|
||||
local inProgress = false
|
||||
|
||||
local function loadTextureDict(dict)
|
||||
if not HasStreamedTextureDictLoaded(dict) then
|
||||
while not HasStreamedTextureDictLoaded(dict) do RequestStreamedTextureDict(dict) Wait(5) end
|
||||
end
|
||||
end
|
||||
|
||||
local function loadAnimDict(animDict)
|
||||
if not DoesAnimDictExist(animDict) then
|
||||
print("^6Bridge^7: ^1ERROR^7: ^2Anim Dictionary^7 - '^6"..animDict.."^7' ^2does not exist in server") return
|
||||
else
|
||||
while not HasAnimDictLoaded(animDict) do RequestAnimDict(animDict) Wait(5) end
|
||||
end
|
||||
end
|
||||
|
||||
local function playAnim(animDict, animName, duration, flag, ped, speed)
|
||||
loadAnimDict(animDict)
|
||||
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
|
||||
|
||||
local function stopAnim(animDict, animName, ped)
|
||||
StopAnimTask(ped or PlayerPedId(), animDict, animName, 0.5)
|
||||
StopAnimTask(ped or PlayerPedId(), animName, animDict, 0.5)
|
||||
RemoveAnimDict(animDict)
|
||||
end
|
||||
|
||||
function redProgressBar(data)
|
||||
local ped = PlayerPedId()
|
||||
|
||||
local result = nil
|
||||
|
||||
loadTextureDict("generic_textures")
|
||||
if inProgress then return false end
|
||||
inProgress = true
|
||||
local wait = debugMode and 1000 or data.time
|
||||
local endTime = GetGameTimer() + wait
|
||||
|
||||
LocalPlayer.state:set("inv_busy", true, true)
|
||||
TriggerEvent('inventory:client:busy:status', true)
|
||||
TriggerEvent('canUseInventoryAndHotbar:toggle', not true)
|
||||
|
||||
-- Setup Animation/Task if specified
|
||||
if data.dict then
|
||||
playAnim(data.dict, data.anim, -1, data.flag or 32)
|
||||
elseif data.task then
|
||||
TaskStartScenarioInPlace(ped, data.task, -1, true)
|
||||
end
|
||||
|
||||
-- Progress bar rendering loop
|
||||
CreateThread(function()
|
||||
while GetGameTimer() < endTime and inProgress do
|
||||
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
|
||||
|
||||
-- Draw your segmented progress bar
|
||||
ShowRedProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
|
||||
|
||||
-- Controls to disable during progress
|
||||
if data.disableMouse then
|
||||
DisableControlAction(0, `INPUT_LOOK_LR`, true)
|
||||
DisableControlAction(0, `INPUT_LOOK_UD`, true)
|
||||
DisableControlAction(0, `INPUT_VEH_MOUSE_CONTROL_OVERRIDE`, true)
|
||||
end
|
||||
|
||||
if data.disableMovement then
|
||||
DisableControlAction(0, `INPUT_MOVE_LR`, true)
|
||||
DisableControlAction(0, `INPUT_MOVE_UD`, true)
|
||||
DisableControlAction(0, `INPUT_DUCK`, true)
|
||||
DisableControlAction(0, `INPUT_SPRINT`, true)
|
||||
end
|
||||
|
||||
if data.disableCarMovement then
|
||||
DisableControlAction(0, `INPUT_VEH_MOVE_LEFT_ONLY`, true)
|
||||
DisableControlAction(0, `INPUT_VEH_MOVE_RIGHT_ONLY`, true)
|
||||
DisableControlAction(0, `INPUT_VEH_ACCELERATE`, true)
|
||||
DisableControlAction(0, `INPUT_VEH_BRAKE`, true)
|
||||
DisableControlAction(0, `INPUT_VEH_EXIT`, true)
|
||||
end
|
||||
|
||||
if data.disableCombat then
|
||||
DisablePlayerFiring(PlayerId(), true)
|
||||
DisableControlAction(0, `INPUT_ATTACK`, true)
|
||||
DisableControlAction(0, `INPUT_AIM`, true)
|
||||
DisableControlAction(0, `INPUT_OPEN_WHEEL_MENU`, true)
|
||||
DisableControlAction(0, `INPUT_DETONATE`, true)
|
||||
DisableControlAction(0, `INPUT_THROW_GRENADE`, true)
|
||||
DisableControlAction(0, `INPUT_MELEE_ATTACK`, true)
|
||||
DisableControlAction(0, `INPUT_MELEE_GRAPPLE_ATTACK`, true)
|
||||
end
|
||||
|
||||
if data.cancel and (
|
||||
IsControlJustReleased(0, `INPUT_FRONTEND_RS`) or
|
||||
IsControlJustReleased(0, `INPUT_SWITCH_SHOULDER`) or
|
||||
IsControlJustReleased(0, `INPUT_GAME_MENU_TAB_RIGHT_SECONDARY`) or
|
||||
IsControlJustReleased(0, `INPUT_FRONTEND_CANCEL`) or
|
||||
IsControlJustReleased(0, `INPUT_GAME_MENU_CANCEL`) or
|
||||
IsControlJustReleased(0, `INPUT_FRONTEND_RRIGHT`) or
|
||||
IsControlJustReleased(0, `INPUT_QUIT`) or
|
||||
IsControlJustReleased(0, `INPUT_MINIGAME_QUIT`)
|
||||
) then
|
||||
inProgress = false
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Wait for completion or cancel
|
||||
while GetGameTimer() < endTime and inProgress do
|
||||
Wait(100)
|
||||
end
|
||||
|
||||
-- Cleanup animations/tasks
|
||||
if data.dict then
|
||||
stopAnim(data.dict, data.anim, ped)
|
||||
end
|
||||
if data.task then
|
||||
ClearPedTasks(ped)
|
||||
end
|
||||
|
||||
result = inProgress
|
||||
inProgress = false
|
||||
|
||||
while result == nil do Wait(10) end
|
||||
|
||||
-- Cleanup
|
||||
FreezeEntityPosition(ped, false)
|
||||
|
||||
LocalPlayer.state:set("inv_busy", false, true)
|
||||
TriggerEvent('inventory:client:busy:status', false)
|
||||
TriggerEvent('canUseInventoryAndHotbar:toggle', not false)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function ShowRedProgressBar(percentage, title, level)
|
||||
local loc = vec2(0.40, 0.90)
|
||||
local size = vec2(0.3, 0.03)
|
||||
local tickCount = 10 -- visually split into 10 segments (9 inner lines)
|
||||
local barHeight = size.y / 3.4
|
||||
local barWidth = 0.21 -- tuned to your existing layout
|
||||
local barLeft = (loc.x - size.x / 4) + 0.075
|
||||
local barCenter = barLeft + barWidth / 2
|
||||
local lineW = 0.001
|
||||
local lineH = barHeight + 0.00
|
||||
|
||||
-- Background plate
|
||||
DrawSprite("generic_textures", "inkroller_1a", loc.x + 0.1, loc.y - 0.01, 0.25, 0.07, 180.0, 0, 0, 0, 200)
|
||||
|
||||
-- Title (left)
|
||||
SetTextFontForCurrentCommand(6)
|
||||
SetTextScale(0.35, 0.35)
|
||||
SetTextColor(255, 255, 255, 255)
|
||||
SetTextDropshadow(1, 0, 0, 0, 200)
|
||||
BgDisplayText(title, loc.x - size.x / 4 + 0.074, loc.y - 0.034)
|
||||
|
||||
-- Percentage (right)
|
||||
SetTextFontForCurrentCommand(1)
|
||||
SetTextScale(0.35, 0.35)
|
||||
SetTextColor(255, 255, 255, 255)
|
||||
SetTextDropshadow(1, 0, 0, 0, 200)
|
||||
BgDisplayText(level, loc.x - size.x / 4 + 0.246, loc.y - 0.030)
|
||||
|
||||
-- Track (bg)
|
||||
DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
|
||||
|
||||
-- Fill
|
||||
local fillWidth = barWidth * (percentage / 100.0)
|
||||
if fillWidth > 0.0 then
|
||||
local fillCenter = barLeft + (fillWidth / 2.0)
|
||||
DrawRect(fillCenter, loc.y, fillWidth, barHeight, 255, 0, 0, 200) -- red fill
|
||||
end
|
||||
|
||||
-- Tick lines (9 inner lines for 10 segments)
|
||||
for i = 1, (tickCount - 1) do
|
||||
local x = barLeft + (barWidth * (i / tickCount))
|
||||
DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
|
||||
end
|
||||
end
|
||||
|
||||
function gtaProgressBar(data)
|
||||
local ped = PlayerPedId()
|
||||
|
||||
local result = nil
|
||||
|
||||
loadTextureDict("timerbars")
|
||||
if inProgress then return false end
|
||||
inProgress = true
|
||||
local wait = debugMode and 1000 or data.time
|
||||
local endTime = GetGameTimer() + wait
|
||||
|
||||
-- Setup Animation/Task if specified
|
||||
if data.dict then
|
||||
playAnim(data.dict, data.anim, -1, data.flag or 32)
|
||||
elseif data.task then
|
||||
TaskStartScenarioInPlace(ped, data.task, -1, true)
|
||||
end
|
||||
|
||||
-- Progress bar rendering loop
|
||||
CreateThread(function()
|
||||
while GetGameTimer() < endTime and inProgress do
|
||||
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
|
||||
|
||||
-- Draw your segmented progress bar
|
||||
ShowGTAProgressBar(percentage, data.label, ("%.0f%%"):format(percentage))
|
||||
|
||||
-- Controls to disable during progress
|
||||
DisablePlayerFiring(PlayerId(), true)
|
||||
DisableControlAction(0, 25, true) -- Disable aim
|
||||
DisableControlAction(0, 21, true) -- Disable sprint
|
||||
DisableControlAction(0, 30, true) -- Disable move left/right
|
||||
DisableControlAction(0, 31, true) -- Disable move forward/back
|
||||
DisableControlAction(0, 36, true) -- Disable stealth
|
||||
|
||||
if data.cancel and (IsControlJustReleased(0, 202) or IsControlJustReleased(0, 177) or IsControlJustReleased(0, 73)) then
|
||||
inProgress = false
|
||||
end
|
||||
end
|
||||
result = inProgress
|
||||
end)
|
||||
|
||||
|
||||
-- Wait for completion or cancel
|
||||
while result == nil do Wait(10) end
|
||||
inProgress = false
|
||||
|
||||
-- Cleanup animations/tasks
|
||||
if data.dict then
|
||||
stopAnim(data.dict, data.anim, ped)
|
||||
end
|
||||
if data.task then
|
||||
ClearPedTasks(ped)
|
||||
end
|
||||
|
||||
-- Cleanup
|
||||
FreezeEntityPosition(ped, false)
|
||||
|
||||
LocalPlayer.state:set("inv_busy", false, true)
|
||||
TriggerEvent('inventory:client:busy:status', false)
|
||||
TriggerEvent('canUseInventoryAndHotbar:toggle', not false)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function ShowGTAProgressBar(percentage, title, level)
|
||||
local loc = vec2(0.37, 0.90)
|
||||
local size = vec2(0.3, 0.03)
|
||||
local tickCount = 10
|
||||
local barHeight = size.y / 3.4
|
||||
local barWidth = 0.19
|
||||
local barLeft = (loc.x - size.x / 4) + 0.075
|
||||
local barCenter = barLeft + barWidth / 2
|
||||
local lineW = 0.001
|
||||
local lineH = barHeight + 0.00
|
||||
|
||||
-- Background plates
|
||||
DrawSprite("timerbars", "all_black_bg", loc.x + 0.028, loc.y - 0.01, 0.15, 0.07, 0.0, 255, 255, 255, 255)
|
||||
DrawSprite("timerbars", "all_black_bg", loc.x + 0.170, loc.y - 0.01, 0.15, 0.07, 180.0, 255, 255, 255, 255)
|
||||
|
||||
-- Title (left)
|
||||
SetTextFont(0)
|
||||
SetTextProportional(1)
|
||||
SetTextScale(0.35, 0.35)
|
||||
SetTextColour(255, 255, 255, 255)
|
||||
SetTextDropshadow(0, 0, 0, 0, 255)
|
||||
SetTextDropShadow()
|
||||
SetTextOutline()
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(title)
|
||||
DrawText(loc.x - size.x / 4 + 0.074, loc.y - 0.034)
|
||||
|
||||
-- Percentage (right)
|
||||
SetTextFont(0)
|
||||
SetTextProportional(1)
|
||||
SetTextScale(0.35, 0.25)
|
||||
SetTextColour(255, 255, 255, 255)
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(level)
|
||||
DrawText(loc.x - size.x / 4 + 0.246, loc.y - 0.030)
|
||||
|
||||
-- Track (bg)
|
||||
DrawRect(barCenter, loc.y, barWidth, barHeight, 100, 100, 100, 255)
|
||||
|
||||
-- Fill
|
||||
local fillWidth = barWidth * (percentage / 100.0)
|
||||
if fillWidth > 0.0 then
|
||||
local fillCenter = barLeft + (fillWidth / 2.0)
|
||||
DrawRect(fillCenter, loc.y, fillWidth, barHeight, 93, 182, 229, 255) -- GTA blue
|
||||
end
|
||||
|
||||
-- Tick lines
|
||||
for i = 1, (tickCount - 1) do
|
||||
local x = barLeft + (barWidth * (i / tickCount))
|
||||
DrawRect(x, loc.y, lineW, lineH, 0, 0, 0, 120)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function stopProgressBar() inProgress = false end
|
||||
function isProgressBar() return inProgress end
|
||||
|
||||
function progressBar(...)
|
||||
if GetCurrentGameName() == "rdr3" then
|
||||
return redProgressBar(...)
|
||||
else
|
||||
return gtaProgressBar(...)
|
||||
end
|
||||
end
|
||||
|
||||
exports("progressBar", function(...)
|
||||
return progressBar(...)
|
||||
end)
|
||||
exports("gtaProgressBar", function(...)
|
||||
return gtaProgressBar(...)
|
||||
end)
|
||||
exports("redProgressBar", function(...)
|
||||
return redProgressBar(...)
|
||||
end)
|
||||
|
||||
exports("stopProgressBar", stopProgressBar)
|
||||
exports("isProgressBar", isProgressBar)
|
||||
|
||||
--RegisterCommand("testprog", function()
|
||||
-- print("test")
|
||||
-- if progressBar({
|
||||
-- label = "Processing...",
|
||||
-- time = 10000,
|
||||
-- disableMovement = true,
|
||||
-- --dict = "amb@world_human_hang_out_street@female_hold_arm@base",
|
||||
-- --anim = "base",
|
||||
-- --flag = 49,
|
||||
-- cancel = true,
|
||||
-- }) then
|
||||
-- Notify("Success!", "Short test message", "success")
|
||||
-- else
|
||||
-- Notify("Error!", "This is a longer example message to show stacking.", "error")
|
||||
-- end
|
||||
--end)
|
||||
127
ui_modules/skillcheck.lua
Normal file
127
ui_modules/skillcheck.lua
Normal file
@@ -0,0 +1,127 @@
|
||||
local activeSkillCheck = false
|
||||
|
||||
function gtaSkillCheck()
|
||||
if activeSkillCheck then return end
|
||||
local result = false
|
||||
local successes = 0
|
||||
local barsRequired = 3
|
||||
|
||||
for bar = 1, barsRequired do
|
||||
activeSkillCheck = true
|
||||
local width, height = 0.2, 0.01
|
||||
local x, y = 0.5, 0.8
|
||||
|
||||
-- Random highlighted zone
|
||||
local highlightSize = math.random(10, 20) / 100
|
||||
local highlightStart = math.random(10, 50) / 100
|
||||
local highlightEnd = highlightStart + highlightSize
|
||||
local highlightAlpha = 0
|
||||
local cursorPos = 0.0
|
||||
local cursorSpeed = 0.025
|
||||
local movingRight = true
|
||||
|
||||
while activeSkillCheck do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect
|
||||
highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha
|
||||
|
||||
-- Draw highlighted zone (success area) with pulsing effect
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha )
|
||||
-- Draw moving cursor
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
|
||||
-- Move cursor
|
||||
if movingRight then
|
||||
cursorPos += cursorSpeed
|
||||
if cursorPos >= 1.0 then movingRight = false end
|
||||
else
|
||||
cursorPos -= cursorSpeed
|
||||
if cursorPos <= 0.0 then movingRight = true end
|
||||
end
|
||||
|
||||
if IsControlJustPressed(0, 177) then -- Backspace to cancel
|
||||
local displayTime = GetGameTimer() + 2000
|
||||
PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1)
|
||||
while GetGameTimer() < displayTime do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
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)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check for keypress (E)
|
||||
if IsControlJustPressed(0, 38) then
|
||||
activeSkillCheck = false
|
||||
result = cursorPos >= highlightStart and cursorPos <= highlightEnd
|
||||
if result then
|
||||
PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true)
|
||||
else
|
||||
PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1)
|
||||
end
|
||||
local displayTime = GetGameTimer() + 2000
|
||||
while GetGameTimer() < displayTime do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
-- Draw highlighted zone
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180)
|
||||
|
||||
-- Draw stationary cursor at result position
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
-- Display result text
|
||||
drawGTASuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52)
|
||||
end
|
||||
if result then
|
||||
successes += 1
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
activeSkillCheck = false
|
||||
print("^5GTAUI^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7")
|
||||
return successes == barsRequired
|
||||
end
|
||||
|
||||
function drawGTASuccessText(x, y, text, r, g, b)
|
||||
SetTextFont(8)
|
||||
SetTextScale(0.45, 0.45)
|
||||
SetTextColour(r, g, b, 255)
|
||||
SetTextDropshadow(0, 0, 0, 0, 255)
|
||||
SetTextEdge(2, 0, 0, 0, 150)
|
||||
SetTextDropShadow()
|
||||
SetTextOutline()
|
||||
SetTextCentre(true)
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(true)
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(text)
|
||||
DrawText(x, y + 0.03)
|
||||
end
|
||||
|
||||
function createScaleBars(x, y, width, height)
|
||||
-- Draw background box
|
||||
DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255)
|
||||
DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255)
|
||||
-- Draw full bar (dark background)
|
||||
DrawRect(x, y, width, height, 100, 100, 100, 255)
|
||||
end
|
||||
|
||||
exports("skillCheck", function(...)
|
||||
if GetCurrentGameName() == "rdr3" then
|
||||
--redSkillCheck(...)
|
||||
else
|
||||
gtaSkillCheck(...)
|
||||
end
|
||||
end)
|
||||
373
ui_modules/target.lua
Normal file
373
ui_modules/target.lua
Normal file
@@ -0,0 +1,373 @@
|
||||
-- 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.
|
||||
local Keys = {
|
||||
[322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5",
|
||||
[167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10",
|
||||
[243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5",
|
||||
[159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=",
|
||||
[177] = "BACKSPACE", [37] = "TAB",
|
||||
[44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y",
|
||||
[303] = "U", [199] = "P",
|
||||
[39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS",
|
||||
[34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G",
|
||||
[74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT",
|
||||
[20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N",
|
||||
[244] = "M", [82] = ",", [81] = "."
|
||||
}
|
||||
|
||||
|
||||
-- ===== 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
|
||||
end
|
||||
TargetRegistry.byKey[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler("onResourceStop", function(res)
|
||||
local owned = TargetRegistry.byResource[res]
|
||||
if not owned then return end
|
||||
local cnt = 0
|
||||
for key in pairs(owned) do
|
||||
removeTargetKey(key, "resource stopped: "..res)
|
||||
cnt = cnt + 1
|
||||
end
|
||||
TargetRegistry.byResource[res] = nil
|
||||
-- print("^6Bridge^7:^5 Target^7: ^2Cleared "..cnt.." target(s) from '"..res.."'")
|
||||
end)
|
||||
|
||||
-- ===== Helpers =====
|
||||
local function vecKey(v)
|
||||
-- stable rounded coord string for entity dedupe when name isn't provided
|
||||
return ("%.3f,%.3f,%.3f"):format(v.x, v.y, v.z)
|
||||
end
|
||||
|
||||
local function bakeButtons(opts)
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] =
|
||||
(" ~b~[~w~%s~b~] ~w~%s"):format(Keys[opts[i].key] or ("K"..opts[i].key), opts[i].label or ("Option "..i))
|
||||
end
|
||||
return tempText
|
||||
end
|
||||
|
||||
-- Update cached text blob
|
||||
local function updateCachedText(target)
|
||||
target.text = table.concat(target.buttontext, "\n")
|
||||
end
|
||||
|
||||
-- ===== Public API: Create targets =====
|
||||
-- ENTITY: createEntityTarget(entity, opts, dist, nameOpt?)
|
||||
function createEntityTarget(entity, opts, dist, name)
|
||||
startTargetLoop()
|
||||
|
||||
if not entity or entity == 0 then return end
|
||||
targetEntities[#targetEntities + 1] = entity
|
||||
|
||||
local owner = getOwnerResource()
|
||||
local coords = GetEntityCoords(entity)
|
||||
local key = tostring(name) or ("entity@" .. vecKey(coords))
|
||||
|
||||
-- Always overwrite on same key
|
||||
local buttontext = bakeButtons(opts)
|
||||
TextTargets[key] = {
|
||||
_key = key,
|
||||
_type = "entity",
|
||||
_owner = owner,
|
||||
entity = entity,
|
||||
coords = vec3(coords.x, coords.y, coords.z),
|
||||
buttontext = buttontext,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
}
|
||||
updateCachedText(TextTargets[key])
|
||||
registerTarget(owner, key)
|
||||
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated ENTITY target '"..key.."' by '"..owner.."' @ "..formatCoord(coords))
|
||||
return key
|
||||
end
|
||||
|
||||
-- ZONE: createZoneTarget(data, opts, dist)
|
||||
-- Expect data[1] = id/name, data[2] = vec3 coords (as in your original)
|
||||
function createZoneTarget(data, opts, dist)
|
||||
startTargetLoop()
|
||||
|
||||
local 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, "_")
|
||||
end
|
||||
|
||||
local buttontext = bakeButtons(opts)
|
||||
TextTargets[key] = {
|
||||
_key = key,
|
||||
_type = "model",
|
||||
_owner = owner,
|
||||
models = models,
|
||||
buttontext = buttontext,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
coords = vec3(0, 0, 0), -- will be updated by the refresher
|
||||
}
|
||||
updateCachedText(TextTargets[key])
|
||||
registerTarget(owner, key)
|
||||
-- print("^6Bridge^7:^5 Target^7: ^2Added/Updated MODEL target '"..key.."' by '"..owner.."' (models: "..table.concat(models, ",")..")")
|
||||
return key
|
||||
end
|
||||
|
||||
-- ===== Public API: Remove targets =====
|
||||
-- Entity removal accepts entity handle or key string.
|
||||
function removeEntityTarget(entityOrKey)
|
||||
local key = nil
|
||||
if type(entityOrKey) == "string" then
|
||||
key = entityOrKey
|
||||
elseif type(entityOrKey) == "number" then
|
||||
for k, t in pairs(TextTargets) do
|
||||
if t._type == "entity" and t.entity == entityOrKey then key = k break end
|
||||
end
|
||||
if not key then
|
||||
-- Fallback: try coord-key match
|
||||
local c = GetEntityCoords(entityOrKey)
|
||||
local guess = "entity@"..vecKey(c)
|
||||
if TextTargets[guess] then key = guess end
|
||||
end
|
||||
end
|
||||
if key then removeTargetKey(key, "removeEntityTarget") end
|
||||
end
|
||||
|
||||
function removeZoneTarget(key)
|
||||
if not key then return end
|
||||
removeTargetKey(key, "removeZoneTarget")
|
||||
end
|
||||
|
||||
-- For models, pass the returned key from createModelTarget (recommended).
|
||||
function removeModelTarget(key)
|
||||
if not key then return end
|
||||
removeTargetKey(key, "removeModelTarget")
|
||||
end
|
||||
|
||||
exports("createEntityTarget", createEntityTarget)
|
||||
exports("createZoneTarget", createZoneTarget)
|
||||
exports("createModelTarget", createModelTarget)
|
||||
|
||||
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
|
||||
|
||||
-- lazy include (unchanged from your original)
|
||||
Config = { System = {} }
|
||||
local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua')))
|
||||
fileLoader()
|
||||
|
||||
-- Model Entity Refresher (kept)
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local pedCoords = GetEntityCoords(PlayerPedId())
|
||||
for _, target in pairs(TextTargets) do
|
||||
if target.models 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
|
||||
end
|
||||
end
|
||||
Wait(3000) -- Refresh every 3s
|
||||
end
|
||||
end)
|
||||
|
||||
-- Main Target Loop (unchanged logic, just uses new TextTargets entries)
|
||||
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, closestDist, targetEntity = nil, math.huge, nil
|
||||
|
||||
for _, target in pairs(TextTargets) do
|
||||
local coords = target.coords
|
||||
if coords then
|
||||
local dist = #(pedCoords - coords)
|
||||
if dist <= target.dist then
|
||||
local normVec = normalizeVector(coords - camCoords)
|
||||
local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z
|
||||
|
||||
if dot > 0.5 and dist < closestDist then
|
||||
closestTarget = target
|
||||
closestDist = dist
|
||||
targetEntity = target.entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, target in pairs(TextTargets) do
|
||||
if not target.coords then goto continue end
|
||||
local dist = #(pedCoords - target.coords)
|
||||
if dist > target.dist then goto continue end
|
||||
local isClosest = (target == closestTarget)
|
||||
|
||||
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
|
||||
if opt.onSelect then opt.onSelect(targetEntity) end
|
||||
if opt.action then opt.action(targetEntity) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local baseZ, lineHeight = target.coords.z + 1.0, -0.16
|
||||
local lineOffset = 0
|
||||
|
||||
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
|
||||
DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest)
|
||||
lineOffset = lineOffset + 1
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
Wait(1)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function DrawText3D(coord, text, highlight)
|
||||
SetTextScale(0.30, 0.30)
|
||||
SetTextFont(0)
|
||||
SetTextProportional(1)
|
||||
SetTextColour(255, 255, 255, 215)
|
||||
SetTextEntry("STRING")
|
||||
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)
|
||||
SetDrawOrigin(coord.x, coord.y, coord.z, 0)
|
||||
DrawText(0.0, 0.0)
|
||||
local count, length = GetLineCountAndMaxLength(txt)
|
||||
|
||||
local padding = 0.005
|
||||
local heightFactor = (count / 43) + padding
|
||||
local weightFactor = (length / 150) + padding
|
||||
|
||||
local height = (heightFactor / 2) - padding / 1
|
||||
local width = (weightFactor / 2) - padding / 1
|
||||
|
||||
DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150)
|
||||
ClearDrawOrigin()
|
||||
end
|
||||
|
||||
function GetLineCountAndMaxLength(text)
|
||||
local lineCount, maxLength = 0, 0
|
||||
for line in text:gmatch("[^\n]+") do
|
||||
lineCount = lineCount + 1
|
||||
local lineLength = string.len(line)
|
||||
if lineLength > maxLength then
|
||||
maxLength = lineLength
|
||||
end
|
||||
end
|
||||
if lineCount == 0 then lineCount = 1 end
|
||||
return lineCount, maxLength
|
||||
end
|
||||
|
||||
function RotationToDirection(rot)
|
||||
local adjust = math.pi / 180
|
||||
return vec3(
|
||||
-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
|
||||
|
||||
function normalizeVector(vec)
|
||||
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
|
||||
18
version.txt
18
version.txt
@@ -1,12 +1,10 @@
|
||||
2.0.14
|
||||
2.1.08
|
||||
|
||||
- Add getPlayer() fallbacks for if player isn't loaded
|
||||
- Add fallback for sellMenu missing locales
|
||||
- Change blip preview export to "jim-blipcontroller"
|
||||
- Support for tgiann-bank
|
||||
- Fixes for OX_Core COX version
|
||||
- Fix QBOX weapons not being added to "Items" cache
|
||||
- Remove Player.Offline from canCarry() as it was erroring
|
||||
- Possible fix for QS_Inv stashes wiping on script start
|
||||
- Fix getStash() always trying to use fallback functions
|
||||
- Start adding stashEditMetadata() function - wip
|
||||
- Add check for broken/missing config.lua in scripts
|
||||
- Add fix for debugMode breaking if no config file
|
||||
- Add a callback to check for society bank balances
|
||||
- Add player head movement for `lookEnt()` to continue looking after turning
|
||||
|
||||
https://github.com/jimathy/jim_bridge
|
||||
https://github.com/jimathy/jim_bridge/releases/latest
|
||||
Reference in New Issue
Block a user