Upload files to "server"

This commit is contained in:
tanthius 2026-02-12 04:17:48 +00:00
parent 964a6fc764
commit 1ece4a3f2f
5 changed files with 901 additions and 0 deletions

88
server/appearance.lua Normal file
View File

@ -0,0 +1,88 @@
-- server/appearance.lua
print("^2[turfwar]^7 appearance.lua loaded (server)")
local function getLicense(src)
for _, id in ipairs(GetPlayerIdentifiers(src)) do
if id:sub(1, 8) == "license:" then
return id
end
end
return nil
end
local function defaultRow(license)
return {
license = license,
gender = "m",
hair_drawable = 0,
hair_texture = 0,
hair_color = 0,
hair_highlight = 0
}
end
RegisterNetEvent("turfwar:appearance:request", function()
local src = source
local license = getLicense(src)
if not license then
print(("^1[turfwar]^7 appearance: missing license for src %d"):format(src))
return
end
local row = exports.oxmysql:singleSync(
"SELECT gender, hair_drawable, hair_texture, hair_color, hair_highlight FROM turfwar_appearance WHERE license = ?",
{ license }
)
if not row then
local def = defaultRow(license)
exports.oxmysql:insertSync(
"INSERT INTO turfwar_appearance (license, gender, hair_drawable, hair_texture, hair_color, hair_highlight) VALUES (?, ?, ?, ?, ?, ?)",
{ def.license, def.gender, def.hair_drawable, def.hair_texture, def.hair_color, def.hair_highlight }
)
row = def
end
TriggerClientEvent("turfwar:appearance:update", src, row)
end)
RegisterNetEvent("turfwar:appearance:setGender", function(gender)
local src = source
local license = getLicense(src)
if not license then return end
gender = (gender == "f") and "f" or "m"
exports.oxmysql:updateSync(
"UPDATE turfwar_appearance SET gender = ? WHERE license = ?",
{ gender, license }
)
local row = exports.oxmysql:singleSync(
"SELECT gender, hair_drawable, hair_texture, hair_color, hair_highlight FROM turfwar_appearance WHERE license = ?",
{ license }
)
TriggerClientEvent("turfwar:appearance:update", src, row)
end)
RegisterNetEvent("turfwar:appearance:setHair", function(drawable, texture, color, highlight)
local src = source
local license = getLicense(src)
if not license then return end
drawable = tonumber(drawable) or 0
texture = tonumber(texture) or 0
color = tonumber(color) or 0
highlight = tonumber(highlight) or color
exports.oxmysql:updateSync(
"UPDATE turfwar_appearance SET hair_drawable=?, hair_texture=?, hair_color=?, hair_highlight=? WHERE license=?",
{ drawable, texture, color, highlight, license }
)
local row = exports.oxmysql:singleSync(
"SELECT gender, hair_drawable, hair_texture, hair_color, hair_highlight FROM turfwar_appearance WHERE license = ?",
{ license }
)
TriggerClientEvent("turfwar:appearance:update", src, row)
end)

104
server/atm_access.lua Normal file
View File

@ -0,0 +1,104 @@
-- server/atm_access.lua
print("^2[turfwar]^7 atm_access.lua loaded (server-side gatekeeper)")
Config = Config or {}
Config.Finance = Config.Finance or {}
local USE_RADIUS = tonumber(Config.Finance.ATM_USE_RADIUS) or 1.8
local TTL_MS = tonumber(Config.Finance.ATM_SESSION_TTL_MS) or 15000
local sessions = {}
local function dist3(a, b)
local dx = (a.x - b.x)
local dy = (a.y - b.y)
local dz = (a.z - b.z)
return math.sqrt(dx*dx + dy*dy + dz*dz)
end
local function newToken()
return ("%08x%08x"):format(math.random(0, 0xffffffff), math.random(0, 0xffffffff))
end
local function getPlayerCoords(src)
local ped = GetPlayerPed(src)
if not ped or ped == 0 then return nil end
local c = GetEntityCoords(ped)
return { x = c.x, y = c.y, z = c.z }
end
RegisterNetEvent("turfwar:atm:open", function(payload)
local src = source
if type(payload) ~= "table" then
TriggerClientEvent("turfwar:atm:openDenied", src, "bad_payload")
return
end
local x, y, z = tonumber(payload.x), tonumber(payload.y), tonumber(payload.z)
local model = payload.model
if not x or not y or not z then
TriggerClientEvent("turfwar:atm:openDenied", src, "bad_coords")
return
end
local okModel = false
for _, m in ipairs(Config.Finance.ATM_MODELS or {}) do
if m == model then okModel = true break end
end
if not okModel then
TriggerClientEvent("turfwar:atm:openDenied", src, "bad_model")
return
end
local pc = getPlayerCoords(src)
if not pc then
TriggerClientEvent("turfwar:atm:openDenied", src, "no_ped")
return
end
local atmC = { x = x, y = y, z = z }
if dist3(pc, atmC) > (USE_RADIUS + 0.75) then
TriggerClientEvent("turfwar:atm:openDenied", src, "too_far")
return
end
local token = newToken()
sessions[src] = {
token = token,
expires = GetGameTimer() + TTL_MS,
atm = { x = x, y = y, z = z, model = model }
}
TriggerClientEvent("turfwar:atm:openGranted", src, token)
end)
local function RequireATM(src, token, verbose)
local s = sessions[src]
if not s then return false, "no_session" end
if token ~= s.token then return false, "bad_token" end
if GetGameTimer() > (s.expires or 0) then
sessions[src] = nil
return false, "expired"
end
local pc = getPlayerCoords(src)
if not pc then return false, "no_ped" end
local atmC = { x = s.atm.x, y = s.atm.y, z = s.atm.z }
if dist3(pc, atmC) > (USE_RADIUS + 0.75) then
if verbose then
print(("^1[ATM BLOCK]^7 src=%d moved away from ATM"):format(src))
end
sessions[src] = nil
return false, "moved_away"
end
return true
end
exports("RequireATM", RequireATM)
AddEventHandler("playerDropped", function()
sessions[source] = nil
end)

304
server/cash.lua Normal file
View File

@ -0,0 +1,304 @@
-- server/cash.lua
print("^2[turfwar]^7 cash.lua loaded")
Cash = {}
local cache = {}
local function PushMoney(src)
TriggerClientEvent('turfwar:money:update', src, Cash.Get(src), Cash.GetBank(src))
end
-- Safe notify helper (won't crash if you don't have turfwar:notify hooked)
local function Notify(src, msg)
TriggerClientEvent('chat:addMessage', src, { args = { "ATM", tostring(msg) } })
end
local function getIdentifier(src)
local ids = GetPlayerIdentifiers(src)
if not ids or #ids == 0 then return nil end
-- Prefer license:
for _, id in ipairs(ids) do
if id:sub(1, 8) == "license:" then
return id
end
end
-- Fallback (steam/discord/etc.)
return ids[1]
end
local function ensureLoaded(src)
local identifier = getIdentifier(src)
if not identifier then return nil end
if not cache[identifier] then
Cash.Load(src)
end
return identifier
end
function Cash.Load(src)
local identifier = getIdentifier(src)
if not identifier then return end
local rows = MySQL.query.await(
'SELECT cash, bank FROM player_money WHERE identifier = ?',
{ identifier }
)
local row = (rows and rows[1]) or nil
if not row then
MySQL.insert.await(
'INSERT INTO player_money (identifier, cash, bank) VALUES (?, 0, 0)',
{ identifier }
)
row = { cash = 0, bank = 0 }
end
cache[identifier] = {
cash = tonumber(row.cash) or 0,
bank = tonumber(row.bank) or 0
}
PushMoney(src)
end
function Cash.Get(src)
local identifier = ensureLoaded(src)
if not identifier then return 0 end
return cache[identifier].cash or 0
end
function Cash.GetBank(src)
local identifier = ensureLoaded(src)
if not identifier then return 0 end
return cache[identifier].bank or 0
end
function Cash.Add(src, amount)
amount = tonumber(amount) or 0
if amount == 0 then return end
local identifier = ensureLoaded(src)
if not identifier then return end
local data = cache[identifier]
data.cash = math.max(0, (data.cash or 0) + amount)
MySQL.update(
'UPDATE player_money SET cash = ? WHERE identifier = ?',
{ data.cash, identifier }
)
PushMoney(src)
end
function Cash.Remove(src, amount)
Cash.Add(src, -(tonumber(amount) or 0))
end
function Cash.Set(src, amount)
amount = math.max(0, tonumber(amount) or 0)
local identifier = ensureLoaded(src)
if not identifier then return end
cache[identifier].cash = amount
MySQL.update(
'UPDATE player_money SET cash = ? WHERE identifier = ?',
{ amount, identifier }
)
PushMoney(src)
end
function Cash.SetBank(src, amount)
amount = math.max(0, tonumber(amount) or 0)
local identifier = ensureLoaded(src)
if not identifier then return end
cache[identifier].bank = amount
MySQL.update(
'UPDATE player_money SET bank = ? WHERE identifier = ?',
{ amount, identifier }
)
PushMoney(src)
end
AddEventHandler('playerJoining', function()
Cash.Load(source)
end)
AddEventHandler('playerDropped', function()
local src = source
local identifier = getIdentifier(src)
if identifier and cache[identifier] then
local data = cache[identifier]
MySQL.update(
'UPDATE player_money SET cash = ?, bank = ? WHERE identifier = ?',
{ data.cash or 0, data.bank or 0, identifier }
)
cache[identifier] = nil
end
end)
RegisterNetEvent('turfwar:money:request', function()
local src = source
Cash.Load(src)
PushMoney(src)
end)
-- =========================================================
-- Ped cash drops -> claim once -> add to player's cash
-- =========================================================
local CashDrops = {}
local NextDropId = 1
RegisterNetEvent("environment:pedCashDrop", function(amount, coords)
amount = tonumber(amount) or 0
if amount < 1 or amount > 1000 then return end
if type(coords) ~= "table" or coords.x == nil or coords.y == nil or coords.z == nil then return end
local dropId = NextDropId
NextDropId = NextDropId + 1
CashDrops[dropId] = {
amount = amount,
taken = false,
createdAt = os.time()
}
TriggerClientEvent("environment:spawnCashPickup", -1, dropId, amount, coords)
SetTimeout(5 * 60 * 1000, function()
if CashDrops[dropId] and not CashDrops[dropId].taken then
CashDrops[dropId] = nil
TriggerClientEvent("environment:removeCashPickup", -1, dropId)
end
end)
end)
RegisterNetEvent("environment:pickupCash", function(dropId)
local src = source
dropId = tonumber(dropId)
if not dropId then return end
local drop = CashDrops and CashDrops[dropId]
if not drop or drop.taken then return end
drop.taken = true
local amount = tonumber(drop.amount) or 0
CashDrops[dropId] = nil
if amount > 0 then
Cash.Add(src, amount)
end
TriggerClientEvent("environment:removeCashPickup", -1, dropId)
TriggerClientEvent("environment:pickupFeedback", src, amount)
end)
-- =========================================================
-- ATM-Gated Bank Transactions (server authoritative)
-- Requires: server/atm_access.lua exporting RequireATM
-- =========================================================
local function RequireATM(src, token)
return exports[GetCurrentResourceName()]:RequireATM(src, token, true)
end
local function clampAmount(x)
x = tonumber(x)
if not x then return nil end
x = math.floor(x)
if x <= 0 then return nil end
return x
end
RegisterNetEvent("turfwar:bank:deposit", function(token, amount)
local src = source
local ok, err = RequireATM(src, token)
if not ok then
print(("^1[ATM BLOCK]^7 deposit src=%s reason=%s"):format(src, err))
return
end
amount = clampAmount(amount)
if not amount then return end
local cash = Cash.Get(src)
if cash < amount then
Notify(src, "Not enough cash.")
return
end
Cash.Set(src, cash - amount)
Cash.SetBank(src, Cash.GetBank(src) + amount)
end)
RegisterNetEvent("turfwar:bank:withdraw", function(token, amount)
local src = source
local ok, err = RequireATM(src, token)
if not ok then
print(("^1[ATM BLOCK]^7 withdraw src=%s reason=%s"):format(src, err))
return
end
amount = clampAmount(amount)
if not amount then return end
local bank = Cash.GetBank(src)
if bank < amount then
Notify(src, "Not enough money in bank.")
return
end
Cash.SetBank(src, bank - amount)
Cash.Set(src, Cash.Get(src) + amount)
end)
RegisterNetEvent("turfwar:atm:balance", function(token)
local src = source
local ok = RequireATM(src, token)
if not ok then return end
PushMoney(src)
end)
-- =========================================================
-- Admin
-- =========================================================
RegisterCommand("cash", function(src)
if src == 0 then
print("[turfwar] /cash can only be used in-game.")
return
end
TriggerClientEvent('chat:addMessage', src, {
args = { "Money", ("Cash: $%d | Bank: $%d"):format(Cash.Get(src), Cash.GetBank(src)) }
})
end, false)
RegisterCommand("addcash", function(src, args)
if src == 0 then
print("[turfwar] Use in-game: /addcash <amount>")
return
end
Cash.Add(src, tonumber(args[1]) or 0)
end, false)
RegisterCommand("setcash", function(src, args)
if src == 0 then
print("[turfwar] Use in-game: /setcash <amount>")
return
end
Cash.Set(src, tonumber(args[1]) or 0)
end, false)

160
server/finance.lua Normal file
View File

@ -0,0 +1,160 @@
print("^2[turfwar]^7 finance server loaded")
-- Uses your existing Cash API:
-- Cash.Get(src), Cash.Add(src, amt), Cash.Remove(src, amt)
-- Cash.GetBank(src), Cash.SetBank(src, amt)
local function Notify(src, msg)
-- This is your existing chat/notify path
TriggerClientEvent('turfwar:finance:notify', src, msg)
end
local function Feed(src, kind, amount)
-- NEW: GTA feed popup (client will render it)
-- kind: "deposit" | "withdraw" | "balance"
TriggerClientEvent('turfwar:finance:feed', src, kind, amount)
end
local function PushMoney(src)
-- Keep using your existing HUD/stat updater (in main.lua)
TriggerClientEvent('turfwar:money:update', src, Cash.Get(src), Cash.GetBank(src))
end
local function parseAmount(arg)
local n = tonumber(arg)
if not n then return nil end
n = math.floor(n)
if n <= 0 then return nil end
return n
end
-- Simple anti-spam
local lastCmd = {}
local CMD_COOLDOWN_MS = 700
local function canRun(src)
local now = GetGameTimer()
local last = lastCmd[src] or 0
if (now - last) < CMD_COOLDOWN_MS then
return false
end
lastCmd[src] = now
return true
end
AddEventHandler('playerDropped', function()
lastCmd[source] = nil
end)
-- Make sure Cash is loaded before commands are used
CreateThread(function()
Wait(0)
if type(Cash) ~= "table" then
print("^1[turfwar]^7 finance.lua: Cash table missing. Ensure cash.lua loads before finance.lua in fxmanifest.")
return
end
if type(Cash.Get) ~= "function" or type(Cash.Add) ~= "function" or type(Cash.Remove) ~= "function" then
print("^1[turfwar]^7 finance.lua: Missing Cash.Get/Add/Remove. Check cash.lua.")
end
if type(Cash.GetBank) ~= "function" or type(Cash.SetBank) ~= "function" then
print("^1[turfwar]^7 finance.lua: Missing Cash.GetBank/SetBank. Check cash.lua.")
end
end)
-- ------------------------------------------------
-- /deposit <amount>
-- ------------------------------------------------
RegisterCommand("deposit", function(src, args)
if not src or src <= 0 then return end
if not canRun(src) then return end
local amount = parseAmount(args[1])
if not amount then
Notify(src, "~r~Usage: /deposit <amount>")
return
end
local cash = Cash.Get(src)
if cash < amount then
Notify(src, "~r~Not enough cash.")
return
end
-- Move cash -> bank
Cash.Remove(src, amount)
Cash.SetBank(src, Cash.GetBank(src) + amount)
-- Update your HUD (no duplication)
PushMoney(src)
-- NEW: GTA feed popup (single line)
Feed(src, "deposit", amount)
-- Keep your existing notify too (optional; remove if you only want GTA feed)
Notify(src, ("~g~Deposited $%d"):format(amount))
end, false)
-- ------------------------------------------------
-- /withdraw <amount>
-- ------------------------------------------------
RegisterCommand("withdraw", function(src, args)
if not src or src <= 0 then return end
if not canRun(src) then return end
local amount = parseAmount(args[1])
if not amount then
Notify(src, "~r~Usage: /withdraw <amount>")
return
end
local bank = Cash.GetBank(src)
if bank < amount then
Notify(src, "~r~Not enough in bank.")
return
end
-- Move bank -> cash
Cash.SetBank(src, bank - amount)
Cash.Add(src, amount)
-- Update your HUD (no duplication)
PushMoney(src)
-- NEW: GTA feed popup (single line)
Feed(src, "withdraw", amount)
-- Keep your existing notify too (optional)
Notify(src, ("~g~Withdrew $%d"):format(amount))
end, false)
-- ------------------------------------------------
-- /balance
-- ------------------------------------------------
RegisterCommand("balance", function(src)
if not src or src <= 0 then return end
local cash = Cash.Get(src)
local bank = Cash.GetBank(src)
PushMoney(src)
-- Optional: feed (shows bank balance)
Feed(src, "balance", bank)
Notify(src, ("~g~Cash:~w~ $%d ~b~Bank:~w~ $%d"):format(cash, bank))
end, false)
local atmOpenUntil = 0
local function OpenATMInterface()
atmOpenUntil = GetGameTimer() + 15000 -- 15s window
PlaySoundFrontend(-1, "ATM_WINDOW", "HUD_FRONTEND_DEFAULT_SOUNDSET", true)
BeginTextCommandThefeedPost("STRING")
AddTextComponentSubstringPlayerName("~g~ATM~s~: /deposit <amt> /withdraw <amt> /balance")
EndTextCommandThefeedPostTicker(false, false)
end
-- When player presses E at ATM, call this instead of the old help:
-- OpenATMInterface()

245
server/gang_shops.lua Normal file
View File

@ -0,0 +1,245 @@
-- server/gang_shops.lua
print("^2[turfwar]^7 gang_shops.lua loaded (server)")
Config = Config or {}
Config.Shops = Config.Shops or {}
-- IMPORTANT:
-- This assumes you already have:
-- - player gang stored server-side somewhere, OR a callback/event to fetch it.
--
-- Below uses a simple server cache:
local playerGang = {} -- [src] = gangId
-- You likely already broadcast this elsewhere.
-- Hook into your existing gang set event if you have one.
RegisterNetEvent("turfwar:myGang:set", function(gangId)
local src = source
playerGang[src] = tonumber(gangId) or 0
end)
AddEventHandler("playerDropped", function()
local src = source
playerGang[src] = nil
end)
local function getGangId(src)
return tonumber(playerGang[src]) or 0
end
-- =========================
-- DB helpers (gang bank)
-- =========================
local function dbFetchBalance(gangId, cb)
exports.oxmysql:query("SELECT balance FROM turfwar_gang_accounts WHERE gang_id = ? LIMIT 1", { gangId }, function(rows)
local bal = 0
if rows and rows[1] and rows[1].balance then bal = tonumber(rows[1].balance) or 0 end
cb(bal)
end)
end
local function dbEnsureRow(gangId, cb)
exports.oxmysql:query("INSERT IGNORE INTO turfwar_gang_accounts (gang_id, balance) VALUES (?, 0)", { gangId }, function()
cb()
end)
end
local function dbDeduct(gangId, amount, cb)
amount = tonumber(amount) or 0
if amount <= 0 then cb(false, "bad_amount") return end
dbEnsureRow(gangId, function()
exports.oxmysql:query("UPDATE turfwar_gang_accounts SET balance = balance - ? WHERE gang_id = ? AND balance >= ?", {
amount, gangId, amount
}, function(result)
-- oxmysql UPDATE result varies by version; handle common cases:
local changed = 0
if type(result) == "table" and result.affectedRows then changed = result.affectedRows end
if type(result) == "number" then changed = result end
cb(changed and changed > 0, changed and changed > 0 and nil or "insufficient")
end)
end)
end
-- =========================
-- Lookup helpers
-- =========================
local function findWeaponItem(itemId)
if not Config.Shops or not Config.Shops.WeaponList then return nil end
for _, it in ipairs(Config.Shops.WeaponList) do
if it.id == itemId then return it end
end
return nil
end
local function findAmmoItem(itemId)
if not Config.Shops or not Config.Shops.AmmoList then return nil end
for _, it in ipairs(Config.Shops.AmmoList) do
if it.id == itemId then return it end
end
return nil
end
local function findVehicleItem(itemId)
if not Config.Shops or not Config.Shops.VehicleList then return nil end
for _, it in ipairs(Config.Shops.VehicleList) do
if it.id == itemId then return it end
end
return nil
end
-- =========================
-- Purchase: weapons
-- =========================
RegisterNetEvent("turfwar:shop:buyWeapon", function(itemId)
local src = source
local gangId = getGangId(src)
if not (Config.Shops.AllowedGang and Config.Shops.AllowedGang(gangId)) then
TriggerClientEvent("turfwar:shop:notify", src, "~r~You can't use the gang shop.")
return
end
local item = findWeaponItem(itemId)
if not item then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Invalid item.")
return
end
local price = tonumber(item.price) or 0
if price < 0 then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Bad price config.")
return
end
dbDeduct(gangId, price, function(ok, reason)
if not ok then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Not enough gang cash.")
return
end
-- Grant on client (server-authoritative purchase)
TriggerClientEvent("turfwar:shop:grantWeapon", src, item, gangId)
TriggerClientEvent("turfwar:shop:notify", src, ("~g~Purchased: ~s~%s"):format(item.label or item.id))
TriggerClientEvent("turfwar:gangbank:requestRefresh", src) -- optional hook
end)
end)
RegisterNetEvent("turfwar:shop:buyAmmo", function(itemId)
local src = source
local gangId = getGangId(src)
if not (Config.Shops.AllowedGang and Config.Shops.AllowedGang(gangId)) then
TriggerClientEvent("turfwar:shop:notify", src, "~r~You can't use the gang shop.")
return
end
local item = findAmmoItem(itemId)
if not item then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Invalid ammo item.")
return
end
local price = tonumber(item.price) or 0
if price <= 0 then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Bad price config.")
return
end
dbDeduct(gangId, price, function(ok)
if not ok then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Not enough gang cash.")
return
end
TriggerClientEvent("turfwar:shop:grantAmmo", src, item)
TriggerClientEvent("turfwar:shop:notify", src, ("~g~Purchased: ~s~%s"):format(item.label or item.id))
TriggerClientEvent("turfwar:gangbank:requestRefresh", src)
end)
end)
-- =========================
-- Purchase: vehicles
-- =========================
RegisterNetEvent("turfwar:shop:buyVehicle", function(itemId)
local src = source
local gangId = getGangId(src)
if not (Config.Shops.AllowedGang and Config.Shops.AllowedGang(gangId)) then
TriggerClientEvent("turfwar:shop:notify", src, "~r~You can't use the gang shop.")
return
end
local item = findVehicleItem(itemId)
if not item then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Invalid vehicle.")
return
end
local price = tonumber(item.price)
if price == nil then price = 0 end
-- Allow FREE vehicles (price = 0). Only reject negative.
if price < 0 then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Bad price config.")
return
end
local function afterPaid()
-- Use your existing spawn-pad system
if not VehicleSpawner or not VehicleSpawner.SpawnModelFor then
TriggerClientEvent("turfwar:shop:notify", src, "~r~VehicleSpawner not available.")
return
end
-- Support special model alias for "gang default"
local model = item.model
if model == "__GANG_DEFAULT__" then
model = (Config.Vehicles and Config.Vehicles.gangModels and Config.Vehicles.gangModels[gangId])
end
if type(model) ~= "string" or model == "" then
TriggerClientEvent("turfwar:shop:notify", src, "~r~No vehicle configured for your gang.")
return
end
local ok2, reason = VehicleSpawner.SpawnModelFor(src, model, { platePrefix = "G" })
if not ok2 then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Vehicle spawn failed.")
return
end
TriggerClientEvent("turfwar:shop:notify", src, ("~g~Purchased: ~s~%s"):format(item.label or item.id))
TriggerClientEvent("turfwar:gangbank:requestRefresh", src)
end
-- If free, don't touch the gang bank
if price == 0 then
afterPaid()
return
end
-- Deduct first (server authority)
dbDeduct(gangId, price, function(ok)
if not ok then
TriggerClientEvent("turfwar:shop:notify", src, "~r~Not enough gang cash.")
return
end
afterPaid()
end)
end)
-- Optional: allow client to query gang balance (for UI)
RegisterNetEvent("turfwar:shop:getBalance", function()
local src = source
local gangId = getGangId(src)
if gangId <= 0 then
TriggerClientEvent("turfwar:shop:balance", src, 0)
return
end
dbFetchBalance(gangId, function(bal)
TriggerClientEvent("turfwar:shop:balance", src, bal)
end)
end)