Compare commits

...

265 Commits

Author SHA1 Message Date
Jim Shield
7738bb4fca Version Bump
`2.1.03` - `2.1.04`
2025-09-30 23:39:55 +01:00
Jim Shield
3ad65810c3 Rework crafting.lua
Changes:
- Crafting prop not spawning
- Crafting "sound" not playing
- `SingleProgress` is now "one progressbar"

I'm working on refactoring crafting table's layout style too

I'm updating my scripts crafting recipes to a new format, this commit basically makes use of it and makes it slightly easier to edit later if needed

It *should* increase readability and users ability to edit these recipes

# "old style" crafting recipes should still be compatible thanks to a wrapper I added so you shouldn't notice a difference.
2025-09-28 14:03:23 +01:00
Jim Shield
45eeaf8f0d update a couple debug prints 2025-09-28 13:48:17 +01:00
Jim Shield
37f7eb1116 add testing code for new crafting table style 2025-09-28 03:22:12 +01:00
Jim Shield
145e3aa156 add new module ropeControl.lua
**Needs work** but adds the ability to easily create ropes/wires from one coord to another
2025-09-27 13:01:09 +01:00
Jim Shield
bb176addbf Create setStress function
This allows per framework stress functions to be set, including ESX

For ESX the math is done in this function as their values are insanely high instead of 0 - 100
2025-09-27 12:35:56 +01:00
Jim Shield
178040348e Rework SingleProgress slightly
Realised it wasn't doing the math to take the correct amount of required items, so this fixes it

also if `SingleProgress` is enabled, it won't show the "Ingredient Progress bars" any more
2025-09-27 12:34:46 +01:00
Jim Shield
ee15f3b076 Version Bump
`2.1.02` - `2.1.03`
2025-09-24 23:12:34 +01:00
Jim Shield
6919f038ee Release Notifier Push 2025-09-24 18:20:02 +01:00
Jim Shield
47029da6ed Revert "Version Bump"
This reverts commit f08638812c.
2025-09-24 14:23:43 +01:00
Jim Shield
e1d292d1bf Remove testing debug print 2025-09-24 14:09:19 +01:00
Jim Shield
f08638812c Version Bump
`2.1.02` - `2.1.03`
2025-09-24 13:36:14 +01:00
Jim Shield
de5424c506 Fix getPlayerInv() for CodeM Inventory 2025-09-24 12:30:08 +01:00
Jim Shield
b46d734442 Testing a rework _loaders.lua
Uses more specific functions per framework now for player loading in and out and optimizes use of them
2025-09-24 12:29:48 +01:00
Jim Shield
92ad96e435 Redo math for ox_inv get player current inv weight 2025-09-24 11:59:44 +01:00
Jim Shield
6166314cbf fix ox_inv current inventory weight check 2025-09-23 15:05:46 +01:00
Jim Shield
9fa8f58cfb Make ox_lib progressbars actually return false
I think this was causing progressbars to return true instead of `nil/false` somehow
this *should* make it more certain
2025-09-23 14:59:16 +01:00
Jim Shield
ed997ad8a2 fix typo breaking qb-core /commands 2025-09-23 12:00:40 +01:00
Jim Shield
20cea95783 Set previous active cam inactive
shouldn't be needed but just in case
2025-09-22 21:10:01 +01:00
Jim Shield
0dc5a45b5d Support for esx_society
`esx_society` requires societies to be registered manually before opening. this forces it

also adds full support for charging, funding and retrieving balances in `esx_society`
2025-09-22 21:08:31 +01:00
Jim Shield
54b1280bef Remove debug print 2025-09-21 12:09:12 +01:00
Jim Shield
3ac65d4630 Add "pre support" for jim-shops exploit security 2025-09-21 12:00:05 +01:00
Jim Shield
ed49876c69 Fix cleanup of targets for qb-target
Realised it was trying to remove `nil` when players were unloading/the scripts were ensured
2025-09-21 10:35:22 +01:00
Jim Shield
69d0f07bb6 Potential fix for playerInfo is nil 2025-09-20 17:56:47 +01:00
Jim Shield
b975f318a0 Fix hunger and thirst events
Missed out the math part when adding hunger and thirst to a player when eating items when I reworked the functions

should fix for all frameworks
2025-09-19 23:06:11 +01:00
Jim Shield
ce1df2c205 Version Bump 2025-09-19 17:54:45 +01:00
Jim Shield
75d8146e24 Fix nil print + use .xy for distance check 2025-09-19 17:22:40 +01:00
Jim Shield
268c95f29a Hotfix for shops and stashes erroring 2025-09-15 22:14:18 +01:00
Jim Shield
91dd069e5a Use the correct illenium appearance trigger 2025-09-15 17:23:30 +01:00
Jim Shield
f96d96f26e adjust stash and store registration
This should fix the `jim_bridge` side of the reports of stores erroring when scripts starts
2025-09-15 16:19:52 +01:00
Jim Shield
ae231b6101 Version Bump
`2.0.23` - `2.1.0`
2025-09-14 21:32:12 +01:00
Jim Shield
c709159ea4 Deprecate ConsumeSuccess()
This function and others were too open to exploiters, my scripts not no longer use these functions and handle it all server side.
2025-09-14 17:40:12 +01:00
Jim Shield
c8089109c6 Add "await" for callbacks
This hopefully should help callbacks get their requested data

Basically gives them a timeout for requests, so they don't accidently receive `nil`
2025-09-14 15:18:36 +01:00
Jim Shield
807cfe1a2a Fix stash dist exploit checker 2025-09-13 02:50:31 +01:00
Jim Shield
420e035e2b fix cam wrapperFunctions 2025-09-13 02:47:17 +01:00
Jim Shield
a0377df713 Bit of tidying 2025-09-10 01:26:02 +01:00
Jim Shield
301b069312 add openClothin() function
Attempts to open clothing scripts menus

Still testing this one
2025-09-09 18:49:26 +01:00
Jim Shield
6c09e55fd4 Optimzie hasJob() function
Now makes use of `GetPlayer()` to grab player data on client or server
2025-09-08 21:00:49 +01:00
Jim Shield
d8467f0b02 allow GetPlayer() function to work client side 2025-09-08 20:20:50 +01:00
Jim Shield
a9832da7d7 Update notify-discord.yml
test 2
2025-09-08 18:35:03 +01:00
Jim Shield
2942907963 Update notify-discord.yml
Testing if the discord bot can understand { ' } ( )
2025-09-08 18:26:57 +01:00
Jim Shield
02ca13bcbd Put distance exploit checks in place
This is a continuation from an earlier commit

- If something is attempted to be opened without correctly registered coordinates, (most likely being called outside the script), refuse it
- If the player isn't close enough to the registered coords, refuse it

Basically, built-in distance based exploit protection for shops/stashes/sellShops
2025-09-08 18:08:17 +01:00
Jim Shield
833bc675ce Tidy up currentInvWeight and add function info 2025-09-06 22:58:58 +01:00
Jim Shield
ac9a7df57b Fix stopTempCam() wrapper function 2025-09-06 22:32:02 +01:00
Jim Shield
657bc3935c Add cooldown for onPlayerLoaded()
Add a cooldown to workaround certain multicharacter scripts triggering events like `QBCore:Client:OnPlayerLoaded` more than once and duplicating targets when a player connected
2025-09-06 14:54:36 +01:00
Jim Shield
10085a3c23 Update fx and warn users about possible errors 2025-09-06 14:08:33 +01:00
Jim Shield
0d3888565c Move getCurrentInvWeight to func table
Moves the getCurrentInvWeight functions to each inventory section of the func table to keep that organised
2025-09-06 12:36:08 +01:00
Jim Shield
8533bbff90 add early end to vehicle ownership check 2025-09-06 12:32:39 +01:00
Jim Shield
42630cc3c4 More enhancements to cameras.lua
Tidy up `createTempCam()`
Add `loadLocation()` and `clearLoadLocation()` functions
Add `renderTime` variable for `stopTempCam()`

Add wrapper functions for each one (for my own sanity):
`createCam()` = `createTempCam()`
`startCam()` = `startTempCam()`
`stopTempCam()` = `stopTempCam()`
2025-09-06 12:22:27 +01:00
Jim Shield
188fb81bdc Merge branch 'main' of https://github.com/jimathy/jim_bridge 2025-09-06 12:03:00 +01:00
Jim Shield
cbc3a52643 Remove references to qs scripts 2025-09-06 12:02:55 +01:00
Jim Shield
bda18260b3 Merge pull request #57 from oosayeroo/main 2025-09-06 11:57:48 +01:00
oosayeroo
32dd938eef Update cameras.lua 2025-09-05 23:38:37 +01:00
Jim Shield
2826bc95c7 Enhance built in camera function file
- Allows filters to be applied to custom cameras when running `startTempCam()`
- Automatically interpolates between cameras created in the same script
- Toggle to attempt to render the location if camera is far away from the ped
- Clears all created cameras when `stopTempCam()` is run
2025-09-05 16:34:45 +01:00
Jim Shield
b35817740b add return when using jim-shops through jim_bridge 2025-09-05 14:53:18 +01:00
Jim Shield
fffef0c228 Quick tidy and optimizations of a few functions 2025-09-05 14:37:39 +01:00
Jim Shield
0caeca4824 Merge branch 'main' of https://github.com/jimathy/jim_bridge 2025-09-04 17:40:35 +01:00
Jim Shield
b5536dbb6b Merge pull request #56 from oosayeroo/main 2025-09-04 17:31:01 +01:00
Jim Shield
26f8140ee8 Merge pull request #55 from desypher/patch-1 2025-09-04 17:30:01 +01:00
oosayeroo
6e4d9bc1d7 Update vehicles.lua 2025-09-04 17:22:36 +01:00
Jim Shield
610f492762 Update itemcontrol.lua 2025-09-04 17:01:30 +01:00
oosayeroo
f88cdb4f9d Merge branch 'main' of https://github.com/oosayeroo/jim_bridge 2025-09-03 11:18:57 +01:00
desypher
463358b53a Changed name to phone on roadphone functions 2025-09-02 14:43:39 +02:00
desypher
3b038fb700 Changed name to phone in phoneFunc array 2025-09-02 14:38:17 +02:00
Jim Shield
8efec21408 Create release.yml
Automate release zips and make them generate the folder name correctly

This helps users download a correctly named script so they dont end up with a script with a version number in the title
2025-09-01 10:25:11 +01:00
Jim Shield
39691a2254 enhance built in ui target code
Now properly tracks scripts that added a new target, if that is restarted it created ones properly

If an overriding target is sent, use that and remove old one
2025-08-30 13:19:01 +01:00
Jim Shield
2924c611f7 Optimize built in progressbar math
Instead of rendering segments and trying to work out how much they should fill, add simple lines to segment it instead, this should make it run a bit smoother as there is way less going on
2025-08-30 12:50:10 +01:00
Jim Shield
be5449b100 Add promise:new() for qb/esx progressbars
This stops them instantly returning `false` and actually waits for the results

This fixes issues with players being unable to complete tasks determined by progressbars eg. installing items on a vehicle and it fails instantly
2025-08-30 12:15:09 +01:00
Jim Shield
1e5d4800fc debug print progressbar results 2025-08-29 20:39:30 +01:00
Jim Shield
cb49fa6949 Update fxmanifest.lua 2025-08-27 19:57:56 +01:00
Jim Shield
315634c173 add supress_updates variable to config
This new `fxmanifest.lua` variable (when true) will stop checking for script updates

People REALLY dont like being told when somethings fixed.

This will stop the update notifications for both `jim_bridge` and the script that loads it.
2025-08-27 19:40:11 +01:00
Jim Shield
eafa881cf8 Adjust frameworkCache.lua to be table based
Reworked `frameworkCache.lua` to the new layout

Fix possibility of invisible props/ peds/vehicles on spawn

Start adding very basic and unfinished support for RedM VorpCore
2025-08-26 13:24:36 +01:00
Jim Shield
a851f8823c Fix sell shops showing nil for items 2025-08-25 16:34:37 +01:00
Jim Shield
354002312e Make use of getItemLabel() in jim_bridge more 2025-08-23 23:36:42 +01:00
Jim Shield
392e0601a8 Update wrapperfunctions.lua
This simply adds names for each "option" it receives, just to make it easier to work with.
2025-08-23 23:36:13 +01:00
Jim Shield
035ef26486 Update contextMenus.lua to use new format 2025-08-23 23:34:15 +01:00
Jim Shield
db9e7fea0a fix stashName variable for stashRemoveItem() 2025-08-22 21:18:51 +01:00
oosayeroo
7077b7d831 Merge branch 'jimathy:main' into main 2025-08-22 10:43:04 +01:00
Jim Shield
bd94b14dad Start working on coordinate based exploit checks
These currently work as intended, but can't put in place yet because my scripts don't fully support it yet. But the base is there for it

Also slip in `bannedItems` table so players can't the abuse sell items function to spawn cash
2025-08-17 02:20:13 +01:00
Jim Shield
8b0e4d803f Fix typos breaking fundPlayer function 2025-08-17 02:10:38 +01:00
Jim Shield
5f81bfa899 Add distExploitCheck function (will be used soon)
I'm adding distance based exploit protection to stashes, shops and selling shops to help fight exploiters

Soon all these created through jim_bridge will refuse opening if its not "registered" correctly

Basically, all my scripts will get updates to support this
2025-08-17 02:10:21 +01:00
Jim Shield
85626d3356 Fix qbx getplayer call 2025-08-15 18:35:21 +01:00
Jim Shield
ceeb9021d9 Raise version check messages to hourly 2025-08-15 17:26:18 +01:00
Jim Shield
09887f3a9d Create compatibility tables for playerFunctions.lua 2025-08-14 21:08:32 +01:00
Jim Shield
26c341c6ec Create bossmenu compatibility table 2025-08-14 20:20:42 +01:00
Jim Shield
bfca698457 More + optimization 2025-08-14 20:08:09 +01:00
oosayeroo
c13d39ca05 Commit[wrapperfunctions.lua]: Fix oxlib command argument
line 27 - changd the function option in oxlib from options[4] to options[3]
2025-08-14 16:33:02 +01:00
Jim Shield
0bcf1b7ed4 Prefer export checks over QBInvNew
Small changes for JPRInv too
2025-08-14 13:34:55 +01:00
Jim Shield
d7622f5143 fix canCarry and add hasItem functions 2025-08-14 13:07:15 +01:00
Jim Shield
d1d992f807 Fix destroy poly function trying to do all options 2025-08-13 22:40:54 +01:00
Jim Shield
16f77f442c Follow same format for targets.lua
God I hope this works as intended
2025-08-13 19:12:56 +01:00
Jim Shield
3b959b8567 Follow same format for societybank.lua
Hopefully becomes easier for devs to add their banking functions to this
2025-08-13 18:41:07 +01:00
Jim Shield
345a81aaf7 Follow same format for polyZone.lua
Just better handling of polyZone scripts with less if statements
2025-08-13 18:27:07 +01:00
Jim Shield
06ef5cb53b Tidy up of helpers, and use joaat() instead 2025-08-13 18:13:58 +01:00
Jim Shield
748e5e2201 Follow same format for notify.lua and drawText.lua
place functions into a table to better handle compatibility of each scripts function
2025-08-13 18:13:08 +01:00
Jim Shield
981563df6a Compile inventory functions into one file
This removes redundant inventory files and compiles all of the functions into one

This allows easier editing and control per inventory and hopefully better optimization, readability and easier addition of support for new inventories
2025-08-13 18:11:39 +01:00
Jim Shield
b4df8deac6 Fix vehicles spawning invisible if not fading in 2025-08-13 02:40:11 +01:00
Jim Shield
e3da31970e lower oxlibs min default polyzone to -20 2025-08-10 01:18:25 +01:00
Jim Shield
6fb06da0e4 Update playerfunctions.lua 2025-08-10 01:18:01 +01:00
Jim Shield
945cd52787 Add support for tgainninv's new item cache 2025-08-10 01:17:14 +01:00
Jim Shield
a880c2dadb Change qb-inv item removal to do in one go 2025-08-05 12:44:21 +01:00
Jim Shield
76855d3246 Add cleanup of targets when switching players 2025-08-05 12:40:36 +01:00
Jim Shield
d6c4619717 Enhance Distance based props and cleanup 2025-08-05 12:38:39 +01:00
Jim Shield
f799e6b2d6 move delete ped loop out of another loop 2025-08-05 12:37:28 +01:00
Jim Shield
2acd506504 Improve cleanup of distance based peds
When changing character, this *should* remove the zone to allow the script to make a new one when switching
2025-08-05 12:34:25 +01:00
Jim Shield
6cfb776eb4 Add function to makeDistPed that runs after creation
Kept forgetting to do this

When using `makeDistPed()` (making a npc that only appears when nearby) you can now send a function to apply custom modifiers to it after the npc is created

eg.
```lua
makeDistPed(v.model[i], b, true, false, v.scenario, nil, nil, function(ped)
	if v["killable"] then
		SetEntityInvincible(ped, false)
		SetBlockingOfNonTemporaryEvents(ped, false)
		FreezeEntityPosition(ped, false)
	end
end)
```
2025-07-29 12:25:31 +01:00
Jim Shield
f09e34b1b5 Version Bump
`2.0.21` - `2.0.22``

- Support for qs-interface
- Fix polyzone minZ + maxZ handling
- Fix ox_target zone location to match qb-target
- Fix "Waiting for script start" loop bug
- Add condition for fading in entities
- Fix typo breaking scripts on gta skillbar fail
- Fix typo for gksphone invoice event
- Fix latio_ui TextUI placement
- Remove registerShop debug message
- Fix typo in print when removing codem stash item
- Fix function typo from origen inv documenation
2025-07-27 20:00:42 +01:00
Jim Shield
1146639d6f Update notify-discord.yml 2025-07-27 18:50:08 +01:00
Jim Shield
a9ac2e8897 Fix typo for orgien_inv getInventory 2025-07-24 21:49:37 +01:00
Jim Shield
1a1960f5a8 Fix entity blips looking for wrong preview script 2025-07-24 21:48:39 +01:00
Jim Shield
996b135973 Improve ox_target zone creation locations 2025-07-24 19:53:30 +01:00
Jim Shield
9337238832 fix typo in debugprint for codem stash staving 2025-07-23 19:53:07 +01:00
Jim Shield
ee049a953c Make ox_lib polyzone conversion code more accurate 2025-07-23 16:46:10 +01:00
Jim Shield
adfe3e8e08 Remove registerShop debug message
People keep thinking this is a massive error, its literally just stating that there isn't a script found to register a shop. not all inventories have these
2025-07-22 16:00:27 +01:00
Jim Shield
71c9c99db7 fix location typo for lation_ui drawtext 2025-07-22 13:04:46 +01:00
Jim Shield
dd66083f45 fix typo for gks sendPhoneInvoice function
when creating a `phone invoice` the function was checking if `gks-phone` was started, but I believe the script folder name is default `gksphone`
2025-07-22 11:44:32 +01:00
Jim Shield
9ddd6422bd fix typo breaking gta skillcheck if failed 2025-07-22 01:25:14 +01:00
Jim Shield
7d24c274cd Fix ox_lib polyzone maxZ and minZ math 2025-07-20 23:20:43 +01:00
Jim Shield
3911824fc6 Add resourceName sendback for onResource functions 2025-07-17 12:47:36 +01:00
Jim Shield
9729d2fef1 Add condition for stopping entity fade in 2025-07-17 12:40:43 +01:00
Jim Shield
3cb4005889 Merge pull request #51 from oosayeroo/main 2025-07-17 12:39:48 +01:00
Jim Shield
bf759ebaf8 Fix inventory config check to check "starting" scripts 2025-07-17 12:38:31 +01:00
Jim Shield
3d0eb3df65 fix PolyZone support for minZ maxZ 2025-07-17 12:37:41 +01:00
oosayeroo
fe09d0f773 qs notify + progressbar + makeVeh edit 2025-07-16 08:18:55 +01:00
Jim Shield
880d2b0233 Version Bump
`2.0.20` - `2.0.21`
2025-07-14 11:29:29 +01:00
Jim Shield
5865d2eeb2 increase authToken timeout from 5 sec to 1 min 2025-07-13 17:41:06 +01:00
Jim Shield
528735abd1 fix anim flag on lation_ui progressbars 2025-07-12 22:24:28 +01:00
Jim Shield
d4df995c71 fix lation_ui progressbars returning true when cancelled 2025-07-12 13:05:10 +01:00
Jim Shield
3b13fa663e Merge pull request #49 from derTweaky/patch-1 2025-07-12 11:31:16 +01:00
derTweaky
e973c64ffd Update makePed.lua
Typo fix
2025-07-12 12:27:31 +02:00
Jim Shield
6ecf10c084 fix inventory config loading if vector4 is present 2025-07-11 21:16:45 +01:00
Jim Shield
79e765fbb0 Stop crafting trying to check blank stash names
Skimmed through the stash checking and editing functions and added more "security" for handling nil or blank stash names
2025-07-11 13:33:44 +01:00
Jim Shield
d4a08d5e2b Rewrite waitForSharedLoad() to fix issues 2025-07-10 22:41:28 +01:00
Jim Shield
eeb3f40018 fix break when trying to fade ents too far away 2025-07-10 22:16:26 +01:00
Jim Shield
e6b4c1d13c Add fade in animations for entity spawning 2025-07-09 15:10:53 +01:00
Jim Shield
6ecbc3392a make breaktool() smarter and add useToolDegrade() 2025-07-09 15:09:41 +01:00
Jim Shield
7f10c394de make cache timeout optional 2025-07-09 15:09:14 +01:00
Jim Shield
fae099fd93 fix progressbar looking for missing function 2025-07-08 11:20:52 +01:00
Jim Shield
64d43b54e9 fix getPlayer() checking users not logged in yet 2025-07-08 00:48:39 +01:00
Jim Shield
d4abdbc93f fix typo stopping mysql cache loading on esx 2025-07-07 17:56:23 +01:00
Jim Shield
b595453efe Rename metahandler functions
Realised I named the player and item metadata functions too closely and made it confusing, this separates them better
2025-07-07 17:47:04 +01:00
Jim Shield
d255034c02 Increase timer again on cache timeout
Also moved around the cache timers to be a bit more accurate (timers start when the cache creation is triggered)
2025-07-07 02:11:36 +01:00
Jim Shield
d6fd2cc28c Fix toggleDuty() for ESX 2025-07-07 00:09:34 +01:00
Jim Shield
7c47de5dc0 Version Bump
`2.0.19` - `2.0.20`
2025-07-06 19:16:01 +01:00
Jim Shield
c1ab2b3505 Support only one progressBar when crafting 2025-07-06 19:13:10 +01:00
Jim Shield
1665ab41de fix multicraft menu for lation_ui 2025-07-06 19:01:10 +01:00
Jim Shield
6d397b59bf fix onExit support for lation_ui 2025-07-05 20:52:08 +01:00
Jim Shield
9dfbcbe878 fix typo breaking tgiann CanCarryItem export check 2025-07-05 13:24:19 +01:00
Jim Shield
ca0970b650 Add fallback config support for QBInv inventory
Enhanced the inventory config loader to support both old and new config file structures for QBInv by introducing a fallback mechanism. The loader now attempts multiple config paths to improve compatibility with different versions.
2025-07-05 11:11:51 +01:00
Jim Shield
02218a4902 enhance dosItemExist() and add getItemLabel() 2025-07-05 00:33:26 +01:00
Jim Shield
a900044306 Fix hang when using jpr-inventory
The script was randomly looping the config file check, if it hit qb-inventory first it would end the check and break the script, changed it to skip instead and fixed it
2025-07-04 23:44:37 +01:00
Jim Shield
7382a534a0 I THINK jim_bridge now fully supports lation_ui 2025-07-04 18:56:19 +01:00
Jim Shield
3b76b15bb4 Add starting support for lation_ui
Added support for:
- Menus
- Draw Text
- Notifications
- Skillchecks
- ProgressBar

TODO:
- Input dialogs
2025-07-04 15:20:02 +01:00
Jim Shield
f2eda0fef6 Add indicator of ingredient items check location 2025-07-04 15:17:56 +01:00
Jim Shield
61c1cce814 Increase cache export timeout to 2 minutes 2025-07-04 15:16:53 +01:00
Jim Shield
0f1660e3ed remove debug print for ox_inv 2025-07-01 13:21:59 +01:00
Jim Shield
e5b55e49b7 Add missing functions for playing animations
Forgot to include animation functions for progressbar ui_module when porting it over
2025-07-01 13:07:36 +01:00
Jim Shield
1ff01526bc Add larger timeouts for caching 2025-06-30 23:19:23 +01:00
Jim Shield
1ed227080b Add missing "Open Wheel" to searchCar() function 2025-06-30 23:18:43 +01:00
Jim Shield
b07c089946 Version Bump
`2.0.18` - `2.0.19`
2025-06-28 20:08:51 +01:00
Jim Shield
d603067c77 Remove files i was testing with (forgot) 2025-06-28 20:04:50 +01:00
Jim Shield
5a97b8f83a Fix script break stop if config file not found 2025-06-28 11:22:12 +01:00
Jim Shield
d0a09871c7 Fix for if using QBOX without ox_inventory
Realised that if using older (and custom) versions of qbox, the item list can't be retrieved through `GetCoreObject()`

This fixes that and also (if needed) grabs the item lists from inventories directly if needed
2025-06-27 13:35:28 +01:00
Jim Shield
e87aa8d74d Fix server lag when loading scripts
This was my bad, set up functions that requested the cache from server, didn't realise it was duplicating it and spamming it

This moves the requests to a separate file and uses an export instead

Also hopefully fixes ox_lib images being an arse
2025-06-26 23:15:33 +01:00
Jim Shield
e204e6153d remove debug prints
oops
2025-06-26 20:08:31 +01:00
Jim Shield
5c69f8dd28 add new function hasFreeInventorySlots()
using the function to grab slot info, this can now check if you have enough inventory slots for items to be given, allowing it to check before TRYING to add them
2025-06-26 20:04:32 +01:00
Jim Shield
f3fa309dbf Revamp framework caching
People were reporting issues with scripts claiming items didn't exist,  I believe this was due to their servers having slot starts (eg. alot of clothes or vehicles being loaded)

I've rewritten it to fully delay loading until everything is correctly cached and let the scripts continue. Also hopefully reducing memory footprint for it too.

---
Also slipped in an `experimental` function that grabs inventory config files to check their set InventoryWeight and InventorySlots to automatically apply it for other functions
---
2025-06-26 20:03:12 +01:00
Jim Shield
9d74e7cb46 Update setMetadata server event
Before it was JUST for durability but fixed it to work for all item metadata
2025-06-26 19:57:03 +01:00
Jim Shield
fea72bba2e hotfix ox_lib progressbars stopping movement 2025-06-23 12:54:24 +01:00
Jim Shield
b364f14260 Version Bump
`2.0.17` - `2.0.18`
2025-06-22 22:16:17 +01:00
Jim Shield
8077ec4f46 Add functions to try and get players inv weights 2025-06-22 20:03:12 +01:00
Jim Shield
e5277574df Rework Auto detection of QBInvNew
It's messy but hopefully it will work as expected
2025-06-22 17:45:33 +01:00
Jim Shield
e0563dd4ce Add extra triggerevent for ps-inventory
They keep changing their event names 🤷‍♀️
added a duplicate with `ps-` at the start in hopes this triggers things correctly when opening a stash
2025-06-22 17:45:04 +01:00
Jim Shield
1987cf3df3 Fix IsAnimal checks getting confused 2025-06-22 16:51:19 +01:00
Jim Shield
9f5145c3b2 Update progressBars.lua 2025-06-22 16:38:02 +01:00
Jim Shield
5921cf8adc ProgressBar default variable fixes 2025-06-22 16:14:32 +01:00
Jim Shield
f154b45076 Enhance built in ui_modules to support RedM 2025-06-21 22:34:03 +01:00
Jim Shield
2c41272c36 Remove ClearStash export call for ps-inv 2025-06-21 18:28:50 +01:00
McKlean
57d4beafed Commit[crafting]: commented print causing errors
- debugPrint was causing crafting to error
2025-06-21 16:32:49 +01:00
McKlean
bdb526bd1e Commit[Billing]: added okok Billing
- Added OKOK's CreateInvoice as a choice for the billing
2025-06-21 14:52:09 +01:00
Jim Shield
bb9dbe248f Update callback.lua 2025-06-21 13:56:49 +01:00
Jim Shield
caf2e7fbed add support for esx_society to openBossMenu() 2025-06-21 13:29:42 +01:00
Jim Shield
d0b8fb16d6 fix for if ox_inv can't find an item when trying to get image 2025-06-21 13:29:01 +01:00
Jim Shield
2e57e66095 Fix confusion with old and new inv shop exports 2025-06-20 23:33:44 +01:00
Jim Shield
05fc856e86 small tidy up 2025-06-20 21:53:44 +01:00
Jim Shield
74e6a679e2 Tidy up onPlayerLoaded for all frameworks 2025-06-20 21:32:08 +01:00
Jim Shield
17db5aa31a Fix QBCore OnPlayerUnload event not triggering 2025-06-20 21:12:53 +01:00
Jim Shield
387c97de71 Fix jpr erroring when clearStash is triggered 2025-06-20 20:56:45 +01:00
Jim Shield
de45491c8c move exploit protection to new file
Moves Auth Token stuff to separate file because it doesn't really need to be in `itemcontrol.lua`

Helps me keep track of it better too

Also added a "cooldown" to workaround people's multicharacter scripts claiming the players loaded more than once and breaking it
2025-06-20 20:55:49 +01:00
Jim Shield
4da4cbfc15 Tidy up and organise several debugPrints better 2025-06-20 18:03:08 +01:00
Jim Shield
9fbf2f2211 Make frameworkCache.lua smurter
Add better checks for if theres errors in ox_inv items and add more prominent error messages all around
2025-06-20 17:47:54 +01:00
Jim Shield
41b2965743 Merge pull request #47 from oosayeroo/main 2025-06-20 13:35:45 +01:00
Jim Shield
a02c11d3cc Update crafting.lua 2025-06-19 22:18:53 +01:00
Jim Shield
c888aa5ecf Fix ox_inv item client info (images) caching 2025-06-19 22:15:49 +01:00
McKlean
30de9a2d9c Version Bump 2025-06-18 20:52:04 +01:00
oosayeroo
804327f3aa Merge branch 'jimathy:main' into main 2025-06-18 19:30:56 +01:00
oosayeroo
ba59418b6d return values (boolean)
- added return values on following functions
:fundPlayer
:chargePlayer

- added check for Config.System in Notify.lua
2025-06-18 19:30:21 +01:00
Jim Shield
f39678002f Fix progressbars getting stuck on ox_lib 2025-06-18 12:02:03 +01:00
McKlean
3e402cae3d Commit[progressBar]: ox progress options
- Added ability to choose circle/bar type
- Added ability for props when using progbar
2025-06-16 19:02:46 +01:00
Jim Shield
b4aa1b3eda Version Bump
`2.0.15 - 2.0.16`
2025-06-16 11:05:46 +01:00
Jim Shield
0f9bab00ef add openBossmenu() function 2025-06-16 00:10:32 +01:00
Jim Shield
9d8706e8c6 Tidy up setThirst() and setHunger() functions
added debug print and fixed non-esx functions setting needs wrong
2025-06-14 22:32:58 +01:00
Jim Shield
6ba1e14888 Change crafting menu slightly to make it more readable 2025-06-14 14:44:16 +01:00
Jim Shield
0454f8c0a9 Add full support for JPR-Inventory
Thanks to JPR for providing access to make this possible

https://jpresources.tebex.io/package/5284060
2025-06-13 23:25:32 +01:00
Jim Shield
e17d8f2b93 Merge pull request #46 from oosayeroo/main 2025-06-13 18:43:21 +01:00
oosayeroo
b44dfa91e5 Merge branch 'jimathy:main' into main 2025-06-11 18:44:49 +01:00
oosayeroo
51d4c88694 light fix (in case of missing Crafting Var)
- added check for missing Config.Crafting variables

- reduces errors in scripts if not added to config
2025-06-11 18:43:31 +01:00
Jim Shield
f5f502875e Comment out a couple debug prints for now 2025-06-11 16:30:51 +01:00
Jim Shield
9c769f4e8e Update frameworkCache.lua 2025-06-11 16:30:29 +01:00
Jim Shield
87cdd5502e Change QBInvNew export because i forgor
I forgot that it needed to check the client side too for the change, but I set it to check for a server only export

got lucky and realised `HasItem` is an export on both client and server
2025-06-10 21:47:12 +01:00
Jim Shield
68d713a8dc Add warnings for QB if theres an error in shared files
If `jim_bridge` attempts to find shared files, and it returns `nil` it will print an error message stating it couldn't find them.

So..people can stop blaming me for my script not working when its an error in their vehicles.lua
2025-06-10 18:29:10 +01:00
Jim Shield
dd34f8aab1 Add auto checkers for QBInvNew
I had to add a checker for if old qbinv was being used or new qbinv

This was made into a variable, but this update makes the script auto set it if it can't find an export. Automating the process
2025-06-10 18:26:57 +01:00
Jim Shield
4a3fb8db4d Add "support" for CodeM shops
This is a tricky one to support

They handle their shops inside their script with no way to add one from the outside

They give the ability to trigger a shop from outside the script, but not create one

This just adds that trigger for now
2025-06-07 19:50:34 +01:00
Jim Shield
59733de08e support for npwd_qbx_mail addon for sending mail 2025-06-07 19:48:18 +01:00
Jim Shield
2b7a1c2e6d Start working on billPlayer() function
The intention here is to try and add multiple "billing" script functions, so the user can pre-set it for scripts that use `jim_bridge`

Currently only handles `jim-payments`
2025-06-07 18:14:53 +01:00
Jim Shield
5db9e88bb9 add function checkExportExists()
Workaround function made to detect if new QB Inventory functions are available

This can be used in other situations like checking if a script is working correctly or not and the export has been registered
2025-06-07 18:13:06 +01:00
Jim Shield
a5b1f750dd Fix weapons caching for ox_inv
I completely overlooked the fact that when ox_inv shows weapon info, it does it with the upper case version `WEAPON_PISTOL` while my scripts were looking for `weapon_pistol`

making my scripts think they didn't exist

This fixes that
2025-06-06 19:36:12 +01:00
Jim Shield
4c514b459e Merge branch 'main' of https://github.com/jimathy/jim_bridge 2025-06-06 01:02:34 +01:00
Jim Shield
32b99d14e1 Version Bump
`2.0.14` - `2.0.15`
2025-06-06 01:01:31 +01:00
Jim Shield
de8f7c050c Complete refactor of handling of framework info
I've moved the framework caching to a new file `frameworkCache.lua`

This now caches the framework data once when `jim_bridge` starts

Then in `coreloader.lua` now, the information that is cached is now called from that script

This also includes better handling of ESX cached data and should stop errors

This greatly optimizes the loading of all my scripts and lowers server load to speed up server start times

I have been testing this for just under a week and 6 different test servers and it appears to work fine
2025-06-06 00:50:13 +01:00
Jim Shield
9c4c92e696 Move GTA notify, target, skillcheck to /ui_modules
I've separated the built-in custom native GTA Notifications, skillcheck and draw text targets to separate files to be loaded along side `jim_bridge`

This now makes it so they only load once instead of with every single script, and each one can call on it

This change also makes these systems able to be called outside of `jim_bridge`

For example in my test server I'm currently using:

```lua
function QBCore.Functions.Notify(text, texttype, length, icon)
    CreateThread(function()
        exports.jim_bridge:Notify(nil, text, icon)
    end)
end
```
2025-06-06 00:45:08 +01:00
Jim Shield
0cedd85afb Add optional warmenu.lua for gta menus 2025-06-06 00:39:24 +01:00
Jim Shield
1bb125ed31 Add clearStash() function 2025-06-06 00:38:51 +01:00
Jim Shield
72778235b8 Merge pull request #45 from oosayeroo/main 2025-06-06 00:37:33 +01:00
Jim Shield
76c34b8c03 Unify isInventoryOpen() function
This should fix issues for crafting complaining inventories are open when they aren't

I was trying to use per inventory checks for them being open until I realised I could just use `IsNuiFocused()` for all inventories to check if there was an nui on the screen that used the mouse, this should be a good workaround for it
2025-06-04 13:53:49 +01:00
Jim Shield
3ca2da2990 Try and enhance built in draw text with job+item checks 2025-06-03 22:22:49 +01:00
Jim Shield
d3035efd5c breakout from inventory exploit during crafting 2025-06-03 16:56:56 +01:00
oosayeroo
64a74a8928 Merge branch 'jimathy:main' into main 2025-06-03 08:54:36 +01:00
oosayeroo
e0fcf23dba added simple "doesItemExist(item)" function
- very simple function to check existance of an item
2025-06-03 08:52:06 +01:00
Jim Shield
ad1ce5f9ea Fix Old QBinv and new/old Psinv stashes being wiped
This appeared to only happen with `jim-mechanic` repair from stash, due to the stashitem's table not being sent to the function
Making it save an empty table to the database

But this was possible in other places too

I've added fallbacks for if it didn't receive a table, grab a fresh one from cache/database
2025-06-02 11:20:47 +01:00
Jim Shield
c3ce0ccc54 Version Bump
`2.0.13` - `2.0.14`
2025-05-31 12:18:44 +01:00
Jim Shield
0b7c96f66a Possibly fix QS Stashes wiping at script start 2025-05-31 12:12:15 +01:00
Jim Shield
3daa6a9772 Better workarounds for no info yet to getPlayer() 2025-05-26 19:10:54 +01:00
Jim Shield
c86cdf9227 Remove "Offline" check from canCarry() 2025-05-26 16:21:25 +01:00
Jim Shield
ca78c1b100 Load QBOX Weaps into Item list 2025-05-26 16:10:41 +01:00
Jim Shield
398701c4d7 attempt to fix support of ox_core job gathering 2025-05-26 16:08:51 +01:00
Jim Shield
00e420fd45 Add fix for ox_core injecting ox_lib itself 2025-05-26 15:58:49 +01:00
Jim Shield
a07cbc467d Merge branch 'main' of https://github.com/jimathy/jim_bridge 2025-05-24 11:01:24 +01:00
Jim Shield
ccd5144e7b Merge pull request #44 from dragon703/main 2025-05-24 11:00:13 +01:00
Jim Shield
195182f850 Merge pull request #43 from oosayeroo/main 2025-05-24 10:56:20 +01:00
Aditya Ray
c2b48c444c Support of Society Bank Features using Tgiann-Bank
Modified functions for fetching society account balance, charging society account and funding society accounts using Tgiann-Bank script.
2025-05-23 17:03:06 +05:30
Jim Shield
156b44a49b change blipinfo stuff to jim-blipcontroller 2025-05-21 21:04:29 +01:00
Jim Shield
a48b83155c add fallback to sellMenu if no locale system 2025-05-21 18:23:38 +01:00
Jim Shield
e455d1b43d getPlayer() fallback if player hasn't loaded yet 2025-05-21 15:38:56 +01:00
Jim Shield
14c2a79673 Version Bump
`2.0.12` - `2.0.13`
2025-05-19 23:39:46 +01:00
Jim Shield
97a1d97678 add random number helper
This function was created to help create random numbers
When you create a table of items with a random hunger value for example
`hunger = math.random(10, 20)` this will be set at script start and never change
using `hunger = {10, 20}` and then calling this function will make it generate a random number every time
It also fallsback if it simply recieved a number instead of a table
-- For example:
-- local hunger = {10, 20}
-- local hungerAmount = GetRandomTiming(hunger)
-- print(hungerAmount) -- number between 10, 20
2025-05-19 19:53:52 +01:00
Jim Shield
5001642504 Update version.txt 2025-05-19 17:14:41 +01:00
Jim Shield
5f413a2823 Update version checkers to support mini changelogs 2025-05-19 17:10:14 +01:00
Jim Shield
4d04682b33 Version Bump
`2.0.11` - `2.0.12`
2025-05-18 22:34:08 +01:00
Jim Shield
cb38d275d1 People were asking how to remove a print
I kept getting people asking how to remove prints because it "was filling up their f8 menu and looked like errors"

So I've made the "success" coreloader prints to be debugmode only.
2025-05-18 16:32:29 +01:00
Jim Shield
84cfe1c97d Fix stash stash opening for some inventories
Mainly older inventories may have had issues with custom stash sizes when opening them

This should fix compatibility with changing slots and max weight with them on trigger
2025-05-18 16:31:30 +01:00
Jim Shield
47b01df621 Remove coords from openGrabBox 2025-05-17 20:38:21 +01:00
Jim Shield
7e073b16ec Update starter.lua 2025-05-17 20:23:32 +01:00
Jim Shield
ab4298fa2d Change coreloader prints to a debugPrint 2025-05-17 19:19:15 +01:00
Jim Shield
7997ea45b7 change "Shared Load Detected" to a debugPrint 2025-05-17 19:16:24 +01:00
Jim Shield
7aa9d90d0d Improve ESX Job cache builder
When collecting Jobs from the server it tries it's best to determine which role is the "Boss"

It checks first of the Job Grade label containing the word "Boss"

If it can't find it, it then uses the highest number grade and adds `.isBoss = true` to it
2025-05-17 16:48:23 +01:00
oosayeroo
451e4cbbdb better documentation layout
- seperated the 2000+ lines in ReadMe.md into seperate files.
 - useful if things expand
 - better readability for users
 - more organised layout
2025-05-16 14:28:41 +01:00
Jim Shield
520758b355 Version Bump
`2.0.10` - `2.0.11`
2025-05-15 20:20:47 +01:00
Jim Shield
e4e9ef0fb2 add openGrabBox event for items like murderbag 2025-05-15 20:02:39 +01:00
Jim Shield
ada724069b Revert qs-inv server inv grab
Found the real issue that was causing the scripts to not think you had items, basically reverting this change as i didn't like having it in the first place
2025-05-15 19:59:51 +01:00
Jim Shield
9198068a19 Adjust hasItem function for QS-Inv
Apparently qs-inv reports two variables on the item data for how you have
`count` and `amount`
while other inventories appear to only have one of those

I'm assuming for compatibility sake

Apparently the `count` is updated differently, and `hasItem()` was checking for this first

I've swapped round the checks and appears to work better
2025-05-15 19:55:14 +01:00
Jim Shield
263d5160aa Add delay for checking for inventory items on qb
Custom inventories apparently load items separately/later into qbshared

so the script starts thinking there is definitely 0 items but its checking too early, this forces it to wait until its available to continue
2025-05-15 15:31:41 +01:00
Jim Shield
65137207ae add fallback for hasItem if no inventory is found 2025-05-14 23:38:34 +01:00
Jim Shield
3bd9935346 Skip item for sellmenu if item isn't in shared
Adds an item check before creating the sellmenu as to avoid errors
2025-05-14 23:36:46 +01:00
Jim Shield
23e2488cd1 fix typo for qs-inv callback for inventory 2025-05-14 16:09:02 +01:00
78 changed files with 11202 additions and 6106 deletions

12
.gitattributes vendored Normal file
View File

@@ -0,0 +1,12 @@
# VCS / CI noise
.gitattributes export-ignore
.gitignore export-ignore
.github/** export-ignore
.gitmodules export-ignore
# Dev tooling/config
.vscode/** export-ignore
*.code-workspace export-ignore
.editorconfig export-ignore
.eslintrc* export-ignore
.prettier* export-ignore

View File

@@ -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
View 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 wont trigger `on: release` when using GITHUB_TOKEN)
notify-from-workflow-run:
if: >
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Fetch release JSON by tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
TAG="${{ github.event.workflow_run.head_branch }}"
# head_branch is the tag name for a tag push workflow_run
curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$OWNER/$REPO/releases/tags/$TAG" \
> release.json
- name: Wrap as 'release' event payload & notify bot
env:
URL: http://${{ secrets.DISCORDBOT }}:3000/github-releases
run: |
set -euo pipefail
jq -n --slurpfile rel release.json \
--arg repo "$GITHUB_REPOSITORY" \
--arg repo_url "https://github.com/$GITHUB_REPOSITORY" \
--arg action "published" '
{ action: $action,
repository: { full_name: $repo, html_url: $repo_url },
release: $rel[0],
sender: { login: "github-actions[bot]" } }' > payload.json
curl -sS -X POST "$URL" \
-H 'Content-Type: application/json' \
--data-binary "@payload.json"

88
.github/workflows/release.yml vendored Normal file
View File

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

1732
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,78 @@
local 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(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 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 parts
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 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
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/jim_bridge/master/version.txt', function(err, newestVersionRaw, headers)
if not newestVersionRaw then
print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)")
return
end
function CheckBridgeVersion()
if IsDuplicityVersion() then
CreateThread(function()
Wait(4000)
local currentVersionRaw = GetResourceMetadata("jim_bridge", 'version')
--PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/test.txt', function(err, body, headers)
PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, body, headers)
if not body then
print("^1Unable to run version check for ^7'^3jim_bridge^7' (^3"..currentVersionRaw.."^7)")
return
end
newestVersionRaw = newestVersionRaw:match("[^\r\n]+")
local compareResult = compareVersions(currentVersionRaw, newestVersionRaw)
local lines = {}
for line in body:gmatch("[^\r\n]+") do
table.insert(lines, line)
end
if compareResult == 0 then
print("^7'^3jim_bridge^7' - ^2You are running the latest version^7. ^7(^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
-- Made the check a bit more obvious
print("^1----------------------------------------------------------------------^7")
print("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function()
-- Do a naughty repeat message every 20 minutes until the the script is updated
CheckBridgeVersion()
end)
else
print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)")
end
end)
end)
end
end
local newestVersionRaw = lines[1] or "0.0.0"
local changelog = {}
for i = 2, #lines do
table.insert(changelog, lines[i])
end
CheckBridgeVersion()
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
CheckBridgeVersion()
end

32
clientFrameworkCache.lua Normal file
View 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)

View File

@@ -0,0 +1,58 @@
## Animal Ped Support
### isAnimal.lua
This module provides logic to detect if a Ped or model is an animal and classify it into specific categories (cat, dog, coyote, etc.). Useful for wildlife, animal roles, or pet systems.
❔At player load in, it attempts to get what kind of ped/animal you are but there are functions to double check in scripts
❔Alot of this is used to determine what models/animations are available
Global flags:
- `isCat`, `isDog`, `isBigDog`, `isSmallDog`, `isCoyote`, `isAnimal` — used to store classification of the player's current ped.
- **isPedAnimal(ped?)**
- Checks if a ped is an animal based on predefined animal models.
- Sets global `isAnimal` to true if matched.
- **Example:**
```lua
local isPlayerAnimal = isPedAnimal()
local isOtherPedAnimal = isPedAnimal(GetPedInVehicleSeat(vehicle, -1))
```
- **isCat(ped)**
- Returns true if the ped model matches a cat.
- **Example:**
```lua
if isCat() then print("You're a cat!") end
```
- **isDog(ped)**
- Returns two values:
- `true`, `true` — if ped is a big dog
- `true`, `false` — if ped is a small dog
- `false`, `nil` — if not a dog
- **Example:**
```lua
local isDog, isBig = isDog()
if isDog then print(isBig and "Big Dog" or "Small Dog") end
```
- **getAnimalModels()**
- Returns a flat list of all registered animal model hashes.
- Can be used to check ped models against
- **Example:**
```lua
for _, model in pairs(getAnimalModels()) do print(model) end
```
- **getAnimalAnims(ped)**
- Returns the animation set defined for the given animal model.
- **Example:**
```lua
local anims = getAnimalAnims(PlayerPedId())
if anims then playAnim(anims.sitDict, anims.sitAnim) end
```

View File

@@ -0,0 +1,177 @@
### helpers.lua
This utility module provides functions for resource checks, debugging, formatting, coordinate math, vector calculations, progress bars, and drawing tools.
- **isStarted(script)**
- Returns `true` if a resource is started.
- **Example:**
```lua
if isStarted("myResource") then print("Resource is running") end
```
- **getScript()**
- Caches and returns the name of the current resource.
- Easier than typing `GetCurrentResourceName()` over and over
- **Example:**
```lua
print("Current script:", getScript())
```
- **isServer()**
- Returns true if running on the server.
- This was mainly made becuase `IsDuplicityVersion()` kept confusing me
- **Example:**
```lua
if isServer() then print("Server-side!") end
```
- **debugPrint(...)** / **eventPrint(...)**
- Prints messages with debug context if debugMode is enabled.
- **Example:**
```lua
debugPrint("Loaded object:", objName)
```
- **jsonPrint(table)**
- Pretty-prints a table with colorized JSON if debugMode is enabled.
- **Example:**
```lua
jsonPrint(myData)
```
- **keyGen()**
- Generates a 3-character unique ID.
- Good grabbing randomly generated strings
- **Example:**
```lua
print("Generated Key:", keyGen())
```
- **cv(amount)**
- Comma-separates a number (e.g., `1000000` to `1,000,000`).
- **Example:**
```lua
print(cv(1000000)) -- "1,000,000"
```
- **formatCoord(vec)**
- Outputs a formatted string from vector types.
- Compacts and adds console colours to the vector to be printed
- **Example:**
```lua
print(formatCoord(vector3(123.45, 678.9, 10.0)))
```
- **getCenterOfZones(coords)**
- Returns average center position of a vector3 list.
- A few use cases, but I used it to see how well spaced blips were together
- **Example:**
```lua
local center = getCenterOfZones({vector3(0,0,0), vector3(10,10,0)})
```
- **countTable(tbl)**
- Returns the number of entries in a table.
- Simple function to print how many "entires" are in a table
- **Example:**
```lua
print(countTable({a=1,b=2,c=3})) -- 3
```
- **pairsByKeys(tbl)**
- Iterator for sorted keys.
- **Example:**
```lua
for k, v in pairsByKeys(myTable) do print(k, v) end
```
- **concatenateText(tbl)**
- Joins string table entries with newlines.
- **Example:**
```lua
print(concatenateText({"Line 1", "Line 2"}))
```
- **RotationToDirection(rot)**
- Converts a heading vector to a directional vector.
- **Example:**
```lua
local dir = RotationToDirection(rotation)
```
- **basicBar(percent)**
- Returns a bar like `████░░░░░` at 50%.
- Basically a progress bar but as a string, I use this in drawTexts when progressbars aren't able to be used
- **Example:**
```lua
print(basicBar(50))
```
- **normalizeVector(vec)**
- Returns a normalized version of a vector3.
- **Example:**
```lua
local norm = normalizeVector(vector3(3,4,0))
```
- **drawLine(start, end, color)** / **drawSphere(pos, color)**
- Debug drawing helpers.
- Stays visible for more than one frame
- **Example:**
```lua
drawLine(vector3(0,0,0), vector3(10,10,10), vector4(255,0,0,255))
drawSphere(vector3(5,5,5), vector4(0,255,0,255))
```
- **PerformRaycast(start, end, entity?, flags?)**
- Raycast with material detection. Returns ray hit data.
- **Example:**
```lua
local hit, hitPos, material = PerformRaycast(startVec, endVec, playerPed, 1)
if hit == 1 then
print("Hit at position:", hitPos)
print("Material:", material)
end
```
- **adjustForGround(coords)**
- Adjusts a z-coordinate to ground height.
- **Example:**
```lua
coords = adjustForGround(vector3(100, 200, 300))
```
- **ensureNetToVeh(id)** / **ensureNetToEnt(id)**
- Resolves net ID to entity safely.
- **Example:**
```lua
local veh = ensureNetToVeh(netId)
```
- **sendLog(text)** / **sendServerLog(data)**
- Logging helpers, includes player name, coords, script source.
- **Example:**
```lua
sendLog("Suspicious activity detected")
```
- **GetGroundMaterialAtPosition(coords)**
- Returns the material hash + readable name from the surface below coords.
- **Example:**
```lua
local hash, name = GetGroundMaterialAtPosition(vector3(0,0,0))
```
- **GetPropDimensions(model)**
- Loads a model and returns width, depth, height.
- I use this to parse a model to create a box target instead of entity target when creating distProps/distPeds
- **Example:**
```lua
local w,d,h = GetPropDimensions("prop_barrel_01a")
```
- **GetEntityForwardVector(entity)**
- Returns the forward direction vector based on entity heading.
- **Example:**
```lua
local fwd = GetEntityForwardVector(PlayerPedId())
```

View File

@@ -0,0 +1,89 @@
### loaders.lua
This module provides loading utilities for common asset types such as models, animations, texture dictionaries, and audio banks. It also provides animation and sound helpers.
- **loadModel(model)**
- Loads a model into memory if valid and not already loaded.
- **Example:**
```lua
loadModel('prop_chair_01a')
```
- **unloadModel(model)**
- Unloads a model from memory.
- **Example:**
```lua
unloadModel('prop_chair_01a')
```
- **loadAnimDict(animDict)**
- Loads an animation dictionary into memory.
- **Example:**
```lua
loadAnimDict('amb@world_human_hang_out_street@male_c@base')
```
- **unloadAnimDict(animDict)**
- Removes an animation dictionary from memory.
- **Example:**
```lua
unloadAnimDict('amb@world_human_hang_out_street@male_c@base')
```
- **loadPtfxDict(ptFxName)**
- Loads a particle effect (ptfx) dictionary.
- **Example:**
```lua
loadPtfxDict('core')
```
- **unloadPtfxDict(dict)**
- Unloads a particle effect dictionary from memory.
- **Example:**
```lua
unloadPtfxDict('core')
```
- **loadTextureDict(dict)**
- Loads a streamed texture dictionary.
- **Example:**
```lua
loadTextureDict('commonmenu')
```
- **loadScriptBank(bank)**
- Loads a script audio bank.
- Returns true on success.
- **Example:**
```lua
local success = loadScriptBank('DLC_HEISTS_GENERAL_FRONTEND_SOUNDS')
```
- **loadAmbientBank(bank)**
- Loads an ambient audio bank.
- Returns true on success.
- **Example:**
```lua
local success = loadAmbientBank('AMB_REVERB_GENERIC')
```
- **playAnim(animDict, animName, duration?, flag?, ped?, speed?)**
- Plays an animation on a ped.
- Loads the dictionary if not already loaded.
- **Example:**
```lua
playAnim('amb@world_human_hang_out_street@male_c@base', 'base', 5000, 1, PlayerPedId(), 1.0)
```
- **stopAnim(animDict, animName, ped?)**
- Stops an animation and unloads the dictionary.
- **Example:**
```lua
stopAnim('amb@world_human_hang_out_street@male_c@base', 'base', PlayerPedId())
```
- **playGameSound(audioBank, soundSet, soundRef, coords, synced, range?)**
- Plays a game sound from a coordinate or entity.
- **Example:**
```lua
playGameSound('DLC_HEIST_HACKING_SNAKE_SOUNDS', 'Beep', vector3(0, 0, 0), false, 15.0)
```

View File

@@ -0,0 +1,38 @@
### callback.lua
These functions wrap the native callback handling of the selected framework (e.g., OX, QBCore, ESX) instead of implementing a standalone callback system, ensuring full compatibility.
- **createCallback(callbackName, funct)**
- 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.
- It adapts the callback function to match the expected signature for the framework.
- **Example:**
```lua
local table = { ["info"] = "HI" }
createCallback('myCallback', function(source, ...)
return table
end)
createCallback("callback:checkVehicleOwned", function(source, plate)
local result = isVehicleOwned(plate)
if result then
return true
else
return false
end
end)
```
- **triggerCallback(callbackName, ...)**
- Triggers a server callback and returns the result.
- This function uses the appropriate framework's method to call the server-side callback and awaits the result.
- **Example:**
```lua
local result = triggerCallback('myCallback')
jsonPrint(result)
local result = triggerCallback("callback:checkVehicleOwned", plate)
print(result)
```

View File

@@ -0,0 +1,28 @@
### cameras.lua
This module provides utilities for managing temporary in-game cameras, useful for cutscenes, cinematic views, or scripted perspectives.
- **createTempCam(ent, coords)**
- Creates a temporary camera at the specified coordinates or relative to an entity.
- 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.
- **Example:**
```lua
local cam = createTempCam(PlayerPedId(), GetEntityCoords(PlayerPedId()) + vector3(0, 2.0, 1.0))
```
- **startTempCam(cam)**
- Activates and renders the temporary camera.
- **Example:**
```lua
startTempCam(cam)
```
- **stopTempCam()**
- Deactivates and deletes all currently running custom camera, restoring normal view.
- **Example:**
```lua
stopTempCam()
```

View File

@@ -0,0 +1,45 @@
### contextmenus.lua
These functions provide a unified way to interact with different context menu systems, such as OX and WarMenu, depending on what's available on the server.
- **openMenu(Menu, data)**
- Opens a context menu using the preferred menu system.
- Automatically selects between supported systems like OX or WarMenu based on availability.
- The `Menu` parameter should be a list of menu entries, and the `data` parameter can be used to set headers, subtexts, and actions like `onBack`, `onExit`, and `canClose`.
- **Example:**
```lua
openMenu({
{ header = "Option 1", txt = "Description 1", onSelect = function() print("Option 1 selected") end },
{ header = "Option 2", txt = "Description 2", onSelect = function() print("Option 2 selected") end },
}, {
header = "Main Menu",
headertxt = "Select an option",
onBack = function() print("Return selected") end,
onExit = function() print("Menu closed") end,
canClose = true,
})
```
- **isOx()**
- Checks whether the OX context menu system is available on the server.
- Allows to do specific things if ox_lib menu is in use
- **Example:**
```lua
if isOx() then
print("OX Context Menu is available")
end
```
- **isWarMenuOpen()**
- Returns whether WarMenu is currently open.
- Useful to prevent opening a new menu if one is already active.
- **Example:**
```lua
if not isWarMenuOpen() then
openMenu("main_menu", menuData)
end
```
- Returns whether the WarMenu is currently open.

View File

@@ -0,0 +1,39 @@
### crafting.lua
These functions support crafting logic, such as opening crafting menus, handling multi-craft operations, and creating item data from recipes.
- **craftingMenu(data)**
- Opens a menu for selecting the quantity to craft.
- Presents the player with multiple crafting quantities based on `Config.Crafting.MultiCraftAmounts`.
- **Parameters:**
- `item` (`string`): The item to craft.
- `craft` (`table`): The crafting recipe.
- `craftable` (`table`): Crafting options.
- `coords` (`vector3`): Where crafting occurs.
- `stashName` (`string`): The stash name(s) for item availability.
- `onBack` (`function`): Callback when returning.
- `metadata` (`table`, optional): Metadata for the crafted item.
- **Example:**
```lua
craftingMenu({
craftable = {
Header = "Weapon Crafting",
Recipes = {
[1] = {
["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 },
amount = 1,
},
-- More recipes...
},
Anims = {
animDict = "amb@prop_human_parking_meter@male@idle_a",
anim = "idle_a",
},
},
coords = vector3(100.0, 200.0, 300.0),
stashTable = "crafting_stash",
job = "mechanic",
onBack = function() print("Returning to previous menu") end,
})
```

View File

@@ -0,0 +1,25 @@
### drawText.lua
These functions handle drawing and hiding styled on-screen prompts or UI overlays, compatible with different styling frameworks such as OX Lib.
- **drawText(image, input, style, oxStyleTable)**
- Displays styled text or prompts on screen.
- Designed to adapt styling depending on which UI system (e.g., OX) is in use.
- **Parameters:**
- `image` (`string`): Optional icon or image path.
- `input` (`string`): The main text to display.
- `style` (`string|table`): Preset or custom styling.
- `oxStyleTable` (`table`, optional): Extended style options if using OX UI.
- **Example:**
```lua
drawText("img_link", { "Test line 1", "Test Line 2" }, "~g~")
```
- **hideText()**
- Hides any active text or UI element previously drawn with `drawText()`.
- **Example:**
```lua
hideText()
```

View File

@@ -0,0 +1,31 @@
### duifunctions.lua
These functions support dynamic UI image rendering in 3D environments using runtime texture dictionaries. Ideal for integrating `nui://` or external `http://` image URLs into MLOs or world props.
- **createDui(name, http, size, txd))**
- Sets up a runtime texture dictionary and links an image from a `nui://` or `http://` URL.
- This function should be used once to create the texture dictionary needed for rendering Dui images.
- **Example:**
```lua
createDui("logo", "https://example.com/logo.png", { x = 512, y = 256 }, scriptTxd)
```
- **DuiSelect(data)**
- Updates the URL on an existing texture dictionary to change the rendered image.
- Can be used at runtime to swap out visuals in MLOs or props.
- **Parameters:**
- `textureDict` (`string`): The texture dictionary to target.
- `texture` (`string`): The specific texture name to override.
- `url` (`string`): The new image URL.
- `width`, `height` (`number`): The texture resolution.
- **Example:**
```lua
DuiSelect({
name = "logo",
texn = "logoTex",
texd = "someTxd",
size = { x = 512, y = 256 }
})
```

View File

@@ -0,0 +1,25 @@
### input.lua
This function displays a customizable input dialog for user text or number entry. It supports both simple and complex input structures.
- **createInput(title, opts)**
- Opens a styled input dialog with configurable fields, labels, input types, and validation.
- Supports multiple field types including `text`, `number`, `password`, `checkbox`, `color`, `slider`, and more.
- **Parameters:**
- `title` (`string`): Title displayed at the top of the input box.
- `opts` (`table`): A table of fields with attributes like `label`, `name`, `type`, `value`, and more.
- **Example:**
```lua
local userInput = createInput("Enter Details", {
{ type = "text", text = "Name", name = "playerName", isRequired = true },
{ type = "number", text = "Age", name = "playerAge", min = 18, max = 99 },
{ type = "radio", label = "Gender", name = "playerGender", options = {
{ text = "Male", value = "male" },
{ text = "Female", value = "female" },
{ text = "Other", value = "other" },
}},
})
if userInput then
print(json.encode(userInput))
end
```

View File

@@ -0,0 +1,144 @@
### inventories.lua
These functions manage inventory locking, item checking, and player inventory retrieval.
- **lockInv(toggle)**
- Locks or unlocks the player's inventory.
- Freezes/unfreezes movement and toggles hotbar state.
- **Example:**
```lua
lockInv(true) -- Lock inventory
lockInv(false) -- Unlock inventory
```
- **hasItem(items, amount, src)**
- Checks if a player has the specified item(s) in sufficient quantity.
- **Returns:**
- `boolean`: Whether the player has the item(s).
- `table`: Details of available item counts.
- **Example:**
```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
```
- **getPlayerInv(src)**
- Retrieves the players current inventory.
- May be used for inventory UI, logging, or crafting systems.
- **Example:**
```lua
local inventory = getPlayerInv(playerId)
for k, item in pairs(inventory) do
print(item.name, item.amount)
end
```
### itemcontrol.lua
This module includes a variety of utility functions for managing items in player inventories, including giving, removing, durability, and use logic.
- **createUseableItem(item, funct)**
⚠️ This doesn't work for `ox_inv`, you will need to add the event info to it's `items.lua`
- Registers a useable item and binds it to a callback function.
- **Example:**
```lua
createUseableItem("bandage", function(source)
-- Heal logic here
end)
```
- **invImg(item)**
- Returns the inventory image path for an item.
- Automatically grabs the inventory image link based on inventory script
- **Example:**
```lua
local imagePath = invImg("water_bottle")
print(imagePath)
```
- **addItem(item, amount, info, src)**
⚠️ Requires Auth callback if called from client side
- Adds a specific amount of an item to a player's inventory.
- Automatically attempts to add items using detected inventory script
- **Example:**
```lua
-- Client Side
addItem("lockpick", 3, {})
-- Server Side
addItem("lockpick", 3, {}, source)
```
- **removeItem(item, amount, src, slot)**
- Removes a specific amount of an item from a players inventory.
- Automatically attempts to remove item using detected inventory script
- **Example:**
```lua
-- Client Side
removeItem(
"ammo_pistol",
30,
nil,
slot --[[optional]]--
)
-- Server Side
removeItem(
"ammo_pistol",
30,
source,
slot --[[optional]]--
)
```
- **dupeWarn(src, item, amount)**
- Logs or handles a potential duplication exploit involving an item.
- This is called automatically if a player has been triggered exploit detection
- Disabled if debugMode is enabled
- **breakTool(data)**
- Breaks a tool or item, often due to exceeding durability.
- **Example:**
```lua
breakTool({ item = "drill", damage = 10 })
```
- **getDurability(item)**
- Returns the current durability value of a given item.
- Searched for the lowest slot number (eg. slot 1) and retreives that items durability
- **Example:**
```lua
local durability, slot = getDurability("drill")
if durability then
print("Durability:", durability)
print("Slot:", slot)
end
```
- **canCarry(itemTable, src)**
⚠️ Currently server side only
- Checks if a player can carry the item(s) specified in `itemTable`.
- **Example:**
```lua
local carryCheck = canCarry({ ["health_potion"] = 2, ["mana_potion"] = 3 }, playerId)
if carryCheck["health_potion"] and carryCheck["mana_potion"] then
-- Player can carry items.
else
-- Notify player.
end
```

View File

@@ -0,0 +1,64 @@
### jobfunctions.lua
This module provides functions for managing job-based logic, such as checking player roles, toggling duty status, and simulating job-related interactions.
- **makeBossRoles(role)**
- Sets up boss-level permissions or access for the specified job role.
- Often used to determine if a player can access job menus or perform administrative actions.
- Used mainly for creating boss locked target tables
- **Example:**
```lua
makeBossRoles("police")
```
- **jobCheck(job)**
- Simple check if the player has the specified job.
- **Example:**
```lua
if jobCheck("mechanic") then
-- Allow mechanic features.
else
-- Deny access.
end
```
- **toggleDuty()**
- Toggles the players on/off duty status, typically used for jobs like police, EMS, etc.
- **Example:**
```lua
toggleDuty()
```
- **washHands(data)**
- Simulates the action of washing hands, used in job scripts or medical job logic.
- Currently only animation and a notfication
- **Example:**
```lua
washHands({ coords = vector3(200.0, 300.0, 40.0) })
```
- **useToilet(data)**
- Triggers toilet-use interaction, likely includes animation or sound.
- Currently only animation and a notfication
- **Example:**
```lua
useToilet({ urinal = true })
-- Player uses a urinal with corresponding animations and notifications
useToilet({ urinal = false, sitcoords = vector4(215.76, -810.12, 29.73, 90.0) })
-- Player sits down to use a toilet with corresponding animations and notifications
```
- **useDoor(data)**
- Handles interactions with doors that aren't openable and teleports the player
- eg. Recycle Center, it's used to teleport the player from outside a building to the IPL
- **Example:**
```lua
useDoor({ telecoords = vector4(215.76, -810.12, 29.73, 90.0) })
```

View File

@@ -0,0 +1,156 @@
### makeBlip.lua
This module provides simple utilities for adding static or entity-based blips to the minimap.
❔ This has basic support for RedM too
- **makeBlip(data)**
- This function adds a map blip at the provided coordinates and sets various display properties such as sprite, color, scale, and more.
- It also handles attaching a preview image to the blip if certain resources are running and a preview is provided.
- **Example:**
```lua
local blipData = {
coords = vector3(123.4, 567.8, 90.1),
sprite = 1,
col = 2,
scale = 0.8,
disp = 4,
category = 7,
name = "My Blip",
preview = "http://example.com/preview.png"
}
local blip = makeBlip(blipData)
```
- **makeEntityBlip(data)**
- This function adds a map blip attached to the provided entity and sets various display properties such as sprite, color, scale, and more.
- It also handles attaching a preview image to the blip if certain resources are running and a preview is provided.
- **Example:**
```lua
local blipData = {
entity = myEntity,
sprite = 1,
col = 2,
scale = 0.8,
disp = 4,
category = 7,
name = "Entity Blip",
preview = "http://example.com/preview.png"
}
local blip = makeEntityBlip(blipData)
```
### makePed.lua
This module provides tools to create persistent or distance-based NPCs with optional animations, scenarios, and config randomization.
- **makeDistPed(data, coords, freeze, collision, scenario, anim, synced)**
- Creates a ped that only spawns when nearby (performance optimization).
- **Example:**
```lua
makeDistPed({model = "a_m_y_business_03" }, -- model data
vector4(450.0, -980.0, 30.0, 100.0), -- coords
true, -- freeze entity
false, -- collision
'WORLD_HUMAN_STAND_IMPATIENT', -- Scenario Animation
{ dict = "amb@world_human_clipboard@male@idle_a", anim = "idle_c" }, -- Anim Table
true -- Network Synced
)
```
- **makePed(data, coords, freeze, collision, scenario, anim, synced)**
- Spawns a ped with more persistent behavior at the given location.
- Supports animation playback and freezing.
- **Example:**
```lua
makePed({model = "a_m_y_business_03" }, -- model data
vector4(450.0, -980.0, 30.0, 100.0), -- coords
true, -- freeze entity
false, -- collision
'WORLD_HUMAN_STAND_IMPATIENT', -- Scenario Animation
{ dict = "amb@world_human_clipboard@male@idle_a", anim = "idle_c" }, -- Anim Table
true -- Network Synced
)
```
- **GenerateRandomPedData(data)**
- Returns randomized ped model/configuration based on input table.
- Useful for dynamic NPC generation.
- **Example:**
```lua
local pedData = GenerateRandomPedData({ model = `MP_M_Freemode_01`, custom = {} })
```
### makeProp.lua
This module allows you to spawn static or distance-loaded props in the world.
- **makeProp(data, freeze, synced)**
- This function loads the model, creates the object, sets its heading, and freezes it if specified.
- **Example:**
```lua
local propData = {
prop = 'prop_chair_01a',
coords = vector4(123.4, 567.8, 90.1, 180.0)
}
local prop = makeProp(propData, true, false)
```
- **makeDistProp(data, freeze, synced, range)**
- Same as `makeProp`, but only spawns if the player is within the specified range.
- **Example:**
```lua
local propData = {
prop = 'prop_chair_01a',
coords = vector4(123.4, 567.8, 90.1, 180.0)
}
makeDistProp(propData, true, false)
```
- **destroyProp(entity)**
- Removes a previously created or targeted prop from the world.
- **Example:**
```lua
destroyProp(barrel)
```
### makeVeh.lua
This module provides functionality for spawning vehicles, including distance-based optimization and entity management.
- **makeVeh(model, coords)**
- This function loads the vehicle model, creates the vehicle in the world at the given coordinates, sets initial properties, and returns the vehicle handle.
- **Example:**
```lua
local vehicle = makeVeh("adder", vector3(250.0, -1000.0, 30.0))
```
- **makeDistVehicle(data, radius, onEnter, onExit)**
- Creates a vehicle that spawns when the player enters a designated polyzone area.
- This is used for `jim-parking` to create a static vehicle that can't move
- **Example:**
```lua
makeDistVehicle({
model = "blista",
coords = vector3(400.0, -800.0, 30.0),
heading = 180.0
}, 50.0)
```
- **removeDistVehicleZone(zoneId)**
- Removes a previously created distance-based vehicle zone.
- **Example:**
```lua
removeDistVehicleZone("garage_zone_1")
```
- **deleteVehicle(vehicle)**
- Deletes a specified vehicle entity.
- **Example:**
```lua
deleteVehicle(vehicle)
```

View File

@@ -0,0 +1,32 @@
### metaHandlers.lua
This module provides access and control over player metadata, which is useful for storing temporary or persistent player-specific values like stats, states, or tags.
- **GetPlayer(source)**
⚠️ Server side only
- Retrieves the player object (as defined by the framework in use) from the given `source` ID.
- **Example:**
```lua
local player = GetPlayer(playerId)
```
- **GetMetadata(player, key)**
- Retrieves the value of a metadata field from the given player.
- If called client-side (player is nil), it triggers a server callback to retrieve metadata.
- **Example:**
```lua
local stress = GetMetadata(player, "stress")
print("Player stress level:", stress)
```
- **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
setPlayerMetadata(player, "stress", 0)
```

View File

@@ -0,0 +1,21 @@
### notify.lua
This module provides a unified interface to trigger styled notifications across supported frameworks.
- **triggerNotify(title, message, type, src)**
- Sends a notification to a player (or to the entire client if `src` is `nil`).
- The notification style is determined by `type` (e.g., `success`, `error`, `info`).
- **Parameters:**
- `title` (`string`): The title or header of the notification.
- `message` (`string`): The body or detail text.
- `type` (`string`): Type of message (e.g., "success", "error", "info").
- `src` (`number`, optional): Server ID of the player to notify (omit to notify locally).
- **Example:**
```lua
-- Client-side usage without specifying a player (shows to the current player)
triggerNotify("Success", "You have completed the task!", "success")
-- Server-side usage specifying a player by their server ID
triggerNotify("Alert", "You have been warned for misconduct.", "error", source)
```

View File

@@ -0,0 +1,21 @@
### phones.lua
This module allows you to send in-game mail/messages to player phones, depending on the phone system integrated (e.g., QB, NPWD).
- **sendPhoneMail(data)**
- Sends a mail message to a player's in-game phone app.
- Typically used to notify players about deliveries, missions, or reminders.
- **Parameters:**
- `data` (`table`): Must contain phone/mail system-compatible fields like `sender`, `subject`, `message`, and `receiver`.
- **Example:**
```lua
sendPhoneMail({
subject = "Welcome!",
sender = "Admin",
message = "Thank you for joining our server.",
actions = {
{ label = "Reply", action = replyFunction }
}
})
```

View File

@@ -0,0 +1,115 @@
### playerfunctions.lua
This module contains helper functions for manipulating player states, interactions, and utility checks.
- **instantLookEnt(ent, ent2)**
- Instantly turns an entity to face a target (entity or coordinates) without animation.
- **Example:**
```lua
instantLookEnt(nil, vector3(200.0, 300.0, 40.0))
instantLookEnt(ped1, ped2)
```
- **lookEnt(entity)**
- Makes the current player look toward the given entity.
- Usually called after when opening a menu or something similar to make the player visually face the location
- **Example:**
```lua
lookEnt(vector3(200.0, 300.0, 40.0))
lookEnt(pedEntity)
```
- **setThirst(src, thirst)**
⚠️ Server Side Only
- Sets the thirst level of a player.
- **Example:**
```lua
setThirst(source, 75)
```
- **setHunger(src, hunger)**
⚠️ Server Side Only
- Sets the hunger level of a player.
- **Example:**
```lua
setHunger(source, 50)
```
- **chargePlayer(cost, moneyType, newsrc)**
⚠️ Server Side Only
- Deducts money from a player of the specified type (`cash`, `bank`, etc).
- **Example:**
```lua
chargePlayer(100, "cash", source)
```
- **fundPlayer(fund, moneyType, newsrc)**
⚠️ Server Side Only
- Adds money to a player's balance of a given type.
- **Example:**
```lua
fundPlayer(250, "bank", source)
```
- **ConsumeSuccess(itemName, type, data)**
- Handles logic when an item is successfully consumed (e.g., food, drink, etc).
- Supports hunger and thirst info directly from table eg. `{ hunger = 10, thirst = 20 }`
- **Example:**
```lua
ConsumeSuccess("health_pack", "food", { hunger = 10 })
ConsumeSuccess("beer", "alcohol", { thirst = 20 })
```
- **hasJob(job, source, grade)**
- Checks if a player has a certain job and optionally checks for a specific grade.
- Similar to `jobCheck()` but also retrieves as much player job info as possible
- **Example:**
```lua
-- Check if the player has the 'police' job and is on duty
local hasPoliceJob, isOnDuty = hasJob("police")
if hasPoliceJob and isOnDuty then
-- Grant access to police-specific features
end
-- Check if a specific player has the 'gang_leader' job with at least grade 2
local hasGangLeaderJob, _ = hasJob("gang_leader", playerId, 2)
if hasGangLeaderJob then
-- Allow gang leader actions
end
```
- **getPlayer(source)**
- Retrieves basic player information (name, cash, bank, job, etc.) based on the active core/inventory system.
- Can be called server-side (passing a player source) or client-side (for current player).
- Called often in my scripts as its makes use of frameworks "GetPlayerData" etc.
- **Example:**
```lua
-- Get information for a specific player
local playerInfo = getPlayer(playerId)
print(playerInfo.name, playerInfo.cash, playerInfo.bank)
-- Get information for the current player (client-side)
local myInfo = getPlayer()
print(myInfo.name, myInfo.cash, myInfo.bank)
```
- **GetPlayersFromCoords(coords, radius)**
- Returns a list of players within a specified radius of a set of coordinates.
- **Example:**
```lua
local nearby = GetPlayersFromCoords(GetEntityCoords(PlayerPedId()), 10.0)
for _, playerId in pairs(nearby) do
print("Nearby player ID:", playerId)
end
```

View File

@@ -0,0 +1,44 @@
### polyZone.lua
This module provides helpers for creating and removing polygon or circular zones using PolyZone-compatible data structures.
- **createPoly(data)**
- Creates a polygonal zone using the detected polyzone library (ox_lib or PolyZone).
- Automatically checks which polyzone script is active. When using ox_lib, it converts the provided 2D points to 3D (setting a constant z value) and sets a thickness value.
- For PolyZone, it creates the zone and attaches onEnter and onExit callbacks for ease of use.
- **Example:**
```lua
createPoly({
name = 'testZone',
debug = true,
points = { vec2(100.0, 100.0), vec2(200.0, 100.0), vec2(200.0, 200.0), vec2(100.0, 200.0) },
onEnter = function() print("Entered Test Zone") end,
onExit = function() print("Exited Test Zone") end,
})
```
- **createCirclePoly(data)**
- When using ox_lib, it creates a sphere zone. For PolyZone, it creates a CircleZone and attaches onEnter and onExit callbacks.
- **Example:**
```lua
createCirclePoly({
name = 'circleZone',
coords = vector3(150.0, 150.0, 20.0),
radius = 50.0,
onEnter = function() print("Entered Circle Zone") end,
onExit = function() print("Exited Circle Zone") end,
})
```
- **removePolyZone(Location)**
- Removes a previously created polygon or circle zone by name.
- Detects the active polyzone library and calls the appropriate removal method.
- **Example:**
```lua
local zone = createPoly({...})
---
removePolyZone(zone)
```

View File

@@ -0,0 +1,23 @@
### progressBars.lua
This module provides a unified progress bar system compatible with various UI frameworks.
❔It also contains an experimental "Shared Progressbar" system which was created for things like "giving an item to another player"
- **progressBar(data)**
- Displays a progress bar using the current configured system (e.g., ox_lib, qb, etc).
- **Example:**
```lua
local success = progressBar({
label = "Processing...",
time = 5000,
dict = "amb@world_human_hang_out_street@female_hold_arm@base",
anim = "base",
flag = 49,
cancel = true,
})
if success then
print("Success!")
else
print("Cancelled")
end
```

View File

@@ -0,0 +1,24 @@
### scaleEntity.lua
This utility provides a simple interface to scale an entity (ped, object, vehicle) in the world.
⚠️ **Important:** Unless the model's collision is removed or modified, the scale will reset when interacted with by other entities (e.g., walking into it, driving over it).
- **scaleEntity(entity, scale)**
- Scales the specified entity by the given factor.
- **Parameters:**
- `entity` (`number`): Entity handle (ped, object, vehicle).
- `scale` (`number`): Scale factor (e.g., `1.0` is normal size, `0.5` is half size).
- **Example:**
```lua
scaleEntity(PlayerPedId(), 0.8) -- Shrinks player slightly
```
- **resetScale(entity)**
- Resets the entity's scale back to its original default.
- **Example:**
```lua
resetScale(PlayerPedId()) -- Return to default size
```

View File

@@ -0,0 +1,63 @@
### shops.lua
This module provides functions to open, sell to, and register in-game shops and markets.
- **sellMenu(data)**
- Opens a UI menu for selling items to a vendor or market system.
- A simple system for the ability to sell all of an item in a players inventory
- Good for money making eg. pawn shops
- **Example:**
```lua
sellMenu({
sellTable = {
Header = "Sell Items",
Items = {
["gold_ring"] = 100,
["diamond"] = 500,
},
},
ped = pedEntity,
onBack = function() print("Returning to previous menu") end,
})
```
- **sellAnim(data)**
- Plays an animation during the selling process for immersion.
- Used in sellMenu, but can be called externally if needed
- **Example:**
```lua
sellAnim({
item = "gold_ring",
price = 100,
ped = pedEntity,
onBack = function() print("Sold Items") end,
)
```
- **openShop(data)**
- Opens a shop interface for purchasing items.
- Checks job/gang restrictions, then uses the active inventory system to open the shop.
- **Example:**
```lua
openShop({
shop = "weapon_shop",
items = weaponShopItems,
coords = vector3(100.0, 200.0, 300.0),
job = "police",
})
```
- **registerShop(name, label, items, society)**
- Registers a named shop with associated items and an optional society for fund handling.
- Supports either OXInv or QBInv (with QBInvNew flag).
- **Example:**
```lua
registerShop("fishing_shop", "Bait & Tackle", {
{ item = "fishing_rod", price = 50 },
{ item = "bait", price = 5 }
}, "fishing_society")
```

View File

@@ -0,0 +1,16 @@
### skillcheck.lua
This module provides a UI-based skill check system, useful for crafting, hacking, or other interactive gameplay scenarios.
- **skillCheck()**
- Starts a skill check minigame sequence using the provided configuration.
- Configuration may define speed, difficulty, bar size, or success zones.
- **Example:**
```lua
if skillCheck() then
print("Success!")
else
print("Failed :(")
end
```

View File

@@ -0,0 +1,28 @@
### societybank.lua
This module handles financial operations for society accounts, typically used for jobs or organizations.
- **getSocietyAccount(society)**
- Retrieves the current balance for the specified society.
- **Example:**
```lua
local balance = getSocietyAccount("police")
print("Police account balance: $"..balance)
```
- **chargeSociety(society, amount)**
- Deducts a specified amount from the society's account.
- **Example:**
```lua
chargeSociety("police", 500)
```
- **fundSociety(society, amount)**
- Adds funds to a societys account.
- **Example:**
```lua
fundSociety("ambulance", 1200)
```

View File

@@ -0,0 +1,44 @@
### stashcontrol.lua
This module handles logic for interacting with stashes—shared inventories for crafting, jobs, or storage systems.
- **checkStashItem(stashes, itemTable)**
- Retrieves (or updates) a local stash cache entry with a timeout.
- **Example:**
```lua
local found, foundinStash = checkStashItem({"crafting_stash"}, { item = "steel", amount = 2 })
if found then print("Found item in "..foundInStash) end
```
- **openStash(data)**
- Opens a stash using the active inventory system.
- Checks for job or gang restrictions before opening the stash.
- **Example:**
```lua
openStash({
stash = "playerStash",
label = "Player Stash",
coords = vector3(100, 200, 30)
})
```
- **getStash(stashName)**
- Retrieves the stash data by name (usually used for querying contents).
- **Example:**
```lua
local items = getStash("playerStash")
for k, v in pairs(items) do
print(k)
end
```
- **stashRemoveItem(stashItems, stashName, items)**
- Removes specified items from a stash. Used during crafting or transfers.
- **Example:**
```lua
stashRemoveItem(currentItems, "playerStash", { iron = 2, wood = 5 })
```

View File

@@ -0,0 +1,108 @@
### targets.lua
⚠️ This module provides utility functions for adding and removing interaction targets with entities, models, zones, and coordinates. Supports common targeting frameworks like `ox_target`, `qb-target`, and more.
- **createEntityTarget(entity, opts, dist)**
- Adds interaction targets to a specific in-world entity.
- **Example:**
```lua
createEntityTarget(entityId, {
{
action = function()
openStorage()
end,
icon = "fas fa-box",
job = "police",
label = "Open Storage",
},
}, 2.0)
```
- **createBoxTarget(data, opts, dist)**
- Creates an interactable box zone with configurable options.
- **Example:**
```lua
createBoxTarget(
{
'storageBox',
vector3(100.0, 200.0, 30.0),
2.0,
2.0,
{
name = 'storageBox',
heading = 100.0,
debugPoly = true,
minZ = 27.0,
maxZ = 32.0,
},
},
{
{
action = function()
openStorage()
end,
icon = "fas fa-box",
job = "police",
label = "Open Storage",
},
}, 2.0)
```
- **createCircleTarget(data, opts, dist)**
- Creates an interactable circular zone.
- **Example:**
```lua
createCircleTarget({
name = 'centralPark',
coords = vector3(200.0, 300.0, 40.0),
radius = 50.0,
options = { debugPoly = false }
}, {
{ icon = "fas fa-tree", label = "Relax", action = relaxAction }
}, 2.0)
```
- **createModelTarget(models, opts, dist)**
- Adds interactions to all matching models globally.
- **Example:**
```lua
createModelTarget({ model1, model2 },
{
{
action = function()
openStorage()
end,
icon = "fas fa-box",
job = "police",
label = "Open Storage",
},
}, 2.0)
```
- **removeEntityTarget(entity)**
- Removes all targets linked to the specified entity.
- **Example:**
```lua
removeEntityTarget(vehicle)
```
- **removeZoneTarget(target)**
- Removes a named zone-based target.
- **Example:**
```lua
removeZoneTarget("shop_box")
```
- **removeModelTarget(model)**
- Removes interactions tied to a model globally.
- **Example:**
```lua
removeModelTarget("prop_vend_soda")
```

View File

@@ -0,0 +1,63 @@
### vehicles.lua
This module provides utilities for reading, modifying, and interacting with vehicle properties and positioning.
- **searchCar(vehicle)**
- Searches the 'Vehicles' table for a specific vehicle's details.
- If the vehicle differs from the last searched, it retrieves its model and updates the carInfo table.
- The table includes the vehicle's name, price, and class information.
- **Example:**
```lua
local info = searchCar(vehicleEntity)
print(info.name, info.price, info.class.name, info.class.index)
```
- **getVehicleProperties(vehicle)**
- Retrieves the properties of a given vehicle using the active framework.
- **Example:**
```lua
local props = getVehicleProperties(vehicle)
if props then
print(json.encode(props))
end
```
- **setVehicleProperties(vehicle, props)**
- Sets the properties of a given vehicle if changes are detected.
- It compares the current properties with the new ones and applies the update using the active framework.
- **Example:**
```lua
setVehicleProperties(vehicle, props)
```
- **checkDifferences(vehicle, newProps)**
- Checks for differences between the current and new vehicle properties.
- Compares properties using JSON encoding for deep comparison and logs differences.
- **Example:**
```lua
if checkDifferences(vehicleEntity, newProperties) then
setVehicleProperties(vehicleEntity, newProperties)
end
```
- **pushVehicle(entity)**
- This function ensures that the vehicle is controlled by the current player and is set as a mission entity.
- It requests network control and sets the vehicle accordingly to synchronize changes across clients.
- **Example:**
```lua
pushVehicle(vehicle)
```
- **getClosestVehicle(coords, src)**
- Finds the closest vehicle to the specified coordinates.
- The function uses different APIs based on whether a source is provided.
- **Example:**
```lua
local closestVeh, distance = getClosestVehicle({ x = 100, y = 200, z = 30 }, source)
```

View File

@@ -0,0 +1,37 @@
### wrapperfunctions.lua
Provides wrapper compatibility functions for command and inventory stash systems across different frameworks (OX, QB, ESX, etc).
- **registerCommand(command, options)**
⚠️ Server Side Only
- Registers a command using the appropriate framework's API.
- **Parameters:**
- `command`: Command name (string)
- `options`: Table including help, params, callback, autocomplete, restrictedGroup
- **Example:**
```lua
registerCommand("greet", {
"Greets the player",
{ name = "name", help = "Name of the player to greet" },
function(source, args) print("Hello, "..args[1].."!") end,
nil,
"admin"
})
```
- **registerStash(name, label, slots?, weight?, owner?, coords?)**
⚠️ Server Side Only
- Registers a stash using OX, or Origen inventory systems.
- **Example:**
```lua
registerStash(
"playerStash",
"Player Stash",
100,
8000000,
"player123",
{ x = 100.0, y = 200.0, z = 30.0 }
)
```

129
documentation/scaleforms.md Normal file
View File

@@ -0,0 +1,129 @@
## Scaleforms
### debugScaleform.lua
This module renders debug text in-game when `debugMode` is enabled. Useful for live diagnostics or UI placement feedback.
- **debugScaleForm(textTable, loc)**
- Displays an overlay of lines of text at the specified screen location.
- **Note:** This only works if `debugMode` is set to `true`.
- **Parameters:**
- `textTable` (`table`): A list of strings to display.
- `loc` (`vector2`, optional): Top-left anchor point on screen (default: `vec2(0.05, 0.65)`).
- **Example:**
```lua
CreateThread(function()
while true do
debugScaleForm({
"Line 1: Debug info",
"Line 2: More info"
})
Wait(0)
end
end)
```
### instructionalButtons.lua
This module renders instructional button prompts using native scaleforms in GTA V and RedM.
⚠️ **Note:** Because it uses native scaleforms, it must be run inside a `while` loop to remain visible.
- **makeInstructionalButtons(info)**
- Draws instructional buttons using the GTA scaleform `instructional_buttons`.
- **Parameters:**
- `info` (`table`): An array of tables, where each entry contains:
- `keys` (`table`): Control key codes (e.g., `{38, 29}`).
- `text` (`string`): The label for the button.
- **Example:**
```lua
CreateThread(function()
while true do
makeInstructionalButtons({
{ keys = {38, 29}, text = "Open Menu" },
{ keys = {45}, text = "Close Menu" }
})
Wait(0)
end
end)
```
- **makeRedInstructionalButtons(info, title)**
- Experimental: Displays prompts using RedM-style `PromptSetGroup` API.
- **Note:** Still must be run in a loop for visibility.
- **Example:**
```lua
CreateThread(function()
while true do
makeRedInstructionalButtons({
{ keys = {0x760A9C6F}, text = "Mount Horse" },
{ keys = {0x4CC0E2FE}, text = "Dismount" }
}, "Horse Controls")
Wait(0)
end
end)
```
### scaleform_basic.lua
This module includes helper functions for 3D text rendering and UI overlays using basic native scaleform techniques.
- **DrawText3D(coord, text, highlight)**
- Draws 3D text in the world at the given coordinates, with optional highlight.
- Includes a semi-transparent black background box for visibility.
- **Parameters:**
- `coord` (`vector3`): World position to draw text.
- `text` (`string`): Text content.
- `highlight` (`boolean`, optional): Highlights `~w~` sections with yellow.
- **Example:**
```lua
CreateThread(function()
while true do
DrawText3D(vector3(100, 200, 300), "Hello World", true)
Wait(0)
end
end)
```
- **DisplayHelpMsg(text)**
- Shows a help message in the top-left corner of the screen.
- **Example:**
```lua
DisplayHelpMsg("Press E to interact")
```
- **displaySpinner(text)**
- Shows a busy spinner with a message (e.g., "Saving...").
- **Example:**
```lua
displaySpinner("Saving data...")
```
- **stopSpinner()**
- Hides any active busy spinner (client-only).
- **Example:**
```lua
stopSpinner()
```
### timerBars.lua
This module provides a native scaleform-based timer bar HUD. Ideal for displaying tasks, loading indicators, or time-sensitive objectives.
- **createTimerHud(title, data, alpha)**
⚠️ This was an attempt to recreate the gta native shooting ranges )fairly specific use case)
- Draws a timer bar with custom label and right-aligned values using the native GTA HUD system.
- **Parameters:**
- `title` (`string`): Title/header displayed on the bar.
- `data` (`table`): List of `label = value` entries to display (max 4 rows).
- `alpha` (`number`, optional): Opacity of the bar (0-255).
- **Example:**
```lua
CreateThread(function()
while true do
createTimerHud("Timer Bar", {
{ stat = "Health", value = "85%" },
{ stat = "Armor", value = "50%", multi = 2 },
{ stat = "Stamina", value = "100%" },
}, 180)
Wait(0)
end
end)
```

556
frameworkCache.lua Normal file
View 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)

View File

@@ -1,13 +1,12 @@
name "Jim_Bridge"
author "Jimathy"
version "2.0.10"
version "2.1.04"
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

149
shared/_authToken.lua Normal file
View File

@@ -0,0 +1,149 @@
-------------------------------------------------------------
-- Exploit Auth System
-------------------------------------------------------------
forceDisableExplotProtection = false -- dangerous, this allows exploits
AuthEvent = nil
currentToken = nil
if isServer() then
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
--debugPrint(GetInvokingResource())
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= Exports.QBExport and GetInvokingResource() ~= Exports.VorpExport then
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
return ""
end
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
--debugPrint(GetInvokingResource())
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= Exports.QBExport and GetInvokingResource() ~= Exports.VorpExport then
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital callback was called from an external resource^7")
return ""
end
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

View File

@@ -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
print("^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

View File

@@ -1,88 +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 -- equal
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)
function CheckVersion()
if isServer() and GetResourceMetadata(getScript(), 'author', nil) == "Jimathy" then
CreateThread(function()
Wait(4000)
local script = getScript()
local currentVersionRaw = GetResourceMetadata(script, 'version')
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
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
fallbackVersionRaw = fallbackVersionRaw:match("[^\r\n]+"):gsub("v", "")
local compareResult = compareVersions(currentVersionRaw, fallbackVersion)
if compareResult == 0 then
print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
elseif compareResult < 0 then
print("^1----------------------------------------------------------------------^7")
print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersion.."^7)")
if #changelog > 0 then
for _, line in ipairs(changelog) do
print((line:find("http") and "^7" or "^5")..line)
end
end
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersion.."^7)")
end
end)
else
local lines = {}
for line in newestVersionRaw:gmatch("[^\r\n]+") do table.insert(lines, line) end
local newestVersion = (lines[1] or "0.0.0"):gsub("v", "")
local changelog = {}
for i = 2, #lines do table.insert(changelog, lines[i]) end
local compareResult = compareVersions(currentVersionRaw, fallbackVersionRaw)
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"..fallbackVersionRaw.."^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()
-- Do a naughty repeat message every 20 minutes until the the script is updated
CheckVersion()
end)
SetTimeout(3600000, function() CheckVersion() end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersionRaw.."^7)")
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersion.."^7)")
end
end)
else
newestVersionRaw = newestVersionRaw:match("[^\r\n]+"):gsub("v", "")
local compareResult = compareVersions(currentVersionRaw, newestVersionRaw)
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"..newestVersionRaw.."^7)")
print("^1----------------------------------------------------------------------^7")
SetTimeout(1200000, function()
-- Do a naughty repeat message every 20 minutes until the the script is updated
CheckVersion()
end)
else
print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)")
end
end
end)
end)
end)
end
end
end
CheckVersion()
CheckVersion()
end

View File

@@ -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

View File

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

View File

@@ -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,258 +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()
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
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(RSGExport) then
Core = Core or exports[RSGExport]:GetCoreObject()
elseif isStarted(VorpExport) then
Core = Core or exports[VorpExport]:GetCore()
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
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)
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)
}
if success and result then
cache = result
end
end
end)
Wait(100)
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")
if timeout and not cache then
print("^1ERROR^7: ^2jim_bridge export not available after timeout^7.")
return
end
Items = cache.Items
Vehicles = cache.Vehicles
Jobs = cache.Jobs
Gangs = cache.Gangs
InventoryWeight = cache.InventoryWeight or InventoryWeight
InventorySlots = cache.InventorySlots or 40
debugPrint("^6Bridge^7: ^2Shared cache successfully loaded from export^7.")
--print(countTable(Items), countTable(Vehicles), countTable(Jobs))
else
CreateThread(function()
while not Vehicles do Wait(1000) end
debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..vehResource)
end)
end
-- 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
-------------------------------------------------------------
-- 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 = OXExport
CreateThread(function()
if isServer() then
createCallback(getScript()..":getOxGroups", function(source)
Jobs = MySQL.query.await('SELECT * FROM `ox_groups`')
return Jobs
end)
else
local TempJobs = triggerCallback(getScript()..":getOxGroups")
Jobs = {}
for k, v in pairs(TempJobs) do
local grades = {}
--for i = 1, #v.grades do
-- grades[i] = { name = v.grades[i], isboss = (i == #v.grades) }
--end
Jobs[v.name] = { label = v.label, grades = grades }
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
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
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)
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()
--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 k, v in pairs(Jobs) do
local count = countTable(Jobs[k].grades) - 1
Jobs[k].grades[tostring(count)].isBoss = true
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 Vehicle 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

View File

@@ -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,6 +62,7 @@ local excludeKeys = {
--- job = "mechanic",
--- onBack = function() print("Returning to previous menu") end,
--- })
--- ```
function craftingMenu(data)
if CraftLock then return end
@@ -72,86 +79,147 @@ function craftingMenu(data)
-- Normalize stash name.
data.stashName = data.stashTable or data.stashName
-- Wrapper to convert old style crafting recipes to be handled by the menu properly
if data.craftable.Recipes[1] then -- assume old style crafting table
local compatTable = {}
local id = 0
for k, v in ipairs(data.craftable.Recipes) do
local Recipe = v
for l, b in pairs(Recipe) do
id += 1
if doesItemExist(l) then
compatTable[l] = {
ingredients = b,
id = id,
info = {
amount = Recipe.amount or 1,
metadata = Recipe.metadata or nil,
job = Recipe.job or nil,
gang = Recipe.gang or nil,
hasCrafted = Recipe.hasCrafted or nil,
},
}
end
end
end
data.craftable.Recipes = compatTable
end
-- Convert to array
local RecipesArray = {}
for k, v in pairs(data.craftable.Recipes) do
RecipesArray[v.id] = { [k] = v }
end
local Menu = {}
local Recipes = data.craftable.Recipes
local craftedItems = {}
local tempCarryTable = {}
-- Build a 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
local tempCarryTable = {}
-- Build a temporary table of all required ingredients (default quantity is 1).
for i = 1, #RecipesArray do
for k, v in pairs(RecipesArray[i]) do
if doesItemExist(k) then
if not v.info.amount then v.info.amount = 1 end
tempCarryTable[k] = tempCarryTable[k] and (tempCarryTable[k] < v.info.amount) or v.info.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.
for i = 1, #Recipes do
if not Recipes[i]["amount"] then Recipes[i]["amount"] = 1 end
for k, _ in pairs(Recipes[i]) do
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, #RecipesArray do
local item = ""
for k in pairs(RecipesArray[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
break
end
--Wait(0)
end
local Recipe = RecipesArray[i][item]
-- Job Check
local hasGroup = true
if Recipe.info.job then
for l, b in pairs(Recipe.info.job) do
hasGroup = hasJob(l, nil, b)
if hasGroup then goto skipcheck end
end
end
if Recipe.info.gang then
for l, b in pairs(Recipe.info.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.info.metadata or Recipe.info.info or nil)
local itemTable = {}
local metaTable = {}
-- Build ingredient details.
for l, b in pairs(Recipe.ingredients) 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.info.amount > 1 and " x"..Recipe.info.amount or "")
local statusEmoji = disable and " " or not canCarryTable[item] and " 📦" or " ✔️"
local isNew = (Recipe.info.hasCrafted ~= nil and craftedItems[item] == nil) and "" or ""
setheader = isNew .. setheader .. statusEmoji
-- Build menu option using info
Menu[#Menu + 1] = {
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 = RecipesArray[i][item],
craftable = data.craftable,
coords = data.coords,
stashName = data.stashName,
onBack = data.onBack,
metadata = metadata,
}
if Config.Crafting.MultiCraft then
multiCraft(transdata)
else
makeItem(transdata)
end
end or nil,
}
end
end
-- open context menu
openMenu(Menu, {
header = data.craftable.Header,
headertxt = data.craftable.Headertxt,
@@ -202,7 +270,7 @@ function multiCraft(data)
for i = 1, maxCreation do
multiItemTable[i] = {}
for l, b in pairs(data.craft[data.item]) do
for l, b in pairs(data.craft.ingredients) do
multiItemTable[i][l] = (b * i)
end
end
@@ -234,7 +302,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 +375,9 @@ end
--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
--- })
--- ```
function makeItem(data)
local Ped = PlayerPedId()
if CraftLock then return end
CraftLock = true
data.stashName = data.stashTable or data.stashName
@@ -321,113 +391,147 @@ 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
jsonPrint(prop)
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
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
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.ingredients) do
if isInventoryOpen() then
print("^1Error^7: ^2Inventory is open, you tried to break things")
stopCam(0)
ClearPedTasks(Ped)
if canReturn then craftingMenu(data) 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
stopCam(0)
ClearPedTasks(Ped)
if canReturn then craftingMenu(data) end
CraftLock = false
return
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.info.hasCrafted ~= nil then
data.craftable.craftedItems[data.item] = true
triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems)
end
if data.craft.info.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
if data.requiredItemfunc then
data.requiredItemfunc()
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.info.amount = craftAmount
for k, v in pairs(data.craft.ingredients) do
-- multiply igredient requirement in sent crafting table for removal
data.craft.ingredients[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.info.hasCrafted ~= nil then
data.craftable.craftedItems[data.item] = true
triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems)
end
if data.craft.info.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
if data.sound then
StopSound(data.sound.soundId)
end
if data.requiredItemfunc then
data.requiredItemfunc()
end
end
Wait(500)
end
stopTempCam()
if craftProp then destroyProp(craftProp) end
--Wait(500)
stopCam(0)
CraftLock = false
if canReturn then craftingMenu(data) end
ClearPedTasks(PlayerPedId())
ClearPedTasks(Ped)
end
-------------------------------------------------------------
-- Server Event Handler: Crafted Item
-------------------------------------------------------------
@@ -444,9 +548,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 +558,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.ingredients) do
for _, b in pairs(stashItems or {}) do
if k == b.name then
itemRemove[k] = v
@@ -471,7 +574,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.ingredients) do
for _, b in pairs(stashItems or {}) do
if k == b.name then
itemRemove[k] = v
@@ -482,12 +585,12 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable,
stashRemoveItem(stashItems, stashName, itemRemove)
else
if craftable then
for k, v in pairs(craftable[ItemMake] or {}) do
for k, v in pairs(craftable.ingredients or {}) do
removeItem(tostring(k), v, src)
end
end
end
addItem(ItemMake, craftable.amount or 1, metadata, src)
addItem(ItemMake, craftable.info.amount or 1, metadata, src)
-- Optionally, add experience here:
-- for example:
-- if isStarted("core_skills") then exports["core_skills"]:AddExperience(src, 2) end

View File

@@ -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

View File

@@ -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)
@@ -550,6 +577,81 @@ end
RegisterNetEvent(getScript()..":server:sendlog", sendServerLog)
-- This function was created to help create random numbers
-- When you create a table of items with a random hunger value for example
-- `hunger = math.random(10, 20)` this will be set at script start and never change
-- using `hunger = {10, 20}` and then calling this function will make it generate a random number every time
-- It also fallsback if it simply recieved a number instead of a table
-- For example:
-- local hunger = {10, 20}
-- local hungerAmount = GetRandomTiming(hunger)
-- print(hungerAmount) -- number between 10, 20
function GetRandomTiming(tbl)
return type(tbl) == "table" and math.random(tbl[1], tbl[2]) or tbl
end
-------------------------------------------------------------
-- Player Movement
-------------------------------------------------------------
--- Instantly turns an entity to face a target (entity or coordinates) without animation.
---
--- @param ent number|nil The Ped to turn (defaults to player's Ped if nil).
--- @param ent2 number|vector3|nil The target entity or coordinates to face.
---
--- @usage
--- ```lua
--- instantLookEnt(nil, vector3(200.0, 300.0, 40.0))
--- instantLookEnt(ped1, ped2)
--- ```
function instantLookEnt(ent, ent2)
local ped = ent or PlayerPedId()
local p1 = GetEntityCoords(ped, true)
local p2 = type(ent2) == "vector3" and ent2 or GetEntityCoords(ent2, true)
local dx = p2.x - p1.x
local dy = p2.y - p1.y
local heading = GetHeadingFromVector_2d(dx, dy)
debugPrint("^6Bridge^7: ^1Forced ^2Turning Player to^7: '^6"..formatCoord(p2).."^7'")
SetEntityHeading(ped, heading)
end
--- Makes the player look towards a specific target with an animated turn.
---
--- If the player is not already facing the target (entity or coordinates), a turning animation is triggered.
---
--- @param entity number|vector3|vector4|nil The target to look at.
---
--- @usage
--- ```lua
--- lookEnt(vector3(200.0, 300.0, 40.0))
--- lookEnt(pedEntity)
--- ```
function lookEnt(entity)
local ped = PlayerPedId()
if entity then
if type(entity) == "vector3" or type(entity) == "vector4" then
if not IsPedHeadingTowardsPosition(ped, entity.xyz, 30.0) then
TaskTurnPedToFaceCoord(ped, entity.xyz, 1500)
debugPrint("^6Bridge^7: ^2Turning Player to^7: '^6"..formatCoord(entity).."^7'")
Wait(1500)
end
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
end
end
end
end
-------------------------------------------------------------
-- Material and Prop Functions
-------------------------------------------------------------
@@ -783,9 +885,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

View File

@@ -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,

View File

@@ -1,265 +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
if itemData and itemData.name == item then
count += (itemData.count or itemData.amount 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
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()
grabInv = triggerCallback(getScript().."getServerInvData", GetPlayerServerId(PlayerId()))
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
createCallback(getScript()..":getServerInvData", function(source)
local src = source
return getPlayerInv(src)
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

View File

@@ -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

View File

@@ -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
-------------------------------------------------------------
@@ -46,7 +113,7 @@ function makeBossRoles(role)
local data = (Jobs and Jobs[role]) or (Gangs and Gangs[role])
if data then
for grade, info in pairs(data.grades) do
if info.isboss or info.bankAuth then
if info.isboss or info.bankAuth or info.isBoss then
boss[role] = boss[role] and math.min(boss[role], tonumber(grade)) or tonumber(grade)
end
end
@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -54,7 +54,7 @@ function makeBlip(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'..string.gsub(data.coords.z, "%.", "")), "[ ()~]", "")
if data.preview:find("http") or data.preview:find("nui") then
@@ -62,8 +62,11 @@ function makeBlip(data)
else
CreateRuntimeTextureFromImage(scriptTxd, txname, data.preview)
end
exports["fs_smallresources"]:SetBlipInfoImage(blip, getScript()..'scriptTxd', 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
@@ -130,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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -1,4 +1,212 @@
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,
},
}
--- Displays a progress bar using the configured progress bar system.
---
@@ -33,7 +241,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 +254,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 +285,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
View 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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -1,259 +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
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[Config.Lan].info["sell_all"]..v.." "..Loc[Config.Lan].info["sell_each"],
onSelect = function()
sellAnim({ item = k, price = v, ped = data.ped, onBack = function() sellMenu(data) 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

View File

@@ -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

View File

@@ -8,8 +8,193 @@
• Renewed-Banking
• fd_banking
• okokBanking
• 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.
@@ -19,48 +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
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
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)
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
@@ -73,45 +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)
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.
@@ -122,59 +251,16 @@ end
--- fundSociety("police", 500)
--- ```
function fundSociety(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)
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
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
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)
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
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
end)
print("^1Error^7: ^3FundSociety^7: ^2No supported banking script found")
end

View File

@@ -1,567 +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, data.stashOptions)
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, data.stashOptions)
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, data.stashOptions)
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

View File

@@ -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 buiilt 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,73 +324,24 @@ 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 buiilt 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])
local target = script.boxTarget(data, opts, dist)
boxTargets[#boxTargets + 1] = target
return target
end
end
return nil
end
-------------------------------------------------------------
@@ -296,68 +381,25 @@ end
--- }, 2.0)
--- ```
function createCircleTarget(data, opts, dist)
-- if force target off, use jim_bridge buiilt 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 +428,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 buiilt 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 +461,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 +487,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 +511,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 +534,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 +552,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 +560,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)

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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,7 +24,12 @@ Exports = {
-- REDM
RSGExport = "rsg-core",
RSGInv = "rsg-inventory"
RSGInv = "rsg-inventory",
VorpExport = "vorp_core",
VorpInv = "vorp_inventory",
VorpMenu = "vorp_menu",
}
-- Required variables
@@ -81,16 +86,23 @@ for Type, ScriptTable in pairs(filesToLoad) do
local state = GetResourceState(scriptName)
-- if the resource is started, load the file
if state == "started" then
if scriptName == Exports.OXLibExport and GetResourceState(Exports.OXCoreExport):find("start") then
if debugMode then print("OX_Core found, skipping OX_Lib loading") end
goto skip
end
-- Force items into a table if they are not
if type(files) == "string" then
files = { files }
end
for _, file in pairs(files) do
--print("^5CoreLoader^7: '"..k.."/"..file.."' ^2into ^7'"..GetCurrentResourceName().."' ...")
--print("^5CoreLoader^7: '"..scriptName.."/"..file.."' ^2into ^7'"..GetCurrentResourceName().."' ...")
local fileLoader = assert(load(LoadResourceFile(scriptName, (file)), ('@@'..scriptName..'/'..file)))
fileLoader()
print("^5CoreLoader^7: ^2loaded ^1Core ^2file^7: ^3"..scriptName.."^7/^3"..(file):gsub("/", "^7/^3"):gsub("%.lua", "^7.lua").." ^2into ^7'"..GetCurrentResourceName().."'")
if debugMode then
print("^5CoreLoader^7: ^2loaded ^1Core ^2file^7: ^3"..scriptName.."^7/^3"..(file):gsub("/", "^7/^3"):gsub("%.lua", "^7.lua").." ^2into ^7'"..GetCurrentResourceName().."'")
end
end
::skip::
end
-- if script is in server, but not started warn the user
@@ -118,7 +130,7 @@ for _, v in pairs({ -- This is a specific load order
'_eventDebug.lua',
'callback.lua',
'coreloader.lua', -- needs to be second to load all core related stuff before everything else
'coreloader.lua',
'duifunctions.lua',
@@ -141,7 +153,6 @@ for _, v in pairs({ -- This is a specific load order
'wrapperfunctions.lua',
'polyZone.lua',
'inventories.lua',
'itemcontrol.lua',
'playerfunctions.lua',
'metaHandlers.lua',
@@ -159,15 +170,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
@@ -177,6 +189,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

View 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)

347
ui_modules/progressbar.lua Normal file
View File

@@ -0,0 +1,347 @@
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
print(tostring(result))
-- 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
View 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
View 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

View File

@@ -1 +1,9 @@
2.0.10
2.1.04
- Crafting fixes (prop/sound/remove ingredients)
- "SingleProgress" is now one progressbar
- Add ropeControl.lua - to help create and edit custom ropes/wires
- Support for new crafting recipe style
- create setStress() multiframework function
https://github.com/jimathy/jim_bridge/releases/latest