diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..0f8d6d4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+* text=auto eol=lf
+
+# Lua and web files - always treat as text
+*.lua text eol=lf
+*.js text eol=lf
+*.html text eol=lf
+*.css text eol=lf
+*.json text eol=lf
+*.xml text eol=lf
+*.yml text eol=lf
+*.md text eol=lf
diff --git a/client/drawtext.lua b/client/drawtext.lua
index c54d401..10480ab 100644
--- a/client/drawtext.lua
+++ b/client/drawtext.lua
@@ -29,7 +29,7 @@ local function changeText(text, position)
end
local function keyPressed()
- CreateThread(function() -- Not sure if a thread is needed but why not eh?
+ CreateThread(function()
SendNUIMessage({
action = 'KEY_PRESSED',
})
diff --git a/client/events.lua b/client/events.lua
index c065d7c..fa6db16 100644
--- a/client/events.lua
+++ b/client/events.lua
@@ -1,6 +1,3 @@
--- Player load and unload handling
--- New method for checking if logged in across all scripts (optional)
--- if LocalPlayer.state['isLoggedIn'] then
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
ShutdownLoadingScreenNui()
LocalPlayer.state:set('isLoggedIn', true, false)
@@ -17,6 +14,7 @@ RegisterNetEvent('QBCore:Client:PvpHasToggled', function(pvp_state)
SetCanAttackFriendly(PlayerPedId(), pvp_state, false)
NetworkSetFriendlyFireOption(pvp_state)
end)
+
-- Teleport Commands
RegisterNetEvent('QBCore:Command:TeleportToPlayer', function(coords)
@@ -182,30 +180,23 @@ end)
-- Other stuff
-RegisterNetEvent('QBCore:Player:SetPlayerData', function(val)
- QBCore.PlayerData = val
-end)
-
-RegisterNetEvent('QBCore:Player:UpdatePlayerDataField', function(key, val)
- if QBCore.PlayerData and key then
+RegisterNetEvent('QBCore:Client:OnPlayerUpdated', function(key, val)
+ if key == 'all' then
+ QBCore.PlayerData = val
+ TriggerEvent('QBCore:Player:SetPlayerData', val)
+ TriggerEvent('QBCore:Client:OnJobUpdate', val.job)
+ TriggerEvent('QBCore:Client:OnGangUpdate', val.gang)
+ elseif QBCore.PlayerData and key then
QBCore.PlayerData[key] = val
+ if key == 'job' then TriggerEvent('QBCore:Client:OnJobUpdate', val) end
+ if key == 'gang' then TriggerEvent('QBCore:Client:OnGangUpdate', val) end
end
end)
-RegisterNetEvent('QBCore:Player:UpdatePlayerData', function()
- TriggerServerEvent('QBCore:UpdatePlayer')
-end)
-
RegisterNetEvent('QBCore:Notify', function(text, type, length, icon)
QBCore.Functions.Notify(text, type, length, icon)
end)
--- This event is exploitable and should not be used. It has been deprecated, and will be removed soon.
-RegisterNetEvent('QBCore:Client:UseItem', function(item)
- QBCore.Debug(string.format('%s triggered QBCore:Client:UseItem by ID %s with the following data. This event is deprecated due to exploitation, and will be removed soon. Check qb-inventory for the right use on this event.', GetInvokingResource(), GetPlayerServerId(PlayerId())))
- QBCore.Debug(item)
-end)
-
RegisterNUICallback('getNotifyConfig', function(_, cb)
cb(QBCore.Config.Notify)
end)
diff --git a/client/functions.lua b/client/functions.lua
index a14d53a..75d2db9 100644
--- a/client/functions.lua
+++ b/client/functions.lua
@@ -1,3 +1,4 @@
+QBCore.PlayerData = {}
QBCore.Functions = {}
-- Callbacks
diff --git a/client/main.lua b/client/main.lua
deleted file mode 100644
index 05d3b68..0000000
--- a/client/main.lua
+++ /dev/null
@@ -1,50 +0,0 @@
-QBCore = {}
-QBCore.PlayerData = {}
-QBCore.Config = QBConfig
-QBCore.Shared = QBShared
-QBCore.ClientCallbacks = {}
-QBCore.ServerCallbacks = {}
-
--- Get the full QBCore object (default behavior):
--- local QBCore = GetCoreObject()
-
--- Get only specific parts of QBCore:
--- local QBCore = GetCoreObject({'Players', 'Config'})
-
-local function GetCoreObject(filters)
- if not filters then return QBCore end
- local results = {}
- for i = 1, #filters do
- local key = filters[i]
- if QBCore[key] then
- results[key] = QBCore[key]
- end
- end
- return results
-end
-exports('GetCoreObject', GetCoreObject)
-
-local function GetSharedItems()
- return QBShared.Items
-end
-exports('GetSharedItems', GetSharedItems)
-
-local function GetSharedVehicles()
- return QBShared.Vehicles
-end
-exports('GetSharedVehicles', GetSharedVehicles)
-
-local function GetSharedWeapons()
- return QBShared.Weapons
-end
-exports('GetSharedWeapons', GetSharedWeapons)
-
-local function GetSharedJobs()
- return QBShared.Jobs
-end
-exports('GetSharedJobs', GetSharedJobs)
-
-local function GetSharedGangs()
- return QBShared.Gangs
-end
-exports('GetSharedGangs', GetSharedGangs)
diff --git a/config.lua b/config.lua
index 3c77309..9df1e8c 100644
--- a/config.lua
+++ b/config.lua
@@ -1,30 +1,31 @@
-QBConfig = {}
+QBCore = {}
+QBCore.Config = {}
-QBConfig.MaxPlayers = GetConvarInt('sv_maxclients', 48) -- Gets max players from config file, default 48
-QBConfig.DefaultSpawn = vector4(-1035.71, -2731.87, 12.86, 0.0)
-QBConfig.UpdateInterval = 5 -- how often to update player data in minutes
-QBConfig.StatusInterval = 5000 -- how often to check hunger/thirst status in milliseconds
+QBCore.Config.MaxPlayers = GetConvarInt('sv_maxclients', 48) -- Gets max players from config file, default 48
+QBCore.Config.DefaultSpawn = vector4(-1035.71, -2731.87, 12.86, 0.0)
+QBCore.Config.UpdateInterval = 5 -- how often to update player data in minutes
+QBCore.Config.StatusInterval = 5000 -- how often to check hunger/thirst status in milliseconds
-QBConfig.Money = {}
-QBConfig.Money.MoneyTypes = { cash = 500, bank = 5000, crypto = 0 } -- type = startamount - Add or remove money types for your server (for ex. blackmoney = 0), remember once added it will not be removed from the database!
-QBConfig.Money.DontAllowMinus = { 'cash', 'crypto' } -- Money that is not allowed going in minus
-QBConfig.Money.MinusLimit = -5000 -- The maximum amount you can be negative
-QBConfig.Money.PayCheckTimeOut = 10 -- The time in minutes that it will give the paycheck
-QBConfig.Money.PayCheckSociety = false -- If true paycheck will come from the society account that the player is employed at, requires qb-management
+QBCore.Config.Money = {}
+QBCore.Config.Money.MoneyTypes = { cash = 500, bank = 5000, crypto = 0 } -- type = startamount - Add or remove money types for your server (for ex. blackmoney = 0), remember once added it will not be removed from the database!
+QBCore.Config.Money.DontAllowMinus = { 'cash', 'crypto' } -- Money that is not allowed going in minus
+QBCore.Config.Money.MinusLimit = -5000 -- The maximum amount you can be negative
+QBCore.Config.Money.PayCheckTimeOut = 10 -- The time in minutes that it will give the paycheck
+QBCore.Config.Money.PayCheckSociety = false -- If true paycheck will come from the society account that the player is employed at, requires qb-management
-QBConfig.Player = {}
-QBConfig.Player.HungerRate = 4.2 -- Rate at which hunger goes down.
-QBConfig.Player.ThirstRate = 3.8 -- Rate at which thirst goes down.
-QBConfig.Player.Bloodtypes = {
+QBCore.Config.Player = {}
+QBCore.Config.Player.HungerRate = 4.2 -- Rate at which hunger goes down.
+QBCore.Config.Player.ThirstRate = 3.8 -- Rate at which thirst goes down.
+QBCore.Config.Player.Bloodtypes = {
'A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-',
}
-QBConfig.Player.PlayerDefaults = {
+QBCore.Config.Player.PlayerDefaults = {
citizenid = function() return QBCore.Player.CreateCitizenId() end,
cid = 1,
money = function()
local moneyDefaults = {}
- for moneytype, startamount in pairs(QBConfig.Money.MoneyTypes) do
+ for moneytype, startamount in pairs(QBCore.Config.Money.MoneyTypes) do
moneyDefaults[moneytype] = startamount
end
return moneyDefaults
@@ -76,7 +77,7 @@ QBConfig.Player.PlayerDefaults = {
rep = {},
currentapartment = nil,
callsign = 'NO CALLSIGN',
- bloodtype = function() return QBConfig.Player.Bloodtypes[math.random(1, #QBConfig.Player.Bloodtypes)] end,
+ bloodtype = function() return QBCore.Config.Player.Bloodtypes[math.random(1, #QBCore.Config.Player.Bloodtypes)] end,
fingerprint = function() return QBCore.Player.CreateFingerId() end,
walletid = function() return QBCore.Player.CreateWalletId() end,
criminalrecord = {
@@ -100,27 +101,27 @@ QBConfig.Player.PlayerDefaults = {
InstalledApps = {}
}
},
- position = QBConfig.DefaultSpawn,
+ position = QBCore.Config.DefaultSpawn,
items = {},
}
-QBConfig.Server = {} -- General server config
-QBConfig.Server.Closed = false -- Set server closed (no one can join except people with ace permission 'qbadmin.join')
-QBConfig.Server.ClosedReason = 'Server Closed' -- Reason message to display when people can't join the server
-QBConfig.Server.Uptime = 0 -- Time the server has been up.
-QBConfig.Server.Whitelist = false -- Enable or disable whitelist on the server
-QBConfig.Server.WhitelistPermission = 'admin' -- Permission that's able to enter the server when the whitelist is on
-QBConfig.Server.PVP = true -- Enable or disable pvp on the server (Ability to shoot other players)
-QBConfig.Server.Discord = '' -- Discord invite link
-QBConfig.Server.CheckDuplicateLicense = true -- Check for duplicate rockstar license on join
-QBConfig.Server.Permissions = { 'god', 'admin', 'mod' } -- Add as many groups as you want here after creating them in your server.cfg
+QBCore.Config.Server = {} -- General server config
+QBCore.Config.Server.Closed = false -- Set server closed (no one can join except people with ace permission 'qbadmin.join')
+QBCore.Config.Server.ClosedReason = 'Server Closed' -- Reason message to display when people can't join the server
+QBCore.Config.Server.Uptime = 0 -- Time the server has been up.
+QBCore.Config.Server.Whitelist = false -- Enable or disable whitelist on the server
+QBCore.Config.Server.WhitelistPermission = 'admin' -- Permission that's able to enter the server when the whitelist is on
+QBCore.Config.Server.PVP = true -- Enable or disable pvp on the server (Ability to shoot other players)
+QBCore.Config.Server.Discord = '' -- Discord invite link
+QBCore.Config.Server.CheckDuplicateLicense = true -- Check for duplicate rockstar license on join
+QBCore.Config.Server.Permissions = { 'god', 'admin', 'mod' } -- Add as many groups as you want here after creating them in your server.cfg
-QBConfig.Commands = {} -- Command Configuration
-QBConfig.Commands.OOCColor = { 255, 151, 133 } -- RGB color code for the OOC command
+QBCore.Config.Commands = {} -- Command Configuration
+QBCore.Config.Commands.OOCColor = { 255, 151, 133 } -- RGB color code for the OOC command
-QBConfig.Notify = {}
+QBCore.Config.Notify = {}
-QBConfig.Notify.NotificationStyling = {
+QBCore.Config.Notify.NotificationStyling = {
group = false, -- Allow notifications to stack with a badge instead of repeating
position = 'right', -- top-left | top-right | bottom-left | bottom-right | top | bottom | left | right | center
progress = true -- Display Progress Bar
@@ -129,7 +130,7 @@ QBConfig.Notify.NotificationStyling = {
-- These are how you define different notification variants
-- The "color" key is background of the notification
-- The "icon" key is the css-icon code, this project uses `Material Icons` & `Font Awesome`
-QBConfig.Notify.VariantDefinitions = {
+QBCore.Config.Notify.VariantDefinitions = {
success = {
classes = 'success',
icon = 'check_circle'
diff --git a/fxmanifest.lua b/fxmanifest.lua
index 924a561..a627f07 100644
--- a/fxmanifest.lua
+++ b/fxmanifest.lua
@@ -11,6 +11,7 @@ shared_scripts {
'locale/en.lua',
'locale/*.lua',
'shared/main.lua',
+ 'shared/functions.lua',
'shared/items.lua',
'shared/jobs.lua',
'shared/vehicles.lua',
@@ -20,7 +21,6 @@ shared_scripts {
}
client_scripts {
- 'client/main.lua',
'client/functions.lua',
'client/loops.lua',
'client/events.lua',
@@ -29,7 +29,6 @@ client_scripts {
server_scripts {
'@oxmysql/lib/MySQL.lua',
- 'server/main.lua',
'server/functions.lua',
'server/player.lua',
'server/events.lua',
diff --git a/html/css/drawtext.css b/html/css/drawtext.css
index f6c920b..8a5b459 100644
--- a/html/css/drawtext.css
+++ b/html/css/drawtext.css
@@ -1,35 +1,8 @@
:root {
- /* Typography */
- --font-primary: "Exo 2", sans-serif;
- --font-weight-regular: 400;
- --font-weight-medium: 500;
- --font-weight-bold: 700;
- --font-weight-light: 300;
- /* Colors */
- --md-primary: #1c75d2;
- --md-primary-container: #6facec;
- --md-error: #c10114;
- --md-error-container: #fe4255;
- --md-success: #20bb44;
- --md-success-container: #6ae587;
- --md-warning: #ff9800;
- --md-warning-container: #ffc107;
- --md-info: #2197f2;
- --md-info-container: #7ac1f7;
- --md-surface: #fffbff;
- --md-on-surface: #1c1b1f;
-
- /* Custom Variables */
- --primary-bg: rgba(23, 23, 23, 90%);
- --active-bg: var(--md-error);
- --font-color: white;
-
- /* Elevation */
+ --primary-bg: #211f26;
+ --active-bg: #c10114;
+ --font-color: #e6e1e5;
--md-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
-
- /* Border Radius */
- --md-radius-small: 5px;
- --md-radius-medium: 10px;
--md-radius-extra-small: 0.15rem;
}
diff --git a/html/css/style.css b/html/css/style.css
index 62ae7d2..9bda9f4 100644
--- a/html/css/style.css
+++ b/html/css/style.css
@@ -1,49 +1,8 @@
:root {
- /* Typography */
--font-primary: "Exo 2", sans-serif;
--font-weight-regular: 400;
--font-weight-medium: 500;
- --font-weight-bold: 700;
- --font-weight-light: 300;
-
- /* Colors */
- --md-primary: #0061a4;
- --md-on-primary: #ffffff;
- --md-primary-container: #d1e4ff;
- --md-on-primary-container: #001d36;
-
- --md-error: #ba1a1a;
- --md-on-error: #ffffff;
- --md-error-container: #ffdad6;
- --md-on-error-container: #410002;
-
- --md-success: #006e1c;
- --md-on-success: #ffffff;
- --md-success-container: #9df996;
- --md-on-success-container: #002106;
-
- --md-warning: #7d5800;
- --md-on-warning: #ffffff;
- --md-warning-container: #ffdf93;
- --md-on-warning-container: #271900;
-
- --md-info: #0062a1;
- --md-on-info: #ffffff;
- --md-info-container: #d0e4ff;
- --md-on-info-container: #001d35;
-
- /* Surface colors */
- --md-surface: #fcfcff;
- --md-on-surface: #1a1c1e;
- --md-surface-variant: #dfe2eb;
- --md-on-surface-variant: #43474e;
-
- /* Elevation */
--md-elevation-1: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);
-
- /* Border Radius */
- --md-radius-small: 4px;
- --md-radius-medium: 8px;
}
* {
@@ -55,94 +14,111 @@ html::-webkit-scrollbar {
display: none;
}
-@media (width: 3840px) and (height: 2160px) {
- .success {
- background-color: rgba(23, 23, 23, 90%);
- color: var(--md-on-surface);
- box-shadow: var(--md-elevation-1);
- border-left: 0.5rem solid var(--md-success);
- font-size: 1.5vh;
- }
-
- .primary {
- background-color: rgba(23, 23, 23, 90%);
- color: var(--md-on-surface);
- box-shadow: var(--md-elevation-1);
- border-left: 5px solid var(--md-primary);
- font-size: 1.5vh;
- }
-
- .error {
- background-color: rgba(23, 23, 23, 90%);
- color: var(--md-on-surface);
- box-shadow: var(--md-elevation-1);
- border-left: 5px solid var(--md-error);
- font-size: 1.5vh;
- }
-
- .warning {
- background-color: rgba(23, 23, 23, 90%);
- color: var(--md-on-surface);
- box-shadow: var(--md-elevation-1);
- border-left: 5px solid var(--md-warning);
- font-size: 1.5vh;
- }
-
- .police {
- background-color: rgba(23, 23, 23, 90%);
- color: var(--md-on-surface);
- box-shadow: var(--md-elevation-1);
- border-left: 5px solid var(--md-info);
- font-size: 1.5vh;
- }
-
- .ambulance {
- background-color: rgba(23, 23, 23, 90%);
- color: var(--md-on-surface);
- box-shadow: var(--md-elevation-1);
- border-left: 5px solid var(--md-error);
- font-size: 1.5vh;
- }
-}
-
.success {
- background-color: var(--md-success-container);
- color: var(--md-on-success-container);
- padding: 12px 16px;
- font-weight: var(--font-weight-medium);
+ --accent: #006e1c;
}
-
.primary {
- background-color: var(--md-primary-container);
- color: var(--md-on-primary-container);
- padding: 12px 16px;
- font-weight: var(--font-weight-medium);
+ --accent: #0061a4;
}
-
.warning {
- background-color: var(--md-warning-container);
- color: var(--md-on-warning-container);
- padding: 12px 16px;
- font-weight: var(--font-weight-medium);
+ --accent: #7d5800;
}
-
.error {
- background-color: var(--md-error-container);
- color: var(--md-on-error-container);
- padding: 12px 16px;
- font-weight: var(--font-weight-medium);
+ --accent: #ba1a1a;
}
-
.police {
- background-color: var(--md-info-container);
- color: var(--md-on-info-container);
- padding: 12px 16px;
- font-weight: var(--font-weight-medium);
+ --accent: #0062a1;
+}
+.ambulance {
+ --accent: #006874;
}
+.success,
+.primary,
+.warning,
+.error,
+.police,
.ambulance {
- background-color: var(--md-error-container);
- color: var(--md-on-error-container);
+ background-color: #211f26;
+ color: #e6e1e5;
padding: 12px 16px;
font-weight: var(--font-weight-medium);
+ box-shadow: var(--md-elevation-1);
+}
+
+.notify-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ border-radius: 0;
+ min-width: 280px;
+ max-width: 400px;
+ opacity: 0;
+ transform: translateX(16px);
+ transition:
+ opacity 0.2s ease,
+ transform 0.2s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.notify-item.notify-show {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.notify-item.notify-hide {
+ opacity: 0;
+ transform: translateX(16px);
+ transition:
+ opacity 0.3s ease,
+ transform 0.3s ease;
+}
+
+.notify-icon {
+ color: var(--accent);
+ flex-shrink: 0;
+ font-size: 20px;
+ margin-top: 1px;
+}
+
+.notify-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.notify-message {
+ font-size: clamp(12px, 1.1vw, 16px);
+ line-height: 1.4;
+}
+
+.notify-multiline {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.notify-caption {
+ font-size: clamp(10px, 0.9vw, 14px);
+ margin-top: 2px;
+ opacity: 0.75;
+}
+
+.notify-progress {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ height: 3px;
+ width: 100%;
+ background: var(--accent);
+ opacity: 0.35;
+ animation: notify-shrink linear forwards;
+}
+
+@keyframes notify-shrink {
+ from {
+ width: 100%;
+ }
+ to {
+ width: 0%;
+ }
}
diff --git a/html/index.html b/html/index.html
index 9d5286f..0630409 100644
--- a/html/index.html
+++ b/html/index.html
@@ -1,18 +1,18 @@
+
+
+
+ qb-core
-
+
-
-
-
-
-
-
+
+
+
-
diff --git a/html/js/app.js b/html/js/app.js
index 007f725..2a748e9 100644
--- a/html/js/app.js
+++ b/html/js/app.js
@@ -1,65 +1,120 @@
-import { determineStyleFromVariant, fetchNotifyConfig, NOTIFY_CONFIG } from "./config.js";
+let NOTIFY_CONFIG = null;
-const { useQuasar } = Quasar;
-const { onMounted, onUnmounted } = Vue;
+const defaultConfig = {
+ NotificationStyling: {
+ group: false,
+ position: "right",
+ progress: true,
+ },
+ VariantDefinitions: {
+ success: { classes: "success", icon: "check_circle" },
+ primary: { classes: "primary", icon: "notifications" },
+ warning: { classes: "warning", icon: "warning" },
+ error: { classes: "error", icon: "error" },
+ police: { classes: "police", icon: "local_police" },
+ ambulance: { classes: "ambulance", icon: "fas fa-ambulance" },
+ },
+};
const fetchNui = async (evName, data) => {
const resourceName = window.GetParentResourceName();
-
- const rawResp = await fetch(`https://${resourceName}/${evName}`, {
+ const resp = await fetch(`https://${resourceName}/${evName}`, {
body: JSON.stringify(data),
- headers: {
- "Content-Type": "application/json; charset=UTF8",
- },
+ headers: { "Content-Type": "application/json; charset=UTF8" },
method: "POST",
});
-
- return await rawResp.json();
+ return resp.json();
};
-window.fetchNui = fetchNui;
+const determineStyleFromVariant = (variant) => {
+ return NOTIFY_CONFIG.VariantDefinitions[variant] ?? NOTIFY_CONFIG.VariantDefinitions["primary"];
+};
-const app = Vue.createApp({
- setup() {
- const $q = useQuasar();
+const fetchNotifyConfig = async () => {
+ try {
+ NOTIFY_CONFIG = await fetchNui("getNotifyConfig", {});
+ if (!NOTIFY_CONFIG) NOTIFY_CONFIG = defaultConfig;
+ } catch (error) {
+ console.error("Failed to fetch notification config, using default", error);
+ NOTIFY_CONFIG = defaultConfig;
+ }
+};
- const showNotif = async ({ data }) => {
- if (data?.action !== "notify") return;
+const POSITION_MAP = {
+ "top-left": { top: "16px", left: "16px" },
+ "top-right": { top: "16px", right: "16px" },
+ top: { top: "16px", left: "50%", transform: "translateX(-50%)" },
+ "bottom-left": { bottom: "16px", left: "16px" },
+ "bottom-right": { bottom: "16px", right: "16px" },
+ bottom: { bottom: "16px", left: "50%", transform: "translateX(-50%)" },
+ left: { top: "50%", left: "16px", transform: "translateY(-50%)" },
+ right: { top: "50%", right: "16px", transform: "translateY(-50%)" },
+ center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
+};
- const { text, length, type, caption, icon: dataIcon } = data;
- let { classes, icon } = determineStyleFromVariant(type);
+let container = null;
- if (dataIcon) {
- icon = dataIcon;
- }
+const getContainer = () => {
+ if (container) return container;
+ const pos = NOTIFY_CONFIG.NotificationStyling.position ?? "right";
+ const isBottom = pos.startsWith("bottom");
+ container = document.createElement("div");
+ container.id = "notify-container";
+ Object.assign(container.style, {
+ position: "fixed",
+ display: "flex",
+ flexDirection: isBottom ? "column-reverse" : "column",
+ gap: "8px",
+ zIndex: "9999",
+ maxWidth: "400px",
+ pointerEvents: "none",
+ ...(POSITION_MAP[pos] ?? POSITION_MAP["right"]),
+ });
+ document.body.appendChild(container);
+ return container;
+};
- if (!NOTIFY_CONFIG) {
- console.error("The notification config did not load properly, trying again for next time");
- await fetchNotifyConfig();
- if (NOTIFY_CONFIG) return showNotif({ data });
- }
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
- $q.notify({
- message: text,
- multiLine: text.length > 100,
- group: NOTIFY_CONFIG.NotificationStyling.group ?? false,
- progress: NOTIFY_CONFIG.NotificationStyling.progress ?? true,
- position: NOTIFY_CONFIG.NotificationStyling.position ?? "right",
- timeout: length,
- caption,
- classes,
- icon,
- });
- };
- onMounted(() => {
- window.addEventListener("message", showNotif);
- });
- onUnmounted(() => {
- window.removeEventListener("message", showNotif);
- });
- return {};
- },
-});
+const showNotif = async ({ data }) => {
+ if (data?.action !== "notify") return;
-app.use(Quasar, { config: {} });
-app.mount("#q-app");
+ if (!NOTIFY_CONFIG) {
+ await fetchNotifyConfig();
+ }
+
+ const { text: message, type, length: duration = 5000, caption } = data;
+ const style = determineStyleFromVariant(type ?? "primary");
+ const showProgress = NOTIFY_CONFIG.NotificationStyling.progress && duration > 0;
+
+ const isFa = style.icon.startsWith("fa");
+ const iconHtml = isFa ? `` : `${style.icon}`;
+
+ const item = document.createElement("div");
+ item.className = `notify-item ${style.classes}`;
+ item.innerHTML = `
+ ${iconHtml}
+
+
${message}
+ ${caption ? `
${caption}
` : ""}
+
+ ${showProgress ? `` : ""}
+ `;
+
+ const c = getContainer();
+ c.appendChild(item);
+
+ await sleep(10);
+ item.classList.add("notify-show");
+
+ if (duration > 0) {
+ setTimeout(() => {
+ item.classList.remove("notify-show");
+ item.classList.add("notify-hide");
+ setTimeout(() => item.remove(), 350);
+ }, duration);
+ }
+};
+
+window.addEventListener("message", showNotif);
+window.addEventListener("load", fetchNotifyConfig);
diff --git a/html/js/config.js b/html/js/config.js
deleted file mode 100644
index 5ff6925..0000000
--- a/html/js/config.js
+++ /dev/null
@@ -1,53 +0,0 @@
-export let NOTIFY_CONFIG = null;
-
-const defaultConfig = {
- NotificationStyling: {
- group: true,
- position: "top-right",
- progress: true,
- },
- VariantDefinitions: {
- success: {
- classes: "success",
- icon: "done",
- },
- primary: {
- classes: "primary",
- icon: "info",
- },
- error: {
- classes: "error",
- icon: "dangerous",
- },
- police: {
- classes: "police",
- icon: "local_police",
- },
- ambulance: {
- classes: "ambulance",
- icon: "fas fa-ambulance",
- },
- },
-};
-
-export const determineStyleFromVariant = (variant) => {
- const variantData = NOTIFY_CONFIG.VariantDefinitions[variant];
- if (!variantData) throw new Error(`Style of type: ${variant}, does not exist in the config`);
- return variantData;
-};
-
-export const fetchNotifyConfig = async () => {
- try {
- NOTIFY_CONFIG = await window.fetchNui("getNotifyConfig", {});
- if (!NOTIFY_CONFIG) {
- NOTIFY_CONFIG = defaultConfig;
- }
- } catch (error) {
- console.error("Failed to fetch notification config, using default", error);
- NOTIFY_CONFIG = defaultConfig;
- }
-};
-
-window.addEventListener("load", async () => {
- await fetchNotifyConfig();
-});
diff --git a/locale/th.lua b/locale/th.lua
index 43bca61..3005b3b 100644
--- a/locale/th.lua
+++ b/locale/th.lua
@@ -1,129 +1,129 @@
-local Translations = {
- error = {
- not_online = 'ผู้เล่นไม่ออนไลน์',
- wrong_format = 'รูปแบบไม่ถูกต้อง',
- missing_args = 'ตัวแปรที่ต้องการไม่ถูกกำหนด (x, y, z)',
- missing_args2 = 'ต้องกำหนดตัวแปรทุกตัวให้ครบ!',
- no_access = 'ไม่สามารถเข้าถึงคำสั่งนี้ได้',
- company_too_poor = 'บริษัทของคุณกำลังจะล้มละลาย',
- item_not_exist = 'ไม่มีไอเทมนี้',
- too_heavy = 'กระเป๋าเต็มแล้ว',
- location_not_exist = 'ไม่มีตำแหน่งนี้',
- duplicate_license = 'พบใบอนุญาต Rockstar ซ้ำ',
- no_valid_license = 'ไม่พบใบอนุญาต Rockstar ที่ถูกต้อง',
- not_whitelisted = 'คุณไม่ได้รับอนุญาตให้เข้าใช้เซิร์ฟเวอร์นี้',
- server_already_open = 'เซิร์ฟเวอร์ถูกเปิดแล้ว',
- server_already_closed = 'เซิร์ฟเวอร์ถูกปิดแล้ว',
- no_permission = 'คุณไม่ได้รับสิทธิ์ในการใช้คำสั่งนี้..',
- no_waypoint = 'ไม่มีจุดเส้นทางที่ตั้ง',
- tp_error = 'ข้อผิดพลาดขณะวาร์ป',
- connecting_database_error = 'เกิดข้อผิดพลาดในการเชื่อมต่อกับเซิร์ฟเวอร์ (SQL server เปิดอยู่ไหม?)',
- connecting_database_timeout = 'เชื่อมต่อกับฐานข้อมูลเกิด timeout (SQL server เปิดอยู่ไหม?'
- },
- success = {
- server_opened = 'เซิฟเวอร์เปิดแล้ว',
- server_closed = 'เซิฟเวอร์เปิดแล้ว',
- teleported_waypoint = 'เคลือนย้ายไปยังจุดหมาย.',
- },
- info = {
- received_paycheck = 'คุณได้รับเงินเดือน $%{value}',
- job_info = 'อาชีพ: %{value} | ระดับ: %{value2} | กำลังปฏิบัติหน้าที่: %{value3}',
- gang_info = 'แก๊ง: %{value} | ระดับ: %{value2}',
- on_duty = 'คุณเริ่มปฏิบัติหน้าที่แล้ว!',
- off_duty = 'คุณออกจากการปฏิบัติหน้าที่แล้ว!',
- checking_ban = 'สวัสดี %s. เรากำลังตรวจสอบสถานะการโดนแบนของคุณ.',
- join_server = 'ยินดีต้อนรับ %s เข้าสู่ {Server Name}.',
- checking_whitelisted = 'สัวสดี %s. เรากำลังเช็ค whitelisted ของคุณ.',
- exploit_banned = 'คุณโดนแบนจากการโกง. เข้าร่วม Discord สำหรับข้อมูลเพิ่มเติม: %{discord}',
- exploit_dropped = 'คุณโดนเตะออกจากเซิฟเวอร์ เนื่องจากเหตุผลเกี่ยวกับการใช้โปรแกรมโกง',
- },
- command = {
- tp = {
- help = 'เคลือนย้ายไปยัง ผู้เล่น หรือ ตำแหน่งพิกัด (สำหรับ Admin)',
- params = {
- x = { name = 'id/x', help = 'ไอดีของผู้เล่น หรือ พิกัด x'},
- y = { name = 'y', help = 'พิกัด y'},
- z = { name = 'z', help = 'พิกัด z'},
- },
- },
- tpm = { help = 'เคลือนย้ายไปบังจุดมาร์ค (สำหรับ Admin)' },
- togglepvp = { help = 'เปิดใช้ pvp ในเซิฟเวอร์ (สำหรับ Admin)' },
- addpermission = {
- help = 'ให้สิทธิ์กับผู้เล่น (สำหรับ God)',
- params = {
- id = { name = 'id', help = 'ไอดีของผู้เล่น' },
- permission = { name = 'permission', help = 'ระดับของสิทธิ์' },
- },
- },
- removepermission = {
- help = 'ลบสิทธิ์ของผู้เล่น (สำหรับ God)',
- params = {
- id = { name = 'id', help = 'ID of player' },
- permission = { name = 'permission', help = 'ระดับของสิทธิ์' },
- },
- },
- openserver = { help = 'เปิดเซิฟเวอร์สำหรับทุกคน (สำหรับ Admin)' },
- closeserver = {
- help = 'ปิดเซิฟเวอร์สำหรับทุกคนที่ไม่มีสิทธิ์การเข้าถึง (สำหรับ Admin)',
- params = {
- reason = { name = 'reason', help = 'เหตุผลที่ปิด (ถ้ามี)' },
- },
- },
- car = {
- help = 'เรียกยานพาหนะ (สำหรับ Admin)',
- params = {
- model = { name = 'model', help = 'ชื่อของยานพาหนะ' },
- },
- },
- dv = { help = 'ลบยานพาหนะ (สำหรับ Admin)' },
- givemoney = {
- help = 'ให้เงินกับผู้เล่น (สำหรับ Admin)',
- params = {
- id = { name = 'id', help = 'ไอดีของผู้เล่น' },
- moneytype = { name = 'moneytype', help = 'ประเภทของเงิน (เงินสด, เงินฝาก, คริปโต)' },
- amount = { name = 'amount', help = 'จำนวนเงิน' },
- },
- },
- setmoney = {
- help = 'กำหนดจำนวนเงินของผู้เล่น (สำหรับ Admin)',
- params = {
- id = { name = 'id', help = 'ไอดีของผู้เล่น' },
- moneytype = { name = 'moneytype', help = 'ประเภทของเงิน (เงินสด, เงินฝาก, คริปโต)' },
- amount = { name = 'amount', help = 'จำนวนเงิน' },
- },
- },
- job = { help = 'ตรวจสอบอาชีพของคุณ' },
- setjob = {
- help = 'กำหนดอาชีพของผู้เล่น (สำหรับ Admin)',
- params = {
- id = { name = 'id', help = 'ไอดีของผู้เล่น' },
- job = { name = 'job', help = 'ชื่ออาชีพ่' },
- grade = { name = 'grade', help = 'ระดับ' },
- },
- },
- gang = { help = 'ตรวจสอบแก๊งของคุณ' },
- setgang = {
- help = 'กำหนดแก๊งของผู้เล่น (สำหรับ Admin)',
- params = {
- id = { name = 'id', help = 'ไอดีของผู้เล่น' },
- gang = { name = 'gang', help = 'ชื่อแก๊ง' },
- grade = { name = 'grade', help = 'ระดับ' },
- },
- },
- ooc = { help = 'ข้อความ OOC' },
- me = {
- help = 'แสดงข้อความใกล้เขียง',
- params = {
- message = { name = 'message', help = 'ข้อความที่จะส่ง' }
- },
- },
- }
-}
-
-if GetConvar('qb_locale', 'en') == 'th' then
- Lang = Locale:new({
- phrases = Translations,
- warnOnMissing = true,
- fallbackLang = Lang,
- })
-end
+local Translations = {
+ error = {
+ not_online = 'ผู้เล่นไม่ออนไลน์',
+ wrong_format = 'รูปแบบไม่ถูกต้อง',
+ missing_args = 'ตัวแปรที่ต้องการไม่ถูกกำหนด (x, y, z)',
+ missing_args2 = 'ต้องกำหนดตัวแปรทุกตัวให้ครบ!',
+ no_access = 'ไม่สามารถเข้าถึงคำสั่งนี้ได้',
+ company_too_poor = 'บริษัทของคุณกำลังจะล้มละลาย',
+ item_not_exist = 'ไม่มีไอเทมนี้',
+ too_heavy = 'กระเป๋าเต็มแล้ว',
+ location_not_exist = 'ไม่มีตำแหน่งนี้',
+ duplicate_license = 'พบใบอนุญาต Rockstar ซ้ำ',
+ no_valid_license = 'ไม่พบใบอนุญาต Rockstar ที่ถูกต้อง',
+ not_whitelisted = 'คุณไม่ได้รับอนุญาตให้เข้าใช้เซิร์ฟเวอร์นี้',
+ server_already_open = 'เซิร์ฟเวอร์ถูกเปิดแล้ว',
+ server_already_closed = 'เซิร์ฟเวอร์ถูกปิดแล้ว',
+ no_permission = 'คุณไม่ได้รับสิทธิ์ในการใช้คำสั่งนี้..',
+ no_waypoint = 'ไม่มีจุดเส้นทางที่ตั้ง',
+ tp_error = 'ข้อผิดพลาดขณะวาร์ป',
+ connecting_database_error = 'เกิดข้อผิดพลาดในการเชื่อมต่อกับเซิร์ฟเวอร์ (SQL server เปิดอยู่ไหม?)',
+ connecting_database_timeout = 'เชื่อมต่อกับฐานข้อมูลเกิด timeout (SQL server เปิดอยู่ไหม?'
+ },
+ success = {
+ server_opened = 'เซิฟเวอร์เปิดแล้ว',
+ server_closed = 'เซิฟเวอร์เปิดแล้ว',
+ teleported_waypoint = 'เคลือนย้ายไปยังจุดหมาย.',
+ },
+ info = {
+ received_paycheck = 'คุณได้รับเงินเดือน $%{value}',
+ job_info = 'อาชีพ: %{value} | ระดับ: %{value2} | กำลังปฏิบัติหน้าที่: %{value3}',
+ gang_info = 'แก๊ง: %{value} | ระดับ: %{value2}',
+ on_duty = 'คุณเริ่มปฏิบัติหน้าที่แล้ว!',
+ off_duty = 'คุณออกจากการปฏิบัติหน้าที่แล้ว!',
+ checking_ban = 'สวัสดี %s. เรากำลังตรวจสอบสถานะการโดนแบนของคุณ.',
+ join_server = 'ยินดีต้อนรับ %s เข้าสู่ {Server Name}.',
+ checking_whitelisted = 'สัวสดี %s. เรากำลังเช็ค whitelisted ของคุณ.',
+ exploit_banned = 'คุณโดนแบนจากการโกง. เข้าร่วม Discord สำหรับข้อมูลเพิ่มเติม: %{discord}',
+ exploit_dropped = 'คุณโดนเตะออกจากเซิฟเวอร์ เนื่องจากเหตุผลเกี่ยวกับการใช้โปรแกรมโกง',
+ },
+ command = {
+ tp = {
+ help = 'เคลือนย้ายไปยัง ผู้เล่น หรือ ตำแหน่งพิกัด (สำหรับ Admin)',
+ params = {
+ x = { name = 'id/x', help = 'ไอดีของผู้เล่น หรือ พิกัด x'},
+ y = { name = 'y', help = 'พิกัด y'},
+ z = { name = 'z', help = 'พิกัด z'},
+ },
+ },
+ tpm = { help = 'เคลือนย้ายไปบังจุดมาร์ค (สำหรับ Admin)' },
+ togglepvp = { help = 'เปิดใช้ pvp ในเซิฟเวอร์ (สำหรับ Admin)' },
+ addpermission = {
+ help = 'ให้สิทธิ์กับผู้เล่น (สำหรับ God)',
+ params = {
+ id = { name = 'id', help = 'ไอดีของผู้เล่น' },
+ permission = { name = 'permission', help = 'ระดับของสิทธิ์' },
+ },
+ },
+ removepermission = {
+ help = 'ลบสิทธิ์ของผู้เล่น (สำหรับ God)',
+ params = {
+ id = { name = 'id', help = 'ID of player' },
+ permission = { name = 'permission', help = 'ระดับของสิทธิ์' },
+ },
+ },
+ openserver = { help = 'เปิดเซิฟเวอร์สำหรับทุกคน (สำหรับ Admin)' },
+ closeserver = {
+ help = 'ปิดเซิฟเวอร์สำหรับทุกคนที่ไม่มีสิทธิ์การเข้าถึง (สำหรับ Admin)',
+ params = {
+ reason = { name = 'reason', help = 'เหตุผลที่ปิด (ถ้ามี)' },
+ },
+ },
+ car = {
+ help = 'เรียกยานพาหนะ (สำหรับ Admin)',
+ params = {
+ model = { name = 'model', help = 'ชื่อของยานพาหนะ' },
+ },
+ },
+ dv = { help = 'ลบยานพาหนะ (สำหรับ Admin)' },
+ givemoney = {
+ help = 'ให้เงินกับผู้เล่น (สำหรับ Admin)',
+ params = {
+ id = { name = 'id', help = 'ไอดีของผู้เล่น' },
+ moneytype = { name = 'moneytype', help = 'ประเภทของเงิน (เงินสด, เงินฝาก, คริปโต)' },
+ amount = { name = 'amount', help = 'จำนวนเงิน' },
+ },
+ },
+ setmoney = {
+ help = 'กำหนดจำนวนเงินของผู้เล่น (สำหรับ Admin)',
+ params = {
+ id = { name = 'id', help = 'ไอดีของผู้เล่น' },
+ moneytype = { name = 'moneytype', help = 'ประเภทของเงิน (เงินสด, เงินฝาก, คริปโต)' },
+ amount = { name = 'amount', help = 'จำนวนเงิน' },
+ },
+ },
+ job = { help = 'ตรวจสอบอาชีพของคุณ' },
+ setjob = {
+ help = 'กำหนดอาชีพของผู้เล่น (สำหรับ Admin)',
+ params = {
+ id = { name = 'id', help = 'ไอดีของผู้เล่น' },
+ job = { name = 'job', help = 'ชื่ออาชีพ่' },
+ grade = { name = 'grade', help = 'ระดับ' },
+ },
+ },
+ gang = { help = 'ตรวจสอบแก๊งของคุณ' },
+ setgang = {
+ help = 'กำหนดแก๊งของผู้เล่น (สำหรับ Admin)',
+ params = {
+ id = { name = 'id', help = 'ไอดีของผู้เล่น' },
+ gang = { name = 'gang', help = 'ชื่อแก๊ง' },
+ grade = { name = 'grade', help = 'ระดับ' },
+ },
+ },
+ ooc = { help = 'ข้อความ OOC' },
+ me = {
+ help = 'แสดงข้อความใกล้เขียง',
+ params = {
+ message = { name = 'message', help = 'ข้อความที่จะส่ง' }
+ },
+ },
+ }
+}
+
+if GetConvar('qb_locale', 'en') == 'th' then
+ Lang = Locale:new({
+ phrases = Translations,
+ warnOnMissing = true,
+ fallbackLang = Lang,
+ })
+end
diff --git a/server/commands.lua b/server/commands.lua
index 368736f..7048ca1 100644
--- a/server/commands.lua
+++ b/server/commands.lua
@@ -92,7 +92,7 @@ QBCore.Commands.Add('tp', Lang:t('command.tp.help'), { { name = Lang:t('command.
TriggerClientEvent('QBCore:Notify', source, Lang:t('error.not_online'), 'error')
end
else
- local location = QBShared.Locations[args[1]]
+ local location = QBCore.Shared.Locations[args[1]]
if location then
TriggerClientEvent('QBCore:Command:TeleportToCoords', source, location.x, location.y, location.z, location.w)
else
@@ -101,12 +101,12 @@ QBCore.Commands.Add('tp', Lang:t('command.tp.help'), { { name = Lang:t('command.
end
else
if args[1] and args[2] and args[3] then
- local x = tonumber((args[1]:gsub(',', ''))) + .0
- local y = tonumber((args[2]:gsub(',', ''))) + .0
- local z = tonumber((args[3]:gsub(',', ''))) + .0
- local heading = args[4] and tonumber((args[4]:gsub(',', ''))) + .0 or false
- if x ~= 0 and y ~= 0 and z ~= 0 then
- TriggerClientEvent('QBCore:Command:TeleportToCoords', source, x, y, z, heading)
+ local x = tonumber((args[1]:gsub(',', '')))
+ local y = tonumber((args[2]:gsub(',', '')))
+ local z = tonumber((args[3]:gsub(',', '')))
+ local heading = args[4] and tonumber((args[4]:gsub(',', ''))) or false
+ if x and y and z then
+ TriggerClientEvent('QBCore:Command:TeleportToCoords', source, x + .0, y + .0, z + .0, heading and heading + .0 or false)
else
TriggerClientEvent('QBCore:Notify', source, Lang:t('error.wrong_format'), 'error')
end
@@ -240,7 +240,9 @@ end, 'admin')
-- Job
QBCore.Commands.Add('job', Lang:t('command.job.help'), {}, false, function(source)
- local PlayerJob = QBCore.Functions.GetPlayer(source).PlayerData.job
+ local Player = QBCore.Functions.GetPlayer(source)
+ if not Player then return end
+ local PlayerJob = Player.PlayerData.job
TriggerClientEvent('QBCore:Notify', source, Lang:t('info.job_info', { value = PlayerJob.label, value2 = PlayerJob.grade.name, value3 = PlayerJob.onduty }))
end, 'user')
@@ -256,7 +258,9 @@ end, 'admin')
-- Gang
QBCore.Commands.Add('gang', Lang:t('command.gang.help'), {}, false, function(source)
- local PlayerGang = QBCore.Functions.GetPlayer(source).PlayerData.gang
+ local Player = QBCore.Functions.GetPlayer(source)
+ if not Player then return end
+ local PlayerGang = Player.PlayerData.gang
TriggerClientEvent('QBCore:Notify', source, Lang:t('info.gang_info', { value = PlayerGang.label, value2 = PlayerGang.grade.name }))
end, 'user')
diff --git a/server/debug.lua b/server/debug.lua
index 8ec63e6..a4366bf 100644
--- a/server/debug.lua
+++ b/server/debug.lua
@@ -9,36 +9,42 @@ local function tPrint(tbl, indent)
print(formatting)
tPrint(v, indent + 1)
elseif tblType == 'boolean' then
- print(('%s^1 %s ^0'):format(formatting, v))
+ print(('%s ^1%s^0'):format(formatting, v))
elseif tblType == 'function' then
- print(('%s^9 %s ^0'):format(formatting, v))
+ print(('%s ^9%s^0'):format(formatting, v))
elseif tblType == 'number' then
- print(('%s^5 %s ^0'):format(formatting, v))
+ print(('%s ^5%s^0'):format(formatting, v))
elseif tblType == 'string' then
- print(("%s ^2'%s' ^0"):format(formatting, v))
+ print(('%s ^2\'%s\'^0'):format(formatting, v))
+ elseif tblType == 'nil' then
+ print(('%s ^8nil^0'):format(formatting))
else
- print(('%s^2 %s ^0'):format(formatting, v))
+ print(('%s ^2%s^0'):format(formatting, tostring(v)))
end
end
else
- print(('%s ^0%s'):format(string.rep(' ', indent), tbl))
+ print(('%s ^0%s'):format(string.rep(' ', indent), tostring(tbl)))
end
end
-RegisterServerEvent('QBCore:DebugSomething', function(tbl, indent, resource)
- print(('\x1b[4m\x1b[36m[ %s : DEBUG]\x1b[0m'):format(resource))
+RegisterNetEvent('QBCore:DebugSomething', function(tbl, indent, resource)
+ resource = resource or 'unknown'
+ print(('^4[ %s : DEBUG]^0'):format(resource))
tPrint(tbl, indent)
- print('\x1b[4m\x1b[36m[ END DEBUG ]\x1b[0m')
+ print('^4[ END DEBUG ]^0')
end)
function QBCore.Debug(tbl, indent)
- TriggerEvent('QBCore:DebugSomething', tbl, indent, GetInvokingResource() or 'qb-core')
+ local resource = GetInvokingResource() or 'qb-core'
+ print(('^4[ %s : DEBUG]^0'):format(resource))
+ tPrint(tbl, indent)
+ print('^4[ END DEBUG ]^0')
end
function QBCore.ShowError(resource, msg)
- print('\x1b[31m[' .. resource .. ':ERROR]\x1b[0m ' .. msg)
+ print(('^1[%s:ERROR]^0 %s'):format(resource, msg))
end
function QBCore.ShowSuccess(resource, msg)
- print('\x1b[32m[' .. resource .. ':LOG]\x1b[0m ' .. msg)
+ print(('^2[%s:LOG]^0 %s'):format(resource, msg))
end
diff --git a/server/events.lua b/server/events.lua
index bd45ec6..49a4c1b 100644
--- a/server/events.lua
+++ b/server/events.lua
@@ -10,16 +10,18 @@ end)
AddEventHandler('playerDropped', function(reason)
local src = source
if not QBCore.Players[src] then return end
- local Player = QBCore.Players[src]
- TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Dropped', 'red', '**' .. GetPlayerName(src) .. '** (' .. Player.PlayerData.license .. ') left..' .. '\n **Reason:** ' .. reason)
- TriggerEvent('QBCore:Server:PlayerDropped', Player)
- Player.Functions.Save()
- QBCore.Player_Buckets[Player.PlayerData.license] = nil
+ local player = QBCore.Players[src]
+ TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Dropped', 'red', '**' .. GetPlayerName(src) .. '** (' .. player.PlayerData.license .. ') left..' .. '\n **Reason:** ' .. reason)
+ player.Functions.Save()
+ TriggerEvent('QBCore:Server:PlayerDropped', src)
+ TriggerEvent('QBCore:Server:OnPlayerUnload', src)
+ QBCore.Player_Buckets[player.PlayerData.license] = nil
+ QBCore.PlayersByCitizenId[player.PlayerData.citizenid] = nil
QBCore.Players[src] = nil
end)
-AddEventHandler("onResourceStop", function(resName)
- for i,v in pairs(QBCore.UsableItems) do
+AddEventHandler('onResourceStop', function(resName)
+ for i, v in pairs(QBCore.UsableItems) do
if v.resource == resName then
QBCore.UsableItems[i] = nil
end
@@ -36,7 +38,7 @@ if readyFunction ~= nil then
local DatabaseInfo = QBCore.Functions.GetDatabaseInfo()
if not DatabaseInfo or not DatabaseInfo.exists then return end
- local result = MySQL.query.await('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = "bans";', {DatabaseInfo.database})
+ local result = MySQL.query.await('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = "bans";', { DatabaseInfo.database })
if result and result[1] then
bansTableExists = true
end
@@ -124,7 +126,7 @@ end)
-- Client Callback
RegisterNetEvent('QBCore:Server:TriggerClientCallback', function(name, ...)
- local ClientCallback = QBCore.ClientCallbacks[name..source]
+ local ClientCallback = QBCore.ClientCallbacks[name .. source]
if ClientCallback then
ClientCallback.promise:resolve(...)
@@ -132,7 +134,7 @@ RegisterNetEvent('QBCore:Server:TriggerClientCallback', function(name, ...)
ClientCallback.callback(...)
end
- QBCore.ClientCallbacks[name..source] = nil
+ QBCore.ClientCallbacks[name .. source] = nil
end
end)
@@ -149,8 +151,12 @@ end)
-- Player
+local updateCooldowns = {}
RegisterNetEvent('QBCore:UpdatePlayer', function()
local src = source
+ local now = GetGameTimer()
+ if updateCooldowns[src] and (now - updateCooldowns[src]) < 10000 then return end
+ updateCooldowns[src] = now
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local newHunger = Player.PlayerData.metadata['hunger'] - QBCore.Config.Player.HungerRate
@@ -161,8 +167,9 @@ RegisterNetEvent('QBCore:UpdatePlayer', function()
if newThirst <= 0 then
newThirst = 0
end
- Player.Functions.SetMetaData('thirst', newThirst)
- Player.Functions.SetMetaData('hunger', newHunger)
+ Player.PlayerData.metadata['hunger'] = newHunger
+ Player.PlayerData.metadata['thirst'] = newThirst
+ Player.Functions.UpdateClient('metadata', Player.PlayerData.metadata)
TriggerClientEvent('hud:client:UpdateNeeds', src, newHunger, newThirst)
Player.Functions.Save()
end)
@@ -183,6 +190,24 @@ RegisterNetEvent('QBCore:ToggleDuty', function()
TriggerClientEvent('QBCore:Client:SetDuty', src, Player.PlayerData.job.onduty)
end)
+RegisterNetEvent('QBCore:Server:OnPlayerLoaded', function()
+ local src = source
+ if not QBCore.Players[src] then return end
+ TriggerClientEvent('QBCore:Client:OnPlayerLoaded', src)
+end)
+
+-- Central server-side data change handler — re-fires legacy events for backward compat
+AddEventHandler('QBCore:Server:OnPlayerUpdated', function(src, key, val)
+ if key == 'job' then
+ TriggerEvent('QBCore:Server:OnJobUpdate', src, val)
+ elseif key == 'gang' then
+ TriggerEvent('QBCore:Server:OnGangUpdate', src, val)
+ elseif key == 'all' then
+ TriggerEvent('QBCore:Server:OnJobUpdate', src, val.job)
+ TriggerEvent('QBCore:Server:OnGangUpdate', src, val.gang)
+ end
+end)
+
-- BaseEvents
-- Vehicles
@@ -224,26 +249,6 @@ RegisterServerEvent('baseevents:leftVehicle', function(veh, seat, modelName)
TriggerClientEvent('QBCore:Client:VehicleInfo', src, data)
end)
--- Items
-
--- This event is exploitable and should not be used. It has been deprecated, and will be removed soon.
-RegisterNetEvent('QBCore:Server:UseItem', function(item)
- print(string.format('%s triggered QBCore:Server:UseItem by ID %s with the following data. This event is deprecated due to exploitation, and will be removed soon. Check qb-inventory for the right use on this event.', GetInvokingResource(), source))
- QBCore.Debug(item)
-end)
-
--- This event is exploitable and should not be used. It has been deprecated, and will be removed soon. function(itemName, amount, slot)
-RegisterNetEvent('QBCore:Server:RemoveItem', function(itemName, amount)
- local src = source
- print(string.format('%s triggered QBCore:Server:RemoveItem by ID %s for %s %s. This event is deprecated due to exploitation, and will be removed soon. Adjust your events accordingly to do this server side with player functions.', GetInvokingResource(), src, amount, itemName))
-end)
-
--- This event is exploitable and should not be used. It has been deprecated, and will be removed soon. function(itemName, amount, slot, info)
-RegisterNetEvent('QBCore:Server:AddItem', function(itemName, amount)
- local src = source
- print(string.format('%s triggered QBCore:Server:AddItem by ID %s for %s %s. This event is deprecated due to exploitation, and will be removed soon. Adjust your events accordingly to do this server side with player functions.', GetInvokingResource(), src, amount, itemName))
-end)
-
-- Non-Chat Command Calling (ex: qb-adminmenu)
RegisterNetEvent('QBCore:CallCommand', function(command, args)
@@ -269,7 +274,7 @@ end)
-- convert it to a vehicle via the NetToVeh native
QBCore.Functions.CreateCallback('QBCore:Server:SpawnVehicle', function(source, cb, model, coords, warp)
local veh = QBCore.Functions.SpawnVehicle(source, model, coords, warp)
- cb(NetworkGetNetworkIdFromEntity(veh))
+ cb(DoesEntityExist(veh) and NetworkGetNetworkIdFromEntity(veh) or nil)
end)
-- Use this for long distance vehicle spawning
@@ -278,7 +283,7 @@ end)
-- convert it to a vehicle via the NetToVeh native
QBCore.Functions.CreateCallback('QBCore:Server:CreateVehicle', function(source, cb, model, coords, warp)
local veh = QBCore.Functions.CreateAutomobile(source, model, coords, warp)
- cb(NetworkGetNetworkIdFromEntity(veh))
+ cb(DoesEntityExist(veh) and NetworkGetNetworkIdFromEntity(veh) or nil)
end)
--QBCore.Functions.CreateCallback('QBCore:HasItem', function(source, cb, items, amount)
diff --git a/server/exports.lua b/server/exports.lua
index d02518c..a8b48e2 100644
--- a/server/exports.lua
+++ b/server/exports.lua
@@ -3,11 +3,11 @@ local function SetMethod(methodName, handler)
if type(methodName) ~= 'string' then
return false, 'invalid_method_name'
end
-
+ if QBCore.Functions[methodName] ~= nil then
+ return false, 'method_exists'
+ end
QBCore.Functions[methodName] = handler
-
TriggerEvent('QBCore:Server:UpdateObject')
-
return true, 'success'
end
@@ -19,11 +19,11 @@ local function SetField(fieldName, data)
if type(fieldName) ~= 'string' then
return false, 'invalid_field_name'
end
-
+ if QBCore[fieldName] ~= nil then
+ return false, 'field_exists'
+ end
QBCore[fieldName] = data
-
TriggerEvent('QBCore:Server:UpdateObject')
-
return true, 'success'
end
@@ -52,32 +52,16 @@ exports('AddJob', AddJob)
-- Multiple Add Jobs
local function AddJobs(jobs)
- local shouldContinue = true
- local message = 'success'
- local errorItem = nil
-
for key, value in pairs(jobs) do
- if type(key) ~= 'string' then
- message = 'invalid_job_name'
- shouldContinue = false
- errorItem = jobs[key]
- break
- end
-
- if QBCore.Shared.Jobs[key] then
- message = 'job_exists'
- shouldContinue = false
- errorItem = jobs[key]
- break
- end
-
+ if type(key) ~= 'string' then return false, 'invalid_job_name', value end
+ if QBCore.Shared.Jobs[key] then return false, 'job_exists', value end
+ end
+ for key, value in pairs(jobs) do
QBCore.Shared.Jobs[key] = value
end
-
- if not shouldContinue then return false, message, errorItem end
TriggerClientEvent('QBCore:Client:OnSharedUpdateMultiple', -1, 'Jobs', jobs)
TriggerEvent('QBCore:Server:UpdateObject')
- return true, message, nil
+ return true, 'success', nil
end
QBCore.Functions.AddJobs = AddJobs
@@ -162,32 +146,16 @@ exports('UpdateItem', UpdateItem)
-- Multiple Add Items
local function AddItems(items)
- local shouldContinue = true
- local message = 'success'
- local errorItem = nil
-
for key, value in pairs(items) do
- if type(key) ~= 'string' then
- message = 'invalid_item_name'
- shouldContinue = false
- errorItem = items[key]
- break
- end
-
- if QBCore.Shared.Items[key] then
- message = 'item_exists'
- shouldContinue = false
- errorItem = items[key]
- break
- end
-
+ if type(key) ~= 'string' then return false, 'invalid_item_name', value end
+ if QBCore.Shared.Items[key] then return false, 'item_exists', value end
+ end
+ for key, value in pairs(items) do
QBCore.Shared.Items[key] = value
end
-
- if not shouldContinue then return false, message, errorItem end
TriggerClientEvent('QBCore:Client:OnSharedUpdateMultiple', -1, 'Items', items)
TriggerEvent('QBCore:Server:UpdateObject')
- return true, message, nil
+ return true, 'success', nil
end
QBCore.Functions.AddItems = AddItems
@@ -235,32 +203,16 @@ exports('AddGang', AddGang)
-- Multiple Add Gangs
local function AddGangs(gangs)
- local shouldContinue = true
- local message = 'success'
- local errorItem = nil
-
for key, value in pairs(gangs) do
- if type(key) ~= 'string' then
- message = 'invalid_gang_name'
- shouldContinue = false
- errorItem = gangs[key]
- break
- end
-
- if QBCore.Shared.Gangs[key] then
- message = 'gang_exists'
- shouldContinue = false
- errorItem = gangs[key]
- break
- end
-
+ if type(key) ~= 'string' then return false, 'invalid_gang_name', value end
+ if QBCore.Shared.Gangs[key] then return false, 'gang_exists', value end
+ end
+ for key, value in pairs(gangs) do
QBCore.Shared.Gangs[key] = value
end
-
- if not shouldContinue then return false, message, errorItem end
TriggerClientEvent('QBCore:Client:OnSharedUpdateMultiple', -1, 'Gangs', gangs)
TriggerEvent('QBCore:Server:UpdateObject')
- return true, message, nil
+ return true, 'success', nil
end
QBCore.Functions.AddGangs = AddGangs
diff --git a/server/functions.lua b/server/functions.lua
index 439b714..2a4650c 100644
--- a/server/functions.lua
+++ b/server/functions.lua
@@ -55,12 +55,7 @@ end
---@param citizenid string
---@return table?
function QBCore.Functions.GetPlayerByCitizenId(citizenid)
- for _, Player in pairs(QBCore.Players) do
- if Player.PlayerData.citizenid == citizenid then
- return Player
- end
- end
- return nil
+ return QBCore.PlayersByCitizenId[citizenid]
end
---Get offline player by citizen id
@@ -304,16 +299,12 @@ end
---@return table|boolean
function QBCore.Functions.GetPlayersInBucket(bucket)
local curr_bucket_pool = {}
- if QBCore.Player_Buckets and next(QBCore.Player_Buckets) then
- for _, v in pairs(QBCore.Player_Buckets) do
- if v.bucket == bucket then
- curr_bucket_pool[#curr_bucket_pool + 1] = v.id
- end
+ for _, v in pairs(QBCore.Player_Buckets) do
+ if v.bucket == bucket then
+ curr_bucket_pool[#curr_bucket_pool + 1] = v.id
end
- return curr_bucket_pool
- else
- return false
end
+ return curr_bucket_pool
end
---Will return an array of all the entities inside the current bucket
@@ -322,16 +313,12 @@ end
---@return table|boolean
function QBCore.Functions.GetEntitiesInBucket(bucket)
local curr_bucket_pool = {}
- if QBCore.Entity_Buckets and next(QBCore.Entity_Buckets) then
- for _, v in pairs(QBCore.Entity_Buckets) do
- if v.bucket == bucket then
- curr_bucket_pool[#curr_bucket_pool + 1] = v.id
- end
+ for _, v in pairs(QBCore.Entity_Buckets) do
+ if v.bucket == bucket then
+ curr_bucket_pool[#curr_bucket_pool + 1] = v.id
end
- return curr_bucket_pool
- else
- return false
end
+ return curr_bucket_pool
end
---Server side vehicle creation with optional callback
@@ -347,14 +334,24 @@ function QBCore.Functions.SpawnVehicle(source, model, coords, warp)
if not coords then coords = GetEntityCoords(ped) end
local heading = coords.w and coords.w or 0.0
local veh = CreateVehicle(model, coords.x, coords.y, coords.z, heading, true, true)
- while not DoesEntityExist(veh) do Wait(0) end
+ local t = 0
+ while not DoesEntityExist(veh) and t < 1000 do
+ Wait(0)
+ t += 1
+ end
if warp then
- while GetVehiclePedIsIn(ped) ~= veh do
+ t = 0
+ while GetVehiclePedIsIn(ped) ~= veh and t < 100 do
Wait(0)
+ t += 1
TaskWarpPedIntoVehicle(ped, veh, -1)
end
end
- while NetworkGetEntityOwner(veh) ~= source do Wait(0) end
+ t = 0
+ while NetworkGetEntityOwner(veh) ~= source and t < 1000 do
+ Wait(0)
+ t += 1
+ end
return veh
end
@@ -373,7 +370,11 @@ function QBCore.Functions.CreateAutomobile(source, model, coords, warp)
local heading = coords.w and coords.w or 0.0
local CreateAutomobile = `CREATE_AUTOMOBILE`
local veh = Citizen.InvokeNative(CreateAutomobile, model, coords, heading, true, true)
- while not DoesEntityExist(veh) do Wait(0) end
+ local t = 0
+ while not DoesEntityExist(veh) and t < 1000 do
+ Wait(0)
+ t += 1
+ end
if warp then TaskWarpPedIntoVehicle(GetPlayerPed(source), veh, -1) end
return veh
end
@@ -395,7 +396,11 @@ function QBCore.Functions.CreateVehicle(source, model, vehtype, coords, warp)
if not coords then coords = GetEntityCoords(GetPlayerPed(source)) end
local heading = coords.w and coords.w or 0.0
local veh = CreateVehicleServerSetter(model, vehtype, coords, heading)
- while not DoesEntityExist(veh) do Wait(0) end
+ local t = 0
+ while not DoesEntityExist(veh) and t < 1000 do
+ Wait(0)
+ t += 1
+ end
if warp then TaskWarpPedIntoVehicle(GetPlayerPed(source), veh, -1) end
return veh
end
@@ -408,9 +413,11 @@ function PaycheckInterval()
CreateThread(function()
for _, Player in pairs(QBCore.Players) do
if Player then
- local payment = QBShared.Jobs[Player.PlayerData.job.name]['grades'][tostring(Player.PlayerData.job.grade.level)].payment
+ local jobData = QBCore.Shared.Jobs[Player.PlayerData.job.name]
+ local gradeData = jobData and jobData['grades'][tostring(Player.PlayerData.job.grade.level)]
+ local payment = gradeData and gradeData.payment
if not payment then payment = Player.PlayerData.job.payment end
- if Player.PlayerData.job and payment > 0 and (QBShared.Jobs[Player.PlayerData.job.name].offDutyPay or Player.PlayerData.job.onduty) then
+ if Player.PlayerData.job and payment > 0 and (QBCore.Shared.Jobs[Player.PlayerData.job.name].offDutyPay or Player.PlayerData.job.onduty) then
if QBCore.Config.Money.PayCheckSociety then
local account = exports['qb-banking']:GetAccountBalance(Player.PlayerData.job.name)
if account ~= 0 then
@@ -463,7 +470,9 @@ function QBCore.Functions.TriggerClientCallback(name, source, ...)
if cb == nil then
Citizen.Await(QBCore.ClientCallbacks[name .. source].promise)
- return QBCore.ClientCallbacks[name .. source].promise.value
+ local value = QBCore.ClientCallbacks[name .. source].promise.value
+ QBCore.ClientCallbacks[name .. source] = nil
+ return value
end
end
@@ -538,7 +547,7 @@ function QBCore.Functions.Kick(source, reason, setKickReason, deferrals)
for _ = 0, 4 do
while true do
if source then
- if GetPlayerPing(source) >= 0 then
+ if GetPlayerPing(source) < 0 then
break
end
CreateThread(function()
@@ -723,27 +732,8 @@ function QBCore.Functions.Notify(source, text, type, length)
TriggerClientEvent('QBCore:Notify', source, text, type, length)
end
----???? ... ok
----@param source any
----@param data any
----@param pattern any
----@return boolean
-function QBCore.Functions.PrepForSQL(source, data, pattern)
- data = tostring(data)
- local src = source
- local player = QBCore.Functions.GetPlayer(src)
- local result = string.match(data, pattern)
- if not result or string.len(result) ~= string.len(data) then
- TriggerEvent('qb-log:server:CreateLog', 'anticheat', 'SQL Exploit Attempted', 'red', string.format('%s attempted to exploit SQL!', player.PlayerData.license))
- return false
- end
- return true
-end
-
for functionName, func in pairs(QBCore.Functions) do
if type(func) == 'function' then
exports(functionName, func)
end
end
--- Access a specific function directly:
--- exports['qb-core']:Notify(source, 'Hello Player!')
diff --git a/server/main.lua b/server/main.lua
deleted file mode 100644
index d6228e5..0000000
--- a/server/main.lua
+++ /dev/null
@@ -1,49 +0,0 @@
-QBCore = {}
-QBCore.Config = QBConfig
-QBCore.Shared = QBShared
-QBCore.ClientCallbacks = {}
-QBCore.ServerCallbacks = {}
-
--- Get the full QBCore object (default behavior):
--- local QBCore = GetCoreObject()
-
--- Get only specific parts of QBCore:
--- local QBCore = GetCoreObject({'Players', 'Config'})
-
-local function GetCoreObject(filters)
- if not filters then return QBCore end
- local results = {}
- for i = 1, #filters do
- local key = filters[i]
- if QBCore[key] then
- results[key] = QBCore[key]
- end
- end
- return results
-end
-exports('GetCoreObject', GetCoreObject)
-
-local function GetSharedItems()
- return QBShared.Items
-end
-exports('GetSharedItems', GetSharedItems)
-
-local function GetSharedVehicles()
- return QBShared.Vehicles
-end
-exports('GetSharedVehicles', GetSharedVehicles)
-
-local function GetSharedWeapons()
- return QBShared.Weapons
-end
-exports('GetSharedWeapons', GetSharedWeapons)
-
-local function GetSharedJobs()
- return QBShared.Jobs
-end
-exports('GetSharedJobs', GetSharedJobs)
-
-local function GetSharedGangs()
- return QBShared.Gangs
-end
-exports('GetSharedGangs', GetSharedGangs)
diff --git a/server/player.lua b/server/player.lua
index ecf6ff3..66d2595 100644
--- a/server/player.lua
+++ b/server/player.lua
@@ -1,80 +1,301 @@
-QBCore.Players = {}
-QBCore.Player = {}
+QBCore.Players = {}
+QBCore.Player = {}
+QBCore.PlayersByCitizenId = {}
+local resourceName = GetCurrentResourceName()
--- On player login get their data or set defaults
--- Don't touch any of this unless you know what you are doing
--- Will cause major issues!
+-- ─────────────────────────── Player class ───────────────────────────────────
-local resourceName = GetCurrentResourceName()
-function QBCore.Player.Login(source, citizenid, newData)
- if source and source ~= '' then
- if citizenid then
- local license = QBCore.Functions.GetIdentifier(source, 'license')
- local PlayerData = MySQL.prepare.await('SELECT * FROM players where citizenid = ?', { citizenid })
- if PlayerData and license == PlayerData.license then
- PlayerData.money = json.decode(PlayerData.money)
- PlayerData.job = json.decode(PlayerData.job)
- PlayerData.gang = json.decode(PlayerData.gang)
- PlayerData.position = json.decode(PlayerData.position)
- PlayerData.metadata = json.decode(PlayerData.metadata)
- PlayerData.charinfo = json.decode(PlayerData.charinfo)
- QBCore.Player.CheckPlayerData(source, PlayerData)
- else
- DropPlayer(source, Lang:t('info.exploit_dropped'))
- TriggerEvent('qb-log:server:CreateLog', 'anticheat', 'Anti-Cheat', 'white', GetPlayerName(source) .. ' Has Been Dropped For Character Joining Exploit', false)
- end
- else
- QBCore.Player.CheckPlayerData(source, newData)
- end
- return true
+local varargMethods = {
+ 'GetPlayerData', 'UpdateClient', 'SetJob', 'SetGang', 'Notify', 'HasItem',
+ 'SetJobDuty', 'SetPlayerData', 'SetMetaData', 'GetMetaData',
+ 'AddRep', 'RemoveRep', 'GetRep', 'AddMoney', 'RemoveMoney', 'SetMoney', 'GetMoney',
+ 'AddMethod', 'AddField',
+}
+local noargMethods = { 'GetName', 'Save', 'Logout' }
+
+local function buildMethodTable(player)
+ local t = {}
+ for _, name in ipairs(varargMethods) do
+ local fn = player[name]
+ t[name] = function(...) return fn(player, ...) end
+ end
+ for _, name in ipairs(noargMethods) do
+ local fn = player[name]
+ t[name] = function() return fn(player) end
+ end
+ return t
+end
+
+local Player = {}
+Player.__index = Player
+
+function Player.new(PlayerData, Offline)
+ local self = setmetatable({}, Player)
+ self.PlayerData = PlayerData
+ self.Offline = Offline or false
+ self.Functions = buildMethodTable(self)
+ self.Functions.UpdatePlayerData = self.Functions.UpdateClient
+ return self
+end
+
+-- ─────────────────────────── instance methods ───────────────────────────────
+
+function Player:GetPlayerData()
+ return self.PlayerData
+end
+
+function Player:UpdateClient(key, val)
+ if self.Offline then return end
+ if key then
+ TriggerEvent('QBCore:Server:OnPlayerUpdated', self.PlayerData.source, key, val)
+ TriggerClientEvent('QBCore:Client:OnPlayerUpdated', self.PlayerData.source, key, val)
else
- QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.LOGIN - NO SOURCE GIVEN!')
- return false
+ TriggerEvent('QBCore:Player:SetPlayerData', self.PlayerData)
+ TriggerEvent('QBCore:Server:OnPlayerUpdated', self.PlayerData.source, 'all', self.PlayerData)
+ TriggerClientEvent('QBCore:Client:OnPlayerUpdated', self.PlayerData.source, 'all', self.PlayerData)
end
end
-function QBCore.Player.GetOfflinePlayer(citizenid)
- if citizenid then
- local PlayerData = MySQL.prepare.await('SELECT * FROM players where citizenid = ?', { citizenid })
- if PlayerData then
- PlayerData.money = json.decode(PlayerData.money)
- PlayerData.job = json.decode(PlayerData.job)
- PlayerData.gang = json.decode(PlayerData.gang)
- PlayerData.position = json.decode(PlayerData.position)
- PlayerData.metadata = json.decode(PlayerData.metadata)
- PlayerData.charinfo = json.decode(PlayerData.charinfo)
- return QBCore.Player.CheckPlayerData(nil, PlayerData)
- end
+function Player:SetJob(job, grade)
+ job = job:lower()
+ grade = grade or '0'
+ if not QBCore.Shared.Jobs[job] then return false end
+ self.PlayerData.job = {
+ name = job,
+ label = QBCore.Shared.Jobs[job].label,
+ onduty = QBCore.Shared.Jobs[job].defaultDuty,
+ type = QBCore.Shared.Jobs[job].type or 'none',
+ grade = {
+ name = 'No Grades',
+ level = 0,
+ payment = 30,
+ isboss = false,
+ }
+ }
+ local gradeKey = tostring(grade)
+ local jobGradeInfo = QBCore.Shared.Jobs[job].grades[gradeKey]
+ if jobGradeInfo then
+ self.PlayerData.job.grade.name = jobGradeInfo.name
+ self.PlayerData.job.grade.level = tonumber(gradeKey)
+ self.PlayerData.job.grade.payment = jobGradeInfo.payment
+ self.PlayerData.job.grade.isboss = jobGradeInfo.isboss or false
+ self.PlayerData.job.isboss = jobGradeInfo.isboss or false
end
- return nil
+ if not self.Offline then
+ self:UpdateClient('job', self.PlayerData.job)
+ end
+ return true
end
-function QBCore.Player.GetPlayerByLicense(license)
- if license then
- local source = QBCore.Functions.GetSource(license)
- if source > 0 then
- return QBCore.Players[source]
- else
- return QBCore.Player.GetOfflinePlayerByLicense(license)
- end
+function Player:SetGang(gang, grade)
+ gang = gang:lower()
+ grade = grade or '0'
+ if not QBCore.Shared.Gangs[gang] then return false end
+ self.PlayerData.gang = {
+ name = gang,
+ label = QBCore.Shared.Gangs[gang].label,
+ grade = {
+ name = 'No Grades',
+ level = 0,
+ isboss = false,
+ }
+ }
+ local gradeKey = tostring(grade)
+ local gangGradeInfo = QBCore.Shared.Gangs[gang].grades[gradeKey]
+ if gangGradeInfo then
+ self.PlayerData.gang.grade.name = gangGradeInfo.name
+ self.PlayerData.gang.grade.level = tonumber(gradeKey)
+ self.PlayerData.gang.grade.isboss = gangGradeInfo.isboss or false
+ self.PlayerData.gang.isboss = gangGradeInfo.isboss or false
end
- return nil
+ if not self.Offline then
+ self:UpdateClient('gang', self.PlayerData.gang)
+ end
+ return true
end
-function QBCore.Player.GetOfflinePlayerByLicense(license)
- if license then
- local PlayerData = MySQL.prepare.await('SELECT * FROM players where license = ?', { license })
- if PlayerData then
- PlayerData.money = json.decode(PlayerData.money)
- PlayerData.job = json.decode(PlayerData.job)
- PlayerData.gang = json.decode(PlayerData.gang)
- PlayerData.position = json.decode(PlayerData.position)
- PlayerData.metadata = json.decode(PlayerData.metadata)
- PlayerData.charinfo = json.decode(PlayerData.charinfo)
- return QBCore.Player.CheckPlayerData(nil, PlayerData)
+function Player:Notify(text, notifyType, length)
+ TriggerClientEvent('QBCore:Notify', self.PlayerData.source, text, notifyType, length)
+end
+
+function Player:HasItem(items, amount)
+ return QBCore.Functions.HasItem(self.PlayerData.source, items, amount)
+end
+
+function Player:GetName()
+ local charinfo = self.PlayerData.charinfo
+ return charinfo.firstname .. ' ' .. charinfo.lastname
+end
+
+function Player:SetJobDuty(onDuty)
+ self.PlayerData.job.onduty = not not onDuty
+ if not self.Offline then
+ self:UpdateClient('job', self.PlayerData.job)
+ end
+end
+
+function Player:SetPlayerData(key, val)
+ if not key or type(key) ~= 'string' then return end
+ self.PlayerData[key] = val
+ self:UpdateClient(key, val)
+end
+
+function Player:SetMetaData(meta, val)
+ if not meta or type(meta) ~= 'string' then return end
+ if meta == 'hunger' or meta == 'thirst' then
+ val = math.min(100, math.max(0, val))
+ end
+ self.PlayerData.metadata[meta] = val
+ self:UpdateClient('metadata', self.PlayerData.metadata)
+end
+
+function Player:GetMetaData(meta)
+ if not meta or type(meta) ~= 'string' then return end
+ return self.PlayerData.metadata[meta]
+end
+
+function Player:AddRep(rep, amount)
+ if not rep or not amount then return end
+ local addAmount = tonumber(amount)
+ local currentRep = self.PlayerData.metadata['rep'][rep] or 0
+ self.PlayerData.metadata['rep'][rep] = currentRep + addAmount
+ self:UpdateClient('metadata', self.PlayerData.metadata)
+end
+
+function Player:RemoveRep(rep, amount)
+ if not rep or not amount then return end
+ local removeAmount = tonumber(amount)
+ local currentRep = self.PlayerData.metadata['rep'][rep] or 0
+ self.PlayerData.metadata['rep'][rep] = math.max(0, currentRep - removeAmount)
+ self:UpdateClient('metadata', self.PlayerData.metadata)
+end
+
+function Player:GetRep(rep)
+ if not rep then return end
+ return self.PlayerData.metadata['rep'][rep] or 0
+end
+
+function Player:AddMoney(moneytype, amount, reason)
+ reason = reason or 'unknown'
+ moneytype = moneytype:lower()
+ amount = tonumber(amount)
+ if not amount or amount < 0 then return end
+ if not self.PlayerData.money[moneytype] then return false end
+ self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] + amount
+ if not self.Offline then
+ self:UpdateClient('money', self.PlayerData.money)
+ local logExtra = amount > 100000
+ TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'AddMoney', 'lightgreen',
+ '**' .. self.PlayerData.name ..
+ ' (citizenid: ' .. self.PlayerData.citizenid ..
+ ' | id: ' .. self.PlayerData.source .. ')** $' .. amount ..
+ ' (' .. moneytype .. ') added, new ' .. moneytype ..
+ ' balance: ' .. self.PlayerData.money[moneytype] ..
+ ' reason: ' .. reason, logExtra)
+ TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, false)
+ TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'add', reason)
+ TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'add', reason)
+ end
+ return true
+end
+
+function Player:RemoveMoney(moneytype, amount, reason)
+ reason = reason or 'unknown'
+ moneytype = moneytype:lower()
+ amount = tonumber(amount)
+ if not amount or amount < 0 then return end
+ if not self.PlayerData.money[moneytype] then return false end
+ for _, mtype in pairs(QBCore.Config.Money.DontAllowMinus) do
+ if mtype == moneytype and (self.PlayerData.money[moneytype] - amount) < 0 then
+ return false
end
end
- return nil
+ if self.PlayerData.money[moneytype] - amount < QBCore.Config.Money.MinusLimit then return false end
+ self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] - amount
+ if not self.Offline then
+ self:UpdateClient('money', self.PlayerData.money)
+ local logExtra = amount > 100000
+ TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'RemoveMoney', 'red',
+ '**' .. self.PlayerData.name ..
+ ' (citizenid: ' .. self.PlayerData.citizenid ..
+ ' | id: ' .. self.PlayerData.source .. ')** $' .. amount ..
+ ' (' .. moneytype .. ') removed, new ' .. moneytype ..
+ ' balance: ' .. self.PlayerData.money[moneytype] ..
+ ' reason: ' .. reason, logExtra)
+ TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, true)
+ if moneytype == 'bank' then
+ TriggerClientEvent('qb-phone:client:RemoveBankMoney', self.PlayerData.source, amount)
+ end
+ TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'remove', reason)
+ TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'remove', reason)
+ end
+ return true
+end
+
+function Player:SetMoney(moneytype, amount, reason)
+ reason = reason or 'unknown'
+ moneytype = moneytype:lower()
+ amount = tonumber(amount)
+ if not amount or amount < 0 then return false end
+ if not self.PlayerData.money[moneytype] then return false end
+ local difference = amount - self.PlayerData.money[moneytype]
+ self.PlayerData.money[moneytype] = amount
+ if not self.Offline then
+ self:UpdateClient('money', self.PlayerData.money)
+ TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'SetMoney', 'green',
+ '**' .. self.PlayerData.name ..
+ ' (citizenid: ' .. self.PlayerData.citizenid ..
+ ' | id: ' .. self.PlayerData.source .. ')** $' .. amount ..
+ ' (' .. moneytype .. ') set, new ' .. moneytype ..
+ ' balance: ' .. self.PlayerData.money[moneytype] ..
+ ' reason: ' .. reason)
+ TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, math.abs(difference), difference < 0)
+ TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'set', reason)
+ TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'set', reason)
+ end
+ return true
+end
+
+function Player:GetMoney(moneytype)
+ if not moneytype then return false end
+ return self.PlayerData.money[moneytype:lower()]
+end
+
+function Player:Save()
+ if self.Offline then
+ QBCore.Player.SaveOffline(self.PlayerData)
+ else
+ QBCore.Player.Save(self.PlayerData.source)
+ end
+end
+
+function Player:Logout()
+ if self.Offline then return end
+ QBCore.Player.Logout(self.PlayerData.source)
+end
+
+function Player:AddMethod(methodName, handler)
+ if type(methodName) ~= 'string' or type(handler) ~= 'function' then return false end
+ self[methodName] = handler
+ self.Functions[methodName] = handler
+ return true
+end
+
+function Player:AddField(fieldName, data)
+ if type(fieldName) ~= 'string' or type(data) == 'function' then return false end
+ self[fieldName] = data
+ return true
+end
+
+-- ─────────────────────────── module-private helpers ─────────────────────────
+
+local function decodePlayerFields(PlayerData)
+ PlayerData.money = json.decode(PlayerData.money)
+ PlayerData.job = json.decode(PlayerData.job)
+ PlayerData.gang = json.decode(PlayerData.gang)
+ PlayerData.position = json.decode(PlayerData.position)
+ PlayerData.metadata = json.decode(PlayerData.metadata)
+ PlayerData.charinfo = json.decode(PlayerData.charinfo)
end
local function applyDefaults(playerData, defaults)
@@ -90,61 +311,123 @@ local function applyDefaults(playerData, defaults)
end
end
+-- ─────────────────────────── login / logout ─────────────────────────────────
+
+function QBCore.Player.Login(source, citizenid, newData)
+ if source and source ~= '' then
+ if citizenid then
+ local license = QBCore.Functions.GetIdentifier(source, 'license')
+ local PlayerData = MySQL.prepare.await('SELECT * FROM players where citizenid = ?', { citizenid })
+ if PlayerData and license == PlayerData.license then
+ decodePlayerFields(PlayerData)
+ QBCore.Player.CheckPlayerData(source, PlayerData)
+ else
+ DropPlayer(source, Lang:t('info.exploit_dropped'))
+ TriggerEvent('qb-log:server:CreateLog', 'anticheat', 'Anti-Cheat', 'white', GetPlayerName(source) .. ' Has Been Dropped For Character Joining Exploit', false)
+ return false
+ end
+ else
+ QBCore.Player.CheckPlayerData(source, newData)
+ end
+ return true
+ else
+ QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.LOGIN - NO SOURCE GIVEN!')
+ return false
+ end
+end
+
+function QBCore.Player.Logout(source)
+ local player = QBCore.Players[source]
+ if not player then return end
+ player.Functions.Save()
+ TriggerClientEvent('QBCore:Client:OnPlayerUnload', source)
+ TriggerEvent('QBCore:Server:OnPlayerUnload', source)
+ QBCore.PlayersByCitizenId[player.PlayerData.citizenid] = nil
+ QBCore.Players[source] = nil
+end
+
+-- ─────────────────────────── offline player lookups ─────────────────────────
+
+function QBCore.Player.GetOfflinePlayer(citizenid)
+ if not citizenid then return nil end
+ local PlayerData = MySQL.prepare.await('SELECT * FROM players where citizenid = ?', { citizenid })
+ if PlayerData then
+ decodePlayerFields(PlayerData)
+ return QBCore.Player.CheckPlayerData(nil, PlayerData)
+ end
+ return nil
+end
+
+function QBCore.Player.GetPlayerByLicense(license)
+ if not license then return nil end
+ local source = QBCore.Functions.GetSource(license)
+ if source > 0 then
+ return QBCore.Players[source]
+ else
+ return QBCore.Player.GetOfflinePlayerByLicense(license)
+ end
+end
+
+function QBCore.Player.GetOfflinePlayerByLicense(license)
+ if not license then return nil end
+ local PlayerData = MySQL.prepare.await('SELECT * FROM players where license = ?', { license })
+ if PlayerData then
+ decodePlayerFields(PlayerData)
+ return QBCore.Player.CheckPlayerData(nil, PlayerData)
+ end
+ return nil
+end
+
+-- ─────────────────────────── data validation / construction ─────────────────
+
function QBCore.Player.CheckPlayerData(source, PlayerData)
- PlayerData = PlayerData or {}
+ PlayerData = PlayerData or {}
local Offline = not source
if source then
- PlayerData.source = source
+ PlayerData.source = source
PlayerData.license = PlayerData.license or QBCore.Functions.GetIdentifier(source, 'license')
- PlayerData.name = GetPlayerName(source)
+ PlayerData.name = GetPlayerName(source)
end
local validatedJob = false
if PlayerData.job and PlayerData.job.name ~= nil and PlayerData.job.grade and PlayerData.job.grade.level ~= nil then
local jobInfo = QBCore.Shared.Jobs[PlayerData.job.name]
-
if jobInfo then
local jobGradeInfo = jobInfo.grades[tostring(PlayerData.job.grade.level)]
if jobGradeInfo then
- PlayerData.job.label = jobInfo.label
- PlayerData.job.grade.name = jobGradeInfo.name
- PlayerData.job.payment = jobGradeInfo.payment
- PlayerData.job.grade.isboss = jobGradeInfo.isboss or false
- PlayerData.job.isboss = jobGradeInfo.isboss or false
- validatedJob = true
+ PlayerData.job.label = jobInfo.label
+ PlayerData.job.grade.name = jobGradeInfo.name
+ PlayerData.job.grade.payment = jobGradeInfo.payment
+ PlayerData.job.grade.isboss = jobGradeInfo.isboss or false
+ PlayerData.job.isboss = jobGradeInfo.isboss or false
+ validatedJob = true
end
end
end
- if validatedJob == false then
- -- set to nil, as the default job (unemployed) will be added by `applyDefaults`
- PlayerData.job = nil
- end
+ if not validatedJob then PlayerData.job = nil end
local validatedGang = false
if PlayerData.gang and PlayerData.gang.name ~= nil and PlayerData.gang.grade and PlayerData.gang.grade.level ~= nil then
local gangInfo = QBCore.Shared.Gangs[PlayerData.gang.name]
-
if gangInfo then
local gangGradeInfo = gangInfo.grades[tostring(PlayerData.gang.grade.level)]
if gangGradeInfo then
- PlayerData.gang.label = gangInfo.label
- PlayerData.gang.grade.name = gangGradeInfo.name
- PlayerData.gang.payment = gangGradeInfo.payment
- PlayerData.gang.grade.isboss = gangGradeInfo.isboss or false
- PlayerData.gang.isboss = gangGradeInfo.isboss or false
- validatedGang = true
+ PlayerData.gang.label = gangInfo.label
+ PlayerData.gang.grade.name = gangGradeInfo.name
+ PlayerData.gang.grade.payment = gangGradeInfo.payment
+ PlayerData.gang.grade.isboss = gangGradeInfo.isboss or false
+ PlayerData.gang.isboss = gangGradeInfo.isboss or false
+ validatedGang = true
end
end
end
- if validatedGang == false then
- -- set to nil, as the default gang (unemployed) will be added by `applyDefaults`
- PlayerData.gang = nil
- end
+ if not validatedGang then PlayerData.gang = nil end
applyDefaults(PlayerData, QBCore.Config.Player.PlayerDefaults)
+
if PlayerData.job and QBCore.Shared.ForceJobDefaultDutyAtLogin then
local jobInfo = QBCore.Shared.Jobs[PlayerData.job.name]
if jobInfo then
@@ -152,362 +435,65 @@ function QBCore.Player.CheckPlayerData(source, PlayerData)
end
end
- if GetResourceState('qb-inventory') ~= 'missing' then
+ if not Offline and GetResourceState('qb-inventory') ~= 'missing' then
PlayerData.items = exports['qb-inventory']:LoadInventory(PlayerData.source, PlayerData.citizenid)
end
return QBCore.Player.CreatePlayer(PlayerData, Offline)
end
--- On player logout
-
-function QBCore.Player.Logout(source)
- TriggerClientEvent('QBCore:Client:OnPlayerUnload', source)
- TriggerEvent('QBCore:Server:OnPlayerUnload', source)
- TriggerClientEvent('QBCore:Player:UpdatePlayerData', source)
- Wait(200)
- QBCore.Players[source] = nil
-end
-
--- Create a new character
--- Don't touch any of this unless you know what you are doing
--- Will cause major issues!
-
function QBCore.Player.CreatePlayer(PlayerData, Offline)
- local self = {}
- self.Functions = {}
- self.PlayerData = PlayerData
- self.Offline = Offline
-
- function self.Functions.UpdatePlayerData(key, val)
- if self.Offline then return end
- TriggerEvent('QBCore:Player:SetPlayerData', self.PlayerData)
- if key and val then
- TriggerClientEvent('QBCore:Player:UpdatePlayerDataField', self.PlayerData.source, key, val)
- else
- TriggerClientEvent('QBCore:Player:SetPlayerData', self.PlayerData.source, self.PlayerData)
- end
- end
-
- function self.Functions.SetJob(job, grade)
- job = job:lower()
- grade = grade or '0'
- if not QBCore.Shared.Jobs[job] then return false end
- self.PlayerData.job = {
- name = job,
- label = QBCore.Shared.Jobs[job].label,
- onduty = QBCore.Shared.Jobs[job].defaultDuty,
- type = QBCore.Shared.Jobs[job].type or 'none',
- grade = {
- name = 'No Grades',
- level = 0,
- payment = 30,
- isboss = false
- }
- }
- local gradeKey = tostring(grade)
- local jobGradeInfo = QBCore.Shared.Jobs[job].grades[gradeKey]
- if jobGradeInfo then
- self.PlayerData.job.grade.name = jobGradeInfo.name
- self.PlayerData.job.grade.level = tonumber(gradeKey)
- self.PlayerData.job.grade.payment = jobGradeInfo.payment
- self.PlayerData.job.grade.isboss = jobGradeInfo.isboss or false
- self.PlayerData.job.isboss = jobGradeInfo.isboss or false
- end
-
- if not self.Offline then
- self.Functions.UpdatePlayerData('job', self.PlayerData.job)
- TriggerEvent('QBCore:Server:OnJobUpdate', self.PlayerData.source, self.PlayerData.job)
- TriggerClientEvent('QBCore:Client:OnJobUpdate', self.PlayerData.source, self.PlayerData.job)
- end
-
- return true
- end
-
- function self.Functions.SetGang(gang, grade)
- gang = gang:lower()
- grade = grade or '0'
- if not QBCore.Shared.Gangs[gang] then return false end
- self.PlayerData.gang = {
- name = gang,
- label = QBCore.Shared.Gangs[gang].label,
- grade = {
- name = 'No Grades',
- level = 0,
- isboss = false
- }
- }
- local gradeKey = tostring(grade)
- local gangGradeInfo = QBCore.Shared.Gangs[gang].grades[gradeKey]
- if gangGradeInfo then
- self.PlayerData.gang.grade.name = gangGradeInfo.name
- self.PlayerData.gang.grade.level = tonumber(gradeKey)
- self.PlayerData.gang.grade.isboss = gangGradeInfo.isboss or false
- self.PlayerData.gang.isboss = gangGradeInfo.isboss or false
- end
-
- if not self.Offline then
- self.Functions.UpdatePlayerData('gang', self.PlayerData.gang)
- TriggerEvent('QBCore:Server:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang)
- TriggerClientEvent('QBCore:Client:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang)
- end
-
- return true
- end
-
- function self.Functions.Notify(text, type, length)
- TriggerClientEvent('QBCore:Notify', self.PlayerData.source, text, type, length)
- end
-
- function self.Functions.HasItem(items, amount)
- return QBCore.Functions.HasItem(self.PlayerData.source, items, amount)
- end
-
- function self.Functions.GetName()
- local charinfo = self.PlayerData.charinfo
- return charinfo.firstname .. ' ' .. charinfo.lastname
- end
-
- function self.Functions.SetJobDuty(onDuty)
- self.PlayerData.job.onduty = not not onDuty
- TriggerEvent('QBCore:Server:OnJobUpdate', self.PlayerData.source, self.PlayerData.job)
- TriggerClientEvent('QBCore:Client:OnJobUpdate', self.PlayerData.source, self.PlayerData.job)
- self.Functions.UpdatePlayerData('job', self.PlayerData.job)
- end
-
- function self.Functions.SetPlayerData(key, val)
- if not key or type(key) ~= 'string' then return end
- self.PlayerData[key] = val
- self.Functions.UpdatePlayerData(key, val)
- end
-
- function self.Functions.SetMetaData(meta, val)
- if not meta or type(meta) ~= 'string' then return end
- if meta == 'hunger' or meta == 'thirst' then
- val = val > 100 and 100 or val
- end
- self.PlayerData.metadata[meta] = val
- self.Functions.UpdatePlayerData('metadata', self.PlayerData.metadata)
- end
-
- function self.Functions.GetMetaData(meta)
- if not meta or type(meta) ~= 'string' then return end
- return self.PlayerData.metadata[meta]
- end
-
- function self.Functions.AddRep(rep, amount)
- if not rep or not amount then return end
- local addAmount = tonumber(amount)
- local currentRep = self.PlayerData.metadata['rep'][rep] or 0
- self.PlayerData.metadata['rep'][rep] = currentRep + addAmount
- self.Functions.UpdatePlayerData('metadata', self.PlayerData.metadata)
- end
-
- function self.Functions.RemoveRep(rep, amount)
- if not rep or not amount then return end
- local removeAmount = tonumber(amount)
- local currentRep = self.PlayerData.metadata['rep'][rep] or 0
- if currentRep - removeAmount < 0 then
- self.PlayerData.metadata['rep'][rep] = 0
- else
- self.PlayerData.metadata['rep'][rep] = currentRep - removeAmount
- end
- self.Functions.UpdatePlayerData('metadata', self.PlayerData.metadata)
- end
-
- function self.Functions.GetRep(rep)
- if not rep then return end
- return self.PlayerData.metadata['rep'][rep] or 0
- end
-
- function self.Functions.AddMoney(moneytype, amount, reason)
- reason = reason or 'unknown'
- moneytype = moneytype:lower()
- amount = tonumber(amount)
- if amount < 0 then return end
- if not self.PlayerData.money[moneytype] then return false end
- self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] + amount
-
- if not self.Offline then
- self.Functions.UpdatePlayerData('money', self.PlayerData.money)
- if amount > 100000 then
- TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'AddMoney', 'lightgreen', '**' .. GetPlayerName(self.PlayerData.source) .. ' (citizenid: ' .. self.PlayerData.citizenid .. ' | id: ' .. self.PlayerData.source .. ')** $' .. amount .. ' (' .. moneytype .. ') added, new ' .. moneytype .. ' balance: ' .. self.PlayerData.money[moneytype] .. ' reason: ' .. reason, true)
- else
- TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'AddMoney', 'lightgreen', '**' .. GetPlayerName(self.PlayerData.source) .. ' (citizenid: ' .. self.PlayerData.citizenid .. ' | id: ' .. self.PlayerData.source .. ')** $' .. amount .. ' (' .. moneytype .. ') added, new ' .. moneytype .. ' balance: ' .. self.PlayerData.money[moneytype] .. ' reason: ' .. reason)
- end
- TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, false)
- TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'add', reason)
- TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'add', reason)
- end
-
- return true
- end
-
- function self.Functions.RemoveMoney(moneytype, amount, reason)
- reason = reason or 'unknown'
- moneytype = moneytype:lower()
- amount = tonumber(amount)
- if amount < 0 then return end
- if not self.PlayerData.money[moneytype] then return false end
- for _, mtype in pairs(QBCore.Config.Money.DontAllowMinus) do
- if mtype == moneytype then
- if (self.PlayerData.money[moneytype] - amount) < 0 then
- return false
- end
- end
- end
- if self.PlayerData.money[moneytype] - amount < QBCore.Config.Money.MinusLimit then return false end
- self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] - amount
-
- if not self.Offline then
- self.Functions.UpdatePlayerData('money', self.PlayerData.money)
- if amount > 100000 then
- TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'RemoveMoney', 'red', '**' .. GetPlayerName(self.PlayerData.source) .. ' (citizenid: ' .. self.PlayerData.citizenid .. ' | id: ' .. self.PlayerData.source .. ')** $' .. amount .. ' (' .. moneytype .. ') removed, new ' .. moneytype .. ' balance: ' .. self.PlayerData.money[moneytype] .. ' reason: ' .. reason, true)
- else
- TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'RemoveMoney', 'red', '**' .. GetPlayerName(self.PlayerData.source) .. ' (citizenid: ' .. self.PlayerData.citizenid .. ' | id: ' .. self.PlayerData.source .. ')** $' .. amount .. ' (' .. moneytype .. ') removed, new ' .. moneytype .. ' balance: ' .. self.PlayerData.money[moneytype] .. ' reason: ' .. reason)
- end
- TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, true)
- if moneytype == 'bank' then
- TriggerClientEvent('qb-phone:client:RemoveBankMoney', self.PlayerData.source, amount)
- end
- TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'remove', reason)
- TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'remove', reason)
- end
-
- return true
- end
-
- function self.Functions.SetMoney(moneytype, amount, reason)
- reason = reason or 'unknown'
- moneytype = moneytype:lower()
- amount = tonumber(amount)
- if amount < 0 then return false end
- if not self.PlayerData.money[moneytype] then return false end
- local difference = amount - self.PlayerData.money[moneytype]
- self.PlayerData.money[moneytype] = amount
-
- if not self.Offline then
- self.Functions.UpdatePlayerData('money', self.PlayerData.money)
- TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'SetMoney', 'green', '**' .. GetPlayerName(self.PlayerData.source) .. ' (citizenid: ' .. self.PlayerData.citizenid .. ' | id: ' .. self.PlayerData.source .. ')** $' .. amount .. ' (' .. moneytype .. ') set, new ' .. moneytype .. ' balance: ' .. self.PlayerData.money[moneytype] .. ' reason: ' .. reason)
- TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, math.abs(difference), difference < 0)
- TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'set', reason)
- TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'set', reason)
- end
-
- return true
- end
-
- function self.Functions.GetMoney(moneytype)
- if not moneytype then return false end
- moneytype = moneytype:lower()
- return self.PlayerData.money[moneytype]
- end
-
- function self.Functions.Save()
- if self.Offline then
- QBCore.Player.SaveOffline(self.PlayerData)
- else
- QBCore.Player.Save(self.PlayerData.source)
- end
- end
-
- function self.Functions.Logout()
- if self.Offline then return end
- QBCore.Player.Logout(self.PlayerData.source)
- end
-
- function self.Functions.AddMethod(methodName, handler)
- self.Functions[methodName] = handler
- end
-
- function self.Functions.AddField(fieldName, data)
- self[fieldName] = data
- end
-
- if self.Offline then
- return self
- else
- QBCore.Players[self.PlayerData.source] = self
- QBCore.Player.Save(self.PlayerData.source)
- TriggerEvent('QBCore:Server:PlayerLoaded', self)
- self.Functions.UpdatePlayerData()
- end
+ local player = Player.new(PlayerData, Offline)
+ if Offline then return player end
+ QBCore.Players[PlayerData.source] = player
+ QBCore.PlayersByCitizenId[PlayerData.citizenid] = player
+ QBCore.Player.Save(PlayerData.source)
+ TriggerEvent('QBCore:Server:PlayerLoaded', player)
+ player:UpdateClient()
+ return player
end
--- Add a new function to the Functions table of the player class
--- Use-case:
---[[
- AddEventHandler('QBCore:Server:PlayerLoaded', function(Player)
- QBCore.Functions.AddPlayerMethod(Player.PlayerData.source, "functionName", function(oneArg, orMore)
- -- do something here
- end)
- end)
-]]
+-- ─────────────────────────── runtime extension API ──────────────────────────
+
+local function forEachPlayer(ids, fn)
+ local idType = type(ids)
+ if idType == 'number' then
+ if ids == -1 then
+ for _, v in pairs(QBCore.Players) do fn(v) end
+ elseif QBCore.Players[ids] then
+ fn(QBCore.Players[ids])
+ end
+ elseif idType == 'table' and table.type(ids) == 'array' then
+ for i = 1, #ids do forEachPlayer(ids[i], fn) end
+ end
+end
function QBCore.Functions.AddPlayerMethod(ids, methodName, handler)
- local idType = type(ids)
- if idType == 'number' then
- if ids == -1 then
- for _, v in pairs(QBCore.Players) do
- v.Functions.AddMethod(methodName, handler)
- end
- else
- if not QBCore.Players[ids] then return end
-
- QBCore.Players[ids].Functions.AddMethod(methodName, handler)
- end
- elseif idType == 'table' and table.type(ids) == 'array' then
- for i = 1, #ids do
- QBCore.Functions.AddPlayerMethod(ids[i], methodName, handler)
- end
- end
+ forEachPlayer(ids, function(v) v:AddMethod(methodName, handler) end)
end
--- Add a new field table of the player class
--- Use-case:
---[[
- AddEventHandler('QBCore:Server:PlayerLoaded', function(Player)
- QBCore.Functions.AddPlayerField(Player.PlayerData.source, "fieldName", "fieldData")
- end)
-]]
-
function QBCore.Functions.AddPlayerField(ids, fieldName, data)
- local idType = type(ids)
- if idType == 'number' then
- if ids == -1 then
- for _, v in pairs(QBCore.Players) do
- v.Functions.AddField(fieldName, data)
- end
- else
- if not QBCore.Players[ids] then return end
-
- QBCore.Players[ids].Functions.AddField(fieldName, data)
- end
- elseif idType == 'table' and table.type(ids) == 'array' then
- for i = 1, #ids do
- QBCore.Functions.AddPlayerField(ids[i], fieldName, data)
- end
- end
+ forEachPlayer(ids, function(v) v:AddField(fieldName, data) end)
end
--- Save player info to database (make sure citizenid is the primary key in your database)
+-- ─────────────────────────── save / persistence ─────────────────────────────
function QBCore.Player.Save(source)
- local ped = GetPlayerPed(source)
- local pcoords = GetEntityCoords(ped)
- local PlayerData = QBCore.Players[source].PlayerData
+ local ped = GetPlayerPed(source)
+ local pcoords = GetEntityCoords(ped)
+ local PlayerData = QBCore.Players[source] and QBCore.Players[source].PlayerData
if PlayerData then
MySQL.insert('INSERT INTO players (citizenid, cid, license, name, money, charinfo, job, gang, position, metadata) VALUES (:citizenid, :cid, :license, :name, :money, :charinfo, :job, :gang, :position, :metadata) ON DUPLICATE KEY UPDATE cid = :cid, name = :name, money = :money, charinfo = :charinfo, job = :job, gang = :gang, position = :position, metadata = :metadata', {
citizenid = PlayerData.citizenid,
- cid = tonumber(PlayerData.cid),
- license = PlayerData.license,
- name = PlayerData.name,
- money = json.encode(PlayerData.money),
- charinfo = json.encode(PlayerData.charinfo),
- job = json.encode(PlayerData.job),
- gang = json.encode(PlayerData.gang),
- position = json.encode(pcoords),
- metadata = json.encode(PlayerData.metadata)
+ cid = tonumber(PlayerData.cid),
+ license = PlayerData.license,
+ name = PlayerData.name,
+ money = json.encode(PlayerData.money),
+ charinfo = json.encode(PlayerData.charinfo),
+ job = json.encode(PlayerData.job),
+ gang = json.encode(PlayerData.gang),
+ position = json.encode(pcoords),
+ metadata = json.encode(PlayerData.metadata)
})
if GetResourceState('qb-inventory') ~= 'missing' then exports['qb-inventory']:SaveInventory(source) end
QBCore.ShowSuccess(resourceName, PlayerData.name .. ' PLAYER SAVED!')
@@ -520,15 +506,15 @@ function QBCore.Player.SaveOffline(PlayerData)
if PlayerData then
MySQL.insert('INSERT INTO players (citizenid, cid, license, name, money, charinfo, job, gang, position, metadata) VALUES (:citizenid, :cid, :license, :name, :money, :charinfo, :job, :gang, :position, :metadata) ON DUPLICATE KEY UPDATE cid = :cid, name = :name, money = :money, charinfo = :charinfo, job = :job, gang = :gang, position = :position, metadata = :metadata', {
citizenid = PlayerData.citizenid,
- cid = tonumber(PlayerData.cid),
- license = PlayerData.license,
- name = PlayerData.name,
- money = json.encode(PlayerData.money),
- charinfo = json.encode(PlayerData.charinfo),
- job = json.encode(PlayerData.job),
- gang = json.encode(PlayerData.gang),
- position = json.encode(PlayerData.position),
- metadata = json.encode(PlayerData.metadata)
+ cid = tonumber(PlayerData.cid),
+ license = PlayerData.license,
+ name = PlayerData.name,
+ money = json.encode(PlayerData.money),
+ charinfo = json.encode(PlayerData.charinfo),
+ job = json.encode(PlayerData.job),
+ gang = json.encode(PlayerData.gang),
+ position = json.encode(PlayerData.position),
+ metadata = json.encode(PlayerData.metadata)
})
if GetResourceState('qb-inventory') ~= 'missing' then exports['qb-inventory']:SaveInventory(PlayerData, true) end
QBCore.ShowSuccess(resourceName, PlayerData.name .. ' OFFLINE PLAYER SAVED!')
@@ -537,9 +523,9 @@ function QBCore.Player.SaveOffline(PlayerData)
end
end
--- Delete character
+-- ─────────────────────────── character deletion ─────────────────────────────
-local playertables = { -- Add tables as needed
+local playertables = {
{ table = 'players' },
{ table = 'apartments' },
{ table = 'bank_accounts' },
@@ -551,25 +537,22 @@ local playertables = { -- Add tables as needed
{ table = 'player_houses' },
{ table = 'player_mails' },
{ table = 'player_outfits' },
- { table = 'player_vehicles' }
+ { table = 'player_vehicles' },
}
function QBCore.Player.DeleteCharacter(source, citizenid)
local license = QBCore.Functions.GetIdentifier(source, 'license')
- local result = MySQL.scalar.await('SELECT license FROM players where citizenid = ?', { citizenid })
+ local result = MySQL.scalar.await('SELECT license FROM players where citizenid = ?', { citizenid })
if license == result then
- local query = 'DELETE FROM %s WHERE citizenid = ?'
- local tableCount = #playertables
- local queries = table.create(tableCount, 0)
-
- for i = 1, tableCount do
- local v = playertables[i]
- queries[i] = { query = query:format(v.table), values = { citizenid } }
+ local query = 'DELETE FROM %s WHERE citizenid = ?'
+ local queries = {}
+ for i = 1, #playertables do
+ queries[i] = { query = query:format(playertables[i].table), values = { citizenid } }
end
-
MySQL.transaction(queries, function(result2)
if result2 then
- TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Character Deleted', 'red', '**' .. GetPlayerName(source) .. '** ' .. license .. ' deleted **' .. citizenid .. '**..')
+ TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Character Deleted', 'red',
+ '**' .. GetPlayerName(source) .. '** ' .. license .. ' deleted **' .. citizenid .. '**..')
end
end)
else
@@ -580,97 +563,115 @@ end
function QBCore.Player.ForceDeleteCharacter(citizenid)
local result = MySQL.scalar.await('SELECT license FROM players where citizenid = ?', { citizenid })
- if result then
- local query = 'DELETE FROM %s WHERE citizenid = ?'
- local tableCount = #playertables
- local queries = table.create(tableCount, 0)
- local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
-
- if Player then
- DropPlayer(Player.PlayerData.source, 'An admin deleted the character which you are currently using')
- end
- for i = 1, tableCount do
- local v = playertables[i]
- queries[i] = { query = query:format(v.table), values = { citizenid } }
- end
-
- MySQL.transaction(queries, function(result2)
- if result2 then
- TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Character Force Deleted', 'red', 'Character **' .. citizenid .. '** got deleted')
- end
- end)
+ if not result then return end
+ local query = 'DELETE FROM %s WHERE citizenid = ?'
+ local queries = {}
+ local existing = QBCore.Functions.GetPlayerByCitizenId(citizenid)
+ if existing then
+ DropPlayer(existing.PlayerData.source, 'An admin deleted the character which you are currently using')
end
+ for i = 1, #playertables do
+ queries[i] = { query = query:format(playertables[i].table), values = { citizenid } }
+ end
+ MySQL.transaction(queries, function(result2)
+ if result2 then
+ TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Character Force Deleted', 'red',
+ 'Character **' .. citizenid .. '** got deleted')
+ end
+ end)
end
--- Inventory Backwards Compatibility
+-- ─────────────────────────── unique ID generators ───────────────────────────
-function QBCore.Player.SaveInventory(source)
- if GetResourceState('qb-inventory') == 'missing' then return end
- exports['qb-inventory']:SaveInventory(source, false)
+local function createUniqueId(generator, query, retries)
+ retries = (retries or 0) + 1
+ if retries > 100 then
+ QBCore.ShowError(resourceName, 'createUniqueId: exceeded 100 retries')
+ return nil
+ end
+ local value = generator()
+ local result = MySQL.prepare.await(query, { value })
+ if result == 0 then return value end
+ return createUniqueId(generator, query, retries)
end
-function QBCore.Player.SaveOfflineInventory(PlayerData)
- if GetResourceState('qb-inventory') == 'missing' then return end
- exports['qb-inventory']:SaveInventory(PlayerData, true)
-end
-
-function QBCore.Player.GetTotalWeight(items)
- if GetResourceState('qb-inventory') == 'missing' then return end
- return exports['qb-inventory']:GetTotalWeight(items)
-end
-
-function QBCore.Player.GetSlotsByItem(items, itemName)
- if GetResourceState('qb-inventory') == 'missing' then return end
- return exports['qb-inventory']:GetSlotsByItem(items, itemName)
-end
-
-function QBCore.Player.GetFirstSlotByItem(items, itemName)
- if GetResourceState('qb-inventory') == 'missing' then return end
- return exports['qb-inventory']:GetFirstSlotByItem(items, itemName)
-end
-
--- Util Functions
-
function QBCore.Player.CreateCitizenId()
- local CitizenId = tostring(QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(5)):upper()
- local result = MySQL.prepare.await('SELECT EXISTS(SELECT 1 FROM players WHERE citizenid = ?) AS uniqueCheck', { CitizenId })
- if result == 0 then return CitizenId end
- return QBCore.Player.CreateCitizenId()
+ return createUniqueId(
+ function() return tostring(QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(5)):upper() end,
+ 'SELECT EXISTS(SELECT 1 FROM players WHERE citizenid = ?) AS uniqueCheck'
+ )
end
function QBCore.Functions.CreateAccountNumber()
- local AccountNumber = 'US0' .. math.random(1, 9) .. 'QBCore' .. math.random(1111, 9999) .. math.random(1111, 9999) .. math.random(11, 99)
- local result = MySQL.prepare.await('SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(charinfo, "$.account")) = ?) AS uniqueCheck', { AccountNumber })
- if result == 0 then return AccountNumber end
- return QBCore.Functions.CreateAccountNumber()
+ return createUniqueId(
+ function() return 'US0' .. math.random(1, 9) .. 'QBCore' .. math.random(1111, 9999) .. math.random(1111, 9999) .. math.random(11, 99) end,
+ 'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(charinfo, "$.account")) = ?) AS uniqueCheck'
+ )
end
function QBCore.Functions.CreatePhoneNumber()
- local PhoneNumber = math.random(100, 999) .. math.random(1000000, 9999999)
- local result = MySQL.prepare.await('SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(charinfo, "$.phone")) = ?) AS uniqueCheck', { PhoneNumber })
- if result == 0 then return PhoneNumber end
- return QBCore.Functions.CreatePhoneNumber()
+ return createUniqueId(
+ function() return math.random(100, 999) .. math.random(1000000, 9999999) end,
+ 'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(charinfo, "$.phone")) = ?) AS uniqueCheck'
+ )
end
function QBCore.Player.CreateFingerId()
- local FingerId = tostring(QBCore.Shared.RandomStr(2) .. QBCore.Shared.RandomInt(3) .. QBCore.Shared.RandomStr(1) .. QBCore.Shared.RandomInt(2) .. QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(4))
- local result = MySQL.prepare.await('SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.fingerprint")) = ?) AS uniqueCheck', { FingerId })
- if result == 0 then return FingerId end
- return QBCore.Player.CreateFingerId()
+ return createUniqueId(
+ function() return tostring(QBCore.Shared.RandomStr(2) .. QBCore.Shared.RandomInt(3) .. QBCore.Shared.RandomStr(1) .. QBCore.Shared.RandomInt(2) .. QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(4)) end,
+ 'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.fingerprint")) = ?) AS uniqueCheck'
+ )
end
function QBCore.Player.CreateWalletId()
- local WalletId = 'QB-' .. math.random(11111111, 99999999)
- local result = MySQL.prepare.await('SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.walletid")) = ?) AS uniqueCheck', { WalletId })
- if result == 0 then return WalletId end
- return QBCore.Player.CreateWalletId()
+ return createUniqueId(
+ function() return 'QB-' .. math.random(11111111, 99999999) end,
+ 'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.walletid")) = ?) AS uniqueCheck'
+ )
end
function QBCore.Player.CreateSerialNumber()
- local SerialNumber = math.random(11111111, 99999999)
- local result = MySQL.prepare.await('SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.phonedata.SerialNumber")) = ?) AS uniqueCheck', { SerialNumber })
- if result == 0 then return SerialNumber end
- return QBCore.Player.CreateSerialNumber()
+ return createUniqueId(
+ function() return math.random(11111111, 99999999) end,
+ 'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.phonedata.SerialNumber")) = ?) AS uniqueCheck'
+ )
end
-PaycheckInterval() -- This starts the paycheck system
+-- ─────────────────────────── export bridge ──────────────────────────────────
+
+local function buildInterface(internalPlayer)
+ if not internalPlayer then return nil end
+ local iface = buildMethodTable(internalPlayer)
+ iface.PlayerData = internalPlayer.PlayerData
+ return iface
+end
+
+exports('GetPlayer', function(source)
+ return buildInterface(QBCore.Players[tonumber(source)])
+end)
+
+exports('GetPlayerByCitizenId', function(citizenid)
+ return buildInterface(QBCore.PlayersByCitizenId[citizenid])
+end)
+
+exports('GetOfflinePlayerByCitizenId', function(citizenid)
+ return buildInterface(QBCore.Player.GetOfflinePlayer(citizenid))
+end)
+
+exports('GetPlayerByLicense', function(license)
+ return buildInterface(QBCore.Player.GetPlayerByLicense(license))
+end)
+
+exports('GetOfflinePlayerByLicense', function(license)
+ return buildInterface(QBCore.Player.GetOfflinePlayerByLicense(license))
+end)
+
+exports('AddPlayerMethod', function(ids, methodName, handler)
+ QBCore.Functions.AddPlayerMethod(ids, methodName, handler)
+end)
+
+exports('AddPlayerField', function(ids, fieldName, data)
+ QBCore.Functions.AddPlayerField(ids, fieldName, data)
+end)
+
+PaycheckInterval()
diff --git a/shared/functions.lua b/shared/functions.lua
new file mode 100644
index 0000000..b970b42
--- /dev/null
+++ b/shared/functions.lua
@@ -0,0 +1,172 @@
+local StringCharset = {}
+local NumberCharset = {}
+
+QBCore.Shared.StarterItems = {
+ ['phone'] = { amount = 1, item = 'phone' },
+ ['id_card'] = { amount = 1, item = 'id_card' },
+ ['driver_license'] = { amount = 1, item = 'driver_license' },
+}
+
+for i = 48, 57 do NumberCharset[#NumberCharset + 1] = string.char(i) end
+for i = 65, 90 do StringCharset[#StringCharset + 1] = string.char(i) end
+for i = 97, 122 do StringCharset[#StringCharset + 1] = string.char(i) end
+
+function QBCore.Shared.RandomStr(length)
+ if length <= 0 then return '' end
+ return QBCore.Shared.RandomStr(length - 1) .. StringCharset[math.random(1, #StringCharset)]
+end
+
+function QBCore.Shared.RandomInt(length)
+ if length <= 0 then return '' end
+ return QBCore.Shared.RandomInt(length - 1) .. NumberCharset[math.random(1, #NumberCharset)]
+end
+
+function QBCore.Shared.SplitStr(str, delimiter)
+ local result = {}
+ local from = 1
+ local delim_from, delim_to = string.find(str, delimiter, from)
+ while delim_from do
+ result[#result + 1] = string.sub(str, from, delim_from - 1)
+ from = delim_to + 1
+ delim_from, delim_to = string.find(str, delimiter, from)
+ end
+ result[#result + 1] = string.sub(str, from)
+ return result
+end
+
+function QBCore.Shared.Trim(value)
+ if not value then return nil end
+ return (string.gsub(value, '^%s*(.-)%s*$', '%1'))
+end
+
+function QBCore.Shared.FirstToUpper(value)
+ if not value then return nil end
+ return (value:gsub('^%l', string.upper))
+end
+
+function QBCore.Shared.Round(value, numDecimalPlaces)
+ if not numDecimalPlaces then return math.floor(value + 0.5) end
+ local power = 10 ^ numDecimalPlaces
+ return math.floor((value * power) + 0.5) / (power)
+end
+
+function QBCore.Shared.ChangeVehicleExtra(vehicle, extra, enable)
+ if DoesExtraExist(vehicle, extra) then
+ if enable then
+ SetVehicleExtra(vehicle, extra, false)
+ if not IsVehicleExtraTurnedOn(vehicle, extra) then
+ QBCore.Shared.ChangeVehicleExtra(vehicle, extra, enable)
+ end
+ else
+ SetVehicleExtra(vehicle, extra, true)
+ if IsVehicleExtraTurnedOn(vehicle, extra) then
+ QBCore.Shared.ChangeVehicleExtra(vehicle, extra, enable)
+ end
+ end
+ end
+end
+
+function QBCore.Shared.IsFunction(value)
+ if type(value) == 'table' then
+ return value.__cfx_functionReference ~= nil and type(value.__cfx_functionReference) == 'string'
+ end
+
+ return type(value) == 'function'
+end
+
+function QBCore.Shared.SetDefaultVehicleExtras(vehicle, config)
+ for i = 1, 20 do
+ if DoesExtraExist(vehicle, i) then
+ SetVehicleExtra(vehicle, i, 1)
+ end
+ end
+
+ for id, enabled in pairs(config) do
+ if type(enabled) ~= 'boolean' then
+ enabled = true
+ end
+
+ QBCore.Shared.ChangeVehicleExtra(vehicle, tonumber(id), enabled)
+ end
+end
+
+QBCore.Shared.MaleNoGloves = {
+ [0] = true,
+ [1] = true,
+ [2] = true,
+ [3] = true,
+ [4] = true,
+ [5] = true,
+ [6] = true,
+ [7] = true,
+ [8] = true,
+ [9] = true,
+ [10] = true,
+ [11] = true,
+ [12] = true,
+ [13] = true,
+ [14] = true,
+ [15] = true,
+ [18] = true,
+ [26] = true,
+ [52] = true,
+ [53] = true,
+ [54] = true,
+ [55] = true,
+ [56] = true,
+ [57] = true,
+ [58] = true,
+ [59] = true,
+ [60] = true,
+ [61] = true,
+ [62] = true,
+ [112] = true,
+ [113] = true,
+ [114] = true,
+ [118] = true,
+ [125] = true,
+ [132] = true
+}
+
+QBCore.Shared.FemaleNoGloves = {
+ [0] = true,
+ [1] = true,
+ [2] = true,
+ [3] = true,
+ [4] = true,
+ [5] = true,
+ [6] = true,
+ [7] = true,
+ [8] = true,
+ [9] = true,
+ [10] = true,
+ [11] = true,
+ [12] = true,
+ [13] = true,
+ [14] = true,
+ [15] = true,
+ [19] = true,
+ [59] = true,
+ [60] = true,
+ [61] = true,
+ [62] = true,
+ [63] = true,
+ [64] = true,
+ [65] = true,
+ [66] = true,
+ [67] = true,
+ [68] = true,
+ [69] = true,
+ [70] = true,
+ [71] = true,
+ [129] = true,
+ [130] = true,
+ [131] = true,
+ [135] = true,
+ [142] = true,
+ [149] = true,
+ [153] = true,
+ [157] = true,
+ [161] = true,
+ [165] = true
+}
diff --git a/shared/gangs.lua b/shared/gangs.lua
index 70dc5a9..a006047 100644
--- a/shared/gangs.lua
+++ b/shared/gangs.lua
@@ -1,5 +1,4 @@
-QBShared = QBShared or {}
-QBShared.Gangs = {
+QBCore.Shared.Gangs = {
none = { label = 'No Gang', grades = { ['0'] = { name = 'Unaffiliated' } } },
lostmc = {
label = 'The Lost MC',
diff --git a/shared/items.lua b/shared/items.lua
index 6164389..d829235 100644
--- a/shared/items.lua
+++ b/shared/items.lua
@@ -1,5 +1,4 @@
-QBShared = QBShared or {}
-QBShared.Items = {
+QBCore.Shared.Items = {
-- WEAPONS
-- Melee
weapon_unarmed = { name = 'weapon_unarmed', label = 'Fists', weight = 1000, type = 'weapon', ammotype = nil, image = 'placeholder.png', unique = true, useable = false, description = 'Fisticuffs' },
diff --git a/shared/jobs.lua b/shared/jobs.lua
index 8575b04..b8c8e3d 100644
--- a/shared/jobs.lua
+++ b/shared/jobs.lua
@@ -1,6 +1,5 @@
-QBShared = QBShared or {}
-QBShared.ForceJobDefaultDutyAtLogin = true -- true: Force duty state to jobdefaultDuty | false: set duty state from database last saved
-QBShared.Jobs = {
+QBCore.Shared.ForceJobDefaultDutyAtLogin = true -- true: Force duty state to jobdefaultDuty | false: set duty state from database last saved
+QBCore.Shared.Jobs = {
unemployed = { label = 'Civilian', defaultDuty = true, offDutyPay = false, grades = { ['0'] = { name = 'Freelancer', payment = 10 } } },
bus = { label = 'Bus', defaultDuty = true, offDutyPay = false, grades = { ['0'] = { name = 'Driver', payment = 50 } } },
judge = { label = 'Honorary', defaultDuty = true, offDutyPay = false, grades = { ['0'] = { name = 'Judge', payment = 100 } } },
diff --git a/shared/locations.lua b/shared/locations.lua
index 3582619..26653f5 100644
--- a/shared/locations.lua
+++ b/shared/locations.lua
@@ -1,4 +1,4 @@
-QBShared.Locations = {
+QBCore.Shared.Locations = {
-- Base game MLO interiors
aircraft_carrier_int = vector4(3081.0042, -4693.6875, 15.2623, 76.8169),
apt_1_int = vector4(-1452.4602, -540.2056, 74.0443, 31.9129),
diff --git a/shared/main.lua b/shared/main.lua
index b92ba6f..a883688 100644
--- a/shared/main.lua
+++ b/shared/main.lua
@@ -1,185 +1,28 @@
-QBShared = QBShared or {}
+QBCore.Shared = {}
+QBCore.ClientCallbacks = {}
+QBCore.ServerCallbacks = {}
-local StringCharset = {}
-local NumberCharset = {}
-
-QBShared.StarterItems = {
- ['phone'] = { amount = 1, item = 'phone' },
- ['id_card'] = { amount = 1, item = 'id_card' },
- ['driver_license'] = { amount = 1, item = 'driver_license' },
-}
-
-for i = 48, 57 do NumberCharset[#NumberCharset + 1] = string.char(i) end
-for i = 65, 90 do StringCharset[#StringCharset + 1] = string.char(i) end
-for i = 97, 122 do StringCharset[#StringCharset + 1] = string.char(i) end
-
-function QBShared.RandomStr(length)
- if length <= 0 then return '' end
- return QBShared.RandomStr(length - 1) .. StringCharset[math.random(1, #StringCharset)]
-end
-
-function QBShared.RandomInt(length)
- if length <= 0 then return '' end
- return QBShared.RandomInt(length - 1) .. NumberCharset[math.random(1, #NumberCharset)]
-end
-
-function QBShared.SplitStr(str, delimiter)
- local result = {}
- local from = 1
- local delim_from, delim_to = string.find(str, delimiter, from)
- while delim_from do
- result[#result + 1] = string.sub(str, from, delim_from - 1)
- from = delim_to + 1
- delim_from, delim_to = string.find(str, delimiter, from)
- end
- result[#result + 1] = string.sub(str, from)
- return result
-end
-
-function QBShared.Trim(value)
- if not value then return nil end
- return (string.gsub(value, '^%s*(.-)%s*$', '%1'))
-end
-
-function QBShared.FirstToUpper(value)
- if not value then return nil end
- return (value:gsub("^%l", string.upper))
-end
-
-function QBShared.Round(value, numDecimalPlaces)
- if not numDecimalPlaces then return math.floor(value + 0.5) end
- local power = 10 ^ numDecimalPlaces
- return math.floor((value * power) + 0.5) / (power)
-end
-
-function QBShared.ChangeVehicleExtra(vehicle, extra, enable)
- if DoesExtraExist(vehicle, extra) then
- if enable then
- SetVehicleExtra(vehicle, extra, false)
- if not IsVehicleExtraTurnedOn(vehicle, extra) then
- QBShared.ChangeVehicleExtra(vehicle, extra, enable)
- end
- else
- SetVehicleExtra(vehicle, extra, true)
- if IsVehicleExtraTurnedOn(vehicle, extra) then
- QBShared.ChangeVehicleExtra(vehicle, extra, enable)
- end
+--- @param filters ('Config'|'Shared'|'ClientCallbacks'|'ServerCallbacks'|'PlayerData'|'Functions'|'Players'|'PlayersByCitizenId'|'Player_Buckets'|'Entity_Buckets'|'UsableItems'|'Commands')[]?
+--- @return table
+local function GetCoreObject(filters)
+ if not filters then return QBCore end
+ local results = {}
+ for i = 1, #filters do
+ local key = filters[i]
+ if QBCore[key] then
+ results[key] = QBCore[key]
end
end
+ return results
end
+exports('GetCoreObject', GetCoreObject)
-function QBShared.IsFunction(value)
- if type(value) == 'table' then
- return value.__cfx_functionReference ~= nil and type(value.__cfx_functionReference) == "string"
- end
-
- return type(value) == 'function'
-end
-
-function QBShared.SetDefaultVehicleExtras(vehicle, config)
- -- Clear Extras
- for i = 1, 20 do
- if DoesExtraExist(vehicle, i) then
- SetVehicleExtra(vehicle, i, 1)
- end
- end
-
- for id, enabled in pairs(config) do
- if type(enabled) ~= 'boolean' then
- enabled = true
- end
-
- QBShared.ChangeVehicleExtra(vehicle, tonumber(id), enabled)
- end
-end
-
-QBShared.MaleNoGloves = {
- [0] = true,
- [1] = true,
- [2] = true,
- [3] = true,
- [4] = true,
- [5] = true,
- [6] = true,
- [7] = true,
- [8] = true,
- [9] = true,
- [10] = true,
- [11] = true,
- [12] = true,
- [13] = true,
- [14] = true,
- [15] = true,
- [18] = true,
- [26] = true,
- [52] = true,
- [53] = true,
- [54] = true,
- [55] = true,
- [56] = true,
- [57] = true,
- [58] = true,
- [59] = true,
- [60] = true,
- [61] = true,
- [62] = true,
- [112] = true,
- [113] = true,
- [114] = true,
- [118] = true,
- [125] = true,
- [132] = true
-}
-
-QBShared.FemaleNoGloves = {
- [0] = true,
- [1] = true,
- [2] = true,
- [3] = true,
- [4] = true,
- [5] = true,
- [6] = true,
- [7] = true,
- [8] = true,
- [9] = true,
- [10] = true,
- [11] = true,
- [12] = true,
- [13] = true,
- [14] = true,
- [15] = true,
- [19] = true,
- [59] = true,
- [60] = true,
- [61] = true,
- [62] = true,
- [63] = true,
- [64] = true,
- [65] = true,
- [66] = true,
- [67] = true,
- [68] = true,
- [69] = true,
- [70] = true,
- [71] = true,
- [129] = true,
- [130] = true,
- [131] = true,
- [135] = true,
- [142] = true,
- [149] = true,
- [153] = true,
- [157] = true,
- [161] = true,
- [165] = true
-}
-
---- Get a shared item from a shared field
--- @param namespace 'Vehicles' | 'VehicleHashes' | 'Items' | 'Gangs' | 'Jobs' | 'Locations' | 'Weapons'
--- @param item string
--- @return table
function GetShared(namespace, item)
- return QBCore.Shared[namespace]?[item]
+ local ns = QBCore.Shared[namespace]
+ return ns and ns[item]
end
-exports('GetShared', GetShared)
\ No newline at end of file
+exports('GetShared', GetShared)
diff --git a/shared/vehicles.lua b/shared/vehicles.lua
index b745c09..7b257fe 100644
--- a/shared/vehicles.lua
+++ b/shared/vehicles.lua
@@ -1,5 +1,4 @@
-QBShared = QBShared or {}
-QBShared.Vehicles = QBShared.Vehicles or {}
+QBCore.Shared.Vehicles = QBCore.Shared.Vehicles or {}
local Vehicles = {
--- Compacts (0)
@@ -765,10 +764,10 @@ local Vehicles = {
{ model = 'formula', name = 'PR4', brand = 'Progen', price = 100000, category = 'openwheel', type = 'automobile', shop = 'none' },
}
-QBShared.VehicleHashes = QBShared.VehicleHashes or {}
+QBCore.Shared.VehicleHashes = QBCore.Shared.VehicleHashes or {}
for i = 1, #Vehicles do
local hash = joaat(Vehicles[i].model)
- QBShared.Vehicles[Vehicles[i].model] = {
+ QBCore.Shared.Vehicles[Vehicles[i].model] = {
spawncode = Vehicles[i].model,
name = Vehicles[i].name,
brand = Vehicles[i].brand,
@@ -779,5 +778,5 @@ for i = 1, #Vehicles do
type = Vehicles[i].type,
shop = Vehicles[i].shop
}
- QBShared.VehicleHashes[hash] = QBShared.Vehicles[Vehicles[i].model]
+ QBCore.Shared.VehicleHashes[hash] = QBCore.Shared.Vehicles[Vehicles[i].model]
end
diff --git a/shared/weapons.lua b/shared/weapons.lua
index 823dda8..f8a32d3 100644
--- a/shared/weapons.lua
+++ b/shared/weapons.lua
@@ -1,5 +1,4 @@
-QBShared = QBShared or {}
-QBShared.Weapons = {
+QBCore.Shared.Weapons = {
-- // WEAPONS
-- Melee
[`weapon_unarmed`] = { name = 'weapon_unarmed', label = 'Fists', weapontype = 'Melee', ammotype = nil, damagereason = 'Melee killed / Whacked / Executed / Beat down / Murdered / Battered' },