This commit is contained in:
2026-07-07 21:08:52 +02:00
commit 4c20cfc716
2613 changed files with 318021 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
if Config.CustomInventory ~= "ox" then return end
MySQL.ready(function()
TriggerEvent("__cfx_export_ox_inventory_Items", function(ref)
if ref then
ESX.Items = ref()
end
end)
AddEventHandler("ox_inventory:itemList", function(items)
ESX.Items = items
end)
end)
---@diagnostic disable-next-line: duplicate-set-field
ESX.GetItemLabel = function(item)
item = exports.ox_inventory:Items(item)
if item then
return item.label
end
end
function setPlayerInventory(playerId, xPlayer, inventory, isNew)
exports.ox_inventory:setPlayerInventory(xPlayer, inventory)
if isNew then
local shared = json.decode(GetConvar("inventory:accounts", '["money"]'))
for i = 1, #shared do
local name = shared[i]
local account = Config.StartingAccountMoney[name]
if account then
exports.ox_inventory:AddItem(playerId, name, account)
end
end
end
end

View File

@@ -0,0 +1,219 @@
local Inventory
if Config.CustomInventory ~= "ox" then return end
AddEventHandler("ox_inventory:loadInventory", function(module)
Inventory = module
end)
Core.PlayerFunctionOverrides.OxInventory = {
getInventory = function(self)
return function(minimal)
if not minimal then
return self.inventory
end
local minimalInventory = {}
for k, v in pairs(self.inventory) do
if v.count and v.count > 0 then
local metadata = v.metadata
if v.metadata and next(v.metadata) == nil then
metadata = nil
end
minimalInventory[#minimalInventory + 1] = {
name = v.name,
count = v.count,
slot = k,
metadata = metadata,
}
end
end
return minimalInventory
end
end,
getLoadout = function()
return function()
return {}
end
end,
setAccountMoney = function(self)
return function(accountName, money, reason)
reason = reason or "unknown"
if money < 0 then return end
local account = self.getAccount(accountName)
if not account then return end
money = account.round and ESX.Math.Round(money) or money
self.accounts[account.index].money = money
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:setAccountMoney", self.source, accountName, money, reason)
if Inventory.accounts[accountName] then
Inventory.SetItem(self.source, accountName, money)
end
end
end,
addAccountMoney = function(self)
return function(accountName, money, reason)
reason = reason or "unknown"
if money < 1 then return end
local account = self.getAccount(accountName)
if not account then return end
money = account.round and ESX.Math.Round(money) or money
self.accounts[account.index].money = self.accounts[account.index].money + money
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:addAccountMoney", self.source, accountName, money, reason)
if Inventory.accounts[accountName] then
Inventory.AddItem(self.source, accountName, money)
end
end
end,
removeAccountMoney = function(self)
return function(accountName, money, reason)
reason = reason or "unknown"
if money < 1 then return end
local account = self.getAccount(accountName)
if not account then return end
money = account.round and ESX.Math.Round(money) or money
self.accounts[account.index].money = self.accounts[account.index].money - money
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:removeAccountMoney", self.source, accountName, money, reason)
if Inventory.accounts[accountName] then
Inventory.RemoveItem(self.source, accountName, money)
end
end
end,
getInventoryItem = function(self)
return function(name, metadata)
return Inventory.GetItem(self.source, name, metadata)
end
end,
addInventoryItem = function(self)
return function(name, count, metadata, slot)
return Inventory.AddItem(self.source, name, count or 1, metadata, slot)
end
end,
removeInventoryItem = function(self)
return function(name, count, metadata, slot)
return Inventory.RemoveItem(self.source, name, count or 1, metadata, slot)
end
end,
setInventoryItem = function(self)
return function(name, count, metadata)
return Inventory.SetItem(self.source, name, count, metadata)
end
end,
canCarryItem = function(self)
return function(name, count, metadata)
return Inventory.CanCarryItem(self.source, name, count, metadata)
end
end,
canSwapItem = function(self)
return function(firstItem, firstItemCount, testItem, testItemCount)
return Inventory.CanSwapItem(self.source, firstItem, firstItemCount, testItem, testItemCount)
end
end,
setMaxWeight = function(self)
return function(newWeight)
self.maxWeight = newWeight
self.triggerEvent("esx:setMaxWeight", self.maxWeight)
return Inventory.SetMaxWeight(self.source, newWeight)
end
end,
addWeapon = function()
return function() end
end,
addWeaponComponent = function()
return function() end
end,
addWeaponAmmo = function()
return function() end
end,
updateWeaponAmmo = function()
return function() end
end,
setWeaponTint = function()
return function() end
end,
getWeaponTint = function()
return function() end
end,
removeWeapon = function()
return function() end
end,
removeWeaponComponent = function()
return function() end
end,
removeWeaponAmmo = function()
return function() end
end,
hasWeaponComponent = function()
return function()
return false
end
end,
hasWeapon = function()
return function()
return false
end
end,
hasItem = function(self)
return function(name, metadata)
return Inventory.GetItem(self.source, name, metadata)
end
end,
getWeapon = function()
return function() end
end,
syncInventory = function(self)
return function(weight, maxWeight, items, money)
self.weight, self.maxWeight = weight, maxWeight
self.inventory = items
if not money then return end
for accountName, amount in pairs(money) do
local account = self.getAccount(accountName)
if account and ESX.Math.Round(account.money) ~= amount then
account.money = amount
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:setAccountMoney", self.source, accountName, amount, "Sync account with item")
end
end
end
end,
}

View File

@@ -0,0 +1,969 @@
---@class ESXAccount
---@field name string # Account name (e.g., "bank", "money").
---@field money number # Current balance in this account.
---@field label string # Human-readable label for the account.
---@field round boolean # Whether amounts are rounded for display.
---@field index number # Index of the account in the player's accounts list.
---@class ESXItem
---@field name string # Item identifier (internal name).
---@field label string # Display name of the item.
---@field weight number # Weight of a single unit of the item.
---@field usable boolean # Whether the item can be used.
---@field rare boolean # Whether the item is rare.
---@field canRemove boolean # Whether the item can be removed from inventory.
---@class ESXInventoryItem:ESXItem
---@field count number # Number of this item in the player's inventory.
---@class ESXJob
---@field id number # Job ID.
---@field name string # Job internal name.
---@field label string # Job display label.
---@field grade number # Current grade/rank number.
---@field grade_name string # Name of the current grade.
---@field grade_label string # Label of the current grade.
---@field grade_salary number # Salary for the current grade.
---@field skin_male table # Skin configuration for male characters.
---@field skin_female table # Skin configuration for female characters.
---@field onDuty boolean? # Whether the player is currently on duty.
---@class ESXWeapon
---@field name string # Weapon identifier (internal name).
---@field label string # Weapon display name.
---@class ESXInventoryWeapon:ESXWeapon
---@field ammo number # Amount of ammo in the weapon.
---@field components string[] # List of components attached to the weapon.
---@field tintIndex number # Current weapon tint index.
---@class ESXWeaponComponent
---@field name string # Component identifier (internal name).
---@field label string # Component display name.
---@field hash string|number # Component hash or identifier.
---@class StaticPlayer
---@field src number # Player's server ID.
--- Money Functions
---@field setMoney fun(money: number) # Set player's cash balance.
---@field getMoney fun(): number # Get player's current cash balance.
---@field addMoney fun(money: number, reason: string) # Add money to the player's cash balance.
---@field removeMoney fun(money: number, reason: string) # Remove money from the player's cash balance.
---@field setAccountMoney fun(accountName: string, money: number, reason?: string) # Set specific account balance.
---@field addAccountMoney fun(accountName: string, money: number, reason?: string) # Add money to an account.
---@field removeAccountMoney fun(accountName: string, money: number, reason?: string) # Remove money from an account.
---@field getAccount fun(account: string): ESXAccount? # Get account data by name.
---@field getAccounts fun(minimal?: boolean): ESXAccount[]|table<string,number> # Get all accounts, optionally minimal.
--- Inventory Functions
---@field getInventory fun(minimal?: boolean): ESXInventoryItem[]|table<string,number> # Get inventory, optionally minimal.
---@field getInventoryItem fun(itemName: string): ESXInventoryItem? # Get a specific item from inventory.
---@field addInventoryItem fun(itemName: string, count: number) # Add items to inventory.
---@field removeInventoryItem fun(itemName: string, count: number) # Remove items from inventory.
---@field setInventoryItem fun(itemName: string, count: number) # Set item count in inventory.
---@field getWeight fun(): number # Get current carried weight.
---@field getMaxWeight fun(): number # Get maximum carry weight.
---@field setMaxWeight fun(newWeight: number) # Set maximum carry weight.
---@field canCarryItem fun(itemName: string, count: number): boolean # Check if player can carry more of an item.
---@field canSwapItem fun(firstItem: string, firstItemCount: number, testItem: string, testItemCount: number): boolean # Check if items can be swapped.
---@field hasItem fun(item: string): ESXInventoryItem|false, number? # Check if player has an item.
---@field getLoadout fun(minimal?: boolean): ESXInventoryWeapon[]|table<string, {ammo:number, tintIndex?:number, components?:string[]}> # Get player's weapon loadout.
--- Job Functions
---@field getJob fun(): ESXJob # Get player's current job.
---@field setJob fun(newJob: string, grade: string, onDuty?: boolean) # Set player's job and grade.
---@field setGroup fun(newGroup: string) # Set player's permission group.
---@field getGroup fun(): string # Get player's permission group.
--- Weapon Functions
---@field addWeapon fun(weaponName: string, ammo: number) # Give player a weapon.
---@field removeWeapon fun(weaponName: string) # Remove weapon from player.
---@field hasWeapon fun(weaponName: string): boolean # Check if player has a weapon.
---@field getWeapon fun(weaponName: string): number?, table? # Get weapon ammo & components.
---@field addWeaponAmmo fun(weaponName: string, ammoCount: number) # Add ammo to a weapon.
---@field removeWeaponAmmo fun(weaponName: string, ammoCount: number) # Remove ammo from a weapon.
---@field updateWeaponAmmo fun(weaponName: string, ammoCount: number) # Update ammo count for a weapon.
---@field addWeaponComponent fun(weaponName: string, weaponComponent: string) # Add component to weapon.
---@field removeWeaponComponent fun(weaponName: string, weaponComponent: string) # Remove component from weapon.
---@field hasWeaponComponent fun(weaponName: string, weaponComponent: string): boolean # Check if weapon has component.
---@field setWeaponTint fun(weaponName: string, weaponTintIndex: number) # Set weapon tint.
---@field getWeaponTint fun(weaponName: string): number # Get weapon tint.
--- Player State Functions
---@field getIdentifier fun(): string # Get player's unique identifier.
---@field getSSN fun(): string # Get player's social security number.
---@field getSource fun(): number # Get player source/server ID.
---@field getPlayerId fun(): number # Alias for getSource.
---@field getName fun(): string # Get player's name.
---@field setName fun(newName: string) # Set player's name.
---@field setCoords fun(coordinates: vector4|vector3|table) # Teleport player to coordinates.
---@field getCoords fun(vector?: boolean, heading?: boolean): vector3|vector4|table # Get player's coordinates.
---@field isAdmin fun(): boolean # Check if player is admin.
---@field kick fun(reason: string) # Kick player from server.
---@field getPlayTime fun(): number # Get total playtime in seconds.
---@field set fun(k: string, v: any) # Set custom variable.
---@field get fun(k: string): any # Get custom variable.
--- Metadata Functions
---@field getMeta fun(index?: string, subIndex?: string|table): any # Get metadata value(s).
---@field setMeta fun(index: string, value: any, subValue?: any) # Set metadata value(s).
---@field clearMeta fun(index: string, subValues?: string|table) # Clear metadata value(s).
--- Notification Functions
---@field showNotification fun(msg: string, notifyType?: string, length?: number, title?: string, position?: string) # Show a simple notification.
---@field showAdvancedNotification fun(sender: string, subject: string, msg: string, textureDict: string, iconType: string, flash: boolean, saveToBrief: boolean, hudColorIndex: number) # Show advanced notification.
---@field showHelpNotification fun(msg: string, thisFrame?: boolean, beep?: boolean, duration?: number) # Show help notification.
--- Misc Functions
---@field togglePaycheck fun(toggle: boolean) # Enable/disable paycheck.
---@field isPaycheckEnabled fun(): boolean # Check if paycheck is enabled.
---@field executeCommand fun(command: string) # Execute a server command.
---@field triggerEvent fun(eventName: string, ...) # Trigger client event for this player.
---@class xPlayer:StaticPlayer
--- Properties
---@field accounts ESXAccount[] # Array of the player's accounts.
---@field coords table # Player's coordinates {x, y, z, heading}.
---@field group string # Player permission group.
---@field identifier string # Unique identifier (usually Steam or license).
---@field license string # Player license string.
---@field inventory ESXInventoryItem[] # Player's inventory items.
---@field job ESXJob # Player's current job.
---@field loadout ESXInventoryWeapon[] # Player's current weapons.
---@field name string # Player's display name.
---@field playerId number # Player's ID (server ID).
---@field source number # Player's source (alias for playerId).
---@field variables table # Custom player variables.
---@field weight number # Current carried weight.
---@field maxWeight number # Maximum carry weight.
---@field metadata table # Custom metadata table.
---@field lastPlaytime number # Last recorded playtime in seconds.
---@field paycheckEnabled boolean # Whether paycheck is enabled.
---@field admin boolean # Whether the player is an admin.
---@param playerId number
---@param identifier string
---@param ssn string
---@param group string
---@param accounts ESXAccount[]
---@param inventory table
---@param weight number
---@param job ESXJob
---@param loadout ESXInventoryWeapon[]
---@param name string
---@param coords vector4|{x: number, y: number, z: number, heading: number}
---@param metadata table
---@return xPlayer
function CreateExtendedPlayer(playerId, identifier, ssn, group, accounts, inventory, weight, job, loadout, name, coords, metadata)
---@diagnostic disable-next-line: missing-fields
local self = {} ---@type xPlayer
self.accounts = accounts
self.coords = coords
self.group = group
self.identifier = identifier
self.ssn = ssn
self.inventory = inventory
self.job = job
self.loadout = loadout
self.name = name
self.playerId = playerId
self.source = playerId
self.variables = {}
self.weight = weight
self.maxWeight = Config.MaxWeight
self.metadata = metadata
self.lastPlaytime = self.metadata.lastPlaytime or 0
self.paycheckEnabled = true
self.admin = Core.IsPlayerAdmin(playerId)
if Config.Multichar then
local startIndex = identifier:find(":", 1)
if startIndex then
self.license = ("%s%s"):format(Config.Identifier, identifier:sub(startIndex, identifier:len()))
end
else
self.license = ("%s:%s"):format(Config.Identifier, identifier)
end
if type(self.metadata.jobDuty) ~= "boolean" then
self.metadata.jobDuty = self.job.name ~= "unemployed" and Config.DefaultJobDuty or false
end
job.onDuty = self.metadata.jobDuty
ExecuteCommand(("add_principal identifier.%s group.%s"):format(self.license, self.group))
local stateBag = Player(self.source).state
stateBag:set("identifier", self.identifier, false)
stateBag:set("license", self.license, false)
stateBag:set("job", self.job, true)
stateBag:set("group", self.group, true)
stateBag:set("name", self.name, true)
function self.triggerEvent(eventName, ...)
assert(type(eventName) == "string", "eventName should be string!")
TriggerClientEvent(eventName, self.source, ...)
end
function self.togglePaycheck(toggle)
self.paycheckEnabled = toggle
end
function self.isPaycheckEnabled()
return self.paycheckEnabled
end
function self.isAdmin()
return Core.IsPlayerAdmin(self.source)
end
function self.setCoords(coordinates)
local ped <const> = GetPlayerPed(self.source)
SetEntityCoords(ped, coordinates.x, coordinates.y, coordinates.z, false, false, false, false)
SetEntityHeading(ped, coordinates.w or coordinates.heading or 0.0)
end
function self.getCoords(vector, heading)
local ped <const> = GetPlayerPed(self.source)
local entityCoords <const> = GetEntityCoords(ped)
local entityHeading <const> = GetEntityHeading(ped)
local coordinates = { x = entityCoords.x, y = entityCoords.y, z = entityCoords.z }
if vector then
coordinates = (heading and vector4(entityCoords.x, entityCoords.y, entityCoords.z, entityHeading) or entityCoords)
else
if heading then
coordinates.heading = entityHeading
end
end
return coordinates
end
function self.kick(reason)
DropPlayer(self.source --[[@as string]], reason)
end
function self.getPlayTime()
-- luacheck: ignore
return self.lastPlaytime + GetPlayerTimeOnline(self.source --[[@as string]])
end
function self.setMoney(money)
assert(type(money) == "number", "money should be number!")
money = ESX.Math.Round(money)
self.setAccountMoney("money", money)
end
function self.getMoney()
return self.getAccount("money").money
end
function self.addMoney(money, reason)
money = ESX.Math.Round(money)
self.addAccountMoney("money", money, reason)
end
function self.removeMoney(money, reason)
money = ESX.Math.Round(money)
self.removeAccountMoney("money", money, reason)
end
function self.getIdentifier()
return self.identifier
end
function self.getSSN()
return self.ssn
end
function self.setGroup(newGroup)
local lastGroup = self.group
ExecuteCommand(("remove_principal identifier.%s group.%s"):format(self.license, self.group))
self.group = newGroup
TriggerEvent("esx:setGroup", self.source, self.group, lastGroup)
self.triggerEvent("esx:setGroup", self.group, lastGroup)
Player(self.source).state:set("group", self.group, true)
ExecuteCommand(("add_principal identifier.%s group.%s"):format(self.license, self.group))
end
function self.getGroup()
return self.group
end
function self.set(k, v)
self.variables[k] = v
self.triggerEvent('esx:updatePlayerData', 'variables', self.variables)
end
function self.get(k)
return self.variables[k]
end
function self.getAccounts(minimal)
if not minimal then
return self.accounts
end
local minimalAccounts = {}
for i = 1, #self.accounts do
minimalAccounts[self.accounts[i].name] = self.accounts[i].money
end
return minimalAccounts
end
function self.getAccount(account)
account = string.lower(account)
for i = 1, #self.accounts do
local accountName = string.lower(self.accounts[i].name)
if accountName == account then
return self.accounts[i]
end
end
return nil
end
function self.getInventory(minimal)
if minimal then
local minimalInventory = {}
for _, v in ipairs(self.inventory) do
if v.count > 0 then
minimalInventory[v.name] = v.count
end
end
return minimalInventory
end
return self.inventory
end
function self.getJob()
return self.job
end
function self.getLoadout(minimal)
if not minimal then
return self.loadout
end
local minimalLoadout = {}
for _, v in ipairs(self.loadout) do
minimalLoadout[v.name] = { ammo = v.ammo }
if v.tintIndex > 0 then
minimalLoadout[v.name].tintIndex = v.tintIndex
end
if #v.components > 0 then
local components = {}
for _, component in ipairs(v.components) do
if component ~= "clip_default" then
components[#components + 1] = component
end
end
if #components > 0 then
minimalLoadout[v.name].components = components
end
end
end
return minimalLoadout
end
function self.getName()
return self.name
end
function self.setName(newName)
self.name = newName
Player(self.source).state:set("name", self.name, true)
end
function self.setAccountMoney(accountName, money, reason)
reason = reason or "unknown"
if not tonumber(money) then
error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money))
return
end
if money >= 0 then
local account = self.getAccount(accountName)
if account then
money = account.round and ESX.Math.Round(money) or money
self.accounts[account.index].money = money
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:setAccountMoney", self.source, accountName, money, reason)
else
error(("Tried To Set Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId))
end
else
error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money))
end
end
function self.addAccountMoney(accountName, money, reason)
reason = reason or "Unknown"
if not tonumber(money) then
error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money))
return
end
if money > 0 then
local account = self.getAccount(accountName)
if account then
money = account.round and ESX.Math.Round(money) or money
self.accounts[account.index].money = self.accounts[account.index].money + money
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:addAccountMoney", self.source, accountName, money, reason)
else
error(("Tried To Set Add To Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId))
end
else
error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money))
end
end
function self.removeAccountMoney(accountName, money, reason)
reason = reason or "Unknown"
if not tonumber(money) then
error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money))
return
end
if money > 0 then
local account = self.getAccount(accountName)
if account then
money = account.round and ESX.Math.Round(money) or money
if self.accounts[account.index].money - money > self.accounts[account.index].money then
error(("Tried To Underflow Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId))
return
end
self.accounts[account.index].money = self.accounts[account.index].money - money
self.triggerEvent("esx:setAccountMoney", account)
TriggerEvent("esx:removeAccountMoney", self.source, accountName, money, reason)
else
error(("Tried To Set Add To Invalid Account ^5%s^1 For Player ^5%s^1!"):format(accountName, self.playerId))
end
else
error(("Tried To Set Account ^5%s^1 For Player ^5%s^1 To An Invalid Number -> ^5%s^1"):format(accountName, self.playerId, money))
end
end
function self.getInventoryItem(itemName)
for _, v in ipairs(self.inventory) do
if v.name == itemName then
return v
end
end
return nil
end
function self.addInventoryItem(itemName, count)
local item = self.getInventoryItem(itemName)
if item then
count = ESX.Math.Round(count)
item.count = item.count + count
self.weight = self.weight + (item.weight * count)
TriggerEvent("esx:onAddInventoryItem", self.source, item.name, item.count)
self.triggerEvent("esx:addInventoryItem", item.name, item.count)
end
end
function self.removeInventoryItem(itemName, count)
local item = self.getInventoryItem(itemName)
if item then
count = ESX.Math.Round(count)
if count > 0 then
local newCount = item.count - count
if newCount >= 0 then
item.count = newCount
self.weight = self.weight - (item.weight * count)
TriggerEvent("esx:onRemoveInventoryItem", self.source, item.name, item.count)
self.triggerEvent("esx:removeInventoryItem", item.name, item.count)
end
else
error(("Player ID:^5%s Tried remove a Invalid count -> %s of %s"):format(self.playerId, count, itemName))
end
end
end
function self.setInventoryItem(itemName, count)
local item = self.getInventoryItem(itemName)
if item and count >= 0 then
count = ESX.Math.Round(count)
if count > item.count then
self.addInventoryItem(item.name, count - item.count)
else
self.removeInventoryItem(item.name, item.count - count)
end
end
end
function self.getWeight()
return self.weight
end
function self.getSource()
return self.source
end
self.getPlayerId = self.getSource
function self.getMaxWeight()
return self.maxWeight
end
function self.canCarryItem(itemName, count)
if ESX.Items[itemName] then
local currentWeight, itemWeight = self.weight, ESX.Items[itemName].weight
local newWeight = currentWeight + (itemWeight * count)
return newWeight <= self.maxWeight
else
print(('[^3WARNING^7] Item ^5"%s"^7 was used but does not exist!'):format(itemName))
return false
end
end
function self.canSwapItem(firstItem, firstItemCount, testItem, testItemCount)
local firstItemObject = self.getInventoryItem(firstItem)
if not firstItemObject then
return false
end
local testItemObject = self.getInventoryItem(testItem)
if not testItemObject then
return false
end
if firstItemObject.count >= firstItemCount then
local weightWithoutFirstItem = ESX.Math.Round(self.weight - (firstItemObject.weight * firstItemCount))
local weightWithTestItem = ESX.Math.Round(weightWithoutFirstItem + (testItemObject.weight * testItemCount))
return weightWithTestItem <= self.maxWeight
end
return false
end
function self.setMaxWeight(newWeight)
self.maxWeight = newWeight
self.triggerEvent("esx:setMaxWeight", self.maxWeight)
end
function self.setJob(newJob, grade, onDuty)
grade = tostring(grade)
local lastJob = self.job
if not ESX.DoesJobExist(newJob, grade) then
return print(("[ESX] [^3WARNING^7] Ignoring invalid ^5.setJob()^7 usage for ID: ^5%s^7, Job: ^5%s^7"):format(self.source, newJob))
end
if newJob == "unemployed" then
onDuty = false
end
if type(onDuty) ~= "boolean" then
onDuty = Config.DefaultJobDuty
end
local jobObject, gradeObject = ESX.Jobs[newJob], ESX.Jobs[newJob].grades[grade]
self.job = {
id = jobObject.id,
name = jobObject.name,
label = jobObject.label,
type = jobObject.type,
onDuty = onDuty,
grade = tonumber(grade) or 0,
grade_name = gradeObject.name,
grade_label = gradeObject.label,
grade_salary = gradeObject.salary,
skin_male = gradeObject.skin_male and json.decode(gradeObject.skin_male) or {},
skin_female = gradeObject.skin_female and json.decode(gradeObject.skin_female) or {},
}
self.metadata.jobDuty = onDuty
TriggerEvent("esx:setJob", self.source, self.job, lastJob)
self.triggerEvent("esx:setJob", self.job, lastJob)
Player(self.source).state:set("job", self.job, true)
end
function self.addWeapon(weaponName, ammo)
if not self.hasWeapon(weaponName) then
local weaponLabel <const> = ESX.GetWeaponLabel(weaponName)
table.insert(self.loadout, {
name = weaponName,
ammo = ammo,
label = weaponLabel,
components = {},
tintIndex = 0,
})
GiveWeaponToPed(GetPlayerPed(self.source), joaat(weaponName), ammo, false, false)
self.triggerEvent("esx:addInventoryItem", weaponLabel, false, true)
self.triggerEvent("esx:addLoadoutItem", weaponName, weaponLabel, ammo)
end
end
function self.addWeaponComponent(weaponName, weaponComponent)
local loadoutNum <const>, weapon <const> = self.getWeapon(weaponName)
if weapon then
local component = ESX.GetWeaponComponent(weaponName, weaponComponent)
if component then
if not self.hasWeaponComponent(weaponName, weaponComponent) then
self.loadout[loadoutNum].components[#self.loadout[loadoutNum].components + 1] = weaponComponent
local componentHash = ESX.GetWeaponComponent(weaponName, weaponComponent).hash
GiveWeaponComponentToPed(GetPlayerPed(self.source), joaat(weaponName), componentHash)
self.triggerEvent("esx:addInventoryItem", component.label, false, true)
end
end
end
end
function self.addWeaponAmmo(weaponName, ammoCount)
local _, weapon = self.getWeapon(weaponName)
if weapon then
weapon.ammo = weapon.ammo + ammoCount
SetPedAmmo(GetPlayerPed(self.source), joaat(weaponName), weapon.ammo)
end
end
function self.updateWeaponAmmo(weaponName, ammoCount)
local _, weapon = self.getWeapon(weaponName)
if not weapon then
return
end
weapon.ammo = ammoCount
if weapon.ammo <= 0 then
local _, weaponConfig = ESX.GetWeapon(weaponName)
if weaponConfig.throwable then
self.removeWeapon(weaponName)
end
end
end
function self.setWeaponTint(weaponName, weaponTintIndex)
local loadoutNum <const>, weapon <const> = self.getWeapon(weaponName)
if weapon then
local _, weaponObject <const> = ESX.GetWeapon(weaponName)
if weaponObject.tints and weaponObject.tints[weaponTintIndex] then
self.loadout[loadoutNum].tintIndex = weaponTintIndex
self.triggerEvent("esx:setWeaponTint", weaponName, weaponTintIndex)
self.triggerEvent("esx:addInventoryItem", weaponObject.tints[weaponTintIndex], false, true)
end
end
end
function self.getWeaponTint(weaponName)
local _, weapon <const> = self.getWeapon(weaponName)
if weapon then
return weapon.tintIndex
end
return 0
end
function self.removeWeapon(weaponName)
local weaponLabel, playerPed <const> = nil, GetPlayerPed(self.source)
if not playerPed then
return error("xPlayer.removeWeapon ^5invalid^1 player ped!")
end
for k, v in ipairs(self.loadout) do
if v.name == weaponName then
weaponLabel = v.label
for _, v2 in ipairs(v.components) do
self.removeWeaponComponent(weaponName, v2)
end
local weaponHash = joaat(v.name)
RemoveWeaponFromPed(playerPed, weaponHash)
SetPedAmmo(playerPed, weaponHash, 0)
table.remove(self.loadout, k)
break
end
end
if weaponLabel then
self.triggerEvent("esx:removeInventoryItem", weaponLabel, false, true)
self.triggerEvent("esx:removeLoadoutItem", weaponName, weaponLabel)
end
end
function self.removeWeaponComponent(weaponName, weaponComponent)
local loadoutNum <const>, weapon <const> = self.getWeapon(weaponName)
if weapon then
local component <const> = ESX.GetWeaponComponent(weaponName, weaponComponent)
if component then
if self.hasWeaponComponent(weaponName, weaponComponent) then
for k, v in ipairs(self.loadout[loadoutNum].components) do
if v == weaponComponent then
table.remove(self.loadout[loadoutNum].components, k)
break
end
end
self.triggerEvent("esx:removeWeaponComponent", weaponName, weaponComponent)
self.triggerEvent("esx:removeInventoryItem", component.label, false, true)
end
end
end
end
function self.removeWeaponAmmo(weaponName, ammoCount)
local _, weapon = self.getWeapon(weaponName)
if weapon then
weapon.ammo = weapon.ammo - ammoCount
SetPedAmmo(GetPlayerPed(self.source), joaat(weaponName), weapon.ammo)
end
end
function self.hasWeaponComponent(weaponName, weaponComponent)
local _, weapon <const> = self.getWeapon(weaponName)
if weapon then
for _, v in ipairs(weapon.components) do
if v == weaponComponent then
return true
end
end
return false
end
return false
end
function self.hasWeapon(weaponName)
for _, v in ipairs(self.loadout) do
if v.name == weaponName then
return true
end
end
return false
end
function self.hasItem(item)
for _, v in ipairs(self.inventory) do
if v.name == item and v.count >= 1 then
return v, v.count
end
end
return false
end
function self.getWeapon(weaponName)
for k, v in ipairs(self.loadout) do
if v.name == weaponName then
return k, v
end
end
return nil, nil
end
function self.showNotification(msg, notifyType, length, title, position)
self.triggerEvent("esx:showNotification", msg, notifyType, length, title, position)
end
function self.showAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex)
self.triggerEvent("esx:showAdvancedNotification", sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex)
end
function self.showHelpNotification(msg, thisFrame, beep, duration)
self.triggerEvent("esx:showHelpNotification", msg, thisFrame, beep, duration)
end
function self.getMeta(index, subIndex)
if not index then
return self.metadata
end
if type(index) ~= "string" then
error("xPlayer.getMeta ^5index^1 should be ^5string^1!")
return
end
local metaData = self.metadata[index]
if metaData == nil then
return Config.EnableDebug and error(("xPlayer.getMeta ^5%s^1 not exist!"):format(index)) or nil
end
if subIndex and type(metaData) == "table" then
local _type = type(subIndex)
if _type == "string" then
local value = metaData[subIndex]
return value
end
if _type == "table" then
local returnValues = {}
for i = 1, #subIndex do
local key = subIndex[i]
if type(key) == "string" then
returnValues[key] = self.getMeta(index, key)
else
error(("xPlayer.getMeta subIndex should be ^5string^1 or ^5table^1! that contains ^5string^1, received ^5%s^1!, skipping..."):format(type(key)))
end
end
return returnValues
end
error(("xPlayer.getMeta subIndex should be ^5string^1 or ^5table^1!, received ^5%s^1!"):format(_type))
return
end
return metaData
end
function self.setMeta(index, value, subValue)
if not index then
return error("xPlayer.setMeta ^5index^1 is Missing!")
end
if type(index) ~= "string" then
return error("xPlayer.setMeta ^5index^1 should be ^5string^1!")
end
if value == nil then
return error("xPlayer.setMeta value is missing!")
end
local _type = type(value)
if not subValue then
if _type ~= "number" and _type ~= "string" and _type ~= "table" then
return error(("xPlayer.setMeta ^5%s^1 should be ^5number^1 or ^5string^1 or ^5table^1!"):format(value))
end
self.metadata[index] = value
else
if _type ~= "string" then
return error(("xPlayer.setMeta ^5value^1 should be ^5string^1 as a subIndex!"):format(value))
end
if not self.metadata[index] or type(self.metadata[index]) ~= "table" then
self.metadata[index] = {}
end
self.metadata[index] = type(self.metadata[index]) == "table" and self.metadata[index] or {}
self.metadata[index][value] = subValue
end
self.triggerEvent('esx:updatePlayerData', 'metadata', self.metadata)
end
function self.clearMeta(index, subValues)
if not index then
return error("xPlayer.clearMeta ^5index^1 is Missing!")
end
if type(index) ~= "string" then
return error("xPlayer.clearMeta ^5index^1 should be ^5string^1!")
end
local metaData = self.metadata[index]
if metaData == nil then
if Config.EnableDebug then
error(("xPlayer.clearMeta ^5%s^1 does not exist!"):format(index))
end
return
end
if not subValues then
-- If no subValues is provided, we will clear the entire value in the metaData table
self.metadata[index] = nil
elseif type(subValues) == "string" then
-- If subValues is a string, we will clear the specific subValue within the table
if type(metaData) == "table" then
metaData[subValues] = nil
else
return error(("xPlayer.clearMeta ^5%s^1 is not a table! Cannot clear subValue ^5%s^1."):format(index, subValues))
end
elseif type(subValues) == "table" then
-- If subValues is a table, we will clear multiple subValues within the table
for i = 1, #subValues do
local subValue = subValues[i]
if type(subValue) == "string" then
if type(metaData) == "table" then
metaData[subValue] = nil
else
error(("xPlayer.clearMeta ^5%s^1 is not a table! Cannot clear subValue ^5%s^1."):format(index, subValue))
end
else
error(("xPlayer.clearMeta subValues should contain ^5string^1, received ^5%s^1, skipping..."):format(type(subValue)))
end
end
else
return error(("xPlayer.clearMeta ^5subValues^1 should be ^5string^1 or ^5table^1, received ^5%s^1!"):format(type(subValues)))
end
self.triggerEvent('esx:updatePlayerData', 'metadata', self.metadata)
end
function self.executeCommand(command)
if type(command) ~= "string" then
error("xPlayer.executeCommand must be of type string!")
return
end
self.triggerEvent("esx:executeCommand", command)
end
for _, funcs in pairs(Core.PlayerFunctionOverrides) do
for fnName, fn in pairs(funcs) do
self[fnName] = fn(self)
end
end
return self
end
local function runStaticPlayerMethod(src, method, ...)
local xPlayer = ESX.Players[src]
if not xPlayer then
return
end
if not ESX.IsFunctionReference(xPlayer[method]) then
error(("Attempted to call invalid method on playerId %s: %s"):format(src, method))
end
return xPlayer[method](...)
end
exports("RunStaticPlayerMethod", runStaticPlayerMethod)

View File

@@ -0,0 +1,237 @@
---@class CVehicleData
---@field plate string
---@field netId number
---@field entity number
---@field modelHash number
---@field owner string
---@class CExtendedVehicle
---@field plate string
---@field isValid fun(self:CExtendedVehicle):boolean
---@field new fun(owner:string, plate:string, coords:vector4): CExtendedVehicle?
---@field getFromPlate fun(plate:string):CExtendedVehicle?
---@field getPlate fun(self:CExtendedVehicle):string?
---@field getNetId fun(self:CExtendedVehicle):number?
---@field getEntity fun(self:CExtendedVehicle):number?
---@field getModelHash fun(self:CExtendedVehicle):number?
---@field getOwner fun(self:CExtendedVehicle):string?
---@field setPlate fun(self:CExtendedVehicle, newPlate:string):boolean
---@field setProps fun(self:CExtendedVehicle, newProps:table):boolean
---@field setOwner fun(self:CExtendedVehicle, newOwner:string):boolean
---@field delete fun(self:CExtendedVehicle, garageName:string?, isImpound:boolean?):nil
Core.vehicleClass = {
plate = "",
new = function(owner, plate, coords)
assert(type(owner) == "string", "Expected 'owner' to be a string")
assert(type(plate) == "string", "Expected 'plate' to be a string")
assert(type(coords) == "vector4", "Expected 'coords' to be a vector4")
local xVehicle = Core.vehicleClass.getFromPlate(plate)
if xVehicle then
return xVehicle
end
local vehicleProps = MySQL.scalar.await("SELECT `vehicle` FROM `owned_vehicles` WHERE `stored` = true AND `owner` = ? AND `plate` = ? LIMIT 1", { owner, plate })
if not vehicleProps then
return
end
vehicleProps = json.decode(vehicleProps)
if type(vehicleProps.model) ~= "number" then
vehicleProps.model = joaat(vehicleProps.model)
end
local netId = ESX.OneSync.SpawnVehicle(vehicleProps.model, coords.xyz, coords.w, vehicleProps)
if not netId then
return
end
local entity = NetworkGetEntityFromNetworkId(netId)
if entity <= 0 then
return
end
Entity(entity).state:set("owner", owner, false)
Entity(entity).state:set("plate", plate, false)
---@type CVehicleData
local vehicleData = {
plate = plate,
entity = entity,
netId = netId,
modelHash = vehicleProps.model,
owner = owner,
}
Core.vehicles[plate] = vehicleData
MySQL.update.await("UPDATE `owned_vehicles` SET `stored` = false WHERE `owner` = ? AND `plate` = ?", { owner, plate })
local obj = table.clone(Core.vehicleClass)
obj.plate = plate
TriggerEvent("esx:createdExtendedVehicle", obj)
return obj
end,
getFromPlate = function(plate)
assert(type(plate) == "string", "Expected 'plate' to be a string")
if Core.vehicles[plate] then
local obj = table.clone(Core.vehicleClass)
obj.plate = plate
if obj:isValid() then
return obj
end
end
end,
isValid = function(self)
local vehicleData = Core.vehicles[self.plate]
if not vehicleData then
return false
end
local entity = NetworkGetEntityFromNetworkId(vehicleData.netId)
if entity <= 0 or Entity(entity).state.owner ~= vehicleData.owner or Entity(entity).state.plate ~= vehicleData.plate then
self:delete()
return false
end
vehicleData.entity = entity
return true
end,
getNetId = function(self)
if not self:isValid() then
return
end
return Core.vehicles[self.plate].netId
end,
getEntity = function(self)
if not self:isValid() then
return
end
return Core.vehicles[self.plate].entity
end,
getPlate = function(self)
if not self:isValid() then
return
end
return Core.vehicles[self.plate].plate
end,
getModelHash = function(self)
if not self:isValid() then
return
end
return Core.vehicles[self.plate].modelHash
end,
getOwner = function(self)
if not self:isValid() then
return
end
return Core.vehicles[self.plate].owner
end,
setPlate = function(self, newPlate)
if not self:isValid() then
return false
end
assert(type(newPlate) == "string", "Expected 'plate' to be a string")
local vehicleData = Core.vehicles[self.plate]
local affectedRows = MySQL.update.await("UPDATE `owned_vehicles` SET `plate` = ? WHERE `plate` = ? AND `owner` = ?", { newPlate, vehicleData.plate, vehicleData.owner })
if affectedRows <= 0 then
self:delete()
return false
end
Entity(vehicleData.entity).state:set("plate", newPlate, false)
SetVehicleNumberPlateText(vehicleData.entity, newPlate)
local oldPlate = vehicleData.plate
vehicleData.plate = newPlate
Core.vehicles[newPlate] = table.clone(vehicleData)
Core.vehicles[oldPlate] = nil
TriggerEvent("esx:changedExtendedVehiclePlate", vehicleData.plate, oldPlate)
Wait(0)
return true
end,
setProps = function(self, newProps)
if not self:isValid() then
return false
end
assert(type(newProps) == "table", "Expected 'props' to be a table")
local vehicleData = Core.vehicles[self.plate]
local affectedRows = MySQL.update.await("UPDATE `owned_vehicles` SET `vehicle` = ? WHERE `plate` = ? AND `owner` = ?", json.encode(newProps), vehicleData.plate, vehicleData.owner)
if affectedRows <= 0 then
self:delete()
return false
end
Entity(vehicleData.entity).state:set("VehicleProperties", newProps, true)
return true
end,
setOwner = function(self, newOwner)
if not self:isValid() then
return false
end
assert(type(newOwner) == "string", "Expected 'owner' to be a string")
local vehicleData = Core.vehicles[self.plate]
if vehicleData.owner == newOwner then
return true
end
local affectedRows = MySQL.update.await("UPDATE `owned_vehicles` SET `owner` = ? WHERE owner = ? AND `plate` = ?", { newOwner, vehicleData.owner, vehicleData.plate })
if affectedRows <= 0 then
self:delete()
return false
end
Entity(vehicleData.entity).state:set("owner", newOwner, false)
vehicleData.owner = newOwner
return true
end,
delete = function(self, garageName, isImpound)
if type(garageName) ~= "string" then
garageName = nil
end
if type(isImpound) ~= "boolean" then
isImpound = false
end
local vehicleData = Core.vehicles[self.plate]
if not vehicleData then
return
end
local entity = NetworkGetEntityFromNetworkId(vehicleData.netId)
if entity >= 0 and Entity(entity).state.owner == vehicleData.owner then
DeleteEntity(vehicleData.entity)
end
local query = "UPDATE `owned_vehicles` SET `stored` = true WHERE `plate` = ? AND `owner` = ?"
local queryParams = { vehicleData.plate, vehicleData.owner }
if garageName then
if isImpound then
query = "UPDATE `owned_vehicles` SET `stored` = true, `parking` = NULL, `pound` = ? WHERE `plate` = ? AND `owner` = ?"
else
query = "UPDATE `owned_vehicles` SET `stored` = true, `pound` = NULL, `parking` = ? WHERE `plate` = ? AND `owner` = ?"
end
queryParams = { garageName, vehicleData.plate, vehicleData.owner }
end
MySQL.update.await(query, queryParams)
TriggerEvent("esx:deletedExtendedVehicle", self)
Core.vehicles[self.plate] = nil
end,
}

View File

@@ -0,0 +1,54 @@
ESX.Players = {}
ESX.Jobs = {}
ESX.Items = {}
RegisterNetEvent("esx:onPlayerSpawn", function()
ESX.Players[source].spawned = true
end)
if Config.CustomInventory then
SetConvarReplicated("inventory:framework", "esx")
SetConvarReplicated("inventory:weight", tostring(Config.MaxWeight * 1000))
end
local function StartDBSync()
CreateThread(function()
local interval <const> = 10 * 60 * 1000
while true do
Wait(interval)
Core.SavePlayers()
end
end)
end
MySQL.ready(function()
Core.DatabaseConnected = true
if not Config.CustomInventory then
ESX.RefreshItems()
end
ESX.RefreshJobs()
print(("[^2INFO^7] ESX ^5Legacy %s^0 initialized!"):format(GetResourceMetadata(GetCurrentResourceName(), "version", 0)))
StartDBSync()
if Config.EnablePaycheck then
StartPayCheck()
end
end)
RegisterNetEvent("esx:clientLog", function(msg)
if Config.EnableDebug then
print(("[^2TRACE^7] %s^7"):format(msg))
end
end)
RegisterNetEvent("esx:ReturnVehicleType", function(Type, Request)
if Core.ClientCallbacks[Request] then
Core.ClientCallbacks[Request](Type)
Core.ClientCallbacks[Request] = nil
end
end)
GlobalState.playerCount = 0

View File

@@ -0,0 +1,14 @@
Core = {}
Core.JobsPlayerCount = {}
Core.UsableItemsCallbacks = {}
Core.RegisteredCommands = {}
Core.Pickups = {}
Core.PickupId = 0
Core.PlayerFunctionOverrides = {}
Core.DatabaseConnected = false
Core.playersByIdentifier = {}
Core.JobsLoaded = false
---@type table<string, CVehicleData>
Core.vehicles = {}
Core.vehicleTypesByModel = {}

View File

@@ -0,0 +1,909 @@
---@param msg string
---@return nil
function ESX.Trace(msg)
if Config.EnableDebug then
print(("[^2TRACE^7] %s^7"):format(msg))
end
end
--- Triggers an event for one or more clients.
---@param eventName string The name of the event to trigger.
---@param playerIds table|number If a number, represents a single player ID. If a table, represents an array of player IDs.
---@param ... any Additional arguments to pass to the event handler.
function ESX.TriggerClientEvent(eventName, playerIds, ...)
if type(playerIds) == "number" then
TriggerClientEvent(eventName, playerIds, ...)
return
end
local payload = msgpack.pack_args(...)
local payloadLength = #payload
for i = 1, #playerIds do
TriggerClientEventInternal(eventName, playerIds[i], payload, payloadLength)
end
end
---@param name string | table
---@param group string | table
---@param cb function
---@param allowConsole? boolean
---@param suggestion? table
function ESX.RegisterCommand(name, group, cb, allowConsole, suggestion)
if type(name) == "table" then
for _, v in ipairs(name) do
ESX.RegisterCommand(v, group, cb, allowConsole, suggestion)
end
return
end
if Core.RegisteredCommands[name] then
print(('[^3WARNING^7] Command ^5"%s" ^7already registered, overriding command'):format(name))
if Core.RegisteredCommands[name].suggestion then
TriggerClientEvent("chat:removeSuggestion", -1, ("/%s"):format(name))
end
end
if suggestion then
if not suggestion.arguments then
suggestion.arguments = {}
end
if not suggestion.help then
suggestion.help = ""
end
TriggerClientEvent("chat:addSuggestion", -1, ("/%s"):format(name), suggestion.help, suggestion.arguments)
end
Core.RegisteredCommands[name] = { group = group, cb = cb, allowConsole = allowConsole, suggestion = suggestion }
RegisterCommand(name, function(playerId, args)
local command = Core.RegisteredCommands[name]
if not command.allowConsole and playerId == 0 then
print(("[^3WARNING^7] ^5%s^0"):format(TranslateCap("commanderror_console")))
else
local xPlayer, err = ESX.Players[playerId], nil
if command.suggestion then
if command.suggestion.validate then
if #args ~= #command.suggestion.arguments then
err = TranslateCap("commanderror_argumentmismatch", #args, #command.suggestion.arguments)
end
end
if not err and command.suggestion.arguments then
local newArgs = {}
for k, v in ipairs(command.suggestion.arguments) do
if v.type then
if v.type == "number" then
local newArg = tonumber(args[k])
if newArg then
newArgs[v.name] = newArg
else
err = TranslateCap("commanderror_argumentmismatch_number", k)
end
elseif v.type == "player" or v.type == "playerId" then
local targetPlayer = tonumber(args[k])
if args[k] == "me" then
targetPlayer = playerId
end
if targetPlayer then
local xTargetPlayer = ESX.GetPlayerFromId(targetPlayer)
if xTargetPlayer then
if v.type == "player" then
newArgs[v.name] = xTargetPlayer
else
newArgs[v.name] = targetPlayer
end
else
err = TranslateCap("commanderror_invalidplayerid")
end
else
err = TranslateCap("commanderror_argumentmismatch_number", k)
end
elseif v.type == "string" then
local newArg = tonumber(args[k])
if not newArg then
newArgs[v.name] = args[k]
else
err = TranslateCap("commanderror_argumentmismatch_string", k)
end
elseif v.type == "item" then
if ESX.Items[args[k]] then
newArgs[v.name] = args[k]
else
err = TranslateCap("commanderror_invaliditem")
end
elseif v.type == "weapon" then
if ESX.GetWeapon(args[k]) then
newArgs[v.name] = string.upper(args[k])
else
err = TranslateCap("commanderror_invalidweapon")
end
elseif v.type == "any" then
newArgs[v.name] = args[k]
elseif v.type == "merge" then
local length = 0
for i = 1, k - 1 do
length = length + string.len(args[i]) + 1
end
local merge = table.concat(args, " ")
newArgs[v.name] = string.sub(merge, length)
elseif v.type == "coordinate" then
local coord = tonumber(args[k]:match("(-?%d+%.?%d*)"))
if not coord then
err = TranslateCap("commanderror_argumentmismatch_number", k)
else
newArgs[v.name] = coord
end
end
end
if ESX.IsFunctionReference(v.Validator?.validate) and not err then
local candidate = newArgs[v.name]
local ok, res = pcall(v.Validator.validate, candidate)
if not ok or res ~= true then
err = v.Validator.err or TranslateCap("commanderror_argumentmismatch")
end
end
--backwards compatibility
if v.validate ~= nil and not v.validate then
err = nil
end
if err then
break
end
end
args = newArgs
end
end
if err then
if playerId == 0 then
print(("[^3WARNING^7] %s^7"):format(err))
else
xPlayer.showNotification(err)
end
else
cb(xPlayer or false, args, function(msg)
if playerId == 0 then
print(("[^3WARNING^7] %s^7"):format(msg))
else
xPlayer.showNotification(msg)
end
end)
end
end
end, true)
if type(group) == "table" then
for _, v in ipairs(group) do
ExecuteCommand(("add_ace group.%s command.%s allow"):format(v, name))
end
else
ExecuteCommand(("add_ace group.%s command.%s allow"):format(group, name))
end
end
local function updateHealthAndArmorInMetadata(xPlayer)
local ped = GetPlayerPed(xPlayer.source)
xPlayer.setMeta("health", GetEntityHealth(ped))
xPlayer.setMeta("armor", GetPedArmour(ped))
xPlayer.setMeta("lastPlaytime", xPlayer.getPlayTime())
end
---@param xPlayer table
---@param cb? function
---@return nil
function Core.SavePlayer(xPlayer, cb)
if not xPlayer.spawned then
return cb and cb()
end
updateHealthAndArmorInMetadata(xPlayer)
local parameters <const> = {
json.encode(xPlayer.getAccounts(true)),
xPlayer.job.name,
xPlayer.job.grade,
xPlayer.group,
json.encode(xPlayer.getCoords(false, true)),
json.encode(xPlayer.getInventory(true)),
json.encode(xPlayer.getLoadout(true)),
json.encode(xPlayer.getMeta()),
xPlayer.identifier,
}
MySQL.prepare(
"UPDATE `users` SET `accounts` = ?, `job` = ?, `job_grade` = ?, `group` = ?, `position` = ?, `inventory` = ?, `loadout` = ?, `metadata` = ? WHERE `identifier` = ?",
parameters,
function(affectedRows)
if affectedRows == 1 then
print(('[^2INFO^7] Saved player ^5"%s^7"'):format(xPlayer.name))
TriggerEvent("esx:playerSaved", xPlayer.playerId, xPlayer)
end
if cb then
cb()
end
end
)
end
---@param cb? function
---@return nil
function Core.SavePlayers(cb)
local xPlayers <const> = ESX.Players
if not next(xPlayers) then
return
end
local startTime <const> = os.time()
local parameters = {}
for _, xPlayer in pairs(ESX.Players) do
updateHealthAndArmorInMetadata(xPlayer)
parameters[#parameters + 1] = {
json.encode(xPlayer.getAccounts(true)),
xPlayer.job.name,
xPlayer.job.grade,
xPlayer.group,
json.encode(xPlayer.getCoords(false, true)),
json.encode(xPlayer.getInventory(true)),
json.encode(xPlayer.getLoadout(true)),
json.encode(xPlayer.getMeta()),
xPlayer.identifier,
}
end
MySQL.prepare(
"UPDATE `users` SET `accounts` = ?, `job` = ?, `job_grade` = ?, `group` = ?, `position` = ?, `inventory` = ?, `loadout` = ?, `metadata` = ? WHERE `identifier` = ?",
parameters,
function(results)
if not results then
return
end
if type(cb) == "function" then
return cb()
end
print(("[^2INFO^7] Saved ^5%s^7 %s over ^5%s^7 ms"):format(#parameters,
#parameters > 1 and "players" or "player", ESX.Math.Round((os.time() - startTime) / 1000000, 2)))
end
)
end
ESX.GetPlayers = GetPlayers
local function checkTable(key, val, xPlayer, xPlayers, minimal)
for valIndex = 1, #val do
local value = val[valIndex]
if not xPlayers[value] then
xPlayers[value] = {}
end
if (key == "job" and xPlayer.job.name == value) or xPlayer[key] == value then
xPlayers[value][#xPlayers[value] + 1] = (minimal and xPlayer.source or xPlayer)
end
end
end
---@param key? string
---@param val? string|table
---@param minimal? boolean
---@return xPlayer[]|number[]|table<any, xPlayer[]>|table<any, number[]>
function ESX.GetExtendedPlayers(key, val, minimal)
if not key then
if not minimal then
return ESX.Table.ToArray(ESX.Players)
end
local xPlayers = {}
local index = 1
for src, _ in pairs(ESX.Players) do
xPlayers[index] = src
index += 1
end
return xPlayers
end
local xPlayers = {}
if type(val) == "table" then
for _, xPlayer in pairs(ESX.Players) do
checkTable(key, val, xPlayer, xPlayers, minimal)
end
return xPlayers
end
for _, xPlayer in pairs(ESX.Players) do
if (key == "job" and xPlayer.job.name == val) or xPlayer[key] == val then
xPlayers[#xPlayers + 1] = (minimal and xPlayer.source or xPlayer)
end
end
return xPlayers
end
---@param key? string
---@param val? string|table
---@return number | table
function ESX.GetNumPlayers(key, val)
if not key then
return #GetPlayers()
end
if type(val) == "table" then
local numPlayers = {}
if key == "job" then
for _, v in ipairs(val) do
numPlayers[v] = (Core.JobsPlayerCount[v] or 0)
end
return numPlayers
end
local filteredPlayers = ESX.GetExtendedPlayers(key, val)
for i, v in pairs(filteredPlayers) do
numPlayers[i] = (#v or 0)
end
return numPlayers
end
if key == "job" then
return (Core.JobsPlayerCount[val] or 0)
end
return #ESX.GetExtendedPlayers(key, val)
end
---@param source number
---@return xPlayer?
function ESX.GetPlayerFromId(source)
return ESX.Players[tonumber(source)]
end
---@param identifier string
---@return xPlayer?
function ESX.GetPlayerFromIdentifier(identifier)
return Core.playersByIdentifier[identifier]
end
---@param identifier string
---@return number playerId
function ESX.GetPlayerIdFromIdentifier(identifier)
return Core.playersByIdentifier[identifier]?.source
end
---@param source number
---@return boolean
---@diagnostic disable-next-line: duplicate-set-field
function ESX.IsPlayerLoaded(source)
return ESX.Players[source] ~= nil
end
---@param playerId number | string
---@return string, number
function ESX.GetIdentifier(playerId)
local fxDk = GetConvarInt("sv_fxdkMode", 0)
if fxDk == 1 then
return "ESX-DEBUG-LICENCE", 0
end
playerId = tostring(playerId)
local identifierType = Config.Identifier
local identifier = GetPlayerIdentifierByType(playerId, identifierType)
assert(identifier, ("[ESX] GetIdentifier failed: no identifier found for playerId %s with type '%s'"):format(playerId, identifierType))
return identifier:gsub(("%s:"):format(identifierType), "")
end
---@param model string|number
---@param player number
---@param cb function?
---@return string?
---@diagnostic disable-next-line: duplicate-set-field
function ESX.GetVehicleType(model, player, cb)
if cb and not ESX.IsFunctionReference(cb) then
error("Invalid callback function")
end
local promise = not cb and promise.new()
local function resolve(result)
if promise then
promise:resolve(result)
elseif cb then
cb(result)
end
return result
end
model = type(model) == "string" and joaat(model) or model
if Core.vehicleTypesByModel[model] then
return resolve(Core.vehicleTypesByModel[model])
end
ESX.TriggerClientCallback(player, "esx:GetVehicleType", function(vehicleType)
Core.vehicleTypesByModel[model] = vehicleType
resolve(vehicleType)
end, model)
if promise then
return Citizen.Await(promise)
end
end
---@param name string
---@param title string
---@param color string
---@param message string
---@return nil
function ESX.DiscordLog(name, title, color, message)
local webHook = Config.DiscordLogs.Webhooks[name] or Config.DiscordLogs.Webhooks.default
local embedData = {
{
["title"] = title,
["color"] = Config.DiscordLogs.Colors[color] or Config.DiscordLogs.Colors.default,
["footer"] = {
["text"] = "| ESX Logs | " .. os.date(),
["icon_url"] =
"https://cdn.discordapp.com/attachments/944789399852417096/1020099828266586193/blanc-800x800.png",
},
["description"] = message,
["author"] = {
["name"] = "ESX Framework",
["icon_url"] = "https://cdn.discordapp.com/emojis/939245183621558362.webp?size=128&quality=lossless",
},
},
}
PerformHttpRequest(
webHook,
function()
return
end,
"POST",
json.encode({
username = "Logs",
embeds = embedData,
}),
{
["Content-Type"] = "application/json",
}
)
end
---@param name string
---@param title string
---@param color string
---@param fields table
---@return nil
function ESX.DiscordLogFields(name, title, color, fields)
for i = 1, #fields do
local field = fields[i]
field.value = tostring(field.value)
end
local webHook = Config.DiscordLogs.Webhooks[name] or Config.DiscordLogs.Webhooks.default
local embedData = {
{
["title"] = title,
["color"] = Config.DiscordLogs.Colors[color] or Config.DiscordLogs.Colors.default,
["footer"] = {
["text"] = "| ESX Logs | " .. os.date(),
["icon_url"] =
"https://cdn.discordapp.com/attachments/944789399852417096/1020099828266586193/blanc-800x800.png",
},
["fields"] = fields,
["description"] = "",
["author"] = {
["name"] = "ESX Framework",
["icon_url"] = "https://cdn.discordapp.com/emojis/939245183621558362.webp?size=128&quality=lossless",
},
},
}
PerformHttpRequest(
webHook,
function()
return
end,
"POST",
json.encode({
username = "Logs",
embeds = embedData,
}),
{
["Content-Type"] = "application/json",
}
)
end
---@return nil
function ESX.RefreshJobs()
Core.JobsLoaded = false
local Jobs = {}
local jobs = MySQL.query.await("SELECT * FROM jobs")
for _, v in ipairs(jobs) do
Jobs[v.name] = v
Jobs[v.name].grades = {}
end
local jobGrades = MySQL.query.await("SELECT * FROM job_grades")
for _, v in ipairs(jobGrades) do
if Jobs[v.job_name] then
Jobs[v.job_name].grades[tostring(v.grade)] = v
else
print(('[^3WARNING^7] Ignoring job grades for ^5"%s"^0 due to missing job'):format(v.job_name))
end
end
for _, v in pairs(Jobs) do
if ESX.Table.SizeOf(v.grades) == 0 then
Jobs[v.name] = nil
print(('[^3WARNING^7] Ignoring job ^5"%s"^0 due to no job grades found'):format(v.name))
end
end
if not Jobs then
-- Fallback data, if no jobs exist
ESX.Jobs["unemployed"] = { name = "unemployed", label = "Unemployed", type = "civ", whitelisted = false, grades = { ["0"] = { grade = 0, name = "unemployed", label = "Unemployed", salary = 200, skin_male = {}, skin_female = {} } } }
else
ESX.Jobs = Jobs
end
TriggerEvent("esx:jobsRefreshed")
Core.JobsLoaded = true
end
---@param item string
---@param cb function
---@return nil
function ESX.RegisterUsableItem(item, cb)
Core.UsableItemsCallbacks[item] = cb
end
---@param source number
---@param item string
---@param ... any
---@return nil
function ESX.UseItem(source, item, ...)
if ESX.Items[item] then
local itemCallback = Core.UsableItemsCallbacks[item]
if itemCallback then
local success, result = pcall(itemCallback, source, item, ...)
if not success then
return result and print(result) or
print(('[^3WARNING^7] An error occured when using item ^5"%s"^7! This was not caused by ESX.'):format(
item))
end
end
else
print(('[^3WARNING^7] Item ^5"%s"^7 was used but does not exist!'):format(item))
end
end
---@param index string
---@param overrides table
---@return nil
function ESX.RegisterPlayerFunctionOverrides(index, overrides)
Core.PlayerFunctionOverrides[index] = overrides
end
---@param index string
---@return nil
function ESX.SetPlayerFunctionOverride(index)
if not index or not Core.PlayerFunctionOverrides[index] then
return print("[^3WARNING^7] No valid index provided.")
end
Config.PlayerFunctionOverride = index
end
---@param item string
---@return string?
---@diagnostic disable-next-line: duplicate-set-field
function ESX.GetItemLabel(item)
if ESX.Items[item] then
return ESX.Items[item].label
else
print(("[^3WARNING^7] Attemting to get invalid Item -> ^5%s^7"):format(item))
end
end
---@param jobType string|string[]?
---@return table
function ESX.GetJobs(jobType)
while not Core.JobsLoaded do
Citizen.Wait(200)
end
if not jobType then
return ESX.Jobs
end
jobType = type(jobType) == "string" and { jobType } or jobType
local jobTypeLookup = {}
for i = 1, #jobType do
jobTypeLookup[jobType[i]] = true
end
local filteredJobs = {}
for jobName, jobObject in pairs(ESX.Jobs) do
if jobTypeLookup[jobObject.type] then
filteredJobs[jobName] = jobObject
end
end
return filteredJobs
end
---@return table
function ESX.GetItems()
return ESX.Items
end
---@return table
function ESX.GetUsableItems()
local Usables = {}
for k in pairs(Core.UsableItemsCallbacks) do
Usables[k] = true
end
return Usables
end
if not Config.CustomInventory then
---@param itemType string
---@param name string
---@param count integer
---@param label string
---@param playerId number
---@param components? string | table
---@param tintIndex? integer
---@param coords? table | vector3
---@return nil
function ESX.CreatePickup(itemType, name, count, label, playerId, components, tintIndex, coords)
local pickupId = (Core.PickupId == 65635 and 0 or Core.PickupId + 1)
local xPlayer = ESX.Players[playerId]
coords = ((type(coords) == "vector3" or type(coords) == "vector4") and coords.xyz or xPlayer.getCoords(true))
Core.Pickups[pickupId] = { type = itemType, name = name, count = count, label = label, coords = coords }
if itemType == "item_weapon" then
Core.Pickups[pickupId].components = components
Core.Pickups[pickupId].tintIndex = tintIndex
end
TriggerClientEvent("esx:createPickup", -1, pickupId, label, coords, itemType, name, components, tintIndex)
Core.PickupId = pickupId
end
local function refreshPlayerInventories()
local xPlayers = ESX.GetExtendedPlayers()
for i = 1, #xPlayers do
local xPlayer = xPlayers[i]
local minimalInv = xPlayer.getInventory(true)
for itemName, _ in pairs(minimalInv) do
if not ESX.Items[itemName] then
xPlayer.setInventoryItem(itemName, 0)
minimalInv[itemName] = nil
end
end
xPlayer.inventory = {}
local playerInvIndex = 1
for itemName, itemData in pairs(ESX.Items) do
xPlayer.inventory[playerInvIndex] = {
name = itemName,
count = minimalInv[itemName] or 0,
label = itemData.label,
weight = itemData.weight,
usable = Core.UsableItemsCallbacks[itemName] ~= nil,
rare = itemData.rare,
canRemove = itemData.canRemove,
}
playerInvIndex += 1
end
TriggerClientEvent("esx:setInventory", xPlayer.source, xPlayer.inventory)
end
end
---@return number newItemCount
function ESX.RefreshItems()
ESX.Items = {}
local items = MySQL.query.await("SELECT * FROM items")
local itemCount = #items
for i = 1, itemCount do
local item = items[i]
ESX.Items[item.name] = { label = item.label, weight = item.weight, rare = item.rare, canRemove = item
.can_remove }
end
refreshPlayerInventories()
return itemCount
end
---@param items { name: string, label: string, weight?: number, rare?: boolean, canRemove?: boolean }[]
function ESX.AddItems(items)
local toInsert = {}
local toInsertIndex = 1
for i = 1, #items do
local item = items[i]
local name = item.name
local label = item.label
local weight = item.weight or 1
local rare = item.rare or false
local canRemove = item.canRemove ~= false
if type(name) ~= "string" then
print(("^1[AddItems]^0 Invalid item name: %s"):format(name))
goto continue
end
if ESX.Items[name] then
goto continue
end
if type(label) ~= "string" then
print(("^1[AddItems]^0 Invalid label for item '%s'"):format(name))
goto continue
end
if type(weight) ~= "number" then
print(("^1[AddItems]^0 Invalid weight for item '%s'"):format(name))
goto continue
end
if type(rare) ~= "boolean" then
print(("^1[AddItems]^0 Invalid rare flag for item '%s'"):format(name))
goto continue
end
if type(canRemove) ~= "boolean" then
print(("^1[AddItems]^0 Invalid canRemove flag for item '%s'"):format(name))
goto continue
end
toInsert[toInsertIndex] = {
name = name,
label = label,
weight = weight,
rare = rare,
canRemove = canRemove,
}
toInsertIndex += 1
::continue::
end
if #toInsert > 0 then
MySQL.prepare.await(
"INSERT IGNORE INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES (?, ?, ?, ?, ?)",
toInsert)
for i = 1, #toInsert do
local row = toInsert[i]
ESX.Items[row.name] = {
label = row.label,
weight = row.weight,
rare = row.rare,
canRemove = row.canRemove,
}
end
refreshPlayerInventories()
end
end
end
---@param job string
---@param grade string
---@return boolean
function ESX.DoesJobExist(job, grade)
while not Core.JobsLoaded do
Citizen.Wait(200)
end
return (ESX.Jobs[job] and ESX.Jobs[job].grades[tostring(grade)] ~= nil) or false
end
---@param playerSrc number
---@return boolean
function Core.IsPlayerAdmin(playerSrc)
if type(playerSrc) ~= "number" then
return false
end
if IsPlayerAceAllowed(playerSrc --[[@as string]], "command") or GetConvar("sv_lan", "") == "true" then
return true
end
local xPlayer = ESX.GetPlayerFromId(playerSrc)
return xPlayer and Config.AdminGroups[xPlayer.getGroup()] or false
end
-- Generates a unique 9-digit SSN in dashed format (XXX-XX-XXXX).
---@param skipUniqueCheck boolean?
---@return string
function Core.generateSSN(skipUniqueCheck)
local reservedSSNs = {
["078-05-1120"] = true,
["219-09-9999"] = true,
["123-45-6789"] = true
}
while true do
-- Generate the first part (area number)
local area = math.random(1, 899)
-- 666 is never assigned
if area == 666 then
goto continue
end
-- Generate the second part (group number)
local group = math.random(1, 99)
-- Generate the last part (serial number)
local serial = math.random(1, 9999)
-- Skip reserved SSN range (987-65-4320..4329)
if area == 987 and group == 65 and serial >= 4320 and serial <= 4329 then
goto continue
end
local candidate = string.format("%03d-%02d-%04d", area, group, serial)
if reservedSSNs[candidate] then
goto continue
end
if skipUniqueCheck then
return candidate
end
local exists = MySQL.scalar.await("SELECT 1 FROM `users` WHERE `ssn` = ? LIMIT 1", { candidate })
if not exists then
return candidate
end
::continue::
end
end
---@param owner string
---@param plate string
---@param coords vector4
---@return CExtendedVehicle?
function ESX.CreateExtendedVehicle(owner, plate, coords)
return Core.vehicleClass.new(owner, plate, coords)
end
---@param plate string
---@return CExtendedVehicle?
function ESX.GetExtendedVehicleFromPlate(plate)
return Core.vehicleClass.getFromPlate(plate)
end

View File

@@ -0,0 +1,792 @@
SetMapName("San Andreas")
SetGameType("ESX Legacy")
local oneSyncState = GetConvar("onesync", "off")
local newPlayer = "INSERT INTO `users` SET `accounts` = ?, `identifier` = ?, `ssn` = ?, `group` = ?"
local loadPlayer = "SELECT `accounts`, `ssn`, `job`, `job_grade`, `group`, `position`, `inventory`, `skin`, `loadout`, `metadata`"
if Config.Multichar then
newPlayer = newPlayer .. ", `firstname` = ?, `lastname` = ?, `dateofbirth` = ?, `sex` = ?, `height` = ?"
end
if Config.StartingInventoryItems then
newPlayer = newPlayer .. ", `inventory` = ?"
end
if Config.Multichar or Config.Identity then
loadPlayer = loadPlayer .. ", `firstname`, `lastname`, `dateofbirth`, `sex`, `height`"
end
loadPlayer = loadPlayer .. " FROM `users` WHERE identifier = ?"
local function createESXPlayer(identifier, playerId, data)
local accounts = {}
for account, money in pairs(Config.StartingAccountMoney) do
accounts[account] = money
end
local defaultGroup = "user"
if Core.IsPlayerAdmin(playerId) then
print(("[^2INFO^0] Player ^5%s^0 Has been granted admin permissions via ^5Ace Perms^7."):format(playerId))
defaultGroup = "admin"
end
local parameters = Config.Multichar and
{ json.encode(accounts), identifier, Core.generateSSN(), defaultGroup, data.firstname, data.lastname, data.dateofbirth, data.sex, data.height }
or { json.encode(accounts), identifier, Core.generateSSN(), defaultGroup }
if Config.StartingInventoryItems then
table.insert(parameters, json.encode(Config.StartingInventoryItems))
end
MySQL.prepare(newPlayer, parameters, function()
loadESXPlayer(identifier, playerId, true)
end)
end
local function onPlayerJoined(playerId)
local identifier = ESX.GetIdentifier(playerId)
if not identifier then
return DropPlayer(playerId, "there was an error loading your character!\nError code: identifier-missing-ingame\n\nThe cause of this error is not known, your identifier could not be found. Please come back later or report this problem to the server administration team.")
end
if ESX.GetPlayerFromIdentifier(identifier) then
DropPlayer(
playerId,
("there was an error loading your character!\nError code: identifier-active-ingame\n\nThis error is caused by a player on this server who has the same identifier as you have. Make sure you are not playing on the same Rockstar account.\n\nYour Rockstar identifier: %s"):format(
identifier
)
)
else
local result = MySQL.scalar.await("SELECT 1 FROM users WHERE identifier = ?", { identifier })
if result then
loadESXPlayer(identifier, playerId, false)
else
createESXPlayer(identifier, playerId)
end
end
end
---@param playerId number
---@param reason string
---@param cb function?
local function onPlayerDropped(playerId, reason, cb)
local p = not cb and promise:new()
local function resolve()
if cb then
return cb()
elseif(p) then
return p:resolve()
end
end
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then
return resolve()
end
TriggerEvent("esx:playerDropped", playerId, reason)
local job = xPlayer.getJob().name
local currentJob = Core.JobsPlayerCount[job]
Core.JobsPlayerCount[job] = ((currentJob and currentJob > 0) and currentJob or 1) - 1
GlobalState[("%s:count"):format(job)] = Core.JobsPlayerCount[job]
Core.SavePlayer(xPlayer, function()
GlobalState["playerCount"] = GlobalState["playerCount"] - 1
ESX.Players[playerId] = nil
Core.playersByIdentifier[xPlayer.identifier] = nil
resolve()
end)
if p then
return Citizen.Await(p)
end
end
AddEventHandler("esx:onPlayerDropped", onPlayerDropped)
if Config.Multichar then
AddEventHandler("esx:onPlayerJoined", function(src, char, data)
while not next(ESX.Jobs) do
Wait(50)
end
if not ESX.Players[src] then
local identifier = char .. ":" .. ESX.GetIdentifier(src)
if data then
createESXPlayer(identifier, src, data)
else
loadESXPlayer(identifier, src, false)
end
end
end)
else
RegisterNetEvent("esx:onPlayerJoined", function()
local _source = source
while not next(ESX.Jobs) do
Wait(50)
end
if not ESX.Players[_source] then
onPlayerJoined(_source)
end
end)
end
if not Config.Multichar then
AddEventHandler("playerConnecting", function(_, _, deferrals)
local playerId = source
deferrals.defer()
Wait(0) -- Required
local identifier
local correctLicense, _ = pcall(function ()
identifier = ESX.GetIdentifier(playerId)
end)
-- luacheck: ignore
if not SetEntityOrphanMode then
return deferrals.done(("[ESX] ESX Requires a minimum Artifact version of 10188, Please update your server."))
end
if oneSyncState == "off" or oneSyncState == "legacy" then
return deferrals.done(("[ESX] ESX Requires Onesync Infinity to work. This server currently has Onesync set to: %s"):format(oneSyncState))
end
if not Core.DatabaseConnected then
return deferrals.done("[ESX] OxMySQL Was Unable To Connect to your database. Please make sure it is turned on and correctly configured in your server.cfg")
end
if not identifier or not correctLicense then
if GetResourceState("esx_identity") ~= "started" then
return deferrals.done("[ESX] There was an error loading your character!\nError code: identifier-missing\n\nThe cause of this error is not known, your identifier could not be found. Please come back later or report this problem to the server administration team.")
end
end
local xPlayer = ESX.GetPlayerFromIdentifier(identifier)
if not xPlayer then
return deferrals.done()
end
if GetPlayerPing(xPlayer.source --[[@as string]]) > 0 then
return deferrals.done(
("[ESX] There was an error loading your character!\nError code: identifier-active\n\nThis error is caused by a player on this server who has the same identifier as you have. Make sure you are not playing on the same account.\n\nYour identifier: %s"):format(identifier)
)
end
deferrals.update(("[ESX] Cleaning stale player entry..."):format(identifier))
onPlayerDropped(xPlayer.source, "esx_stale_player_obj")
deferrals.done()
end)
end
function loadESXPlayer(identifier, playerId, isNew)
local userData = {
accounts = {},
inventory = {},
loadout = {},
weight = 0,
name = GetPlayerName(playerId),
identifier = identifier,
firstName = "John",
lastName = "Doe",
dateofbirth = "01/01/2000",
height = 120,
dead = false,
}
local result = MySQL.prepare.await(loadPlayer, { identifier })
-- Accounts
local accounts = result.accounts
accounts = (accounts and accounts ~= "") and json.decode(accounts) or {}
for account, data in pairs(Config.Accounts) do
data.round = data.round or data.round == nil
local index = #userData.accounts + 1
userData.accounts[index] = {
name = account,
money = accounts[account] or Config.StartingAccountMoney[account] or 0,
label = data.label,
round = data.round,
index = index,
}
end
-- SSN
userData.ssn = result.ssn
-- Job
local job, grade = result.job, tostring(result.job_grade)
if not ESX.DoesJobExist(job, grade) then
print(("[^3WARNING^7] Ignoring invalid job for ^5%s^7 [job: ^5%s^7, grade: ^5%s^7]"):format(identifier, job, grade))
job, grade = "unemployed", "0"
end
local jobObject, gradeObject = ESX.Jobs[job], ESX.Jobs[job].grades[grade]
userData.job = {
id = jobObject.id,
name = jobObject.name,
label = jobObject.label,
type = jobObject.type,
grade = tonumber(grade),
grade_name = gradeObject.name,
grade_label = gradeObject.label,
grade_salary = gradeObject.salary,
skin_male = gradeObject.skin_male and json.decode(gradeObject.skin_male) or {},
skin_female = gradeObject.skin_female and json.decode(gradeObject.skin_female) or {},
}
-- Inventory
if not Config.CustomInventory then
local inventory = (result.inventory and result.inventory ~= "") and json.decode(result.inventory) or {}
for name, item in pairs(ESX.Items) do
local count = inventory[name] or 0
userData.weight += (count * item.weight)
userData.inventory[#userData.inventory + 1] = {
name = name,
count = count,
label = item.label,
weight = item.weight,
usable = Core.UsableItemsCallbacks[name] ~= nil,
rare = item.rare,
canRemove = item.canRemove,
}
end
table.sort(userData.inventory, function(a, b)
return a.label < b.label
end)
elseif result.inventory and result.inventory ~= "" then
userData.inventory = json.decode(result.inventory)
end
-- Group
if result.group then
if result.group == "superadmin" then
userData.group = "admin"
print("[^3WARNING^7] ^5Superadmin^7 detected, setting group to ^5admin^7")
else
userData.group = result.group
end
else
userData.group = "user"
end
-- Loadout
if not Config.CustomInventory then
if result.loadout and result.loadout ~= "" then
local loadout = json.decode(result.loadout)
for name, weapon in pairs(loadout) do
local label = ESX.GetWeaponLabel(name)
if label then
userData.loadout[#userData.loadout + 1] = {
name = name,
ammo = weapon.ammo,
label = label,
components = weapon.components or {},
tintIndex = weapon.tintIndex or 0,
}
end
end
end
end
-- Position
userData.coords = json.decode(result.position) or Config.DefaultSpawns[ESX.Math.Random(1,#Config.DefaultSpawns)]
-- Skin
userData.skin = (result.skin and result.skin ~= "") and json.decode(result.skin) or { sex = userData.sex == "f" and 1 or 0 }
-- Metadata
userData.metadata = (result.metadata and result.metadata ~= "") and json.decode(result.metadata) or {}
-- xPlayer Creation
local xPlayer = CreateExtendedPlayer(playerId, identifier, userData.ssn, userData.group, userData.accounts, userData.inventory, userData.weight, userData.job, userData.loadout, GetPlayerName(playerId), userData.coords, userData.metadata)
GlobalState["playerCount"] = GlobalState["playerCount"] + 1
ESX.Players[playerId] = xPlayer
Core.playersByIdentifier[identifier] = xPlayer
-- Identity
if result.firstname and result.firstname ~= "" then
userData.firstName = result.firstname
userData.lastName = result.lastname
local name = ("%s %s"):format(result.firstname, result.lastname)
userData.name = name
xPlayer.set("firstName", result.firstname)
xPlayer.set("lastName", result.lastname)
xPlayer.setName(name)
if result.dateofbirth then
userData.dateofbirth = result.dateofbirth
xPlayer.set("dateofbirth", result.dateofbirth)
end
if result.sex then
userData.sex = result.sex
xPlayer.set("sex", result.sex)
end
if result.height then
userData.height = result.height
xPlayer.set("height", result.height)
end
end
TriggerEvent("esx:playerLoaded", playerId, xPlayer, isNew)
userData.money = xPlayer.getMoney()
userData.maxWeight = xPlayer.getMaxWeight()
userData.variables = xPlayer.variables or {}
xPlayer.triggerEvent("esx:playerLoaded", userData, isNew, userData.skin)
if not Config.CustomInventory then
xPlayer.triggerEvent("esx:createMissingPickups", Core.Pickups)
elseif setPlayerInventory then
setPlayerInventory(playerId, xPlayer, userData.inventory, isNew)
end
xPlayer.triggerEvent("esx:registerSuggestions", Core.RegisteredCommands)
print(('[^2INFO^0] Player ^5"%s"^0 has connected to the server. ID: ^5%s^7'):format(xPlayer.getName(), playerId))
end
AddEventHandler("chatMessage", function(playerId, _, message)
local xPlayer = ESX.GetPlayerFromId(playerId)
if xPlayer and message:sub(1, 1) == "/" and playerId > 0 then
CancelEvent()
local commandName = message:sub(1):gmatch("%w+")()
xPlayer.showNotification(TranslateCap("commanderror_invalidcommand", commandName))
end
end)
---@param reason string
AddEventHandler("playerDropped", function(reason)
onPlayerDropped(source --[[@as number]], reason)
end)
AddEventHandler("esx:playerLoaded", function(_, xPlayer, isNew)
local job = xPlayer.getJob().name
local jobKey = ("%s:count"):format(job)
Core.JobsPlayerCount[job] = (Core.JobsPlayerCount[job] or 0) + 1
GlobalState[jobKey] = Core.JobsPlayerCount[job]
if isNew then
Player(xPlayer.source).state:set('isNew', true, false)
end
end)
AddEventHandler("esx:setJob", function(_, job, lastJob)
local lastJobKey = ("%s:count"):format(lastJob.name)
local jobKey = ("%s:count"):format(job.name)
local currentLastJob = Core.JobsPlayerCount[lastJob.name]
Core.JobsPlayerCount[lastJob.name] = ((currentLastJob and currentLastJob > 0) and currentLastJob or 1) - 1
Core.JobsPlayerCount[job.name] = (Core.JobsPlayerCount[job.name] or 0) + 1
GlobalState[lastJobKey] = Core.JobsPlayerCount[lastJob.name]
GlobalState[jobKey] = Core.JobsPlayerCount[job.name]
end)
AddEventHandler("esx:playerLogout", function(playerId, cb)
onPlayerDropped(playerId, "esx_player_logout", cb)
TriggerClientEvent("esx:onPlayerLogout", playerId)
end)
if not Config.CustomInventory then
RegisterNetEvent("esx:updateWeaponAmmo", function(weaponName, ammoCount)
local xPlayer = ESX.GetPlayerFromId(source)
if xPlayer then
xPlayer.updateWeaponAmmo(weaponName, ammoCount)
end
end)
RegisterNetEvent("esx:giveInventoryItem", function(target, itemType, itemName, itemCount)
local playerId = source
local sourceXPlayer = ESX.GetPlayerFromId(playerId)
local targetXPlayer = ESX.GetPlayerFromId(target)
local distance = #(GetEntityCoords(GetPlayerPed(playerId)) - GetEntityCoords(GetPlayerPed(target)))
if not sourceXPlayer or not targetXPlayer or distance > Config.DistanceGive then
print(("[^3WARNING^7] Player Detected Cheating: ^5%s^7"):format(GetPlayerName(playerId)))
return
end
if itemType == "item_standard" then
local sourceItem = sourceXPlayer.getInventoryItem(itemName)
if not sourceItem then
return
end
if itemCount < 1 or sourceItem.count < itemCount then
return sourceXPlayer.showNotification(TranslateCap("imp_invalid_quantity"))
end
if not targetXPlayer.canCarryItem(itemName, itemCount) then
return sourceXPlayer.showNotification(TranslateCap("ex_inv_lim", targetXPlayer.name))
end
sourceXPlayer.removeInventoryItem(itemName, itemCount)
targetXPlayer.addInventoryItem(itemName, itemCount)
sourceXPlayer.showNotification(TranslateCap("gave_item", itemCount, sourceItem.label, targetXPlayer.name))
targetXPlayer.showNotification(TranslateCap("received_item", itemCount, sourceItem.label, sourceXPlayer.name))
elseif itemType == "item_account" then
if itemCount < 1 or sourceXPlayer.getAccount(itemName).money < itemCount then
return sourceXPlayer.showNotification(TranslateCap("imp_invalid_amount"))
end
sourceXPlayer.removeAccountMoney(itemName, itemCount, "Gave to " .. targetXPlayer.name)
targetXPlayer.addAccountMoney(itemName, itemCount, "Received from " .. sourceXPlayer.name)
sourceXPlayer.showNotification(TranslateCap("gave_account_money", ESX.Math.GroupDigits(itemCount), Config.Accounts[itemName].label, targetXPlayer.name))
targetXPlayer.showNotification(TranslateCap("received_account_money", ESX.Math.GroupDigits(itemCount), Config.Accounts[itemName].label, sourceXPlayer.name))
elseif itemType == "item_weapon" then
if not sourceXPlayer.hasWeapon(itemName) then
return
end
local weaponLabel = ESX.GetWeaponLabel(itemName)
if targetXPlayer.hasWeapon(itemName) then
sourceXPlayer.showNotification(TranslateCap("gave_weapon_hasalready", targetXPlayer.name, weaponLabel))
targetXPlayer.showNotification(TranslateCap("received_weapon_hasalready", sourceXPlayer.name, weaponLabel))
return
end
local _, weapon = sourceXPlayer.getWeapon(itemName)
if not weapon then
return
end
local _, weaponObject = ESX.GetWeapon(itemName)
itemCount = weapon.ammo
local weaponComponents = ESX.Table.Clone(weapon.components)
local weaponTint = weapon.tintIndex
if weaponTint then
targetXPlayer.setWeaponTint(itemName, weaponTint)
end
if weaponComponents then
for _, v in pairs(weaponComponents) do
targetXPlayer.addWeaponComponent(itemName, v)
end
end
sourceXPlayer.removeWeapon(itemName)
targetXPlayer.addWeapon(itemName, itemCount)
if weaponObject.ammo and itemCount > 0 then
local ammoLabel = weaponObject.ammo.label
sourceXPlayer.showNotification(TranslateCap("gave_weapon_withammo", weaponLabel, itemCount, ammoLabel, targetXPlayer.name))
targetXPlayer.showNotification(TranslateCap("received_weapon_withammo", weaponLabel, itemCount, ammoLabel, sourceXPlayer.name))
else
sourceXPlayer.showNotification(TranslateCap("gave_weapon", weaponLabel, targetXPlayer.name))
targetXPlayer.showNotification(TranslateCap("received_weapon", weaponLabel, sourceXPlayer.name))
end
elseif itemType == "item_ammo" then
if not sourceXPlayer.hasWeapon(itemName) then
return
end
local _, weapon = sourceXPlayer.getWeapon(itemName)
if not weapon then
return
end
if not targetXPlayer.hasWeapon(itemName) then
sourceXPlayer.showNotification(TranslateCap("gave_weapon_noweapon", targetXPlayer.name))
targetXPlayer.showNotification(TranslateCap("received_weapon_noweapon", sourceXPlayer.name, weapon.label))
return
end
local _, weaponObject = ESX.GetWeapon(itemName)
if not weaponObject.ammo then return end
local ammoLabel = weaponObject.ammo.label
if weapon.ammo >= itemCount then
sourceXPlayer.removeWeaponAmmo(itemName, itemCount)
targetXPlayer.addWeaponAmmo(itemName, itemCount)
sourceXPlayer.showNotification(TranslateCap("gave_weapon_ammo", itemCount, ammoLabel, weapon.label, targetXPlayer.name))
targetXPlayer.showNotification(TranslateCap("received_weapon_ammo", itemCount, ammoLabel, weapon.label, sourceXPlayer.name))
end
end
end)
RegisterNetEvent("esx:removeInventoryItem", function(itemType, itemName, itemCount)
local playerId = source
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then
return
end
if itemType == "item_standard" then
if not itemCount or itemCount < 1 then
return xPlayer.showNotification(TranslateCap("imp_invalid_quantity"))
end
local xItem = xPlayer.getInventoryItem(itemName)
if not xItem then
return
end
if itemCount > xItem.count or xItem.count < 1 then
return xPlayer.showNotification(TranslateCap("imp_invalid_quantity"))
end
xPlayer.removeInventoryItem(itemName, itemCount)
local pickupLabel = ("%s [%s]"):format(xItem.label, itemCount)
ESX.CreatePickup("item_standard", itemName, itemCount, pickupLabel, playerId)
xPlayer.showNotification(TranslateCap("threw_standard", itemCount, xItem.label))
elseif itemType == "item_account" then
if itemCount == nil or itemCount < 1 then
return xPlayer.showNotification(TranslateCap("imp_invalid_amount"))
end
local account = xPlayer.getAccount(itemName)
if not account then
return
end
if itemCount > account.money or account.money < 1 then
return xPlayer.showNotification(TranslateCap("imp_invalid_amount"))
end
xPlayer.removeAccountMoney(itemName, itemCount, "Threw away")
local pickupLabel = ("%s [%s]"):format(account.label, TranslateCap("locale_currency", ESX.Math.GroupDigits(itemCount)))
ESX.CreatePickup("item_account", itemName, itemCount, pickupLabel, playerId)
xPlayer.showNotification(TranslateCap("threw_account", ESX.Math.GroupDigits(itemCount), string.lower(account.label)))
elseif itemType == "item_weapon" then
itemName = string.upper(itemName)
if not xPlayer.hasWeapon(itemName) then return end
local _, weapon = xPlayer.getWeapon(itemName)
if not weapon then
return
end
local _, weaponObject = ESX.GetWeapon(itemName)
-- luacheck: ignore weaponPickupLabel
local weaponPickupLabel = ""
local components = ESX.Table.Clone(weapon.components)
xPlayer.removeWeapon(itemName)
if weaponObject.ammo and weapon.ammo > 0 then
local ammoLabel = weaponObject.ammo.label
weaponPickupLabel = ("%s [%s %s]"):format(weapon.label, weapon.ammo, ammoLabel)
xPlayer.showNotification(TranslateCap("threw_weapon_ammo", weapon.label, weapon.ammo, ammoLabel))
else
weaponPickupLabel = ("%s"):format(weapon.label)
xPlayer.showNotification(TranslateCap("threw_weapon", weapon.label))
end
ESX.CreatePickup("item_weapon", itemName, weapon.ammo, weaponPickupLabel, playerId, components, weapon.tintIndex)
end
end)
RegisterNetEvent("esx:useItem", function(itemName)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then
return
end
local count = xPlayer.getInventoryItem(itemName).count
if count < 1 then
return xPlayer.showNotification(TranslateCap("act_imp"))
end
ESX.UseItem(source, itemName)
end)
RegisterNetEvent("esx:onPickup", function(pickupId)
local pickup, xPlayer, success = Core.Pickups[pickupId], ESX.GetPlayerFromId(source)
if not xPlayer then
return
end
if not pickup then return end
local playerPickupDistance = #(pickup.coords - xPlayer.getCoords(true))
if playerPickupDistance > 5.0 then
print(("[^3WARNING^7] Player Detected Cheating (Out of range pickup): ^5%s^7"):format(xPlayer.getIdentifier()))
return
end
if pickup.type == "item_standard" then
if not xPlayer.canCarryItem(pickup.name, pickup.count) then
return xPlayer.showNotification(TranslateCap("threw_cannot_pickup"))
end
xPlayer.addInventoryItem(pickup.name, pickup.count)
success = true
elseif pickup.type == "item_account" then
success = true
xPlayer.addAccountMoney(pickup.name, pickup.count, "Picked up")
elseif pickup.type == "item_weapon" then
if xPlayer.hasWeapon(pickup.name) then
return xPlayer.showNotification(TranslateCap("threw_weapon_already"))
end
success = true
xPlayer.addWeapon(pickup.name, pickup.count)
xPlayer.setWeaponTint(pickup.name, pickup.tintIndex)
for _, v in ipairs(pickup.components) do
xPlayer.addWeaponComponent(pickup.name, v)
end
end
if success then
Core.Pickups[pickupId] = nil
TriggerClientEvent("esx:removePickup", -1, pickupId)
end
end)
end
ESX.RegisterServerCallback("esx:getPlayerData", function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then
return
end
cb({
identifier = xPlayer.identifier,
accounts = xPlayer.getAccounts(),
inventory = xPlayer.getInventory(),
job = xPlayer.getJob(),
loadout = xPlayer.getLoadout(),
money = xPlayer.getMoney(),
position = xPlayer.getCoords(true),
metadata = xPlayer.getMeta(),
})
end)
ESX.RegisterServerCallback("esx:isUserAdmin", function(source, cb)
cb(Core.IsPlayerAdmin(source))
end)
ESX.RegisterServerCallback("esx:getGameBuild", function(_, cb)
cb(tonumber(GetConvar("sv_enforceGameBuild", "1604")))
end)
ESX.RegisterServerCallback("esx:getOtherPlayerData", function(_, cb, target)
local xPlayer = ESX.GetPlayerFromId(target)
if not xPlayer then
return
end
cb({
identifier = xPlayer.identifier,
accounts = xPlayer.getAccounts(),
inventory = xPlayer.getInventory(),
job = xPlayer.getJob(),
loadout = xPlayer.getLoadout(),
money = xPlayer.getMoney(),
position = xPlayer.getCoords(true),
metadata = xPlayer.getMeta(),
})
end)
ESX.RegisterServerCallback("esx:getPlayerNames", function(source, cb, players)
players[source] = nil
for playerId, _ in pairs(players) do
local xPlayer = ESX.GetPlayerFromId(playerId)
if xPlayer then
players[playerId] = xPlayer.getName()
else
players[playerId] = nil
end
end
cb(players)
end)
ESX.RegisterServerCallback("esx:spawnVehicle", function(source, cb, vehData)
local ped = GetPlayerPed(source)
ESX.OneSync.SpawnVehicle(vehData.model or `ADDER`, vehData.coords or GetEntityCoords(ped), vehData.coords.w or 0.0, vehData.props or {}, function(id)
if vehData.warp then
local vehicle = NetworkGetEntityFromNetworkId(id)
local timeout = 0
while GetVehiclePedIsIn(ped, false) ~= vehicle and timeout <= 15 do
Wait(0)
TaskWarpPedIntoVehicle(ped, vehicle, -1)
timeout += 1
end
end
cb(id)
end)
end)
AddEventHandler("txAdmin:events:scheduledRestart", function(eventData)
if eventData.secondsRemaining == 60 then
CreateThread(function()
Wait(50000)
Core.SavePlayers()
end)
end
end)
AddEventHandler("txAdmin:events:serverShuttingDown", function()
Core.SavePlayers()
end)
local DoNotUse = {
["essentialmode"] = true,
["es_admin2"] = true,
["basic-gamemode"] = true,
["mapmanager"] = true,
["fivem-map-skater"] = true,
["fivem-map-hipster"] = true,
["qb-core"] = true,
["default_spawnpoint"] = true,
}
AddEventHandler("onResourceStart", function(key)
if DoNotUse[string.lower(key)] then
while GetResourceState(key) ~= "started" do
Wait(0)
end
StopResource(key)
error(("WE STOPPED A RESOURCE THAT WILL BREAK ^1ESX^1, PLEASE REMOVE ^5%s^1"):format(key))
end
-- luacheck: ignore
if not SetEntityOrphanMode then
CreateThread(function()
while true do
error("ESX Requires a minimum Artifact version of 10188, Please update your server.")
Wait(60 * 1000)
end
end)
end
end)
for key in pairs(DoNotUse) do
if GetResourceState(key) == "started" or GetResourceState(key) == "starting" then
StopResource(key)
error(("WE STOPPED A RESOURCE THAT WILL BREAK ^1ESX^1, PLEASE REMOVE ^5%s^1"):format(key))
end
end

View File

@@ -0,0 +1,46 @@
RegisterCommand("resetmigrations", function(src)
if src > 0 then
print("^1[ERROR]^7 This command can only be run from the server console.")
return
end
for version, _ in pairs(Core.Migrations or {}) do
DeleteResourceKvp(("esx_migration:%s"):format(version))
end
print("^2[SUCCESS]^7 Reset all migrations. This will re-run all migrations on the next server start.")
end)
local migrationsRan = 0
local restartRequired = false
for esxVersion, migrations in pairs(Core.Migrations or {}) do
---@cast esxVersion string
---@cast migrations table<string, function>
if ESX.Table.SizeOf(migrations) > 0 then
print(("^4[INFO]^7 Running migrations for ESX version %s"):format(esxVersion))
for migrationName, migration in pairs(migrations) do
local success, result = pcall(migration)
if not success then
local err = result --[[@as string]]
error(("^1[ERROR]^7 Failed migration ^4['%s.%s']^7: %s"):format(esxVersion, migrationName, err))
end
local migrationRequiresRestart = result --[[@as boolean]]
if not restartRequired and migrationRequiresRestart then
restartRequired = true
end
end
SetResourceKvpInt(("esx_migration:%s"):format(esxVersion), 1)
print(("^2[SUCCESS]^7 Successfully completed migrations for ESX version %s"):format(esxVersion))
migrationsRan += 1
end
end
while restartRequired do
print(("^4[INFO]^7 Ran migrations for %d ESX version(s). ^1Server restart required!^7"):format(migrationsRan))
Wait(500)
end

View File

@@ -0,0 +1,70 @@
local esxVersion = "v1.13.3"
Core.Migrations = Core.Migrations or {}
Core.Migrations[esxVersion] = Core.Migrations[esxVersion] or {}
if GetResourceKvpInt(("esx_migration:%s"):format(esxVersion)) == 1 then
return
end
---@return boolean restartRequired
Core.Migrations[esxVersion].ssn = function()
print("^4[esx_migration:v.1.13.3:ssn]^7 Adding SSN column to users table.")
local col = MySQL.scalar.await([[
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME = 'ssn'
]])
local idx = MySQL.scalar.await([[
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND INDEX_NAME = 'unique_ssn'
]])
if col == 0 and idx == 0 then
MySQL.update.await([[
ALTER TABLE `users`
ADD COLUMN `ssn` VARCHAR(11) NULL DEFAULT NULL AFTER `identifier`,
ADD UNIQUE KEY `unique_ssn` (`ssn`)
]])
elseif col == 0 then
MySQL.update.await("ALTER TABLE `users` ADD COLUMN `ssn` VARCHAR(11) NULL DEFAULT NULL AFTER `identifier`")
elseif idx == 0 then
MySQL.update.await("ALTER TABLE `users` ADD UNIQUE KEY `unique_ssn` (`ssn`)")
end
local Result = MySQL.query.await("SELECT `identifier` FROM `users` WHERE `ssn` IS NULL")
if #Result == 0 then
print("^4[esx_migration:v.1.13.3:ssn]^7 No users found without SSN, migration not needed.")
return false
end
print("^4[esx_migration:v.1.13.3:ssn]^7 Generating SSN for existing users.")
local GeneratedSSNs = {}
local Parameters = {}
for i = 1, #Result do
local ssn
repeat
ssn = Core.generateSSN(true)
until not GeneratedSSNs[ssn]
GeneratedSSNs[ssn] = true
Parameters[i] = { ssn, Result[i].identifier }
end
print("^4[esx_migration:v.1.13.3:ssn]^7 Updating users with generated SSN. This may take a minute...")
MySQL.prepare.await("UPDATE `users` SET `ssn` = ? WHERE `identifier` = ?", Parameters)
print("^4[esx_migration:v.1.13.3:ssn]^7 Removing SSN default value.")
MySQL.update.await("ALTER TABLE `users` MODIFY `ssn` VARCHAR(11) NOT NULL")
print(("^4[esx_migration:v.1.13.3:ssn]^7 Successfully migrated %d users."):format(#Parameters))
return true
end

View File

@@ -0,0 +1,35 @@
local esxVersion = "v1.13.5"
Core.Migrations = Core.Migrations or {}
Core.Migrations[esxVersion] = Core.Migrations[esxVersion] or {}
if GetResourceKvpInt(("esx_migration:%s"):format(esxVersion)) == 1 then
return
end
---@return boolean restartRequired
Core.Migrations[esxVersion].jobTypes = function()
print("^4[esx_migration:v1.13.5:jobTypes]^7 Adding job type column to jobs table.")
local col = MySQL.scalar.await([[
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'jobs'
AND COLUMN_NAME = 'type'
]])
if col == 0 then
print("^4[esx_migration:v1.13.5:jobTypes]^7 Column not found, altering jobs table.")
MySQL.update.await([[
ALTER TABLE `jobs`
ADD COLUMN `type` VARCHAR(50) NOT NULL DEFAULT 'civ' AFTER `label`
]])
else
print("^4[esx_migration:v1.13.5:jobTypes]^7 Column already exists, migration not needed.")
return false
end
print("^4[esx_migration:v1.13.5:jobTypes]^7 Migration complete.")
return true
end

View File

@@ -0,0 +1,155 @@
---@diagnostic disable: duplicate-set-field
-- =============================================
-- MARK: Variables
-- =============================================
Callbacks = {}
Callbacks.requests = {}
Callbacks.storage = {}
Callbacks.id = 0
-- =============================================
-- MARK: Internal Functions
-- =============================================
function Callbacks:Register(name, resource, cb)
self.storage[name] = {
resource = resource,
cb = cb
}
end
function Callbacks:Execute(cb, ...)
local success, errorString = pcall(cb, ...)
if not success then
print(("[^1ERROR^7] Failed to execute Callback with RequestId: ^5%s^7"):format(self.currentId))
print("^3Callback Error:^7 " .. tostring(errorString)) -- just log, don't throw
self.currentId = nil
return
end
self.currentId = nil
end
function Callbacks:Trigger(player, event, cb, invoker, ...)
self.requests[self.id] = {
await = type(cb) == "boolean",
cb = cb or promise:new()
}
local table = self.requests[self.id]
TriggerClientEvent("esx:triggerClientCallback", player, event, self.id, invoker, ...)
self.id += 1
return table.cb
end
function Callbacks:ServerRecieve(player, event, requestId, invoker, ...)
self.currentId = requestId
if not self.storage[event] then
return error(("Server Callback with requestId ^5%s^1 Was Called by ^5%s^1 but does not exist."):format(event, invoker))
end
local returnCb = function(...)
TriggerClientEvent("esx:serverCallback", player, requestId, invoker, ...)
end
local callback = self.storage[event].cb
self:Execute(callback, player, returnCb, ...)
end
function Callbacks:RecieveClient(requestId, invoker, ...)
self.currentId = requestId
if not self.requests[self.currentId] then
return error(("Client Callback with requestId ^5%s^1 Was Called by ^5%s^1 but does not exist."):format(self.currentId, invoker))
end
local callback = self.requests[self.currentId]
self.requests[requestId] = nil
if callback.await then
callback.cb:resolve({ ... })
else
self:Execute(callback.cb, ...)
end
end
-- =============================================
-- MARK: ESX Functions
-- =============================================
---@param player number playerId
---@param eventName string
---@param callback function
---@param ... any
function ESX.TriggerClientCallback(player, eventName, callback, ...)
local invokingResource = GetInvokingResource()
local invoker = (invokingResource and invokingResource ~= "Unknown") and invokingResource or "es_extended"
Callbacks:Trigger(player, eventName, callback, invoker, ...)
end
---@param player number playerId
---@param eventName string
---@param ... any
---@return ...
function ESX.AwaitClientCallback(player, eventName, ...)
local invokingResource = GetInvokingResource()
local invoker = (invokingResource and invokingResource ~= "Unknown") and invokingResource or "es_extended"
local p = Callbacks:Trigger(player, eventName, false, invoker, ...)
if not p then return end
SetTimeout(15000, function()
if p.state == "pending" then
p:reject("Server Callback Timed Out")
end
end)
Citizen.Await(p)
return table.unpack(p.value)
end
---@param eventName string
---@param callback function
---@return nil
function ESX.RegisterServerCallback(eventName, callback)
local invokingResource = GetInvokingResource()
local invoker = (invokingResource and invokingResource ~= "Unknown") and invokingResource or "es_extended"
Callbacks:Register(eventName, invoker, callback)
end
---@param eventName string
---@return boolean
function ESX.DoesServerCallbackExist(eventName)
return Callbacks.storage[eventName] ~= nil
end
-- =============================================
-- MARK: Events
-- =============================================
RegisterNetEvent("esx:clientCallback", function(requestId, invoker, ...)
Callbacks:RecieveClient(requestId, invoker, ...)
end)
RegisterNetEvent("esx:triggerServerCallback", function(eventName, requestId, invoker, ...)
local source = source
Callbacks:ServerRecieve(source, eventName, requestId, invoker, ...)
end)
AddEventHandler("onResourceStop", function(resource)
for k, v in pairs(Callbacks.storage) do
if v.resource == resource then
Callbacks.storage[k] = nil
end
end
end)

View File

@@ -0,0 +1,774 @@
ESX.RegisterCommand(
{ "setcoords", "tp" },
"admin",
function(xPlayer, args)
xPlayer.setCoords({ x = args.x, y = args.y, z = args.z })
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Set Coordinates /setcoords Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "X Coord", value = args.x, inline = true },
{ name = "Y Coord", value = args.y, inline = true },
{ name = "Z Coord", value = args.z, inline = true },
})
end
end,
false,
{
help = TranslateCap("command_setcoords"),
validate = true,
arguments = {
{ name = "x", help = TranslateCap("command_setcoords_x"), type = "coordinate" },
{ name = "y", help = TranslateCap("command_setcoords_y"), type = "coordinate" },
{ name = "z", help = TranslateCap("command_setcoords_z"), type = "coordinate" },
},
}
)
ESX.RegisterCommand(
"setjob",
"admin",
function(xPlayer, args, showError)
if not ESX.DoesJobExist(args.job, args.grade) then
return showError(TranslateCap("command_setjob_invalid"))
end
args.playerId.setJob(args.job, args.grade)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Set Job /setjob Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Job", value = args.job, inline = true },
{ name = "Grade", value = args.grade, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_setjob"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "job", help = TranslateCap("command_setjob_job"), type = "string" },
{ name = "grade", help = TranslateCap("command_setjob_grade"), type = "number" },
},
}
)
local upgrades = Config.SpawnVehMaxUpgrades and {
plate = "ADMINCAR",
modEngine = 3,
modBrakes = 2,
modTransmission = 2,
modSuspension = 3,
modArmor = true,
windowTint = 1,
} or {}
ESX.RegisterCommand(
"car",
"admin",
function(xPlayer, args, showError)
if not xPlayer then
return showError("[^1ERROR^7] The xPlayer value is nil")
end
local playerPed = GetPlayerPed(xPlayer.source)
local playerCoords = GetEntityCoords(playerPed)
local playerHeading = GetEntityHeading(playerPed)
local playerVehicle = GetVehiclePedIsIn(playerPed, false)
if not args.car or type(args.car) ~= "string" then
args.car = "adder"
end
if playerVehicle then
DeleteEntity(playerVehicle)
end
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Spawn Car /car Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Vehicle", value = args.car, inline = true },
})
end
local xRoutingBucket = GetPlayerRoutingBucket(xPlayer.source)
ESX.OneSync.SpawnVehicle(args.car, playerCoords, playerHeading, upgrades, function(networkId)
if networkId then
local vehicle = NetworkGetEntityFromNetworkId(networkId)
if xRoutingBucket ~= 0 then
SetEntityRoutingBucket(vehicle, xRoutingBucket)
end
for _ = 1, 100 do
Wait(0)
SetPedIntoVehicle(playerPed, vehicle, -1)
if GetVehiclePedIsIn(playerPed, false) == vehicle then
break
end
end
if GetVehiclePedIsIn(playerPed, false) ~= vehicle then
showError("[^1ERROR^7] The player could not be seated in the vehicle")
end
end
end)
end,
false,
{
help = TranslateCap("command_car"),
validate = false,
arguments = {
{ name = "car", validate = false, help = TranslateCap("command_car_car"), type = "string" },
},
}
)
ESX.RegisterCommand(
{ "cardel", "dv" },
"admin",
function(xPlayer, args)
local ped = GetPlayerPed(xPlayer.source)
local pedVehicle = GetVehiclePedIsIn(ped, false)
if DoesEntityExist(pedVehicle) then
DeleteEntity(pedVehicle)
end
local coords = GetEntityCoords(ped)
local Vehicles = ESX.OneSync.GetVehiclesInArea(coords, tonumber(args.radius) or 5.0)
for i = 1, #Vehicles do
local Vehicle = NetworkGetEntityFromNetworkId(Vehicles[i])
if DoesEntityExist(Vehicle) then
DeleteEntity(Vehicle)
end
end
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Delete Vehicle /dv Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
})
end
end,
false,
{
help = TranslateCap("command_cardel"),
validate = false,
arguments = {
{ name = "radius", validate = false, help = TranslateCap("command_cardel_radius"), type = "number" },
},
}
)
ESX.RegisterCommand(
{ "fix", "repair" },
"admin",
function(xPlayer, args, showError)
local xTarget = args.playerId
local ped = GetPlayerPed(xTarget.source)
local pedVehicle = GetVehiclePedIsIn(ped, false)
if not pedVehicle or GetPedInVehicleSeat(pedVehicle, -1) ~= ped then
showError(TranslateCap("not_in_vehicle"))
return
end
xTarget.triggerEvent("esx:repairPedVehicle")
xPlayer.showNotification(TranslateCap("command_repair_success"), true, false, 140)
if xPlayer.source ~= xTarget.source then
xTarget.showNotification(TranslateCap("command_repair_success_target"), true, false, 140)
end
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Fix Vehicle /fix Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = xTarget.name, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_repair"),
validate = false,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand(
"setaccountmoney",
"admin",
function(xPlayer, args, showError)
if not args.playerId.getAccount(args.account) then
return showError(TranslateCap("command_giveaccountmoney_invalid"))
end
args.playerId.setAccountMoney(args.account, args.amount, "Government Grant")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Set Account Money /setaccountmoney Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Account", value = args.account, inline = true },
{ name = "Amount", value = args.amount, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_setaccountmoney"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "account", help = TranslateCap("command_giveaccountmoney_account"), type = "string" },
{ name = "amount", help = TranslateCap("command_setaccountmoney_amount"), type = "number" },
},
}
)
ESX.RegisterCommand(
"giveaccountmoney",
"admin",
function(xPlayer, args, showError)
if not args.playerId.getAccount(args.account) then
return showError(TranslateCap("command_giveaccountmoney_invalid"))
end
args.playerId.addAccountMoney(args.account, args.amount, "Government Grant")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Give Account Money /giveaccountmoney Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Account", value = args.account, inline = true },
{ name = "Amount", value = args.amount, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_giveaccountmoney"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "account", help = TranslateCap("command_giveaccountmoney_account"), type = "string" },
{ name = "amount", help = TranslateCap("command_giveaccountmoney_amount"), type = "number" },
},
}
)
ESX.RegisterCommand(
"removeaccountmoney",
"admin",
function(xPlayer, args, showError)
if not args.playerId.getAccount(args.account) then
return showError(TranslateCap("command_removeaccountmoney_invalid"))
end
args.playerId.removeAccountMoney(args.account, args.amount, "Government Tax")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Remove Account Money /removeaccountmoney Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Account", value = args.account, inline = true },
{ name = "Amount", value = args.amount, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_removeaccountmoney"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "account", help = TranslateCap("command_removeaccountmoney_account"), type = "string" },
{ name = "amount", help = TranslateCap("command_removeaccountmoney_amount"), type = "number" },
},
}
)
if not Config.CustomInventory then
ESX.RegisterCommand(
"giveitem",
"admin",
function(xPlayer, args)
args.playerId.addInventoryItem(args.item, args.count)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Give Item /giveitem Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Item", value = args.item, inline = true },
{ name = "Quantity", value = args.count, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_giveitem"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "item", help = TranslateCap("command_giveitem_item"), type = "item" },
{ name = "count", help = TranslateCap("command_giveitem_count"), type = "number", Validator = {
validate = function(x) return x > 0 end,
err = TranslateCap("commanderror_argumentmismatch_positive_number", "count")
}},
},
}
)
ESX.RegisterCommand(
"giveweapon",
"admin",
function(xPlayer, args, showError)
if args.playerId.hasWeapon(args.weapon) then
return showError(TranslateCap("command_giveweapon_hasalready"))
end
args.playerId.addWeapon(args.weapon, args.ammo)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Give Weapon /giveweapon Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Weapon", value = args.weapon, inline = true },
{ name = "Ammo", value = args.ammo, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_giveweapon"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "weapon", help = TranslateCap("command_giveweapon_weapon"), type = "weapon" },
{ name = "ammo", help = TranslateCap("command_giveweapon_ammo"), type = "number" },
},
}
)
ESX.RegisterCommand(
"giveammo",
"admin",
function(xPlayer, args, showError)
if not args.playerId.hasWeapon(args.weapon) then
return showError(TranslateCap("command_giveammo_noweapon_found"))
end
args.playerId.addWeaponAmmo(args.weapon, args.ammo)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Give Ammunition /giveammo Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Weapon", value = args.weapon, inline = true },
{ name = "Ammo", value = args.ammo, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_giveweapon"),
validate = false,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "weapon", help = TranslateCap("command_giveammo_weapon"), type = "weapon" },
{ name = "ammo", help = TranslateCap("command_giveammo_ammo"), type = "number" },
},
}
)
ESX.RegisterCommand(
"giveweaponcomponent",
"admin",
function(xPlayer, args, showError)
if args.playerId.hasWeapon(args.weaponName) then
local component = ESX.GetWeaponComponent(args.weaponName, args.componentName)
if component then
if args.playerId.hasWeaponComponent(args.weaponName, args.componentName) then
showError(TranslateCap("command_giveweaponcomponent_hasalready"))
else
args.playerId.addWeaponComponent(args.weaponName, args.componentName)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Give Weapon Component /giveweaponcomponent Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Weapon", value = args.weaponName, inline = true },
{ name = "Component", value = args.componentName, inline = true },
})
end
end
else
showError(TranslateCap("command_giveweaponcomponent_invalid"))
end
else
showError(TranslateCap("command_giveweaponcomponent_missingweapon"))
end
end,
true,
{
help = TranslateCap("command_giveweaponcomponent"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "weaponName", help = TranslateCap("command_giveweapon_weapon"), type = "weapon" },
{ name = "componentName", help = TranslateCap("command_giveweaponcomponent_component"), type = "string" },
},
}
)
end
ESX.RegisterCommand({ "clear", "cls" }, "user", function(xPlayer)
xPlayer.triggerEvent("chat:clear")
end, false, { help = TranslateCap("command_clear") })
ESX.RegisterCommand({ "clearall", "clsall" }, "admin", function(xPlayer)
TriggerClientEvent("chat:clear", -1)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Clear Chat /clearall Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
})
end
end, true, { help = TranslateCap("command_clearall") })
ESX.RegisterCommand("refreshjobs", "admin", function()
ESX.RefreshJobs()
end, true, { help = TranslateCap("command_clearall") })
if not Config.CustomInventory then
ESX.RegisterCommand("refreshitems", "admin", function(xPlayer)
local itemCount = ESX.RefreshItems()
xPlayer.showNotification(Translate("command_refreshitems_success", itemCount), true, false, 140)
end, true, { help = TranslateCap("command_refreshitems") })
ESX.RegisterCommand(
"clearinventory",
"admin",
function(xPlayer, args)
for _, v in ipairs(args.playerId.inventory) do
if v.count > 0 then
args.playerId.setInventoryItem(v.name, 0)
end
end
TriggerEvent("esx:playerInventoryCleared", args.playerId)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Clear Inventory /clearinventory Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_clearinventory"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand(
"clearloadout",
"admin",
function(xPlayer, args)
for i = #args.playerId.loadout, 1, -1 do
args.playerId.removeWeapon(args.playerId.loadout[i].name)
end
TriggerEvent("esx:playerLoadoutCleared", args.playerId)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "/clearloadout Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_clearloadout"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
end
ESX.RegisterCommand(
"setgroup",
"admin",
function(xPlayer, args)
if not args.playerId then
args.playerId = xPlayer.source
end
if args.group == "superadmin" then
args.group = "admin"
print("[^3WARNING^7] ^5Superadmin^7 detected, setting group to ^5admin^7")
end
args.playerId.setGroup(args.group)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "/setgroup Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Group", value = args.group, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_setgroup"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "group", help = TranslateCap("command_setgroup_group"), type = "string" },
},
}
)
ESX.RegisterCommand(
"save",
"admin",
function(_, args)
Core.SavePlayer(args.playerId)
print(("[^2Info^0] Saved Player - ^5%s^0"):format(args.playerId.source))
end,
true,
{
help = TranslateCap("command_save"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand("saveall", "admin", function()
Core.SavePlayers()
end, true, { help = TranslateCap("command_saveall") })
ESX.RegisterCommand("group", { "user", "admin" }, function(xPlayer, _, _)
print(("%s, you are currently: ^5%s^0"):format(xPlayer.getName(), xPlayer.getGroup()))
end, true)
ESX.RegisterCommand("job", { "user", "admin" }, function(xPlayer, _, _)
local job = xPlayer.getJob()
print(("%s, your job is: ^5%s^0 - ^5%s^0 - ^5%s^0"):format(xPlayer.getName(), job.name, job.grade_label, job.onDuty and "On Duty" or "Off Duty"))
end, false)
ESX.RegisterCommand("info", { "user", "admin" }, function(xPlayer)
local job = xPlayer.getJob().name
print(("^2ID: ^5%s^0 | ^2Name: ^5%s^0 | ^2Group: ^5%s^0 | ^2Job: ^5%s^0"):format(xPlayer.source, xPlayer.getName(), xPlayer.getGroup(), job))
end, false)
ESX.RegisterCommand("playtime", { "user", "admin" }, function(xPlayer)
local playtime = xPlayer.getPlayTime()
local days = math.floor(playtime / 86400)
local hours = math.floor((playtime % 86400) / 3600)
local minutes = math.floor((playtime % 3600) / 60)
print(("Playtime: ^5%s^0 Days | ^5%s^0 Hours | ^5%s^0 Minutes"):format(days, hours, minutes))
end, false)
ESX.RegisterCommand("coords", "admin", function(xPlayer)
local ped = GetPlayerPed(xPlayer.source)
local coords = GetEntityCoords(ped, false)
local heading = GetEntityHeading(ped)
print(("Coords - Vector3: ^5%s^0"):format(vector3(coords.x, coords.y, coords.z)))
print(("Coords - Vector4: ^5%s^0"):format(vector4(coords.x, coords.y, coords.z, heading)))
end, false)
ESX.RegisterCommand("tpm", "admin", function(xPlayer)
xPlayer.triggerEvent("esx:tpm")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin Teleport /tpm Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
})
end
end, false)
ESX.RegisterCommand(
"goto",
"admin",
function(xPlayer, args)
local targetCoords = args.playerId.getCoords()
local srcDim = GetPlayerRoutingBucket(xPlayer.source)
local targetDim = GetPlayerRoutingBucket(args.playerId.source)
if srcDim ~= targetDim then
SetPlayerRoutingBucket(xPlayer.source, targetDim)
end
xPlayer.setCoords(targetCoords)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin Teleport /goto Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Target Coords", value = targetCoords, inline = true },
})
end
end,
false,
{
help = TranslateCap("command_goto"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand(
"bring",
"admin",
function(xPlayer, args)
local targetCoords = args.playerId.getCoords()
local playerCoords = xPlayer.getCoords()
local targetDim = GetPlayerRoutingBucket(args.playerId.source)
local srcDim = GetPlayerRoutingBucket(xPlayer.source)
if targetDim ~= srcDim then
SetPlayerRoutingBucket(args.playerId.source, srcDim)
end
args.playerId.setCoords(playerCoords)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin Teleport /bring Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Target Coords", value = targetCoords, inline = true },
})
end
end,
false,
{
help = TranslateCap("command_bring"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand(
"kill",
"admin",
function(xPlayer, args)
args.playerId.triggerEvent("esx:killPlayer")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Kill Command /kill Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_kill"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand(
"freeze",
"admin",
function(xPlayer, args)
args.playerId.triggerEvent("esx:freezePlayer", "freeze")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin Freeze /freeze Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_freeze"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand(
"unfreeze",
"admin",
function(xPlayer, args)
args.playerId.triggerEvent("esx:freezePlayer", "unfreeze")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin UnFreeze /unfreeze Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_unfreeze"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
},
}
)
ESX.RegisterCommand("noclip", "admin", function(xPlayer)
xPlayer.triggerEvent("esx:noclip")
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin NoClip /noclip Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
})
end
end, false)
ESX.RegisterCommand("players", "admin", function()
local xPlayers = ESX.GetExtendedPlayers() -- Returns all xPlayers
print(("^5%s^2 online player(s)^0"):format(#xPlayers))
for i = 1, #xPlayers do
local xPlayer = xPlayers[i]
print(("^1[^2ID: ^5%s^0 | ^2Name : ^5%s^0 | ^2Group : ^5%s^0 | ^2Identifier : ^5%s^1]^0\n"):format(xPlayer.source, xPlayer.getName(), xPlayer.getGroup(), xPlayer.identifier))
end
end, true)
ESX.RegisterCommand(
{"setdim", "setbucket"},
"admin",
function(xPlayer, args)
SetPlayerRoutingBucket(args.playerId.source, args.dimension)
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Admin Set Dim /setdim Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Target", value = args.playerId.name, inline = true },
{ name = "Dimension", value = args.dimension, inline = true },
})
end
end,
true,
{
help = TranslateCap("command_setdim"),
validate = true,
arguments = {
{ name = "playerId", help = TranslateCap("commandgeneric_playerid"), type = "player" },
{ name = "dimension", help = TranslateCap("commandgeneric_dimension"), type = "number" },
},
}
)

View File

@@ -0,0 +1,101 @@
local NOTIFY_TYPES = {
INFO = "^5[%s]^7-^6[INFO]^7 %s",
SUCCESS = "^5[%s]^7-^2[SUCCESS]^7 %s",
ERROR = "^5[%s]^7-^1[ERROR]^7 %s"
}
local function doesJobAndGradesExist(name, grades)
if not ESX.Jobs[name] then
return false
end
for _, grade in ipairs(grades) do
if not ESX.DoesJobExist(name, grade.grade) then
return false
end
end
return true
end
local function generateNewJobTable(name, label, grades, jobType)
local job = { name = name, label = label, type = jobType, grades = {} }
for _, v in pairs(grades) do
job.grades[tostring(v.grade)] = { job_name = name, grade = v.grade, name = v.name, label = v.label, salary = v.salary, skin_male = v.skin_male or '{}', skin_female = v.skin_female or '{}' }
end
return job
end
local function notify(notifyType,resourceName,message,...)
local formattedMessage = string.format(message, ...)
if not NOTIFY_TYPES[notifyType] then
return print(NOTIFY_TYPES.INFO:format(resourceName,formattedMessage))
end
return print(NOTIFY_TYPES[notifyType]:format(resourceName,formattedMessage))
end
--- Create Job at Runtime
--- @param name string
--- @param label string
--- @param grades table
--- @param jobType string
function ESX.CreateJob(name, label, grades, jobType)
local currentResourceName = GetInvokingResource()
local success = false
if not name or name == '' then
notify("ERROR",currentResourceName, 'Missing argument `name`')
return success
end
if not label or label == '' then
notify("ERROR",currentResourceName, 'Missing argument `label`')
return success
end
if not grades or not next(grades) then
notify("ERROR",currentResourceName, 'Missing argument `grades`')
return success
end
if type(jobType) ~= "string" then
jobType = "civ"
end
local currentJobExist = doesJobAndGradesExist(name, grades)
if currentJobExist then
notify("ERROR",currentResourceName, 'Job or grades already exists: `%s`', name)
return success
end
local queries = {
{ query = 'INSERT INTO `jobs` (`name`, `label`, `type`) VALUES (?, ?, ?)', values = { name, label, jobType } }
}
for _, grade in pairs(grades) do
queries[#queries + 1] = {
query = 'INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES (?, ?, ?, ?, ?, ?, ?)',
values = { name, grade.grade, grade.name, grade.label, grade.salary, grade.skin_male and json.encode(grade.skin_male) or '{}', grade.skin_female and json.encode(grade.skin_female) or '{}' }
}
end
success = exports.oxmysql:transaction_async(queries)
if not success then
notify("ERROR", currentResourceName, 'Failed to insert one or more grades for job: `%s`', name)
return success
end
ESX.Jobs[name] = generateNewJobTable(name, label, grades, jobType)
notify("SUCCESS", currentResourceName, 'Job created successfully: `%s`', name)
TriggerEvent('esx:jobCreated', name, ESX.Jobs[name])
return success
end

View File

@@ -0,0 +1,52 @@
local npwd = GetResourceState("npwd"):find("start") and exports.npwd or nil
AddEventHandler("onServerResourceStart", function(resource)
if resource ~= "npwd" then
return
end
npwd = GetResourceState("npwd"):find("start") and exports.npwd or nil
if not npwd then
return
end
for _, xPlayer in pairs(ESX.Players) do
npwd:newPlayer({
source = xPlayer.source,
identifier = xPlayer.identifier,
firstname = xPlayer.get("firstName"),
lastname = xPlayer.get("lastName"),
})
end
end)
AddEventHandler("onServerResourceStop", function(resource)
if resource == "npwd" then
npwd = nil
end
end)
AddEventHandler("esx:playerLoaded", function(playerId, xPlayer)
if not npwd then
return
end
if not xPlayer then
xPlayer = ESX.GetPlayerFromId(playerId)
end
npwd:newPlayer({
source = playerId,
identifier = xPlayer.identifier,
firstname = xPlayer.get("firstName"),
lastname = xPlayer.get("lastName"),
})
end)
AddEventHandler("esx:playerLogout", function(playerId)
if not npwd then
return
end
npwd:unloadPlayer(playerId)
end)

View File

@@ -0,0 +1,387 @@
ESX.OneSync = {}
---@param source number|vector3
---@param closest boolean
---@param distance? number
---@param ignore? table
---@param routingBucket? number
local function getNearbyPlayers(source, closest, distance, ignore, routingBucket)
local result = {}
local count = 0
local playerPed
local playerCoords
ignore = ignore or {}
if not distance then
distance = 100
end
if type(source) == "number" then
playerPed = GetPlayerPed(source)
if not source then
error("Received invalid first argument (source); should be playerId")
end
playerCoords = GetEntityCoords(playerPed)
if not playerCoords then
error("Received nil value (playerCoords); perhaps source is nil at first place?")
end
end
if type(source) == "vector3" then
playerCoords = source
if not playerCoords then
error("Received nil value (playerCoords); perhaps source is nil at first place?")
end
end
for _, xPlayer in pairs(ESX.Players) do
if not ignore[xPlayer.source] and (not routingBucket or GetPlayerRoutingBucket(xPlayer.source) == routingBucket) then
local entity = GetPlayerPed(xPlayer.source)
local coords = GetEntityCoords(entity)
if not closest then
local dist = #(playerCoords - coords)
if dist <= distance then
count = count + 1
result[count] = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist }
end
else
if xPlayer.source ~= source then
local dist = #(playerCoords - coords)
if dist <= (result.dist or distance) then
result = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist }
end
end
end
end
end
return result
end
---@param source vector3|number playerId or vector3 coordinates
---@param maxDistance number
---@param ignore? table playerIds to ignore, where the key is playerId and value is true
---@param routingBucket? number
function ESX.OneSync.GetPlayersInArea(source, maxDistance, ignore, routingBucket)
return getNearbyPlayers(source, false, maxDistance, ignore, routingBucket)
end
---@param source vector3|number playerId or vector3 coordinates
---@param maxDistance number
---@param ignore? table playerIds to ignore, where the key is playerId and value is true
---@param routingBucket? number
function ESX.OneSync.GetClosestPlayer(source, maxDistance, ignore, routingBucket)
return getNearbyPlayers(source, true, maxDistance, ignore, routingBucket)
end
---@param vehicleModel number|string
---@param coords vector3|table
---@param heading number
---@param vehicleProperties table
---@param cb? fun(netId: number)
---@param vehicleType string?
---@return number? netId
function ESX.OneSync.SpawnVehicle(vehicleModel, coords, heading, vehicleProperties, cb, vehicleType)
if cb and not ESX.IsFunctionReference(cb) then
error("Invalid callback function")
end
vehicleModel = joaat(vehicleModel)
local promise = not cb and promise.new()
local function resolve(result)
if promise then
promise:resolve(result)
elseif cb then
cb(result)
end
return result
end
local function reject(err)
if promise then
promise:reject(err)
end
error(err)
end
CreateThread(function()
if not vehicleType then
local xPlayer = ESX.GetExtendedPlayers()[1]
if xPlayer then
vehicleType = ESX.GetVehicleType(vehicleModel, xPlayer.source)
end
end
if not vehicleType then
return reject("No players found nearby to check vehicle type! Alternatively, you can specify the vehicle type manually.")
end
local createdVehicle = CreateVehicleServerSetter(vehicleModel, vehicleType, coords.x, coords.y, coords.z, heading)
local tries = 0
local hasNetOwner = next(ESX.OneSync.GetClosestPlayer(coords, 300, nil, 0) or {}) ~= nil
while not createdVehicle or createdVehicle == 0
or (hasNetOwner and NetworkGetEntityOwner(createdVehicle) == -1)
or (not hasNetOwner and not DoesEntityExist(createdVehicle)) do
Wait(200)
tries = tries + 1
if tries > 40 then
return reject(("Could not spawn vehicle - ^5%s^7!"):format(vehicleModel))
end
end
-- luacheck: ignore
SetEntityOrphanMode(createdVehicle, 2)
local networkId = NetworkGetNetworkIdFromEntity(createdVehicle)
Entity(createdVehicle).state:set("VehicleProperties", vehicleProperties, true)
resolve(networkId)
end)
if promise then
return Citizen.Await(promise)
end
end
---@param model number|string
---@param coords vector3|table
---@param heading number
---@param cb? fun(netId: number)
---@return number? netId
function ESX.OneSync.SpawnObject(model, coords, heading, cb)
if type(model) == "string" then
model = joaat(model)
end
local promise = not cb and promise.new()
local objectCoords = type(coords) == "vector3" and coords or vector3(coords.x, coords.y, coords.z)
local function resolve(result)
if promise then
promise:resolve(result)
elseif cb then
cb(result)
end
end
local function reject(err)
if promise then
promise:reject(err)
end
error(err)
end
CreateThread(function()
local entity = CreateObject(model, objectCoords.x, objectCoords.y, objectCoords.z, true, true, false)
local tries = 0
while not DoesEntityExist(entity) do
Wait(200)
tries = tries + 1
if tries > 40 then
return reject(("Could not spawn object - ^5%s^7!"):format(entity))
end
end
local networkId = NetworkGetNetworkIdFromEntity(entity)
SetEntityHeading(entity, heading)
resolve(networkId)
end)
if promise then
return Citizen.Await(promise)
end
end
---@param model number|string
---@param coords vector3|table
---@param heading number
---@param cb? fun(netId: number)
---@return number? netId
function ESX.OneSync.SpawnPed(model, coords, heading, cb)
if type(model) == "string" then
model = joaat(model)
end
local promise = not cb and promise.new()
local function resolve(result)
if promise then
promise:resolve(result)
elseif cb then
cb(result)
end
end
local function reject(err)
if promise then
promise:reject(err)
end
error(err)
end
CreateThread(function()
local entity = CreatePed(0, model, coords.x, coords.y, coords.z, heading, true, true)
local tries = 0
while not DoesEntityExist(entity) do
Wait(200)
tries = tries + 1
if tries > 40 then
return reject(("Could not spawn ped - ^5%s^7!"):format(model))
end
end
local networkId = NetworkGetNetworkIdFromEntity(entity)
resolve(networkId)
end)
if promise then
return Citizen.Await(promise)
end
end
---@param model number|string
---@param vehicle number entityId
---@param seat number
---@param cb? fun(netId: number)
---@return number? netId
function ESX.OneSync.SpawnPedInVehicle(model, vehicle, seat, cb)
if type(model) == "string" then
model = joaat(model)
end
local promise = not cb and promise.new()
local function resolve(result)
if promise then
promise:resolve(result)
elseif cb then
cb(result)
end
end
local function reject(err)
if promise then
promise:reject(err)
end
error(err)
end
CreateThread(function()
local entity = CreatePedInsideVehicle(vehicle, 1, model, seat, true, true)
local tries = 0
while not DoesEntityExist(entity) do
Wait(200)
tries = tries + 1
if tries > 40 then
reject(("Could not spawn ped - ^5%s^7!"):format(model))
end
end
local networkId = NetworkGetNetworkIdFromEntity(entity)
resolve(networkId)
end)
if promise then
return Citizen.Await(promise)
end
end
local function getNearbyEntities(entities, coords, modelFilter, maxDistance, isPed)
local nearbyEntities = {}
coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z)
for _, entity in pairs(entities) do
if not isPed or (isPed and not IsPedAPlayer(entity)) then
if not modelFilter or modelFilter[GetEntityModel(entity)] then
local entityCoords = GetEntityCoords(entity)
if not maxDistance or #(coords - entityCoords) <= maxDistance then
nearbyEntities[#nearbyEntities + 1] = NetworkGetNetworkIdFromEntity(entity)
end
end
end
end
return nearbyEntities
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return table
function ESX.OneSync.GetPedsInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllPeds(), coords, modelFilter, maxDistance, true)
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return table
function ESX.OneSync.GetObjectsInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllObjects(), coords, modelFilter, maxDistance)
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table | nil models to ignore, where the key is the model hash and the value is true
---@return table
function ESX.OneSync.GetVehiclesInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllVehicles(), coords, modelFilter, maxDistance)
end
local function getClosestEntity(entities, coords, modelFilter, isPed)
local distance, closestEntity, closestCoords = 100, 0, vector3(0, 0, 0)
coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z)
for _, entity in pairs(entities) do
if not isPed or (isPed and not IsPedAPlayer(entity)) then
if not modelFilter or modelFilter[GetEntityModel(entity)] then
local entityCoords = GetEntityCoords(entity)
local dist = #(coords - entityCoords)
if dist < distance then
closestEntity, distance, closestCoords = entity, dist, entityCoords
end
end
end
end
return NetworkGetNetworkIdFromEntity(closestEntity), distance, closestCoords
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function ESX.OneSync.GetClosestPed(coords, modelFilter)
return getClosestEntity(GetAllPeds(), coords, modelFilter, true)
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function ESX.OneSync.GetClosestObject(coords, modelFilter)
return getClosestEntity(GetAllObjects(), coords, modelFilter)
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function ESX.OneSync.GetClosestVehicle(coords, modelFilter)
return getClosestEntity(GetAllVehicles(), coords, modelFilter)
end

View File

@@ -0,0 +1,71 @@
function StartPayCheck()
CreateThread(function()
while true do
Wait(Config.PaycheckInterval)
for player, xPlayer in pairs(ESX.Players) do
local jobLabel = xPlayer.job.label
local job = xPlayer.job.grade_name
local onDuty = xPlayer.job.onDuty
local salary = (job == "unemployed" or onDuty) and xPlayer.job.grade_salary or ESX.Math.Round(xPlayer.job.grade_salary * Config.OffDutyPaycheckMultiplier)
if xPlayer.paycheckEnabled then
if salary > 0 then
if job == "unemployed" then -- unemployed
xPlayer.addAccountMoney("bank", salary, "Welfare Check")
TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_help", salary), "CHAR_BANK_MAZE", 9)
if Config.LogPaycheck then
ESX.DiscordLogFields("Paycheck", "Paycheck - Unemployment Benefits", "green", {
{ name = "Player", value = xPlayer.name, inline = true },
{ name = "ID", value = xPlayer.source, inline = true },
{ name = "Amount", value = salary, inline = true },
})
end
elseif Config.EnableSocietyPayouts then -- possibly a society
TriggerEvent("esx_society:getSociety", xPlayer.job.name, function(society)
if society ~= nil then -- verified society
TriggerEvent("esx_addonaccount:getSharedAccount", society.account, function(account)
if account.money >= salary then -- does the society money to pay its employees?
xPlayer.addAccountMoney("bank", salary, "Paycheck")
account.removeMoney(salary)
if Config.LogPaycheck then
ESX.DiscordLogFields("Paycheck", "Paycheck - " .. jobLabel, "green", {
{ name = "Player", value = xPlayer.name, inline = true },
{ name = "ID", value = xPlayer.source, inline = true },
{ name = "Amount", value = salary, inline = true },
})
end
TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9)
else
TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), "", TranslateCap("company_nomoney"), "CHAR_BANK_MAZE", 1)
end
end)
else -- not a society
xPlayer.addAccountMoney("bank", salary, "Paycheck")
if Config.LogPaycheck then
ESX.DiscordLogFields("Paycheck", "Paycheck - " .. jobLabel, "green", {
{ name = "Player", value = xPlayer.name, inline = true },
{ name = "ID", value = xPlayer.source, inline = true },
{ name = "Amount", value = salary, inline = true },
})
end
TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9)
end
end)
else -- generic job
xPlayer.addAccountMoney("bank", salary, "Paycheck")
if Config.LogPaycheck then
ESX.DiscordLogFields("Paycheck", "Paycheck - Generic", "green", {
{ name = "Player", value = xPlayer.name, inline = true },
{ name = "ID", value = xPlayer.source, inline = true },
{ name = "Amount", value = salary, inline = true },
})
end
TriggerClientEvent("esx:showAdvancedNotification", player, TranslateCap("bank"), TranslateCap("received_paycheck"), TranslateCap("received_salary", salary), "CHAR_BANK_MAZE", 9)
end
end
end
end
end
end)
end