diff --git a/README.md b/README.md
index 4f85642..4350c17 100644
--- a/README.md
+++ b/README.md
@@ -1,639 +1,1804 @@
-# Jim_Bridge
-
-This script is intended to be used with my all my scripts (soon)
-
-It was started due to wanting to bring the same features from some scripts into others with minimal work and multiple updates
-- Having certain functions in one place(this script) makes it easier to update, enchance and fix things
-- This brings the possibility of branching to mutliple frameworks as I've added some already:
- - `"qb-core"`
- - `"qbx-core"`
- - `"ox_core"`
- - `"es_extended"` (requires ox_lib and ox_inventory)
-
-All the next updates of my scripts will use this script and be added as a dependancy
-
-------
-
-It was a tough decision to put it up on github instead of tebex and encrypted
-
-But I want this script to grow with help of others who know more about other cores
-
----
-
-The installation of this script is simple
-- REMOVE `-main` from the folder name, like any other github hosted script
-- it just needs to start before any script that requires it
-- it can start before core scripts if you want
-- for `qb-core` I personally place this script in `resources > [standalone]`
-
-
----
-
-### Support for different exports and scripts
-
-In exports.lua is the list of script folder names
-
-This is for people who have customised/renamed scripts
-
-eg. for people who use `ps-inventory`, this is mainly based on qb-inventory
-so you need to rename
-```lua
- QBInv = "qb-inventory",
-```
-to
-```lua
- QBInv = "ps-inventory",
-```
-
-This will now use events from `ps-inventory` and use it through out the scripts.
-
-# WIP
-## Documentation
-
-This script brings alot of features to simplify making scripts with preset functions and automations.
-
-It attempts to make use of configs from the scripts its loaded into. For example:
-
-### `Config`
-This needs to be in every script that uses it, a `System` table with Debug, Menu, Notify, drawText, progressBar
+# Jim_Bridge
+
+This script is intended to be used with my all my scripts (soon)
+
+It was started due to wanting to bring the same features from some scripts into others with minimal work and without multiple updates fer script
+
+It BRIDGES frameworks and cores together through this script and does it best to detect what is being used to automate the process
+- Having certain functions in one place makes it easier to update, enchance and fix things already in place
+- This brings the possibility of branching to mutliple frameworks as I've added some already:
+ - `"qb-core"`
+ - `"qbx-core"`
+ - `"ox_core"`
+ - `"es_extended"` - requires ox_lib and ox_inventory
+ - `"rsg-core"` - basic support for RedM's RSG Core
+
+All of my scripts will use this script and be added as a dependancy
+
+This was originally designed to be used for my scripts but has grown into a whole framework of unified functions that anyone can use for their own, I encourage it
+
+------
+
+## I want this script to grow with help of others who know more about other cores, I'm not a book of framework knowledge
+This script was designed by me through over a year of research and testing.
+Some of it of it hasn't been personally tested but the information has been gathered through documentation on other scripts
+I hope it works as well I intend, but feel free to do pull requests if you know how to fix an issue
+
+(Please also keep it to a similar format to prevent breakages in other scripts)
+
+------
+
+## Installation
+
+The installation of this script is simple
+- REMOVE `-main` from the folder name, like any other github hosted script
+- It just needs to start before any script that requires it
+- It can start before core scripts if you want
+- For example with `qb-core` I personally place this script in `resources > [standalone]`
+
+### Optional
+
+I've added the ability to add override server convars to your server.cfg
+- This can be used to ensure you don't have silly mistakes like forgetting to change what inventory system you use
+- Also it can force debug mode off, to ensure your live server doesn't accidently get polyzones and debug information showing
+- This isn't required but helpful if you are like me
-This is required to use jim_bridge with your script
-```lua
-Config = {
- System = {
- Debug = true, -- This enables Debug mode
- -- Revealing debug prints and debug boxes on targets
-
- Menu = "qb", -- This specifies what menu script will be loaded
- -- "qb" = `qb-menu` and edited versions of it
- -- "ox" = `ox_lib`'s context menu system
- -- "gta" = `WarMenu' a free script for a gta style menu
-
- Notify = "gta", -- This allows you to choose the notification system for scripts
- -- "qb" = `qb-core`'s built in notifications
- -- "ox" = `ox_lib`'s built in notifications
- -- "esx" = `esx_notify` esx's default notifications
- -- "okok" = `okok-notify` okok's notifications
- -- "gta" = Native GTA style popups
-
- drawText = "gta", -- The style of drawText you want to use
- -- "qb" = `qb-core`'s drawText system
- -- "ox" = `ox_lib`'s drawTextUI system
- -- "gta" = Native GTA style popups
-
-
- progressBar = "gta" -- The style of progressBar you want to use
- -- "qb" = `qb-core`'s style progressBar
- -- "ox" = `ox_lib`'s default progressBar
- -- "gta" = Native GTA style "spinner"
- },
-}
-```
-
-### `openMenu(Menu, data)`
-
-This handles creation of menus using `OX_Lib`, `qb-menu` or `WarMenu`
-
-It uses mixed/new functions to bring more compatability to one another
-
-`Menu` is your button entries and works like qb-menu or ox_lib, for example:
-
-```lua
-local Menu = {}
-Menu[#Menu + 1] = {
- isMenuHeader = true, -- This makes the current button unclickable
- icon = invImg("lockpick") -- Supports fontawesome or custom images
- -- This example use the custom function `invImg()` to retreive an nui:// link to the given item's image
- arrow = true, -- Adds a arrow icon to the button (in qb-menu overrides the icon)
- header = "Header Test", -- The header/title for the button
- txt = "Text test", -- The txt/description for the button
-
- onSelect = function() -- This brings the onSelect function to qb-menu
- TriggerEvent("lolhi", { lol = hi }),
- end,
- -- Enter what happens when you click the button
-}
-```
-
-As you can see above, it mixes variables but makes it possible to switch between menus just by changing the config option
-
-After you have created the info above, you need to then trigger opening of this menu with:
-```lua
-openMenu(Menu, -- Menu here is your table name you created above
-{ -- Next entry in openMenu is a table
- header = "Menu Header", -- What your menu title will be shown as
- headertxt = "Header info", -- Info to be displayed under the title
-
- onExit = function() -- Will create a "Close button"
- TriggerEvent("lolhi", { lol = hi }),
- end, -- When clicked it will trigger the onExit event
-
- onBack = function() -- Will create a "Back button"
- TriggerEvent("lolhi", { lol = hi }),
- end, -- When clicked it will trigger the onBack event
-})
-```
-
-### Support for multiple target events
-These automatically detect what target script you are using
-
-They are also automatically removed when the script is stopped (for helping optimization)
-### `createEntityTarget(entity, opts, dist)`
-Create an entity based target
-```lua
-createEntityTarget(
- entity, -- The entity ID of what you want to target
- {
- { -- Your target options here
- icon = "icon", -- Your icon, only supports font awesome icons
- label = "Test Label", -- The label of your target
- item = "lockpick" -- The required it em
- job = "mechanic", -- The required job
- gang = "lostmc", -- The required gang
- action = function() -- What happens when the target is selected
- TriggerEvent("lolhi", { lol = hi }),
- end,
- },
- }
-, dist) -- How close you ned to be to see the target
-```
-
-### `createBoxTarget(data, opts, dist)`
-Create an entity based target
-```lua
-createBoxTarget(
- {
- "TargetName", -- The name/id of your target here
- vec3(0, 0, 0), -- The coordinates of your target
- 2.0, -- The width of your target box
- 2.0, -- The depth of your target box
- {
- name = "TargetName", -- The name/id of your target here
- heading = 200.0, -- The direction your target will be placed
- debugPoly = true, -- Wether to show debug boxes to help place targets
- minZ = 190.0, -- The bottom of your box
- maxZ = 210.0, -- The top of your box
- },
- },
- {
- { -- Your target options here
- icon = "icon", -- Your icon, only supports font awesome icons
- label = "Test Label", -- The label of your target
- item = "lockpick" -- The required it em
- job = "mechanic", -- The required job
- gang = "lostmc", -- The required gang
- action = function() -- What happens when the target is selected
- TriggerEvent("lolhi", { lol = hi }),
- end,
- },
- },
-dist) -- How close you ned to be to see the target
-```
-
-### `createCircleTarget(data, opts, dist)`
-Create an entity based target
-```lua
-createCircleTarget(
- {
- "TargetName", -- The name/id of your target here
- vec3(0, 0, 0), -- The coordinates of your target
- 2.0, -- The radius of your target circle
- {
- name = "TargetName", -- The name/id of your target here
- heading = 200.0, -- The direction your target will be placed
- debugPoly = true, -- Wether to show debug boxes to help place targets
- minZ = 190.0, -- The bottom of your box
- maxZ = 210.0, -- The top of your box
- },
- },
- {
- { -- Your target options here
- icon = "icon", -- Your icon, only supports font awesome icons
- label = "Test Label", -- The label of your target
- item = "lockpick" -- The required it em
- job = "mechanic", -- The required job
- gang = "lostmc", -- The required gang
- action = function() -- What happens when the target is selected
- TriggerEvent("lolhi", { lol = hi }),
- end,
- },
- },
-dist) -- How close you ned to be to see the target
-```
-
-### `removeEntityTarget(entity)`
-Triggers removal of the target entity, by checking the entity name
-
-### `removeZoneTarget(target)`
-Triggers removal of a zone(Box/Circle) target by calling the target's name/id
-
-### `triggerNotify(title, message, type, src)`
-Handles notifications for the script called from either the server or client
-
-Supports:
-- `okok`
-- `qb`
-- `ox`
-- `gta`
-- `esx`
-
-```lua
-triggerNotify(
- title = "Notification Title", -- Usually 'nil' in my scripts, supports notifications with titles
- message = "Notification Message", -- The notification's message
- type = "success" -- The type of notification, depends on the supporting script
- src = 1, -- If in the server, this is required to send to player
-)
-```
-
-### `drawText(image, input, style)`
-This handles calling drawText functions
-
-Supports:
-- `gta`
-- `qb`
-- `ox`
-- `esx`
-
-```lua
-drawText(
- 187, -- Very specific for adding blip images to drawtexts, usually nil
- {
- "Line 1", -- Supports multiple lines, helpful for displaying button prompts
- "Line 2",
- },
- "g" -- Sets colour of text after a ":" when using GTA drawtext
-)
-```
-
-### `hideText()`
-Simply used to hide drawText prompts when not needed anymore
-
-### `createCallback(callbackName, funct)`
-This is my attempt at making multiframework server callbacks by using their provided events
-
-(Only works server side)
-
-```lua
-createCallback(
- "jimsCallback", -- Callback event name, needs to be something that isn't already set
- function()
-
- end)
-end
-```
-
-### `triggerCallback(callBackName, value)`
-This is an attempt at a mutliframework callback
-
-### `onPlayerLoaded(func)`
-This is a multiframework event that is triggered when a player has fully loaded their character in
-
-```lua
-onPlayerLoaded(
- function()
- print("Player Loaded In!")
- end
-)
-```
-
-### `createInput(title, opts)`
-
-### `searchCar(vehicle)`
-
-This function was made for `jim-mechanic` but can be used in other instances
-
-I searches the model name of a currently spawned vehicle and retrieves info about it
-
-It is smart, in terms of, if you use this multiple times it reteives the previously found info instead of searching again
-
-It retrieves data from your vehicles.lua/database:
-- `name` for example: "Zentorno Pegassi"
-- `price` for example: 100000
-- `class` this converts the class number to a String, for example: if the class is 10 it converts this to "Off-road"
-
-### `getVehicleProperties(vehicle)`
-Gets the current properties of the vehicle in a table
-- if using qb-core it will default to its version
-- if not it will attempt to use ox_libs version
-
-### `setVehicleProperties(vehicle, props)`
-Set's the vehicles properites using the `props` table provided
-- if using qb-core it will default to its version
-- if not it will attempt to use ox_libs version
-
-### `checkDifferences(vehicle, newProps)`
-This function is used by `setVehicleProperties`
-
-It determine's what differences there are between the current vehicle and the new set of properites
-
-If there are differences, return `true`
-
-### `RegisterNetEvent(GetCurrentResourceName()..":server:ChargePlayer", function(cost, type, newsrc)`
-This event is made to REMOVE money from a player
-
-It can be called from client with `TriggerServerEvent`
-
-Also can be called from server with `TriggerEvent` and a source id in `newsrc`
-
-The name of the event uses `GetCurrentResourceName()` so it doesn't double up results with other scripts
-```lua
-cost = 100 -- The amount of money to be removed
-type = "cash" or "card" -- The type of money that should be removed
-newsrc = 1 -- The source of the player, must be nil if calling from client
```
-Examples of use:
+# Jim Config Settings
+# These optional but forces settings for all jim scripts
+#-------
+# Add this to your live server.cfg and set to true to force debug mode off
+# Set to false if to dev server to allow debug mode
+setr jim_DisableDebug true
+setr jim_DisableEventDebug true
+
+# Force the default setting for what framework scripts should be used
+setr jim_menuScript qb # qb, ox, gta, jim
+setr jim_notifyScript gta # qb, ox, gta, esx, okok, red
+setr jim_drawTextScript qb # qb, ox, gta, esx
+setr jim_progressBarScript qb # qb, ox, gta, esx
+setr jim_skillCheckScript qb # qb, ox, gta
+setr jim_dontUseTarget false # Set to true to disable target systems and use draw text 3d
+```
+
+-----
+
+## Support for different frameworks and scripts
+
+In `starter.lua` is the list of script folder names, this is already setup but this is for people who have customised/renamed their cores or scripts
+
+# WIP
+## Documentation
+
+## Usage
+
+In your own resource, simply call the desired function exported from `jim_bridge`. Each function is built to work across multiple frameworks, offering compatibility and consistency.
+
```lua
--- Client
-TriggerEvent(GetCurrentResourceName()..":server:ChargePlayer", function(1000, "cash")
+-- Example usage:
+createCallback("myCallback", function(data)
+ print("Callback received:", data)
+end)
--- Server
-TriggerServerEvent(GetCurrentResourceName()..":server:ChargePlayer", function(1000, "bank", 1)
+triggerCallback("myCallback", "Hello World")
```
-
-## `RegisterNetEvent(GetCurrentResourceName()..":server:FundPlayer", function(cost, type, newsrc)`
-This event is made to ADD money from a player
-
-It can be called from client with `TriggerServerEvent`
-
-Also can be called from server with `TriggerEvent` and a source id in `newsrc`
-
-The name of the event uses `GetCurrentResourceName()` so it doesn't double up results with other scripts
-```lua
-fund = 100 -- The amount of money to be added
-type = "cash" or "card" -- The type of money that should be added
-newsrc = 1 -- The source of the player, must be `nil` if calling from client
-```
-Examples of use:
-```lua
--- Client
-TriggerEvent(GetCurrentResourceName()..":server:FundPlayer", function(1000, "cash")
--- Server
-TriggerServerEvent(GetCurrentResourceName()..":server:FundPlayer", function(1000, "bank", newsrc)
-```
-
-### `createUseableItem(item, funct)`
-This is a server side event to make an item usable
-
-Note: If using ox_inv and the items.lua info has event or a `status` section, this will be ignored
-
-```lua
-createUseableItem(
- "lockpick", -- The item you want to make usable
- function(source, item)
- TriggerClientEvent("lolhi", source, { lol = item.name }),
- end
-)
-```
-
-### `hasJob(job, source, grade)`
-This is an event that makes checking if the player has the requested job simple
-
-It works both client side and server side
-
-returns `true` or `false` and if they are on duty or not
-```lua
-local hasjob, duty =
- hasJob(
- "mechanic", -- the job role
- 1, -- the source id of the player, set to nil if on client
- 3, -- the required grade of the player, can be nil to check job
- )
-```
-
-### `getPlayer(source)`
-This retrieves basic info of the player
-
-works client side and server side
-Retrieves:
-- Players Name
-- Players Current Cash
-- Players Current Bank Balance
-
-```lua
-local PlayerInfo =
- getPlayer(
- 1 -- The
- )
-print(json.encode(PlayerInfo, { indent = true })
-```
-
-### `registerCommand(command, options)`
-This is a server side event that uses
-- `ox_lib`'s - `lib.addCommand`
-- `qb-core`'s - `QBCore.Commands.Add`
-
-Example:
-```lua
-registerCommand(
- "hello", -- /hello the command to be used
- "Print 'hello world'", -- text to show in chat
- { name = "lol", help = "hi" }, -- Help text for the command
- false,
- function() -- Function to be ran when the command is triggered
- print("Hello World")
- end,
- "admin", -- the restriction, can be nil
-)
-```
-
-### `invImg(item)`
-This is used mainly for menu's to retrieve the item images
-
-It detects what inventory you are using and automatically generates an `nui://` link
-
-```lua
-local imgLink = invImg("lockpick")
-print(imgLink)
-```
-
-### `registerStash(name, label, slots, weight)`
-This is a serverside function used to register a new stash in `ox_inventory` and `qs-inventory`
-
-```lua
-registerStash(
- "newStash", -- The stash name/ID, this is used to open it later
- "New created Stash", -- The name of the stash that shows in inventories
- 50, -- The amount of slots in the inventory
- 4000000, -- The max weight in the inventory
-)
-```
-
-### `loadModel(model)`
-This loads the requested model into the memory cache to help spawning of props
-- Checks if the model exists in the server
-- Attempts to load the model with a timeout, if not loaded, sends warning
-
-### `unloadModel(model)`
-This unloads a model to help clear the memory cache and help optimization
-- Recommended to run after spawning a prop
-
-### `loadAnimDict(animDict)`
-This loads the requested animDict into the memory cache to help loading anims
-- Checks if the dict exists in the server
-
-### `unloadAnimDict(animDict)`
-This unloads the animDict to help clear the memory cache and help optimization
-- Recommended to run after running an animation
-
-### `loadPtfxDict(ptFxName)`
-This loads the requested ptFx dict into the memory cache to help loading particle effects
-- Skips if the effect is alredy loaded
-
-### `unloadPtfxDict(dict)`
-This unloads a particle effect to help clear the memory cache and help optimization
-- Recommended to run after running an ptfx
-
-### `loadTextureDict(dict)`
-This loads the requested texture dictionary into memory
-
-### `countTable(table)`
-This is a simple function to count how many entires are in a table, for if your table keys aren't numbered
+---
-Example:
-```lua
-local table = {
- ["tableentry"] = true,
- ["anotherentry"] = true,
-}
-print("countTable", countTable(table))
-```
-
-### `pairsByKeys(t)`
-Searches through a table alphabetically instead of randomly
-
-This is an optional function made to replace:
-```lua
-for k, v in pairs(table) do end
-```
-with:
-```lua
-for k, v in pairsByKeys(table) do end
-```
-
-### `playAnim(animDict, animName, duration, flag, ped)`
-A simplified version of `TaskPlayAnim()`
-
-Has some settings already set and basic ones ready to change
-
-Loads the animDict automatically with `loadAnimDict()`
-```lua
-playAnim(
- animDict, -- The animation dictionary
- animName, -- The animation's name
- duration, -- How far into the animation it should stop
- flag, -- The animation flag
- ped, -- Optional, for if you want any one other than the player to do the animation
-)
-```
-
-### `stopAnim(animDict, animName, ped)`
-Similar to `StopAnimTask()`
-
-Made to stop the given animation with being able to choose which ped
-```lua
-stopAnim(
- animDict, -- The animation dictionary
- animName, -- The animation's name
- ped, -- Optional, for if you want any one other than the player to do the animation
-)
-```
-### `makeVeh(model, coords)`
-Spawns a vehicle for the player to use
-- Server Synced
-- Easy creation
-- Returns entity id for further control through the script
-- Loads model before spawning
-- Unloads model from memory cache after spawn
+## Main Functions
+### callback.lua
-Example of use:
-```lua
-local vehicle = makeVeh(
- `zentorno`,
- vec4(-596.74, 2090.99, 131.41, 16.6)
-)
-print(vehicle, GetEntityCoords(vehicle))
-```
-
-### `makePed(model, coords, freeze, collision, scenario, anim, synced)`
-Spawns a controllable ped
-- Loads the model before spawning
-- Unloads model from memory cache after spawn
-- Several options for creation
-- Can spawn with scenario name or anims
-- Spawns invincible
+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.
-Example of use:
-```lua
-local ped = makePed(
- `MP_M_Freemode_011,
- vec4(-596.74, 2090.99, 131.41, 16.6),
- true,
- false,
- nil,
- { "amb@prop_human_parking_meter@male@idle_a", "idle_a" },
- false
-)
-print(ped, GetEntityCoords(ped))
-```
-
-### `makeProp(data, freeze, synced)`
-This function is made to easily load a prop in the world
-- Has a simplified process
-- Lodas model before spawning prop
-- Unloads model from memory cache when done
-- Returns entity id for control through the script
+- **createCallback(callbackName, funct)**
-Example of use:
-```lua
-local entityid = makeProp(
- {
- prop = "v_serv_plas_boxgt2", -- Prop model, can be a string or hash key
- coords = vec4(-596.74, 2090.99, 131.41, 16.6), -- needs to be vector4 or vec4, 4th variable is heading
- },
- true, -- Decide if the entiy is frozen in place
- false -- Does this prop spawn for everyone or just the client
-)
-print(entityid, GetEntityCoords(entityid))
-```
-
-### `instantLookEnt(ent, ent2)`
-This function forcibly changes `ent`'s heading to face `ent2`
-
-Helpful for animations in a specific direction
-
-### `lookEnt(entity)`
-This function attempts to slowly turn the player to the given entity/coords
-
-Accepts either a `entity ID` or `vector3`
-
-### `destroyProp(entity)`
-Attempts to remove a spawned prop
-
-If its attached to a player it attempts to to detatch it first
-
-### `pushVehicle(entity)`
-This attempts to make the current entity(vehicle) network controlled
-
-This helps with syncing it with other players (used in jim-mechanic often)
-
-### `ensureNetToVeh(vehNetId)`
-This was created to get around fivem's warnings of failing to get network objects
-
-Although these warnings mean't nothing, it is annoying
-
-This is made to replace the native `NetToVeh()` but checking first if it exists
+ - 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)
-### `makeBlip(data)`
-
-
----
-
-
-
-
-
-
-
-
+ 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)
+ ```
+
+### 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.
+
+### 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,
+ })
+ ```
+
+### 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()
+ ```
+
+### 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 }
+ })
+ ```
+
+### 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
+ ```
+
+### 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 player’s 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 player’s 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
+ ```
+
+### 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 player’s 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) })
+ ```
+
+### 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)
+ ```
+
+- **SetMetadata(player, key, value)**
+
+ ⚠️ Server side only
+ - Updates or assigns a value to a specific metadata key for a player.
+ - The function updates the player's metadata using the active core export.
+ - **Example:**
+ ```lua
+ SetMetadata(player, "stress", 0)
+ ```
+
+### 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)
+ ```
+
+### 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 }
+ }
+ })
+ ```
+
+### 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
+ ```
+
+### 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)
+ ```
+
+### 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
+ ```
+
+### 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")
+ ```
+
+### 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
+ ```
+
+### 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 society’s account.
+ - **Example:**
+ ```lua
+ fundSociety("ambulance", 1200)
+ ```
+
+### 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 })
+ ```
+
+### 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")
+ ```
+
+### 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)
+ ```
+
+### wrapperfunctions.lua
+
+Provides wrapper compatibility functions for command and inventory stash systems across different frameworks (OX, QB, ESX, QS, 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, QS, or Origen inventory systems.
+ - **Example:**
+ ```lua
+ registerStash(
+ "playerStash",
+ "Player Stash",
+ 100,
+ 8000000,
+ "player123",
+ { x = 100.0, y = 200.0, z = 30.0 }
+ )
+ ```
+
+### 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()
+ ```
+
+### 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)
+ ```
+
+### 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
+ ```
+
+## 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)
+ ```
+
+## 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
+ ```
+
+## Helpful functions
+
+### 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)
+ ```
+
+### 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())
+ ```
\ No newline at end of file
diff --git a/_versioncheck.lua b/_versioncheck.lua
new file mode 100644
index 0000000..0a17ebc
--- /dev/null
+++ b/_versioncheck.lua
@@ -0,0 +1,47 @@
+function parseVersion(version)
+ local parts = {}
+ for num in version:gmatch("%d+") do
+ table.insert(parts, tonumber(num))
+ 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
+ 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
+
+ newestVersionRaw = newestVersionRaw:match("[^\r\n]+")
+ 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("^7'^3jim_bridge^7' - ^1You are running an outdated version^7! ^7(^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
+ else
+ print("^7'^3jim_bridge^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..newestVersionRaw.."^7)")
+ end
+ end)
+ end)
+ end
+end
+
+CheckBridgeVersion()
\ No newline at end of file
diff --git a/crafting.lua b/crafting.lua
deleted file mode 100644
index 43eed56..0000000
--- a/crafting.lua
+++ /dev/null
@@ -1,635 +0,0 @@
-if IsDuplicityVersion() then
- if GetResourceState(OXLibExport):find("start") then
- createCallback(GetCurrentResourceName()..':server:GetStashItems', function(source, stashName) local stash = getStash(stashName) return stash end)
- else
- createCallback(GetCurrentResourceName()..':server:GetStashItems', function(source, cb, stashName) local stash = getStash(stashName) cb(stash) end)
- end
-end
-
-local timeout, timing, stashItems = 0, false, {}
-function GetStashTimeout(stashName, stop)
- if stop then stashItems, timing, timeout = {}, false, 0 return end
- if #stashItems > 0 then return true end
- if timeout <= 0 then
- stashItems = triggerCallback(GetCurrentResourceName()..':server:GetStashItems', stashName)
- timeout = 10000
- if not timing then
- CreateThread(function()
- timing = true
- while timeout > 0 do timeout -= 1000 Wait(1000) end
- timing, stashItems, timeout = false, {}, 0
- end)
- end
- end
- return false
-end
-
-local CraftLock = false
-function craftingMenu(data)
- if CraftLock then return end
- if data.stashName and not GetStashTimeout(data.stashName) then
- --triggerNotify(nil, "Chacking", "success")
- end
- if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
- local Menu, hasjob = {}, false
- local Recipes = data.craftable.Recipes
- local tempCarryTable = {}
- for i = 1, #Recipes do
- for k in pairs(Recipes[i]) do
- if k ~= "amount" and k ~= "job" and k ~= "gang" then
- tempCarryTable[k] = Recipes[i].amount or 1
- end
- end
- end
- local canCarryTable = triggerCallback(GetCurrentResourceName()..':server:canCarry', tempCarryTable)
- for i = 1, #Recipes do
- if not Recipes[i]["amount"] then Recipes[i]["amount"] = 1 end
- for k, v in pairs(Recipes[i]) do
- if k ~= "amount" and k ~= "job" and k ~= "gang" then
- if Recipes[i].job then
- for l, b in pairs(Recipes[i].job) do
- hasjob = hasJob(l, nil, b)
- if hasjob == true then break end
- end
- else hasjob = true end
- local setheader, settext, disable = "", "", false
- if hasjob then
- local itemTable = {}
- 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 "")
- itemTable[l] = b
- Wait(0)
- end
- while not canCarryTable do Wait(0) end
- if Config.System.Debug then print("^6Bridge^7: ^2Checking"..(data.stashName and " ^7'^6"..data.stashName.."^7'" or "").." ^2ingredients^7 - ^6"..k.."^7") end
- if data.stashName then disable = not stashhasItem(stashItems, itemTable)
- else disable = not hasItem(itemTable) end
- setheader = (Items[tostring(k)] and Items[tostring(k)].label or "error - " .. tostring(k)) .. (Recipes[i]["amount"] > 1 and " x" .. Recipes[i]["amount"] or "")
- if not disable then
- if not canCarryTable[k] then setheader = setheader .. " 📦"
- else setheader = setheader .. " ✔️" end
- elseif not canCarryTable[k] then setheader = setheader .. " 📦" end
- Menu[#Menu + 1] = {
- isMenuHeader = disable or not canCarryTable[k],
- icon = invImg(tostring(k)),
- header = setheader,
- txt = settext,
- onSelect = function()
- local transdata = { item = k, craft = data.craftable.Recipes[i], craftable = data.craftable, coords = data.coords, stashName = data.stashName, onBack = data.onBack, }
- if Config.Crafting.MultiCraft then multiCraft(transdata) else makeItem(transdata) end
- end,
- }
- end
- end
- Wait(0)
- end
- end
- openMenu(Menu, { header = data.craftable.Header, onBack = data.onBack or nil, canClose = true, onExit = function() end, })
- lookEnt(data.coords)
-end
-
-function multiCraft(data) local Menu = {}
- local success = Config.Crafting.MultiCraftAmounts
- if data.stashName and not GetStashTimeout(data.stashName) then
- --triggerNotify(nil, "Refreshing stashinfo", "success")
- end
- Menu[#Menu+1] = {
- isMenuHeader = true,
- icon = invImg(data.item),
- header = Items[data.item].label,
- }
- for k in pairsByKeys(success) do
- local settext = ""
- local itemTable = {}
- for l, b in pairs(data.craft[data.item]) do
- itemTable[l] = (b * k)
- settext = settext..(settext ~= "" and br or "")..Items[l].label..(b*k > 1 and "- x"..b*k or "")
- Wait(0)
- end
- local disable = false
- if Config.System.Debug then print("^6Bridge^7: ^2Checking "..(data.stashName and "^7'^6"..data.stashName.."^7'" or "inventory").."^7x^5"..k.." ^2ingredients^7 - ^6"..data.item.."^7") end
- if data.stashName then disable = not stashhasItem(stashItems, itemTable)
- else disable = not hasItem(itemTable) end
-
- Menu[#Menu + 1] = {
- isMenuHeader = disable,
- arrow = not disable,
- header = "Craft - x"..k * data.craft.amount,
- txt = settext,
- onSelect = function ()
- makeItem({item = data.item, craft = data.craft, craftable = data.craftable, amount = k, coords = data.coords, stashName = data.stashName, onBack = data.onBack })
- end,
- }
- end
- openMenu(Menu, { header = data.craftable.Header, onBack = function() craftingMenu(data) end, })
-end
-
-function makeItem(data)
- if CraftLock then return end
- CraftLock = true
-
- local bartime = data.craftable.progressBar and data.craftable.progressBar.time or 5000
- local bartext = (data.craftable.progressBar and data.craftable.progressBar.label) or (Loc[Config.Lan].progressbar and Loc[Config.Lan].progressbar["progress_make"]) or "Making a"
- local animDict = data.craftable.Anims and data.craftable.Anims.animDict or "amb@prop_human_parking_meter@male@idle_a"
- local anim = data.craftable.Anims and data.craftable.Anims.anim or "idle_a"
- local amount = data.amount and (data.amount ~= 1) and data.amount or 1
-
- local crafted, crafting = true, true
- local cam = createTempCam(PlayerPedId(), data.coords)
- startTempCam(cam)
- for i = 1, amount do
- for k, v in pairs(data.craft) do
- if k ~= "amount" and k ~= "job" then
- if type(v) == "table" then
- for l, b in pairs(v) do
- if crafting and progressBar({
- label = "Using "..b.." "..Items[l].label,
- time = 1000,
- cancel = true,
- dict = 'pickup_object',
- anim = "putdown_low",
- flag = 48,
- icon = l,
- }) then
- --TriggerEvent('inventory:client:ItemBox', Items[l], "use", b) -- Show item box for each item
- else
- crafted, crafting = false, false
- break
- end
- Wait(200)
- end
- if crafted then
- if crafting and progressBar({
- label = bartext..Items[data.item].label,
- time = bartime,
- cancel = true,
- dict = animDict,
- anim = anim,
- flag = 8,
- icon = data.item,
- }) then
- TriggerServerEvent(GetCurrentResourceName()..":Crafting:GetItem", data.item, data.craft, data.stashName)
- else
- crafting = false
- break
- end
- end
- end
- end
- end
- Wait(500)
- end
- stopTempCam()
- CraftLock = false
- lockInv(false)
- craftingMenu(data)
- ClearPedTasks(PlayerPedId())
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":Crafting:GetItem", function(ItemMake, craftable, stashName)
- local src, amount, stashItems = source, craftable and craftable.amount or 1, stashName and getStash(stashName)
- if stashName then
- local itemRemove = {}
- for k, v in pairs(craftable[ItemMake] or {}) do
- for _, b in pairs(stashItems or {}) do
- if k == b.name then itemRemove[k] = v end
- end
- end
- stashRemoveItem(stashItems, stashName, itemRemove)
- else
- if craftable then
- for k, v in pairs(craftable[ItemMake] or {}) do
- TriggerEvent(GetCurrentResourceName()..":server:toggleItem", false, tostring(k), v, src)
- end
- end
- end
- TriggerEvent(GetCurrentResourceName()..":server:toggleItem", true, ItemMake, amount, src)
- if GetResourceState("core_skills"):find("start") then exports["core_skills"]:AddExperience(src, 2) end
-end)
-
---[[SHOPS]]--
-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 hasitems, 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), canClose = true, onBack = data.onBack })
-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
- for k, v in pairs(GetGamePool('CObject')) do
- for _, model in pairs({`p_cs_clipboard`}) do
- if GetEntityModel(v) == model then
- if IsEntityAttachedToEntity(data.ped, v) then
- DeleteObject(v) DetachEntity(v, 0, 0) SetEntityAsMissionEntity(v, true, true)
- Wait(100) DeleteEntity(v)
- end
- end
- end
- end
- TriggerServerEvent(GetCurrentResourceName().."Sellitems", data) -- Had to slip in the sell command during the animation command
- lookEnt(data.ped)
- local dict = "mp_common"
- playAnim(dict, "givetake2_a", 0.3, 2)
- playAnim(dict, "givetake2_b", 0.3, 2, 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
-
-RegisterNetEvent(GetCurrentResourceName().."Sellitems", function(data)
- local src = source
- local hasItems, hasTable = hasItem(data.item, 1, src)
- if hasItems then
- TriggerEvent(GetCurrentResourceName()..":server:toggleItem", false, data.item, hasTable[data.item].count, src)
- TriggerEvent(GetCurrentResourceName()..":server: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)
-
-function openShop(data)
- if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
- if GetResourceState(OXInv):find("start") then
- exports[OXInv]:openInventory('shop', { type = data.shop })
- else
- TriggerServerEvent(Config.General.JimShops and "jim-shops:ShopOpen" or "inventory:server:OpenInventory", "shop", data.items.label, data.items)
- end
- lookEnt(data.coords)
-end
-
--- Client & Server side
-function hasItem(items, amount, src) local amount = amount and amount or 1
- local grabInv = nil
- local foundInv = ""
- if type(items) ~= "table" then items = { [items] = amount and amount or 1, } end
- if GetResourceState(OXInv):find("start") then
- foundInv = OXInv
- if src then grabInv = exports[OXInv]:GetInventoryItems(src)
- else grabInv = exports[OXInv]:GetPlayerItems() end
-
- elseif GetResourceState(QSInv):find("start") then
- foundInv = QSInv
- if src then grabInv = exports[QSInv]:GetInventory(src)
- else grabInv = exports[QSInv]:getUserInventory() end
-
- elseif GetResourceState(OrigenInv):find("start") then
- foundInv = OrigenInv
- if src then grabInv = exports[OrigenInv]:GetInventory(src)
- else grabInv = exports[OrigenInv]:getPlayerInventory() end
-
- elseif GetResourceState(CoreInv):find("start") then
- foundInv = CoreInv
- if src then
- if GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then
- grabInv = Core.Functions.GetPlayer(src).PlayerData.items
- elseif GetResourceState(ESXExport):find("start") then
- local Player = ESX.GetPlayerFromId(src)
- grabInv = Player.getInventory(false)
- end
- else
- local p = promise.new()
- Core.Functions.TriggerCallback('core_inventory:server:getInventory', function(cb) p:resolve(cb) end)
- local result = Citizen.Await(p)
- if type(result) == "string" then result = json.decode(result) end
- grabInv = result
- end
-
- elseif GetResourceState(CodeMInv):find("start") then
- foundInv = CodeMInv
- if src then grabInv = exports[CodeMInv]:GetUserInventory(src)
- else grabInv = exports[CodeMInv]:GetClientPlayerInventory() end
-
- elseif GetResourceState(QBInv):find("start") then
- foundInv = QBInv
- 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 ^3exports^1.^2lua^7")
- end
-
- if grabInv then
- local hasTable = {}
- for item, amount 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"..amount
- if count >= amount then foundMessage = foundMessage.." ^5FOUND^7" else foundMessage = foundMessage .." ^1NOT FOUND^7" end
- if Config.System.Debug then print(foundMessage) end
- hasTable[item] = { hasItem = count >= amount, 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
-
--- Stash Items
-function openStash(data)
- if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
- if GetResourceState(OXInv):find("start") then
- exports[OXInv]:openInventory('stash', data.stash)
- elseif GetResourceState(CodeMInv):find("start") then
- exports[CodeMInv]:OpenStash(data.stash, 400000, 100)
- else
- TriggerEvent("inventory:client:SetCurrentStash", data.stash)
- TriggerServerEvent("inventory:server:OpenInventory", "stash", data.stash, data.stashOptions)
- end
- lookEnt(data.coords)
-end
-
-function getStash(stashName) local stashResource = ""
- local stashItems, items = {}, {}
- if GetResourceState(OXInv):find("start") then stashResource = OXInv
- stashItems = exports[OXInv]:Inventory(stashName).items
-
- elseif GetResourceState(QSInv):find("start") then stashResource = QSInv
- stashItems = exports[QSInv]:GetStashItems(stashName)
-
- elseif GetResourceState(CoreInv):find("start") then stashResource = CoreInv
- stashItems = exports[CoreInv]:getInventory(stashName)
-
- elseif GetResourceState(CodeMInv):find("start") then stashResource = CodeMInv
- stashItems = exports[CodeMInv]:GetInventoryItems('Stash', stashName)
-
- elseif GetResourceState(OrigenInv):find("start") then stashResource = OrigenInv
- stashItems = exports[OrigenInv]:GetStashItems(stashName)
-
- elseif GetResourceState(QBInv):find("start") then stashResource = QBInv
- local result = MySQL.scalar.await('SELECT items FROM stashitems WHERE stash = ?', { stashName })
- if result then stashItems = json.decode(result) end
- end
-
- if Config.System.Debug then print("^6Bridge^7: ^2Retrieving ^3Stash^2 with ^7"..stashResource) end
- if stashItems then
- for _, item in pairs(stashItems) do
- local itemInfo = Items[item.name:lower()]
- if itemInfo then
- local indexNum = #items+1 -- Added to help recreate missing slot numbers
- items[(item.slot and 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,
- }
- end
- end
- if Config.System.Debug then print("^6Bridge^7: ^3GetStashItems^7: ^2Stash information for ^7'^6"..stashName.."^7' ^2retrieved^7") end
- end
- return items
-end
-
-function stashRemoveItem(stashItems, stashName, items) local amount = amount and amount or 1
- if GetResourceState(OXInv):find("start") then
- for k, v in pairs(items) do
- exports[OXInv]:RemoveItem(stashName, k, v)
- if Config.System.Debug then print("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..OXInv, k, v) end
- end
-
- elseif GetResourceState(QSInv):find("start") 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
- if Config.System.Debug then
- print("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
- end
- stashItems[l] = nil
- else
- if Config.System.Debug then
- print("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..QBInv, k, v)
- end
- exports[QSInv]:RemoveItemIntoStash(stashName, k, v, l)
- end
- end
- end
- end
-
- elseif GetResourceState(CoreInv):find("start") then
- for k, v in pairs(items) do
- exports[CoreInv]:removeItemExact(stashName, k, v)
- if Config.System.Debug then print("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..CoreInv, k, v) end
- end
-
- elseif GetResourceState(CodeMInv):find("start") 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
- if Config.System.Debug then
- print("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
- end
- stashItems[l] = nil
- else
- if Config.System.Debug then
- print("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..CodeMInv, k, v)
- end
- stashItems[l].amount -= v
- end
- end
- end
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash ^7'^6"..stashName.."^7'")
- end
-
- elseif GetResourceState(OrigenInv):find("start") then
- for k, v in pairs(items) do
- exports[OrigenInv]:RemoveFromStash(stashName, k, v)
- if Config.System.Debug then print("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..OrigenInv, k, v) end
- end
-
- elseif GetResourceState(QBInv):find("start") 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
- if Config.System.Debug then
- print("^6Bridge^7: ^2None of this item left in stash ^3Stash^7", k, v)
- end
- stashItems[l] = nil
- else
- if Config.System.Debug then
- print("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..QBInv, k, v)
- end
- stashItems[l].amount -= v
- end
- end
- end
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash ^7'^6"..stashName.."^7'")
- end
- MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { ['stash'] = stashName, ['items'] = json.encode(stashItems) })
- else
- print("^4ERROR^7: ^2No Inventory detected ^7- ^2Check ^3exports^1.^2lua^7")
- end
-end
-RegisterNetEvent(GetCurrentResourceName()..":server:stashRemoveItem", stashRemoveItem)
-
-function stashhasItem(stashItems, items, amount)
- local invs = {OXInv, QSInv, CoreInv, CodeMInv, OrigenInv, QBInv}
- local foundInv = ""
- for _, inv in ipairs(invs) do
- if GetResourceState(inv):find("start") then
- foundInv = inv:gsub("%-", "^7-^6"):gsub("%_", "^7_^6")
- break
- end
- end
-
- if type(items) ~= "table" then items = { [items] = amount and amount or 1, } end
- local hasTable = {}
- for item, amount 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 >= amount and "^5FOUND^7" or "^1NOT FOUND^7"), item, count, amount)
- if Config.System.Debug then print(debugMsg) end
-
- hasTable[item] = { hasItem = (count >= amount), count = count }
- end
- for k, v in pairs(hasTable) do if v.hasItem == false then return false, hasTable end end
- return true, hasTable
-end
-
-if IsDuplicityVersion() then
- if GetResourceState(OXLibExport):find("start") then
- createCallback(GetCurrentResourceName()..':server:canCarry', function(source, itemTable) local result = canCarry(itemTable, source) return result end)
- else
- createCallback(GetCurrentResourceName()..':server:canCarry', function(source, cb, itemTable) local result = canCarry(itemTable, source) cb(result) end)
- end
-end
-
-function canCarry(itemTable, src)
- local resultTable = {}
- if src then
- if GetResourceState(OXInv):find("start") then
- for k, v in pairs(itemTable) do
- resultTable[k] = exports[OXInv]:CanCarryItem(src, k, v)
- end
-
- elseif GetResourceState(QSInv):find("start") then
- for k, v in pairs(itemTable) do
- resultTable[k] = exports[OXInv]:CanCarryItem(src, k, v)
- end
-
- elseif GetResourceState(CoreInv):find("start") then
- --??
-
- elseif GetResourceState(CodeMInv):find("start") then
- for k, v in pairs(itemTable) do
- local weight = Items[k].weight
- resultTable[k] = exports[CodeMInv]:CanCarryItem(src, weight, v)
- end
-
- elseif GetResourceState(OrigenInv):find("start") then
- for k, v in pairs(itemTable) do
- resultTable[k] = exports[OrigenInv]:canCarryItem(src, k, v)
- end
-
- elseif GetResourceState(QBInv):find("start") then
- local Player = Core.Functions.GetPlayer(src)
- local items = Player.PlayerData.items
- local weight, totalWeight = 0, 0
- if not items then return false end
- for _, item in pairs(items) do weight += item.weight * item.amount end
- totalWeight = tonumber(weight)
-
- for k, v in pairs(itemTable) do
- local itemInfo = Items[k]
- if not itemInfo and not Player.Offline then
- triggerNotify(nil, 'Item does not exist', 'error', src)
- resultTable[k] = true
- else
- resultTable[k] = (totalWeight + (Items[k]['weight'] * v)) <= 120000
- end
- end
- end
- end
- return resultTable
-end
-
-function getRandomReward(itemName) -- intended for job scripts
- if Config.Rewards.RewardPool then
- local reward = false
- if type(Config.Rewards.RewardItem) == "string" then Config.Rewards.RewardItem = { Config.Rewards.RewardItem } end
- for k, v in pairs(Config.Rewards.RewardItem) do
- if v == itemName then reward = true break end
- end
- if reward then
- removeItem(itemName, 1)
- local totalRarity = 0
- for i=1, #Config.Rewards.RewardPool do
- totalRarity += Config.Rewards.RewardPool[i].rarity
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3getRandomReward^7: ^2Total Rarity ^7'^6"..totalRarity.."^7'")
- end
-
- local randomNum = math.random(1, totalRarity)
- if Config.System.Debug then
- print("^6Bridge^7: ^3getRandomReward^7: ^2Random Number ^7'^6"..randomNum.."^7'")
- end
- local currentRarity = 0
- for i=1, #Config.Rewards.RewardPool do
- currentRarity += Config.Rewards.RewardPool[i].rarity
- if randomNum <= currentRarity then
- if Config.System.Debug then
- print("^6Bridge^7: ^3getRandomReward^7: ^2Selected toy ^7'^6"..Config.Rewards.RewardPool[i].item.."^7'")
- end
- addItem(Config.Rewards.RewardPool[i].item, 1)
- return
- end
- end
- end
- end
-end
\ No newline at end of file
diff --git a/exports.lua b/exports.lua
deleted file mode 100644
index bab1fa0..0000000
--- a/exports.lua
+++ /dev/null
@@ -1,20 +0,0 @@
-Exports = {
- QBExport = "qb-core",
- QBXExport = "qbx_core",
- ESXExport = "es_extended",
- OXCoreExport = "ox_core",
-
- OXInv = "ox_inventory",
- QBInv = "qb-inventory",
- QSInv = "qs-inventory",
- CoreInv = "core_inventory",
- CodeMInv = "codem-inventory",
- OrigenInv = "origen_inventory",
-
- OXLibExport = "ox_lib",
-
- QBMenuExport = "qb-menu",
-
- QBTargetExport = "qb-target",
- OXTargetExport = "ox_target"
-}
\ No newline at end of file
diff --git a/functions.lua b/functions.lua
deleted file mode 100644
index 44c8147..0000000
--- a/functions.lua
+++ /dev/null
@@ -1,1168 +0,0 @@
-onDuty = false
-function jobCheck(job)
- canDo = true
- if not hasJob(job) or not onDuty then
- triggerNotify(nil, Loc[Config.Lan].error["not_clockedin"])
- canDo = false
- end
- return canDo
-end
-
-local time = 500
-function loadModel(model)
- if not IsModelValid(model) then print("^6Bridge^7: ^1ERROR^7: ^2Model^7 - '^6"..model.."^7' ^2does not exist in server") return
- else
- if not HasModelLoaded(model) then
- if Config.System.Debug then print("^6Bridge^7: ^2Loading Model^7: '^6"..model.."^7'") end
- while not HasModelLoaded(model) and time > 0 do time -= 1 RequestModel(model) Wait(0) end
- if not HasModelLoaded(model) then print("^6Bridge^7: ^3LoadModel^7: ^2Timed out loading model ^7'^6"..model.."^7'") end
- end
- time = 500
- end
-end
-function unloadModel(model) if Config.System.Debug then print("^6Bridge^7: ^2Removing Model from memory cache^7: '^6"..model.."^7'") end SetModelAsNoLongerNeeded(model) end
-
-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
- if Config.System.Debug then print("^6Bridge^7: ^2Loading Anim Dictionary^7: '^6"..animDict.."^7'") end
- while not HasAnimDictLoaded(animDict) do RequestAnimDict(animDict) Wait(5) end
- end
-end
-function unloadAnimDict(animDict) if Config.System.Debug then print("^6Bridge^7: ^2Removing Anim Dictionary from memory cache^7: '^6"..animDict.."^7'") end RemoveAnimDict(animDict) end
-
-function loadPtfxDict(ptFxName)
- if not HasNamedPtfxAssetLoaded(ptFxName) then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Loading Ptfx Dictionary^7: '^6"..ptFxName.."^7'")
- end
- while not HasNamedPtfxAssetLoaded(ptFxName) do RequestNamedPtfxAsset(ptFxName) Wait(5) end
- end
-end
-function unloadPtfxDict(dict) if Config.System.Debug then print("^6Bridge^7: ^2Removing Ptfx Dictionary^7: '^6"..dict.."^7'") end RemoveNamedPtfxAsset(dict) end
-
-function loadTextureDict(dict)
- if not HasStreamedTextureDictLoaded(dict) then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Loading Texture Dictionary^7: '^6"..dict.."^7'")
- end
- while not HasStreamedTextureDictLoaded(dict) do RequestStreamedTextureDict(dict) Wait(5) end
- end
-end
-
-function countTable(table) local i = 0 for keys in pairs(table) do i += 1 end return i end
-
-function pairsByKeys(t) local a = {} for n in pairs(t) do a[#a+1] = n end table.sort(a) local i = 0 local iter = function() i += 1 if a[i] == nil then return nil else return a[i], t[a[i]] end end return iter end
-
-function playAnim(animDict, animName, duration, flag, ped)
- loadAnimDict(animDict)
- TaskPlayAnim(ped and ped or PlayerPedId(), animDict, animName, 8.0, -8.0, duration or 30000, flag or 50, 1, false, false, false)
-end
-
-function stopAnim(animDict, animName, ped)
- StopAnimTask(ped or PlayerPedId(), animDict, animName, 0.5)
- StopAnimTask(ped or PlayerPedId(), animName, animDict, 0.5)
- unloadAnimDict(animDict)
-end
-
-function makeVeh(model, coords)
- loadModel(model)
- local veh = CreateVehicle(model, coords.x, coords.y, coords.z, coords.w, true, false)
- SetVehicleHasBeenOwnedByPlayer(veh, true)
- SetNetworkIdCanMigrate(NetworkGetNetworkIdFromEntity(veh), true)
- Wait(100)
- SetVehicleNeedsToBeHotwired(veh, false)
- SetVehRadioStation(veh, 'OFF')
- SetVehicleFuelLevel(veh, 100.0)
- SetVehicleModKit(veh, 0)
- SetVehicleOnGroundProperly(veh)
- if Config.System.Debug then
- local coords = { string.format("%.2f", coords.x), string.format("%.2f", coords.y), string.format("%.2f", coords.z), (string.format("%.2f", coords.w or 0.0)) }
- print("^6Bridge^7: ^1Veh ^2Created^7: '^6"..veh.."^7' | ^2Hash^7: ^7'^6"..(model).."^7' | ^2Coord^7: ^5vec4^7(^6"..(coords[1]).."^7, ^6"..(coords[2]).."^7, ^6"..(coords[3]).."^7, ^6"..(coords[4]).."^7)")
- end
- unloadModel(model)
- return veh
-end
-
-local Peds = {}
-local Props = {}
-function makePed(model, coords, freeze, collision, scenario, anim, synced)
- loadModel(model)
- local ped = CreatePed(0, model, coords.x, coords.y, coords.z-1.03, coords.w, synced and synced or false, false)
- SetEntityInvincible(ped, true)
- SetBlockingOfNonTemporaryEvents(ped, true)
- FreezeEntityPosition(ped, freeze and freeze or true)
-
- if collision then SetEntityNoCollisionEntity(ped, PlayerPedId(), false) end
- if scenario then TaskStartScenarioInPlace(ped, scenario, 0, true) end
- if anim then
- loadAnimDict(anim[1])
- TaskPlayAnim(ped, anim[1], anim[2], 0.5, 1.0, -1, 1, 0.2, 0, 0, 0)
- end
- if Config.System.Debug then
- local coords = { string.format("%.2f", coords.x), string.format("%.2f", coords.y), string.format("%.2f", coords.z), (string.format("%.2f", coords.w or 0.0)) }
- print("^6Bridge^7: ^1Ped ^2Created^7: '^6"..ped.."^7' | ^2Hash^7: ^7'^6"..(model).."^7' | ^2Coord^7: ^5vec4^7(^6"..(coords[1]).."^7, ^6"..(coords[2]).."^7, ^6"..(coords[3]).."^7, ^6"..(coords[4]).."^7)")
- end
- unloadModel(model)
- Peds[#Peds+1] = ped
- return ped
-end
-
-function makeProp(data, freeze, synced)
- loadModel(data.prop)
- local prop = CreateObject(data.prop, data.coords.x, data.coords.y, data.coords.z-1.03, synced and synced or false, synced and synced or false, false)
- SetEntityHeading(prop, data.coords.w + 180.0)
- FreezeEntityPosition(prop, freeze and freeze or 0)
- if Config.System.Debug then
- local coords = { string.format("%.2f", data.coords.x), string.format("%.2f", data.coords.y), string.format("%.2f", data.coords.z), (string.format("%.2f", data.coords.w or 0.0)) }
- print("^6Bridge^7: ^1Prop ^2Created^7: '^6"..prop.."^7' | ^2Hash^7: ^7'^6"..(data.prop).."^7' | ^2Coord^7: ^5vec4^7(^6"..(coords[1]).."^7, ^6"..(coords[2]).."^7, ^6"..(coords[3]).."^7, ^6"..(coords[4]).."^7)")
- end
- unloadModel(data.prop)
- Props[#Props+1] = prop
- return prop
-end
-
-function cv(amount)
- local formatted = 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) end
- return formatted
-end
-
-function DrawText3D(x, y, z, text)
- SetTextScale(0.35, 0.35)
- SetTextFont(4)
- SetTextProportional(true)
- SetTextColour(255, 255, 255, 215)
- BeginTextCommandDisplayText("STRING")
- SetTextCentre(true)
- AddTextComponentSubstringPlayerName(text)
- SetDrawOrigin(x,y,z, 0)
- EndTextCommandDisplayText(0.0, 0.0)
- local factor = string.len(text) / 370
- DrawRect(0.0, 0.0 + 0.0125, 0.017 + factor, 0.03, 0, 0, 0, 75)
- ClearDrawOrigin()
-end
-
-function instantLookEnt(ent, ent2)
- local p1 = GetEntityCoords(ent, true)
- local p2 = GetEntityCoords(ent2, true)
-
- local dx = p2.x - p1.x
- local dy = p2.y - p1.y
-
- local heading = GetHeadingFromVector_2d(dx, dy)
- SetEntityHeading( ent, heading )
-end
-
-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)
- if Config.System.Debug then print("^6Bridge^7: ^2Turning Player to^7: '^6"..json.encode(entity).."^7'") end
- Wait(1500)
- end
- else
- if DoesEntityExist(entity) then
- if not IsPedHeadingTowardsPosition(ped, GetEntityCoords(entity), 30.0) then
- TaskTurnPedToFaceCoord(ped, GetEntityCoords(entity), 1500)
- if Config.System.Debug then print("^6Bridge^7: ^2Turning Player to^7: '^6"..entity.."^7'") end
- Wait(1500)
- end
- end
- end
- end
-end
-
-function destroyProp(entity)
- if entity then
- if Config.System.Debug then print("^6Bridge^7: ^2Destroying Prop^7: '^6"..entity.."^7'") end
- if IsEntityAttachedToEntity(entity, PlayerPedId()) then
- SetEntityAsMissionEntity(entity)
- DetachEntity(entity, true, true)
- end
- DeleteObject(entity)
- end
-end
-
-function pushVehicle(entity)
- SetVehicleModKit(entity, 0)
- if entity ~= 0 and DoesEntityExist(entity) then
- if not NetworkHasControlOfEntity(entity) then
- if Config.System.Debug then print("^6Bridge^7: ^3pushVehicle^7: ^2Requesting network control of vehicle^7.") end
- NetworkRequestControlOfEntity(entity)
- local timeout = 2000
- while timeout > 0 and not NetworkHasControlOfEntity(entity) do
- Wait(100)
- timeout = timeout - 100
- end
- if NetworkHasControlOfEntity(entity) and Config.System.Debug then print("^6Bridge^7: ^3pushVehicle^7: ^2Network has control of entity^7.") end
- end
- if not IsEntityAMissionEntity(entity) then
- if Config.System.Debug then print("^6Bridge^7: ^3pushVehicle^7: ^2Setting vehicle as a ^7'^2mission^7' &2entity^7.") end
- SetEntityAsMissionEntity(entity, true, true)
- local timeout = 2000
- while timeout > 0 and not IsEntityAMissionEntity(entity) do
- Wait(100)
- timeout = timeout - 100
- end
- if IsEntityAMissionEntity(entity) and Config.System.Debug then print("^6Bridge^7: ^3pushVehicle^7: ^2Vehicle is a ^7'^2mission^7'^2 entity^7.") end
- end
- end
-end
-
-function ensureNetToVeh(vehNetID)
- if Config.System.Debug then print("^6Bridge^7: ^3ensureNetToVeh^7: ^2Requesting NetworkDoesNetworkIdExist^7(^6"..vehNetID.."^7)") end
- local timeout = 100
- while not NetworkDoesNetworkIdExist(vehNetID) and timeout > 0 do timeout -= 1 Wait(10) end
- if not NetworkDoesNetworkIdExist(vehNetID) then return 0 end
- timeout = 100
- local vehicle = NetToVeh(vehNetID)
- while not DoesEntityExist(vehicle) and vehicle ~= 0 and timeout > 0 do timeout -= 1 Wait(10) end
- if not DoesEntityExist(vehicle) then return 0 end
- return vehicle
-end
-
-local scriptTxd = not IsDuplicityVersion() and CreateRuntimeTxd(GetCurrentResourceName()..'scriptTxd') or nil
-
-local customDUIList = {}
-function makeBlip(data)
- local blip = AddBlipForCoord(vec3(data.coords.x, data.coords.y, data.coords.z))
- SetBlipAsShortRange(blip, true)
- SetBlipSprite(blip, data.sprite or 106)
- SetBlipColour(blip, data.col or 5)
- SetBlipScale(blip, data.scale or 0.7)
- SetBlipDisplay(blip, (data.disp or 6))
- if data.category then SetBlipCategory(blip, data.category) end
- BeginTextCommandSetBlipName('STRING')
- AddTextComponentString(tostring(data.name))
- EndTextCommandSetBlipName(blip)
- if GetResourceState("blip_info"):find("start") or GetResourceState("blip-info"):find("start") or GetResourceState("blipinfo"):find("start") then
- if data.preview then
- local txname = tostring(data.name..'preview'..string.gsub(data.coords.z, "%.", ""))
- if data.preview:find("http") then
- createDui(txname, data.preview, vec2(512, 256), scriptTxd)
- else
- CreateRuntimeTextureFromImage(scriptTxd, txname, data.preview)
- end
- exports["blip_info"]:SetBlipInfoImage(blip, GetCurrentResourceName()..'scriptTxd', txname)
- exports["blip_info"]:SetBlipInfoTitle(blip, data.name, false)
- end
- end
- if Config.System.Debug then print("^6Bridge^7: ^6Blip ^2created for location^7: '^6"..data.name.."^7'") end
- return blip
-end
-
-function makeEntityBlip(data)
- AddBlipForEntity(data.entity)
- local blip = GetBlipFromEntity(data.entity)
- SetBlipAsShortRange(blip, true)
- SetBlipSprite(blip, data.sprite or 106)
- SetBlipColour(blip, data.col or 5)
- SetBlipScale(blip, data.scale or 0.7)
- SetBlipDisplay(blip, (data.disp or 6))
- if data.category then SetBlipCategory(blip, data.category) end
- BeginTextCommandSetBlipName('STRING')
- AddTextComponentString(tostring(data.name))
- EndTextCommandSetBlipName(blip)
- if GetResourceState("blip_info"):find("start") or GetResourceState("blip-info"):find("start") or GetResourceState("blipinfo"):find("start") then
- if data.preview then
- local txname = data.name..'preview'
- if data.preview:find("http") then
- createDui(txname, data.preview, vec2(512, 256), scriptTxd)
- else
- CreateRuntimeTextureFromImage(scriptTxd, txname, data.preview)
- end
- exports["blip_info"]:SetBlipInfoImage(blip, GetCurrentResourceName()..'previewTxd', txname)
- exports["blip_info"]:SetBlipInfoTitle(blip, data.name, false)
- end
- end
- if Config.System.Debug then print("^6Bridge^7: ^6Blip ^2created for Entity^7: '^6"..data.name.."^7'") end
- return blip
-end
-
--- DUI STUFF - WIP --
-
--- DUI CLIENT
-function createDui(name, http, size, txd)
- if not customDUIList[name] then
- local newTxt = CreateDui(http, math.floor(size.x), math.floor(size.y))
- while not GetDuiHandle(newTxt) do Wait(0) end
- CreateRuntimeTextureFromDuiHandle(txd, name, GetDuiHandle(newTxt))
- customDUIList[name] = newTxt
- SetDuiUrl(customDUIList[name], http)
- else
- SetDuiUrl(customDUIList[name], http)
- end
-end
-
-function DuiSelect(data)
- local image = ""
- for k, v in pairs(duiList[data.name]) do
- if v.tex.texn == data.texn then
- if duiList[data.name][k] then
- image = "
"
- end
- end
- end
- local dialog = exports['qb-input']:ShowInput({
- header = image..Loc[Config.Lan].menu["dui_new"],
- submitText = Loc[Config.Lan].menu["dui_change"],
- inputs = { { type = 'text', isRequired = true, name = 'url', text = Loc[Config.Lan].menu["dui_url"] } } })
- if dialog then
- if not dialog.url then return end
- data.url = dialog.url
- --Scan the link to see if it has an image extention otherwise, stop here.
- local searchList = { "png", "jpg", "jpeg", "gif", "webp", "bmp" }
- --Scan the link for certain terms that will flag it and refuse to show it
- local banList = { "porn" } -- I dunno, let me know what links people manage to find
- local searchFound = false
- for k, v in pairs(searchList) do
- if string.find(tostring(data.url), tostring(v))then
- searchFound = true
- end
- end
- for k, v in pairs(banList) do
- if string.find(tostring(data.url), tostring(v)) then
- searchFound = false print("BANNED WORD: "..v)
- end
- end
- if searchFound then
- TriggerServerEvent(GetCurrentResourceName()..":Server:ChangeDUI", data)
- end
- end
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":Client:ChangeDUI", function(data)
- if Config.System.Debug then print("^6Bridge^7: ^2Recieving new DUI ^7- ^6"..data.url.."^7") end
- if tostring(data.url) ~= "-" then
- createDui(data.texn, tostring(data.url), data.size, scriptTxd)
- AddReplaceTexture(tostring(data.texd), tostring(data.texn), GetCurrentResourceName()..'scriptTxd', tostring(data.texn))
- end
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":Client:ClearDUI", function(data)
- if customDUIList[tostring(data.texn)] then
- RemoveReplaceTexture(tostring(data.texd), tostring(data.texn))
- if IsDuiAvailable(customDUIList[tostring(data.texn)]) then
- SetDuiUrl(customDUIList[data.name], nil)
- end
- end
-end)
-
--- DUI SERVER
-RegisterNetEvent(GetCurrentResourceName()..":Server:ChangeDUI", function(data)
- -- if no url given, "reset" it back to preset
- if not data.url then
- for k, v in pairs(duiList[data.name]) do
- if v.tex.texn == data.texn then
- if Config.System.Debug then print("^6Bridge^7: ^2Preset^7: ^6"..tostring(duiList[data.name][k].preset).."^7") end
- data.url = duiList[data.name][k].preset
- end
- end
- end
- -- if it has a url, update server DUI list and send to players
- for k, v in pairs(duiList[data.name]) do
- if v.tex.texn == data.texn then
- duiList[data.name][k].url = data.url
- end
- end
- if Config.System.Debug then print("^6Bridge^7: ^3DUI^2 Sending new DUI to all players^7 - ^6"..data.url.."^7") end
- TriggerClientEvent(GetCurrentResourceName()..":Client:ChangeDUI", -1, data)
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":Server:ClearDUI", function(data)
- if data.url == "-" then
- for k, v in pairs(duiList[data.name]) do
- if v.tex.texn == data.texn then
- duiList[data.name][k].url = "-"
- end
- end
- end
- -- Clear the DUI from loading
- TriggerClientEvent(GetCurrentResourceName()..":Client:ClearDUI", -1, data)
- --duiList[tostring(data.tex)].url = ""
-end)
-
-AddEventHandler('onResourceStop', function(r) if r ~= GetCurrentResourceName() then return end
- for k, v in pairs(duiList or {}) do
- for i = 1, #v do
- RemoveReplaceTexture(tostring(v[i].tex.texd), tostring(v[i].tex.texn))
- end
- end
-end)
-
-function lockInv(toggle)
- FreezeEntityPosition(PlayerPedId(), toggle)
- LocalPlayer.state:set("inv_busy", toggle, true)
- TriggerEvent('inventory:client:busy:status', toggle)
- TriggerEvent('canUseInventoryAndHotbar:toggle', not toggle)
-end
-
-function createTempCam(ent, coords)
- local cam = nil
- if Config.Crafting.craftCam then
- if Config.System.Debug then
- triggerNotify(nil, "ModCam Created", "success")
- end
- local camCoords = nil
- if type(ent) ~= "vector3" then
- camCoords = GetOffsetFromEntityInWorldCoords(ent, 1.2, -0.3, 0.8)
- else
- camCoords = ent
- end
- --local pedCoords = GetEntityCoords(PlayerPedId())
- cam = CreateCamWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z+0.5, 1.0, 0.0, 0.0, 60.00, false, 0)
- PointCamAtCoord(cam, coords)
- end
- return cam
-end
-
-function keyGen()
- local charset = {
- "q","w","e","r","t","y","u","i","o","p","a","s","d","f","g","h","j","k","l","z","x","c","v","b","n","m",
- "Q","W","E","R","T","Y","U","I","O","P","A","S","D","F","G","H","J","K","L","Z","X","C","V","B","N","M",
- "1","2","3","4","5","6","7","8","9","0"
- }
- local GeneratedID = ""
- for i = 1, 3 do GeneratedID = GeneratedID..charset[math.random(1, #charset)] end
- return GeneratedID
-end
-
-function startTempCam(cam)
- if Config.Crafting.craftCam then
- SetCamActive(cam, true)
- RenderScriptCams(true, true, 1000, true, true)
- end
-end
-function stopTempCam()
- if Config.Crafting.craftCam then
- CreateThread(function()
- Wait(1000)
- RenderScriptCams(false, true, 500, true, true)
- DestroyAllCams()
- end)
- end
-end
-
-local inProgress = false
-function progressBar(data)
- local result = nil
- if data.cam then
- startTempCam(data.cam)
- end
- if Config.System.ProgressBar == "ox" then
- if exports[OXLibExport]:progressBar({
- duration = Config.System.Debug and 1000 or data.time,
- label = data.label,
- useWhileDead = data.dead and 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
- lockInv(false)
- if data.cam then stopTempCam(data.cam) end
- else
- result = false
- lockInv(false)
- if data.cam then stopTempCam(data.cam) end
- end
-
- elseif Config.System.ProgressBar == "qb" then
- Core.Functions.Progressbar("mechbar",
- data.label,
- Config.System.Debug and 1000 or data.time,
- data.dead and 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 and data.flag or 32, task = data.task }, {}, {},
- function()
- result = true
- lockInv(false)
- if data.cam then
- stopTempCam(data.cam)
- end
- end, function()
- result = false
- lockInv(false)
- if data.cam then
- stopTempCam(data.cam)
- end
- end, data.icon)
-
- elseif Config.System.ProgressBar == "esx" then
- ESX.Progressbar(data.label, Config.System.Debug and 1000 or data.time, {
- FreezePlayer = true,
- animation ={
- type =data.anim,
- dict = data.dict,
- scenario = data.task,
- },
- onFinish = function()
- result = true
- FreezeEntityPosition(PlayerPedId(), false)
- lockInv(false)
- if data.cam then
- stopTempCam(data.cam)
- end
- end, onCancel = function()
- result = false
- FreezeEntityPosition(PlayerPedId(), false)
- lockInv(false)
- if data.cam then
- stopTempCam(data.cam)
- end
- end
- })
-
- elseif Config.System.ProgressBar == "gta" then
- local wait, inProgress = (Config.System.Debug and 1000 or data.time), true
- --local wait = data.time
- inProgress = true
- if not (data.dead and data.dead or false) then
- lockInv(true)
- displaySpinner(data.label)
- local ped = PlayerPedId()
- if data.dict then
- playAnim(data.dict, data.anim, -1, (data.flag == 8 and 32 or data.flag) or nil)
- end
- if data.task then
- TaskStartScenarioInPlace(ped, data.task, -1, true)
- end
- while inProgress and wait > 0 do wait -= 15
- local waitTimer = 0
- DisablePlayerFiring(ped, true)
- DisableControlAction(0, 25, true)
- DisableControlAction(0, 21, true)
- DisableControlAction(0, 30, true)
- DisableControlAction(0, 31, true)
- DisableControlAction(0, 36, true)
- if data.cam ~= nil then
- DisableControlAction(0, 1, true)
- DisableControlAction(0, 2, true)
- DisableControlAction(0, 106, true)
- end
- if data.cancel then
- if IsControlJustReleased(0, 202) or IsControlJustReleased(0, 77) then
- inProgress = false
- waitTimer = 1500
- displaySpinner(Loc[Config.Lan].error["cancel"])
- end
- end
- Wait(waitTimer)
- end
- inProgress = false
- if data.cam then stopTempCam(data.cam) end
- if data.dict then stopAnim(data.dict, data.anim, ped) end
- ClearPedTasks(ped)
- end
- lockInv(false)
- stopSpinner()
- result = (wait <= 0)
- end
- while result == nil do Wait(10) end
- return result
-end
-
-function displaySpinner(text)
- BeginTextCommandBusyspinnerOn('STRING')
- AddTextComponentSubstringPlayerName(text)
- EndTextCommandBusyspinnerOn(4)
-end
-
-function stopSpinner() if not IsDuplicityVersion() then BusyspinnerOff() end end
-
-function stopPropgressBar()
- 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
- BusyspinnerOff()
- end
-end
-
--- [[ OTHER ]] --
-function washHands(data)
- lookEnt(data.coords)
- local cam = createTempCam(PlayerPedId(), data.coords)
- if progressBar({
- label = Loc[Config.Lan].progressbar["progress_washing"],
- time = 5000,
- cancel = true,
- dict = "mp_arresting",
- anim = "a_uncuff",
- flag = 32,
- icon = "fas fa-hand-holding-droplet",
- cam = cam
- }) then
- triggerNotify(nil, Loc[Config.Lan].success["washed_hands"], "success")
- else
- triggerNotify(nil, Loc[Config.Lan].error["cancel"], 'error')
- end
- ClearPedTasks(PlayerPedId())
-end
-
-function useToilet(data)
- if data.urinal then
- if progressBar({
- label = "Using Urinal",
- time = 5000,
- cancel = true,
- dict = "misscarsteal2peeing",
- anim = "peeing_loop",
- flag = 32
- }) then
- TriggerServerEvent(GetCurrentResourceName().."server:Urinal")else
- lockInv(false)
- triggerNotify(nil, Loc[Config.Lan].error["cancelled"], 'error')
- end
- else
- TaskStartScenarioAtPosition(PlayerPedId(), "PROP_HUMAN_SEAT_CHAIR_MP_PLAYER", data.sitcoords.x, data.sitcoords.y, data.sitcoords.z, data.sitcoords[4], 0, 1, true)
- if progressBar({
- label = "Using Toilet",
- time = 10000,
- cancel = true
- }) then
- TriggerServerEvent(GetCurrentResourceName().."server:Urinal")
- ClearPedTasks(PlayerPedId())
- else
- lockInv(false)
- triggerNotify(nil, Loc[Config.Lan].error["cancelled"], 'error')
- end
- end
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":server:Urinal", function()
- local src = source
- local Player = getPlayer(src)
- local thirstamt = math.random(10,30)
- local thirst = Player.thirst - thirstamt
- setThirst(src, getPlayer(src).thirst - thirst)
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":server:setNeed", function(type, amount) local src = source
- if type == "thirst" then
- setThirst(src, amount)
- elseif type == "hunger" then
- setHunger(src, amount)
- end
-end)
-
-function setThirst(src, thirst)
- if GetResourceState(ESXExport):find("start") then
- TriggerClientEvent('esx_status:add', src, 'thirst', thirst)
- elseif GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then
- local Player = Core.Functions.GetPlayer(src)
- Player.Functions.SetMetaData('thirst', thirst)
- TriggerClientEvent("hud:client:UpdateNeeds", src, thirst, Player.PlayerData.metadata.thirst)
- end
-end
-
-function setHunger(src, hunger)
- if GetResourceState(ESXExport):find("start") then
- TriggerClientEvent('esx_status:add', src, 'hunger', hunger)
- elseif GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then
- local Player = Core.Functions.GetPlayer(src)
- Player.Functions.SetMetaData('hunger', hunger)
- TriggerClientEvent("hud:client:UpdateNeeds", src, hunger, Player.PlayerData.metadata.hunger)
- end
-end
-
-function useDoor(data)
- DoScreenFadeOut(500)
- while not IsScreenFadedOut() do Wait(10) end
- SetEntityCoords(PlayerPedId(), data.telecoords.xyz, 0, 0, 0, false)
- SetEntityHeading(PlayerPedId(), data.telecoords.w)
- DoScreenFadeIn(1000)
- Wait(100)
-end
-
--- [[CONSUME]] --
-function ConsumeSuccess(itemName, type)
- ExecuteCommand("e c")
- removeItem(itemName, 1)
- if GetResourceState(ESXExport):find("start") then
- if Items[itemName].hunger then
- TriggerServerEvent(GetCurrentResourceName()..":server:setNeed", "hunger", Items[itemName].hunger * 10000)
- end
- if Items[itemName].thirst then
- TriggerServerEvent(GetCurrentResourceName()..":server:setNeed", "thirst", Items[itemName].thirst * 10000)
- end
- else
- if Items[itemName].hunger then
- TriggerServerEvent(GetCurrentResourceName()..":server:setNeed", "hunger", Core.Functions.GetPlayerData().metadata["hunger"] + Items[itemName].hunger)
- end
- if Items[itemName].thirst then
- TriggerServerEvent(GetCurrentResourceName()..":server:setNeed", "thirst", Core.Functions.GetPlayerData().metadata["thirst"] + Items[itemName].thirst)
- end
- end
- if type == "alcohol" then alcoholCount += 1
- if alcoholCount > 1 and alcoholCount < 4 then
- TriggerEvent("evidence:client:SetStatus", "alcohol", 200)
- elseif alcoholCount >= 4 then
- TriggerEvent("evidence:client:SetStatus", "heavyalcohol", 200)
- AlienEffect()
- end
- end
- getRandomReward(itemName) -- check if a reward item should be given
-end
-
-function addItem(item, amount, info)
- TriggerServerEvent(GetCurrentResourceName()..":server:toggleItem", true, item, amount, nil, info)
-end
-
-function removeItem(item, amount)
- TriggerServerEvent(GetCurrentResourceName()..":server:toggleItem", false, item, amount, nil, info)
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":server:toggleItem", function(give, item, amount, newsrc)
- local src = newsrc or source
- local addremove = (tostring(give) == "true" and "addItem" or "removeItem")
- if Config.System.Debug then
- print("^6Bridge^7: ^3toggleItem ^2triggered^7: ^6"..addremove.."^7 - '"..tostring(item).."' x"..(tostring(amount) or "1"))
- end
- local remamount = (amount and amount or 1)
- if item == nil then return end
- if give == 0 or give == false then
- if hasItem(item, amount and amount or 1, src) then -- check if you still have the item
- if GetResourceState(OXInv):find("start") then
- local success = exports[OXInv]:RemoveItem(src, item, (amount and amount or 1), nil)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..OXInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(QSInv):find("start") then
- local success = exports[QSInv]:RemoveItem(src, item, amount)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..QSInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(CoreInv):find("start") then
- if GetResourceState(QBExport):find("start") then
- Core.Functions.GetPlayer(src).Functions.RemoveItem(item, amount, nil)
- elseif GetResourceState(ESXExport):find("start") then
- ESX.GetPlayerFromId(src).removeInventoryItem(item, count)
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..CoreInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(OrigenInv):find("start") then
- local success = exports[OrigenInv]:RemoveItem(src, item, amount)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..OrigenInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(CodeMInv):find("start") then
- local success = exports[CodeMInv]:RemoveItem(src, item, amount)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..CodeMInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(QBInv):find("start") then
- while remamount > 0 do
- if Core.Functions.GetPlayer(src).Functions.RemoveItem(item, 1) then end
- remamount -= 1
- end
- if Config.Crafting.showItemBox then
- TriggerClientEvent('inventory:client:ItemBox', src, Items[item], "remove", (amount and amount or 1))
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..QBInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
- else
- print("^4ERROR^7: ^2No Inventory detected ^7- ^2Check ^3exports^1.^2lua^7")
- end
- else
- dupeWarn(src, item, amount) -- if not boot the player
- end
- else
- local amount = amount and amount or 1
- if GetResourceState(OXInv):find("start") then
- local success = exports[OXInv]:AddItem(src, item, amount or 1, nil)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..OXInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(QSInv):find("start") then
- local success = exports[QSInv]:AddItem(src, item, amount)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..QSInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(CoreInv):find("start") then
- if GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then
- Core.Functions.GetPlayer(src).Functions.AddItem(item, amount, nil, nil)
- elseif GetResourceState(ESXExport):find("start") then
- ESX.GetPlayerFromId(src).addInventoryItem(item, amount)
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..CoreInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(CodeMInv):find("start") then
- local success = exports[CodeMInv]:AddItem(src, item, amount)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..CodeMInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(OrigenInv):find("start") then
- local success = exports[OrigenInv]:AddItem(src, item, amount)
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..OrigenInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- elseif GetResourceState(QBInv):find("start") then
- if Core.Functions.GetPlayer(src).Functions.AddItem(item, amount or 1) then
- --if Config.Crafting.showItemBox then
- TriggerClientEvent("inventory:client:ItemBox", src, Items[item], "add", amount and amount or 1)
- --end
- end
- if Config.System.Debug then
- print("^6Bridge^7: ^3"..addremove.."^7[^6"..QBInv.."^7] ^2Player^7("..src..") ^6"..Items[item].label.."^7("..item..") x^5"..(amount and amount or "1").."^7")
- end
-
- else
- print("^4ERROR^7: ^2No Inventory detected ^7- ^2Check ^3exports^1.^2lua^7")
- end
- end
-end)
-
---Item Exploit protection
-function dupeWarn(src, item, amount)
- print("^5DupeWarn^7: (^1"..tostring(src).."^7) ^2Tried to remove item ^7('^3"..item.."^7')^2 but it wasn't there^7")
- if Config.System.Debug == false then
- DropPlayer(src, src.." ^1Kicked for suspected duplicating items:"..item)
- end
- print("^5DupeWarn:^7: (^1"..tostring(src).."^7) ^2Dropped from server - exploit protection detected an item not being found in players inventory^7")
-end
-
-function breakTool(data) --wip
- local durability, slot = getDurability(item)
- durability -= data.damage
- if not durability then durability = 100 end
- if durability <= 0 then
- removeItem(data.item, 1)
- local breakId = GetSoundId()
- PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
- else
- TriggerServerEvent(GetCurrentResourceName()..":server:setMetaData", { item = data.item, slot = slot, metadata = { durability = durability }})
- end
-end
-
-function getDurability(item)
- local lowestSlot = 100 -- anything above your players max slots
- local durability = nil
- if GetResourceState(QBInv):find("start") then
- local itemcheck = Core.Functions.GetPlayerData().items
- for k, v in pairs(itemcheck) do
- if v.name == item then
- if v.slot <= lowestSlot then
- lowestSlot = v.slot
- durability = itemcheck[k].info.durability
- end
- end
- end
- end
-
- if GetResourceState(OXInv):find("start") then
- local itemcheck = exports[OXInv]:Search('slots', item)
- for k, v in pairs(itemcheck) do
- if v.slot <= lowestSlot then
- lowestSlot = v.slot
- durability = itemcheck[k].metadata.durability
- end
- end
- end
-
- if GetResourceState(QSInv):find("start") then
- local itemcheck = exports[QSInv]:getUserInventory()
- for k, v in pairs(itemcheck) do
- if v.name == item and v.slot <= lowestSlot then
- lowestSlot = v.slot
- durability = itemcheck[k].metadata.durability
- end
- end
- end
-
- if GetResourceState(OrigenInv):find("start") then
- local itemcheck = exports[OrigenInv]:getPlayerInventory()
- for k, v in pairs(itemcheck) do
- if v.name == item and v.slot <= lowestSlot then
- lowestSlot = v.slot
- durability = itemcheck[k].metadata.durability
- end
- end
- end
- return durability, lowestSlot
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":server:setMetaData", function(data)
- local src = source
- if GetResourceState(QBInv):find("start") then
- local Player = Core.Functions.GetPlayer(src)
- Player.PlayerData.items[data.slot].info = data.metadata
- Player.PlayerData.items[data.slot].description = "HP : "..data.metadata.durability
- Player.Functions.SetInventory(Player.PlayerData.items)
- end
-
- if GetResourceState(OXInv):find("start") then
- exports[OXInv]:SetMetadata(source, data.slot, data.metadata)
- end
-
- if GetResourceState(QSInv):find("start") then
- exports[QSInv]:SetItemMetadata(source, data.slot, data.metadata)
- end
-
- if GetResourceState(OrigenInv):find("start") then
- local item = exports[OrigenInv]:GetItemBySlot(source, data.slot)
- if item then
- exports[OrigenInv]:SetItemData(source, item.name, "durability", data.metadata.durability)
- end
- end
-end)
-
-function toggleDuty()
- if GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then
- TriggerServerEvent("QBCore:ToggleDuty")
- else
- onDuty = not onDuty
- if onDuty then
- triggerNotify(nil, "Now on duty", "success")
- else
- triggerNotify(nil, "Now off duty", "success")
- end
- end
-end
-
-local function CheckVersion()
- if IsDuplicityVersion() then
- local currentVersion = "^3"..GetResourceMetadata(GetCurrentResourceName(), 'version'):gsub("%.", "^7.^3").."^7"
- PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..GetCurrentResourceName()..'.txt', function(err, newestVersion, headers)
- if not newestVersion then
- PerformHttpRequest('https://raw.githubusercontent.com/jimathy/'..GetCurrentResourceName()..'/master/version.txt', function(err, freeVersion, headers)
- if not freeVersion then print("^1Currently unable to run a version check for ^7'^3"..GetCurrentResourceName().."^7' ("..currentVersion.."^7)") return end
- local currentVersion = "^3"..GetResourceMetadata(GetCurrentResourceName(), 'version'):gsub("%.", "^7.^3").."^7"
- freeVersion = "^3"..freeVersion:sub(1, -2):gsub("%.", "^7.^3"):gsub("%\r", "").."^7"
- print("^6Version Check^7: ^2Running^7: "..currentVersion.." ^2Latest^7: "..freeVersion)
- print(freeVersion == currentVersion and "^7'^3"..GetCurrentResourceName().."^7' - ^6You are running the latest version.^7 ("..currentVersion..")" or "^7'^3"..GetCurrentResourceName().."^7' - ^1You are currently running an outdated version^7, ^1please update^7!")
- end)
- else
- newestVersion = "^3"..newestVersion:sub(1, -2):gsub("%.", "^7.^3"):gsub("%\r", "").."^7"
- print("^6Version Check^7: ^2Running^7: "..currentVersion.." ^2Latest^7: "..newestVersion)
- print(newestVersion == currentVersion and '^6You are running the latest version.^7 ('..currentVersion..')' or "^1You are currently running an outdated version^7, ^1please update^7!")
- end
- end)
- end
-end
-CheckVersion()
-
---Screen Effects
-local alienEffect = false
-function AlienEffect()
- if alienEffect then return else alienEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3AlienEffect^7() ^2activated") end
- AnimpostfxPlay("DrugsMichaelAliensFightIn", 3.0, 0)
- Wait(math.random(5000, 8000))
- local Ped = PlayerPedId()
- local animDict = "MOVE_M@DRUNK@VERYDRUNK"
- loadAnimDict(animDict)
- SetPedCanRagdoll(Ped, true)
- ShakeGameplayCam('DRUNK_SHAKE', 2.80)
- SetTimecycleModifier("Drunk")
- SetPedMovementClipset(Ped, animDict, 1)
- SetPedMotionBlur(Ped, true)
- SetPedIsDrunk(Ped, true)
- Wait(1500)
- SetPedToRagdoll(Ped, 5000, 1000, 1, 0, 0, 0)
- Wait(13500)
- SetPedToRagdoll(Ped, 5000, 1000, 1, 0, 0, 0)
- Wait(120500)
- ClearTimecycleModifier()
- ResetScenarioTypesEnabled()
- ResetPedMovementClipset(Ped, 0)
- SetPedIsDrunk(Ped, false)
- SetPedMotionBlur(Ped, false)
- AnimpostfxStopAll()
- ShakeGameplayCam('DRUNK_SHAKE', 0.0)
- AnimpostfxPlay("DrugsMichaelAliensFight", 3.0, 0)
- Wait(math.random(45000, 60000))
- AnimpostfxPlay("DrugsMichaelAliensFightOut", 3.0, 0)
- AnimpostfxStop("DrugsMichaelAliensFightIn")
- AnimpostfxStop("DrugsMichaelAliensFight")
- AnimpostfxStop("DrugsMichaelAliensFightOut")
- alienEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3AlienEffect^7() ^2stopped") end
-end
-local weedEffect = false
-function WeedEffect()
- if weedEffect then return else weedEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3WeedEffect^7() ^2activated") end
- AnimpostfxPlay("DrugsMichaelAliensFightIn", 3.0, 0)
- Wait(math.random(3000, 20000))
- AnimpostfxPlay("DrugsMichaelAliensFight", 3.0, 0)
- Wait(math.random(15000, 20000))
- AnimpostfxPlay("DrugsMichaelAliensFightOut", 3.0, 0)
- AnimpostfxStop("DrugsMichaelAliensFightIn")
- AnimpostfxStop("DrugsMichaelAliensFight")
- AnimpostfxStop("DrugsMichaelAliensFightOut")
- weedEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3WeedEffect^7() ^2stopped") end
-end
-local trevorEffect = false
-function TrevorEffect()
- if trevorEffect then return else trevorEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3TrevorEffect^7() ^2activated") end
- AnimpostfxPlay("DrugsTrevorClownsFightIn", 3.0, 0)
- Wait(3000)
- AnimpostfxPlay("DrugsTrevorClownsFight", 3.0, 0)
- Wait(30000)
- AnimpostfxPlay("DrugsTrevorClownsFightOut", 3.0, 0)
- AnimpostfxStop("DrugsTrevorClownsFight")
- AnimpostfxStop("DrugsTrevorClownsFightIn")
- AnimpostfxStop("DrugsTrevorClownsFightOut")
- trevorEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3TrevorEffect^7() ^2stopped") end
-end
-local turboEffect = false
-function TurboEffect()
- if turboEffect then return else turboEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3TurboEffect^7() ^2activated") end
- AnimpostfxPlay('RaceTurbo', 0, true)
- SetTimecycleModifier('rply_motionblur')
- ShakeGameplayCam('SKY_DIVING_SHAKE', 0.25)
- Wait(30000)
- StopGameplayCamShaking(true)
- SetTransitionTimecycleModifier('default', 0.35)
- Wait(1000)
- ClearTimecycleModifier()
- AnimpostfxStop('RaceTurbo')
- turboEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3TurboEffect^7() ^2stopped") end
-end
-local rampageEffect = false
-function RampageEffect()
- if rampageEffect then return else rampageEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3RampageEffect^7() ^2activated") end
- AnimpostfxPlay('Rampage', 0, true)
- SetTimecycleModifier('rply_motionblur')
- ShakeGameplayCam('SKY_DIVING_SHAKE', 0.25)
- Wait(30000)
- StopGameplayCamShaking(true)
- SetTransitionTimecycleModifier('default', 0.35)
- Wait(1000)
- ClearTimecycleModifier()
- AnimpostfxStop('Rampage')
- rampageEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3RampageEffect^7() ^2stopped") end
-end
-local focusEffect = false
-function FocusEffect()
- if focusEffect then return else focusEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3FocusEffect^7() ^2activated") end
- Wait(1000)
- AnimpostfxPlay('FocusIn', 0, true)
- Wait(30000)
- AnimpostfxStop('FocusIn')
- focusEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3FocusEffect^7() ^2stopped") end
-end
-local nightVisionEffect = false
-function NightVisionEffect()
- if NightVisionEffect then return else nightVisionEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3NightVisionEffect^7() ^2activated") end
- SetNightvision(true)
- Wait(math.random(3000, 4000)) -- FEEL FREE TO CHANGE THIS
- SetNightvision(false)
- SetSeethrough(false)
- nightVisionEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3NightVisionEffect^7() ^2stopped") end
-end
-local thermalEffect = false
-function ThermalEffect()
- if thermalEffect then return else thermalEffect = true end
- if Config.System.Debug then print("^5Debug^7: ^3ThermalEffect^7() ^2activated") end
- SetNightvision(true)
- SetSeethrough(true)
- Wait(math.random(2000, 3000)) -- FEEL FREE TO CHANGE THIS
- SetNightvision(false)
- SetSeethrough(false)
- thermalEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3ThermalEffect^7() ^2stopped") end
-end
-
---Built-in Buff effects
-local healEffect = false
-function HealEffect(data)
- if healEffect then return end
- if Config.System.Debug then print("^5Debug^7: ^3HealEffect^7() ^2activated") end
- healEffect = true
- local count = (data[1] / 1000)
- while count > 0 do
- Wait(1000)
- count -= 1
- SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) + data[2])
- end
- healEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3HealEffect^7() ^2stopped") end
-end
-
-local staminaEffect = false
-function StaminaEffect(data)
- if staminaEffect then return end
- if Config.System.Debug then print("^5Debug^7: ^3StaminaEffect^7() ^2activated") end
- staminaEffect = true
- local startStamina = (data[1] / 1000)
- SetRunSprintMultiplierForPlayer(PlayerId(), 1.49)
- while startStamina > 0 do
- Wait(1000)
- if math.random(5, 100) < 10 then RestorePlayerStamina(PlayerId(), data[2]) end
- startStamina -= 1
- if math.random(5, 100) < 51 then end
- end
- startStamina = 0
- SetRunSprintMultiplierForPlayer(PlayerId(), 1.0)
- staminaEffect = false
- if Config.System.Debug then print("^5Debug^7: ^3StaminaEffect^7() ^2stopped") end
-end
-
-function StopEffects() -- Used to clear up any effects stuck on screen
- if Config.System.Debug then print("^5Debug^7: ^2All screen effects stopped") end
- ShakeGameplayCam('DRUNK_SHAKE', 0.0)
- SetPedToRagdoll(PlayerPedId(), 5000, 1000, 1, 0, 0, 0)
- ClearTimecycleModifier()
- ResetScenarioTypesEnabled()
- ResetPedMovementClipset(PlayerPedId(), 0)
- SetPedIsDrunk(PlayerPedId(), false)
- SetPedMotionBlur(PlayerPedId(), false)
- SetNightvision(false)
- SetSeethrough(false)
- AnimpostfxStop("DrugsMichaelAliensFightIn")
- AnimpostfxStop("DrugsMichaelAliensFight")
- AnimpostfxStop("DrugsMichaelAliensFightOut")
- AnimpostfxStop("DrugsTrevorClownsFight")
- AnimpostfxStop("DrugsTrevorClownsFightIn")
- AnimpostfxStop("DrugsTrevorClownsFightOut")
- AnimpostfxStop('RaceTurbo')
- AnimpostfxStop('FocusIn')
- AnimpostfxStop('Rampage')
-end
-
-AddEventHandler('onResourceStop', function(r)
- if r ~= GetCurrentResourceName() then return end
- stopSpinner()
- for i = 1, #Peds do DeletePed(Peds[i]) end
- for i = 1, #Props do destroyProp(Props[i]) end
-end)
\ No newline at end of file
diff --git a/fxmanifest.lua b/fxmanifest.lua
index db3dbad..11e9e1a 100644
--- a/fxmanifest.lua
+++ b/fxmanifest.lua
@@ -1,14 +1,24 @@
name "Jim_Bridge"
author "Jimathy"
-version "1.0.14"
+version "2.0"
description "Framework Bridge By Jimathy"
fx_version "cerulean"
-game "gta5"
+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 {
- 'exports.lua',
- 'functions.lua',
- 'wrapper.lua',
- 'crafting.lua',
+ 'starter.lua',
+ 'shared/*.lua',
+ 'shared/make/*.lua',
+ 'shared/scaleforms/*.lua',
}
+
+-- Version checker
+server_scripts { '_versioncheck.lua' }
+
+-- NUI Menu Loading
+client_scripts { 'nui/*.lua' }
+ui_page 'nui/index.html'
+files { 'nui/index.html', 'nui/script.js', 'nui/style.css' }
\ No newline at end of file
diff --git a/nui/index.html b/nui/index.html
new file mode 100644
index 0000000..f4da756
--- /dev/null
+++ b/nui/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+ QB Menu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nui/main.lua b/nui/main.lua
new file mode 100644
index 0000000..c62087d
--- /dev/null
+++ b/nui/main.lua
@@ -0,0 +1,138 @@
+Config = Config or { System = {} }
+
+CreateThread(function()
+ local fileLoader = assert(load(LoadResourceFile('jim_bridge', ('starter.lua')), ('@@jim_bridge/starter.lua')))
+ fileLoader()
+end)
+
+local headerShown = false
+local sendData = nil
+local sendModifiers = nil
+
+
+--Colours for progressbar
+local colours = {
+ ["dark.0"] = "#C1C2C5", ["dark.1"] = "#A6A7AB", ["dark.2"] = "#909296", ["dark.3"] = "#5C5F66", ["dark.4"] = "#373A40", ["dark.5"] = "#2C2E33", ["dark.6"] = "#25262B", ["dark.7"] = "#1A1B1E", ["dark.8"] = "#141517", ["dark.9"] = "#101113",
+ ["gray.0"] = "#F8F9FA", ["gray.1"] = "#F1F3F5", ["gray.2"] = "#E9ECEF", ["gray.3"] = "#DEE2E6", ["gray.4"] = "#CED4DA", ["gray.5"] = "#ADB5BD", ["gray.6"] = "#868E96", ["gray.7"] = "#495057", ["gray.8"] = "#343A40", ["gray.9"] = "#212529",
+ ["red.0"] = "#FFF5F5", ["red.1"] = "#FFE3E3", ["red.2"] = "#FFC9C9", ["red.3"] = "#FFA8A8", ["red.4"] = "#FF8787", ["red.5"] = "#FF6B6B", ["red.6"] = "#FA5252", ["red.7"] = "#F03E3E", ["red.8"] = "#E03131", ["red.9"] = "#C92A2A",
+ ["pink.0"] = "#FFF0F6", ["pink.1"] = "#FFDEEB", ["pink.2"] = "#FCC2D7", ["pink.3"] = "#FAA2C1", ["pink.4"] = "#F783AC", ["pink.5"] = "#F06595", ["pink.6"] = "#E64980", ["pink.7"] = "#D6336C", ["pink.8"] = "#C2255C", ["pink.9"] = "#A61E4D",
+ ["grape.0"] = "#F8F0FC", ["grape.1"] = "#F3D9FA", ["grape.2"] = "#EEBEFA", ["grape.3"] = "#E599F7", ["grape.4"] = "#DA77F2", ["grape.5"] = "#CC5DE8", ["grape.6"] = "#BE4BDB", ["grape.7"] = "#AE3EC9", ["grape.8"] = "#9C36B5", ["grape.9"] = "#862E9C",
+ ["violet.0"] = "#F3F0FF", ["violet.1"] = "#E5DBFF", ["violet.2"] = "#D0BFFF", ["violet.3"] = "#B197FC", ["violet.4"] = "#9775FA", ["violet.5"] = "#845EF7", ["violet.6"] = "#7950F2", ["violet.7"] = "#7048E8", ["violet.8"] = "#6741D9", ["violet.9"] = "#5F3DC4",
+ ["indigo.0"] = "#EDF2FF", ["indigo.1"] = "#DBE4FF", ["indigo.2"] = "#BAC8FF", ["indigo.3"] = "#91A7FF", ["indigo.4"] = "#748FFC", ["indigo.5"] = "#5C7CFA", ["indigo.6"] = "#4C6EF5", ["indigo.7"] = "#4263EB", ["indigo.8"] = "#3B5BDB", ["indigo.9"] = "#364FC7",
+ ["blue.0"] = "#E7F5FF", ["blue.1"] = "#D0EBFF", ["blue.2"] = "#A5D8FF", ["blue.3"] = "#74C0FC", ["blue.4"] = "#4DABF7", ["blue.5"] = "#339AF0", ["blue.6"] = "#228BE6", ["blue.7"] = "#1C7ED6", ["blue.8"] = "#1971C2", ["blue.9"] = "#1864AB",
+ ["cyan.0"] = "#E3FAFC", ["cyan.1"] = "#C5F6FA", ["cyan.2"] = "#99E9F2", ["cyan.3"] = "#66D9E8", ["cyan.4"] = "#3BC9DB", ["cyan.5"] = "#22B8CF", ["cyan.6"] = "#15AABF", ["cyan.7"] = "#1098AD", ["cyan.8"] = "#0C8599", ["cyan.9"] = "#0B7285",
+ ["teal.0"] = "#E6FCF5", ["teal.1"] = "#C3FAE8", ["teal.2"] = "#96F2D7", ["teal.3"] = "#63E6BE", ["teal.4"] = "#38D9A9", ["teal.5"] = "#20C997", ["teal.6"] = "#12B886", ["teal.7"] = "#0CA678", ["teal.8"] = "#099268", ["teal.9"] = "#087F5B",
+ ["green.0"] = "#EBFBEE", ["green.1"] = "#D3F9D8", ["green.2"] = "#B2F2BB", ["green.3"] = "#8CE99A", ["green.4"] = "#69DB7C", ["green.5"] = "#51CF66", ["green.6"] = "#40C057", ["green.7"] = "#37B24D", ["green.8"] = "#2F9E44", ["green.9"] = "#2B8A3E",
+ ["lime.0"] = "#F4FCE3", ["lime.1"] = "#E9FAC8", ["lime.2"] = "#D8F5A2", ["lime.3"] = "#C0EB75", ["lime.4"] = "#A9E34B", ["lime.5"] = "#94D82D", ["lime.6"] = "#82C91E", ["lime.7"] = "#74B816", ["lime.8"] = "#66A80F", ["lime.9"] = "#5C940D",
+ ["yellow.0"] = "#FFF9DB", ["yellow.1"] = "#FFF3BF", ["yellow.2"] = "#FFEC99", ["yellow.3"] = "#FFE066", ["yellow.4"] = "#FFD43B", ["yellow.5"] = "#FCC419", ["yellow.6"] = "#FAB005", ["yellow.7"] = "#F59F00", ["yellow.8"] = "#F08C00", ["yellow.9"] = "#E67700",
+ ["orange.0"] = "#FFF4E6", ["orange.1"] = "#FFE8CC", ["orange.2"] = "#FFD8A8", ["orange.3"] = "#FFC078", ["orange.4"] = "#FFA94D", ["orange.5"] = "#FF922B", ["orange.6"] = "#FD7E14", ["orange.7"] = "#F76707",["orange.8"] = "#E8590C",["orange.9"] = "#D9480F"
+}
+
+-- Functions
+function openNuiMenu(data, modifiers)
+ if not data or not next(data) then return end
+ for _, v in pairs(data) do
+ v["icon"] = v["arrow"] and "fas fa-angle-right" or v["icon"] or nil
+ v["colorScheme"] = v["colourScheme"] and colours[v["colourScheme"]] or (v["colorScheme"] and colours[v["colorScheme"]] or colours["green.7"])
+ if v["onSelect"] then
+ v.params = { isAction = true, event = v["onSelect"] }
+ end
+ end
+ SetNuiFocus(true, true)
+ headerShown = false
+ sendData = data
+ sendModifiers = modifiers or {}
+ SendNUIMessage({ action = 'OPEN_MENU', data = table.clone(data) })
+end
+
+local function closeMenu()
+ sendData = nil
+ sendModifiers = nil
+ headerShown = false
+ SetNuiFocus(false, false)
+ SendNUIMessage({ action = 'CLOSE_MENU' })
+end
+
+local function showHeader(data)
+ if not data or not next(data) then return end
+ headerShown = true
+ sendData = data
+ SendNUIMessage({ action = 'SHOW_HEADER', data = table.clone(data) })
+end
+
+-- Events
+
+RegisterNetEvent("jim_bridge:client:openMenu", function(data) openNuiMenu(data) end)
+
+RegisterNetEvent("jim_bridge:client:closeMenu", function() closeMenu() end)
+
+-- NUI Callbacks
+
+RegisterNUICallback('clickedButton', function(option)
+ if headerShown then headerShown = false end
+ PlaySound(-1, "CLICK_BACK", "WEB_NAVIGATION_SOUNDS_PHONE", 0, 0, 1)
+ SetNuiFocus(false, false)
+ if sendData then
+ local data = sendData[tonumber(option)]
+ sendData = nil
+ if data then
+ if data.params and data.params.event then
+ if data.params.isServer then
+ TriggerServerEvent(data.params.event, data.params.args)
+ elseif data.params.isCommand then
+ ExecuteCommand(data.params.event)
+ elseif data.params.isQBCommand then
+ TriggerServerEvent('QBCore:CallCommand', data.params.event, data.params.args)
+ elseif data.params.isAction then
+ data.params.event(data.params.args)
+ else
+ TriggerEvent(data.params.event, data.params.args)
+ end
+ end
+ end
+ end
+end)
+
+RegisterNUICallback('closeMenu', function()
+ if sendModifiers and sendModifiers.onExit then sendModifiers.onExit() end -- when close menu is triggered (with esc or using a button to close it) trigger onExit function
+ headerShown = false
+ sendModifiers = nil
+ sendData = nil
+ SetNuiFocus(false, false)
+end)
+
+-- Command and Keymapping
+RegisterCommand('playerfocus', function() if headerShown then SetNuiFocus(true, true) end end)
+RegisterKeyMapping('playerFocus', 'Give Menu Focus', 'keyboard', 'LMENU')
+
+-- Exports
+exports('openMenu', function(data) openNuiMenu(data) end)
+exports('closeMenu', function() closeMenu() end)
+exports('showHeader', function(data) showHeader(data) end)
+
+
+-- Input Dialog
+-- function inputDialog(title, config)
+-- local p = promise.new()
+-- local cbId = math.random(111111, 999999)
+--
+-- RegisterNUICallback("inputResult", function(data, cb)
+-- if data.cbId == cbId then
+-- cb({})
+-- SetNuiFocus(false, false)
+-- p:resolve(data.result)
+-- end
+-- end)
+--
+-- SendNUIMessage({
+-- action = "SHOW_INPUT",
+-- title = title,
+-- data = config,
+-- cbId = cbId
+-- })
+--
+-- SetNuiFocus(true, true)
+-- return Citizen.Await(p)
+-- end
+--
+-- exports('inputDialog', inputDialog)
\ No newline at end of file
diff --git a/nui/script.js b/nui/script.js
new file mode 100644
index 0000000..b15110f
--- /dev/null
+++ b/nui/script.js
@@ -0,0 +1,342 @@
+let buttonParams = [];
+let menuItems = [];
+
+const openMenu = (data = null) => {
+ let html = "
";
+
+ // Add search as a title-like fixed header
+ html += `
+
+
+
+ `;
+
+ data.forEach((item, index) => {
+ if (!item.hidden) {
+ let header = item.header || item.title;
+ let message = item.txt || item.text || item.description;
+ let isMenuHeader = item.isMenuHeader;
+ let isDisabled = item.disabled;
+ let icon = item.icon;
+ let progress = item.progress || item.progressbar;
+ let colour = item.colorScheme;
+ html += getButtonRender(header, message, index, isMenuHeader, isDisabled, icon, progress, colour);
+ if (item.params) buttonParams[index] = item.params;
+ }
+ });
+
+ html += "
`
+ : ""}
+ `;
+};
+
+const closeMenu = () => {
+ $("#buttons").html(" ");
+ buttonParams = [];
+ $("#search-input").hide(); // hide search bar
+};
+
+const postData = (id) => {
+ $.post(`https://${GetParentResourceName()}/clickedButton`, JSON.stringify(parseInt(id) + 1));
+ return closeMenu();
+};
+
+const cancelMenu = () => {
+ $.post(`https://${GetParentResourceName()}/closeMenu`);
+ return closeMenu();
+};
+
+const filterButtons = (query) => {
+ const filteredItems = menuItems.filter(item => item.header.toLowerCase().includes(query.toLowerCase()));
+ openMenu(filteredItems);
+};
+
+$("#search-input").on('input', function() {
+ filterButtons($(this).val());
+});
+
+document.onkeyup = function (event) {
+ const charCode = event.key;
+ if (charCode == "Escape") {
+ cancelMenu();
+ }
+};
+
+let inputCallbackId = null;
+
+window.addEventListener("message", (event) => {
+ const data = event.data;
+ const buttons = data.data;
+ const action = data.action;
+ debugLog("Opening Input Popup", data.data); // Log the input config specifically
+
+ switch (action) {
+ case "OPEN_MENU":
+ case "SHOW_HEADER":
+ return openMenu(buttons);
+ case "CLOSE_MENU":
+ return closeMenu();
+ case "SHOW_INPUT":
+ inputCallbackId = data.cbId;
+ debugLog("Opening Input Popup", data.data); // Log the input config specifically
+ return openInputPopup(data.data);
+ default:
+ return;
+ }
+});
+
+document.onkeyup = function (event) {
+ const charCode = event.key;
+ if (charCode == "Escape") {
+ cancelMenu();
+ }
+};
+
+const openInputPopup = (config) => {
+ if (!config || !Array.isArray(config)) {
+ console.error("Invalid input config: missing or malformed 'fields'", config);
+ return;
+ }
+ const container = document.getElementById("input-popup");
+ const form = document.getElementById("input-form");
+ form.innerHTML = ""; // clear form
+
+ config.forEach(field => {
+ let input;
+
+ switch (field.type) {
+ case "text":
+ case "number":
+ input = document.createElement("input");
+ input.type = field.type;
+ break;
+ case "radio":
+ input = document.createElement("input");
+ input.type = "radio";
+ break;
+ case "select":
+ input = document.createElement("select");
+ field.options.forEach(opt => {
+ const option = document.createElement("option");
+ option.value = opt;
+ option.textContent = opt;
+ input.appendChild(option);
+ });
+ break;
+ case "slider":
+ input = document.createElement("input");
+ input.type = "range";
+ input.min = field.min;
+ input.max = field.max;
+ input.step = field.step || 1;
+ break;
+ case "color":
+ input = document.createElement("input");
+ input.type = "color";
+ break;
+ }
+
+ if (!input) return;
+
+ input.name = field.name;
+ input.className = "input-field";
+ input.placeholder = field.label || field.placeholder || "";
+ if (field.required) input.required = true;
+
+ const wrapper = document.createElement("div");
+ wrapper.className = "input-wrapper";
+ if (field.type === "radio") {
+ if (field.label) {
+ const titleLabel = document.createElement("label");
+ titleLabel.className = "input-label";
+ titleLabel.textContent = field.label;
+ wrapper.appendChild(titleLabel);
+ }
+
+ if (Array.isArray(field.options)) {
+ field.options.forEach(opt => {
+ const radioWrapper = document.createElement("label");
+ radioWrapper.className = "radio-wrapper";
+
+ const radio = document.createElement("input");
+ radio.type = "radio";
+ radio.name = field.name;
+ radio.value = opt.value;
+ radio.className = "radio-input";
+
+ // Set default checked radio
+ if (field.default === opt.value) {
+ radio.checked = true;
+ }
+
+ const label = document.createElement("span");
+ label.className = "radio-label";
+ label.textContent = opt.label || opt.value;
+
+ radioWrapper.appendChild(radio);
+ radioWrapper.appendChild(label);
+ wrapper.appendChild(radioWrapper);
+ });
+ }
+
+ form.appendChild(wrapper);
+ return;
+ }
+
+ if (field.type === "color") {
+ if (field.label) {
+ const titleLabel = document.createElement("label");
+ titleLabel.className = "input-label";
+ titleLabel.textContent = field.label;
+ wrapper.appendChild(titleLabel);
+ }
+ const colorPreview = document.createElement("span");
+ colorPreview.className = "color-preview";
+ colorPreview.textContent = input.value;
+
+ input.addEventListener("input", () => {
+ const hex = input.value;
+ const rgb = hexToRgb(hex);
+ colorPreview.textContent = `${hex.toUpperCase()} (${rgb})`;
+ });
+ wrapper.classList.add("color-picker");
+
+ wrapper.appendChild(input);
+ wrapper.appendChild(colorPreview);
+ form.appendChild(wrapper);
+ return;
+ }
+
+ if (field.type === "slider") {
+ if (field.label) {
+ const titleLabel = document.createElement("label");
+ titleLabel.className = "input-label";
+ titleLabel.textContent = field.label;
+ wrapper.appendChild(titleLabel);
+ }
+ input.value = field.default || field.min;
+ const sliderValue = document.createElement("div");
+ sliderValue.className = "slider-values";
+ sliderValue.innerHTML = `
+ ${field.min}
+ ${input.value}
+ ${field.max}
+ `;
+
+ input.addEventListener("input", () => {
+ sliderValue.querySelector(".slider-current").textContent = input.value;
+ });
+
+ wrapper.appendChild(sliderValue);
+ wrapper.appendChild(input);
+ form.appendChild(wrapper);
+ return;
+ }
+
+ // Common label
+ const label = document.createElement("label");
+ label.className = "input-label";
+ label.textContent = field.label || field.name || "Input";
+ wrapper.appendChild(label);
+
+ // Input
+ input.name = field.name;
+ input.className = "input-field";
+ if (field.required) input.required = true;
+ wrapper.appendChild(input);
+
+ form.appendChild(wrapper);
+
+ form.appendChild(wrapper);
+ });
+
+ form.onsubmit = function (e) {
+ e.preventDefault();
+ const data = {};
+ new FormData(form).forEach((val, key) => {
+ // checkboxes return "on" when checked
+ if (form[key].type === "checkbox") {
+ data[key] = form[key].checked;
+ } else if (form[key].type === "color") {
+ const hex = val;
+ const rgb = hexToRgb(hex);
+ data[key] = {
+ hex: hex.toUpperCase(),
+ rgb: rgb
+ };
+ } else {
+ data[key] = val;
+ }
+ });
+ returnInputData(data);
+ };
+
+ document.querySelector(".input-cancel").onclick = () => {
+ returnInputData(null);
+ };
+
+ container.classList.remove("hidden");
+};
+
+const closeInputPopup = () => {
+ document.getElementById("input-popup").classList.add("hidden");
+};
+
+const returnInputData = (result) => {
+ $.post(`https://${GetParentResourceName()}/inputResult`, JSON.stringify({
+ cbId: inputCallbackId,
+ result
+ }));
+ closeInputPopup();
+};
+
+const debugLog = (label, data) => {
+ //console.log(`^4[DEBUG] ${label}`);
+ //console.log(JSON.stringify(data, null, 2));
+};
+
+function hexToRgb(hex) {
+ const bigint = parseInt(hex.slice(1), 16);
+ const r = (bigint >> 16) & 255;
+ const g = (bigint >> 8) & 255;
+ const b = bigint & 255;
+ return `RGB(${r}, ${g}, ${b})`;
+}
\ No newline at end of file
diff --git a/nui/style.css b/nui/style.css
new file mode 100644
index 0000000..07824c3
--- /dev/null
+++ b/nui/style.css
@@ -0,0 +1,450 @@
+@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;500&display=swap");
+
+:root {
+ --font-family: "Poppins", sans-serif !important;
+ --width: 65%;
+
+
+ --text-colour: white;
+ --text-colour-hover: black;
+ --background-button: rgba(23, 23, 23, 85%);
+ --background-button-hover: rgba(200, 200, 200, 85%);
+ --background-title: rgba(23, 23, 23, 100%);
+
+ /*
+ --text-colour: black;
+ --text-colour-hover: white;
+ --background-button: rgba(200, 200, 200, 85%);
+ --background-button-hover: rgba(0, 0, 0, 85%);
+ --background-title: rgba(200, 200, 200);
+ */
+}
+
+* {
+ padding: 0;
+ margin: 0;
+ font-family: var(--font-family);
+ font-weight: 300;
+}
+
+#container {
+ position: absolute;
+ height: auto;
+ top: 15%;
+ right: 20%;
+ z-index: 2;
+}
+.button {
+ cursor: pointer;
+ display: flex;
+ flex-direction: row !important;
+ gap: 10px;
+}
+.title {
+ cursor: default;
+ gap: 10px;
+ display: flex;
+ flex-direction: row !important;
+}
+
+#buttons {
+ max-height: 80vh;
+ width: 250%;
+ overflow-x: none;
+ overflow-y: auto;
+ padding: 10px;
+}
+
+html, body { background: transparent !important; }
+
+#buttons::-webkit-scrollbar { display: none; }
+
+body::-webkit-scrollbar { display: none; }
+
+.button {
+ max-width: 56%;
+ height: 60%;
+ background-color: var(--background-button);
+ color: var(--text-colour);
+ margin: auto;
+ position: relative;
+ top: 10%;
+ overflow: hidden;
+ padding: 0.45rem;
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+ z-index: 1;
+ transition-property: color;
+ transition-duration: 0.1s, 0.2s;
+ transition-timing-function: linear, ease-in;
+}
+
+.button::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0%;
+ height: 100%;
+ background-color: var(--background-button-hover);
+ z-index: 0;
+ transition: width 0.2s ease;
+}
+
+.button:hover::before {
+ width: 100%;
+}
+
+.button > * {
+ position: relative;
+ z-index: 1;
+}
+
+.button:hover {
+ color: var(--text-colour-hover);
+}
+
+.icon > img {
+ width: 2.2vh !important;
+}
+
+.icon {
+ font-size: 1.2vh;
+ transition-property: all, filter;
+ transition-duration: 0.1s, 0.3s;
+ transition-timing-function: linear, ease-in;
+ display: inline-flex;
+ align-items: center;
+ position: static;
+ justify-content: left;
+ opacity: 0.5;
+}
+
+.button:hover > .icon {
+ opacity: 1.0;
+ animation: bounce 2s linear;
+ filter: drop-shadow(-1px -1px 15px white);
+}
+
+.bounce {
+ animation-name: bounce-4;
+ animation-timing-function: ease;
+}
+
+@keyframes bounce {
+ 0% { transform: scale(1,1); }
+ 10% { transform: scale(1.1); }
+ 30% { transform: scale(1.5); }
+ 50% { transform: scale(1.5); }
+ 100% { transform: scale(1.5); }
+}
+
+.title {
+ font-family: var(--font-family);
+ width: auto;
+ max-width: 56%;
+ height: 60%;
+ background: var(--background-title);
+ color: var(--text-colour);
+ margin: auto;
+ margin-left: -0.5;
+ position: relative;
+ top: 10%;
+ overflow: hidden;
+ padding: 0.45rem;
+ flex-direction: column;
+ border-left: 0px;
+}
+
+.title > div.header {
+ font-family: var(--font-family);
+ text-decoration: underline !important;
+}
+
+.disabled {
+ cursor: default;
+}
+
+div > .text {
+ font-family: var(--font-family);
+ flex-direction: column;
+ font-size: 1.0vh;
+ overflow: hidden;
+}
+div > .header {
+ font-family: var(--font-family);
+ width: 100%;
+ max-width: 100%;
+ display: flex;
+ align-items: center;
+ position: relative;
+ justify-content: left;
+ overflow: wrap;
+ font-size: 1.3vh;
+ font-weight: 400;
+ overflow: hidden;
+}
+
+/* Search input */
+
+.title.search-container {
+ background: var(--background-button);
+ color: var(--text-colour);
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ z-index: 3;
+}
+
+.title.search-container input[type="text"] {
+ color: var(--text-colour);
+ font-family: var(--font-family);
+ font-size: 1rem;
+ width: auto;
+ max-width: 56%;
+ border: none;
+ background-color: transparent;
+ transition: background-color 0.2s ease, color 0.2s ease;
+ outline: none;
+}
+
+
+.title.search-container input[type="text"]::placeholder {
+ color: var(--text-colour);
+ opacity: 0.5;
+}
+
+.progress-container {
+ z-index: 99999;
+ width: auto;
+ max-width: 56%;
+ background-color: var(--background-title);
+ height: 0.4vh;
+ padding-left: 0.45rem;
+ padding-right: 0.45rem;
+ margin: auto;
+}
+
+.progress-bar {
+ height: 90%;
+ max-width: auto;
+ transition: width 0.3s ease-in-out;
+}
+
+
+
+.hidden {
+ display: none !important;
+}
+
+#input-popup {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 999;
+}
+
+.input-box {
+ font-weight: 300;
+ background: var(--background-button);
+ color: var(--text-colour);
+ width: 25vw;
+ box-shadow: 0 0 15px var(--background-button-hover);
+}
+
+.input-header {
+ font-size: 1.4rem;
+ margin-bottom: 1rem;
+ margin-top: 0.5rem;
+ font-weight: 500;
+ text-align: center;
+}
+
+.input-actions {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 1rem;
+}
+
+.input-actions button {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 6px;
+ margin-bottom: 0.8rem;
+ margin-left: 0.6rem;
+ margin-right: 0.6rem;
+ cursor: pointer;
+ background: var(--background-button);
+ color: var(--text-colour);
+ transition: background 0.2s ease;
+}
+
+.input-actions button:hover {
+ color: var(--text-colour-hover);
+ background: var(--background-button-hover);
+}
+
+.input-wrapper {
+ position: relative;
+ width: 95%;
+ padding: 0.1rem;
+ margin-left: 0.6rem;
+ margin-bottom: 0.8rem;
+ background-color: var(--background-title);
+ overflow: hidden;
+}
+
+.input-wrapper::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0%;
+ height: 100%;
+ background-color: var(--background-button-hover);
+ z-index: 0;
+ transition: width 0.2s ease;
+}
+
+.input-wrapper:focus-within::before {
+ width: 100%;
+}
+
+.input-wrapper:focus-within .input-field {
+ color: var(--text-colour-hover) !important;
+}
+
+.input-wrapper:focus-within .input-label {
+ color: var(--text-colour-hover) !important;
+}
+
+.checkbox-wrapper:has(.checkbox-input:focus) .checkbox-label {
+ color: var(--text-colour-hover) !important;
+}
+
+.input-wrapper:focus-within .color-preview {
+ color: var(--text-colour-hover) !important;
+}
+
+.input-wrapper:has(input[type="range"]:focus) .slider-values {
+ color: var(--text-colour-hover) !important;
+}
+
+.input-wrapper:focus-within .slider-current {
+ color: var(--text-colour-hover) !important;
+}
+
+
+.input-field {
+ width: 100%;
+ position: relative;
+ z-index: 1;
+ border: transparent;
+ background: transparent;
+ color: var(--text-colour);
+ font-size: 1rem;
+ transition: color 0.2s ease;
+}
+
+.input-wrapper:hover .input-field {
+ color: var(--text-colour-hover);
+}
+
+.input-label {
+ display: block;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: var(--text-colour);
+ margin-bottom: 0.25rem;
+ z-index: 1;
+ position: relative;
+}
+
+.radio-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: var(--background-title);
+ border-radius: 4px;
+ z-index: 1;
+}
+
+.radio-input {
+ transform: scale(1.2);
+ accent-color: var(--background-button-hover);
+ z-index: 1;
+}
+
+.radio-label {
+ color: var(--text-colour);
+ font-size: 0.95rem;
+ z-index: 1;
+}
+
+/* Focus styling */
+.radio-wrapper:has(.radio-input:focus) .radio-label {
+ color: var(--text-colour-hover) !important;
+}
+.input-wrapper:focus-within .radio-label {
+ color: var(--text-colour-hover) !important;
+ }
+.input-wrapper.color-picker {
+ display: flex;
+ flex-direction: column; /* this is key */
+ align-items: flex-start;
+ justify-content: flex-start;
+ z-index: 1;
+}
+
+.input-field[type="color"] {
+ width: 100%; /* half width */
+ padding: 0.2rem;
+ cursor: pointer;
+ z-index: 1;
+}
+
+.color-preview {
+ display: inline-block;
+ margin-left: 0.5rem;
+ font-size: 0.9rem;
+ color: var(--text-colour);
+ z-index: 1;
+ position: relative;
+}
+
+.slider-values {
+ z-index: 2;
+ position: relative; /* <-- this is the key */
+ margin-left: 0.6rem;
+ margin-right: 0.6rem;
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.85rem;
+ color: var(--text-colour);
+ margin-bottom: 0.3rem;
+}
+
+.slider-current {
+ font-weight: bold;
+ color: var(--text-colour);
+ z-index: 1;
+}
+
+.input-field select,
+.input-field option {
+ background-color: var(--background-button);
+ color: var(--text-colour);
+}
+
+.input-field option:hover,
+.input-field option:focus {
+ background-color: var(--background-button-hover);
+ color: var(--text-colour-hover);
+}
\ No newline at end of file
diff --git a/shared/_eventDebug.lua b/shared/_eventDebug.lua
new file mode 100644
index 0000000..524ab20
--- /dev/null
+++ b/shared/_eventDebug.lua
@@ -0,0 +1,149 @@
+-- IN NO WAY PERFECT -- ** Experimental debugging
+function toggleDebug()
+ Config.System.Debug = not Config.System.Debug
+ print("Debug Prints = "..tostring(Config.System.Debug))
+end
+exports("toggleDebug", toggleDebug)
+
+function getDebug() return Config.System.Debug end
+exports("getDebug", getDebug)
+
+local origRegisterNetEvent = RegisterNetEvent
+local origTriggerEvent = TriggerEvent
+local origTriggerServerEvent = TriggerServerEvent
+local origTriggerClientEvent = TriggerClientEvent
+local origExecuteCommand = ExecuteCommand
+local origRegisterCommand = RegisterCommand
+local origPairs = pairs
+local origiPairs = ipairs
+
+function getDebugInfo(info)
+ local info = info
+ local level = 2
+ if info and info.short_src:match("scheduler.lua") then
+ repeat
+ info = debug.getinfo(level, "nSl")
+ level += 1
+ local found = false
+ for _, v in pairs({
+ "deffered.lua",
+ "scheduler.lua",
+ "_eventDebug.lua",
+ "targets.lua",
+ "init.lua",
+ "MySQL.lua",
+ "helpers.lua",
+ }) do
+ if info and info.short_src:match(v) then
+ found = true
+ end
+ end
+ until not info or (info.short_src and found == false)
+ end
+
+ return " ^7[^3"..(info and info.short_src:match("^.+/(.+)$") or "unknown").."^7:^3"..(info and info.currentline or "unknown").."^7]"
+end
+
+--This is just a for debugging, not important, just announces which events are being registered triggered when these functions are used
+function RegisterNetEvent(name, funct)
+ if Config.System.EventDebug then
+ if name:find("__ox_cb_") then
+ print("^6Bridge^7: ^2Registered ^3"..(isServer() and "Server" or "Client").." ^2Callback^7: ^6"..name:gsub("__ox_cb_", ""):gsub("%:", "^7:^4").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ else
+ print("^6Bridge^7: ^2Registering ^3"..(isServer() and "Server" or "Client").." ^2Net event^7: ^6"..name:gsub("%:", "^7:^4").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ end
+ origRegisterNetEvent(name, funct)
+end
+
+function TriggerEvent(name, ...)
+ local data = {...}
+ if Config.System.EventDebug then
+ if name:find("__cfx_export") then
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggering ^3"..(isServer() and "Server" or "Client").." ^2Export^7: ^6"..name:gsub("__cfx_export_", ""):gsub("%:", "^7:^4").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ else
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggering ^3"..(isServer() and "Server" or "Client").." ^2Net event^7: ^6"..name:gsub("%:", "^7:^4").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ for i, value in ipairs(data) do
+ if value then
+ local valueStr = (type(value) == "table" and json.encode(value) or tostring(value))
+ print(string.format("^6Bridge^7: ^7[^3%d^7]: ^7(^5%s^7): %s".."^7", i, type(value), valueStr))
+ end
+ end end
+ origTriggerEvent(name, ...)
+end
+
+function TriggerServerEvent(name, ...) -- Client side, trigger a server event
+ local data = {...}
+ if Config.System.EventDebug then
+ if name:find("__ox_cb") then
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggered ^3Server ^2Callback: ^6"..name:gsub("__ox_cb_", "").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ else
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggering ^3Server ^2Net event^7: ^6"..name:gsub("%:", "^7:^4").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ for i, value in ipairs(data) do
+ if value then
+ local valueStr = (type(value) == "table" and json.encode(value) or tostring(value))
+ print(string.format("^6Bridge^7: ^7[^3%d^7]: ^7(^5%s^7): %s".."^7", i, type(value), valueStr))
+ end
+ end end
+ origTriggerServerEvent(name, ...)
+end
+
+function TriggerClientEvent(name, ...) -- Server side, trigger a client event
+ local data = {...}
+ if Config.System.EventDebug then
+ if name:find("__ox_cb") then
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggered ^3Client ^2Callback: ^6"..name:gsub("__ox_cb_", "").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ else
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggering ^3Client ^2Net event^7: ^6"..name:gsub("%:", "^7:^4").."^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ for i, value in ipairs(data) do
+ if value then
+ local valueStr = (type(value) == "table" and json.encode(value) or tostring(value))
+ print(string.format("^6Bridge^7: ^7[^3%d^7]: ^7(^5%s^7): %s".."^7", i, type(value), valueStr))
+ end
+ end end
+ origTriggerClientEvent(name, ...)
+end
+
+function RegisterCommand(command, funct, restrict)
+ if Config.System.EventDebug then
+ print("^6Bridge^7: ^2Registering ^2Command^7: /"..command.." ^7| ^4Funct^7: "..tostring(funct):gsub("function: ", "").." ^7| ^4Admin^7: "..(restict and "true" or "false")..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ origRegisterCommand(command, funct, restrict)
+end
+
+function ExecuteCommand(comm) -- Client side, execute /command
+ if Config.System.EventDebug then
+ print("^6Bridge^7: "..GetPrintTime().." ^2Triggering ^3ExecuteCommand^7: /"..comm..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ origExecuteCommand(comm)
+end
+
+function pairs(tbl)
+ if not tbl then
+ print("^1Error^7: ^1nil ^2for ^3pairs^7(), ^2setting to ^7{} ^2to prevent break^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ return origPairs({})
+ end
+ return origPairs(tbl)
+end
+
+function ipairs(tbl)
+ local tbl = tbl
+ if not tbl then
+ if Config.System.EventDebug then
+ print("^1Error^7: ^3iPairs^7() ^1nil ^2recieved^7, ^2setting to ^7{} ^2to prevent break^7"..getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+ tbl = {}
+ end
+ return pairsByKeys(tbl) -- change to pairsByKeys for less errors
+end
+
+--[[
+local origPrint = print
+function print(...)
+ origPrint(getDebugInfo(debug.getinfo(2, "nSl"))..":")
+ origPrint(...)
+end
+]]
\ No newline at end of file
diff --git a/shared/_loaders.lua b/shared/_loaders.lua
new file mode 100644
index 0000000..04c6987
--- /dev/null
+++ b/shared/_loaders.lua
@@ -0,0 +1,178 @@
+--[[
+ Player & Resource Event Utility Functions
+ -------------------------------------------
+ This module provides functions to:
+ • Execute code when the player character is loaded or unloaded.
+ • Execute code on resource start and stop.
+ • Wait for the player to be logged in before proceeding.
+]]
+
+-------------------------------------------------------------
+-- Player Loaded and Unloaded Events
+-------------------------------------------------------------
+
+--- Executes a function when the player character is loaded.
+--- If onStart is true, the function will also run on resource start (after ensuring the player is logged in).
+---
+--- @param func function The function to execute when the player is loaded.
+--- @param onStart boolean (optional) If true, also execute on resource start. Default is false.
+--- @usage
+--- ```lua
+--- onPlayerLoaded(function()
+--- print("Player logged in")
+--- -- Your initialization code here.
+--- 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()
+ end, true)
+ end
+
+ if not loaded then
+ local tempFunc = function()
+ debugPrint("^6Bridge^7: ^2Executing onPlayerLoaded")
+ 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', tempFunc)
+ 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")
+ end
+ end
+end
+
+--- Executes a function when the player character is unloaded.
+--- @param func function The function to execute when the player unloads.
+--- @usage
+--- ```lua
+--- onPlayerUnload(function()
+--- print("Player has logged out of their character")
+--- -- Your cleanup code here.
+--- 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
+end
+
+-------------------------------------------------------------
+-- Resource Start and Stop Events
+-------------------------------------------------------------
+
+--- Executes a function when the resource starts.
+--- @param func function The function to execute.
+--- @param thisScript boolean (optional) If true, only runs when this resource starts (default true).
+--- @usage
+--- ```lua
+--- onResourceStart(function()
+--- print("Script ensured")
+--- -- Initialization code on resource start.
+--- end, true)
+--- ```
+function onResourceStart(func, thisScript)
+ debugPrint("^6Bridge^7: Registering ^3onResourceStart^7()")
+ AddEventHandler('onResourceStart', function(resourceName)
+ if getScript() == resourceName and (thisScript or true) then
+ func()
+ end
+ end)
+end
+
+--- Executes a function when the resource stops.
+--- @param func function The function to execute.
+--- @param thisScript boolean (optional) If true, only runs when this resource stops (default true).
+--- @usage
+--- ```lua
+--- onResourceStop(function()
+--- -- Cleanup code here.
+--- end, true)
+--- ```
+function onResourceStop(func, thisScript)
+ debugPrint("^6Bridge^7: ^2Registering ^3onResourceStop^7()")
+ AddEventHandler('onResourceStop', function(resourceName)
+ if getScript() == resourceName and (thisScript or true) then
+ func()
+ end
+ end)
+end
+
+-------------------------------------------------------------
+-- Wait for Login
+-------------------------------------------------------------
+
+--- Blocks execution until the player is logged in.
+--- @usage
+--- waitForLogin()
+function waitForLogin()
+ local timeout = 10000 -- 10 seconds in milliseconds
+ local startTime = GetGameTimer()
+ local loggedIn = false
+
+ if isStarted(ESXExport) then
+ debugPrint("^6Bridge^7: ^3ESX waitForLogin^7() ^2running^7")
+ while (GetGameTimer() - startTime) < timeout do
+ while not ESX do Wait(100) end
+ local playerData = ESX.GetPlayerData()
+ if playerData and playerData.job then
+ loggedIn = true
+ break
+ end
+ Wait(100)
+ end
+ elseif isStarted(OXCoreExport) then
+ if OxPlayer["stateId"] then
+ loggedIn = true
+ end
+ while not OxPlayer["stateId"] do
+ Wait(1000)
+ debugPrint("Waiting for stateId to class as logged in")
+ if OxPlayer.get["stateId"] then
+ loggedIn = true
+ break
+ end
+ end
+ else
+ -- For other frameworks, use LocalPlayer.state.isLoggedIn.
+ while not LocalPlayer.state.isLoggedIn and (GetGameTimer() - startTime) < timeout do
+ Wait(100)
+ end
+ loggedIn = LocalPlayer.state.isLoggedIn
+ end
+
+ if not loggedIn then
+ print("^4Error^7: ^2Timeout reached while waiting for player login^7.")
+ return false
+ else
+ debugPrint("^6Bridge^7: ^2Player Login Detected^7.")
+ return true
+ end
+end
\ No newline at end of file
diff --git a/shared/_scriptversioncheck.lua b/shared/_scriptversioncheck.lua
new file mode 100644
index 0000000..5105767
--- /dev/null
+++ b/shared/_scriptversioncheck.lua
@@ -0,0 +1,76 @@
+function parseVersion(version)
+ local parts = {}
+ for num in version:gmatch("%d+") do
+ table.insert(parts, tonumber(num))
+ 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
+ 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
+
+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")
+
+function CheckVersion()
+ if isServer() then
+ CreateThread(function()
+ Wait(4000)
+
+ local script = getScript()
+ local currentVersionRaw = GetResourceMetadata(script, 'version')
+
+ PerformHttpRequest('https://raw.githubusercontent.com/jimathy/UpdateVersions/master/'..script..'.txt', function(err, newestVersionRaw, headers)
+ if not newestVersionRaw then
+ 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
+
+ fallbackVersionRaw = fallbackVersionRaw:match("[^\r\n]+"):gsub("v", "")
+
+ local compareResult = compareVersions(currentVersionRaw, fallbackVersionRaw)
+ if compareResult == 0 then
+ print("^7'^3"..script.."^7' - ^2You are running the latest version^7. (^3"..currentVersionRaw.."^7)")
+ elseif compareResult < 0 then
+ print("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..fallbackVersionRaw.."^7)")
+ else
+ print("^7'^3"..script.."^7' - ^5You are running a newer version ^7(^3"..currentVersionRaw.."^7 ← ^3"..fallbackVersionRaw.."^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("^7'^3"..script.."^7' - ^1You are currently running an outdated version^7! (^3"..currentVersionRaw.."^7 → ^3"..newestVersionRaw.."^7)")
+ 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
+
+CheckVersion()
\ No newline at end of file
diff --git a/shared/callback.lua b/shared/callback.lua
new file mode 100644
index 0000000..f60f106
--- /dev/null
+++ b/shared/callback.lua
@@ -0,0 +1,87 @@
+--- 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.
+---
+---@param callbackName string The name of the callback to register.
+---@param funct function The function to be called when the callback is triggered.
+---
+---@usage
+--- ```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)
+--- ```
+function createCallback(callbackName, funct)
+ if isServer() then
+ debugPrint("^6Bridge^7: ^3Registering callback^7:", callbackName)
+ if isStarted(OXLibExport) then
+ lib.callback.register(callbackName, funct)
+ else
+ local adaptedFunction = function(source, cb, ...)
+ local result = funct(source, ...)
+ cb(result)
+ end
+
+ if isStarted(QBExport) then
+ Core = Core or exports[QBExport]:GetCoreObject()
+ Core.Functions.CreateCallback(callbackName, adaptedFunction)
+ elseif isStarted(ESXExport) then
+ ESX.RegisterServerCallback(callbackName, adaptedFunction)
+ else
+ print("^6Bridge^7: ^1ERROR^7: ^3Can't find any script to register callback with", callbackName)
+ end
+ end
+ end
+end
+
+--- Triggers a server callback and returns the result.
+---
+--- This function triggers a server callback using the appropriate framework's method and awaits the result.
+---
+---@param callbackName string The name of the callback to trigger.
+---@param ... any Additional arguments to pass to the callback.
+---
+---@return any any The result returned by the callback function.
+---
+---@usage
+--- ```lua
+--- local result = triggerCallback('myCallback')
+--- jsonPrint(result)
+---
+--- local result = triggerCallback("callback:checkVehicleOwned", plate)
+--- print(result)
+--- ```
+function triggerCallback(callbackName, ...)
+ local result = nil
+ debugPrint("^6Bridge^7: ^3Triggering callback^7:", callbackName)
+ 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)
+ end
+ return result
+end
\ No newline at end of file
diff --git a/shared/contextmenus.lua b/shared/contextmenus.lua
new file mode 100644
index 0000000..48a0b39
--- /dev/null
+++ b/shared/contextmenus.lua
@@ -0,0 +1,315 @@
+--[[
+ Menu Opening Module
+ ---------------------
+ This module provides a unified function to open menus using the configured menu system.
+ Supported systems include:
+ • jim_bridge (built-in nui menu that works on any framework)
+ • ox (or ox_context)
+ • qb (using QBMenuExport)
+ • gta (using WarMenu)
+ • esx (using ESX.UI.Menu)
+]]
+
+--- 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`.
+---
+---@param Menu table A table containing the menu options to display.
+--- Each menu item can include:
+--- - header (`string`): The text to display for the menu item.
+--- - txt (`string`, optional): Additional text or description.
+--- - icon (`string`, optional): Icon to display with the menu item.
+--- - onSelect (`function`, optional): Function to execute when the menu item is selected.
+--- - arrow (`boolean`, optional): Whether to display an arrow next to the item (for certain menus).
+--- - params (`table`, optional): Additional parameters, such as events and arguments.
+--- - isMenuHeader (`boolean`, optional): Marks the item as a header.
+--- - disabled (`boolean`, optional): Disables the menu item if `true`.
+---
+---@param data table A table containing configuration data for the menu.
+--- - header (`string`): The header/title of the menu.
+--- - headertxt (`string`, optional): Additional header text.
+--- - onBack (`function`, optional): Function to call when the "Return" option is selected.
+--- - onExit (`function`, optional): Function to call when the menu is exited.
+--- - onSelected (`function`, optional): Function to call when a menu item is selected (for certain menu systems).
+--- - canClose (`boolean`, optional): Whether the menu can be closed by the user.
+---
+---@usage
+--- ```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,
+--- })
+--- ```
+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
+end
+
+--- A line break constant used for menu header formatting.
+br = (Config.System.Menu == "ox" or Config.System.Menu == "gta") and "\n" or " "
+
+--- 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.
+--- @usage
+--- ```lua
+--- if isOx() then
+--- -- Use specific formatting
+--- end
+--- ```
+function isOx() return (Config.System.Menu == "ox" or Config.System.Menu == "gta") end
+
+
+--- Checks if any WarMenu menu is currently open.
+---
+--- @return boolean boolean Returns `true` if a WarMenu menu is open; otherwise, `false`.
+---
+--- @usage
+--- ```lua
+--- if isWarMenuOpen() then
+--- -- Do something
+--- end
+--- ```
+function isWarMenuOpen() if Config.System.Menu == "gta" then return WarMenu.IsAnyMenuOpened() else return false end end
\ No newline at end of file
diff --git a/shared/coreloader.lua b/shared/coreloader.lua
new file mode 100644
index 0000000..d9422c7
--- /dev/null
+++ b/shared/coreloader.lua
@@ -0,0 +1,292 @@
+--[[
+ 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.
+]]
+
+-------------------------------------------------------------
+-- Global Variable Initialization
+-------------------------------------------------------------
+Items, Vehicles, Jobs, Gangs, Core, ESX = {}, nil, nil, nil, nil, nil
+
+-------------------------------------------------------------
+-- Correct QB Inventory Export
+-------------------------------------------------------------
+-- Ensure that the QB inventory export is corrected from 'qb-inventory' to 'ps-inventory' or 'lj-inventory' if needed.
+Exports.QBInv = (isStarted("ps-inventory") and "ps-inventory") or (isStarted("lj-inventory") and "lj-inventory") or Exports.QBInv
+
+-------------------------------------------------------------
+-- Framework Exports and Inventory Identifiers
+-------------------------------------------------------------
+OXLibExport, QBXExport, QBExport, ESXExport, OXCoreExport =
+ Exports.OXLibExport or "",
+ Exports.QBXExport or "",
+ Exports.QBExport or "",
+ Exports.ESXExport or "",
+ Exports.OXCoreExport or ""
+
+OXInv, QBInv, PSInv, QSInv, CoreInv, CodeMInv, OrigenInv =
+ Exports.OXInv or "",
+ Exports.QBInv or "",
+ Exports.PSInv or "",
+ Exports.QSInv or "",
+ Exports.CoreInv or "",
+ Exports.CodeMInv or "",
+ Exports.OrigenInv 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
+ ESX = exports[ESXExport]:getSharedObject()
+ while ESX == nil do
+ print("Waiting for ESX")
+ Wait(0)
+ end
+ CreateThread(function()
+ while not ESX do Wait(0) end
+ 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
+ if isStarted(RSGExport) and not isStarted(QBXExport) then
+ RegisterNetEvent('QBCore:Client:UpdateObject', function()
+ Core = Core or exports[RSGExport]:GetCoreObject()
+ Items = Core and Core.Shared.Items or nil
+ end)
+ 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
+ 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
+ vehResource = QBExport
+
+elseif isStarted(OXCoreExport) then
+ 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
+ vehResource = OXCoreExport
+
+elseif isStarted(ESXExport) then
+ CreateThread(function()
+ if isServer() then
+ vehResource = ESXExport
+ createCallback(getScript()..":getVehiclesPrices", function(source)
+ return Vehicles
+ end)
+ while not MySQL do Wait(2000) print("^1Waiting for MySQL to exist") end
+ Vehicles = MySQL.query.await('SELECT model, price, name FROM vehicles')
+ --jsonPrint(Vehicles)
+ --while not createCallback do print("waiting") Wait(100) end
+ end
+ if not isServer() then
+ --while not triggerCallback do print("waiting") Wait(100) end
+ local TempVehicles = triggerCallback(getScript()..":getVehiclesPrices")
+ for _, v in pairs(TempVehicles) do
+ Vehicles = Vehicles or {}
+ Vehicles[v.model] = {
+ model = v.model,
+ hash = GetHashKey(v.model),
+ price = v.price,
+ name = v.name,
+ brand = GetMakeNameFromVehicleModel(v.model):lower():gsub("^%l", string.upper)
+ }
+ end
+ end
+ end)
+
+elseif isStarted(RSGExport) then
+ Core = Core or exports[RSGExport]:GetCoreObject()
+ Vehicles = Core and Core.Shared.Vehicles
+ if isStarted(RSGExport) then
+ RegisterNetEvent('RSGExport:Client:UpdateObject', function()
+ Core = Core or exports[RSGExport]:GetCoreObject()
+ Vehicles = Core and Core.Shared.Vehicles
+ end)
+ end
+ vehResource = RSGExport
+end
+if vehResource == nil then
+ print("^4ERROR^7: ^2No Vehicle info detected ^7- ^2Check ^3starter^1.^2lua^7")
+else
+ CreateThread(function()
+ while not Vehicles do Wait(1000) print("Waiting") end
+ debugPrint("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..vehResource)
+ end)
+end
+
+-------------------------------------------------------------
+-- Loading Jobs and Gangs
+-------------------------------------------------------------
+-- Compile jobs and gangs from the detected framework.
+if isStarted(QBXExport) then
+ jobResource = QBXExport
+ Core = Core or exports[QBExport]:GetCoreObject()
+ Jobs, Gangs = exports[QBXExport]:GetJobs(), exports[QBXExport]:GetGangs()
+
+elseif isStarted(OXCoreExport) then
+ jobResource = 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 }
+ 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
+ end)
+ end
+
+elseif isStarted(ESXExport) then
+ ESX = exports[ESXExport]:getSharedObject()
+ if isServer() then
+ Jobs = ESX.GetJobs()
+ for k, v in pairs(Jobs) do
+ local count = countTable(Jobs[k].grades) - 1
+ Jobs[k].grades[tostring(count)].isBoss = true
+ end
+ Gangs = Jobs
+ end
+ CreateThread(function()
+ while not ESX do Wait(0) end
+ if isServer() then
+ createCallback(getScript()..":getJobs", function(source)
+ return Jobs
+ end)
+ end
+ if not isServer() then
+ Jobs = triggerCallback(getScript()..":getJobs")
+ Gangs = Jobs
+ end
+ end)
+
+elseif isStarted(RSGExport) then
+ jobResource = RSGExport
+ Core = Core or exports[RSGExport]:GetCoreObject()
+ Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
+ if isStarted(RSGExport) and not isStarted(QBXExport) then
+ RegisterNetEvent('QBCore:Client:UpdateObject', function()
+ Core = exports[RSGExport]:GetCoreObject()
+ Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
+ end)
+ 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(100) 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
\ No newline at end of file
diff --git a/shared/crafting.lua b/shared/crafting.lua
new file mode 100644
index 0000000..334667d
--- /dev/null
+++ b/shared/crafting.lua
@@ -0,0 +1,468 @@
+--[[
+ Crafting, Selling, and Shop Module
+ -----------------------------------
+ This module provides functions for opening crafting menus, handling multi-crafting,
+ performing the crafting process (with animations and progress bars), selling items,
+ and opening shop interfaces. It integrates with various inventory and menu systems,
+ and uses server callbacks to check item carry capacity.
+]]
+
+-------------------------------------------------------------
+-- Global Variables
+-------------------------------------------------------------
+CraftLock = false
+
+-- helper filter table for crafting menus
+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,
+}
+
+-------------------------------------------------------------
+-- Crafting Menu
+-------------------------------------------------------------
+
+--- Opens the crafting menu based on provided data.
+--- Checks job restrictions, builds the recipe menu, and opens the menu.
+---
+--- @param data table Crafting menu configuration containing:
+--- - craftable (`table`) Table with Header, Recipes, Anims, and (optionally) craftedItems.
+--- - coords (`vector3`) The coordinates where the crafting menu is being opened.
+--- - stashTable|stashName (`string\table`) Name(s) of the stash for checking item availability.
+--- - job|gang (`string`) Job or gang requirements.
+--- - onBack (optional): Function to call when returning.
+---
+--- @usage
+--- ```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,
+--- })
+function craftingMenu(data)
+ if CraftLock then return end
+
+ -- Job or gang check; exit if not authorized.
+ if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
+
+ -- Display a temporary "thinking" notification.
+ if Config.System.Menu == "jim" then
+ triggerNotify(nil, "Thinking", "info")
+ else
+ openMenu({ { header = "Thinking...", icon = "fas fa-hourglass-end", isMenuHeader = true } }, { header = "Crafting Menu" } )
+ end
+
+ -- Normalize stash name.
+ data.stashName = data.stashTable or data.stashName
+
+ 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 == "hasCrafted" and not data.craftable.craftedItems then
+ -- Retreive list of already crafted items from playermetadata to see if we should class this recipe as "new"
+ craftedItems = GetMetadata(nil, "craftedItems") or {}
+ data.craftable.craftedItems = craftedItems
+ end
+ if k ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" then
+ tempCarryTable[k] = Recipes[i].amount or 1
+ 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
+ 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 "")
+
+ if not disable then
+ if not canCarryTable[k] then
+ setheader = setheader.." 📦"
+ else
+ setheader = setheader.." ✔️"
+ end
+ elseif not canCarryTable[k] then
+ setheader = setheader.." 📦"
+ end
+ if Recipes[i]["hasCrafted"] ~= nil and craftedItems[k] == nil then
+ setheader = "✨ "..setheader
+ end
+
+ 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..((disable or not canCarryTable[k]) and " ❌" or ""),
+ 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
+ end
+ Wait(0)
+ end
+ end
+
+ openMenu(Menu, {
+ header = data.craftable.Header,
+ headertxt = data.craftable.Headertxt,
+ onBack = data.onBack or nil,
+ canClose = true,
+ onExit = data.onExit or (function() end),
+ })
+ lookEnt(data.coords)
+end
+
+-------------------------------------------------------------
+-- Multi-Craft Menu
+-------------------------------------------------------------
+
+--- Opens a menu for selecting the quantity to craft.
+---
+--- Presents the player with multiple crafting quantities based on Config.Crafting.MultiCraftAmounts.
+---
+--- @param data table Crafting configuration containing:
+--- - 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.
+---
+--- @usage
+--- ```lua
+--- multiCraft({
+--- item = "weapon_pistol",
+--- craft = { ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, amount = 1 },
+--- craftable = craftingOptions,
+--- coords = vector3(100,200,300),
+--- stashName = "crafting_stash",
+--- onBack = function() craftingMenu(data) end,
+--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
+--- })
+--- ```
+function multiCraft(data)
+ local max = 0
+ local stashName = nil
+ for i = 1, 100 do
+ local itemTable = {}
+ for l, b in pairs(data.craft[data.item]) do
+ debugPrint("")
+ itemTable[l] = (b * i)
+ end
+
+ if data.stashName then
+ debugPrint("")
+ local hasItems, stashname = checkStashItem(data.stashName, itemTable)
+ if hasItems == true then
+ max += 1
+ stashName = stashname
+ else
+ break
+ end
+ else
+ debugPrint("")
+ local has, _ = hasItem(itemTable, nil, nil)
+ if has then
+ max += 1
+ else
+ break
+ end
+ end
+ Wait(10)
+ end
+
+ local dialog = createInput(data.craftable.Header, {
+ ((Config.System.Menu == "ox") and {
+ type = "slider",
+ label = "How many to craft?",
+ required = true,
+ default = 1,
+ min = 1,
+ max = max
+ }) or nil,
+ ((Config.System.Menu == "qb") and {
+ type = "number",
+ label = "How many to craft?"..br.."Max: "..max,
+ name = "amount",
+ isRecuired = true,
+ default = 1,
+ }) or nil,
+ })
+
+ if dialog then
+ if Config.System.Menu == "ox" then
+
+ end
+ if Config.System.Menu == "qb" then
+ if dialog["amount"] > max or dialog["amount"] < 1 or dialog["amount"] == nil or dialog["amount"] == "" then
+ triggerNotify(nil, "Invalid Amount", "error")
+ craftingMenu(data)
+ return
+ end
+ end
+
+
+ makeItem({
+ item = data.item,
+ craft = data.craft,
+ craftable = data.craftable,
+ amount = dialog["amount"] or dialog[1],
+ coords = data.coords,
+ stashName = stashName or nil,
+ --stashTable = data.stashName,
+ onBack = data.onBack,
+ metadata = data.metadata,
+ })
+ end
+end
+
+-------------------------------------------------------------
+-- Crafting Process
+-------------------------------------------------------------
+
+--- Initiates the crafting process for a specified item.
+---
+--- Plays crafting animations, shows progress bars, removes ingredients, and triggers item creation.
+---
+--- @param data table Crafting configuration containing:
+--- - item `string`) The item to craft.
+--- - craft (`table`) The crafting recipe.
+--- - craftable (`table`) Crafting options.
+--- - amount (`number`) (optional): Quantity to craft (default 1).
+--- - 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.
+---
+--- @usage
+--- ```lua
+--- makeItem({
+--- item = "weapon_pistol",
+--- craft = { ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, amount = 1 },
+--- craftable = craftingOptions,
+--- amount = 2,
+--- coords = vector3(100,200,300),
+--- stashName = "crafting_stash",
+--- onBack = function() craftingMenu(data) end,
+--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
+--- })
+--- ```
+function makeItem(data)
+ if CraftLock then return end
+ CraftLock = true
+ data.stashName = data.stashTable or data.stashName
+
+ local bartime = (data.craftable.progressBar and data.craftable.progressBar.time) or 5000
+ local bartext = (data.craftable.progressBar and data.craftable.progressBar.label)
+ or (Loc[Config.Lan].progressbar and Loc[Config.Lan].progressbar["progress_make"])
+ or "Making "
+ local animDict = (data.craftable.Anims and data.craftable.Anims.animDict) or "amb@prop_human_parking_meter@male@idle_a"
+ local anim = (data.craftable.Anims and data.craftable.Anims.anim) or "idle_a"
+ local craftAmount = (data.amount and data.amount ~= 1) and data.amount or 1
+ local metadata = data.metadata or nil
+ local prop = data.craftable.Anims and data.craftable.Anims.prop or nil
+ local canReturn = true
+
+ local crafted, crafting = true, true
+ local cam = createTempCam(PlayerPedId(), data.coords)
+ startTempCam(cam)
+
+ for i = 1, craftAmount do
+ for k, v in pairs(data.craft) do
+ if not excludeKeys[k] then
+ if type(v) == "table" then
+ for l, b in pairs(v) do
+ if crafting and progressBar({
+ label = "Using "..b.." "..Items[l].label,
+ time = 1000,
+ cancel = true,
+ dict = 'pickup_object',
+ anim = "putdown_low",
+ flag = 48,
+ 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 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 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,
+ }) then
+ TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata)
+ CreateThread(function()
+ if data.craft["hasCrafted"] ~= nil then
+ debugPrint("hasCrafted Found, marking '"..data.item.."' as crafted for player")
+ data.craftable.craftedItems[data.item] = true
+ triggerCallback(getScript()..":server:SetMetadata", "craftedItems", data.craftable.craftedItems)
+ end
+ Wait(100)
+ if data.craft["exp"] ~= nil then
+ craftingLevel += data.craft["exp"].give
+ jsonPrint(data.craft["exp"])
+ debugPrint("exp found, giving exp for '"..data.item.."'")
+ triggerCallback(getScript()..":server:SetMetadata", "craftingLevel", craftingLevel)
+ end
+ end)
+ if data.craftable.Recipes[1].oneUse == true then
+ removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
+ local breakId = GetSoundId()
+ PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
+ canReturn = false
+ end
+ if data.sound then
+ StopSound(data.sound.soundId)
+ end
+ if data.requiredItemfunc then
+ data.requiredItemfunc()
+ end
+ else
+ crafting = false
+ break
+ end
+ if craftProp then destroyProp(craftProp) end
+ end
+ end
+ end
+ end
+ Wait(500)
+ end
+ stopTempCam()
+ CraftLock = false
+ lockInv(false)
+ if canReturn then craftingMenu(data) end
+ ClearPedTasks(PlayerPedId())
+end
+
+-------------------------------------------------------------
+-- Server Event Handler: Crafted Item
+-------------------------------------------------------------
+
+--- Server event handler for giving the crafted item to the player.
+---
+--- Removes required ingredients from the player's inventory or stash,
+--- then adds the crafted item to their inventory.
+---
+--- @param ItemMake string The item being crafted.
+--- @param craftable table The crafting recipe and details.
+--- @param stashName string|table The stash name(s) to remove ingredients from.
+--- @param metadata table (optional) Metadata for the crafted item.
+--- @usage
+RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, stashName, metadata)
+ local src = source
+ 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 _, b in pairs(stashItems or {}) do
+ if k == b.name then itemRemove[k] = v end
+ end
+ end
+ end
+ else
+ stashItems = getStash(stashName)
+ for k, v in pairs(craftable[ItemMake] or {}) do
+ for _, b in pairs(stashItems or {}) do
+ if k == b.name then itemRemove[k] = v end
+ end
+ end
+ end
+ stashRemoveItem(stashItems, stashName, itemRemove)
+ else
+ if craftable then
+ for k, v in pairs(craftable[ItemMake] or {}) do
+ removeItem(tostring(k), v, src)
+ end
+ end
+ end
+ addItem(ItemMake, craftable.amount or 1, metadata, src)
+ -- Optionally, add experience here:
+ -- for example:
+ -- if isStarted("core_skills") then exports["core_skills"]:AddExperience(src, 2) end
+end)
\ No newline at end of file
diff --git a/shared/drawText.lua b/shared/drawText.lua
new file mode 100644
index 0000000..62ccb1f
--- /dev/null
+++ b/shared/drawText.lua
@@ -0,0 +1,98 @@
+--[[
+ Text Drawing Module
+ ---------------------
+ This module provides functions to display and hide text on screen using
+ various frameworks: QB, OX, GTA, and ESX.
+]]
+
+--- Displays text on the screen using the configured draw text system.
+---
+--- Depending on Config.System.drawText, this function will use different methods to
+--- display text along with optional images/icons.
+---
+--- @param image string|nil Optional image/icon identifier to display with the text.
+--- @param input table An array of strings; each string is a line of text to display.
+--- @param style string|nil Optional style code for default GTA popups (e.g., "~g~" for green).
+--- @param oxStyleTable table|nil Optional table specifying style parameters for the OX text UI.
+---
+---@usage
+--- ```lua
+--- 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.
+ for i = 1, #input do
+ text = text..input[i]..""..(input[i + 1] and " " or "")
+ end
+ text = text:gsub("%:", ":")
+ if image then
+ text = ''..text
+ end
+ exports[QBExport]:DrawText(text, 'left')
+
+ 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 Config.System.drawText == "gta" then
+ -- Concatenate input lines and apply GTA style formatting.
+ for i = 1, #input do
+ if input[i] ~= "" then
+ text = text..input[i].."\n~s~"
+ end
+ end
+ if image then
+ text = "~BLIP_"..image.."~ "..text
+ end
+ DisplayHelpMsg(text:gsub("%:", ":~"..(style or "g").."~"))
+
+ elseif Config.System.drawText == "esx" then
+ -- ESX-based text UI uses similar HTML formatting as QB.
+ for i = 1, #input do
+ text = text..input[i]..""..(input[i + 1] and " " or "")
+ end
+ text = text:gsub("%:", ":")
+ if image then
+ text = ''..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)
+
+ end
+end
+
+--- Hides any text currently displayed on the screen.
+---
+--- Clears the text using the appropriate method for the configured draw text system.
+---
+--- @usage
+--- ```lua
+--- 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")
+ end
+end
\ No newline at end of file
diff --git a/shared/duifunctions.lua b/shared/duifunctions.lua
new file mode 100644
index 0000000..e346aca
--- /dev/null
+++ b/shared/duifunctions.lua
@@ -0,0 +1,161 @@
+if gameName ~= "rdr3" then
+ --[[
+ DUI Module (Experimental)
+ --------------------------
+ This module handles the creation, modification, and removal of custom DUI (Display UI)
+ elements using runtime textures. It supports both client and server functionality to update DUI
+ images dynamically.
+ ]]
+
+ -- Create a runtime texture dictionary on the client if not running on the server.
+ scriptTxd = not isServer() and CreateRuntimeTxd(getScript()..'scriptTxd') or nil
+ customDUIList = {}
+
+ -------------------------------------------------------------
+ -- DUI Client Functions
+ -------------------------------------------------------------
+
+ --- Creates or updates a DUI element.
+ ---
+ --- @param name string The unique name for the DUI element.
+ --- @param http string The URL to load into the DUI.
+ --- @param size table A table with .x and .y fields specifying the DUI dimensions.
+ --- @param txd table The runtime texture dictionary where the DUI texture will be created.
+ --- @usage
+ --- ```lua
+ --- createDui("logo", "https://example.com/logo.png", { x = 512, y = 256 }, scriptTxd)
+ --- ```
+ function createDui(name, http, size, txd)
+ if not customDUIList[name] then
+ local newDui = CreateDui(http, math.floor(size.x), math.floor(size.y))
+ while not GetDuiHandle(newDui) do Wait(0) end
+ CreateRuntimeTextureFromDuiHandle(txd, name, GetDuiHandle(newDui))
+ customDUIList[name] = newDui
+ SetDuiUrl(customDUIList[name], http)
+ else
+ SetDuiUrl(customDUIList[name], http)
+ end
+ end
+
+ --- Opens a DUI selection input allowing the user to change the DUI image URL.
+ ---
+ --- @param data table A table containing DUI data:
+ --- - name: The key name in the DUI list.
+ --- - texn: The texture name.
+ --- - texd: The texture dictionary.
+ --- - size: A table with .x and .y dimensions.
+ ---
+ --- @usage
+ --- ```lua
+ --- DuiSelect({ name = "logo", texn = "logoTex", texd = "someTxd", size = { x = 512, y = 256 } })
+ --- ```
+ function DuiSelect(data)
+ local imagePreview = ""
+ --local imagePreview = "
"
+
+ local dialog = createInput(imagePreview, {
+ { type = "text", text = "dui_url", name = "url", isRequired = true },
+ })
+
+ if dialog then
+ data.url = dialog.url or dialog[1]
+ -- Scan URL for valid image extension and banned words.
+ local searchList = { "png", "jpg", "jpeg", "gif", "webp", "bmp" }
+ local banList = { "porn" }
+ local searchFound = false
+ for _, ext in pairs(searchList) do
+ if string.find(tostring(data.url), ext) then
+ searchFound = true
+ break
+ end
+ end
+ for _, banned in pairs(banList) do
+ if string.find(tostring(data.url), banned) then
+ searchFound = false
+ print("BANNED WORD: "..banned)
+ break
+ end
+ end
+ if searchFound then
+ TriggerServerEvent(getScript()..":Server:ChangeDUI", data)
+ end
+ end
+ end
+
+ --- Client event handler to update DUI elements.
+ RegisterNetEvent(getScript()..":Client:ChangeDUI", function(data)
+ debugPrint("^6Bridge^7: ^2Receiving new DUI ^7- ^6"..data.url.."^7")
+ if tostring(data.url) ~= "-" then
+ createDui(data.texn, tostring(data.url), data.size, scriptTxd)
+ AddReplaceTexture(tostring(data.texd), tostring(data.texn), getScript().."scriptTxd", tostring(data.texn))
+ end
+ end)
+
+ --- Client event handler to clear DUI elements.
+ RegisterNetEvent(getScript()..":Client:ClearDUI", function(data)
+ if customDUIList[tostring(data.texn)] then
+ RemoveReplaceTexture(tostring(data.texd), tostring(data.texn))
+ if IsDuiAvailable(customDUIList[tostring(data.texn)]) then
+ SetDuiUrl(customDUIList[data.name], nil)
+ end
+ end
+ end)
+
+ -------------------------------------------------------------
+ -- DUI Server Functions
+ -------------------------------------------------------------
+
+ --- Server event handler to change DUI settings.
+ --- If no URL is provided, resets to the preset value.
+ RegisterNetEvent(getScript()..":Server:ChangeDUI", function(data)
+ if not data.url then
+ debugPrint("^6Bridge^7: ^2Preset: ^6"..tostring(data.preset).."^7")
+ data.url = data.preset
+ else
+ for k, v in pairs(Locations[data.name].duiList) do
+ if v.tex.texn == data.texn then
+ Locations[data.name].duiList[k].url = data.url
+ end
+ end
+ end
+ debugPrint("^6Bridge^7: ^3DUI^2 Sending new DUI to all players^7 - ^6"..data.url.."^7")
+ TriggerClientEvent(getScript()..":Client:ChangeDUI", -1, data)
+ end)
+
+ --- Server event handler to clear DUI settings.
+ RegisterNetEvent(getScript()..":Server:ClearDUI", function(data)
+ if data.url == "-" then
+ for k, v in pairs(Locations[data.name].duiList) do
+ if v.tex.texn == data.texn then
+ Locations[data.name].duiList[k].url = "-"
+ end
+ end
+ end
+ TriggerClientEvent(getScript()..":Client:ClearDUI", -1, data)
+ end)
+
+ -------------------------------------------------------------
+ -- Resource Cleanup
+ -------------------------------------------------------------
+
+ onResourceStop(function()
+ for k, v in pairs(duiList or {}) do
+ for i = 1, #v do
+ RemoveReplaceTexture(tostring(v[i].tex.texd), tostring(v[i].tex.texn))
+ end
+ end
+ end, true)
+
+ -------------------------------------------------------------
+ -- DUI List Callback (Server)
+ -------------------------------------------------------------
+
+ if isServer() then
+ createCallback(getScript()..":Server:duiList", function(source)
+ return duiList
+ end)
+ end
+
+ end
\ No newline at end of file
diff --git a/shared/effects.lua b/shared/effects.lua
new file mode 100644
index 0000000..353aef1
--- /dev/null
+++ b/shared/effects.lua
@@ -0,0 +1,190 @@
+--Screen Effects
+local alienEffect = false
+function AlienEffect()
+ if alienEffect then return else alienEffect = true end
+ debugPrint("^5Debug^7: ^3AlienEffect^7() ^2activated")
+ AnimpostfxPlay("DrugsMichaelAliensFightIn", 3.0, 0)
+ Wait(math.random(5000, 8000))
+ local Ped = PlayerPedId()
+ local animDict = "MOVE_M@DRUNK@VERYDRUNK"
+ loadAnimDict(animDict)
+ SetPedCanRagdoll(Ped, true)
+ ShakeGameplayCam('DRUNK_SHAKE', 2.80)
+ SetTimecycleModifier("Drunk")
+ SetPedMovementClipset(Ped, animDict, 1)
+ SetPedMotionBlur(Ped, true)
+ SetPedIsDrunk(Ped, true)
+ Wait(1500)
+ SetPedToRagdoll(Ped, 5000, 1000, 1, 0, 0, 0)
+ Wait(13500)
+ SetPedToRagdoll(Ped, 5000, 1000, 1, 0, 0, 0)
+ Wait(120500)
+ ClearTimecycleModifier()
+ ResetScenarioTypesEnabled()
+ ResetPedMovementClipset(Ped, 0)
+ SetPedIsDrunk(Ped, false)
+ SetPedMotionBlur(Ped, false)
+ AnimpostfxStopAll()
+ ShakeGameplayCam('DRUNK_SHAKE', 0.0)
+ AnimpostfxPlay("DrugsMichaelAliensFight", 3.0, 0)
+ Wait(math.random(45000, 60000))
+ AnimpostfxPlay("DrugsMichaelAliensFightOut", 3.0, 0)
+ AnimpostfxStop("DrugsMichaelAliensFightIn")
+ AnimpostfxStop("DrugsMichaelAliensFight")
+ AnimpostfxStop("DrugsMichaelAliensFightOut")
+ alienEffect = false
+ debugPrint("^5Debug^7: ^3AlienEffect^7() ^2stopped")
+end
+local weedEffect = false
+function WeedEffect()
+ if weedEffect then return else weedEffect = true end
+ debugPrint("^5Debug^7: ^3WeedEffect^7() ^2activated")
+ AnimpostfxPlay("DrugsMichaelAliensFightIn", 3.0, 0)
+ Wait(math.random(3000, 20000))
+ AnimpostfxPlay("DrugsMichaelAliensFight", 3.0, 0)
+ Wait(math.random(15000, 20000))
+ AnimpostfxPlay("DrugsMichaelAliensFightOut", 3.0, 0)
+ AnimpostfxStop("DrugsMichaelAliensFightIn")
+ AnimpostfxStop("DrugsMichaelAliensFight")
+ AnimpostfxStop("DrugsMichaelAliensFightOut")
+ weedEffect = false
+ debugPrint("^5Debug^7: ^3WeedEffect^7() ^2stopped")
+end
+local trevorEffect = false
+function TrevorEffect()
+ if trevorEffect then return else trevorEffect = true end
+ debugPrint("^5Debug^7: ^3TrevorEffect^7() ^2activated")
+ AnimpostfxPlay("DrugsTrevorClownsFightIn", 3.0, 0)
+ Wait(3000)
+ AnimpostfxPlay("DrugsTrevorClownsFight", 3.0, 0)
+ Wait(30000)
+ AnimpostfxPlay("DrugsTrevorClownsFightOut", 3.0, 0)
+ AnimpostfxStop("DrugsTrevorClownsFight")
+ AnimpostfxStop("DrugsTrevorClownsFightIn")
+ AnimpostfxStop("DrugsTrevorClownsFightOut")
+ trevorEffect = false
+ debugPrint("^5Debug^7: ^3TrevorEffect^7() ^2stopped")
+end
+local turboEffect = false
+function TurboEffect()
+ if turboEffect then return else turboEffect = true end
+ debugPrint("^5Debug^7: ^3TurboEffect^7() ^2activated")
+ AnimpostfxPlay('RaceTurbo', 0, true)
+ SetTimecycleModifier('rply_motionblur')
+ ShakeGameplayCam('SKY_DIVING_SHAKE', 0.25)
+ Wait(30000)
+ StopGameplayCamShaking(true)
+ SetTransitionTimecycleModifier('default', 0.35)
+ Wait(1000)
+ ClearTimecycleModifier()
+ AnimpostfxStop('RaceTurbo')
+ turboEffect = false
+ debugPrint("^5Debug^7: ^3TurboEffect^7() ^2stopped")
+end
+local rampageEffect = false
+function RampageEffect()
+ if rampageEffect then return else rampageEffect = true end
+ debugPrint("^5Debug^7: ^3RampageEffect^7() ^2activated")
+ AnimpostfxPlay('Rampage', 0, true)
+ SetTimecycleModifier('rply_motionblur')
+ ShakeGameplayCam('SKY_DIVING_SHAKE', 0.25)
+ Wait(30000)
+ StopGameplayCamShaking(true)
+ SetTransitionTimecycleModifier('default', 0.35)
+ Wait(1000)
+ ClearTimecycleModifier()
+ AnimpostfxStop('Rampage')
+ rampageEffect = false
+ debugPrint("^5Debug^7: ^3RampageEffect^7() ^2stopped")
+end
+local focusEffect = false
+function FocusEffect()
+ if focusEffect then return else focusEffect = true end
+ debugPrint("^5Debug^7: ^3FocusEffect^7() ^2activated")
+ Wait(1000)
+ AnimpostfxPlay('FocusIn', 0, true)
+ Wait(30000)
+ AnimpostfxStop('FocusIn')
+ focusEffect = false
+ debugPrint("^5Debug^7: ^3FocusEffect^7() ^2stopped")
+end
+local nightVisionEffect = false
+function NightVisionEffect()
+ if nightVisionEffect then return else nightVisionEffect = true end
+ debugPrint("^5Debug^7: ^3NightVisionEffect^7() ^2activated")
+ SetNightvision(true)
+ Wait(math.random(3000, 4000)) -- FEEL FREE TO CHANGE THIS
+ SetNightvision(false)
+ SetSeethrough(false)
+ nightVisionEffect = false
+ debugPrint("^5Debug^7: ^3NightVisionEffect^7() ^2stopped")
+end
+local thermalEffect = false
+function ThermalEffect()
+ if thermalEffect then return else thermalEffect = true end
+ debugPrint("^5Debug^7: ^3ThermalEffect^7() ^2activated")
+ SetNightvision(true)
+ SetSeethrough(true)
+ Wait(math.random(2000, 3000)) -- FEEL FREE TO CHANGE THIS
+ SetNightvision(false)
+ SetSeethrough(false)
+ thermalEffect = false
+ debugPrint("^5Debug^7: ^3ThermalEffect^7() ^2stopped")
+end
+
+--Built-in Buff effects
+local healEffect = false
+function HealEffect(data)
+ if healEffect then return end
+ debugPrint("^5Debug^7: ^3HealEffect^7() ^2activated")
+ healEffect = true
+ local count = (data[1] / 1000)
+ while count > 0 do
+ Wait(1000)
+ count -= 1
+ SetEntityHealth(PlayerPedId(), GetEntityHealth(PlayerPedId()) + data[2])
+ end
+ healEffect = false
+ debugPrint("^5Debug^7: ^3HealEffect^7() ^2stopped")
+end
+
+local staminaEffect = false
+function StaminaEffect(data)
+ if staminaEffect then return end
+ debugPrint("^5Bridge^7: ^3StaminaEffect^7() ^2activated")
+ staminaEffect = true
+ local startStamina = (data[1] / 1000)
+ SetRunSprintMultiplierForPlayer(PlayerId(), 1.49)
+ while startStamina > 0 do
+ Wait(1000)
+ if math.random(5, 100) < 10 then RestorePlayerStamina(PlayerId(), data[2]) end
+ startStamina -= 1
+ if math.random(5, 100) < 51 then end
+ end
+ startStamina = 0
+ SetRunSprintMultiplierForPlayer(PlayerId(), 1.0)
+ staminaEffect = false
+ debugPrint("^5Bridge^7: ^3StaminaEffect^7() ^2stopped")
+end
+
+function StopEffects() -- Used to clear up any effects stuck on screen
+ debugPrint("^5Bridge^7: ^2All screen effects stopped")
+ ShakeGameplayCam('DRUNK_SHAKE', 0.0)
+ SetPedToRagdoll(PlayerPedId(), 5000, 1000, 1, 0, 0, 0)
+ ClearTimecycleModifier()
+ ResetScenarioTypesEnabled()
+ ResetPedMovementClipset(PlayerPedId(), 0)
+ SetPedIsDrunk(PlayerPedId(), false)
+ SetPedMotionBlur(PlayerPedId(), false)
+ SetNightvision(false)
+ SetSeethrough(false)
+ AnimpostfxStop("DrugsMichaelAliensFightIn")
+ AnimpostfxStop("DrugsMichaelAliensFight")
+ AnimpostfxStop("DrugsMichaelAliensFightOut")
+ AnimpostfxStop("DrugsTrevorClownsFight")
+ AnimpostfxStop("DrugsTrevorClownsFightIn")
+ AnimpostfxStop("DrugsTrevorClownsFightOut")
+ AnimpostfxStop('RaceTurbo')
+ AnimpostfxStop('FocusIn')
+ AnimpostfxStop('Rampage')
+end
\ No newline at end of file
diff --git a/shared/helpers.lua b/shared/helpers.lua
new file mode 100644
index 0000000..ade1bd7
--- /dev/null
+++ b/shared/helpers.lua
@@ -0,0 +1,830 @@
+--[[
+ Utility Functions for Resource Management and Debugging
+ ----------------------------------------------------------
+ This script provides a set of utility functions for managing resources,
+ debugging, and handling common tasks in the game environment.
+ It includes functions to check resource states, generate unique keys,
+ format numbers and coordinates, handle JSON data, perform raycasts, and more.
+]]
+
+-------------------------------------------------------------
+-- Resource and Environment Checks
+-------------------------------------------------------------
+
+--- Checks if a specific resource is started.
+--- @param script string The name of the resource.
+--- @return boolean boolean True if the resource state contains "start", false otherwise.
+---@usage
+--- ```lua
+--- if isStarted("myResource") then
+--- print("Resource is running")
+--- end
+--- ```
+function isStarted(script)
+ return GetResourceState(script):find("start") ~= nil
+end
+
+local scriptName = nil
+
+--- Retrieves the current resource name, caching it for efficiency.
+--- @return string string The current resource name.
+--- @usage
+--- ```lua
+--- local currentScript = getScript()
+--- print("Current script:", currentScript)
+--- ```
+function getScript()
+ if not scriptName then scriptName = GetCurrentResourceName() end
+ return scriptName
+end
+
+--- Determines if the current context is the server.
+--- @return boolean boolean True if running on the server, false otherwise.
+---@usage
+--- ```lua
+--- if isServer() then
+--- -- Server-specific code
+--- else
+--- -- Client-specific code
+--- end
+--- ```
+function isServer()
+ return IsDuplicityVersion()
+end
+
+-------------------------------------------------------------
+-- Debugging and JSON Utilities
+-------------------------------------------------------------
+
+--- Prints debug messages if debugMode is enabled.
+--- Concatenates all arguments and prints them with debug info.
+--- @param ... any One or more values to print.
+--- @usage
+--- ```lua
+--- debugPrint("Player has joined:", playerName)
+--- ```
+function debugPrint(...)
+ if debugMode then
+ local args = {...}
+ local output = table.concat(args, " ")
+ print(output, getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+end
+
+--- Prints event-related debug messages if event debugging is enabled.
+--- @param ... any One or more values to print.
+--- @usage
+--- ```lua
+--- eventPrint("Event triggered:", eventName)
+--- ```
+function eventPrint(...)
+ if Config.System.EventDebug then
+ print(...)
+ end
+end
+
+--- Returns the keys of a table in sorted order.
+--- @param tbl table The table to sort keys for.
+--- @return table table A sorted array of keys.
+function getSortedKeys(tbl)
+ local keys = {}
+ for k in pairs(tbl) do keys[#keys + 1] = k end
+ table.sort(keys, function(a, b)
+ local numA, numB = tonumber(a), tonumber(b)
+ if numA and numB then return numA < numB else return tostring(a) < tostring(b) end
+ end)
+ return keys
+end
+
+--- Recursively colorizes a table for debug printing.
+--- @param tbl table The table to colorize.
+--- @return table table A new table with colorized keys and values.
+--- ```lua
+--- local colorizedData = colorizeTable(myTable)
+--- jsonPrint(colorizedData)
+--- ```
+function colorizeTable(tbl)
+ local newData, sortedKeys = {}, getSortedKeys(tbl)
+ for _, k in ipairs(sortedKeys) do
+ local v = tbl[k]
+ newData["^6"..tostring(k).."^7"] =
+ (type(v) == "table" and colorizeTable(v))
+ or (tostring(type(v)):find("vector") and formatCoord(v))
+ or "^2"..tostring(v).."^7"
+ end
+ return newData
+end
+
+--- Encodes a table into an ordered JSON string with indentation.
+--- @param data table The table to encode.
+--- @param indent string The indentation string (e.g., " ").
+--- @param level number The current level of indentation.
+--- @return string The formatted JSON string.
+--- @usage
+--- ```lua
+--- local jsonString = encodeOrderedJSON(myTable, " ", 0)
+--- print(jsonString)
+--- ```
+function encodeOrderedJSON(data, indent, level)
+ local jsonParts, prefix, sortedKeys = {"{"}, string.rep(indent, level), getSortedKeys(data)
+ for i, k in ipairs(sortedKeys) do
+ jsonParts[#jsonParts + 1] = (i > 1 and ",\n" or "\n")..prefix..indent..json.encode(k)..": "
+ jsonParts[#jsonParts + 1] = (type(data[k]) == "table") and encodeOrderedJSON(data[k], indent, level + 1) or json.encode(data[k])
+ end
+ jsonParts[#jsonParts + 1] = "\n"..prefix.."}"
+ return table.concat(jsonParts)
+end
+
+--- Prints a table as a colorized and ordered JSON string if debugMode is enabled.
+--- @param data table The table to print.
+--- @usage
+--- ```lua
+--- jsonPrint(myTable)
+--- ```
+function jsonPrint(data)
+ if debugMode then
+ print(encodeOrderedJSON(colorizeTable(data), " ", 0), getDebugInfo(debug.getinfo(2, "nSl")))
+ end
+end
+
+--- Retrieves the current time formatted for debug prints.
+--- @return string string The formatted time string, e.g., "^7(14:23:45)".
+--- @usage
+--- ```lua
+--- local currentTime = GetPrintTime()
+--- debugPrint("Current Time:", currentTime)
+--- ```
+function GetPrintTime()
+ if isServer() then
+ 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
+ local _, _, _, hour, min, sec = GetLocalTime()
+ return "^7("..string.format("%02d", hour)..":"..string.format("%02d", min)..":"..string.format("%02d", sec)..")"
+ end
+end
+
+--- Generates a unique 3-character alphanumeric key.
+--- @return string string The generated key.
+--- @usage
+--- ```lua
+--- local uniqueKey = keyGen()
+--- print("Generated Key:", uniqueKey)
+--- ```
+function keyGen()
+ local charset = {
+ "q","w","e","r","t","y","u","i","o","p",
+ "a","s","d","f","g","h","j","k","l",
+ "z","x","c","v","b","n","m",
+ "Q","W","E","R","T","Y","U","I","O","P",
+ "A","S","D","F","G","H","J","K","L",
+ "Z","X","C","V","B","N","M",
+ "1","2","3","4","5","6","7","8","9","0"
+ }
+ local GeneratedID = ""
+ for i = 1, 3 do
+ GeneratedID = GeneratedID..charset[math.random(1, #charset)]
+ end
+ return GeneratedID
+end
+
+-------------------------------------------------------------
+-- Formatting and Vector Math Functions
+-------------------------------------------------------------
+
+--- Formats a number with commas as thousand separators.
+--- @param amount number The number to format.
+--- @return string string The formatted number.
+--- @usage
+--- ```lua
+--- local formattedNumber = cv(1000000) -- "1,000,000"
+--- print(formattedNumber)
+--- ``
+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)
+ end
+ return formatted
+end
+
+--- Formats a coordinate vector for debug printing.
+--- @param coord table A vector3 or vector4 with x, y, z (and optional w).
+--- @return string string The formatted coordinate string.
+--- @usage
+--- ```lua
+--- local formattedCoord = formatCoord(vector3(100.0, 200.0, 300.0))
+--- debugPrint("Player Position:", formattedCoord)
+--- ```
+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)"
+end
+
+--- Calculates the center point of a list of coordinates.
+--- @param tbl table An array of vector3 coordinates.
+--- @return vector3 vector3 The center coordinate.
+--- @usage
+--- ```lua
+--- local center = getCenterOfZones({vector3(100, 200, 300), vector3(110, 210, 310)})
+--- print("Center of Zones:", center)
+--- ```
+function getCenterOfZones(tbl)
+ local totalX, totalY, totalZ = 0, 0, 0
+ for _, coord in ipairs(tbl) do
+ totalX = totalX + coord.x
+ totalY = totalY + coord.y
+ totalZ = totalZ + coord.z
+ end
+ local count = #tbl
+ return vector3(totalX / count, totalY / count, totalZ / count)
+end
+
+--- Counts the number of keys in a table.
+--- @param tbl table The table to count.
+--- @return number number The key count.
+--- @usage
+--- ```lua
+--- local count = countTable(myTable)
+--- print("Number of keys:", count)
+--- ```
+function countTable(tbl)
+ local i = 0
+ for _ in pairs(tbl) do i = i + 1 end
+ return i
+end
+
+--- Returns an iterator over a table's keys in sorted order.
+--- @param t table The table to iterate over.
+--- @return function An iterator function for sorted keys.
+--- @usage
+--- ```lua
+--- for k, v in pairsByKeys(myTable) do
+--- print(k, v)
+--- end
+--- ```
+function pairsByKeys(t)
+ if not t then
+ 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
+end
+
+--- Creates a new table with consecutive numerical indices sorted by the 'id' field.
+--- @param originalTable table The table containing entries with an 'id' field.
+--- @return table table A sorted table with consecutive indices.
+--- @usage
+--- ```lua
+--- local sortedTable = createConsecutiveTable(originalTable)
+--- for i, entry in ipairs(sortedTable) do
+--- print(i, entry)
+--- 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
+ end
+ return newTable
+end
+
+--- Concatenates a table of strings into a single string separated by newlines.
+--- @param tbl table The table containing strings.
+--- @return string string The concatenated string.
+--- @usage
+--- ```lua
+--- local combinedText = concatenateText({"Line 1", "Line 2", "Line 3"})
+--- print(combinedText)
+--- ```
+function concatenateText(tbl)
+ local result = ""
+ for i = 1, #tbl do
+ result = result..tbl[i]..(i < #tbl and "\n" or "")
+ end
+ return result
+end
+
+--- Converts a rotation (degrees) to a direction vector.
+--- @param rot vector3 A vector3 with rotation values.
+--- @return vector3 vector3 The forward direction vector.
+--- @usage
+--- ```lua
+--- local direction = RotationToDirection({ z = 90 })
+--- print(direction)
+--- ```
+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
+
+--- Creates a basic progress bar string.
+--- @param percentage number Completion percentage (0-100).
+--- @return string string The progress bar (e.g., "█████░░░░░").
+--- @usage
+--- ```lua
+--- local bar = basicBar(50) -- "█████░░░░░"
+--- 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)
+end
+
+--- Normalizes a 3D vector.
+--- @param vec vector3 A vector3 table.
+--- @return vector3 vector3 A normalized vector.
+--- @usage
+--- ```lua
+--- local normalizedVec = normalizeVector(vector3(1, 2, 3))
+--- 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
+end
+
+-------------------------------------------------------------
+-- Drawing and Raycasting Functions
+-------------------------------------------------------------
+
+--- Draws a line between two coordinates (for debugging).
+--- @param startCoords vector3 The starting coordinate.
+--- @param endCoords vector3 The ending coordinate.
+--- @param col vector4 A vector4 specifying color and opacity.
+--- @usage
+--- ```lua
+--- 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
+end
+
+--- Draws a sphere at the specified coordinates (for debugging).
+--- @param coords vector3 The center of the sphere.
+--- @param col vector4 A vector4 specifying color and opacity.
+--- @usage
+--- ```lua
+--- 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
+end
+
+--- Performs a raycast between two coordinates and returns the results.
+--- @param startCoords vector3 The starting coordinate.
+--- @param endCoords vector3 The ending coordinate.
+--- @param entity number|nil An entity to ignore.
+--- @param flags number|nil Optional raycast flags (default: 4294967295).
+--- @return multiple Multiple values returned by GetShapeTestResultIncludingMaterial.
+--- @usage
+--- ```lua
+--- local hit, hitPos, material = PerformRaycast(startVec, endVec, playerPed, 1)
+--- if hit == 1 then
+--- print("Hit at position:", hitPos)
+--- print("Material:", material)
+--- 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
+ )
+ )
+ return val1, val2, val3, val4, val5, val6
+end
+
+--- Adjusts the Z-coordinate of a position to the ground level.
+--- @param coords vector4 A vector3 or vector4 with x, y, z (and optional w).
+--- @return vector3|vector4 vector The coordinates adjusted for ground level.
+--- @usage
+--- ```lua
+--- local groundCoords = adjustForGround(playerCoords)
+--- print("Ground Position:", groundCoords)
+--- ```
+function adjustForGround(coords)
+ local foundGround, zPos = 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
+ end
+end
+
+--- Ensures a network vehicle exists from its network ID.
+--- @param vehNetID number The network ID.
+--- @return number number The vehicle entity, or 0 if not found.
+--- @usage
+--- ```lua
+--- local vehicle = ensureNetToVeh(netID)
+--- if vehicle ~= 0 then
+--- print("Vehicle exists:", vehicle)
+--- 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
+end
+
+--- Ensures a network entity exists from its network ID.
+--- @param entNetID number The network ID.
+--- @return number number The entity, or 0 if not found.
+--- @usage
+--- ```lua
+--- local entity = ensureNetToEnt(netID)
+--- if entity ~= 0 then
+--- print("Entity exists:", entity)
+--- 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
+end
+
+function sendLog(text)
+ local Player = getPlayer()
+ local coords = GetEntityCoords(PlayerPedId())
+ local _, _, _, hour, min, sec = GetLocalTime()
+ local data = {
+ script = debug.getinfo(2, "nSl"),
+ coords = coords,
+ localTime = { hour = hour, min = min, sec = sec },
+ firstname = Player.firstname,
+ lastname = Player.lastname,
+ source = Player.source,
+ id = Player.citizenId,
+ text = text,
+ }
+
+ debugPrint("^5Log Message^7: "..getScript().." - "..Player.firstname.." "..Player.lastname.."("..Player.source..") ["..Player.citizenId.."]", text)
+ TriggerServerEvent(getScript()..":server:sendlog", data)
+end
+
+function sendServerLog(data)
+ local hour, min, sec = os.date('%H'), os.date('%M'), os.date('%S')
+ data.serverTime = { house = hour, min = min, sec = sec }
+ --jsonPrint(data)
+ debugPrint("^5Log Message^7: "..getScript().." - "..data.firstname.." "..data.lastname.."("..data.source..") ["..data.id.."]", data.text)
+
+ -- Add your logger here
+
+end
+
+RegisterNetEvent(getScript()..":server:sendlog", sendServerLog)
+
+-------------------------------------------------------------
+-- Material and Prop Functions
+-------------------------------------------------------------
+
+--- A table mapping material names to their corresponding hash values.
+---
+--- Used for identifying materials based on hash codes.
+local materials = {
+ none = -1,
+ Unknown = -1775485061,
+ concrete = 1187676648,
+ concrete_pothole = 359120722,
+ concrete_dusty = -1084640111,
+ tarmac = 282940568,
+ tarmac_painted = -1301352528,
+ tarmac_pothole = 1886546517,
+ rumble_strip = -250168275,
+ breeze_block = -954112554,
+ rock = -840216541,
+ rock_mossy = -124769592,
+ stone = 765206029,
+ cobblestone = 576169331,
+ brick = 1639053622,
+ marble = 1945073303,
+ paving_slab = 1907048430,
+ sandstone_solid = 592446772,
+ sandstone_brittle = 1913209870,
+ sand_loose = -1595148316,
+ sand_compact = 510490462,
+ sand_wet = 909950165,
+ sand_track = -1907520769,
+ sand_underwater = -1136057692,
+ sand_dry_deep = 509508168,
+ sand_wet_deep = 1288448767,
+ ice = -786060715,
+ ice_tarmac = -1931024423,
+ snow_loose = -1937569590,
+ snow_compact = -878560889,
+ snow_deep = 1619704960,
+ snow_tarmac = 1550304810,
+ gravel_small = 951832588,
+ gravel_large = 2128369009,
+ gravel_deep = -356706482,
+ gravel_train_track = 1925605558,
+ dirt_track = -1885547121,
+ mud_hard = -1942898710,
+ mud_pothole = 312396330,
+ mud_soft = 1635937914,
+ mud_underwater = -273490167,
+ mud_deep = 1109728704,
+ marsh = 223086562,
+ marsh_deep = 1584636462,
+ soil = -700658213,
+ clay_hard = 1144315879,
+ clay_soft = 560985072,
+ grass_long = -461750719,
+ grass = 1333033863,
+ grass_short = -1286696947,
+ hay = -1833527165,
+ bushes = 581794674,
+ twigs = -913351839,
+ leaves = -2041329971,
+ woodchips = -309121453,
+ tree_bark = -1915425863,
+ metal_solid_small = -1447280105,
+ metal_solid_medium = -365631240,
+ metal_solid_large = 752131025,
+ metal_hollow_small = 15972667,
+ metal_hollow_medium = 1849540536,
+ metal_hollow_large = -583213831,
+ metal_chainlink_small = 762193613,
+ metal_chainlink_large = 125958708,
+ metal_corrugated_iron = 834144982,
+ metal_grille = -426118011,
+ metal_railing = 2100727187,
+ metal_duct = 1761524221,
+ metal_garage_door = -231260695,
+ metal_manhole = -754997699,
+ wood_solid_small = -399872228,
+ wood_solid_medium = 555004797,
+ wood_solid_large = 815762359,
+ wood_solid_polished = 126470059,
+ wood_floor_dusty = -749452322,
+ wood_hollow_small = 1993976879,
+ wood_hollow_medium = -365476163,
+ wood_hollow_large = -925419289,
+ wood_chipboard = 1176309403,
+ wood_old_creaky = 722686013,
+ wood_high_density = -1742843392,
+ wood_lattice = 2011204130,
+ ceramic = -1186320715,
+ roof_tile = 1755188853,
+ roof_felt = -1417164731,
+ fibreglass = 1354180827,
+ tarpaulin = -642658848,
+ plastic = -2073312001,
+ plastic_hollow = 627123000,
+ plastic_high_density = -1625995479,
+ plastic_clear = -1859721013,
+ plastic_hollow_clear = 772722531,
+ plastic_high_density_clear = -1338473170,
+ fibreglass_hollow = -766055098,
+ rubber = -145735917,
+ rubber_hollow = -783934672,
+ linoleum = 289630530,
+ laminate = 1845676458,
+ carpet_solid = 669292054,
+ carpet_solid_dusty = 158576196,
+ carpet_floorboard = -1396484943,
+ cloth = 122789469,
+ plaster_solid = -574122433,
+ plaster_brittle = -251888898,
+ cardboard_sheet = 236511221,
+ cardboard_box = -1409054440,
+ paper = 474149820,
+ foam = 808719444,
+ feather_pillow = 1341866303,
+ polystyrene = -1756927331,
+ leather = -570470900,
+ tvscreen = 1429989756,
+ slatted_blinds = 673696729,
+ glass_shoot_through = 937503243,
+ glass_bulletproof = 244521486,
+ glass_opaque = 1500272081,
+ perspex = -1619794068,
+ car_metal = -93061983,
+ car_plastic = 2137197282,
+ car_softtop = -979647862,
+ car_softtop_clear = 2130571536,
+ car_glass_weak = 1247281098,
+ car_glass_medium = 602884284,
+ car_glass_strong = 1070994698,
+ car_glass_bulletproof = -1721915930,
+ car_glass_opaque = 513061559,
+ water = 435688960,
+ blood = 5236042,
+ oil = -634481305,
+ petrol = -1634184340,
+ fresh_meat = 868733839,
+ dried_meat = -1445160429,
+ emissive_glass = 1501078253,
+ emissive_plastic = 1059629996,
+ vfx_metal_electrified = -309134265,
+ vfx_metal_water_tower = 611561919,
+ vfx_metal_steam = -691277294,
+ vfx_metal_flame = 332778253,
+ phys_no_friction = 1666473731,
+ phys_golf_ball = -1693813558,
+ phys_tennis_ball = -256704763,
+ phys_caster = -235302683,
+ phys_caster_rusty = 2016463089,
+ phys_car_void = 1345867677,
+ phys_ped_capsule = -291631035,
+ phys_electric_fence = -1170043733,
+ phys_electric_metal = -2013761145,
+ phys_barbed_wire = -1543323456,
+ phys_pooltable_surface = 605776921,
+ phys_pooltable_cushion = 972939963,
+ phys_pooltable_ball = -748341562,
+ buttocks = 483400232,
+ thigh_left = -460535871,
+ shin_left = 652772852,
+ foot_left = 1926285543,
+ thigh_right = -236981255,
+ shin_right = -446036155,
+ foot_right = -1369136684,
+ spine0 = -1922286884,
+ spine1 = -1140112869,
+ spine2 = 1457572381,
+ spine3 = 32752644,
+ clavicle_left = -1469616465,
+ upper_arm_left = -510342358,
+ lower_arm_left = 1045062756,
+ hand_left = 113101985,
+ clavicle_right = -1557288998,
+ upper_arm_right = 1501153539,
+ lower_arm_right = 1777921590,
+ hand_right = 2000961972,
+ neck = 1718294164,
+ head = -735392753,
+ animal_default = 286224918,
+ car_engine = -1916939624,
+ puddle = 999829011,
+ concrete_pavement = 2015599386,
+ brick_pavement = -1147361576,
+ phys_dynamic_cover_bound = -2047468855,
+ vfx_wood_beer_barrel = 998201806,
+ wood_high_friction = -2140087047,
+ rock_noinst = 127813971,
+ bushes_noinst = 1441114862,
+ metal_solid_road_surface = -729112334,
+ stunt_ramp_surface = -2088174996,
+ temp_01 = 746881105,
+ temp_02 = -1977970111,
+ temp_03 = 1911121241,
+ temp_04 = 1923995104,
+ temp_05 = -1393662448,
+ temp_06 = 1061250033,
+ temp_07 = -1765523682,
+ temp_08 = 1343679702,
+ temp_09 = 1026054937,
+ temp_10 = 63305994,
+ temp_11 = 47470226,
+ temp_12 = 702596674,
+ temp_13 = -1637485913,
+ temp_14 = -645955574,
+ temp_15 = -1583997931,
+ temp_16 = -1512735273,
+ temp_17 = 1011960114,
+ temp_18 = 1354993138,
+ temp_19 = -801804446,
+ temp_20 = -2052880405,
+ temp_21 = -1037756060,
+ temp_22 = -620388353,
+ temp_23 = 465002639,
+ temp_24 = 1963820161,
+ temp_25 = 1952288305,
+ temp_26 = -1116253098,
+ temp_27 = 889255498,
+ temp_28 = -1179674098,
+ temp_29 = 1078418101,
+ temp_30 = 13626292
+}
+
+--- Retrieves the ground material at a given position.
+--- @param coords vector3 The coordinate to test.
+--- @return number|nil number The material hash if hit, nil otherwise.
+--- @return string string The material name.
+--- @usage
+--- ```lua
+--- 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)
+ local _, hit, _, _, materialHash, _ = GetShapeTestResultIncludingMaterial(rayHandle)
+ local materialName = "Unknown"
+ for k, v in pairs(materials) do
+ if v == materialHash then
+ materialName = k
+ break
+ end
+ end
+ if hit then return materialHash, materialName else return nil, materialName end
+end
+
+--- Retrieves the dimensions (width, depth, height) of a prop/model.
+--- @param model string The name or hash of the model.
+--- @return number number The width of the prop.
+--- @return number number The depth of the prop.
+--- @return number number The height of the prop.
+---
+--- @usage
+--- ```lua
+--- local width, depth, height = GetPropDimensions("prop_barrel_01a")
+--- print("Dimensions:", width, depth, height)
+--- ```
+function GetPropDimensions(model)
+ loadModel(model)
+ local minDim, maxDim = GetModelDimensions(model)
+ local width, depth, height = maxDim.x - minDim.x, maxDim.y - minDim.y, maxDim.z - minDim.z
+
+ return width, depth, height
+end
+
+--- Retrieves the forward direction vector of an entity based on its heading.
+---
+--- This function calculates the forward direction vector using the entity's heading angle.
+---
+--- @param entity number The entity whose forward vector is to be calculated.
+--- @return vector3 vector3 The forward direction vector.
+---
+--- @usage
+--- ```lua
+--- local forwardVec = GetEntityForwardVector(playerPed)
+--- print("Forward Vector:", forwardVec)
+--- ```
+function GetEntityForwardVector(entity)
+ local heading = math.rad(GetEntityHeading(entity) + 90)
+ return vec3(math.cos(heading), math.sin(heading), 0.0)
+end
\ No newline at end of file
diff --git a/shared/input.lua b/shared/input.lua
new file mode 100644
index 0000000..579cdb5
--- /dev/null
+++ b/shared/input.lua
@@ -0,0 +1,241 @@
+--[[
+ Input Dialog Module
+ ---------------------
+ This module provides a function to create a simple input dialog compatible with
+ multiple menu systems (OX, QB, GTA/WarMenu, and ESX). It supports various input
+ types such as radio buttons, numbers, text, and select dropdowns.
+
+]]
+
+--- Creates a simple input dialog using the configured menu system.
+---
+--- @param title string The title or header of the input dialog.
+--- @param opts table A table of input option definitions. Each option should include:
+--- - type (string): The input type ("radio", "number", "text", "select").
+--- - label (string, optional): A label for the input (used in radio/select for OX).
+--- - text (string, optional): The text prompt for the input.
+--- - name (string): The identifier for the input.
+--- - isRequired (boolean, optional): Whether input is mandatory.
+--- - default (any, optional): The default value.
+--- - options (table, optional): A table of choices for "radio" and "select" types.
+--- - min (number, optional): Minimum value (for "number" and "select").
+--- - max (number, optional): Maximum value.
+--- - txt (string, optional): Additional description.
+---
+--- @return table|nil table Returns the user's input as a table if submitted, otherwise nil.
+---
+---@usage
+--- ```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" },
+--- }},
+--- })
+--- ```
+function createInput(title, opts)
+ local dialog = nil
+ local options = {}
+ local currentNum = 0
+ if Config.System.Menu == "ox" then
+ for i = 1, #opts do
+ currentNum += 1
+ if opts[i] == nil then currentNum -= 1 goto skip end
+ if opts[i].type == "radio" then
+ -- Convert radio options to select type for OX
+ for k in pairs(opts[i].options) do
+ opts[i].options[k].label = opts[i].options[k].text
+ end
+ options[currentNum] = {
+ type = "select",
+ isRequired = 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)..(opts[i].txt and " - "..opts[i].txt or ""),
+ isRequired = 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].text..(opts[i].txt and " - "..opts[i].txt or ""),
+ default = opts[i].default,
+ isRequired = opts[i].isRequired,
+ }
+ end
+ if opts[i].type == "select" then
+ options[currentNum] = {
+ type = opts[i].type,
+ label = opts[i].text..(opts[i].txt and " - "..opts[i].txt or ""),
+ isRequired = 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
+ jsonPrint(opts[i])
+ 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,
+ isRequired = 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,
+ isRequired = opts[i].required,
+ min = opts[i].min,
+ max = opts[i].max,
+ default = opts[i].default,
+ }
+ end
+ ::skip::
+ end
+ dialog = exports[OXLibExport]:inputDialog(title, options)
+ return dialog
+ elseif Config.System.Menu == "qb" then
+ for k, v in pairs(opts) do
+ currentNum += 1
+ if opts[k] == nil then
+ currentNum -= 1
+ else
+ options[currentNum] = opts[k]
+ end
+ end
+ dialog = exports['qb-input']:ShowInput(
+ { header = title, submitText = "Accept", inputs = options }
+ )
+ return dialog
+
+ elseif Config.System.Menu == "gta" then
+ WarMenu.CreateMenu(tostring(opts),
+ title,
+ " ",
+ {
+ titleColor = { 222, 255, 255 },
+ maxOptionCountOnScreen = 15,
+ width = 0.25,
+ x = 0.7,
+ })
+ if WarMenu.IsAnyMenuOpened() then return end
+ WarMenu.OpenMenu(tostring(opts))
+
+ local close = true
+ local _comboBoxItems = {}
+ local _comboBoxIndex = { 1, 1 }
+
+ while true do
+ if WarMenu.Begin(tostring(opts)) then
+ for i = 1, #opts do
+ if opts[i].type == "radio" then
+ for k in pairs(opts[i].options) do
+ if not _comboBoxItems[i] then _comboBoxItems[i] = {} end
+ _comboBoxItems[i][k] = opts[i].options[k].text
+ end
+ local _, comboBoxIndex = WarMenu.ComboBox(opts[i].label, _comboBoxItems[i], _comboBoxIndex[i])
+ if _comboBoxIndex[i] ~= comboBoxIndex then
+ _comboBoxIndex[i] = comboBoxIndex
+ end
+ end
+ if opts[i].type == "number" then
+ for b = 1, opts[i].max do
+ if not _comboBoxItems[i] then _comboBoxItems[i] = {} end
+ _comboBoxItems[i][b] = b
+ end
+ local _, comboBoxIndex = WarMenu.ComboBox(opts[i].text, _comboBoxItems[i], _comboBoxIndex[i])
+ if _comboBoxIndex[i] ~= comboBoxIndex then
+ _comboBoxIndex[i] = comboBoxIndex
+ end
+ end
+ end
+ local pressed = WarMenu.Button("Pay")
+ if pressed then
+ WarMenu.CloseMenu()
+ close = false
+ local result = {}
+ for i = 1, #_comboBoxIndex do
+ result[i] = _comboBoxItems[i][_comboBoxIndex[i]]
+ end
+ return result
+ end
+ WarMenu.End()
+ else
+ return
+ end
+ if not WarMenu.IsAnyMenuOpened() and close then
+ if data.onExit then data.onExit() end
+ end
+ Wait(0)
+ end
+ elseif Config.System.Menu == "esx" then -- horrible input dialog, not even worth using, get OX
+
+ local results = {}
+ for i, opt in ipairs(opts) do
+ local prompt = opt.text or opt.label or "Enter value"
+ -- For radio/select types, append available options in the prompt.
+ if (opt.type == "radio" or opt.type == "select") and opt.options then
+ local choices = ""
+ for j, choice in ipairs(opt.options) do
+ choices = choices .. choice.text .. " (" .. tostring(choice.value) .. ")"
+ if j < #opt.options then choices = choices .. ", " end
+ end
+ prompt = prompt .. " [" .. choices .. "]"
+ elseif opt.type == "number" then
+ prompt = prompt .. " (number between " .. (opt.min or 0) .. " and " .. (opt.max or 100) .. ")"
+ end
+
+ local value = nil
+ ESX.UI.Menu.Open('dialog', getScript(), 'input_' .. i, {
+ title = prompt
+ }, function(data, menu)
+ value = data.value
+ menu.close()
+ end, function(data, menu)
+ menu.close()
+ end)
+
+ -- Wait until the player submits a value.
+ while value == nil do
+ Wait(0)
+ end
+
+ -- Convert to a number if needed.
+ if opt.type == "number" then
+ value = tonumber(value)
+ end
+ results[opt.name or i] = value
+ end
+ return results
+ end
+end
\ No newline at end of file
diff --git a/shared/inventories.lua b/shared/inventories.lua
new file mode 100644
index 0000000..348cb65
--- /dev/null
+++ b/shared/inventories.lua
@@ -0,0 +1,163 @@
+-------------------------------------------------------------
+-- 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)
+ 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()
+ 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(src)
+ else
+ grabInv = exports[CodeMInv]:GetClientPlayerInventory()
+ 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
\ No newline at end of file
diff --git a/shared/isAnimal.lua b/shared/isAnimal.lua
new file mode 100644
index 0000000..1de037c
--- /dev/null
+++ b/shared/isAnimal.lua
@@ -0,0 +1,284 @@
+--[[
+ Animal Detection Module
+ -------------------------
+ This module determines whether a Ped is an animal and categorizes it as a cat, dog,
+ 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
+ current animal classification.
+
+ When running client-side (not on the server), the module checks the player's Ped after they load.
+
+ Usage Examples:
+ -- Check if the player's Ped is an animal:
+ local animalStatus = isPedAnimal()
+
+ -- Check if a given Ped is a cat:
+ if isCat(somePed) then print("This is a cat!") end
+
+ -- Determine if a Ped is a dog and whether it's big or small:
+ local isDogFlag, isBig = isDog(somePed)
+
+ -- Retrieve a flat list of all animal model hashes:
+ local allAnimalModels = getAnimalModels()
+
+]]
+
+-- Global animal classification flags.
+isCat, isDog, isBigDog, isSmallDog, isCoyote, isAnimal = false, false, false, false, false, false
+
+if not isServer() then
+ onPlayerLoaded(function()
+ Wait(2000)
+ -- Reset classification flags
+ isCat, isDog, isBigDog, isSmallDog, isCoyote = false, false, false, false, false
+ -- Check if the player's Ped is an animal.
+ isPedAnimal()
+ if isAnimal then
+ local ped = PlayerPedId()
+ local pedModel = GetEntityModel(ped)
+
+ -- 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`)
+
+ -- Determine if the Ped is a dog and whether it's big:
+ isDog, isBigDog = isDog(ped)
+ isSmallDog = not isBigDog
+ if isDog and pedModel == `a_c_coyote` then isDog = 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
+ end
+ end, true)
+
+
+ -------------------------------------------------------------
+ -- Animal Classification Functions
+ -------------------------------------------------------------
+
+ --- Determines whether a given Ped is classified as an animal.
+ ---
+ --- Checks if the Ped's model hash appears in any of the animal categories defined in AnimalPeds.
+ ---
+ --- @param ped number|nil Optional. The Ped entity to check. Defaults to the player's Ped.
+ --- @return boolean boolean True if the Ped is an animal, otherwise false.
+ ---
+ --- @usage
+ --- ```lua
+ --- local isPlayerAnimal = isAnimal()
+ --- local isSpecificPedAnimal = isAnimal(somePedEntity)
+ --- ```
+ 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")
+ return true
+ end
+ end
+ end
+ return false
+ end
+
+ --- Checks if a given Ped is classified as a cat.
+ ---
+ --- Iterates through the CatPeds table and returns true if the Ped's model matches.
+ ---
+ --- @param ped number|nil Optional. The Ped entity to check. Defaults to the player's Ped.
+ --- @return boolean True if the Ped is a cat, otherwise false.
+ ---
+ ---@usage
+ --- ```lua
+ --- if isCat() then
+ --- print("Player is a cat!")
+ --- end
+ ---
+ --- local anotherPed = GetPedInVehicleSeat(vehicle, -1)
+ --- if isCat(anotherPed) then
+ --- print("Driver is a cat!")
+ --- end
+ --- ```
+ function isCat(ped)
+ local PedModel = GetEntityModel(ped or PlayerPedId())
+ for modelHash, _ in pairs(AnimalPeds.CatPeds) do
+ if PedModel == modelHash then
+ return true
+ end
+ end
+ return false
+ end
+
+ --- Determines if a given Ped is a dog and identifies its size category.
+ ---
+ --- Checks the BigDogs and SmallDogs tables to see if the Ped's model matches any dog model.
+ ---
+ --- @param ped number|nil Optional. The Ped entity to check. Defaults to the player's Ped.
+ ---@return boolean, boolean|nil boolean Returns `true` and `true` if the Ped is a big dog,
+ --- `true` and `false` if it's a small dog,
+ --- or `false` and `nil` if it's not a dog.
+ ---
+ ---@usage
+ --- ```lua
+ --- local isDog, isBigDog = isDog()
+ --- if isDog then
+ --- if isBigDog then
+ --- print("Player is a big dog!")
+ --- else
+ --- print("Player is a small dog!")
+ --- end
+ --- else
+ --- print("Player is not a dog.")
+ --- end
+ ---
+ --- local somePed = GetPedInVehicleSeat(vehicle, 0)
+ --- local isPetDog, isLargeDog = isDog(somePed)
+ --- if isPetDog then
+ --- if isLargeDog then
+ --- print("Passenger is a big dog!")
+ --- else
+ --- print("Passenger is a small dog!")
+ --- end
+ --- end
+ --- ```
+ function isDog(ped)
+ local PedModel = GetEntityModel(ped or PlayerPedId())
+ for modelHash, _ in pairs(AnimalPeds.BigDogs) do
+ if PedModel == modelHash then
+ return true, true
+ end
+ end
+ for modelHash, _ in pairs(AnimalPeds.SmallDogs) do
+ if PedModel == modelHash then
+ return true, false
+ end
+ end
+ return false, nil
+ end
+
+ --- Compiles and returns a flat table of all animal model hashes.
+ ---
+ --- Iterates through every category in AnimalPeds and collects all model hashes.
+ ---
+ --- @return table table A table containing all animal model hashes.
+ ---
+ ---@usage
+ --- ```lua
+ --- local allAnimalModels = getAnimalModels()
+ --- for _, modelHash in ipairs(allAnimalModels) do
+ --- print("Animal Model Hash:", modelHash)
+ --- end
+ --- ```
+ function getAnimalModels()
+ local animalModels = {}
+ for _, animalCategory in pairs(AnimalPeds) do
+ for modelHash, _ in pairs(animalCategory) do
+ table.insert(animalModels, modelHash)
+ end
+ end
+ return animalModels
+ end
+
+ --- Compiles and returns a table of animal animations for the ped model.
+ ---
+ --- Iterates through every category in AnimalPeds and collects all anims.
+ ---
+ --- @return table table A table containing all current model anims.
+ ---
+ ---@usage
+ --- ```lua
+ --- local getAnim = getAnimalAnims(ped)
+ --- playAnim(getAnim.sitDict, getAnim.sitAnim, -1, 1)
+ --- ```
+ function getAnimalAnims(ped)
+ local model = GetEntityModel(ped)
+ local animalTable = {}
+ for _, animalCategory in pairs(AnimalPeds) do
+ for k, v in pairs(animalCategory) do
+ if k == model then
+ animalTable = v
+ break
+ end
+ end
+ end
+ return animalTable
+ end
+end
+
+-------------------------------------------------------------
+-- Animal Models Data
+-------------------------------------------------------------
+-- Define the animal models and their associated animations.
+AnimalPeds = {
+ BigDogs = {
+ [`a_c_chop`] = { deathAnim = "dead_right", deathDict = "creatures@chop@move", exitAnim = "getup_r", exitDict = "creatures@chop@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_k9`] = { deathAnim = "dead_right", deathDict = "creatures@chop@move", exitAnim = "getup_r", exitDict = "creatures@chop@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_husky`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_retriever`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_shepherd`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_rottweiler`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-aushep`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`golden_r`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-dobermanv2`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`doberman`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-gs`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`k9_husky`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-bloodhound`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`bernard`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-pterrier`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-labrador`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`dane`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft_malinois`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`abdog`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`dalmatian`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_dalmatian`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-boxer`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`ft-bs`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`chowchow`] = { deathAnim = "dead_right", deathDict = "creatures@rottweiler@move", exitAnim = "getup_r", exitDict = "creatures@rottweiler@getup", sitAnim = "sit", sitDict = "creatures@rottweiler@in_vehicle@std_car" },
+ [`a_c_coyote`] = { deathAnim = "dead_right", deathDict = "creatures@coyote@move", exitAnim = "getup_r", exitDict = "creatures@coyote@getup", sitAnim = "base", sitDict = "creatures@coyote@amb@world_coyote_howl@base" },
+ [`a_c_coyote_02`] = { deathAnim = "dead_right", deathDict = "creatures@coyote@move", exitAnim = "getup_r", exitDict = "creatures@coyote@getup", sitAnim = "base", sitDict = "creatures@coyote@amb@world_coyote_howl@base" },
+ },
+ SmallDogs = {
+ [`a_c_poodle`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ [`ft-chihuahua`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ [`a_c_pug`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ [`a_c_pug_02`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ [`a_c_westy`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ [`ft-pretriever`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ [`ft-shepk9`] = { deathAnim = "dead_right", deathDict = "creatures@pug@move", exitAnim = "getup_r", exitDict = "creatures@pug@getup", sitAnim = "base", sitDict = "creatures@pug@amb@world_dog_sitting@base" },
+ },
+ CatPeds = {
+ [`bshorthair`] = { deathAnim = "dead_right", deathDict = "creatures@cat@move", exitAnim = "getup_r", exitDict = "creatures@cat@getup", sitAnim = "base", sitDict = "creatures@cat@amb@world_cat_sleeping_ledge@base" },
+ [`a_c_cat_01`] = { deathAnim = "dead_right", deathDict = "creatures@cat@move", exitAnim = "getup_r", exitDict = "creatures@cat@getup", sitAnim = "base", sitDict = "creatures@cat@amb@world_cat_sleeping_ledge@base" },
+ [`ft-sphynx`] = { deathAnim = "dead_right", deathDict = "creatures@coyote@move", exitAnim = "getup_r", exitDict = "creatures@coyote@getup", sitAnim = "base", sitDict = "creatures@coyote@amb@world_coyote_howl@base" },
+ },
+ OtherPeds = {
+ [`ft-raccoon`] = { deathAnim = "dead_right", deathDict = "creatures@cat@move", exitAnim = "getup_r", exitDict = "creatures@cat@getup", sitAnim = "base", sitDict = "creatures@cat@amb@world_cat_sleeping_ledge@base" },
+ [`a_c_hen`] = { deathAnim = "dead_right", deathDict = "creatures@hen@move", exitAnim = "getup_r", exitDict = "creatures@hen@getup" },
+ [`a_c_rabbit_01`] = { deathAnim = "dead_right", deathDict = "creatures@rabbit@move", exitAnim = "getup_r", exitDict = "creatures@rabbit@getup", sitAnim = "idle_c", sitDict = "creatures@coyote@amb@world_coyote_howl@base" },
+ [`a_c_rabbit_02`] = { deathAnim = "dead_right", deathDict = "creatures@rabbit@move", exitAnim = "getup_r", exitDict = "creatures@rabbit@getup", sitAnim = "idle_c", sitDict = "creatures@coyote@amb@world_coyote_howl@base" },
+ [`a_c_rat`] = { deathAnim = "dead_right", deathDict = "creatures@rat@move", exitAnim = "getup_r", exitDict = "creatures@rat@getup" },
+ [`a_c_deer`] = { deathAnim = "dead_right", deathDict = "creatures@deer@move", exitAnim = "getup_r", exitDict = "creatures@deer@getup" },
+ [`a_c_boar`] = { deathAnim = "dead_right", deathDict = "creatures@boar@move", exitAnim = "getup_r", exitDict = "creatures@boar@getup" },
+ [`a_c_boar_02`] = { deathAnim = "dead_right", deathDict = "creatures@boar@move", exitAnim = "getup_r", exitDict = "creatures@boar@getup" },
+ [`a_c_chicken`] = { deathAnim = "dead_right", deathDict = "creatures@chicken@move", exitAnim = "getup_r", exitDict = "creatures@chicken@getup" },
+ [`a_c_pig`] = { deathAnim = "dead_right", deathDict = "creatures@pig@move", exitAnim = "getup_r", exitDict = "creatures@pig@getup" },
+ [`a_c_sharkhammer`] = { deathAnim = "dead_right", deathDict = "creatures@sharkhammer@move", exitAnim = "getup_r", exitDict = "creatures@sharkhammer@getup" },
+ [`a_c_sharktiger`] = { deathAnim = "dead_right", deathDict = "creatures@sharktiger@move", exitAnim = "getup_r", exitDict = "creatures@sharktiger@getup" },
+ [`a_c_crow`] = { deathAnim = "dead_down", deathDict = "creatures@crow@move", exitAnim = "nill", exitDict = "creatures@pug@move" },
+ [`a_c_pigeon`] = { deathAnim = "dead_down", deathDict = "creatures@pigeon@move", exitAnim = "nill", exitDict = "creatures@pug@move" },
+ },
+ Monekys = {
+ [`ft-chimpanzee`] = { deathAnim = "dead", deathDict = "dead_a", exitAnim = "get_up@sat_on_floor@to_stand", exitDict = "getup_0" },
+ [`a_c_chimp`] = { deathAnim = "dead", deathDict = "dead_a", exitAnim = "get_up@sat_on_floor@to_stand", exitDict = "getup_0" },
+ [`a_c_chimp_02`] = { deathAnim = "dead", deathDict = "dead_a", exitAnim = "get_up@sat_on_floor@to_stand", exitDict = "getup_0" },
+ [`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" },
+ }
+}
\ No newline at end of file
diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua
new file mode 100644
index 0000000..7b5bce8
--- /dev/null
+++ b/shared/itemcontrol.lua
@@ -0,0 +1,725 @@
+--[[
+ Usable Items & Inventory Utilities Module
+ -------------------------------------------
+ This module provides functions for:
+ • Registering items as usable across different inventory systems (ESX, QBcore, QBX).
+ • Retrieving an item's image as a NUI link.
+ • Adding and removing items from a player's inventory.
+ • Toggling items in inventory (with server event for exploit protection).
+ • Checking for item duplication exploits.
+ • Handling tool durability mechanics.
+ • Checking item availability and retrieving inventory.
+ • Granting random rewards from a reward pool.
+ • Checking if a player can carry specific items based on weight.
+]]
+validTokens = {}
+
+-------------------------------------------------------------
+-- Registering Usable Items
+-------------------------------------------------------------
+
+--- Registers an item as usable for ESX, QBcore, or QBX.
+---
+--- @param item string The name of the item.
+--- @param funct function The function to execute when the item is used.
+---
+--- @usage
+--- ```lua
+--- createUseableItem("health_potion", function(source)
+--- -- Code to consume the health potion
+--- end)
+--- ```
+function createUseableItem(item, funct)
+ if isStarted(ESXExport) then
+ debugPrint("^6Bridge^7: ^2Registering item as ^3Usable^2 with ^7es_extended", item)
+ while not ESX do Wait(0) end
+ ESX.RegisterUsableItem(item, funct)
+ elseif isStarted(QBExport) and not isStarted(QBXExport) then
+ debugPrint("^6Bridge^7: ^2Registering item as ^3Usable^2 with ^7qb-core", item)
+ Core.Functions.CreateUseableItem(item, funct)
+ elseif isStarted(QBXExport) then
+ debugPrint("^6Bridge^7: ^2Registering item as ^3Usable^2 with ^7qbx_core", item)
+ exports[QBXExport]:CreateUseableItem(item, funct)
+ else
+ print("^4ERROR^7: No supported framework detected for registering usable item: ^3"..item.."^7")
+ 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(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
+
+-------------------------------------------------------------
+-- Adding and Removing Items
+-------------------------------------------------------------
+
+--- Adds an item to a player's inventory.
+---
+--- Triggers a server event (or local event) to add the specified item.
+---
+--- @param item string The item name.
+--- @param amount number The quantity to add.
+--- @param info table|nil Additional metadata for the item.
+--- @param src number|nil Optional player source; if nil, defaults to the caller.
+---
+--- @usage
+--- ```lua
+--- addItem("health_potion", 2, { quality = "high" })
+--- ```
+function addItem(item, amount, info, src)
+ if not Items[item] then
+ print("^6Bridge^7: ^1Error^7 - ^2Tried to give ^7'^3"..item.."^7'^2 but it doesn't exist")
+ return
+ end
+
+ if src then
+ TriggerEvent(getScript()..":server:toggleItem", true, item, amount, src, info)
+ else
+ TriggerServerEvent(getScript()..":server:toggleItem", true, item, amount, nil, info, nil, currentToken)
+ currentToken = nil -- clear client cached token
+ end
+end
+
+--- Removes an item from a player's inventory.
+---
+--- Triggers a server event (or local event) to remove the specified item.
+---
+--- @param item string The item name.
+--- @param amount number The quantity to remove.
+--- @param src number|nil Optional player source.
+--- @param slot number|nil Optional inventory slot.
+---
+--- @usage
+--- ```lua
+--- removeItem("health_potion", 1)
+--- ```
+function removeItem(item, amount, src, slot)
+ if not Items[item] then
+ print("^6Bridge^7: ^1Error^7 - ^2Tried to remove ^7'^3"..item.."^7'^2 but it doesn't exist")
+ return
+ end
+
+ if src then
+ debugPrint(src)
+ TriggerEvent(getScript()..":server:toggleItem", false, item, amount, src, nil, slot)
+ else
+ TriggerServerEvent(getScript()..":server:toggleItem", false, item, amount, nil, nil, slot)
+ end
+end
+
+-------------------------------------------------------------
+-- Toggle Items (Server Event)
+-------------------------------------------------------------
+
+--- Server event handler to toggle (add or remove) an item from a player's inventory.
+---
+--- This function validates the item, then calls the appropriate export functions based on the active inventory system.
+--- It also includes exploit protection via the dupeWarn function.
+---
+--- @param give boolean True to add the item, false to remove.
+--- @param item string The item name.
+--- @param amount number The quantity.
+--- @param newsrc number|nil The player source; defaults to event source.
+--- @param info table|nil Additional metadata.
+--- @param slot number|nil Optional inventory slot.
+---
+--- @usage
+--- ```lua
+--- TriggerServerEvent(getScript()..":server:toggleItem", true, "health_potion", 1)
+--- ```
+RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount, newsrc, info, slot, token)
+ debugPrint(GetInvokingResource())
+ if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
+ debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
+ return
+ end
+ if not Items[item] then
+ print("^6Bridge^7: ^1Error^7 - ^2Tried to "..(tostring(give) == "true" and "add" or "remove").." '^3"..item.."^7' but it doesn't exist")
+ return
+ end
+
+ local src = newsrc or source
+ if (give == true or give == 1) then
+ if newsrc == nil then -- must be coming from client this would be blank
+ if token == nil then
+ debugPrint("^1Auth^7: ^1No token recieved^7")
+ dupeWarn(src, item, "Auth: Player "..src.." attempted to spawn "..item.." without an auth token")
+ else
+ if type(token) ~= "number" then -- checks if the newsrc is a source or token, if number its coming form the server itself
+ 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])
+ dupeWarn(src, item, "Auth: "..src.." attempted to spawn "..item.." with an incorrect auth token")
+ else
+ debugPrint("^1Auth^7: ^2Client and Server Auth tokens match^7!", token, validTokens[src])
+ validTokens[src] = nil
+ end
+ end
+ end
+ end
+ end
+
+ local action = (tostring(give) == "true" and "addItem" or "removeItem")
+ local remamount = amount or 1
+ if item == nil then return end
+
+ -- Grab the current inventory (you can expand usage of 'inv' if needed)
+ local invName = ""
+ if give == 0 or give == false then
+ if not hasItem(item, amount or 1, src) then
+ dupeWarn(src, item, amount)
+
+ else
+ if isStarted(OXInv) then invName = OXInv
+ exports[OXInv]:RemoveItem(src, item, remamount, nil)
+
+ elseif isStarted(QSInv) then invName = QSInv
+ exports[QSInv]:RemoveItem(src, item, remamount)
+
+ elseif isStarted(CoreInv) then invName = CoreInv
+ exports[CoreInv]:removeItem(src, item, remamount)
+
+ elseif isStarted(OrigenInv) then invName = OrigenInv
+ exports[OrigenInv]:removeItem(src, item, remamount)
+
+ elseif isStarted(CodeMInv) then invName = CodeMInv
+ exports[CodeMInv]:RemoveItem(src, item, remamount)
+
+ elseif isStarted(QBInv) then invName = QBInv
+ while remamount > 0 do
+ if Core.Functions.GetPlayer(src).Functions.RemoveItem(item, 1, slot) then
+ remamount -= 1
+ else
+ print("^1Error removing "..item.." Amount left: "..remamount)
+ break
+ end
+ end
+ if Config.Crafting.showItemBox then
+ TriggerClientEvent((isStarted(QBInv) and QBInvNew and "qb-" or "").."inventory:client:ItemBox", src, Items[item], "remove", amount or 1)
+ end
+ elseif isStarted(PSInv) then invName = PSInv
+ while remamount > 0 do
+ if Core.Functions.GetPlayer(src).Functions.RemoveItem(item, 1, slot) then
+ remamount -= 1
+ else
+ print("^1Error removing "..item.." Amount left: "..remamount)
+ break
+ end
+ end
+ if Config.Crafting.showItemBox then
+ TriggerClientEvent("inventory:client:ItemBox", src, Items[item], "remove", amount or 1)
+ end
+
+ elseif isStarted(RSGInv) then invName = RSGInv
+ while remamount > 0 do
+ if Core.Functions.GetPlayer(src).Functions.RemoveItem(item, 1, slot) then
+ remamount -= 1
+ else
+ print("^1Error removing "..item.." Amount left: "..remamount)
+ break
+ end
+ end
+ if Config.Crafting.showItemBox then
+ TriggerClientEvent("rsg-inventory:client:ItemBox", src, Items[item], "remove", amount or 1)
+ end
+
+ end
+ -----
+ -- Fallback for if no inventory found:
+ -----
+ if invName == "" then
+ if isStarted(QBExport) or isStarted(QBXExport) then -- if qbcore or qbxcore, just use core functions
+ invName = isStarted(QBXExport) and QBXExport or isStarted(QBExport) and QBExport
+ Core.Functions.GetPlayer(src).Functions.RemoveItem(item, remamount, slot)
+
+ elseif ESX and isStarted(ESXExport) then -- if esx then use core functions
+ invName = ESX
+ ESX.GetPlayerFromId(src).removeInventoryItem(item, remamount)
+
+ end
+ end
+ -- Final check for if inventory was found
+ if invName == "" then
+ print("^4ERROR^7: No Inventory detected - Check starter.lua")
+ else
+ debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..Items[item].label.."("..item..") x"..(amount or 1))
+ end
+ end
+ else
+ local amountToAdd = amount or 1
+ if isStarted(OXInv) then invName = OXInv
+ exports[OXInv]:AddItem(src, item, amountToAdd, info, slot)
+
+ elseif isStarted(QSInv) then invName = QSInv
+ exports[QSInv]:AddItem(src, item, amountToAdd, slot, info)
+
+ elseif isStarted(CoreInv) then invName = CoreInv
+ exports[CoreInv]:addItem(src, item, amountToAdd, info)
+
+ elseif isStarted(CodeMInv) then invName = CodeMInv
+ exports[CodeMInv]:AddItem(src, item, amountToAdd, slot, info)
+
+ elseif isStarted(OrigenInv) then invName = OrigenInv
+ exports[OrigenInv]:addItem(src, item, amountToAdd, info, slot)
+
+ elseif isStarted(QBInv) then invName = QBInv
+ if Core.Functions.GetPlayer(src).Functions.AddItem(item, amountToAdd, nil, info) then
+ TriggerClientEvent((isStarted(QBInv) and QBInvNew and "qb-" or "").."inventory:client:ItemBox", src, Items[item], "add", amountToAdd)
+ end
+
+ elseif isStarted(RSGInv) then invName = RSGInv
+ if Core.Functions.GetPlayer(src).Functions.AddItem(item, amountToAdd, nil, info) then
+ TriggerClientEvent("rsg-inventory:client:ItemBox", src, Items[item], "add", amountToAdd)
+ end
+
+ elseif isStarted(PSInv) then invName = PSInv
+ if Core.Functions.GetPlayer(src).Functions.AddItem(item, amountToAdd, nil, info) then
+ if Config.Crafting.showItemBox then
+ TriggerClientEvent("inventory:client:ItemBox", src, Items[item], "add", amountToAdd)
+ end
+ end
+ end
+
+ if invName == "" then
+ if isStarted(QBExport) or isStarted(QBXExport) then -- if qbcore or qbxcore, just use core functions
+ invName = isStarted(QBXExport) and QBXExport or isStarted(QBExport) and QBExport
+ Core.Functions.GetPlayer(src).Functions.AddItem(item, amountToAdd, nil, info)
+
+ elseif ESX and isStarted(ESXExport) then -- if esx then use core functions
+ invName = ESX
+ ESX.GetPlayerFromId(src).addInventoryItem(item, amountToAdd)
+ end
+ end
+
+ -- Final check for if inventory was found
+ if invName == "" then
+ print("^4ERROR^7: No Inventory detected - Check starter.lua")
+ else
+ debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..Items[item].label.."("..item..") x"..(amount or 1))
+ end
+ end
+end)
+
+-------------------------------------------------------------
+-- Exploit Protection
+-------------------------------------------------------------
+
+--- Warns and kicks a player if they try to remove an item they don't have.
+---
+--- @param src number The player's source ID.
+--- @param item string The item name.
+---
+--- @usage
+--- ```lua
+--- dupeWarn(playerId, "health_potion")
+--- ```
+function dupeWarn(src, item, amount)
+ local name = getPlayer(src).name
+ print("^5DupeWarn^7: "..name.." (^1"..tostring(src).."^7) ^2Tried to remove item '^3"..item.."^7' but it wasn't there")
+ if not debugMode then
+ DropPlayer(src, name.."("..tostring(src)..") Kicked for suspected duplicating items: "..item)
+ end
+ print("^5DupeWarn^7: "..name.."(^1"..tostring(src).."^7) ^2Dropped from server - exploit protection detected an item not being found in players inventory^7")
+end
+
+-------------------------------------------------------------
+-- Tool Durability & Metadata
+-------------------------------------------------------------
+
+--- Reduces the durability of a tool by a specified damage amount.
+---
+--- If durability reaches zero or below, the tool is removed and a break sound is played.
+---
+--- @param data table Contains:
+--- - item (string): The tool's name.
+--- - damage (number): The damage % to apply.
+---
+--- @usage
+--- ```lua
+--- breakTool({ item = "drill", damage = 10 })
+--- ```
+function breakTool(data) -- WIP
+ local durability, slot = getDurability(data.item)
+ if not durability then durability = 100 end
+ durability -= data.damage
+ if durability <= 0 then
+ removeItem(data.item, 1)
+ local breakId = GetSoundId()
+ PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
+ else
+ TriggerServerEvent(getScript()..":server:setMetaData", { item = data.item, slot = slot, metadata = { durability = durability } })
+ end
+end
+
+--- Retrieves the durability and slot number of an item in a player's inventory.
+---
+--- Searches through the player's inventory for the specified item.
+---
+--- @param item string The item name.
+--- @return number|nil number The durability, or nil if not found.
+--- @return number|nil number The slot number, or nil if not found.
+---
+--- @usage
+--- ```lua
+--- local durability, slot = getDurability("drill")
+--- if durability then
+--- print("Durability:", durability)
+--- print("Slot:", slot)
+--- end
+--- ```
+function getDurability(item)
+ local lowestSlot = 100
+ local durability = nil
+ if isStarted(QBInv) or isStarted(PSInv) or isStarted(RSGInv) then
+ local itemcheck = Core.Functions.GetPlayerData().items
+ for k, v in pairs(itemcheck) do
+ if v.name == item then
+ if v.slot <= lowestSlot then
+ lowestSlot = v.slot
+ durability = itemcheck[k].info.durability
+ end
+ end
+ end
+ end
+
+ if isStarted(OXInv) then
+ local itemcheck = exports[OXInv]:Search('slots', item)
+ for k, v in pairs(itemcheck) do
+ if v.slot <= lowestSlot then
+ debugPrint(v.slot, itemcheck[k].metadata.durability)
+ lowestSlot = v.slot
+ durability = v.metadata.durability
+ end
+ end
+ end
+
+ if isStarted(QSInv) then
+ local itemcheck = exports[QSInv]:getUserInventory()
+ for _, v in pairs(itemcheck) do
+ if v.name == item and v.slot <= lowestSlot then
+ lowestSlot = v.slot
+ durability = v.metadata.durability
+ end
+ end
+ end
+
+ if isStarted(CoreInv) then
+ local itemcheck = exports[CoreInv]:getInventory()
+ for _, v in pairs(itemcheck) do
+ if v.name == item and v.slot <= lowestSlot then
+ lowestSlot = v.slot
+ durability = v.metadata.durability
+ end
+ end
+ end
+
+ if isStarted(CodeMInv) then
+ local itemcheck = exports[CodeMInv]:GetClientPlayerInventory()
+ for _, v in pairs(itemcheck) do
+ if v.name == item and v.slot <= lowestSlot then
+ lowestSlot = v.slot
+ durability = v.metadata.durability
+ end
+ end
+ end
+
+ if isStarted(OrigenInv) then
+ local itemcheck = exports[OrigenInv]:getPlayerInventory()
+ for _, v in pairs(itemcheck) do
+ if v.name == item and v.slot <= lowestSlot then
+ lowestSlot = v.slot
+ durability = v.metadata.durability
+ end
+ end
+ end
+
+ -- For ESX default inventory (es_extended)
+ if ESX and isStarted(ESXExport) then
+ local xPlayer = ESX.GetPlayerData() or {}
+ if xPlayer.inventory then
+ for _, v in ipairs(xPlayer.inventory) do
+ if v.name == item then
+ -- Optionally use a slot field if available; otherwise, use the index
+ if v.slot and v.slot <= lowestSlot then
+ lowestSlot = v.slot
+ end
+ if v.metadata and v.metadata.durability then
+ durability = v.metadata.durability
+ end
+ end
+ end
+ end
+ end
+
+ return durability, lowestSlot
+end
+
+--- Server event handler to set metadata for an item.
+---
+--- Updates item metadata (e.g. durability) for the player's inventory based on slot.
+---
+--- @param data table Contains:
+--- - item (string): The item name.
+--- - slot (number): The inventory slot.
+--- - metadata (table): The metadata to set.
+---
+--- @usage
+--- ```lua
+--- TriggerServerEvent("script:server:setMetaData", { item = "drill", slot = 5, metadata = { durability = 80 } })
+--- ```
+RegisterNetEvent(getScript()..":server:setMetaData", function(data)
+ local src = source
+ if isStarted(QBInv) or isStarted(PSInv) or isStarted(RSGInv) then
+ debugPrint(src, data.item, 1, data.slot)
+ local Player = Core.Functions.GetPlayer(src)
+ Player.PlayerData.items[data.slot].info = data.metadata
+ Player.PlayerData.items[data.slot].description = "HP : "..data.metadata.durability
+ Player.Functions.SetInventory(Player.PlayerData.items)
+
+ elseif isStarted(OXInv) then
+ exports[OXInv]:SetDurability(src, data.slot, data.metadata.durability)
+
+ elseif isStarted(QSInv) then
+ exports[QSInv]:SetItemMetadata(src, data.slot, data.metadata)
+
+ elseif isStarted(CoreInv) then
+ exports[CoreInv]:setMetadata(src, data.slot, data.metadata)
+
+ elseif isStarted(CodeMInv) then
+ exports[CodeMInv]:SetItemMetadata(src, data.slot, data.metadata)
+
+ elseif isStarted(OrigenInv) then
+ exports[OrigenInv]:setMetadata(src, data.slot, data.metadata)
+ end
+end)
+
+-------------------------------------------------------------
+-- Random Reward
+-------------------------------------------------------------
+
+--- Grants a random reward from a predefined reward pool if the player is eligible.
+---
+--- Checks if the item qualifies for a reward, removes the item, then calculates a random reward based on rarity.
+---
+--- @param itemName string The item name to check.
+---
+---@usage
+--- ```lua
+--- getRandomReward("gold_ring")
+--- ```
+function getRandomReward(itemName)
+ if Config.Rewards.RewardPool then
+ local reward = false
+ if type(Config.Rewards.RewardItem) == "string" then
+ Config.Rewards.RewardItem = { Config.Rewards.RewardItem }
+ end
+ for k, v in pairs(Config.Rewards.RewardItem) do
+ if v == itemName then
+ reward = true
+ break
+ end
+ end
+ if reward then
+ removeItem(itemName, 1)
+ local totalRarity = 0
+ for i = 1, #Config.Rewards.RewardPool do
+ totalRarity += Config.Rewards.RewardPool[i].rarity
+ end
+ debugPrint("^6Bridge^7: ^3getRandomReward^7: Total Rarity '"..totalRarity.."'")
+ local randomNum = math.random(1, totalRarity)
+ debugPrint("^6Bridge^7: ^3getRandomReward^7: Random Number '"..randomNum.."'")
+ local currentRarity = 0
+ for i = 1, #Config.Rewards.RewardPool do
+ currentRarity += Config.Rewards.RewardPool[i].rarity
+ if randomNum <= currentRarity then
+ debugPrint("^6Bridge^7: ^3getRandomReward^7: ^2Selected toy ^7'^6"..Config.Rewards.RewardPool[i].item.."^7'")
+ addItem(Config.Rewards.RewardPool[i].item, 1)
+ return
+ end
+ end
+ end
+ end
+end
+
+-------------------------------------------------------------
+-- Carry Capacity Check
+-------------------------------------------------------------
+
+--- Checks if a player can carry the specified items based on weight.
+---
+--- Calculates the current total weight in the player's inventory and determines whether adding the new items would exceed capacity.
+---
+--- @param itemTable table A table where keys are item names and values are required quantities.
+--- @param src number The player's source ID.
+--- @return table A table mapping each item to a boolean indicating if it can be carried.
+---
+--- @usage
+--- 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
+function canCarry(itemTable, src)
+ local resultTable = {}
+ if src then
+ if isStarted(OXInv) then
+ for k, v in pairs(itemTable) do
+ resultTable[k] = exports[OXInv]:CanCarryItem(src, k, v)
+ end
+
+ elseif isStarted(QSInv) then
+ for k, v in pairs(itemTable) do
+ resultTable[k] = exports[QSInv]:CanCarryItem(src, k, v)
+ end
+
+ elseif isStarted(CoreInv) then
+ for k, v in pairs(itemTable) do
+ resultTable[k] = exports[CoreInv]:canCarry(src, k, v)
+ end
+
+ elseif isStarted(CodeMInv) then --- This really needs updating, their docs are confusing..
+ local items = getPlayerInv(src)
+ local totalWeight = 0
+ if not items then return false end
+ for _, item in pairs(items) do
+ totalWeight += (item.weight * item.amount)
+ end
+ for k, v in pairs(itemTable) do
+ local itemInfo = Items[k]
+ if not itemInfo then
+ resultTable[k] = true
+ else
+ resultTable[k] = (totalWeight + (itemInfo.weight * v)) <= InventoryWeight
+ end
+ end
+
+ elseif isStarted(OrigenInv) then
+ for k, v in pairs(itemTable) do
+ resultTable[k] = exports[OrigenInv]:canCarryItem(src, k, v)
+ end
+
+ elseif isStarted(QBInv) then
+ for k, v in pairs(itemTable) do
+ resultTable[k] = exports[QBInv]:CanAddItem(src, k, v)
+ end
+
+ elseif isStarted(PSInv) then
+ local items = getPlayerInv(src)
+ local totalWeight = 0
+ if not items then return false end
+ for _, item in pairs(items) do
+ totalWeight += (item.weight * item.amount)
+ end
+ for k, v in pairs(itemTable) do
+ local itemInfo = Items[k]
+ if not itemInfo and not Player.Offline then
+ resultTable[k] = true
+ else
+ resultTable[k] = (totalWeight + (itemInfo.weight * v)) <= InventoryWeight
+ end
+ end
+ end
+ end
+ return resultTable
+end
+
+-------------------------------------------------------------
+-- Server Callback Registration
+-------------------------------------------------------------
+currentToken = nil
+if isServer() then
+ createCallback(getScript()..":server:canCarry", function(source, itemTable)
+ local result = canCarry(itemTable, source)
+ return result
+ end)
+
+ 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() ~= "qb-core" 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 = {}
+ createCallback(getScript()..":callback:GetAuthEvent", function(source)
+ local src = source
+ debugPrint(GetInvokingResource())
+ if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
+ debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital callback was called from an external resource^7")
+ return ""
+ end
+ debugPrint("^1Auth^7: ^2Player Source^7: "..src.." ^2requested ^3AuthEvent^7", AuthEvent)
+ 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)
+else
+ debugPrint("^1Auth^7: ^2Requesting ^3Auth Event^7")
+ AuthEvent = triggerCallback(getScript()..":callback:GetAuthEvent")
+end
diff --git a/shared/jobfunctions.lua b/shared/jobfunctions.lua
new file mode 100644
index 0000000..36cbb58
--- /dev/null
+++ b/shared/jobfunctions.lua
@@ -0,0 +1,211 @@
+--[[
+ Duty & Interaction Utilities Module
+ --------------------------------------
+ This module provides functions related to:
+ • Determining boss roles from Jobs and Gangs tables.
+ • Checking a player's job and duty status.
+ • Toggling duty state.
+ • Simulating player interactions such as hand washing, using toilets/urinals,
+ and teleporting via doors.
+]]
+
+-------------------------------------------------------------
+-- Global Duty Status
+-------------------------------------------------------------
+onDuty = false
+
+-------------------------------------------------------------
+-- Boss Role Detection
+-------------------------------------------------------------
+
+--- Scans the 'Jobs' and 'Gangs' tables to identify roles classified as bosses.
+---
+--- Iterates through the specified role's grades in the Jobs or Gangs table and returns
+--- a table mapping the role to the lowest grade number that qualifies as a boss (isboss or bankAuth).
+---
+--- @param role string The job or gang role to check.
+--- @return table table A table with the role mapped to its boss grade number.
+---
+--- @usage
+--- ```lua
+--- local bosses = makeBossRoles("police")
+--- if bosses["police"] then
+--- print("Police role has a boss grade.")
+--- end
+--- ```
+function makeBossRoles(role)
+ local boss = {}
+ 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
+ boss[role] = boss[role] and math.min(boss[role], tonumber(grade)) or tonumber(grade)
+ end
+ end
+ end
+ return boss
+end
+
+-------------------------------------------------------------
+-- Job & Duty Checks
+-------------------------------------------------------------
+
+--- Checks if the player has a specific job (or gang) and is on duty.
+---
+--- Verifies whether the player possesses the specified role. If the role is defined in the Jobs table,
+--- it also checks that the player is clocked in (onDuty). If the check fails, a notification is sent.
+---
+--- @param job string The job or gang to check.
+--- @return boolean Returns true if the player meets the criteria; false otherwise.
+---
+--- @usage
+--- ```lua
+--- if jobCheck("mechanic") then
+--- -- Allow mechanic features.
+--- else
+--- -- Deny access.
+--- end
+--- ```
+function jobCheck(job)
+ local canDo = true
+ if Jobs[job] then
+ if not hasJob(job) or not getPlayer().onDuty then
+ triggerNotify(nil, Loc[Config.Lan].error["not_clockedin"])
+ canDo = false
+ end
+ end
+ if Gangs[job] then
+ if not hasJob(job) then
+ canDo = false
+ end
+ end
+ return canDo
+end
+
+--- Toggles the player's duty status.
+---
+--- Switches the player's duty state between on-duty and off-duty. If using QBcore,
+--- it triggers the appropriate server event. Otherwise, it manually toggles the onDuty variable and notifies the player.
+---
+--- @usage
+--- ```lua
+--- toggleDuty() -- Player receives a notification of their new duty status.
+--- ```
+function toggleDuty()
+ onDuty = not onDuty
+ if isStarted(QBExport) or isStarted(QBXExport) then
+ TriggerServerEvent("QBCore:ToggleDuty")
+ else
+ if onDuty then
+ triggerNotify(nil, "Now on duty", "success")
+ else
+ triggerNotify(nil, "Now off duty", "success")
+ end
+ end
+end
+
+-------------------------------------------------------------
+-- Interaction Functions
+-------------------------------------------------------------
+
+--- Initiates the hand-washing action for the player.
+---
+--- Triggers an animation and a progress bar to simulate hand washing at the specified coordinates.
+--- On success, it notifies the player; if cancelled, it sends an error notification.
+---
+--- @param data table A table containing:
+--- - coords (vector3): The location where the hand-washing action occurs.
+---
+--- @usage
+--- ```lua
+--- washHands({ coords = vector3(200.0, 300.0, 40.0) })
+--- ```
+function washHands(data)
+ local ped = PlayerPedId()
+ lookEnt(data.coords)
+ local cam = createTempCam(ped, data.coords)
+ if progressBar({
+ label = Loc[Config.Lan].progressbar["progress_washing"],
+ time = 5000,
+ cancel = true,
+ dict = "mp_arresting",
+ anim = "a_uncuff",
+ flag = 32,
+ icon = "fas fa-hand-holding-droplet",
+ cam = cam
+ }) then
+ triggerNotify(nil, Loc[Config.Lan].success["washed_hands"], "success")
+ else
+ triggerNotify(nil, Loc[Config.Lan].error["cancel"], "error")
+ end
+ ClearPedTasks(ped)
+end
+
+--- Handles the player's interaction with a toilet or urinal.
+---
+--- Manages animations and progress bars for using a urinal or a toilet. If the action is successful,
+--- it triggers the appropriate server event (urinal usage) or notifies the player if cancelled.
+---
+--- @param data table A table containing:
+--- - urinal (boolean): `true if using a urinal; false for a toilet.`
+--- - sitcoords (vector4): `Coordinates and heading for seating when using a toilet.`
+---
+--- @usage
+--- ```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
+--- ```
+function useToilet(data)
+ if data.urinal then
+ if progressBar({
+ label = "Using Urinal",
+ time = 5000,
+ cancel = true,
+ dict = "misscarsteal2peeing",
+ anim = "peeing_loop",
+ flag = 32
+ }) then
+ TriggerServerEvent(getScript().."server:Urinal")
+ else
+ lockInv(false)
+ triggerNotify(nil, Loc[Config.Lan].error["cancelled"], "error")
+ end
+ else
+ TaskStartScenarioAtPosition(PlayerPedId(), "PROP_HUMAN_SEAT_CHAIR_MP_PLAYER", data.sitcoords.x, data.sitcoords.y, data.sitcoords.z, data.sitcoords[4], 0, 1, true)
+ if progressBar({
+ label = "Using Toilet",
+ time = 10000,
+ cancel = true
+ }) then
+ TriggerServerEvent(getScript().."server:Urinal")
+ ClearPedTasks(PlayerPedId())
+ else
+ lockInv(false)
+ triggerNotify(nil, Loc[Config.Lan].error["cancelled"], "error")
+ end
+ end
+end
+
+--- Teleports the player to specified coordinates with a fade effect.
+---
+--- Fades the screen out, moves the player to the target coordinates, sets the player's heading,
+--- then fades the screen back in. Commonly used for door interactions or teleportation points.
+---
+--- @param data table A table containing:
+--- - telecoords (vector4): The target coordinates and heading.
+---
+--- @usage
+--- ```lua
+--- useDoor({ telecoords = vector4(215.76, -810.12, 29.73, 90.0) })
+--- ```
+function useDoor(data)
+ DoScreenFadeOut(500)
+ while not IsScreenFadedOut() do Wait(10) end
+ SetEntityCoords(PlayerPedId(), data.telecoords.xyz, 0, 0, 0, false)
+ SetEntityHeading(PlayerPedId(), data.telecoords.w)
+ DoScreenFadeIn(1000)
+ Wait(100)
+end
diff --git a/shared/make/cameras.lua b/shared/make/cameras.lua
new file mode 100644
index 0000000..ae6b34b
--- /dev/null
+++ b/shared/make/cameras.lua
@@ -0,0 +1,81 @@
+--- 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.
+-- The camera is only created if `Config.Crafting.craftCam` is enabled in the configuration.
+--
+---@param ent entityId|coords The base position for the camera. Can be an entity handle or a `vector3` position.
+-- 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.
+--
+---@return cam camID The handle of the created camera, or `nil` if the camera was not created (e.g., if `Config.Crafting.craftCam` is `false`).
+--
+---@usage
+-- ```lua
+-- local cam = createTempCam(entity, targetCoords)
+-- ```
+function createTempCam(ent, coords)
+ local cam = nil
+ if Config.Crafting.craftCam then
+ if debugMode then
+ triggerNotify(nil, "ModCam Created", "success")
+ end
+ local camCoords = nil
+ local pointCoords = nil
+ if type(ent) ~= "vector3" then
+ camCoords = GetOffsetFromEntityInWorldCoords(ent, 1.0, -0.3, 0.8)
+ else
+ camCoords = ent
+ end
+
+ cam = CreateCamWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z + 0.5, 1.0, 0.0, 0.0, 60.00, false, 0)
+
+ if type(coords) == "number" then
+ SetCamCoord(cam, GetCamCoord(cam) + vec3(0, 0, 1.0))
+ PointCamAtEntity(cam, coords)
+ else
+ PointCamAtCoord(cam, coords)
+ end
+ end
+ return cam
+end
+
+--- 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.
+--
+---@usage
+-- ```lua
+-- startTempCam(cam)
+-- ```
+function startTempCam(cam)
+ if Config.Crafting.craftCam then
+ SetCamActive(cam, true)
+ RenderScriptCams(true, true, 1000, true, true)
+ end
+end
+
+--- Deactivates the temporary camera and stops rendering.
+--
+-- This function waits for one second, then stops rendering script cameras and destroys all cameras.
+-- The delay allows for any transitions or animations to complete.
+--
+-- The camera is only deactivated if `Config.Crafting.craftCam` is enabled in the configuration.
+--
+---@usage
+-- ```lua
+-- stopTempCam()
+-- ```
+function stopTempCam()
+ if Config.Crafting.craftCam then
+ CreateThread(function()
+ Wait(1000)
+ RenderScriptCams(false, true, 500, true, true)
+ DestroyAllCams()
+ end)
+ end
+end
\ No newline at end of file
diff --git a/shared/make/loaders.lua b/shared/make/loaders.lua
new file mode 100644
index 0000000..e3605f9
--- /dev/null
+++ b/shared/make/loaders.lua
@@ -0,0 +1,241 @@
+local time = 500
+
+--- Loads a specified model into memory.
+---
+--- This function checks if the model is valid and not already loaded.
+--- If not loaded, it requests the model and waits until it is loaded or times out.
+---
+---@param model string|number The name or hash of the model to load.
+---
+---@usage
+--- ```lua
+--- loadModel('prop_chair_01a')
+--- ```
+function loadModel(model)
+ if not IsModelValid(model) then print("^6Bridge^7: ^1ERROR^7: ^2Model^7 - '^6"..model.."^7' ^2does not exist in server") return
+ else
+ if not HasModelLoaded(model) then
+ debugPrint("^6Bridge^7: ^2Loading Model^7: '^6"..model.."^7'")
+ while not HasModelLoaded(model) and time > 0 do time -= 1 RequestModel(model) Wait(0) end
+ if not HasModelLoaded(model) then print("^6Bridge^7: ^3LoadModel^7: ^2Timed out loading model ^7'^6"..model.."^7'") end
+ end
+ time = 500
+ end
+end
+
+--- Unloads a model from memory.
+---
+--- This function marks a model as no longer needed, allowing the game to free up memory.
+---
+---@param model string|number The name or hash of the model to unload.
+---
+---@usage
+--- ```lua
+--- unloadModel('prop_chair_01a')
+--- ```
+function unloadModel(model)
+ debugPrint("^6Bridge^7: ^2Removing Model from memory cache^7: '^6"..model.."^7'")
+ SetModelAsNoLongerNeeded(model)
+end
+
+--- Loads an animation dictionary into memory.
+---
+--- This function checks if the animation dictionary exists and requests it.
+--- It waits until the animation dictionary is loaded before proceeding.
+---
+---@param animDict string The name of the animation dictionary to load.
+---
+---@usage
+--- ```lua
+--- loadAnimDict('amb@world_human_hang_out_street@male_c@base')
+--- ```
+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
+ debugPrint("^6Bridge^7: ^2Loading Anim Dictionary^7: '^6"..animDict.."^7'")
+ while not HasAnimDictLoaded(animDict) do RequestAnimDict(animDict) Wait(5) end
+ end
+end
+
+--- Unloads an animation dictionary from memory.
+---
+--- This function removes the animation dictionary from the game's memory cache.
+---
+---@param animDict string The name of the animation dictionary to unload.
+---
+---@usage
+---@
+--- ```lua
+--- unloadAnimDict('amb@world_human_hang_out_street@male_c@base')
+--- ```
+function unloadAnimDict(animDict)
+ debugPrint("^6Bridge^7: ^2Removing Anim Dictionary from memory cache^7: '^6"..animDict.."^7'")
+ RemoveAnimDict(animDict)
+end
+
+--- Loads a particle effects (ptfx) dictionary into memory.
+---
+--- This function requests the named particle effects asset and waits until it's loaded.
+---
+---@param ptFxName string The name of the particle effects dictionary to load.
+---
+---@usage
+--- ```lua
+--- loadPtfxDict('core')
+--- ```
+function loadPtfxDict(ptFxName)
+ if not HasNamedPtfxAssetLoaded(ptFxName) then
+ debugPrint("^6Bridge^7: ^2Loading Ptfx Dictionary^7: '^6"..ptFxName.."^7'")
+ while not HasNamedPtfxAssetLoaded(ptFxName) do RequestNamedPtfxAsset(ptFxName) Wait(5) end
+ end
+end
+
+--- Unloads a particle effects (ptfx) dictionary from memory.
+---
+--- This function removes the named particle effects asset from the game's memory cache.
+---
+---@param dict string The name of the particle effects dictionary to unload.
+---
+---@usage
+--- ```lua
+--- unloadPtfxDict('core')
+--- ```
+function unloadPtfxDict(dict)
+ debugPrint("^6Bridge^7: ^2Removing Ptfx Dictionary^7: '^6"..dict.."^7'")
+ RemoveNamedPtfxAsset(dict)
+end
+
+--- Loads a texture dictionary into memory.
+---
+--- This function requests the streamed texture dictionary and waits until it's loaded.
+---
+---@param dict string The name of the texture dictionary to load.
+---
+---@usage
+--- ```lua
+--- loadTextureDict('commonmenu')
+--- ```
+function loadTextureDict(dict)
+ if not HasStreamedTextureDictLoaded(dict) then
+ debugPrint("^6Bridge^7: ^2Loading Texture Dictionary^7: '^6"..dict.."^7'")
+ while not HasStreamedTextureDictLoaded(dict) do RequestStreamedTextureDict(dict) Wait(5) end
+ end
+end
+
+--- Loads a script audio bank into memory.
+---
+--- This function requests a script audio bank and waits until it's loaded or times out.
+---
+---@param bank string The name of the script audio bank to load.
+---
+---@return boolean `true` if the audio bank was successfully loaded; otherwise, `false`.
+---
+---@usage
+--- ```lua
+--- local success = loadScriptBank('DLC_HEISTS_GENERAL_FRONTEND_SOUNDS')
+--- ```
+function loadScriptBank(bank)
+ local timeout = 2000
+ debugPrint("^6Bridge^7: ^2Loading ^3Script ^2AudioBank^7...")
+ while not RequestScriptAudioBank(bank, false) do Wait(10) timeout -= 10 if timeout <= 0 then break end end
+
+ local success = RequestScriptAudioBank(bank, false)
+ debugPrint("^6Bridge^7: "..(success and "^3Successfully ^2loaded^7: '^4" or "^1Failed to ^2load^7: '^4")..bank.."^7'")
+ return success
+end
+
+--- Loads an ambient audio bank into memory.
+---
+--- This function requests an ambient audio bank and waits until it's loaded or times out.
+---
+---@param bank string The name of the ambient audio bank to load.
+---
+---@return boolean `true` if the audio bank was successfully loaded; otherwise, `false`.
+---
+---@usage
+--- ```lua
+--- local success = loadAmbientBank('AMB_REVERB_GENERIC')
+--- ```
+function loadAmbientBank(bank)
+ local timeout = 2000
+ debugPrint("^6Bridge^7: ^2Loading ^3Ambient ^2AudioBank^7...")
+ while not RequestAmbientAudioBank(bank, 0) do
+ Wait(10)
+ timeout -= 10
+ if timeout <= 0 then break end
+ end
+ local success = RequestAmbientAudioBank(bank, 0)
+ debugPrint("^6Bridge^7: "..(success and "^3Successfully ^2loaded^7: '^4" or "^1Failed to ^2load^7: '^4")..bank.."^7'")
+ return success
+end
+
+--- Plays an animation on a specified ped.
+---
+--- This function loads the animation dictionary and instructs the ped to play the animation.
+---
+---@param animDict string The name of the animation dictionary.
+---@param animName string The name of the animation within the dictionary.
+---@param duration number (optional) The duration to play the animation in milliseconds. Default is `30000`.
+---@param flag number (optional) The animation flag controlling how the animation is played. Default is `50`.
+---@param ped number (optional) The ped on which to play the animation. Defaults to the player's ped if not specified.
+---@param speed number (optional) The speed multiplier for the animation. Default is `8.0`.
+---
+---@usage
+--- ```lua
+--- playAnim('amb@world_human_hang_out_street@male_c@base', 'base', 5000, 1, PlayerPedId(), 1.0)
+--- ```
+function playAnim(animDict, animName, duration, flag, ped, speed)
+ loadAnimDict(animDict)
+ debugPrint("Attempting to make player play anim", animDict, animName)
+ TaskPlayAnim(ped and ped or PlayerPedId(), animDict, animName, speed or 8.0, speed or -8.0, duration or 30000, flag or 50, 1, false, false, false)
+end
+
+--- Stops a specified animation on a ped.
+---
+--- This function stops the animation and unloads the animation dictionary from memory.
+---
+---@param animDict string The name of the animation dictionary.
+---@param animName string The name of the animation within the dictionary.
+---@param ped number (optional) The ped on which to stop the animation. Defaults to the player's ped if not specified.
+---
+---@usage
+--- ```lua
+--- stopAnim('amb@world_human_hang_out_street@male_c@base', 'base', PlayerPedId())
+--- ```
+function stopAnim(animDict, animName, ped)
+ debugPrint("Stopping anim for "..(ped or PlayerPedId()))
+ StopAnimTask(ped or PlayerPedId(), animDict, animName, 0.5)
+ StopAnimTask(ped or PlayerPedId(), animName, animDict, 0.5)
+ unloadAnimDict(animDict)
+end
+
+--- Plays a game sound from a specified coordinate or entity.
+---
+--- This function attempts to play a sound from either a coordinate or an entity, using the specified audio bank and sound name.
+---
+---@param bank string The name of the audio bank containing the sound.
+---@param sound string The name of the sound to play.
+---@param coords vector3|number A `vector3` coordinate or an entity handle from which to play the sound.
+---@param synced boolean A boolean indicating whether the sound is synced across clients.
+---@param range number (optional) The maximum range at which the sound can be heard. Default is `10.0`.
+---
+---@usage
+--- ```lua
+--- playGameSound('DLC_HEIST_HACKING_SNAKE_SOUNDS', 'Beep', vector3(0, 0, 0), false, 15.0)
+--- ```
+function playGameSound(audioBank, soundSet, soundRef, coords, synced, range)
+ debugPrint("^6Bridge^7: ^2Attempting to play: ^3"..soundRef.." ^7('^4"..audioBank.."^7')")
+ loadScriptBank(audioBank)
+ local range = range or 10.0
+ local soundId = GetSoundId()
+ while not soundId do Wait(10) end
+ if type(coords) == "vector3" or type(coords) == "vector4" then
+ debugPrint("^6Bridge^7: ^2Playing sound from Coord^7: "..formatCoord(coords.xyz))
+ PlaySoundFromCoord(soundId, soundRef, coords.x, coords.y, coords.z, soundSet, synced, range, 0)
+ else
+ debugPrint("^6Bridge^7: ^2Playing sound from Entity^7: ^4"..coords.."^7")
+ PlaySoundFromEntity(soundId, soundRef, coords, soundSet, synced, 1.0)
+ end
+ ReleaseScriptAudioBank(audioBank)
+end
\ No newline at end of file
diff --git a/shared/make/makeBlip.lua b/shared/make/makeBlip.lua
new file mode 100644
index 0000000..52c9dbd
--- /dev/null
+++ b/shared/make/makeBlip.lua
@@ -0,0 +1,162 @@
+local blipTable = {}
+
+--- Creates a blip at specified coordinates with given properties.
+--
+-- 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.
+--
+---@param data A table containing blip data and properties.
+-- - **coords**: A `vector3` containing x, y, z coordinates where the blip will be placed.
+-- - **sprite** (optional): The blip sprite/icon ID. Default is `106`.
+-- - **col** (optional): The color ID of the blip. Default is `5`.
+-- - **scale** (optional): The scale of the blip. Default is `0.7`.
+-- - **disp** (optional): The display option of the blip. Default is `6`.
+-- - **category** (optional): The category ID for the blip.
+-- - **name**: The name of the blip, used for display on the map.
+-- - **preview** (optional): A URL or image path for a preview image to display with the blip.
+--
+---@return blip blipID The handle of the created blip.
+--
+---@usage
+-- ```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)
+-- ```
+function makeBlip(data)
+ local blip = nil
+ if gameName == "rdr3" then
+ blip = BlipAddForCoords(1664425300, data.coords.x, data.coords.y, data.coords.z)
+ SetBlipSprite(blip, data.sprite or `blip_shop_market_stall`)
+ SetBlipScale(blip, data.scale or 0.2)
+ SetBlipName(blip, data.name)
+ --BlipSetStyle(blip, data.col or `BLIP_STYLE_CREATOR_DEFAULT`)
+ else
+ blip = AddBlipForCoord(vec3(data.coords.x, data.coords.y, data.coords.z))
+ SetBlipCoords(blip, data.coords.x, data.coords.y, data.coords.z) -- Manually set blip coordinates again as sometimes it just refuses
+ SetBlipAsShortRange(blip, true)
+ SetBlipSprite(blip, data.sprite or 106)
+ SetBlipColour(blip, data.col or 5)
+ SetBlipScale(blip, data.scale or 0.7)
+ SetBlipDisplay(blip, data.disp or 6)
+ if data.category then
+ SetBlipCategory(blip, data.category)
+ end
+ BeginTextCommandSetBlipName('STRING')
+ 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 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
+ createDui(txname, data.preview, vec2(512, 256), scriptTxd)
+ else
+ CreateRuntimeTextureFromImage(scriptTxd, txname, data.preview)
+ end
+ exports["fs_smallresources"]:SetBlipInfoImage(blip, getScript()..'scriptTxd', txname)
+ exports["fs_smallresources"]:SetBlipInfoTitle(blip, data.name, false)
+ end
+ end
+ end
+ blipTable[blip] = blip
+
+ if DoesBlipExist(blip) then
+ debugPrint("^6Bridge^7: ^6Blip ^2created^7: '^6"..data.name.."^7' - '"..formatCoord(data.coords).."'")
+ else
+ print("Error making blip")
+ end
+ return blip
+end
+
+--- Creates a blip attached to a specified entity with given properties.
+--
+-- 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.
+--
+---@param data table A table containing blip data and properties.
+-- - **entity**: The entity to which the blip will be attached.
+-- - **sprite** (optional): The blip sprite/icon ID. Default is `106`.
+-- - **col** (optional): The color ID of the blip. Default is `5`.
+-- - **scale** (optional): The scale of the blip. Default is `0.7`.
+-- - **disp** (optional): The display option of the blip. Default is `6`.
+-- - **category** (optional): The category ID for the blip.
+-- - **name**: The name of the blip, used for display on the map.
+-- - **preview** (optional): A URL or image path for a preview image to display with the blip.
+--
+--
+---@return number blipID The handle of the created blip.
+---@usage
+-- ```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)
+-- ```
+function makeEntityBlip(data)
+ local blip = nil
+ if gameName == "rdr3" then
+ blip = BlipAddForEntity(1664425300, data.entity)
+ SetBlipSprite(blip, data.sprite or `blip_ambient_coach`)
+ SetBlipScale(blip, data.scale or 0.2)
+ SetBlipName(blip, data.name)
+
+ else
+ AddBlipForEntity(data.entity)
+ blip = GetBlipFromEntity(data.entity)
+ blipTable[blip] = blip
+ SetBlipAsShortRange(blip, true)
+ SetBlipSprite(blip, data.sprite or 106)
+ SetBlipColour(blip, data.col or 5)
+ SetBlipScale(blip, data.scale or 0.7)
+ SetBlipDisplay(blip, data.disp or 6)
+ if data.category then SetBlipCategory(blip, data.category) end
+ BeginTextCommandSetBlipName('STRING')
+ 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 data.preview then
+ local txname = string.gsub(tostring(data.name..'preview'), "[ ()~]", "")
+ if data.preview:find("http") or data.preview:find("nui") then
+ createDui(txname, data.preview, vec2(512, 256), scriptTxd)
+ else
+ CreateRuntimeTextureFromImage(scriptTxd, txname, data.preview)
+ end
+ exports["fs_smallresources"]:SetBlipInfoImage(blip, getScript()..'previewTxd', txname)
+ exports["fs_smallresources"]:SetBlipInfoTitle(blip, data.name, false)
+ end
+ end
+ end
+ blipTable[blip] = blip
+
+ if DoesBlipExist(blip) then
+ debugPrint("^6Bridge^7: ^6Blip ^2created for Entity^7: '^6"..data.name.."^7'")
+ else
+ print("Error making blip")
+ end
+ return blip
+end
+
+if gameName == "rdr3" then
+ onResourceStop(function()
+ for k in pairs(blipTable) do
+ RemoveBlip(k)
+ end
+ end, true)
+end
\ No newline at end of file
diff --git a/shared/make/makePed.lua b/shared/make/makePed.lua
new file mode 100644
index 0000000..5910d6e
--- /dev/null
+++ b/shared/make/makePed.lua
@@ -0,0 +1,246 @@
+--- A table to keep track of all created Peds.
+local Peds = {}
+
+--- Creates a distance-based Ped (pedestrian) that spawns when the player enters a specified area.
+--
+-- This function sets up a circular area using `createCirclePoly`. When the player enters this area, a Ped is created using `makePed`.
+-- When the player exits the area, the Ped is deleted.
+--
+---@param data table A table containing Ped data and properties. Should include at least `model` and `coords`.
+---@param coords vector4 A `vector3` or `vector4` specifying the coordinates where the Ped will be placed.
+---@param freeze boolean (optional) Boolean indicating whether the Ped should be frozen in place. Default is `true`.
+---@param collision boolean (optional) Boolean indicating whether collision with the Ped is enabled. Default is `false`.
+---@param scenario boolean (optional) String specifying the scenario the Ped should perform.
+---@param anim table (optional) A table containing animation dictionary and name `{animDict, animName}`.
+---@param synced boolean (optional) Boolean indicating whether the Ped is synced across clients. Default is `false`.
+--
+---@usage
+-- ```lua
+-- makeDistPed(pedData, pedCoords, true, false, 'WORLD_HUMAN_STAND_IMPATIENT', nil, true)
+-- ```
+function makeDistPed(data, coords, freeze, collision, scenario, anim, synced)
+ local zoneCoords = type(data) == "table" and data.coords or coords
+ local randName = keyGen()..keyGen()
+ 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)
+ end,
+ onExit = function()
+ DeletePed(Peds[randName])
+ end,
+ debug = debugMode,
+ })
+end
+
+--- Creates a Ped (pedestrian character) with specified properties.
+--
+-- This function creates a Ped at the given coordinates and applies appearance and clothing based on the provided data.
+--
+-- If `data` is a table with `custom` properties, it customizes the Ped's appearance accordingly.
+--
+---@param data modelHash|table Either a string/model hash of the Ped model to use, or a table containing `model` and `custom` data.
+---@param coords vector4 `vector3` or `vector4` specifying the coordinates where the Ped will be placed.
+---@param freeze boolean (optional) Boolean indicating whether the Ped should be frozen in place. Default is `true`.
+---@param collision boolean (optional) Boolean indicating whether collision with the Ped is enabled. Default is `false`.
+---@param scenario string (optional) String specifying the scenario the Ped should perform.
+---@param anim table (optional) A table containing animation dictionary and name `{animDict, animName}`.
+---@param synced boolean (optional) Boolean indicating whether the Ped is synced across clients. Default is `false`.
+--
+---@return ped entityID The handle of the created Ped.
+--
+---@usage
+-- ```lua
+-- local ped = makePed(pedData, pedCoords, true, false, nil, {'animDict', 'animName'}, true)
+-- ```
+function makePed(data, coords, freeze, collision, scenario, anim, synced)
+ local ped = nil
+ local model = nil
+ if type(data) == "table" then
+ model = data.model
+ loadModel(data.model)
+ ped = CreatePed(0, model, coords.x, coords.y, coords.z - 1.03, coords.w, synced and synced or false, false)
+
+ -- Inheritance
+ SetPedHeadBlendData(ped, data.custom.faceFather, data.custom.faceMother, data.custom.raceShape, data.custom.skinFather, data.custom.skinMother, data.custom.raceSkin, data.custom.faceMix or 0, data.custom.skinMix or 0, data.custom.raceMix or 0, false)
+
+ -- Face Features
+ for k, v in pairs({
+ "noseWidth", "noseHeight", "noseSize", "noseBoneHeight", "nosePeakHeight", "noseBoneTwist",
+ "eyebrowHeight", "eyebrowDepth",
+ "cheekBoneHeight", "cheekBoneWidth", "cheeckWidth",
+ "eyeOpening", "lipThickness",
+ "jawWidth", "jawSize",
+ "chinLowering", "chinLength", "chinSize", "chinHole",
+ "neckThickness"
+ }) do
+ SetPedFaceFeature(ped, k - 1, data.custom[v])
+ end
+
+ -- Appearance
+ SetPedComponentVariation(ped, 2, data.custom.Hair, 0, 0)
+ SetPedHairColor(ped, data.custom.HairTexture, data.custom.HairHighlight or 0)
+ SetPedHeadOverlay(ped, 2, data.custom.Eyebrows, data.custom.EyebrowsOpacity)
+ SetPedHeadOverlayColor(ped, 2, 1, data.custom.EyebrowsColor, 0)
+ SetPedEyeColor(ped, data.custom.Eyecolor)
+ SetPedHeadOverlay(ped, 4, data.custom.Makeup, data.custom.MakeupOpacity)
+ SetPedHeadOverlayColor(ped, 4, 1, data.custom.MakeupColor, 0)
+ SetPedHeadOverlay(ped, 8, data.custom.Lipstick, data.custom.LipstickOpacity)
+ SetPedHeadOverlayColor(ped, 8, 1, data.custom.LipstickColor, 0)
+ SetPedHeadOverlay(ped, 1, data.custom.Beard, data.custom.BeardOpacity)
+ SetPedHeadOverlayColor(ped, 1, 1, data.custom.BeardColor, 0)
+
+ -- Clothes
+ SetPedComponentVariation(ped, 1, data.custom.Mask, data.custom.MaskVariant, 0)
+ SetPedComponentVariation(ped, 7, data.custom.Scarf, data.custom.ScarfVariant, 0)
+ SetPedComponentVariation(ped, 11, data.custom.Jacket, data.custom.JacketVariant, 0)
+ SetPedComponentVariation(ped, 8, data.custom.Shirt, data.custom.ShirtVariant, 0)
+ SetPedComponentVariation(ped, 9, data.custom.Vest, data.custom.VestVariant, 0)
+ SetPedComponentVariation(ped, 5, data.custom.Bags, data.custom.BagsVariant, 0)
+ SetPedComponentVariation(ped, 3, data.custom.Arms, data.custom.ArmsVariant, 0)
+ SetPedComponentVariation(ped, 4, data.custom.Pants, data.custom.PantsVariant, 0)
+ SetPedComponentVariation(ped, 6, data.custom.Shoes, data.custom.ShoesVariant, 0)
+ SetPedComponentVariation(ped, 10, data.custom.Decal, data.custom.DecalVariant, 0)
+
+ -- Accessories
+ SetPedPropIndex(ped, 0, data.custom.Hat, data.custom.HatVariant, true)
+ SetPedPropIndex(ped, 1, data.custom.Glasses, data.custom.GlassesVariant, true)
+
+ SetPedPropIndex(ped, 2, data.custom.Ear, data.custom.EarVariant, true)
+ SetPedPropIndex(ped, 6, data.custom.Watches, data.custom.WatchesVariant, true)
+ SetPedPropIndex(ped, 7, data.custom.Bracelets, data.custom.BraceletsVariant, true)
+ else
+ model = data
+ loadModel(model)
+ if gameName == "rdr3" then
+ ped = CreatePed(model, coords.x, coords.y, coords.z - 1.03, coords.w, synced or false, false)
+ SetEntityVisible(ped, 1) -- SetEntityVisible
+ SetEntityAlpha(ped, 255, false) -- SetEntityAlpha
+ SetRandomOutfitVariation(ped, true) -- Invisible without
+ else
+ ped = CreatePed(0, model, coords.x, coords.y, coords.z - 1.03, coords.w, synced or false, false)
+ end
+ end
+
+ SetEntityInvincible(ped, true)
+ SetBlockingOfNonTemporaryEvents(ped, true)
+ FreezeEntityPosition(ped, freeze and freeze or true)
+
+ if collision then SetEntityNoCollisionEntity(ped, PlayerPedId(), false) end
+ if scenario then TaskStartScenarioInPlace(ped, scenario, 0, true) end
+ if anim then
+ loadAnimDict(anim[1])
+ TaskPlayAnim(ped, anim[1], anim[2], 0.5, 1.0, -1, 1, 0.2, 0, 0, 0)
+ end
+ if DoesEntityExist(ped) then
+ debugPrint("^6Bridge^7: ^1Ped ^2Created^7: '^6"..ped.."^7' | ^2Hash^7: ^7'^5"..(model).."^7' | ^2Coord^7: "..formatCoord(coords))
+ else
+ print("error ped")
+ end
+ unloadModel(model)
+ Peds[keyGen()..keyGen()] = ped
+ return ped
+end
+
+--- Generates random Ped data by filling in missing customization options with random values.
+--
+-- This function takes in a data table that may have some customization options missing in `data.custom`.
+--
+-- It generates random values for any missing options and returns a new data table with complete customization.
+--
+---@param data table A table containing at least a `model` field, and possibly a `custom` table with customization options.
+--
+---@return generatedTable table A new table containing `model` and `custom` with all customization options filled.
+--
+---@usage
+-- ```lua
+-- local pedData = GenerateRandomPedData({ model = `MP_M_Freemode_01`, custom = {} })
+-- ```
+function GenerateRandomPedData(data)
+ local newTable = {
+ model = data.model,
+ custom = {},
+ }
+ local isMale = data.model == `MP_M_Freemode_01`
+ local randomTable = {
+ -- Inheritance
+ faceFather = math.random(0, 45), faceMother = math.random(0, 45), faceMix = (math.random(0, 9) / 10),
+ skinFather = math.random(0, 45), skinMother = math.random(0, 45), skinMix = (math.random(0, 9) / 10),
+ raceShape = math.random(0, 45), raceSkin = math.random(0, 45), raceMix = (math.random(0, 9) / 10),
+
+ -- Face Features
+ noseWidth = (math.random(0, 9) / 10),
+ noseHeight = (math.random(0, 9) / 10),
+ noseSize = (math.random(0, 9) / 10),
+ noseBoneHeight = (math.random(0, 9) / 10),
+ nosePeakHeight = (math.random(0, 9) / 10),
+ noseBoneTwist = (math.random(0, 9) / 10),
+
+ eyebrowHeight = (math.random(0, 9) / 10),
+ eyebrowDepth = (math.random(0, 9) / 10),
+
+ cheekBoneHeight = (math.random(0, 9) / 10),
+ cheekBoneWidth = (math.random(0, 9) / 10),
+ cheeckWidth = (math.random(0, 9) / 10),
+
+ eyeOpening = (math.random(0, 9) / 10),
+ lipThickness = (math.random(0, 9) / 10),
+
+ jawWidth = (math.random(0, 9) / 10),
+ jawSize = (math.random(0, 9) / 10),
+
+ chinLowering = (math.random(0, 9) / 10),
+ chinLength = (math.random(0, 9) / 10),
+ chinSize = (math.random(0, 9) / 10),
+ chinHole = (math.random(0, 9) / 10),
+
+ neckThickness = (math.random(0, 9) / 10),
+
+ -- Appearance
+ Hair = math.random(0, isMale and 147 or 261), HairTexture = math.random(0, 63), HairHighlight = math.random(0, 63),
+ Eyebrows = math.random(0, 33),
+ EyebrowsOpacity = 0.9, EyebrowsColor = 0,
+ Eyecolor = math.random(0, 30),
+ Makeup = 0, MakeupOpacity = 0, MakeupColor = 0,
+ Lipstick = 0, LipstickOpacity = 0, LipstickColor = 0,
+ Beard = isMale and math.random(0, 28) or -1,
+ BeardOpacity = isMale and 0.9 or 0.0, BeardColor = 0,
+
+ -- Clothing
+ Mask = math.random(0, 252), MaskVariant = 0,
+ Scarf = math.random(0, isMale and 249 or 198), ScarfVariant = 0,
+ Jacket = math.random(0, isMale and 634 or 713), JacketVariant = 0,
+ Shirt = math.random(0, isMale and 237 or 299), ShirtVariant = 0,
+ Vest = math.random(0, isMale and 81 or 91), VestVariant = 0,
+ Bags = math.random(0, isMale and 138 or 148), BagsVariant = 0,
+ Arms = math.random(0, isMale and 224 or 261), ArmsVariant = 0,
+ Pants = math.random(0, isMale and 255 or 275), PantsVariant = 0,
+ Shoes = math.random(0, isMale and 157 or 199), ShoesVariant = 0,
+ Decal = math.random(0, isMale and 238 or 253), DecalVariant = 0,
+
+ -- Accessories
+ Hat = math.random(0, isMale and 232 or 229), HatVariant = 0,
+ Glasses = math.random(0, isMale and 68 or 71), GlassesVariant = 0,
+ Ear = math.random(0, isMale and 51 or 40), EarVariant = 0,
+ Watches = math.random(0, isMale and 46 or 35), WatchesVariant = 0,
+ Bracelets = math.random(0, isMale and 13 or 20), BraceletsVariant = 0,
+ }
+ for option in pairs(randomTable) do
+ if not data.custom[option] then
+ newTable.custom[option] = randomTable[option]
+ debugPrint("^6Bridge^7: ^2Picking Random Ped option ^7[^5"..option.."^7]: ^6"..newTable.custom[option].."^7")
+ else
+ newTable.custom[option] = data.custom[option]
+ end
+ end
+ return newTable
+end
+
+--- Cleans up all created Peds when the resource stops.
+onResourceStop(function()
+ for k in pairs(Peds) do
+ DeletePed(Peds[k])
+ end
+end, true)
\ No newline at end of file
diff --git a/shared/make/makeProp.lua b/shared/make/makeProp.lua
new file mode 100644
index 0000000..e95c304
--- /dev/null
+++ b/shared/make/makeProp.lua
@@ -0,0 +1,94 @@
+local Props = {}
+
+--- Creates a prop (object) in the game world at specified coordinates.
+---
+--- This function loads the model, creates the object, sets its heading, and freezes it if specified.
+---
+---@param data table A table containing prop data.
+--- - **prop** `string`: The model name or hash of the prop to create.
+--- - **coords** `vector4`: The coordinates where the prop will be placed. Should include x, y, z, and w (heading).
+---@param freeze boolean (optional) Whether to freeze the prop in place. Defaults to `false`.
+---@param synced boolean (optional) Whether the prop should be synced across clients. Defaults to `false`.
+---
+---@return number entityID The handle of the created prop object.
+---
+---@usage
+--- ```lua
+--- local propData = {
+--- prop = 'prop_chair_01a',
+--- coords = vector4(123.4, 567.8, 90.1, 180.0)
+--- }
+--- local prop = makeProp(propData, true, false)
+--- ```
+function makeProp(data, freeze, synced)
+ loadModel(data.prop)
+ local prop = CreateObject(data.prop, data.coords.x, data.coords.y, data.coords.z-1.03, synced and synced or false, synced and synced or false, false)
+ SetEntityHeading(prop, data.coords.w + 180.0)
+ FreezeEntityPosition(prop, freeze or false)
+
+ debugPrint("^6Bridge^7: ^1Prop ^2Created^7: '^6"..prop.."^7' | ^2Hash^7: ^7'^6"..data.prop.."^7' | ^2Coord^7: "..formatCoord(data.coords))
+ SetModelAsNoLongerNeeded(data.prop)
+ Props[keyGen()..keyGen()] = prop
+ return prop
+end
+
+--- Creates a prop that appears when the player is within a certain distance.
+---
+--- This function sets up a proximity area, and when the player enters it, the prop is created.
+--- When the player exits the area, the prop is destroyed.
+---
+---@param data table A table containing prop data.
+--- - **prop** `string`: The model name or hash of the prop to create.
+--- - **coords** `vector4`: The coordinates where the prop will be placed. Should include x, y, z, and w (heading).
+---@param freeze boolean (optional) Whether to freeze the prop in place. Defaults to `false`.
+---@param synced boolean (optional) Whether the prop should be synced across clients. Defaults to `false`.
+---
+---@usage
+--- ```lua
+--- local propData = {
+--- prop = 'prop_chair_01a',
+--- coords = vector4(123.4, 567.8, 90.1, 180.0)
+--- }
+--- makeDistProp(propData, true, false)
+--- ```
+function makeDistProp(data, freeze, synced, range)
+ local name = keyGen()..keyGen()
+ 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)
+ end,
+ onExit = function()
+ destroyProp(Props[name])
+ end,
+ debug = debugMode,
+ })
+end
+
+--- Destroys a prop, detaching it if attached to the player beforehand.
+---
+---@param entity number The handle of the prop entity to destroy.
+---
+---@usage
+--- ```lua
+--- destroyProp(prop)
+--- ```
+function destroyProp(entity)
+ if entity then
+ debugPrint("^6Bridge^7: ^2Destroying Prop^7: '^6"..entity.."^7'")
+ if IsEntityAttachedToEntity(entity, PlayerPedId()) then
+ SetEntityAsMissionEntity(entity)
+ DetachEntity(entity, true, true)
+ end
+ DeleteObject(entity)
+ end
+end
+
+--- Cleans up all created props when the resource stops.
+onResourceStop(function()
+ for k in pairs(Props) do
+ destroyProp(Props[k])
+ end
+end, true)
diff --git a/shared/make/makeVeh.lua b/shared/make/makeVeh.lua
new file mode 100644
index 0000000..77551a7
--- /dev/null
+++ b/shared/make/makeVeh.lua
@@ -0,0 +1,107 @@
+local Vehicles = {}
+
+--- Creates a vehicle with the specified model and coordinates.
+---
+--- This function loads the vehicle model, creates the vehicle in the world at the given coordinates, sets initial properties, and returns the vehicle handle.
+---
+---@param model string|number The model name or hash of the vehicle to create.
+---@param coords vector4 The coordinates where the vehicle will be placed, including x, y, z, and w (heading).
+---
+---@return number entityID The handle of the created vehicle.
+---
+---@usage
+--- ```lua
+--- local vehicle = makeVeh('adder', vector4(123.4, 567.8, 90.1, 180.0))
+--- ```
+function makeVeh(model, coords)
+ loadModel(model)
+ local veh = CreateVehicle(model, coords.x, coords.y, coords.z, coords.w, true, false)
+ SetVehicleHasBeenOwnedByPlayer(veh, true)
+ if gameName ~= "rdr3" then
+ SetNetworkIdCanMigrate(NetworkGetNetworkIdFromEntity(veh), true)
+ 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
+ return veh
+end
+
+local distanceVehicles = {}
+--- Creates a vehicle that spawns when the player enters a designated polyzone area.
+---
+--- This function sets up a circular polyzone; when the player enters the zone, the vehicle is spawned,
+--- and when the player exits, the vehicle is deleted.
+---
+---@param data table A table containing vehicle data.
+--- - **vehicle** `string`: The model name or hash of the vehicle to spawn.
+--- - **coords** `vector4`: The coordinates where the vehicle will be placed. Should include x, y, z, and w (heading).
+---@param freeze boolean (optional) Whether to freeze the vehicle in place. Defaults to `false`.
+---@param synced boolean (optional) Whether the vehicle should be synced across clients. Defaults to `false`.
+function makeDistVehicle(data, radius, onEnter, onExit)
+ local vehicle = nil
+ local zoneId = keyGen() .. keyGen()
+ local zone = createCirclePoly({
+ name = zoneId,
+ coords = vec3(data.coords.x, data.coords.y, data.coords.z),
+ radius = radius,
+ onEnter = function()
+ vehicle = makeVeh(data.model, data.coords)
+ if onEnter then
+ debugPrint("makeDistVehicle onEnter running")
+ onEnter(vehicle)
+ end
+ end,
+ onExit = function()
+ deleteVehicle(vehicle)
+ if onExit then
+ debugPrint("makeDistVehicle onExit running")
+ onExit(vehicle)
+ end
+ end,
+ debug = debugMode,
+ })
+ distanceVehicles[zoneId] = { zone = zone, vehicle = vehicle }
+ return zoneId
+end
+
+--- Removes a specific distance-based vehicle spawning zone.
+---
+---@param zoneId string The unique identifier of the zone to remove.
+function removeDistVehicleZone(zoneId)
+ if distanceVehicles[zoneId].zone then
+ removePolyZone(distanceVehicles[zoneId].zone) -- Adjust this if your polyzone library uses a different removal method.
+ if distanceVehicles[zoneId].vehicle then
+ deleteVehicle(distanceVehicles[zoneId].vehicle)
+ end
+ distanceVehicles[zoneId] = nil
+ print("Removed polyzone for zoneId: " .. zoneId)
+ else
+ print("No zone found with zoneId: " .. zoneId)
+ end
+end
+
+--- Deletes a spawned vehicle.
+---
+---@param vehicle number The handle of the vehicle entity to delete.
+function deleteVehicle(vehicle)
+ if vehicle then
+ debugPrint("^6Bridge^7: ^2Destroying Vehicle^7: '^6" .. vehicle .. "^7'")
+ if IsEntityAttachedToEntity(vehicle, PlayerPedId()) then
+ SetEntityAsMissionEntity(vehicle)
+ DetachEntity(vehicle, true, true)
+ end
+ DeleteVehicle(vehicle)
+ end
+end
+
+--- Cleans up all created vehicles when the resource stops.
+onResourceStop(function(r)
+ for i = 1, #Vehicles do DeleteVehicle(Vehicles[i]) end
+end)
\ No newline at end of file
diff --git a/shared/make/progressBars.lua b/shared/make/progressBars.lua
new file mode 100644
index 0000000..e9e6d4c
--- /dev/null
+++ b/shared/make/progressBars.lua
@@ -0,0 +1,302 @@
+local inProgress = false
+
+--- Displays a progress bar using the configured progress bar system.
+---
+--- This function handles displaying a progress bar to the player using the specified progress bar system (e.g., ox, qb, esx, gta).
+--- It supports shared progress bars between players, animations, camera effects, and more.
+---
+---@param data table A table containing the progress bar configuration.
+--- - **label** (`string`): The text label to display on the progress bar.
+--- - **time** (`number`): The duration of the progress bar in milliseconds.
+--- - **dict** (`string`, optional): The animation dictionary to use.
+--- - **anim** (`string`, optional): The animation name to play.
+--- - **task** (`string`, optional): The task scenario to perform.
+--- - **flag** (`number`, optional): The animation flag.
+--- - **dead** (`boolean`, optional): Whether to allow the progress bar when the player is dead. Default is `false`.
+--- - **cancel** (`boolean`, optional): Whether the progress bar can be canceled by the player. Default is `true`.
+--- - **icon** (`string`, optional): The icon to display (for qb progress bar).
+--- - **cam** (`number`, optional): The camera handle to use.
+--- - **shared** (`table`, optional): Data for shared progress bars.
+--- - **pid** (`number`): The player ID to share the progress bar with.
+--- - **label** (`string`): The label to display on the shared progress bar.
+---
+--- @return boolean `true` if the progress bar completed successfully, or `false` if it was canceled.
+---
+---@usage
+--- ```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,
+--- })
+--- ```
+function progressBar(data)
+ local ped = PlayerPedId()
+ if data.shared then
+ debugPrint("^6Bridge^7: ^6Sharing progressBar to player^7: ^6"..data.shared.pid.."^7")
+ storedPID = data.shared.pid
+ TriggerServerEvent(getScript()..":server:sharedProg:Start", 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
+ end
+
+ while result == nil do Wait(10) end
+
+ -- Cleanup
+ FreezeEntityPosition(ped, false)
+ lockInv(false)
+ if data.cam then
+ stopTempCam(data.cam)
+ end
+ if result == false and data.shared then
+ debugPrint("^6Bridge^7: ^2Sending cancel to ^6"..storedPID.."^7")
+ TriggerServerEvent(getScript().."server:sharedProg:cancel", storedPID)
+ end
+ storedPID = nil
+ if result == false then
+ currentToken = nil
+ TriggerServerEvent(getScript()..":clearAuthToken")
+ end
+ if result == true and data.request then
+ TriggerServerEvent(getScript()..":clearAuthToken")
+ currentToken = triggerCallback(AuthEvent)
+ end
+ 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
+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.
+--- It adjusts the data to prevent loops and sends the data to the target client.
+RegisterNetEvent(getScript()..":server:sharedProg:Start", function(data)
+ local pid = data.shared.pid -- Get player ID from the client
+ data.label = data.shared.label -- Set progress bar label to the shared label
+ data.cancel = false -- Make it so it can't be canceled
+ data.dead = true -- Allow progress bar even if player is dead
+ data.shared = nil -- Remove shared info to prevent loops
+ data.anim = nil -- Remove animation so players don't share it
+ debugPrint("^6Bridge^7: ^6"..source.." ^2is sending shared progressBar to player^7, ^6"..pid.."^7")
+ TriggerClientEvent(getScript()..":client:sharedProg:Start", pid, data)
+end)
+
+--- Client event handler for starting a shared progress bar.
+--- This event is triggered when the server wants the client to start a shared progress bar.
+RegisterNetEvent(getScript()..":client:sharedProg:Start", function(data)
+ debugPrint("^6Bridge^7: ^2You have been sent a progressBar^7")
+ progressBar(data)
+end)
+
+--- Server event handler for canceling a shared progress bar.
+--- This event is triggered when a progress bar is canceled and the server needs to notify the other player.
+RegisterNetEvent(getScript()..":server:sharedProg:Cancel", function(pid)
+ debugPrint("^6Bridge^7: ^2Sending cancel progressBar to ^6"..pid.."^7")
+ TriggerClientEvent(getScript()..":client:sharedProg:Cancel", pid)
+end)
+
+--- Client event handler for canceling a shared progress bar.
+--- This event is triggered when the server wants the client to cancel a shared progress bar.
+RegisterNetEvent(getScript()..":client:sharedProg:Cancel", function()
+ debugPrint("^6Bridge^7: ^2Receiving cancel progressBar^7")
+ stopProgressBar()
+end)
\ No newline at end of file
diff --git a/shared/metaHandlers.lua b/shared/metaHandlers.lua
new file mode 100644
index 0000000..8a19766
--- /dev/null
+++ b/shared/metaHandlers.lua
@@ -0,0 +1,135 @@
+--[[
+ Player Metadata Utilities Module
+ ----------------------------------
+ This module provides functions for retrieving and setting metadata for players
+ across different frameworks (QB, ESX, OXCore). It also registers server callbacks
+ for getting and setting metadata.
+]]
+
+-------------------------------------------------------------
+-- Player Retrieval
+-------------------------------------------------------------
+
+--- Retrieves the player object using the active core export.
+---
+--- @param source number The server ID of the player.
+--- @return table|nil table The player object, or nil if no supported core is detected.
+---
+--- @usage
+--- ```lua
+--- 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)
+ end
+ return nil
+end
+
+-------------------------------------------------------------
+-- Metadata Retrieval
+-------------------------------------------------------------
+
+--- Retrieves metadata from a player object.
+---
+--- If called client-side (player is nil), it triggers a server callback to retrieve metadata.
+---
+--- @param player table|nil The player object; if nil, metadata is retrieved via a server callback.
+--- @param key string The metadata key to retrieve.
+--- @return any The value of the requested metadata, or nil if not found.
+---
+--- @usage
+--- ```lua
+--- local myMeta = GetMetadata(player, "myKey")
+--- ```
+function GetMetadata(player, key)
+ 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 or 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)
+ end
+ end
+ return nil
+end
+
+-- Register a server callback for retrieving metadata.
+createCallback(getScript()..":server:GetMetadata", function(source, key)
+ debugPrint("^6Bridge^7: ^3GetMetadata Callback^7 from source: "..tostring(source)..", key: "..tostring(key))
+ local player = GetPlayer(source)
+ if not player then
+ print("Error getting metadata: player not found for source "..tostring(source))
+ return
+ end
+
+ if type(key) == "table" then
+ local Metadata = {}
+ for _, k in ipairs(key) do
+ Metadata[k] = GetMetadata(player, k)
+ end
+ return Metadata
+ elseif type(key) == "string" then
+ return GetMetadata(player, key)
+ end
+end)
+
+-------------------------------------------------------------
+-- Metadata Setting
+-------------------------------------------------------------
+
+--- Sets metadata on a player object.
+---
+--- The function updates the player's metadata using the active core export.
+---
+--- @param player table The player object.
+--- @param key string The metadata key to set.
+--- @param value any The new value for the metadata key.
+---
+--- @usage
+--- ```lua
+--- SetMetadata(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)
+ end
+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)
+ print("Metadata set successfully.", key)
+ return true
+end)
\ No newline at end of file
diff --git a/shared/notify.lua b/shared/notify.lua
new file mode 100644
index 0000000..4dc327c
--- /dev/null
+++ b/shared/notify.lua
@@ -0,0 +1,140 @@
+--[[
+ Notifications Module
+ ----------------------
+ This module provides a unified interface for displaying notifications using various
+ notification systems. The active system is determined by the Config.System.Notify setting.
+
+ Supported systems include:
+ • okok
+ • qb
+ • ox
+ • gta (default)
+ • esx
+]]
+
+--- Displays notifications to the player using the configured notification system.
+---
+--- Supports multiple notification systems based on Config.System.Notify. Can be triggered from both
+--- client and server contexts.
+---
+--- @param title string|nil The notification title (optional for some systems).
+--- @param message string The main message content.
+--- @param type string The notification type ("success", "error", "info").
+--- @param src number|nil Optional server ID; if provided, the notification is sent to that player.
+---
+--- @usage
+--- ```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", 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
+
+ elseif Config.System.Notify == "red" then
+ if isStarted("jim-redui") then
+ if not src then
+ TriggerEvent("jim-redui:Notify", title, message, type)
+ else
+ TriggerClientEvent("jim-redui:Notify", src, title, message, type)
+ end
+ end
+ end
+end
+
+-------------------------------------------------------------
+-- ESX Notifications
+-------------------------------------------------------------
+
+--- Registers a server-side event to display ESX notifications to clients.
+---
+--- 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
+--- ```lua
+--- TriggerClientEvent(getScript()..":DisplayESXNotify", playerId, "success", "New achievement unlocked!")
+--- ```
+RegisterNetEvent(getScript()..":DisplayESXNotify", function(type, text)
+ exports["esx_notify"]:Notify(type, 4000, text)
+end)
+
+-------------------------------------------------------------
+-- GTA-style Notifications
+-------------------------------------------------------------
+
+--- Displays GTA-style text notifications using native GTA functions.
+---
+--- Selects an appropriate icon based on the current script (if applicable) and renders the notification.
+---
+--- @param title string The notification title/identifier (used to select an icon).
+--- @param text string The notification message.
+---
+--- @usage
+--- ```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)
\ No newline at end of file
diff --git a/shared/phones.lua b/shared/phones.lua
new file mode 100644
index 0000000..62d9c28
--- /dev/null
+++ b/shared/phones.lua
@@ -0,0 +1,166 @@
+--[[
+ Phone Mails Module
+ ------------------
+ This module handles sending phone mails using different phone systems.
+ Supported systems include:
+ - gksphone
+ - yflip-phone
+ - qs-smartphone
+ - qs-smartphone-pro
+ - roadphone
+ - lb-phone
+ - qb-phone
+ - jpr-phonesystem
+]]
+
+--- Sends a phone mail using the detected phone system.
+--- The function iterates through a prioritized list of supported phone systems.
+--- Once an active system is found (via `isStarted`), the corresponding mail function is executed.
+---
+--- @param data table A table containing the mail data.
+--- - subject (string): The email subject.
+--- - sender (string): The sender identifier.
+--- - message (string): The email body content.
+--- - actions (table|nil): Optional action buttons for the email.
+--- @usage
+--- sendPhoneMail({
+--- subject = "Welcome!",
+--- sender = "Admin",
+--- message = "Thank you for joining our server.",
+--- actions = {
+--- { label = "Reply", action = replyFunction }
+--- }
+--- })
+function sendPhoneMail(data)
+ -- Define each supported phone system and its corresponding mail-sending function.
+ local phoneSystems = {
+ { name = "gksphone",
+ send = function(mailData)
+ exports["gksphone"]:SendNewMail(mailData)
+ end,
+ },
+ { 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("% ", "\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("% ", "\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,
+ },
+ }
+
+ local activePhone = nil
+ -- 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
+ 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
+end
+
+-------------------------------------------------------------
+-- Phone System Event Handlers
+-------------------------------------------------------------
+
+--- Handles sending mail for lb-phone.
+--- Listens for the `lbphone:SendMail` event and sends an email using lb-phone's API.
+---
+--- @event lbphone:SendMail
+--- @param data table The mail data.
+--- - subject (string): The email subject.
+--- - message (string): The email content.
+--- - buttons (table|nil): Optional action buttons (mapped from data.actions if present).
+RegisterNetEvent(getScript()..":lbphone:SendMail", function(data)
+ local src = source
+ local phoneNumber = exports["lb-phone"]:GetEquippedPhoneNumber(src)
+ local emailAddress = exports["lb-phone"]:GetEmailAddress(phoneNumber)
+ -- Map actions to buttons if provided.
+ data.buttons = data.actions or data.buttons
+
+ exports["lb-phone"]:SendMail({
+ to = emailAddress,
+ subject = data.subject,
+ message = data.message,
+ actions = data.buttons,
+ })
+end)
+
+--- Handles sending mail for yflip-phone.
+--- Listens for the `yflip:SendMail` event and sends an email using yflip-phone's API.
+---
+--- @event yflip:SendMail
+--- @param data table The mail data.
+--- - subject (string): The email subject.
+--- - sender (string): The sender identifier.
+--- - message (string): The email content.
+--- - buttons (table|nil): Optional action buttons.
+RegisterNetEvent(getScript()..":yflip:SendMail", function(data)
+ local src = source
+ exports["yflip-phone"]:SendMail({
+ title = data.subject,
+ sender = data.sender,
+ senderDisplayName = data.sender,
+ content = data.message,
+ actions = data.buttons,
+ }, 'source', src)
+end)
+
+--- Handles sending mail for jpr-phonesystem.
+--- Listens for the `jpr:SendMail` event and sends an email using jpr-phonesystem's API.
+---
+--- @event jpr:SendMail
+--- @param data table The mail data.
+--- - subject (string): The email subject.
+--- - sender (string): The sender identifier.
+--- - message (string): The email content.
+--- - buttons (table|nil): Optional action buttons.
+RegisterNetEvent(getScript()..":jpr:SendMail", function(data)
+ local src = source
+ local Player = Core.Functions.GetPlayer(src)
+ TriggerEvent('jpr-phonesystem:server:sendEmail', {
+ Assunto = data.subject, -- Email subject
+ Conteudo = data.message, -- Email content
+ Enviado = data.sender, -- Sender information
+ Destinatario = Player.PlayerData.citizenid, -- Recipient identifier
+ Event = {}, -- Optional event details
+ })
+end)
\ No newline at end of file
diff --git a/shared/playerfunctions.lua b/shared/playerfunctions.lua
new file mode 100644
index 0000000..46eed39
--- /dev/null
+++ b/shared/playerfunctions.lua
@@ -0,0 +1,714 @@
+--[[
+ Player Utility & Server Event Handlers Module
+ ------------------------------------------------
+ This module provides utility functions for:
+ • Locking/unlocking the player's inventory.
+ • Instantly turning or gradually turning the player to face a target.
+ • Handling player needs (thirst and hunger) via server events.
+ • Charging/funding players (money removal/addition).
+ • Processing item consumption and applying effects.
+ • Checking player job/gang roles and retrieving player information.
+ • Getting active players near a coordinate.
+]]
+
+-------------------------------------------------------------
+-- 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
+
+-------------------------------------------------------------
+-- Server Event Handlers for Needs
+-------------------------------------------------------------
+
+--- Server event handler for urinal usage.
+--- Decreases player's thirst by a random amount.
+RegisterNetEvent(getScript()..":server:Urinal", function()
+ local src = source
+ local Player = getPlayer(src)
+ local thirstamt = math.random(10, 30)
+ local thirst = Player.thirst - thirstamt
+ setThirst(src, getPlayer(src).thirst - thirst)
+end)
+
+--- Server event handler for setting player needs (thirst or hunger).
+---
+--- @event
+--- @param type string "thirst" or "hunger".
+--- @param amount number New value to set.
+RegisterNetEvent(getScript()..":server:setNeed", function(needType, amount)
+ local src = source
+ if needType == "thirst" then
+ setThirst(src, amount)
+ elseif needType == "hunger" then
+ setHunger(src, amount)
+ end
+end)
+
+--- Sets the player's thirst level.
+---
+--- @param src number The player's server ID.
+--- @param thirst number The new thirst level.
+---
+--- @usage
+--- ```lua
+--- setThirst(playerId, 80)
+--- ```
+function setThirst(src, thirst)
+ if isStarted(ESXExport) then
+ TriggerClientEvent('esx_status:add', src, 'thirst', thirst)
+ elseif isStarted(QBExport) or isStarted(QBXExport) then
+ local Player = Core.Functions.GetPlayer(src)
+ Player.Functions.SetMetaData('thirst', thirst)
+ TriggerClientEvent("hud:client:UpdateNeeds", src, thirst, Player.PlayerData.metadata.thirst)
+ end
+end
+
+--- Sets the player's hunger level.
+---
+--- @param src number The player's server ID.
+--- @param hunger number The new hunger level.
+---
+--- @usage
+--- ```lua
+--- setHunger(playerId, 60)
+--- ```
+function setHunger(src, hunger)
+ if isStarted(ESXExport) then
+ TriggerClientEvent('esx_status:add', src, 'hunger', hunger)
+ elseif isStarted(QBExport) or isStarted(QBXExport) then
+ local Player = Core.Functions.GetPlayer(src)
+ Player.Functions.SetMetaData('hunger', hunger)
+ TriggerClientEvent("hud:client:UpdateNeeds", src, hunger, Player.PlayerData.metadata.hunger)
+ end
+end
+
+-------------------------------------------------------------
+-- Economy Event Handlers
+-------------------------------------------------------------
+
+--- Charges a player by removing money from their account.
+---
+--- @param cost number The amount to charge.
+--- @param type string "cash" or "bank".
+--- @param newsrc number|nil Optional player ID; defaults to event source.
+---
+--- @usage
+--- ```lua
+--- chargePlayer(100, "cash", playerId)
+--- ```
+function chargePlayer(cost, moneyType, newsrc)
+ local src = newsrc or source
+ local fundResource = ""
+ if cost < 0 then
+ debugPrint("^1Error^7: ^7SRC: ^3"..src.." ^2Tried to charge a minus value^7", cost)
+ return
+ end
+ if moneyType == "cash" then
+ if isStarted(OXInv) then fundResource = OXInv
+ exports[OXInv]:RemoveItem(src, "money", cost)
+ elseif isStarted(QBExport) or isStarted(QBXExport) then
+ fundResource = QBExport
+ Core.Functions.GetPlayer(src).Functions.RemoveMoney("cash", cost)
+ elseif isStarted(RSGExport) then
+ fundResource = QBExport
+ Core.Functions.GetPlayer(src).Functions.RemoveMoney("cash", cost)
+ elseif isStarted(ESXExport) then
+ fundResource = ESXExport
+ ESX.GetPlayerFromId(src).removeMoney(cost, "")
+ end
+ elseif moneyType == "bank" then
+ if isStarted(QBExport) or isStarted(QBXExport) then fundResource = QBExport
+ Core.Functions.GetPlayer(src).Functions.RemoveMoney("bank", cost)
+ elseif isStarted(ESXExport) then fundResource = ESXExport
+ ESX.GetPlayerFromId(src).removeMoney(cost, "")
+ end
+ end
+
+ if fundResource == "" then
+ print("Cannot charge player - check starter.lua")
+ else
+ debugPrint("^6Bridge^7: ^2Charging ^2Player^7: '^6"..cost.."^7'", moneyType, fundResource)
+ end
+end
+RegisterNetEvent(getScript()..":server:ChargePlayer", function(cost, moneyType, newsrc)
+ debugPrint(GetInvokingResource())
+ if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
+ debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
+ return
+ end
+ chargePlayer(cost, moneyType, newsrc)
+end)
+
+--- Funds a player by adding money to their account.
+---
+--- @param fund number The amount to add.
+--- @param type string "cash" or "bank".
+--- @param newsrc number|nil Optional player ID; defaults to event source.
+---
+--- @usage
+--- ```lua
+--- fundPlayer(150, "cash", playerId)
+--- ```
+function fundPlayer(fund, moneyType, newsrc)
+ local src = newsrc or source
+ local fundResource = ""
+
+ if moneyType == "cash" then
+ if isStarted(OXInv) then
+ fundResource = OXInv
+ exports[OXInv]:AddItem(src, "money", fund)
+ elseif isStarted(QBExport) or isStarted(QBXExport) then
+ fundResource = QBExport
+ Core.Functions.GetPlayer(src).Functions.AddMoney("cash", fund)
+ elseif isStarted(ESXExport) then
+ fundResource = ESXExport
+ PlayESX.GetPlayerFromId(src).addMoney(fund, "")
+ end
+ elseif moneyType == "bank" then
+ if isStarted(QBExport) or isStarted(QBXExport) then
+ fundResource = QBExport
+ Core.Functions.GetPlayer(src).Functions.AddMoney("bank", fund)
+ elseif isStarted(ESXExport) then
+ fundResource = ESXExport
+ ESX.GetPlayerFromId(src).addMoney(fund, "")
+ end
+ end
+
+ if fundResource == "" then
+ print("Cannot fund player - check starter.lua")
+ else
+ debugPrint("^6Bridge^7: ^2Funding Player: '^2"..fund.."^7'", moneyType, fundResource)
+ end
+end
+
+-------------------------------------------------------------
+-- Item Consumption & Effects
+-------------------------------------------------------------
+
+--- Handles successful consumption of an item.
+---
+--- Plays a consumption animation, removes the item, updates player needs, handles alcohol effects,
+--- and checks for random rewards.
+---
+--- @param itemName string The name of the consumed item.
+--- @param type string The category of the item (e.g., "alcohol").
+--- @param data table Additional data (e.g., hunger and thirst values).
+---
+--- @usage
+--- ```lua
+--- -- Player consumes a health pack
+--- ConsumeSuccess("health_pack", "health")
+---
+--- -- Player consumes an alcohol drink
+--- ConsumeSuccess("beer", "alcohol")
+--- ```
+function ConsumeSuccess(itemName, type, data)
+ local hunger = data and data.hunger or Items[itemName].hunger
+ local thirst = data and data.thirst or Items[itemName].thirst
+
+ ExecuteCommand("e c")
+ removeItem(itemName, 1)
+
+ if isStarted(ESXExport) then
+ if hunger then
+ TriggerServerEvent(getScript()..":server:setNeed", "hunger", hunger * 10000)
+ end
+ if thirst then
+ TriggerServerEvent(getScript()..":server:setNeed", "thirst", thirst * 10000)
+ end
+ else
+ if hunger then
+ TriggerServerEvent(getScript()..":server:setNeed", "hunger", Core.Functions.GetPlayerData().metadata["hunger"] + hunger)
+ end
+ if thirst then
+ TriggerServerEvent(getScript()..":server:setNeed", "thirst", Core.Functions.GetPlayerData().metadata["thirst"] + thirst)
+ end
+ end
+
+ if type == "alcohol" then
+ alcoholCount = (alcoholCount or 0) + 1
+ if alcoholCount > 1 and alcoholCount < 4 then
+ TriggerEvent("evidence:client:SetStatus", "alcohol", 200)
+ elseif alcoholCount >= 4 then
+ TriggerEvent("evidence:client:SetStatus", "heavyalcohol", 200)
+ AlienEffect()
+ end
+ end
+
+ getRandomReward(itemName)
+end
+
+-------------------------------------------------------------
+-- Player Job & Information Utilities
+-------------------------------------------------------------
+
+--- Checks if a player has a specific job or gang (and optionally meets a minimum grade).
+---
+--- @param job string The job or gang name to check.
+--- @param source number|nil Optional player source; if nil, checks current player.
+--- @param grade number|nil Optional minimum grade level.
+--- @return boolean, boolean boolean Returns true and duty status if the check passes; false otherwise.
+---
+--- @usage
+--- ```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
+--- ```
+function hasJob(job, source, grade)
+ local hasJobFlag, duty = false, true
+ if source then
+ local src = tonumber(source)
+ if not src then print(tostring(source).." is not a valid player source") end
+ if isStarted(ESXExport) then
+ local info = ESX.GetPlayerFromId(src).job
+ while not info do
+ info = ESX.GetPlayerData(src).job
+ Wait(100)
+ end
+ if info.name == job then hasJobFlag = true end
+
+ elseif isStarted(OXCoreExport) then
+ local chunk = assert(load(LoadResourceFile('ox_core', ('imports/%s.lua'):format('server')), ('@@ox_core/%s'):format(file)))
+ chunk()
+ local player = Ox.GetPlayer(src)
+ for k, v in pairs(player.getGroups()) do
+ if k == job then hasJobFlag = true end
+ end
+
+ elseif isStarted(QBXExport) then
+ local jobinfo = exports[QBXExport]:GetPlayer(src).PlayerData.job
+ if jobinfo.name == job then
+ hasJobFlag = true
+ duty = exports[QBXExport]:GetPlayer(src).PlayerData.job.onduty
+ if grade and not (grade <= jobinfo.grade.level) then hasJobFlag = false end
+ end
+ local ganginfo = exports[QBXExport]:GetPlayer(src).PlayerData.gang
+ if ganginfo.name == job then
+ hasJobFlag = true
+ if grade and not (grade <= ganginfo.grade.level) then hasJobFlag = false end
+ end
+
+ elseif isStarted(QBExport) and not isStarted(QBXExport) then
+ if Core.Functions.GetPlayer then
+ local player = Core.Functions.GetPlayer(src)
+ if not player then print("Player not found for src: "..src) end
+ local jobinfo = player.PlayerData.job
+ if jobinfo.name == job then
+ hasJobFlag = true
+ duty = player.PlayerData.job.onduty
+ if grade and not (grade <= jobinfo.grade.level) then hasJobFlag = false end
+ end
+ local ganginfo = player.PlayerData.gang
+ if ganginfo.name == job then
+ hasJobFlag = true
+ if grade and not (grade <= ganginfo.grade.level) then hasJobFlag = false end
+ end
+ else
+ local jobinfo = exports[QBExport]:GetPlayer(src).PlayerData.job
+ if jobinfo.name == job then
+ hasJobFlag = true
+ duty = exports[QBExport]:GetPlayer(src).PlayerData.job.onduty
+ if grade and not (grade <= jobinfo.grade.level) then hasJobFlag = false end
+ end
+ local ganginfo = exports[QBExport]:GetPlayer(src).PlayerData.gang
+ if ganginfo.name == job then
+ hasJobFlag = true
+ if grade and not (grade <= ganginfo.grade.level) then hasJobFlag = false end
+ end
+ end
+ else
+ print("^4ERROR^7: ^2No Core detected for hasJob ^7- ^2Check ^3starter^1.^2lua^7")
+ end
+ else
+ -- Client-side check.
+ if isStarted(ESXExport) and ESX ~= nil then
+ local info = ESX.GetPlayerData().job
+ while not info do
+ info = ESX.GetPlayerData().job
+ Wait(100)
+ end
+ if info.name == job then hasJobFlag = true end
+
+ elseif isStarted(OXCoreExport) then
+ local info = OxPlayer.getGroups()
+ for k, v in pairs(info) do
+ if k == job then hasJobFlag = true break end
+ end
+
+ elseif isStarted(QBXExport) then
+ local info = exports[QBXExport]:GetPlayerData()
+ if info.job.name == job then
+ hasJobFlag = true
+ duty = info.job.onduty
+ if grade and not (grade <= info.job.grade.level) then hasJobFlag = false end
+ end
+ if info.gang.name == job then
+ hasJobFlag = true
+ if grade and not (grade <= info.gang.grade.level) then hasJobFlag = false end
+ end
+
+ elseif isStarted(QBExport) and not isStarted(QBXExport) then
+ local info = nil
+ Core.Functions.GetPlayerData(function(PlayerData) info = PlayerData end)
+ local jobinfo = info.job
+ if jobinfo.name == job then
+ hasJobFlag = true
+ duty = jobinfo.onduty
+ if grade and not (grade <= jobinfo.grade.level) then hasJobFlag = false end
+ end
+ local ganginfo = info.gang
+ if ganginfo.name == job then
+ hasJobFlag = true
+ if grade and not (grade <= ganginfo.grade.level) then hasJobFlag = false end
+ end
+ else
+ print("^4ERROR^7: ^2No Core detected for hasJob() ^7- ^2Check ^3starter^1.^2lua^7")
+ end
+ end
+ return hasJobFlag, duty
+end
+
+--- 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).
+---
+--- @param source number|nil Optional player server ID.
+--- @return table A table containing player details.
+---
+--- @usage
+--- ```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)
+--- ```
+function getPlayer(source)
+ local Player = {}
+ debugPrint("^6Bridge^7: ^2Getting ^3Player^2 info^7")
+
+ if source then
+ local src = tonumber(source)
+ if isStarted(ESXExport) then
+ local info = ESX.GetPlayerFromId(src)
+ Player = {
+ name = info.getName(),
+ cash = info.getMoney(),
+ bank = info.getAccount("bank").money,
+
+ firstname = info.variables.firstName,
+ lastname = info.variables.lastName,
+
+ source = info.source,
+ job = info.job.name,
+ --jobBoss = info.job.isboss,
+ --gang = info.gang.name,
+ --gangBoss = info.gang.isboss,
+ onDuty = info.job.onDuty,
+ --account = info.charinfo.account,
+ citizenId = info.identifier,
+ }
+
+ elseif isStarted(OXCoreExport) then
+ local file = ('imports/%s.lua'):format('server')
+ local import = LoadResourceFile('ox_core', file)
+ local chunk = assert(load(import, ('@@ox_core/%s'):format(file)))
+ chunk()
+ local player = Ox.GetPlayer(src)
+ Player = {
+ firstname = player.firstName,
+ lastname = player.lastName ,
+ name = ('%s %s'):format(player.firstName, player.lastName),
+ cash = exports[OXInv]:Search(src, 'count', "money"),
+ bank = 0,
+ source = src,
+ --job = OxPlayer.getGroups(),
+ --jobBoss = info.job.isboss,
+ --gang = OxPlayer.getGroups(),
+ --gangBoss = info.gang.isboss,
+ --onDuty = info.job.onduty,
+ --account = info.charinfo.account,
+ citizenId = player.stateId,
+
+ }
+ elseif isStarted(QBXExport) then
+ local info = exports[QBXExport]:GetPlayer(src)
+ Player = {
+ firstname = info.PlayerData.charinfo.firstname,
+ lastname = info.PlayerData.charinfo.lastname,
+ name = info.PlayerData.charinfo.firstname.." "..info.PlayerData.charinfo.lastname,
+ cash = exports[OXInv]:Search(src, 'count', "money"),
+ bank = info.Functions.GetMoney("bank"),
+ source = info.PlayerData.source,
+ job = info.PlayerData.job.name,
+ jobBoss = info.PlayerData.job.isboss,
+ jobInfo = info.PlayerData.job,
+ gang = info.PlayerData.gang.name,
+ gangInfo = info.PlayerData.gang,
+ gangBoss = info.PlayerData.gang.isboss,
+ onDuty = info.PlayerData.job.onduty,
+ account = info.PlayerData.charinfo.account,
+ citizenId = info.PlayerData.citizenid,
+ isDead = info.PlayerData.metadata["isdead"],
+ isDown = info.PlayerData.metadata["inlaststand"],
+ charInfo = info.charinfo,
+ }
+ elseif isStarted(QBExport) and not isStarted(QBXExport) then
+ if Core.Functions.GetPlayer then
+ local info = Core.Functions.GetPlayer(src).PlayerData
+ Player = {
+ firstname = info.charinfo.firstname,
+ lastname = info.charinfo.lastname,
+ name = info.charinfo.firstname.." "..info.charinfo.lastname,
+ cash = info.money["cash"],
+ bank = info.money["bank"],
+ source = info.source,
+ job = info.job.name,
+ jobBoss = info.job.isboss,
+ jobInfo = info.job,
+ gang = info.gang.name,
+ gangBoss = info.gang.isboss,
+ gangInfo = info.gang,
+ onDuty = info.job.onduty,
+ account = info.charinfo.account,
+ citizenId = info.citizenid,
+ isDead = info.metadata["isdead"],
+ isDown = info.metadata["inlaststand"],
+ charInfo = info.charinfo,
+ }
+ end
+ elseif isStarted(RSGExport) then
+ if Core.Functions.GetPlayer then
+ local info = Core.Functions.GetPlayer(src).PlayerData
+ Player = {
+ firstname = info.charinfo.firstname,
+ lastname = info.charinfo.lastname,
+ name = info.charinfo.firstname.." "..info.charinfo.lastname,
+ cash = info.money["cash"],
+ bank = info.money["bank"],
+ source = info.source,
+ job = info.job.name,
+ jobBoss = info.job.isboss,
+ jobInfo = info.job,
+ gang = info.gang.name,
+ gangBoss = info.gang.isboss,
+ gangInfo = info.gang,
+ onDuty = info.job.onduty,
+ account = info.charinfo.account,
+ citizenId = info.citizenid,
+ isDead = info.metadata["isdead"],
+ isDown = info.metadata["inlaststand"],
+ charInfo = info.charinfo,
+ }
+ end
+ else
+ print("^4ERROR^7: ^2No Core detected for getPlayer() - Check starter.lua")
+ end
+ else
+ -- Client-side: Get current player info.
+ if isStarted(ESXExport) and ESX ~= nil then
+ local info = ESX.GetPlayerData()
+ local cash, bank = 0, 0
+ for k, v in pairs(info.accounts) do
+ if v.name == "money" then cash = v.money end
+ if v.name == "bank" then bank = v.money end
+ end
+ Player = {
+ firstname = info.firstName,
+ lastname = info.lastName,
+ name = info.firstName.." "..info.lastName,
+ cash = cash,
+ bank = bank,
+ source = GetPlayerServerId(PlayerId()),
+ job = info.job.name,
+ --jobBoss = info.job.isboss,
+ --gang = info.gang.name,
+ --gangBoss = info.gang.isboss,
+ onDuty = info.job.onDuty,
+ --account = info.charinfo.account,
+ citizenId = info.identifier,
+ isDead = IsEntityDead(PlayerPedId()),
+ isDown = IsPedDeadOrDying(PlayerPedId(), true)
+ }
+ elseif isStarted(OXCoreExport) then
+ Player = {
+ firstname = OxPlayer.get("firstName"),
+ lastname = OxPlayer.get("lastName"),
+ name = OxPlayer.get("firstName").." "..OxPlayer.get("lastName"),
+ cash = exports[OXInv]:Search('count', "money"),
+ bank = 0,
+ source = GetPlayerServerId(PlayerId()),
+ job = OxPlayer.getGroups(),
+ --jobBoss = info.job.isboss,
+ gang = OxPlayer.getGroups(),
+ --gangBoss = info.gang.isboss,
+ --onDuty = info.job.onduty,
+ --account = info.charinfo.account,
+ citizenId = OxPlayer.userId,
+ isDead = IsEntityDead(PlayerPedId()),
+ isDown = IsPedDeadOrDying(PlayerPedId(), true)
+ }
+ elseif isStarted(QBXExport) then
+ local info = exports[QBXExport]:GetPlayerData()
+ Player = {
+ firstname = info.charinfo.firstname,
+ lastname = info.charinfo.lastname,
+ name = info.charinfo.firstname.." "..info.charinfo.lastname,
+ cash = info.money["cash"],
+ bank = info.money["bank"],
+ source = info.source,
+ job = info.job.name,
+ jobBoss = info.job.isboss,
+ jobInfo = info.job,
+ gang = info.gang.name,
+ gangBoss = info.gang.isboss,
+ gangInfo = info.gang,
+ onDuty = info.job.onduty,
+ account = info.charinfo.account,
+ citizenId = info.citizenid,
+ isDead = info.metadata["isdead"],
+ isDown = info.metadata["inlaststand"],
+ charInfo = info.charinfo,
+ }
+ elseif isStarted(QBExport) and not isStarted(QBXExport) then
+ local info = nil
+ Core.Functions.GetPlayerData(function(PlayerData) info = PlayerData end)
+ Player = {
+ firstname = info.charinfo.firstname,
+ lastname = info.charinfo.lastname,
+ name = info.charinfo.firstname.." "..info.charinfo.lastname,
+ cash = info.money["cash"],
+ bank = info.money["bank"],
+ source = info.source,
+ job = info.job.name,
+ jobBoss = info.job.isboss,
+ jobInfo = info.job,
+ gang = info.gang.name,
+ gangBoss = info.gang.isboss,
+ gangInfo = info.gang,
+ onDuty = info.job.onduty,
+ account = info.charinfo.account,
+ citizenId = info.citizenid,
+ isDead = info.metadata["isdead"],
+ isDown = info.metadata["inlaststand"],
+ charInfo = info.charinfo,
+ }
+ elseif isStarted(RSGExport) then
+ local info = nil
+ Core.Functions.GetPlayerData(function(PlayerData) info = PlayerData end)
+ Player = {
+ firstname = info.charinfo.firstname,
+ lastname = info.charinfo.lastname,
+ name = info.charinfo.firstname.." "..info.charinfo.lastname,
+ cash = info.money["cash"],
+ bank = info.money["bank"],
+ source = info.source,
+ job = info.job.name,
+ jobBoss = info.job.isboss,
+ jobInfo = info.job,
+ gang = info.gang.name,
+ gangBoss = info.gang.isboss,
+ gangInfo = info.gang,
+ onDuty = info.job.onduty,
+ account = info.charinfo.account,
+ citizenId = info.citizenid,
+ isDead = info.metadata["isdead"],
+ isDown = info.metadata["inlaststand"],
+ charInfo = info.charinfo,
+ }
+ else
+ print("^4ERROR^7: ^2No Core detected for hasJob ^7- ^2Check ^3starter^1.^2lua^7")
+ end
+ end
+ return Player
+end
+
+--- Retrieves all active players within a given radius from the specified coordinates.
+---
+--- @param coords vector3 The reference coordinates.
+--- @param radius number The radius within which to find players.
+--- @return table table An array of player IDs.
+---
+--- @usage
+--- ```lua
+--- local nearbyPlayers = GetPlayersFromCoords(vector3(100, 200, 30), 20)
+--- ```
+function GetPlayersFromCoords(coords, radius)
+ local players = {}
+ for _, playerId in ipairs(GetActivePlayers()) do
+ local ped = GetPlayerPed(playerId)
+ if ped and DoesEntityExist(ped) then
+ local playerCoords = GetEntityCoords(ped)
+ if #(coords - playerCoords) <= radius then
+ players[#players + 1] = playerId
+ end
+ end
+ end
+ return players
+end
\ No newline at end of file
diff --git a/shared/polyZone.lua b/shared/polyZone.lua
new file mode 100644
index 0000000..fbe5e42
--- /dev/null
+++ b/shared/polyZone.lua
@@ -0,0 +1,140 @@
+--[[
+ PolyZone Management Module
+ ----------------------------
+ This module automatically detects the available polyzone library (ox_lib or PolyZone)
+ and creates polygonal and circular zones accordingly. It also provides a function to remove
+ previously created zones.
+
+ Functions:
+ • createPoly(data) - Creates a polygonal zone.
+ • createCirclePoly(data) - Creates a circular zone.
+ • removePolyZone(Location) - Removes a created zone.
+]]
+
+-------------------------------------------------------------
+-- Polygonal Zone Creation
+-------------------------------------------------------------
+
+--- 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.
+---
+--- @param data table Zone configuration table with the following keys:
+--- - name (string): The zone's identifier.
+--- - debug (boolean): Whether debug mode is enabled.
+--- - points (table): A list of vec2 points defining the polygon.
+--- - onEnter (function): Callback when a player enters the zone.
+--- - onExit (function): Callback when a player exits the zone.
+---
+--- @return table|nil table Returns the created zone object or nil if creation failed.
+---
+---@usage
+---```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,
+---})
+---```
+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)
+ 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
+end
+
+-------------------------------------------------------------
+-- Circular Zone Creation
+-------------------------------------------------------------
+
+--- Creates a circular zone using the detected polyzone library (ox_lib or PolyZone).
+---
+--- When using ox_lib, it creates a sphere zone. For PolyZone, it creates a CircleZone and attaches
+--- onEnter and onExit callbacks.
+---
+--- @param data table Zone configuration with the following keys:
+--- - name (string): The zone's identifier.
+--- - coords (vector3): The center of the circle.
+--- - radius (number): The radius of the circle.
+--- - onEnter (function): Callback when a player enters the zone.
+--- - onExit (function): Callback when a player exits the zone.
+---
+--- @return table|nil table Returns the created circular zone object or nil if creation failed.
+---
+--- @usage
+--- ```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,
+--- })
+--- ```
+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")
+ end
+ debugPrint("^6Bridge^7: ^2Zone Stats - Coords: "..formatCoord(data.coords).." Radius: "..data.radius)
+ return Location
+end
+
+-------------------------------------------------------------
+-- PolyZone Removal Function
+-------------------------------------------------------------
+
+--- Removes a previously created polyzone.
+---
+--- Detects the active polyzone library and calls the appropriate removal method.
+---
+--- @param Location table The zone object to be removed.
+---
+--- @usage
+--- ```lua
+--- local zone = createPoly({...})
+---
+--- 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()
+ end
+end
\ No newline at end of file
diff --git a/shared/scaleEntity.lua b/shared/scaleEntity.lua
new file mode 100644
index 0000000..1247ebe
--- /dev/null
+++ b/shared/scaleEntity.lua
@@ -0,0 +1,77 @@
+local cacheOrigScale = {}
+local initialOffset = {}
+
+--- Sets the scale of an entity.
+---
+--- This function scales an entity by adjusting its forward, right, and up vectors.
+--- It also applies an initial offset to maintain the entity's position relative to the ground.
+---
+---@param entity number The entity ID to scale.
+---@param scale number The scale factor to apply to the entity.
+---
+---@usage
+--- ```lua
+--- -- Scale an entity to twice its original size
+--- SetEntityScale(entityId, 2.0)
+--- ```
+function SetEntityScale(entity, scale)
+ local forward, right, up = GetEntityMatrix(entity)
+ if not cacheOrigScale[entity] then
+ cacheOrigScale[entity] = {
+ forward = forward,
+ right = right,
+ up = up
+ }
+ end
+ local minDim, maxDim = GetModelDimensions(GetEntityModel(entity))
+ local originalHeight = maxDim.z - minDim.z
+ local newHeight = originalHeight * scale
+ initialOffset[entity] = (newHeight - originalHeight) / 3
+
+ local forwardTemp = cacheOrigScale[entity].forward * scale
+ local rightTemp = cacheOrigScale[entity].right * scale
+ local upTemp = cacheOrigScale[entity].up * scale
+
+ -- Apply the initial offset to the current position
+ local currentPosition = GetEntityCoords(entity)
+ local newPosition = vector3(currentPosition.x, currentPosition.y, currentPosition.z + initialOffset[entity])
+
+ SetEntityMatrix(entity, forwardTemp, rightTemp, upTemp, currentPosition)
+end
+
+--- Resets the scale of an entity to its original values.
+---
+--- This function restores an entity's original forward, right, and up vectors,
+--- effectively undoing any scaling applied by `SetEntityScale`.
+---
+---@param entity number The entity ID to reset.
+---
+---@usage
+--- ```lua
+--- -- Reset the scale of an entity
+--- resetScale(entityId)
+--- ```
+function resetScale(entity)
+ if cacheOrigScale[entity] then
+ SetEntityMatrix(entity, cacheOrigScale[entity].forward, cacheOrigScale[entity].right, cacheOrigScale[entity].up, GetEntityCoords(entity))
+ cacheOrigScale[entity] = nil
+ end
+end
+
+--[[
+CreateThread(function()
+ -- Example usage:
+ -- local prop = makeProp({prop = "v_res_r_figcat", coords = vec4(-1025.88, -1417.58, 5.43, 76.30)}, false, false)
+ -- local ped = makePed(`a_c_cat_01`, vec4(-1022.42, -1429.97, 13.79, 68.36), true, false, nil)
+ -- SetEntityCollision(prop, false, true)
+
+ SetEntityScale(prop, 12)
+ --[[CreateThread(function()
+ while true do
+ Wait(1000)
+ resetScale(prop)
+ Wait(1000)
+ end
+ end)
+end)
+]]
\ No newline at end of file
diff --git a/shared/scaleforms/bigMessageInstance.lua b/shared/scaleforms/bigMessageInstance.lua
new file mode 100644
index 0000000..4ba9d60
--- /dev/null
+++ b/shared/scaleforms/bigMessageInstance.lua
@@ -0,0 +1,306 @@
+--[[
+ BigMessage Module
+ -----------------
+ This module provides a flexible way to display large, attention-grabbing messages
+ on screen using a Scaleform movie ("MP_BIG_MESSAGE_FREEMODE"). It supports multiple
+ message types (mission passed, colored shard, old-style, simple shard, rank-up, weapon purchased,
+ and large multiplayer messages), including customizable transitions and durations.
+]]
+
+BigMessage = {}
+BigMessage.__index = BigMessage
+
+--- Creates a new BigMessage instance.
+--- @return table table A new BigMessage object.
+function BigMessage:new()
+ local self = setmetatable({}, BigMessage)
+ self.scaleform = nil
+ self.startTime = 0
+ self.duration = 0
+ self.transition = "TRANSITION_OUT"
+ self.transitionDuration = 0.15
+ self.transitionPreventAutoExpansion = false
+ self.transitionExecuted = false
+ self.manualDispose = false
+ self.isDisplaying = false
+ return self
+end
+
+--- Loads the Scaleform movie if it has not been loaded yet.
+function BigMessage:Load()
+ if self.scaleform then return end
+ self.scaleform = RequestScaleformMovie("MP_BIG_MESSAGE_FREEMODE")
+ while not HasScaleformMovieLoaded(self.scaleform) do
+ Wait(0)
+ end
+end
+
+--- Disposes of the Scaleform movie.
+--- If manualDispose is true, executes a transition before disposing.
+function BigMessage:Dispose()
+ if not self.scaleform then return end
+
+ if self.manualDispose then
+ BeginScaleformMovieMethod(self.scaleform, self.transition)
+ ScaleformMovieMethodAddParamBool(false)
+ ScaleformMovieMethodAddParamFloat(self.transitionDuration)
+ ScaleformMovieMethodAddParamBool(self.transitionPreventAutoExpansion)
+ EndScaleformMovieMethod()
+
+ -- Wait a fraction of the transition duration (in milliseconds)
+ Wait((self.transitionDuration * 0.5) * 1000)
+ self.manualDispose = false
+ end
+
+ self.startTime = 0
+ self.transitionExecuted = false
+ SetScaleformMovieAsNoLongerNeeded(self.scaleform)
+ self.scaleform = nil
+ self.isDisplaying = false
+end
+
+--- Updates the display by drawing the Scaleform movie fullscreen.
+function BigMessage:Update()
+ if not self.scaleform then return end
+
+ DrawScaleformMovieFullscreen(self.scaleform, 255, 255, 255, 255, 0)
+
+ if self.manualDispose then return end
+
+ if self.startTime ~= 0 and (GetGameTimer() - self.startTime) > self.duration then
+ if not self.transitionExecuted then
+ BeginScaleformMovieMethod(self.scaleform, self.transition)
+ ScaleformMovieMethodAddParamBool(false)
+ ScaleformMovieMethodAddParamFloat(self.transitionDuration)
+ ScaleformMovieMethodAddParamBool(self.transitionPreventAutoExpansion)
+ EndScaleformMovieMethod()
+ self.transitionExecuted = true
+ -- Extend duration slightly for smooth transition
+ self.duration = self.duration + ((self.transitionDuration * 0.5) * 1000)
+ else
+ self:Dispose()
+ end
+ end
+end
+
+--- Sets the transition properties for disposing the message.
+--- @param transition string The transition function name (default: "TRANSITION_OUT").
+--- @param duration number The duration for the transition (default: 0.4).
+--- @param preventAutoExpansion boolean Whether to prevent auto-expansion (default: true).
+function BigMessage:SetTransition(transition, duration, preventAutoExpansion)
+ self.transition = transition or "TRANSITION_OUT"
+ self.transitionDuration = duration or 0.4
+ self.transitionPreventAutoExpansion = preventAutoExpansion or true
+end
+
+--- Starts a thread to continuously update the HUD until the message is done.
+function BigMessage:StartUpdate()
+ if self.isDisplaying then return end
+
+ self.isDisplaying = true
+ CreateThread(function()
+ while self.isDisplaying do
+ Wait(0)
+ self:Update()
+ end
+ end)
+end
+
+--- Displays a mission passed message.
+--- @param msg string The message to display.
+--- @param duration number|nil The duration (in milliseconds) to display the message (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose the Scaleform after display (default: false).
+--- @usage
+--- ```lua
+--- BigMessage:ShowMissionPassedMessage("MISSION PASSED", 5000)
+--- ```
+function BigMessage:ShowMissionPassedMessage(msg, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_MISSION_PASSED_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ ScaleformMovieMethodAddParamPlayerNameString("")
+ ScaleformMovieMethodAddParamInt(100)
+ ScaleformMovieMethodAddParamBool(true)
+ ScaleformMovieMethodAddParamInt(0)
+ ScaleformMovieMethodAddParamBool(true)
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays a colored shard message.
+--- @param msg string The main message.
+--- @param desc string The description text.
+--- @param textColor number The text color index.
+--- @param bgColor number The background color index.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose the Scaleform (default: false).
+function BigMessage:ShowColoredShard(msg, desc, textColor, bgColor, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_SHARD_CENTERED_MP_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ ScaleformMovieMethodAddParamPlayerNameString(desc)
+ ScaleformMovieMethodAddParamInt(bgColor)
+ ScaleformMovieMethodAddParamInt(textColor)
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays an old-style mission passed message.
+--- @param msg string The message.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose (default: false).
+function BigMessage:ShowOldMessage(msg, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_MISSION_PASSED_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays a simple shard message.
+--- @param msg string The main message.
+--- @param subtitle string The subtitle text.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose (default: false).
+function BigMessage:ShowSimpleShard(msg, subtitle, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_SHARD_CREW_RANKUP_MP_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ ScaleformMovieMethodAddParamPlayerNameString(subtitle)
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays a rank-up message.
+--- @param msg string The main message.
+--- @param subtitle string The subtitle text.
+--- @param rank number The rank level achieved.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose (default: false).
+function BigMessage:ShowRankupMessage(msg, subtitle, rank, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_BIG_MP_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ ScaleformMovieMethodAddParamPlayerNameString(subtitle)
+ ScaleformMovieMethodAddParamInt(rank)
+ ScaleformMovieMethodAddParamPlayerNameString("")
+ ScaleformMovieMethodAddParamPlayerNameString("")
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays a weapon purchased message.
+--- @param bigMessage string The main message.
+--- @param weaponName string The name of the weapon purchased.
+--- @param weaponHash number The weapon hash.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose (default: false).
+function BigMessage:ShowWeaponPurchasedMessage(bigMessage, weaponName, weaponHash, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_WEAPON_PURCHASED")
+ ScaleformMovieMethodAddParamPlayerNameString(bigMessage)
+ ScaleformMovieMethodAddParamPlayerNameString(weaponName)
+ ScaleformMovieMethodAddParamInt(weaponHash)
+ ScaleformMovieMethodAddParamPlayerNameString("")
+ ScaleformMovieMethodAddParamInt(100)
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays a large multiplayer message.
+--- @param msg string The main message.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose (default: false).
+function BigMessage:ShowMpMessageLarge(msg, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_CENTERED_MP_MESSAGE_LARGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ ScaleformMovieMethodAddParamPlayerNameString("")
+ ScaleformMovieMethodAddParamInt(100)
+ ScaleformMovieMethodAddParamBool(true)
+ ScaleformMovieMethodAddParamInt(100)
+ EndScaleformMovieMethod()
+
+ BeginScaleformMovieMethod(self.scaleform, "TRANSITION_IN")
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Displays a "Wasted" multiplayer message.
+--- @param msg string The main message.
+--- @param subtitle string The subtitle text.
+--- @param duration number|nil Duration in milliseconds (default: 5000).
+--- @param manualDispose boolean|nil Whether to manually dispose (default: false).
+function BigMessage:ShowMpWastedMessage(msg, subtitle, duration, manualDispose)
+ duration = duration or 5000
+ self:Load()
+ self.startTime = GetGameTimer()
+ self.manualDispose = manualDispose or false
+
+ BeginScaleformMovieMethod(self.scaleform, "SHOW_SHARD_WASTED_MP_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(msg)
+ ScaleformMovieMethodAddParamPlayerNameString(subtitle)
+ EndScaleformMovieMethod()
+
+ self.duration = duration
+ self:StartUpdate()
+end
+
+--- Starts the update loop for displaying the message.
+function BigMessage:StartUpdate()
+ if self.isDisplaying then return end
+ self.isDisplaying = true
+ CreateThread(function()
+ while self.isDisplaying do
+ Wait(0)
+ self:Update()
+ end
+ end)
+end
+
+-- Create an instance of BigMessage and return it.
+BigMessage = BigMessage:new()
+
+return BigMessage
\ No newline at end of file
diff --git a/shared/scaleforms/countDownHandler.lua b/shared/scaleforms/countDownHandler.lua
new file mode 100644
index 0000000..6c15049
--- /dev/null
+++ b/shared/scaleforms/countDownHandler.lua
@@ -0,0 +1,135 @@
+--[[
+ CountdownHandler Module
+ -------------------------
+ This module provides a countdown HUD using a Scaleform movie ("COUNTDOWN").
+ It handles loading, updating, and disposing of the scaleform, as well as
+ playing sounds and displaying messages for each countdown tick.
+
+ TriggerNetEvent(getScript()..":startCountdown", 5, 25)
+]]
+
+CountdownHandler = {}
+CountdownHandler.__index = CountdownHandler
+
+--- Creates a new CountdownHandler instance.
+--- @return table table A new CountdownHandler object.
+function CountdownHandler:new()
+ local self = setmetatable({}, CountdownHandler)
+ self.scaleform = nil
+ self.renderCountdown = false
+ self.colour = { r = 255, g = 255, b = 255, a = 255 }
+ return self
+end
+
+--- Loads the "COUNTDOWN" scaleform movie.
+function CountdownHandler:Load()
+ if self.scaleform then
+ return
+ end
+ self.scaleform = RequestScaleformMovie("COUNTDOWN")
+ while not HasScaleformMovieLoaded(self.scaleform) do
+ Wait(0)
+ end
+end
+
+--- Disposes of the currently loaded scaleform movie.
+function CountdownHandler:Dispose()
+ if self.scaleform then
+ SetScaleformMovieAsNoLongerNeeded(self.scaleform)
+ self.scaleform = nil
+ end
+end
+
+--- Updates the HUD by drawing the scaleform movie fullscreen.
+function CountdownHandler:Update()
+ if self.scaleform then
+ DrawScaleformMovieFullscreen(self.scaleform, 255, 255, 255, 255, 0)
+ end
+end
+
+--- Displays a message on the countdown HUD.
+--- @param message string The message to display.
+function CountdownHandler:ShowMessage(message)
+ local r, g, b, a = self.colour.r, self.colour.g, self.colour.b, self.colour.a
+
+ -- Set the message in the scaleform.
+ BeginScaleformMovieMethod(self.scaleform, "SET_MESSAGE")
+ ScaleformMovieMethodAddParamPlayerNameString(message)
+ ScaleformMovieMethodAddParamInt(r)
+ ScaleformMovieMethodAddParamInt(g)
+ ScaleformMovieMethodAddParamInt(b)
+ ScaleformMovieMethodAddParamBool(true)
+ EndScaleformMovieMethod()
+
+ -- Trigger a fade effect (optional).
+ BeginScaleformMovieMethod(self.scaleform, "FADE_MP")
+ ScaleformMovieMethodAddParamPlayerNameString(message)
+ ScaleformMovieMethodAddParamInt(r)
+ ScaleformMovieMethodAddParamInt(g)
+ ScaleformMovieMethodAddParamInt(b)
+ EndScaleformMovieMethod()
+end
+
+--- Starts the countdown HUD.
+--- @param number number|nil The starting number for the countdown (default: 3).
+--- @param hudColour number|nil The HUD colour index (default: 18).
+--- @return boolean boolean True when the countdown has finished.
+--- @usage
+--- ```lua
+--- if CountdownHandler:Start(5, 25) then
+--- -- When run in an if statement, the script will wait until its finished to continue
+--- print("Countdown Complete")
+--- end
+--- ```
+function CountdownHandler:Start(number, hudColour)
+ local finished = false
+ number = number or 3
+ hudColour = hudColour or 18
+
+ -- Get HUD colour using framework function; alternatives could be added here.
+ local r, g, b, a = GetHudColour(hudColour)
+ self.colour = { r = r, g = g, b = b, a = a }
+
+ self:Load()
+
+ self.renderCountdown = true
+ CreateThread(function()
+ while self.renderCountdown do
+ Wait(0)
+ self:Update()
+ end
+ end)
+
+ -- Countdown logic
+ CreateThread(function()
+ local currentNumber = number
+ while currentNumber > 0 do
+ playSound("Count")
+ self:ShowMessage(tostring(currentNumber))
+ Wait(1000)
+ currentNumber = currentNumber - 1
+ end
+
+ playSound("Go")
+ self:ShowMessage("GO")
+ finished = true
+
+ Wait(1000)
+ self.renderCountdown = false
+ self:Dispose()
+ finished = true
+ end)
+
+ while not finished do Wait(10) end
+ return true
+end
+
+-- Create a singleton instance of CountdownHandler.
+CountdownHandler = CountdownHandler:new()
+
+-- Register an event to start the countdown.
+RegisterNetEvent(getScript()..":startCountdown", function(number, hudColour)
+ CountdownHandler:Start(number, hudColour)
+end)
+
+return CountdownHandler
\ No newline at end of file
diff --git a/shared/scaleforms/debugScaleform.lua b/shared/scaleforms/debugScaleform.lua
new file mode 100644
index 0000000..641f17e
--- /dev/null
+++ b/shared/scaleforms/debugScaleform.lua
@@ -0,0 +1,44 @@
+-------------------------------------------------------------
+-- Debug Text Display Functionality
+-------------------------------------------------------------
+
+--- Draws debug text on the screen if debugMode is enabled.
+---
+--- Calculates a background rectangle based on the number of text lines and renders each line on-screen.
+---
+--- @param textTable table An array of strings to display.
+--- @param loc vector2 (Optional) Top-left coordinate for the text box (default: vec2(0.05, 0.65)).
+---
+--- @usage
+--- ```lua
+---CreateThread(function()
+--- while true do
+--- debugScaleForm({
+--- "Line 1: Debug info",
+--- "Line 2: More info"
+--- })
+--- Wait(0)
+--- end
+---end)
+--- ```
+function debugScaleForm(textTable, loc)
+ if debugMode then
+ loc = loc or vec2(0.05, 0.65)
+
+ local lineHeight = 0.025 -- Height per line.
+ local totalHeight = #textTable * lineHeight
+ local boxPadding = 0.01 -- Padding around the text.
+ local size = vec2(0.18, totalHeight + boxPadding * 2)
+
+ -- Draw background rectangle.
+ DrawRect(loc.x + size.x / 2, loc.y + size.y / 2, size.x, size.y, 0, 0, 0, 200)
+
+ -- Render each line of text.
+ for i = 1, #textTable do
+ SetTextScale(0.30, 0.30)
+ BeginTextCommandDisplayText("STRING")
+ AddTextComponentSubstringKeyboardDisplay(textTable[i])
+ EndTextCommandDisplayText(loc.x + 0.005, loc.y + (i - 1) * lineHeight + 0.01)
+ end
+ end
+end
\ No newline at end of file
diff --git a/shared/scaleforms/instructionalButtons.lua b/shared/scaleforms/instructionalButtons.lua
new file mode 100644
index 0000000..9fdfd71
--- /dev/null
+++ b/shared/scaleforms/instructionalButtons.lua
@@ -0,0 +1,110 @@
+-------------------------------------------------------------
+-- Instructional Buttons Functionality
+-------------------------------------------------------------
+
+--- Loads and draws instructional buttons on-screen using a scaleform movie.
+---
+--- Requests the "instructional_buttons" scaleform, clears previous data, sets clear space,
+--- creates data slots for each button option provided in `info`, and then draws the scaleform fullscreen.
+---
+--- @param info table An array of tables, where each table represents a button option:
+--- - keys (table): An array of key codes (e.g., {38, 29}) to display.
+--- - text (string): The label for the button.
+---
+--- @usage
+--- ```lua
+---CreateThread(function()
+--- while true do
+--- makeInstructionalButtons({
+--- { keys = {38, 29}, text = "Open Menu" },
+--- { keys = {45}, text = "Close Menu" }
+--- })
+--- Wait(0)
+--- end
+---end)
+--- ```
+function makeInstructionalButtons(info)
+ local build = RequestScaleformMovie("instructional_buttons")
+ while not HasScaleformMovieLoaded(build) do Wait(0) end
+
+ -- Draw the scaleform fullscreen (initial draw).
+ DrawScaleformMovieFullscreen(build, 255, 255, 255, 0, 0)
+
+ -- Clear previous instructions.
+ BeginScaleformMovieMethod(build, "CLEAR_ALL")
+ EndScaleformMovieMethod()
+
+ -- Set clear spacing between buttons.
+ BeginScaleformMovieMethod(build, "SET_CLEAR_SPACE")
+ ScaleformMovieMethodAddParamInt(200)
+ EndScaleformMovieMethod()
+
+ -- Add each button option to the scaleform.
+ for i = 1, #info do
+ BeginScaleformMovieMethod(build, "SET_DATA_SLOT")
+ ScaleformMovieMethodAddParamInt(i - 1)
+ for k = 1, #info[i].keys do
+ ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(2, info[i].keys[k], true))
+ end
+ BeginTextCommandScaleformString("STRING")
+ AddTextComponentSubstringKeyboardDisplay(info[i].text)
+ EndTextCommandScaleformString()
+ EndScaleformMovieMethod()
+ end
+
+ -- Draw the instructional buttons.
+ BeginScaleformMovieMethod(build, "DRAW_INSTRUCTIONAL_BUTTONS")
+ EndScaleformMovieMethod()
+
+ -- Set a translucent black background.
+ BeginScaleformMovieMethod(build, "SET_BACKGROUND_COLOUR")
+ ScaleformMovieMethodAddParamInt(0)
+ ScaleformMovieMethodAddParamInt(0)
+ ScaleformMovieMethodAddParamInt(0)
+ ScaleformMovieMethodAddParamInt(80)
+ EndScaleformMovieMethod()
+
+ -- Final full-screen draw with full opacity.
+ DrawScaleformMovieFullscreen(build, 255, 255, 255, 255, 0)
+end
+
+-- EXPERIMENTAL --
+-- RedM Button Prompts --
+-- Creates the promot, then shows it, this needs to be run in a loop
+local promptGroups = {}
+
+function makeRedInstructionalButtons(info, title)
+ if not promptGroups[title] then -- Create group if not exists
+ promptGroups[title] = {
+ title = CreateVarString(10, 'LITERAL_STRING', title),
+ id = GetRandomIntInRange(0, 0xffffff),
+ prompts = {},
+ }
+ for i = 1, #info do
+ promptGroups[title].prompts[i] = {
+ keys = info[i].keys,
+ text = info[i].text,
+ }
+ local keyTitle = CreateVarString(10, 'LITERAL_STRING', info[i].text)
+ -- Create one prompt per entry
+ local promptSet = UiPromptRegisterBegin()
+ -- Register all keys for this prompt
+ for k = 1, #info[i].keys do
+ PromptSetControlAction(promptSet, info[i].keys[k])
+ end
+ PromptSetText(promptSet, keyTitle)
+ PromptSetEnabled(promptSet, true)
+ PromptSetVisible(promptSet, true)
+ PromptSetGroup(promptSet, promptGroups[title].id)
+ PromptRegisterEnd(promptSet)
+ end
+ end
+ PromptSetActiveGroupThisFrame(promptGroups[title].id, promptGroups[title].title)
+end
+
+onResourceStop(function()
+ for k, v in pairs(promptGroups) do
+ print("^5Bridge^7: ^2Removing Prompt Group^7: ^3" .. k .. "^7")
+ PromptDelete(promptGroups[k].id, 1)
+ end
+end, true)
\ No newline at end of file
diff --git a/shared/scaleforms/scaleform_basic.lua b/shared/scaleforms/scaleform_basic.lua
new file mode 100644
index 0000000..aa6b75c
--- /dev/null
+++ b/shared/scaleforms/scaleform_basic.lua
@@ -0,0 +1,115 @@
+-------------------------------------------------------------
+-- 3D Text Rendering
+-------------------------------------------------------------
+
+--- Draws 3D text at specified world coordinates.
+---
+--- Configures text properties, draws the text, and displays a background rectangle behind it.
+---
+--- @param coord table A vector3 with x, y, and z coordinates.
+--- @param text string The text to display.
+--- @param highlight boolean (Optional) If true, highlights parts of the text.
+---
+--- @usage
+--- ```lua
+--- CreateThread(function()
+--- while true do
+--- DrawText3D(vector3(100, 200, 300), "Hello World", true)
+--- Wait(0)
+--- 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 -- max 99
+ local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text
+ AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text)
+ SetDrawOrigin(coord.x, coord.y, coord.z, 0)
+ DrawText(0.0, 0.0)
+ local count, length = GetLineCountAndMaxLength(text)
+
+ local padding = 0.005
+ local heightFactor = (count / 43) + padding
+ local weightFactor = (length / 150) + padding
+
+ local height = (heightFactor / 2) - padding / 1
+ local width = (weightFactor / 2) - padding / 1
+
+ DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150)
+ ClearDrawOrigin()
+end
+
+--- Calculates the number of lines and the maximum line length from the given text.
+---
+--- @param text string The text to analyze.
+--- @return number, number The line count and maximum line length.
+---
+--- @usage
+--- ```lua
+--- local count, maxLen = GetLineCountAndMaxLength("Hello World")
+--- ```
+function GetLineCountAndMaxLength(text)
+ local lineCount, maxLength = 0, 0
+ for line in text:gmatch("[^\n]+") do
+ lineCount += 1
+ local lineLength = string.len(line)
+ if lineLength > maxLength then
+ maxLength = lineLength
+ end
+ end
+ if lineCount == 0 then lineCount = 1 end
+ return lineCount, maxLength
+end
+
+-------------------------------------------------------------
+-- Additional UI Helpers
+-------------------------------------------------------------
+
+--- Displays a help message on the screen.
+---
+--- @param text string The message to display.
+---
+--- @usage
+--- ```lua
+--- DisplayHelpMsg("Press E to interact")
+--- ```
+function DisplayHelpMsg(text)
+ BeginTextCommandDisplayHelp("STRING")
+ AddTextComponentScaleform(text)
+ EndTextCommandDisplayHelp(0, true, false, -1)
+end
+
+--- Displays a "Saving/Loading" spinner with a custom message.
+---
+--- @param text string The message to display alongside the spinner.
+---
+--- @usage
+--- ```lua
+--- displaySpinner("Saving data...")
+--- ```
+function displaySpinner(text)
+ BeginTextCommandBusyspinnerOn('STRING')
+ AddTextComponentSubstringPlayerName(text)
+ EndTextCommandBusyspinnerOn(4)
+end
+
+--- Stops the "Saving/Loading" spinner.
+---
+--- This function should only be called client-side.
+---
+--- @usage
+--- ```lua
+--- stopSpinner()
+--- ```
+function stopSpinner()
+ if not isServer() then
+ BusyspinnerOff()
+ end
+end
\ No newline at end of file
diff --git a/shared/scaleforms/timerBars.lua b/shared/scaleforms/timerBars.lua
new file mode 100644
index 0000000..59f9fea
--- /dev/null
+++ b/shared/scaleforms/timerBars.lua
@@ -0,0 +1,78 @@
+--- Creates and displays a timer HUD on the screen.
+--- Draws a title (if provided) and a series of timer bars from the supplied data.
+---
+--- @param title string|nil Optional title to display at the top of the HUD.
+--- @param data table A table of timer bar entries. Each entry should include:
+--- - stat (string): The statistic name.
+--- - value (string): The value to display.
+--- - multi (number|nil): Optional, indicates multiple checkpoints (e.g., progress levels).
+--- @param alpha number|nil Optional alpha value (transparency) for the HUD; defaults to 255.
+---
+--- @usage
+--- ```lua
+--- createTimerHud("Timer", {
+--- { stat = "Health", value = "85%" },
+--- { stat = "Armor", value = "50%", multi = 2 },
+--- { stat = "Stamina", value = "100%" },
+--- }, 255)
+--- ```
+function createTimerHud(title, data, alpha)
+ loadTextureDict("timerbars")
+
+ local loc = vec2(0.89, 0.90)
+ alpha = alpha or 255 -- Default to fully opaque if alpha is not provided
+
+ if title then
+ local x = loc.x+0.037
+ local y = 0.1
+
+ DrawSprite("timerbars", "all_black_bg", x, y, 0.12, 0.05, 0.0, 255, 255, 255, alpha)
+ SetTextScale(0.80, 0.80)
+ SetTextWrap(0.75, 0.985)
+ SetTextJustification(2)
+ SetTextFont(4)
+ SetTextColour(255, 255, 255, alpha)
+ BeginTextCommandDisplayText("STRING")
+ AddTextComponentSubstringKeyboardDisplay("~y~"..title)
+ EndTextCommandDisplayText(x+0.06, y - 0.026)
+ end
+
+ local displayIndex = 0
+ for i = #data, 1, -1 do
+ local space = 0.044 * displayIndex
+
+ DrawSprite("timerbars", "all_black_bg", loc.x+0.02, loc.y - space, 0.15, 0.04, 0.0, 255, 255, 255, alpha)
+ SetTextScale(0.0, 0.35)
+ SetTextWrap(0.5, 0.92)
+ SetTextJustification(2)
+ SetTextColour(255, 255, 255, alpha)
+ BeginTextCommandDisplayText("STRING")
+ AddTextComponentSubstringKeyboardDisplay(data[i].stat:upper())
+ EndTextCommandDisplayText(loc.x-0.05, (loc.y - space) - 0.0125)
+
+ SetTextScale(0.55, 0.55)
+ SetTextWrap(0.85, 0.98 - (data[i].multi and 0.026 or 0.0))
+ SetTextFont(4)
+ SetTextJustification(2)
+ SetTextColour(255, 255, 255, alpha)
+ if data[i].multi then
+ local startX = 0.071
+ DrawSprite("timerbars", "circle_checkpoints",
+ loc.x + startX, (loc.y - space)+0.005,
+ 0.011, 0.018, 0.0, 255, 191, 0, 200)
+
+ DrawSprite("timerbars", "circle_checkpoints",
+ loc.x + (startX + 0.008), (loc.y - space)+0.005,
+ 0.011, 0.018, 0.0, 255, 191, 0, data[i].multi > 1 and 200 or 75)
+
+ DrawSprite("timerbars", "circle_checkpoints",
+ loc.x + (startX + 0.016), (loc.y - space)+0.005,
+ 0.011, 0.018, 0.0, 255, 191, 0, data[i].multi > 2 and 200 or 75)
+ end
+ BeginTextCommandDisplayText("STRING")
+ AddTextComponentSubstringKeyboardDisplay(data[i].value)
+ EndTextCommandDisplayText(loc.x - 0.02, (loc.y - space) - 0.017)
+ displayIndex += 1
+ end
+ makeInstructionalButtons({ { text = "Exit", keys = { 194 }}})
+end
\ No newline at end of file
diff --git a/shared/shops.lua b/shared/shops.lua
new file mode 100644
index 0000000..b1d39d4
--- /dev/null
+++ b/shared/shops.lua
@@ -0,0 +1,219 @@
+-------------------------------------------------------------
+-- 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),
+ headertxt = data.sellTable.Header and "Amount of items: "..countTable(data.sellTable.Items) or "",
+ 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, 2)
+ playAnim(dict, "givetake2_b", 0.3, 2, 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)
+ 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(QBInv) then
+ if QBInvNew then
+ TriggerServerEvent(getScript()..':server:OpenShopNewQB', data.shop)
+ else
+ TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items)
+ end
+ elseif isStarted(RSGInv) then
+ TriggerServerEvent(getScript()..':server:OpenShopNewRSG', data.shop)
+ end
+ lookEnt(data.coords)
+end
+
+--- Server event handler for opening a shop using the new QB inventory system.
+RegisterNetEvent(getScript()..':server:OpenShopNewQB', function(data)
+ exports[QBInv]:OpenShop(source, data)
+end)
+
+RegisterNetEvent(getScript()..':server:OpenShopNewRSG', function(data)
+ exports[RSGInv]:OpenShop(source, data)
+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(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(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
\ No newline at end of file
diff --git a/shared/skillcheck.lua b/shared/skillcheck.lua
new file mode 100644
index 0000000..945d69a
--- /dev/null
+++ b/shared/skillcheck.lua
@@ -0,0 +1,156 @@
+local activeSkillCheck = false
+
+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
+ 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)
+end
\ No newline at end of file
diff --git a/shared/societybank.lua b/shared/societybank.lua
new file mode 100644
index 0000000..2657ed6
--- /dev/null
+++ b/shared/societybank.lua
@@ -0,0 +1,180 @@
+--[[
+ Society Banking Module
+ ------------------------
+ This module provides functions to interact with society bank accounts across
+ different banking systems. Supported systems include:
+ • qb-banking
+ • esx_society *testing*
+ • Renewed-Banking
+ • fd_banking
+ • okokBanking
+]]
+
+--- 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.
+--- @usage
+--- ```lua
+--- local balance = getSocietyAccount("police")
+--- print("Police account balance: $"..balance)
+--- ```
+function getSocietyAccount(society)
+ local bankScript, amount = "", 0
+ if society == nil or society == "none" then return amount end
+
+ if isStarted("qb-banking") then
+ bankScript = "qb-banking"
+ if not exports["qb-banking"]:GetAccount(society) then
+ if Jobs[society] then
+ print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
+ exports["qb-banking"]:CreateJobAccount(society, 0)
+ Wait(150)
+ elseif Gangs[society] then
+ print("^6Bridge^7: ^2Making new bank account in ^7'^3qb-banking^7' ^2for ^7'^3"..society.."^7'")
+ exports["qb-banking"]:CreateGangAccount(society, 0)
+ Wait(150)
+ end
+ end
+ amount = exports["qb-banking"]:GetAccountBalance(society)
+
+ --elseif isStarted("esx_society") then
+ -- bankScript = "esx_society"
+ -- -- Since esx_society does not have a native client export for retrieving money,
+ -- -- we use a server callback to get the final amount.
+ -- amount = triggerCallback(getScript()..":getESXSocietyAccount", society) or 0
+
+ elseif isStarted("Renewed-Banking") then
+ bankScript = "Renewed-Banking"
+ amount = exports["Renewed-Banking"]:getAccountMoney(society)
+
+ elseif isStarted("fd_banking") then
+ bankScript = "fd_banking"
+ amount = exports["fd_banking"]:GetAccount(society)
+
+ elseif isStarted("okokBanking") then
+ bankScript = "okokBanking"
+ amount = exports['okokBanking']:GetAccount(society)
+ 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
+
+ return amount
+end
+
+--- Deducts funds from a society's bank account.
+--- @param society string The identifier of the society.
+--- @param amount number The amount of money to remove.
+--- @usage
+--- ```lua
+--- 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)
+
+ 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
+end
+
+--- Adds funds to a society's bank account.
+--- @param society string The identifier of the society.
+--- @param amount number The amount of money to add.
+--- @usage
+--- ```lua
+--- 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
+ 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)
+end
\ No newline at end of file
diff --git a/shared/stashcontrol.lua b/shared/stashcontrol.lua
new file mode 100644
index 0000000..f392554
--- /dev/null
+++ b/shared/stashcontrol.lua
@@ -0,0 +1,463 @@
+--[[
+ 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 ^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.." ^2Items found in cache, skipping 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 = 15000 -- 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
+ local successes = 0
+ local itemCount = countTable(itemTable)
+ -- Iterate over each provided stash name.
+ for _, name in pairs(stashes) do
+ Wait(10) -- Delay to avoid multiple callbacks issues.
+ GetStashTimeout(name)
+ for item, amount in pairs(itemTable) do
+ debugPrint("^6Bridge^7: ^2Checking "..(name and " '^3"..name.."^7'" or "").." ingredients - ^6"..item.."^7")
+ if stashhasItem(stashCache[name].items, item, amount) then
+ successes += 1
+ if successes == itemCount then
+ return true, name
+ end
+ end
+ 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(QBInv) then
+ if QBInvNew then
+ TriggerServerEvent(getScript()..':server:OpenStashQB', {
+ 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:OpenStashRSG', {
+ 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
+
+-- Register an event for opening QB stashes.
+RegisterNetEvent(getScript()..':server:OpenStashQB', function(data)
+ exports[QBInv]:OpenInventory(source, data.stashName, data)
+end)
+
+RegisterNetEvent(getScript()..':server:OpenStashRSG', function(data)
+ exports[RSGInv]:OpenInventory(source, data.stashName, data)
+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
+ stashItems = exports[OXInv]:Inventory(stashName).items
+
+ 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(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
+ local result = MySQL.scalar.await("SELECT items FROM "..(QBInvNew and "inventories" or "stashitem").." WHERE identifier = ?", { stashName })
+ if result then stashItems = json.decode(result) 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 isStarted(OXInv) then
+ for k, v in pairs(items) do
+ if type(stashName) == "table" then
+ 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
+ else
+ exports[OXInv]:RemoveItem(stashName, k, v)
+ end
+ end
+
+ elseif isStarted(QSInv) 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"..QBInv, k, v)
+ exports[QSInv]:RemoveItemIntoStash(stashName, k, v, l)
+ end
+ end
+ end
+ end
+
+ elseif isStarted(CoreInv) then
+ for k, v in pairs(items) do
+ exports[CoreInv]:removeItemExact(stashName, 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, 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, k, v)
+ debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..OrigenInv, k, v)
+ 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"..QBInv, k, v)
+ stashItems[l].amount -= v
+ end
+ end
+ end
+ end
+ debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash ^7'^6"..stashName.."^7'")
+ MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
+ ['stash'] = stashName,
+ ['items'] = json.encode(stashItems)
+ })
+
+ elseif isStarted(QBInv) then
+ if QBInvNew then
+ for k, v in pairs(items) do
+ exports[QBInv]:RemoveItem(stashName[1], k, v, false, 'crafting')
+ debugPrint("^6Bridge^7: ^2Removing item from ^3Stash^2 with ^7"..QBInv, k, v)
+ end
+ debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QB^2 stash ^7'^6"..stashName[1].."^7'")
+ MySQL.Async.insert('INSERT INTO inventories (identifier, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
+ ['stash'] = stashName[1],
+ ['items'] = json.encode(stashItems)
+ })
+ 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.."^7'")
+ MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', {
+ ['stash'] = stashName,
+ ['items'] = json.encode(stashItems)
+ })
+ 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, QBInv, PSInv }
+ 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
diff --git a/shared/targets.lua b/shared/targets.lua
new file mode 100644
index 0000000..05d0621
--- /dev/null
+++ b/shared/targets.lua
@@ -0,0 +1,612 @@
+--[[
+ Experimental GTA In-World Text Prompts Targets Module
+ -------------------------------------------------------
+ This module handles the creation, removal, and management of in-world text targets
+ for interacting with entities and zones using GTA text prompts. It supports multiple
+ targeting systems: OX Target, QB Target, or a fallback using DrawText3D.
+
+ Available functionalities:
+ • createEntityTarget - Creates a target for a specific entity.
+ • createBoxTarget - Creates a box-shaped zone target.
+ • createCircleTarget - Creates a circular zone target.
+ • createModelTarget - Creates a target for specified models.
+ • removeEntityTarget - Removes a target from an entity.
+ • removeZoneTarget - Removes a zone target.
+
+ Fallback: If no targeting system is detected (or if disabled via Config.System.DontUseTarget),
+ the module uses DrawText3D prompts. This is experimental and may not work as expected.
+]]
+
+-------------------------------------------------------------
+-- 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.
+
+-------------------------------------------------------------
+-- Entity Target Creation
+-------------------------------------------------------------
+
+--- Creates a target for an entity with specified options and interaction distance.
+--- Supports different targeting systems (OX Target, QB Target, or custom DrawText3D)
+--- based on the server configuration.
+---
+--- @param entity number The entity ID for which the target is created.
+--- @param opts table Array of option tables. Each option should include:
+--- - icon (string): The icon to display.
+--- - label (string): The text label for the option.
+--- - item (string|nil): (Optional) An associated item.
+--- - job (string|nil): (Optional) The job required to interact.
+--- - gang (string|nil): (Optional) The gang required to interact.
+--- - action (function|nil): (Optional) The function executed on selection.
+--- @param dist number The interaction distance for the target.
+---
+--- @usage
+--- ```lua
+---createEntityTarget(entityId, {
+--- {
+--- action = function()
+--- openStorage()
+--- end,
+--- icon = "fas fa-box",
+--- job = "police",
+--- label = "Open Storage",
+--- },
+---}, 2.0)
+--- ```
+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,
+ item = 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)
+ end
+end
+
+-------------------------------------------------------------
+-- Box Zone Target Creation
+-------------------------------------------------------------
+
+--- Creates a box-shaped target zone with specified options and interaction distance.
+--- Supports different targeting systems based on the server configuration.
+---@param data table A table containing the box zone configuration.
+--- - name (`string`): The name identifier for the zone.
+--- - coords (`vector3`): The center coordinates of the box.
+--- - width (`number`): The width of the box.
+--- - height (`number`): The height of the box.
+--- - options (`table`): A table with additional options:
+--- - heading (`number`): The rotation angle of the box.
+--- - debugPoly (`boolean`): Whether to enable debug mode for the zone.
+---
+---@param opts table A table of option configurations for the target.
+--- - icon (`string`): The icon to display for the option.
+--- - label (`string`): The label text for the option.
+--- - item (`string|nil`): (Optional) The item associated with the option.
+--- - job (`string|nil`): (Optional) The job required to interact with the option.
+--- - gang (`string|nil`): (Optional) The gang required to interact with the option.
+--- - onSelect (`function|nil`): (Optional) The function to execute when the option is selected.
+---@param dist number The interaction distance for the target.
+---
+---@return string|table name identifier or target object of the created zone.
+---
+---@usage
+---```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)
+---```
+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,
+ item = 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]
+ end
+end
+
+-------------------------------------------------------------
+-- Circle Zone Target Creation
+-------------------------------------------------------------
+
+--- Creates a circular target zone with specified options and interaction distance.
+--- Supports different targeting systems based on server configuration.
+---
+---@param data table A table containing the circle zone configuration.
+--- - name (`string`): The name identifier for the zone.
+--- - coords (`vector3`): The center coordinates of the circle.
+--- - radius (`number`): The radius of the circle.
+--- - options (`table`): A table with additional options:
+--- - debugPoly (`boolean`): Whether to enable debug mode for the zone.
+---
+---@param opts table A table of option configurations for the target.
+--- - icon (`string`): The icon to display for the option.
+--- - label (`string`): The label text for the option.
+--- - item (`string|nil`): (Optional) The item associated with the option.
+--- - job (`string|nil`): (Optional) The job required to interact with the option.
+--- - gang (`string|nil`): (Optional) The gang required to interact with the option.
+--- - onSelect (`function|nil`): (Optional) The function to execute when the option is selected.
+---@param dist number The interaction distance for the target.
+---
+---@return string|table name identifier or target object of the created zone.
+---
+---@usage
+--- ```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)
+--- ```
+function createCircleTarget(data, opts, dist)
+ 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
+ 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,
+ item = 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
+end
+
+-------------------------------------------------------------
+-- Model Target Creation
+-------------------------------------------------------------
+
+--- Creates a target for models with specified options and interaction distance.
+--- Supports different targeting systems (OX Target, QB Target) based on server configuration.
+---
+--- @param models table Array of model identifiers.
+--- @param opts table Array of option tables (same structure as in createEntityTarget).
+--- @param dist number The interaction distance for the target.
+---
+--- @usage
+--- ```lua
+---createModelTarget({ model1, model2 },
+---{
+--- {
+--- action = function()
+--- openStorage()
+--- end,
+--- icon = "fas fa-box",
+--- job = "police",
+--- label = "Open Storage",
+--- },
+---}, 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,
+ item = opts[i].item or nil,
+ groups = opts[i].job or opts[i].gang,
+ onSelect = opts[i].action,
+ distance = dist,
+ canInteract = opts[i].canInteract or nil,
+ }
+ end
+ exports[OXTargetExport]:addModel(models, options)
+ elseif isStarted(QBTargetExport) then
+ debugPrint("^6Bridge^7: ^2Creating new ^3Model^2 target with ^6"..QBTargetExport)
+ local options = { options = opts, distance = dist }
+ exports[QBTargetExport]:AddTargetModel(models, options)
+ end
+end
+
+-------------------------------------------------------------
+-- Target Removal Functions
+-------------------------------------------------------------
+
+--- Removes a previously created entity target.
+---
+--- @param entity number The entity ID whose target should be removed.
+---
+--- @usage
+--- ```lua
+--- removeEntityTarget(entityId)
+--- ```
+function removeEntityTarget(entity)
+ if isStarted(QBTargetExport) then
+ exports[QBTargetExport]:RemoveTargetEntity(entity)
+ end
+ if isStarted(OXTargetExport) then
+ exports[OXTargetExport]:removeLocalEntity(entity, nil)
+ end
+ if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then
+ TextTargets[entity] = nil
+ end
+end
+
+--- Removes a previously created zone target.
+---
+--- @param target string|table The name identifier or target object of the zone to remove.
+---
+--- @usage
+--- ```lua
+--- removeZoneTarget('centralPark')
+--- removeZoneTarget(targetObject)
+--- ```
+function removeZoneTarget(target)
+ if isStarted(QBTargetExport) then
+ exports[QBTargetExport]:RemoveZone(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
+ end
+end
+
+--- Removes a previously created model target.
+---
+--- @param model table The model ID whose target should be removed.
+---
+--- @usage
+--- ```lua
+--- removeModelTarget(model)
+--- ```
+function removeModelTarget(model)
+ if isStarted(QBTargetExport) then
+ exports[QBTargetExport]:RemoveTargetModel(model, "Test")
+ 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)
+ end
+ end)
+end
+
+function ShowFloatingHelpNotification(coord, text, highlight)
+ AddTextEntry("FloatingText", text)
+ SetFloatingHelpTextWorldPosition(1, coord.x, coord.y, coord.z)
+ SetFloatingHelpTextStyle(1, 1, 62, -1, 3, 0)
+ BeginTextCommandDisplayHelp("FloatingText")
+ 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()
+ -- Remove entity targets.
+ for i = 1, #targetEntities do
+ if isStarted(OXTargetExport) then
+ exports[OXTargetExport]:removeLocalEntity(targetEntities[i], nil)
+ elseif isStarted(QBTargetExport) then
+ exports[QBTargetExport]:RemoveTargetEntity(targetEntities[i])
+ end
+ end
+ -- Remove box zone targets.
+ for i = 1, #boxTargets do
+ if isStarted(OXTargetExport) then
+ exports[OXTargetExport]:removeZone(boxTargets[i], true)
+ elseif isStarted(QBTargetExport) then
+ exports[QBTargetExport]:RemoveZone(boxTargets[i].name)
+ end
+ end
+ -- Remove circle zone targets.
+ for i = 1, #circleTargets do
+ if isStarted(OXTargetExport) then
+ exports[OXTargetExport]:removeZone(circleTargets[i], true)
+ elseif isStarted(QBTargetExport) then
+ exports[QBTargetExport]:RemoveZone(circleTargets[i].name)
+ end
+ end
+end, true)
\ No newline at end of file
diff --git a/shared/vehicles.lua b/shared/vehicles.lua
new file mode 100644
index 0000000..7430e01
--- /dev/null
+++ b/shared/vehicles.lua
@@ -0,0 +1,299 @@
+--[[
+ Vehicle Info & Properties Module
+ ----------------------------------
+ This module provides utilities for:
+ - Retrieving vehicle information from a Vehicles table.
+ - Getting and setting vehicle properties using the active framework.
+ - Comparing vehicle property differences.
+ - Synchronizing vehicle properties across clients.
+ - Managing network control of vehicles.
+ - Finding the closest vehicle to a given position.
+]]
+
+-- Cached vehicle info to avoid unnecessary re-searches.
+local lastCar, carInfo = nil, {}
+
+--- 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.
+---
+--- @param vehicle number The entity ID of the vehicle to search for.
+--- @return table|nil table A table containing the vehicle's details or nil if the vehicle is invalid.
+---
+--- @usage
+--- ```lua
+--- local info = searchCar(vehicleEntity)
+--- print(info.name, info.price, info.class.name, info.class.index)
+--- ```
+function searchCar(vehicle)
+ if lastCar ~= vehicle then -- If same car, use previous info
+ lastCar = vehicle
+ carInfo = {}
+ local model = GetEntityModel(vehicle)
+ local classlist = {
+ "Compacts", --1
+ "Sedans", --2
+ "SUVs", --3
+ "Coupes", --4
+ "Muscle", --5
+ "Sports Classics", --6
+ "Sports", --7
+ "Super", --8
+ "Motorcycles", --9
+ "Off-road", --10
+ "Industrial", --11
+ "Utility", --12
+ "Vans", --13
+ "Cycles", --14
+ "Boats", --15
+ "Helicopters", --16
+ "Planes", --17
+ "Service", --18
+ "Emergency", --19
+ "Military", --20
+ "Commercial", --21
+ "Trains", --22
+ }
+ 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
+ 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
+ carInfo.class = classlist[GetVehicleClass(vehicle) + 1], GetVehicleClass(vehicle)
+ break
+ end
+ end
+
+ if not carInfo.name then
+ debugPrint("^6Bridge^7: ^2Vehicle ^1not ^2found in ^4vehicles^7 ^2table^7: ^6"..model.." ^7(^6"..GetDisplayNameFromVehicleModel(model):lower().."^7)")
+ carInfo.name = string.upper(GetMakeNameFromVehicleModel(model).." "..GetDisplayNameFromVehicleModel(model))
+ carInfo.price = 0
+ carInfo.class = classlist[GetVehicleClass(vehicle) + 1], GetVehicleClass(vehicle)
+ end
+ return carInfo
+ else
+ if not carInfo.name then
+ debugPrint("^6Bridge^7: ^2Vehicle ^1not ^2found in ^4vehicles^7 ^2table^7: ^6"..model.." ^7(^6"..GetDisplayNameFromVehicleModel(model):lower().."^7)")
+ carInfo.name = string.upper(GetMakeNameFromVehicleModel(model).." "..GetDisplayNameFromVehicleModel(model))
+ carInfo.price = 0
+ carInfo.class = classlist[GetVehicleClass(vehicle) + 1], GetVehicleClass(vehicle)
+ end
+ end
+ else
+ return carInfo
+ end
+end
+
+-------------------------------------------------------------
+-- Vehicle Properties Functions
+-------------------------------------------------------------
+
+--- Retrieves the properties of a given vehicle using the active framework.
+---
+--- @param vehicle number The entity ID of the vehicle.
+--- @return table|nil table A table containing the vehicle's properties or nil if invalid.
+---
+--- @usage
+--- ```lua
+--- local props = getVehicleProperties(vehicleEntity)
+--- if props then
+--- -- Use vehicle properties
+--- 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]")
+ end
+ return properties
+end
+
+--- 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.
+---
+--- @param vehicle number The entity ID of the vehicle.
+--- @param props table The new properties to apply.
+---
+--- @usage
+--- ```lua
+--- setVehicleProperties(vehicleEntity, newProperties)
+--- ```
+function setVehicleProperties(vehicle, props)
+ if checkDifferences(vehicle, props) then
+ if not DoesEntityExist(vehicle) then
+ print("Unable to set vehicle properties for '"..vehicle.."' (^1entity does not exist^7)")
+ end
+
+ if isStarted(QBExport) and not isStarted(QBXExport) then
+ Core.Functions.SetVehicleProperties(vehicle, props)
+ debugPrint("^6Bridge^7: ^2Setting Vehicle Properties ^7[^6"..QBExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..props.model.."^7] - [^3"..props.plate.."^7]")
+ elseif isStarted(OXLibExport) then
+ lib.setVehicleProperties(vehicle, props, false)
+ debugPrint("^6Bridge^7: ^2Setting Vehicle Properties ^7[^6"..OXLibExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..props.model.."^7] - [^3"..props.plate.."^7]")
+ end
+ else
+ debugPrint("^6Bridge^7: ^2No Changes Found ^7 [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..props.model.."^7] - [^3"..props.plate.."^7]")
+ end
+end
+
+--- Checks for differences between the current and new vehicle properties.
+--- Compares properties using JSON encoding for deep comparison and logs differences.
+---
+--- @param vehicle number The entity ID of the vehicle.
+--- @param newProps table The new properties to compare.
+--- @return boolean `true` if differences are found; `false` otherwise.
+---
+--- @usage
+--- ```lua
+--- if checkDifferences(vehicleEntity, newProperties) then
+--- setVehicleProperties(vehicleEntity, newProperties)
+--- end
+--- ```
+function checkDifferences(vehicle, newProps)
+ local oldProps = getVehicleProperties(vehicle)
+ debugPrint("^6Bridge^7: ^2Finding differences in ^3Vehicle Properties^7")
+ local differencesFound = true
+
+ for k in pairs(oldProps) do
+ if json.encode(oldProps[k]) ~= json.encode(newProps[k]) then
+ differencesFound = true
+ debugPrint("^6Bridge^7: ^5Old ^7[^3"..k.."^7] - "..json.encode(oldProps[k], { indent = true }))
+ debugPrint("^6Bridge^7: ^5New ^7[^3"..k.."^7] - "..json.encode(newProps[k], { indent = true }))
+ end
+ end
+
+ return differencesFound
+end
+
+-------------------------------------------------------------
+-- Vehicle Properties Synchronization
+-------------------------------------------------------------
+
+--- Event handler for setting vehicle properties received from the server.
+--- Listens for the `ox:setVehicleProperties` event and applies the properties.
+---
+--- @event `getScript()..ox:setVehicleProperties`
+--- @param netId number The network ID of the vehicle.
+--- @param props table The new vehicle properties.
+RegisterNetEvent(getScript()..":ox:setVehicleProperties", function(netId, props)
+ local vehicle = NetworkGetEntityFromNetworkId(netId)
+ local value = props
+ Entity(vehicle).state[getScript()..':setVehicleProperties'] = value
+end)
+
+--- Handles state bag changes for updating vehicle properties.
+--- When the state bag changes, the new properties are applied to the vehicle.
+---
+--- @param bagName string The state bag's name.
+--- @param key string The key that changed.
+--- @param value table The new state value.
+AddStateBagChangeHandler(getScript()..':setVehicleProperties', '', function(bagName, _, value)
+ if not value or not GetEntityFromStateBagName then return end
+ local entity = GetEntityFromStateBagName(bagName)
+ local networked = not bagName:find('localEntity')
+ debugPrint("^6Bridge^7: ^2Setting Vehicle Properties ^7[^6"..OXLibExport.."^7] - [^3"..entity.."^7] - [^3"..GetEntityModel(entity).."^7] - [^3"..value.plate.."^7]")
+
+ if networked then return end
+
+ if lib.setVehicleProperties(entity, value) then
+ Entity(entity).state:set('setVehicleProperties', nil, true)
+ end
+end)
+
+-------------------------------------------------------------
+-- Vehicle Control Functions
+-------------------------------------------------------------
+
+--- 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.
+---
+---@param entity number The entity ID of the vehicle to push.
+---
+---@usage
+--- ```lua
+--- pushVehicle(vehicleEntity)
+--- ```
+function pushVehicle(entity)
+ SetVehicleModKit(entity, 0)
+ 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.")
+ NetworkRequestControlOfEntity(entity)
+ local timeout = 2000
+ while timeout > 0 and not NetworkHasControlOfEntity(entity) do
+ Wait(100)
+ timeout = timeout - 100
+ end
+ if NetworkHasControlOfEntity(entity) then
+ debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Network now has control of the entity^7.")
+ end
+ end
+
+ -- Set as mission entity if not already set.
+ if not IsEntityAMissionEntity(entity) then
+ debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Setting vehicle as a ^7'^2mission^7' ^2entity^7.")
+ SetEntityAsMissionEntity(entity, true, true)
+ local timeout = 2000
+ while timeout > 0 and not IsEntityAMissionEntity(entity) do
+ Wait(100)
+ timeout = timeout - 100
+ end
+ if IsEntityAMissionEntity(entity) then
+ debugPrint("^6Bridge^7: ^3pushVehicle^7: ^2Vehicle is a ^7'^2mission^7'^2 entity^7.")
+ end
+ end
+ end
+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.
+---
+--- @usage
+--- ```lua
+--- local closestVeh, distance = getClosestVehicle({ x = 100, y = 200, z = 30 }, src)
+--- ```
+function getClosestVehicle(coords, src)
+ local ped, vehicles, closestDistance, closestVehicle
+
+ if src then
+ ped = GetPlayerPed(src)
+ vehicles = GetAllVehicles()
+ else
+ ped = PlayerPedId()
+ vehicles = GetGamePool('CVehicle')
+ end
+
+ local closestDistance, closestVehicle = -1, -1
+
+ if coords then
+ if type(coords) == 'table' then
+ coords = vec3(coords.x, coords.y, coords.z)
+ end
+ else
+ coords = GetEntityCoords(ped)
+ end
+
+ for i = 1, #vehicles, 1 do
+ local vehicleCoords = GetEntityCoords(vehicles[i])
+ local distance = #(vehicleCoords - coords)
+
+ if closestDistance == -1 or distance < closestDistance then
+ closestDistance = distance
+ closestVehicle = vehicles[i]
+ end
+ end
+
+ return closestVehicle, closestDistance
+end
\ No newline at end of file
diff --git a/shared/wrapperfunctions.lua b/shared/wrapperfunctions.lua
new file mode 100644
index 0000000..09f4c2c
--- /dev/null
+++ b/shared/wrapperfunctions.lua
@@ -0,0 +1,105 @@
+
+--- Registers a command with the active command system.
+--- This function supports multiple command systems (OXLib, qb-core, ESX Legacy).
+---
+--- @param command string The name of the command to register.
+--- @param options table A table containing command options.
+--- - help (`string`): The help description for the command.
+--- - params (`table`): A table of parameters for the command.
+--- - callback (`function`): The function to execute when the command is called.
+--- - autocomplete (`function|nil`): (Optional) A function for autocompletion.
+--- - restrictedGroup (`string|nil`): (Optional) The user group required to execute the command.
+---
+--- @usage
+--- ```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"
+--- })
+--- ```
+function registerCommand(command, options)
+ 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])
+ 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)
+ 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] })
+ end
+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(nil, name, 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)
+ 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)
+ --- ```
+ RegisterNetEvent(getScript()..":server:makeOXStash", function(name, label, slots, weight, owner, coords, token)
+ local src = source or nil
+ if src then
+ 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])
+ else
+ debugPrint("^1Auth^7: ^2Client and Server Auth tokens match^7!", token, validTokens[src])
+ validTokens[src] = nil
+ end
+ end
+
+ registerStash(name, label, slots, weight, owner, coords)
+ end)
+end
diff --git a/starter.lua b/starter.lua
new file mode 100644
index 0000000..5dc6bc0
--- /dev/null
+++ b/starter.lua
@@ -0,0 +1,127 @@
+gameName = not IsDuplicityVersion() and GetCurrentGameName()
+
+Exports = {
+ QBExport = "qb-core",
+ QBXExport = "qbx_core",
+ ESXExport = "es_extended",
+ OXCoreExport = "ox_core",
+
+ OXInv = "ox_inventory",
+ QBInv = "qb-inventory",
+ PSInv = "ps-inventory",
+ QSInv = "qs-inventory",
+ CoreInv = "core_inventory",
+ CodeMInv = "codem-inventory",
+ OrigenInv = "origen_inventory",
+
+ OXLibExport = "ox_lib",
+
+ QBMenuExport = "qb-menu",
+
+ QBTargetExport = "qb-target",
+ OXTargetExport = "ox_target",
+
+ -- REDM
+ RSGExport = "rsg-core",
+ RSGInv = "rsg-inventory"
+}
+
+-- Required variables
+debugMode = Config.System.Debug
+
+-- Check server convars for hard set defaults
+if Config and Config.System then
+ if Config.System.Debug then
+ if GetConvar("jim_DisableDebug", "false") == "true" then
+ debugMode = false
+ end
+ if GetConvar("jim_DisableEventDebug", "false") == "true" then
+ Config.System.EventDebug = false
+ end
+ end
+
+ Config.System.Menu = GetConvar("jim_menuScript", Config.System.Menu)
+ Config.System.Notify = GetConvar("jim_notifyScript", Config.System.Notify)
+ Config.System.ProgressBar = GetConvar("jim_progressBarScript", Config.System.ProgressBar)
+ Config.System.drawText = GetConvar("jim_drawTextScript", Config.System.drawText)
+ Config.System.skillCheck = GetConvar("jim_skillCheckScript", Config.System.skillCheck)
+
+ if GetConvar("jim_dontUseTarget", "false") == "true" then
+ Config.System.DontUseTarget = true
+ end
+end
+
+QBInvNew = true
+
+InventoryWeight = 120000
+
+-- Load files here into the invoking script
+for _, v in pairs({ -- This is a specific load order
+ --'convarCheck.lua',
+ 'helpers.lua', -- needs to be first
+ '_loaders.lua',
+
+ '_eventDebug.lua',
+ 'callback.lua',
+ 'coreloader.lua', -- needs to be second to load all core related stuff before everything else
+
+ 'duifunctions.lua',
+
+ -- Native Scaleforms
+ 'scaleforms/scaleform_basic.lua',
+ 'scaleforms/bigMessageInstance.lua',
+ 'scaleforms/countDownHandler.lua',
+ 'scaleforms/debugScaleform.lua',
+ 'scaleforms/instructionalButtons.lua',
+ 'scaleforms/timerBars.lua',
+
+ -- Required functions
+ 'make/loaders.lua',
+ 'make/makeBlip.lua',
+ 'make/makePed.lua',
+ 'make/makeProp.lua',
+ 'make/makeVeh.lua',
+ 'make/cameras.lua',
+ 'make/progressBars.lua',
+
+ 'wrapperfunctions.lua',
+ 'polyZone.lua',
+ 'inventories.lua',
+ 'itemcontrol.lua',
+ 'playerfunctions.lua',
+ 'metaHandlers.lua',
+ 'jobfunctions.lua',
+ 'societybank.lua',
+ 'phones.lua',
+
+ -- Interactions
+ 'targets.lua',
+ 'contextmenus.lua',
+ 'input.lua',
+ 'notify.lua',
+ 'drawText.lua',
+ 'skillcheck.lua',
+
+ -- Crafting / Shops / Stashes
+ 'crafting.lua',
+ 'shops.lua',
+ 'stashcontrol.lua',
+
+ -- Kind of "other"
+ 'isAnimal.lua',
+ 'scaleEntity.lua',
+ 'vehicles.lua',
+ 'effects.lua',
+
+ -- Do version check last
+ '_scriptversioncheck.lua'
+}) do
+ if debugMode then
+ --print("^5Loading^7: 'jim_bridge/shared/"..v.."' ^2into ^7'"..GetCurrentResourceName().."' ...")
+ end
+ 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")
+ end
+end
diff --git a/version.txt b/version.txt
index bba5b3d..415b19f 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-1.0.14
+2.0
\ No newline at end of file
diff --git a/wrapper.lua b/wrapper.lua
deleted file mode 100644
index df269f3..0000000
--- a/wrapper.lua
+++ /dev/null
@@ -1,1395 +0,0 @@
-Items, Vehicles, Jobs, Gangs, Core, ESX = {}, nil, nil, nil, nil, nil
-
-Exports.QBInv = GetResourceState("ps-inventory"):find("start") and "ps-inventory" or GetResourceState("lj-inventory"):find("start") and "lj-inventory" or Exports.QBInv
-
-OXLibExport, QBXExport, QBExport, ESXExport, OXCoreExport = Exports.OXLibExport or "", Exports.QBXExport or "", Exports.QBExport or "", Exports.ESXExport or "", Exports.OXCoreExport or ""
-
-OXInv, QBInv, QSInv, CoreInv, CodeMInv, OrigenInv = Exports.OXInv or "", Exports.QBInv or "", Exports.QSInv or "", Exports.CoreInv or "", Exports.CodeMInv or "", Exports.OrigenInv or ""
-
-QBMenuExport = Exports.QBMenuExport or ""
-
-QBTargetExport, OXTargetExport = Exports.QBTargetExport or "", Exports.OXTargetExport or ""
-
-function CheckBridgeVersion()
- if IsDuplicityVersion() then
- local currentVersion = "^3"..GetResourceMetadata("jim_bridge", 'version'):gsub("%.", "^7.^3").."^7"
- PerformHttpRequest('https://raw.githubusercontent.com/jimathy/jim_bridge/master/version.txt', function(err, newestVersion, headers)
- if not newestVersion then print("^1Currently unable to run a version check for ^7'^3jim_bridge^7' ("..currentVersion.."^7)") return end
- newestVersion = "^3"..newestVersion:sub(1, -2):gsub("%.", "^7.^3"):gsub("%\r", "").."^7"
- print(newestVersion == currentVersion and "^7'^3jim_bridge^7' - ^6You are running the latest version.^7 ("..currentVersion..")" or "^7'^3jim_bridge^7' - ^1You are currently running an outdated version^7, ^1please update^7!")
- end)
- end
-end
-CheckBridgeVersion()
-
-for k, v in pairs(Exports) do
- if GetResourceState(v):find("start") then print("^6Bridge^7: '^3"..v.."^7' ^2export found ^7") end
-end
-
-local itemResource, jobResource = "", ""
-
--- Load item lists
-if GetResourceState(OXInv):find("start") 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 GetResourceState(QBExport):find("start") then itemResource = QBExport
- Core = Core or exports[QBExport]:GetCoreObject()
- Items = Core and Core.Shared.Items or nil
-
-elseif GetResourceState(ESXExport):find("start") then itemResource = ESXExport
- ESX = exports[ESXExport]:getSharedObject()
- Items = ESX and ESX.Items or nil
-end
-if not Items then
- print("^4ERROR^7: ^2No Core Items detected ^7- ^2Check ^3exports^1.^2lua^7")
-else
- print("^6Bridge^7: ^2Loading ^6"..countTable(Items).." ^3Items^2 from ^7" .. itemResource)
-end
-
--- Load Vehicles
-if GetResourceState(QBXExport):find("start") or GetResourceState(QBExport):find("start") then
- Core = Core or exports[QBExport]:GetCoreObject()
- if GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- RegisterNetEvent('QBCore:Client:UpdateObject', function()
- Core = Core or exports[QBExport]:GetCoreObject()
- end)
- end
- Vehicles = Core and Core.Shared.Vehicles
- print("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..QBExport)
-elseif GetResourceState(OXCoreExport):find("start") then
- Vehicles = {}
- for k, v in pairs(Ox.GetVehicleData()) do
- Vehicles[k] = { model = k, price = v.price, name = v.name, brand = v.make }
- end
- print("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..OXCoreExport)
-elseif GetResourceState(ESXExport):find("start") then
- print("^6Bridge^7: ^2Loading ^3Vehicles^2 from ^7"..ESXExport)
- --print("^6Bridge^7: ^2Loading ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..ESXExport)
- CreateThread(function()
- if IsDuplicityVersion() then
- createCallback(GetCurrentResourceName()..":getVehiclesPrices", function(source)
- Vehicles = MySQL.query.await('SELECT model, price, name FROM vehicles')
- print("^6Bridge^7: ^3Found ^6"..countTable(Vehicles).." ^3Vehicles^2 from ^7"..ESXExport)
- return Vehicles
- end)
- end
- if not IsDuplicityVersion() then
- local TempVehicles = triggerCallback(GetCurrentResourceName()..":getVehiclesPrices")
- for _, v in pairs(TempVehicles) do
- Vehicles = Vehicles or {}
- Vehicles[v.model] = { model = v.model, price = v.price, name = v.name, brand = GetMakeNameFromVehicleModel(v.model):lower():gsub("^%l", string.upper) }
- end
- end
- end)
-else
- print("^4ERROR^7: ^2No Vehicle info detected ^7- ^2Check ^3exports^1.^2lua^7")
-end
-
--- Load Jobs
-local jobResource = ""
-if GetResourceState(QBXExport):find("start") then jobResource = QBXExport
- Core = Core or exports[QBExport]:GetCoreObject()
- Jobs, Gangs = exports[QBXExport]:GetJobs(), exports[QBXExport]:GetGangs()
-
-elseif GetResourceState(OXCoreExport):find("start") then jobResource = OXExport
- CreateThread(function()
- if IsDuplicityVersion() then
- createCallback(GetCurrentResourceName()..":getOxGroups", function(source)
- Jobs = MySQL.query.await('SELECT * FROM `ox_groups`') return Jobs
- end)
- else
- local TempJobs = triggerCallback(GetCurrentResourceName()..":getOxGroups")
- Jobs = TempJobs and {}
- 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 }
- end
- Gangs = Jobs
- end
- end)
-
-elseif GetResourceState(QBExport):find("start") then jobResource = QBExport
- Core = Core or Core or exports[QBExport]:GetCoreObject()
- RegisterNetEvent('QBCore:Client:UpdateObject', function() Core = Core or exports[QBExport]:GetCoreObject() end)
- Jobs, Gangs = Core.Shared.Jobs, Core.Shared.Gangs
-
-elseif GetResourceState(ESXExport):find("start") then
- print("^6Bridge^7: ^2Loading ^3Jobs^7/^3Gangs^2 from ^7"..ESXExport)
- ESX = exports[ESXExport]:getSharedObject()
- if IsDuplicityVersion() then
- Jobs = ESX.GetJobs()
- for k, v in pairs(Jobs) do
- local count = countTable(Jobs[k].grades)-1
- Jobs[k].grades[tostring(count)].isBoss = true
- end
- Gangs = Jobs
- end
- CreateThread(function()
- while not ESX do Wait(0) end
- if IsDuplicityVersion() then
- createCallback(GetCurrentResourceName()..":getJobs", function(source)
- return Jobs
- end)
- end
- if not IsDuplicityVersion() then
- Jobs = triggerCallback(GetCurrentResourceName()..":getJobs")
- Gangs = Jobs
- end
- end)
-end
-if not GetResourceState(ESXExport):find("start") and Jobs then
- print("^6Bridge^7: ^2Loading ^6"..countTable(Jobs).." ^3Jobs^2 from ^7"..jobResource, "^6Bridge^7: ^2Loading ^6"..countTable(Gangs).." ^3Gangs^2 from ^7"..jobResource)
-end
-
-function makeBossRoles(role)
- local boss = {}
- 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 then
- boss[role] = boss[role] and math.min(boss[role], tonumber(grade)) or tonumber(grade)
- end
- end
- end
- return boss
-end
-
-function createPoly(data) local Location = nil
- if GetResourceState(OXLibExport):find("start") then -- if it finds ox_lib, use it instead of PolyZone
- if Config.System.Debug then print("^6Bridge^7: ^2Creating new poly with ^7 "..OXLibExport.." "..data.name) end
- for i = 1, #data.points do
- data.points[i] = vec3(data.points[i].x, data.points[i].y, 12.0)
- end
- data.thickness = 1000
- Location = lib.zones.poly(data)
- elseif GetResourceState("PolyZone"):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Creating new poly with ^7PolyZone "..data.name) end
- 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
-end
-
-function createCirclePoly(data) local Location = nil
- if GetResourceState(OXLibExport):find("start") then -- if it finds ox_lib, use it instead of PolyZone
- if Config.System.Debug then print("^6Bridge^7: ^2Creating new ^3Cricle^2 poly with ^7"..OXLibExport.." "..data.name) end
- Location = lib.zones.sphere(data)
- elseif GetResourceState("PolyZone"):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Creating new ^3Cricle^2 poly with ^7PolyZone ".. data.name) end
- Location = CircleZone:Create(data.coords, data.radius, { name = data.name, debugPoly = Config.System.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
-end
-
-function openMenu(Menu, data)
- if Config.System.Menu == "ox" then
- local index = nil
- if data.onBack and not data.onSelected then
- table.insert(Menu, 1, { icon = "fas fa-circle-arrow-left",
- title = "Return",
- onSelect = data.onBack,
- label = "Return"
- })
- end
- for k in pairs(Menu) do
- if data.onSelected and Menu[k].arrow then
- Menu[k].icon = "fas fa-angle-right"
- end
- if 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
- 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].disabled = 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 and data.headertxt or "", isMenuHeader = true }
- Menu = tempMenu
- end
- for k in pairs(Menu) do
- if Menu[k].onSelect then
- Menu[k].params = {
- isAction = true,
- event = Menu[k].onSelect,
- }
- end
- if not Menu[k].header then Menu[k].header = " " end
- if Menu[k].arrow then Menu[k].icon = "fas fa-angle-right" end
- 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 -- can't display more than one line - BIG problem for crafting menus mainly
- 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", GetCurrentResourceName(), "Example_Menu", {
- title = data.header,
- align = 'top-right',
- elements = Menu
- },
- function(menuData, menu) -- OnSelect Function
- 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() -- close menu
- end)
- end
-end
-
-br = (Config.System.Menu == "ox" or Config.System.Menu == "gta") and "\n" or " "
-
-function isOx() return (Config.System.Menu == "ox" or Config.System.Menu == "gta") end
-
-function isWarMenuOpen() if Config.System.Menu == "gta" then return WarMenu.IsAnyMenuOpened() else return false end end
-
-local TextTargets = {}
-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] = "."
-}
--- Targets --
-local targetEntities = {}
-function createEntityTarget(entity, opts, dist)
- targetEntities[#targetEntities+1] = entity
- if GetResourceState(OXTargetExport):find("start") then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Creating new ^3Entity^2 target with ^6"..OXTargetExport.." ^7"..entity)
- end
- local options = {}
- for i = 1, #opts do
- options[i] = {
- icon = opts[i].icon,
- label = opts[i].label,
- item = opts[i].item or nil,
- groups = opts[i].job or opts[i].gang,
- onSelect = opts[i].action,
- canInteract = function(_, distance)
- return distance < dist and true or false
- end
- }
- end
- exports[OXTargetExport]:addLocalEntity(entity, options)
- elseif GetResourceState(QBTargetExport):find("start") then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Creating new ^3Entity^2 target with ^6"..QBTargetExport.." ^7"..entity)
- end
- local options = { options = opts, distance = dist }
- exports[QBTargetExport]:AddTargetEntity(entity, options)
- else
- local tempText = ""
- local keyTable = { 38, 29, 303, }
- for i = 1, #opts do
- opts[i].key = keyTable[i]
- tempText = tempText.."~b~["..Keys[opts[i].key].."] ~w~"..opts[i].label.." "
- end
- TextTargets[entity] = { coords = GetEntityCoords(entity), buttontext = tempText, options = opts, dist = dist }
- end
-end
-
-local boxTargets = {}
-function createBoxTarget(data, opts, dist)
- if GetResourceState(OXTargetExport):find("start") then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..OXTargetExport.." ^7"..data[1])
- end
- local options = {}
- for i = 1, #opts do
- options[i] = {
- icon = opts[i].icon,
- label = opts[i].label,
- item = opts[i].item or nil,
- groups = opts[i].job or opts[i].gang,
- onSelect = opts[i].onSelect or opts[i].action,
- canInteract = function(_, distance)
- return distance < dist and true or false
- end
- }
- 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 GetResourceState(QBTargetExport):find("start") then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..QBTargetExport.." ^7"..data[1])
- end
- 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]
- else
- local tempText = ""
- local keyTable = { 38, 29, 303, }
- for i = 1, #opts do
- opts[i].key = keyTable[i]
- tempText = tempText.."~b~["..Keys[opts[i].key].."] ~w~"..opts[i].label.." "
- end
- TextTargets[data[1]] = { coords = data[2], buttontext = tempText, options = opts, dist = dist }
- return data[1]
- end
-end
-
-local circleTargets = {}
-function createCircleTarget(data, opts, dist)
- if GetResourceState(OXTargetExport):find("start") then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Creating new ^3Sphere^2 target with ^6"..OXTargetExport.." ^7"..data[1])
- end
- local options = {}
- for i = 1, #opts do
- options[i] = {
- icon = opts[i].icon,
- label = opts[i].label,
- item = opts[i].item or nil,
- groups = opts[i].job or opts[i].gang,
- onSelect = opts[i].onSelect or opts[i].action,
- canInteract = function(_, distance)
- return distance < dist and true or false
- end
- }
- 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 GetResourceState(QBTargetExport):find("start") then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Creating new ^3Circle^2 target with ^6"..QBTargetExport.." ^7"..data[1])
- end
- 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]
- else
- local tempText = ""
- local keyTable = { 38, 29, 303, }
- for i = 1, #opts do
- opts[i].key = keyTable[i]
- tempText = tempText.."~b~["..Keys[opts[i].key].."] ~w~"..opts[i].label.." "
- end
- TextTargets[data[1]] = { coords = data[2], buttontext = tempText, options = opts, dist = dist }
- return data[1]
- end
-end
-
-function removeEntityTarget(entity)
- if GetResourceState(QBTargetExport):find("start") then exports[QBTargetExport]:RemoveTargetEntity(entity) end
- if GetResourceState(OXTargetExport):find("start") then exports[OXTargetExport]:removeLocalEntity(entity, nil) end
- if not GetResourceState(OXTargetExport):find("start") and not GetResourceState(QBTargetExport):find("start") then
- TextTargets[entity] = nil
- end
-end
-
-function removeZoneTarget(target)
- if GetResourceState(QBTargetExport):find("start") then exports[QBTargetExport]:RemoveZone(target) end
- if GetResourceState(OXTargetExport):find("start") then exports[OXTargetExport]:removeZone(target, true) end
- if not GetResourceState(OXTargetExport):find("start") and not GetResourceState(QBTargetExport):find("start") then
- TextTargets[target] = nil
- end
-end
-
-if not GetResourceState(OXTargetExport):find("start") and not GetResourceState(QBTargetExport):find("start") and not IsDuplicityVersion() then
- CreateThread(function()
- while true do
- local pedCoords = GetEntityCoords(PlayerPedId())
- for k, v in pairs (TextTargets) do
- if #(pedCoords - v.coords) <= v.dist then
- DrawText3D(v.coords.x, v.coords.y, v.coords.z + 1.0, v.buttontext)
- for i = 1, #v.options do
- if IsControlJustPressed(0, v.options[i].key) then
- if v.options[i].onSelect then v.options[i].onSelect() end
- if v.options[i].action then v.options[i].action() end
- end
- end
- end
- end
- Wait(0)
- end
- end)
-end
-
-AddEventHandler('onResourceStop', function(r)
- if r ~= GetCurrentResourceName() then return end
- for i = 1, #targetEntities do
- if GetResourceState(OXTargetExport):find("start") then exports[OXTargetExport]:removeLocalEntity(targetEntities[i], nil)
- elseif GetResourceState(QBTargetExport):find("start") then exports[QBTargetExport]:RemoveTargetEntity(targetEntities[i]) end
- end
- for i = 1, #boxTargets do
- if GetResourceState(OXTargetExport):find("start") then exports[OXTargetExport]:removeZone(boxTargets[i], true)
- elseif GetResourceState(QBTargetExport):find("start") then exports[QBTargetExport]:RemoveZone(boxTargets[i].name) end
- end
- for i = 1, #circleTargets do
- if GetResourceState(OXTargetExport):find("start") then exports[OXTargetExport]:removeZone(circleTargets[i], true)
- elseif GetResourceState(QBTargetExport):find("start") then exports[QBTargetExport]:RemoveZone(circleTargets[i].name) end
- end
-end)
-
--- NOTIFICATIONS --
-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, { type = type or "success", title = title, description = message }) end
- elseif Config.System.Notify == "gta" then
- if not src then TriggerEvent(GetCurrentResourceName()..":DisplayGTANotify", title, message)
- else TriggerClientEvent(GetCurrentResourceName()..":DisplayGTANotify", src, title, message) end
- elseif Config.System.Notify == "esx" then
- if not src then exports["esx_notify"]:Notify(type, 4000, message)
- else TriggerClientEvent(GetCurrentResourceName()..":DisplayESXNotify", src, type, title, message) end
- end
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":DisplayESXNotify", function(type, title, text)
- exports["esx_notify"]:Notify(type, 4000, message)
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":DisplayGTANotify", function(title, text) local iconTable = {}
- if GetCurrentResourceName() == "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)
-
---DrawText
-function drawText(image, input, style) local text = ""
- if Config.System.drawText == "qb" then
- for i = 1, #input do
- text = text..input[i].."
"..(input[i+1] ~= nil and " " or "") end
- local text = text:gsub("%:", ":")
- if image then
- text = ''..text
- end
- exports[CoreExport]:DrawText(text, 'left')
-
- elseif Config.System.drawText == "ox" then
- for k, v in pairs(input) do
- input[k] = v.." \n"
- end
- lib.showTextUI(table.concat(input), { icon = radarTable[image], position = 'left-center' })
-
- elseif Config.System.drawText == "gta" then
- 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.."~"))
- elseif Config.System.drawText == "esx" then
- for i = 1, #input do
- text = text..input[i]..""..(input[i+1] ~= nil and " " or "") end
- local text = text:gsub("%:", ":")
- if image then
- text = ''..text
- end
- ESX.TextUI(text, nil)
- end
-end
-
-function hideText()
- if Config.System.drawText == "qb" then
- exports[CoreExport]:HideText()
- elseif Config.System.drawText == "ox" then
- lib.hideTextUI()
- elseif Config.System.drawText == "gta" then
- ClearAllHelpMessages()
- elseif Config.System.drawText == "esx" then
- ESX.HideUI()
- end
-end
-
-function DisplayHelpMsg(text)
- BeginTextCommandDisplayHelp("STRING")
- AddTextComponentScaleform(text)
- EndTextCommandDisplayHelp(0, true, false, -1)
-end
-
--- Callbacks
-function createCallback(callbackName, funct)
- if GetResourceState(OXLibExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3Callback^2 with ^7"..OXLibExport, callbackName) end
- lib.callback.register(callbackName, funct)
- elseif GetResourceState(QBExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3Callback^2 with ^7"..QBExport, callbackName) end
- Core = Core or exports[QBExport]:GetCoreObject()
- Core.Functions.CreateCallback(callbackName, funct)
- elseif GetResourceState(ESXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3Callback^2 with ^7"..ESXExport, callbackName) end
- ESX.RegisterServerCallback(callbackName, funct)
- else
- print("^6Bridge^7: ^1ERROR^7: ^3Can't find any script to register callback with", callbackName)
- end
-end
-
-function triggerCallback(callBackName, value) local result = nil
- if GetResourceState(OXLibExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Triggering ^3Callback^2 with ^7"..OXLibExport, callBackName) end
- result = lib.callback.await(callBackName, false, value)
- elseif GetResourceState(QBExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Triggering ^3Callback^2 with ^7"..QBExport, callBackName) end
- local p = promise.new()
- Core.Functions.TriggerCallback(callBackName, function(cb) p:resolve(cb) end, value)
- result = Citizen.Await(p)
- elseif GetResourceState(ESXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Triggering ^3Callback^2 with ^7"..ESXExport, callBackName) end
- local p = promise.new()
- ESX.TriggerServerCallback(callBackName, function(cb) p:resolve(cb) end, value)
- result = Citizen.Await(p)
- else
- print("^6Bridge^7: ^1ERROR^7: ^3Can't find any script to trigger callback with", callBackName)
- end
- return result
-end
-
--- onPlayerLoaded events
-function onPlayerLoaded(func)
- if GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3onPlayerLoaded^2 with ^7"..QBExport) end
- AddEventHandler('QBCore:Client:OnPlayerLoaded', func)
- elseif GetResourceState(ESXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3onPlayerLoaded^2 with ^7"..ESXExport) end
- AddEventHandler('esx:playerLoaded', func)
- elseif GetResourceState(OXCoreExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3onPlayerLoaded^2 with ^7"..OXLibExport) end
- AddEventHandler('ox:playerLoaded', func)
- else
- print("^4ERROR^7: ^2No Core detected for onPlayerLoaded ^7- ^2Check ^3exports^1.^2lua^7")
- end
-end
-
--- INPUT --
-function createInput(title, opts)
- local dialog = nil
- local options = {}
- if Config.System.Menu == "ox" then
- for i = 1, #opts do
- 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[i] = {
- type = "select",
- isRequired = 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[i] = {
- type = "number",
- label = opts[i].text ..(opts[i].txt and " - "..opts[i].txt or ""),
- isRequired = opts[i].isRequired,
- name = opts[i].name,
- options = opts[i].options,
- }
- end
- if opts[i].type == "text" then
- options[i] = {
- type = "input",
- label = opts[i].text ..(opts[i].txt and " - "..opts[i].txt or ""),
- default = opts[i].default,
- isRequired = opts[i].isRequired,
- }
- end
- if opts[i].type == "select" then
- options[i] = {
- type = "select",
- label = opts[i].text ..(opts[i].txt and " - "..opts[i].txt or ""),
- isRequired = opts[i].isRequired,
- name = opts[i].name,
- options = opts[i].options,
- min = opts[i].min,
- max = opts[i].max,
- default = opts[i].default,
- }
- end
- end
- dialog = exports[OXLibExport]:inputDialog(title, options)
- return dialog
- end
- if Config.System.Menu == "qb" then
- dialog = exports['qb-input']:ShowInput({ header = title, submitText = "Accept", inputs = opts })
- return dialog
- end
- if Config.System.Menu == "gta" then
- WarMenu.CreateMenu(tostring(opts),
- title,
- " ",
- { titleColor = { 222, 255, 255 },
- maxOptionCountOnScreen = 15,
- width = 0.25,
- x = 0.7,
- })
- if WarMenu.IsAnyMenuOpened() then return end
- WarMenu.OpenMenu(tostring(opts))
- local close = true
- local _comboBoxItems = { }
- local _comboBoxIndex = { 1, 1 }
- while true do
- if WarMenu.Begin(tostring(opts)) then
- for i = 1, #opts do
- if opts[i].type == "radio" then
- for k in pairs(opts[i].options) do
- if not _comboBoxItems[i] then _comboBoxItems[i] = {} end
- _comboBoxItems[i][k] = opts[i].options[k].text
- end
- local _, comboBoxIndex = WarMenu.ComboBox(opts[i].label, _comboBoxItems[i], _comboBoxIndex[i])
- if _comboBoxIndex[i] ~= comboBoxIndex then
- _comboBoxIndex[i] = comboBoxIndex
- end
- end
- if opts[i].type == "number" then
- for b = 1, opts[i].max do
- if not _comboBoxItems[i] then _comboBoxItems[i] = {} end
- _comboBoxItems[i][b] = b
- end
- local _, comboBoxIndex = WarMenu.ComboBox(opts[i].text, _comboBoxItems[i], _comboBoxIndex[i])
- if _comboBoxIndex[i] ~= comboBoxIndex then
- _comboBoxIndex[i] = comboBoxIndex
- end
- end
- end
- local pressed = WarMenu.Button("Pay")
- if pressed then
- WarMenu.CloseMenu()
- close = false
- local result = {}
- for i = 1, #_comboBoxIndex do
- result[i] = _comboBoxItems[i][_comboBoxIndex[i]]
- end
- return result
- end
- WarMenu.End()
- else
- return
- end
- if not WarMenu.IsAnyMenuOpened() and close then
- if data.onExit then data.onExit() end
- end
- Wait(0)
- end
- end
-end
-
--- Get Vehile Info --
-local lastCar = nil
-local carInfo = {}
-function searchCar(vehicle)
- if lastCar ~= vehicle then -- If same car, use previous info
- lastCar = vehicle
- carInfo = {}
- local model = GetEntityModel(vehicle)
- local classlist = {
- "Compacts", --1
- "Sedans", --2
- "SUVs", --3
- "Coupes", --4
- "Muscle", --5
- "Sports Classics", --6
- "Sports", --7
- "Super", --8
- "Motorcycles", --9
- "Off-road", --10
- "Industrial", --11
- "Utility", --12
- "Vans", --13
- "Cycles", --14
- "Boats", --15
- "Helicopters", --16
- "Planes", --17
- "Service", --18
- "Emergency", --19
- "Military", --20
- "Commercial", --21
- "Trains", --22
- }
- 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 Config.System.Debug then
- print("^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)")
- end
- carInfo.name = Vehicles[k].name.." "..Vehicles[k].brand
- carInfo.price = Vehicles[k].price
- carInfo.class = classlist[GetVehicleClass(vehicle) + 1], GetVehicleClass(vehicle)
- break
- end
- end
-
- if not carInfo.name then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Vehicle ^1not ^2found in ^4vehicles^7 ^2table^7: ^6"..model.." ^7(^6"..GetDisplayNameFromVehicleModel(model):lower().."^7)")
- end
- carInfo.name = string.upper(GetMakeNameFromVehicleModel(model).." "..GetDisplayNameFromVehicleModel(model))
- carInfo.price = 0
- carInfo.class = classlist[GetVehicleClass(vehicle) + 1], GetVehicleClass(vehicle)
- end
- return carInfo
- else
- if not carInfo.name then
- if Config.System.Debug then
- print("^6Bridge^7: ^2Vehicle ^1not ^2found in ^4vehicles^7 ^2table^7: ^6"..model.." ^7(^6"..GetDisplayNameFromVehicleModel(model):lower().."^7)")
- end
- carInfo.name = string.upper(GetMakeNameFromVehicleModel(model).." "..GetDisplayNameFromVehicleModel(model))
- carInfo.price = 0
- carInfo.class = classlist[GetVehicleClass(vehicle) + 1], GetVehicleClass(vehicle)
- end
- end
- else
- return carInfo
- end
-end
-
--- Vehicle Properties --
-function getVehicleProperties(vehicle)
- local properties = {}
- if vehicle == nil then return nil end
- if GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- properties = Core.Functions.GetVehicleProperties(vehicle)
- if Config.System.Debug then print("^6Bridge^7: ^2Getting Vehicle Properties ^7[^6"..QBExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..properties.model.."^7] - [^3"..properties.plate.."^7]") end
- elseif GetResourceState(OXLibExport):find("start") then
- properties = lib.getVehicleProperties(vehicle)
- if Config.System.Debug then print("^6Bridge^7: ^2Getting Vehicle Properties ^7[^6"..OXLibExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..properties.model.."^7] - [^3"..properties.plate.."^7]") end
- end
- return properties
-end
-
-function setVehicleProperties(vehicle, props)
- local oldProps = getVehicleProperties(vehicle)
- if checkDifferences(vehicle, props) then
- --if Config.System.Debug then debugDifferences(vehicle, props) end
- if not DoesEntityExist(vehicle) then
- print(("Unable to set vehicle properties for '%s' (entity does not exist)"):
- format(vehicle))
- end
- if GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- Core.Functions.SetVehicleProperties(vehicle, props)
- if Config.System.Debug then print("^6Bridge^7: ^2Setting Vehicle Properties ^7[^6"..QBExport.."^7] - [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..props.model.."^7] - [^3"..props.plate.."^7]") end
- else
- TriggerServerEvent(GetCurrentResourceName()..":ox:setVehicleProperties", VehToNet(vehicle), props)
- end
- else
- if Config.System.Debug then print("^6Bridge^7: ^2No Changes Found ^7 [^3"..vehicle.."^7] - [^3"..GetEntityModel(vehicle).."^7/^3"..props.model.."^7] - [^3"..props.plate.."^7]") end
- end
-end
-
-function checkDifferences(vehicle, newProps)
- local oldProps = getVehicleProperties(vehicle)
- if Config.System.Debug then print("^6Bridge^7: ^2Finding differences in ^3Vehicle Properties^7") end
- local allow = false
- for k in pairs(oldProps) do
- if json.encode(oldProps[k]) ~= json.encode(newProps[k]) then
- allow = true
- if Config.System.Debug then
- print("^6Bridge^7: ^5Old ^7[^3"..k.."^7] - "..json.encode(oldProps[k], { indent = true }))
- print("^6Bridge^7: ^5New ^7[^3"..k.."^7] - "..json.encode(newProps[k], { indent = true }))
- end
- end
- end
- return allow
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":ox:setVehicleProperties", function(netId, props)
- local vehicle = NetworkGetEntityFromNetworkId(netId)
- local value = props
- Entity(vehicle).state[GetCurrentResourceName()..':setVehicleProperties'] = value
-end)
-
-AddStateBagChangeHandler(GetCurrentResourceName()..':setVehicleProperties', '', function(bagName, _, value)
- if not value or not GetEntityFromStateBagName then return end
- local entity = GetEntityFromStateBagName(bagName)
- local networked = not bagName:find('localEntity')
- if Config.System.Debug then print("^6Bridge^7: ^2Setting Vehicle Properties ^7[^6"..OXLibExport.."^7] - [^3"..entity.."^7] - [^3"..GetEntityModel(entity).."^7] - [^3"..value.plate.."^7]") end
-
- if networked and NetworkGetEntityOwner(entity) ~= cache.playerId then return end
-
- if lib.setVehicleProperties(entity, value) then
- Entity(entity).state:set('setVehicleProperties', nil, true)
- end
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":server:ChargePlayer", function(cost, type, newsrc)
- local src = newsrc or source
- local fundResource = ""
- if type == "cash" then
- if GetResourceState(OXInv):find("start") then fundResource = OXInv
- exports[OXInv]:RemoveItem(src, "money", cost)
- elseif GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then fundResource = QBExport
- Core.Functions.GetPlayer(src).Functions.RemoveMoney("cash", cost)
- elseif GetResourceState(ESXExport):find("start") then fundResource = ESXExport
- local Player = ESX.GetPlayerFromId(src)
- Player.removeMoney(cost, "")
- end
- end
- if type == "bank" then
- if GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then fundResource = QBExport
- Core.Functions.GetPlayer(src).Functions.RemoveMoney("bank", cost)
- elseif GetResourceState(ESXExport):find("start") then fundResource = ESXExport
- local Player = ESX.GetPlayerFromId(src)
- Player.removeMoney(cost, "")
- end
- end
- if fundResource == "" then print("error - check exports.lua")
- else
- if Config.System.Debug then print("^6Bridge^7: ^2Charging ^2Player^7: '^6"..cost.."^7'", type, fundResource) end
- end
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":server:FundPlayer", function(fund, type, newsrc)
- local src = newsrc or source
- local fundResource = ""
- if type == "cash" then
- if GetResourceState(OXInv):find("start") then fundResource = OXInv
- exports[OXInv]:AddItem(src, "money", fund)
- elseif GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then fundResource = QBExport
- Core.Functions.GetPlayer(src).Functions.AddMoney("cash", fund)
- elseif GetResourceState(ESXExport):find("start") then fundResource = ESXExport
- local Player = ESX.GetPlayerFromId(src)
- Player.addMoney(fund, "")
- end
- end
- if type == "bank" then
- if GetResourceState(QBExport):find("start") or GetResourceState(QBXExport):find("start") then fundResource = QBExport
- Core.Functions.GetPlayer(src).Functions.AddMoney("bank", fund)
- elseif GetResourceState(ESXExport):find("start") then fundResource = ESXExport
- local Player = ESX.GetPlayerFromId(src)
- Player.addMoney(fund, "")
- end
- end
- if fundResource == "" then print("error - check exports.lua")
- else
- if Config.System.Debug then print("^6Bridge^7: ^2Funding ^2Player^7: '^2"..fund.."^7'", type, fundResource) end
- end
-end)
-
-function createUseableItem(item, funct)
- if GetResourceState(ESXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering item as ^3Useable^2 with ^7es_extended", item) end
- while not ESX do Wait(0) end
- ESX.RegisterUsableItem(item, funct)
- elseif GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering item as ^3Useable^2 with ^7qb-core", item) end
- Core.Functions.CreateUseableItem(item, funct)
- elseif GetResourceState(QBXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering item as ^3Useable^2 with ^7qbx_core", item) end
- exports[QBXExport]:CreateUseableItem(item, funct)
- end
-end
-
-function hasJob(job, source, grade) local hasJob, duty = false, true
- if source then
- local src = tonumber(source)
- if GetResourceState(ESXExport):find("start") then
- local info = ESX.GetPlayerFromId(src).job
- while not info do
- info = ESX.GetPlayerData(src).job
- Wait(100)
- end
- if info.name == job then hasJob = true end
-
- elseif GetResourceState(OXCoreExport):find("start") then
- local chunk = assert(load(LoadResourceFile('ox_core', ('imports/%s.lua'):format('server')), ('@@ox_core/%s'):format(file)))
- chunk()
- local player = Ox.GetPlayer(tonumber(src))
- for k, v in pairs(player.getGroups()) do
- if k == job then hasJob = true end
- end
-
- elseif GetResourceState(QBXExport):find("start") then
- local jobinfo = exports[QBXExport]:GetPlayer(src).PlayerData.job
- if jobinfo.name == job then hasJob = true
- duty = exports[QBXExport]:GetPlayer(src).PlayerData.job.onduty
- if grade and not (grade <= jobinfo.grade.level) then hasJob = false end
- end
- local ganginfo = exports[QBXExport]:GetPlayer(src).PlayerData.gang
- if ganginfo.name == job then hasJob = true
- if grade and not (grade <= ganginfo.grade.level) then hasJob = false end
- end
-
- elseif GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- if Core.Functions.GetPlayer then -- support older qb-core functions
- local jobinfo = Core.Functions.GetPlayer(src).PlayerData.job
- if jobinfo.name == job then hasJob = true
- duty = Core.Functions.GetPlayer(src).PlayerData.job.onduty
- if grade and not (grade <= jobinfo.grade.level) then hasJob = false end
- end
- local ganginfo = Core.Functions.GetPlayer(src).PlayerData.gang
- if ganginfo.name == job then hasJob = true
- if grade and not (grade <= ganginfo.grade.level) then hasJob = false end
- end
- else -- support newer qb-core exports
- local jobinfo = exports[QBExport]:GetPlayer(src).PlayerData.job
- if jobinfo.name == job then hasJob = true
- duty = exports[QBExport]:GetPlayer(src).PlayerData.job.onduty
- if grade and not (grade <= jobinfo.grade.level) then hasJob = false end
- end
- local ganginfo = exports[QBExport]:GetPlayer(src).PlayerData.gang
- if ganginfo.name == job then hasJob = true
- if grade and not (grade <= ganginfo.grade.level) then hasJob = false end
- end
- end
- else
- print("^4ERROR^7: ^2No Core detected for hasJob ^7- ^2Check ^3exports^1.^2lua^7")
- end
- else
- if GetResourceState(ESXExport):find("start") then
- while not ESX do Wait(10) end
- local info = ESX.GetPlayerData().job
- while not info do
- info = ESX.GetPlayerData().job
- Wait(100)
- end
- if info.name == job then hasJob = true end
-
- elseif GetResourceState(OXCoreExport):find("start") then
- for k, v in pairs(exports[OXCoreExport]:GetPlayerData().groups) do
- if k == job then hasJob = true end break
- end
-
- elseif GetResourceState(QBXExport):find("start") then
- local jobinfo = QBX.PlayerData.job
- if jobinfo.name == job then hasJob = true
- duty = QBX.PlayerData.job.onduty
- if grade and not (grade <= jobinfo.grade.level) then hasJob = false end
- end
- local ganginfo = QBX.PlayerData.gang
- if ganginfo.name == job then hasJob = true
- if grade and not (grade <= ganginfo.grade.level) then hasJob = false end
- end
-
- elseif GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- local info = nil
- Core.Functions.GetPlayerData(function(PlayerData)
- info = PlayerData
- end)
- local jobinfo = info.job
- if jobinfo.name == job then hasJob = true
- duty = jobinfo.onduty
- if grade and not (grade <= jobinfo.grade.level) then hasJob = false end
- end
- local ganginfo = info.gang
- if ganginfo.name == job then
- hasJob = true
- if grade and not (grade <= ganginfo.grade.level) then hasJob = false end
- end
-
- else
- print("^4ERROR^7: ^2No Core detected for hasJob() ^7- ^2Check ^3exports^1.^2lua^7")
- end
- end
- return hasJob, duty
-end
-
-function getPlayer(source) local Player = {}
- if Config.System.Debug then print("^6Bridge^7: ^2Getting ^3Player^2 info^7") end
- if source then -- If called from server
- local src = tonumber(source)
- if GetResourceState(ESXExport):find("start") then
- local info = ESX.GetPlayerFromId(src)
- Player = {
- name = info.getName(),
- cash = info.getMoney(),
- bank = info.getAccount("bank").money,
- }
-
- elseif GetResourceState(OXCoreExport):find("start") then
- local file = ('imports/%s.lua'):format('server')
- local import = LoadResourceFile('ox_core', file)
- local chunk = assert(load(import, ('@@ox_core/%s'):format(file)))
- chunk()
- local player = Ox.GetPlayer(tonumber(src))
- Player = {
- name = ('%s %s'):format(player.firstName, player.lastName),
- cash = exports[OXInv]:Search(src, 'count', "money"),
- bank = 0,
- }
-
- elseif GetResourceState(QBXExport):find("start") then
- local info = exports[QBXExport]:GetPlayer(src)
- Player = {
- name = info.PlayerData.charinfo.firstname.." "..info.PlayerData.charinfo.lastname,
- cash = exports[OXInv]:Search(src, 'count', "money"),
- bank = info.Functions.GetMoney("bank"),
- }
-
- elseif GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- if Core.Functions.GetPlayer ~= nil then -- support older qb-core functions
- local info = Core.Functions.GetPlayer(src).PlayerData
- Player = {
- name = info.charinfo.firstname.." "..info.charinfo.lastname,
- cash = info.money["cash"],
- bank = info.money["bank"],
- }
- else
- local info = exports[QBExport]:GetPlayer(src).PlayerData
- Player = {
- name = info.charinfo.firstname.." "..info.charinfo.lastname,
- cash = info.money["cash"],
- bank = info.money["bank"],
- }
- end
-
- else
- print("^4ERROR^7: ^2No Core detected for getPlayer() ^7- ^2Check ^3exports^1.^2lua^7")
- end
- else
- if GetResourceState(ESXExport):find("start") and ESX ~= nil then
- local info = ESX.GetPlayerData()
- local cash, bank = 0, 0
- for k, v in pairs(ESX.GetPlayerData().accounts) do
- if v.name == "money" then cash = v.money end
- if v.name == "bank" then bank = v.money end
- end
- Player = {
- name = ('%s %s'):format(info.firstName, info.lastName),
- cash = cash,
- bank = bank,
- }
- elseif GetResourceState(OXCoreExport):find("start") then
- local info = exports[OXCoreExport]:GetPlayerData()
- Player = {
- name = info.firstName.." "..info.lastName,
- cash = exports[OXInv]:Search('count', "money"),
- bank = 0,
- }
- elseif GetResourceState(QBXExport):find("start") then
- local info = exports[QBXExport]:GetPlayerData()
- Player = {
- name = info.charinfo.firstname.." "..info.charinfo.lastname,
- cash = info.money["cash"],
- bank = info.money["bank"],
- }
- elseif GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- local info = nil
- Core.Functions.GetPlayerData(function(PlayerData) info = PlayerData end)
- Player = {
- name = info.charinfo.firstname.." "..info.charinfo.lastname,
- cash = info.money["cash"],
- bank = info.money["bank"],
- }
- else
- print("^4ERROR^7: ^2No Core detected for hasJob ^7- ^2Check ^3exports^1.^2lua^7")
- end
- end
- return Player
-end
-
-function sendPhoneMail(data) local phoneResource = ""
- if GetResourceState("gksphone"):find("start") then phoneResource = "gksphone"
- exports["gksphone"]:SendNewMail(data)
-
- elseif GetResourceState("yflip-phone"):find("start") then phoneResource = "yflip-phone"
- TriggerServerEvent(GetCurrentResourceName()..":yflip:SendMail", data)
-
- elseif GetResourceState("qs-smartphone"):find("start") then phoneResource = "qs-smartphone"
- TriggerServerEvent('qs-smartphone:server:sendNewMail', data)
-
- elseif GetResourceState("qs-smartphone-pro"):find("start") then phoneResource = "qs-smartphone-pro"
- TriggerServerEvent('phone:sendNewMail', data)
-
- elseif GetResourceState("roadphone"):find("start") then phoneResource = "roadphone"
- data.message = data.message:gsub("% ", "\n")
- exports['roadphone']:sendMail(data)
-
- elseif GetResourceState("lb-phone"):find("start") then phoneResource = "lb-phone"
- TriggerServerEvent(GetCurrentResourceName()..":lbphone:SendMail", data)
-
- elseif GetResourceState("qb-phone"):find("start") then phoneResource = "qb-phone"
- TriggerServerEvent('qb-phone:server:sendNewMail', data)
-
- elseif GetResourceState("jpr-phonesystem"):find("start") then phoneResource = "jpr-phonesystem"
- TriggerServerEvent(GetCurrentResourceName()..":jpr:SendMail", data)
- end
-
- if phoneResource ~= "" then if Config.System.Debug then print("^6Bridge^7[^3"..phoneResource.."^7]: ^2Sending mail to player") end
- else print("^6Bridge^7: ^1ERROR ^2Sending mail to player ^7 - ^2No supported phone found") end
-end
-
-RegisterNetEvent(GetCurrentResourceName()..":lbphone:SendMail", function(data)
- local src = source
- local phoneNumber = exports["lb-phone"]:GetEquippedPhoneNumber(src)
- local emailAddress = exports["lb-phone"]:GetEmailAddress(phoneNumber)
- exports["lb-phone"]:SendMail({
- to = emailAddress,
- subject = data.subject,
- message = data.message,
- --[[attachments = {
- "https://cdn.discordapp.com/attachments/1035667053115363349/1042500877426110474/upload.png",
- },]]
- actions = data.buttons,
- })
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":yflip:SendMail", function(data)
- local src = source
- exports["yflip-phone"]:SendMail({
- title = data.subject,
- sender = data.sender,
- senderDisplayName = data.sender,
- content = data.message,
- actions = data.buttons,
- }, 'source', src)
-end)
-
-RegisterNetEvent(GetCurrentResourceName()..":jpr:SendMail", function(data)
- local QBCore = exports['qb-core']:GetCoreObject()
- local src = source
- local Player = QBCore.Functions.GetPlayer(src)
- TriggerEvent('jpr-phonesystem:server:sendEmail', {
- Assunto = data.subject, -- Subject
- Conteudo = data.message, -- Content
- Enviado = data.sender, -- Submitted by
- Destinatario = Player.PlayerData.citizenid, -- Target
- Event = {},
- })
-end)
-
-function registerCommand(command, options)
- if GetResourceState(OXLibExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3Command^2 with ^7"..OXLibExport, command) end
- lib.addCommand(command, { help = options[1], restricted = options[5] and "group."..options[5] or nil }, options[4])
- elseif GetResourceState(QBExport):find("start") and not GetResourceState(QBXExport):find("start") then
- if Config.System.Debug then print("^6Bridge^7: ^2Registering ^3Command^2 with ^7qb-core"..QBExport, command) end
- Core.Commands.Add(command, options[1], options[2], options[3], options[4], options[5] and options[5] or nil)
- end
-end
-
-function invImg(item)
- local imgLink = ""
- if item ~= "" and Items[item] then
- if GetResourceState(OXInv):find("start") then
- imgLink = "nui://"..OXInv.."/web/images/"..(Items[item].image or "")
- elseif GetResourceState(QSInv):find("start") then
- imgLink = "nui://"..QSInv.."/html/images/"..(Items[item].image or "")
- elseif GetResourceState(CoreInv):find("start") then
- imgLink = "nui://"..CoreInv.."/html/img/"..(Items[item].image or "")
- elseif GetResourceState(OrigenInv):find("start") then
- imgLink = "nui://"..OrigenInv.."/html/img/"..(Items[item].image or "")
- elseif GetResourceState(QBInv):find("start") then
- imgLink = "nui://"..QBInv.."/html/images/"..(Items[item].image or "")
- else
- print("^4ERROR^7: ^2No Inventory detected for invImg ^7- ^2Check ^3exports^1.^2lua^7")
- end
- end
- return imgLink
-end
-
-function registerStash(name, label, slots, weight)
- if GetResourceState(OXInv):find("start") then
- --print("Registering OX Stash:", name, label)
- exports[OXInv]:RegisterStash(name, label, slots or 50, weight or 4000000)
- elseif GetResourceState(QSInv):find("start") then
- --print("Registering QS Stash:", name, label)
- exports[QSInv]:RegisterStash(name, slots or 50, weight or 4000000)
- end
-end
-
--- duiList callback here because it wouldnt load in functions.lua
-
-if IsDuplicityVersion() then
- createCallback(GetCurrentResourceName()..":Server:duiList", function(source, cb)
- if GetResourceState(OXLibExport):find("start") then
- return duiList
- else
- cb(duiList)
- end
- end)
-end
-
-
--- IN NO WAY PERFECT --