From 74ac036d4ab97203ed824d0aaf70b2b3909bcef1 Mon Sep 17 00:00:00 2001 From: Kakarot Date: Tue, 19 May 2026 18:13:35 -0500 Subject: [PATCH] Refactor core structure and notification UI Major refactor to centralize QBCore and replace the web UI stack: - Move QBConfig into a unified QBCore.Config and initialize QBCore.PlayerData; add shared/functions.lua. - Remove legacy client/main.lua and server/main.lua exports and update fxmanifest to include shared/functions and drop those main scripts. - Update client event handling: adapt player data events to QBCore:Client:OnPlayerUpdated and trigger job/gang update events; remove deprecated exploitable handlers. - Web UI rewrite: remove Vue/Quasar dependency and config module; implement a lightweight vanilla JS notification system (html/js/app.js) with updated html and css assets, plus drawtext tweaks. - CSS overhaul for notification styling and animations; HTML head/meta/font cleanup. - Server fixes: use QBCore.Shared.Locations in teleport command and strengthen numeric parsing/validation for coords. - Add .gitattributes for consistent text/eol handling and small code cleanups (thread/comment removal). These changes consolidate config/shared logic under QBCore, simplify the frontend dependencies, and fix a few parsing/event issues. --- .gitattributes | 11 + client/drawtext.lua | 2 +- client/events.lua | 29 +- client/functions.lua | 1 + client/main.lua | 50 -- config.lua | 69 +-- fxmanifest.lua | 3 +- html/css/drawtext.css | 33 +- html/css/style.css | 210 ++++----- html/index.html | 16 +- html/js/app.js | 153 ++++-- html/js/config.js | 53 --- locale/th.lua | 258 +++++------ server/commands.lua | 22 +- server/debug.lua | 30 +- server/events.lua | 73 +-- server/exports.lua | 90 +--- server/functions.lua | 90 ++-- server/main.lua | 49 -- server/player.lua | 1031 +++++++++++++++++++++-------------------- shared/functions.lua | 172 +++++++ shared/gangs.lua | 3 +- shared/items.lua | 3 +- shared/jobs.lua | 5 +- shared/locations.lua | 2 +- shared/main.lua | 191 +------- shared/vehicles.lua | 9 +- shared/weapons.lua | 3 +- 28 files changed, 1242 insertions(+), 1419 deletions(-) create mode 100644 .gitattributes delete mode 100644 client/main.lua delete mode 100644 html/js/config.js delete mode 100644 server/main.lua create mode 100644 shared/functions.lua 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' },