From bee27aeb847be372f59f293b996d8f2ac01c5d2a Mon Sep 17 00:00:00 2001 From: tanthius Date: Thu, 12 Feb 2026 04:18:36 +0000 Subject: [PATCH] Upload files to "server" --- server/shop.lua | 448 ++++++++++++++++++++++++++++++++++++++++++++ server/vehicles.lua | 178 ++++++++++++++++++ 2 files changed, 626 insertions(+) create mode 100644 server/shop.lua create mode 100644 server/vehicles.lua diff --git a/server/shop.lua b/server/shop.lua new file mode 100644 index 0000000..b5c435d --- /dev/null +++ b/server/shop.lua @@ -0,0 +1,448 @@ +-- server/shop.lua +print("^2[turfwar]^7 server/shop.lua loaded (shop purchases + police support + DB gang bank + anti-doublefire + oxmysql-safe)") + +Config = Config or {} +Config.Shops = Config.Shops or {} + +local POLICE_GANG_ID = 3 + +-- DB table +local GANG_TABLE = "turfwar_gang_accounts" +local COL_GANG_ID = "gang_id" +local COL_BALANCE = "balance" + +-- Debug toggle: set Config.Shops.Debug = true +local function D(msg) + if Config and Config.Shops and Config.Shops.Debug then + print("^3[turfwar:shop]^7 " .. msg) + end +end + +-- ========================================================= +-- Gang tracking +-- ========================================================= +local playerGang = {} -- [src] = gangId + +-- ========================================================= +-- Time helper (must be defined before used) +-- ========================================================= +local function nowMs() + return GetGameTimer() +end + +-- ========================================================= +-- Gang sync handshake state (must be defined before used) +-- ========================================================= +local freshSync = {} -- [src] = lastGameTimerMs +local pendingBuy = {} -- [src] = { nonce, kind, itemId } +local GANG_SYNC_TTL_MS = 2000 + + +local function SetGangFor(src, gangId) + playerGang[src] = tonumber(gangId) or 0 +end + +local function GetPlayerGangId(src) + return tonumber(playerGang[src]) or 0 +end + +RegisterNetEvent("turfwar:setFaction", function(gangId) + local src = source + SetGangFor(src, gangId) + freshSync[src] = nowMs() +end) + +RegisterNetEvent("turfwar:gangUpdate", function(gangId) + local src = source + SetGangFor(src, gangId) + freshSync[src] = nowMs() +end) + +RegisterNetEvent("turfwar:setMyGang", function(gangId) + local src = source + SetGangFor(src, gangId) + freshSync[src] = nowMs() +end) + + +-- ========================================================= +-- Gang sync handshake +-- ========================================================= +local freshSync = {} -- [src] = lastGameTimerMs +local pendingBuy = {} -- [src] = { nonce, kind, itemId } +local GANG_SYNC_TTL_MS = 2000 + +local function nowMs() + return GetGameTimer() +end + +local function isGangSyncFresh(src) + local t = freshSync[src] + return t and (nowMs() - t) <= GANG_SYNC_TTL_MS +end + +local function requestGangSync(src, nonce) + TriggerClientEvent("turfwar:shop:syncGangRequest", src, nonce) +end + +-- ========================================================= +-- Anti-doublefire / anti-overlap +-- ========================================================= +local inFlight = {} -- [src] = true +local seen = {} -- seen[src][key] = expireMs +local SEEN_TTL_MS = 800 + +local function seenKey(src, kind, itemId) + local bucket = math.floor(nowMs() / 250) + return ("%d|%s|%s|%d"):format(src, kind, tostring(itemId or ""), bucket) +end + +local function isSeen(src, key) + local t = seen[src] + if not t then return false end + local exp = t[key] + if not exp then return false end + if exp < nowMs() then + t[key] = nil + return false + end + return true +end + +local function markSeen(src, key) + seen[src] = seen[src] or {} + seen[src][key] = nowMs() + SEEN_TTL_MS +end + +local function clearSrcState(src) + playerGang[src] = nil + freshSync[src] = nil + pendingBuy[src] = nil + inFlight[src] = nil + seen[src] = nil +end + +AddEventHandler("playerDropped", function() + clearSrcState(source) +end) + +-- ========================================================= +-- Shop tables (police vs gangs) +-- ========================================================= +local function GetShopTablesForGang(gangId) + local s = Config.Shops or {} + if gangId == POLICE_GANG_ID then + return s.PoliceWeapons or {}, s.PoliceAmmo or {}, s.PoliceVehicles or {}, true + end + return s.Weapons or {}, s.Ammo or {}, s.Vehicles or {}, false +end + +-- ========================================================= +-- DB Adapter (oxmysql or mysql-async) - FIXED normalize affectedRows +-- ========================================================= +local function have_oxmysql() + return GetResourceState("oxmysql") == "started" and exports.oxmysql ~= nil +end + +local function have_mysql_async() + return MySQL and MySQL.Async and MySQL.Async.fetchScalar ~= nil +end + +local function normalizeAffected(result) + -- oxmysql sometimes returns a table, mysql-async usually returns number + if type(result) == "number" then + return result + end + if type(result) == "table" then + -- common oxmysql shapes: + -- { affectedRows = 1, changedRows = 1, insertId = 0 } + if result.affectedRows ~= nil then return tonumber(result.affectedRows) or 0 end + if result.changedRows ~= nil then return tonumber(result.changedRows) or 0 end + + -- sometimes wrapped in array: + if result[1] and type(result[1]) == "table" then + if result[1].affectedRows ~= nil then return tonumber(result[1].affectedRows) or 0 end + if result[1].changedRows ~= nil then return tonumber(result[1].changedRows) or 0 end + end + end + return 0 +end + +local function db_fetchScalar(query, params, cb) + if have_oxmysql() then + exports.oxmysql:scalar(query, params, cb) + return + end + if have_mysql_async() then + MySQL.Async.fetchScalar(query, params, cb) + return + end + print("^1[turfwar]^7 ERROR: No SQL adapter found. Start oxmysql or mysql-async.") + cb(nil) +end + +local function db_execute(query, params, cb) + if have_oxmysql() then + exports.oxmysql:execute(query, params, function(result) + cb(normalizeAffected(result)) + end) + return + end + if have_mysql_async() then + MySQL.Async.execute(query, params, function(affected) + cb(normalizeAffected(affected)) + end) + return + end + print("^1[turfwar]^7 ERROR: No SQL adapter found. Start oxmysql or mysql-async.") + cb(0) +end + +-- ========================================================= +-- Gang bank: balance + atomic charge +-- ========================================================= +local function ensureGangRow(gangId) + -- NOTE: ON DUPLICATE KEY requires UNIQUE KEY on gang_id. + -- You *should* add: ALTER TABLE turfwar_gang_accounts ADD UNIQUE KEY uq_gang_id (gang_id); + local q = ("INSERT INTO %s (%s,%s) VALUES (?,0) ON DUPLICATE KEY UPDATE %s=%s") + :format(GANG_TABLE, COL_GANG_ID, COL_BALANCE, COL_GANG_ID, COL_GANG_ID) + db_execute(q, { gangId }, function(_) end) +end + +local function GetGangBalanceAsync(gangId, cb) + ensureGangRow(gangId) + local q = ("SELECT %s FROM %s WHERE %s = ? LIMIT 1") + :format(COL_BALANCE, GANG_TABLE, COL_GANG_ID) + db_fetchScalar(q, { gangId }, function(val) + cb(tonumber(val) or 0) + end) +end + +local function TryChargeGangAsync(gangId, amount, cb) + ensureGangRow(gangId) + amount = tonumber(amount) or 0 + if amount <= 0 then + GetGangBalanceAsync(gangId, function(bal) cb(true, bal) end) + return + end + + local q = ("UPDATE %s SET %s = %s - ? WHERE %s = ? AND %s >= ?") + :format(GANG_TABLE, COL_BALANCE, COL_BALANCE, COL_GANG_ID, COL_BALANCE) + + db_execute(q, { amount, gangId, amount }, function(affected) + affected = tonumber(affected) or 0 + D(("UPDATE affectedRows=%d gang=%d amount=%d"):format(affected, gangId, amount)) + + if affected > 0 then + GetGangBalanceAsync(gangId, function(newBal) cb(true, newBal) end) + else + GetGangBalanceAsync(gangId, function(bal) cb(false, bal) end) + end + end) +end + +-- ========================================================= +-- Balance request +-- ========================================================= +RegisterNetEvent("turfwar:shop:requestBalance", function() + local src = source + local gangId = GetPlayerGangId(src) + + if gangId == POLICE_GANG_ID then + TriggerClientEvent("turfwar:shop:balance", src, gangId, 0) + return + end + + GetGangBalanceAsync(gangId, function(bal) + TriggerClientEvent("turfwar:shop:balance", src, gangId, bal) + end) +end) + +-- ========================================================= +-- Finish helper +-- ========================================================= +local function Finish(src, ok, message, newBal) + TriggerClientEvent("turfwar:shop:result", src, ok, message or "", newBal) + inFlight[src] = nil +end + +-- ========================================================= +-- Purchase cores +-- ========================================================= +local function HandleBuyWeapon(src, gangId, itemId) + itemId = tostring(itemId or "") + + if gangId == 0 then + Finish(src, false, "You are not in a gang.", nil) + return + end + + local weapons, _, _, isPolice = GetShopTablesForGang(gangId) + local item = weapons[itemId] + if type(item) ~= "table" then + Finish(src, false, "Invalid weapon.", nil) + return + end + + local price = tonumber(item.price) or 0 + + if isPolice or price <= 0 then + TriggerClientEvent("turfwar:shop:grantWeapon", src, item.weapon, item.ammo or 0) + Finish(src, true, ("Issued %s"):format(item.label or "item"), 0) + return + end + + TryChargeGangAsync(gangId, price, function(ok, newBal) + D(("CHARGE weapon src=%d gang=%d price=%d ok=%s newBal=%d item=%s"):format( + src, gangId, price, tostring(ok), tonumber(newBal) or -1, itemId + )) + + if not ok then + Finish(src, false, "Not enough gang funds.", newBal) + return + end + + + TriggerClientEvent("turfwar:shop:grantWeapon", src, item.weapon, item.ammo or 0) + Finish(src, true, ("Purchased %s"):format(item.label or "item"), newBal) + end) +end + +local function HandleBuyAmmo(src, gangId, itemId) + itemId = tostring(itemId or "") + + if gangId == 0 then + Finish(src, false, "You are not in a gang.", nil) + return + end + + local _, ammo, _, isPolice = GetShopTablesForGang(gangId) + local item = ammo[itemId] + if type(item) ~= "table" then + Finish(src, false, "Invalid ammo.", nil) + return + end + + local price = tonumber(item.price) or 0 + + if isPolice or price <= 0 then + TriggerClientEvent("turfwar:shop:grantAmmo", src, item.forWeapon, item.amount or 0) + Finish(src, true, ("Issued %s"):format(item.label or "ammo"), 0) + return + end + + TryChargeGangAsync(gangId, price, function(ok, newBal) + D(("CHARGE ammo src=%d gang=%d price=%d ok=%s newBal=%d item=%s"):format( + src, gangId, price, tostring(ok), tonumber(newBal) or -1, itemId + )) + + if not ok then + Finish(src, false, "Not enough gang funds.", newBal) + return + end + + + TriggerClientEvent("turfwar:shop:grantAmmo", src, item.forWeapon, item.amount or 0) + Finish(src, true, ("Purchased %s"):format(item.label or "ammo"), newBal) + end) +end + +local function HandleBuyVehicle(src, gangId, itemId) + itemId = tostring(itemId or "") + + if gangId == 0 then + Finish(src, false, "You are not in a gang.", nil) + return + end + + local _, _, vehicles, isPolice = GetShopTablesForGang(gangId) + local item = vehicles[itemId] + if type(item) ~= "table" then + Finish(src, false, "Invalid vehicle.", nil) + return + end + + local price = tonumber(item.price) or 0 + + if isPolice or price <= 0 then + TriggerClientEvent("turfwar:shop:grantVehicle", src, item.model) + Finish(src, true, ("Issued %s"):format(item.label or "vehicle"), 0) + return + end + + TryChargeGangAsync(gangId, price, function(ok, newBal) + D(("CHARGE vehicle src=%d gang=%d price=%d ok=%s newBal=%d item=%s"):format( + src, gangId, price, tostring(ok), tonumber(newBal) or -1, itemId + )) + + if not ok then + Finish(src, false, "Not enough gang funds.", newBal) + return + end + + + TriggerClientEvent("turfwar:shop:grantVehicle", src, item.model) + Finish(src, true, ("Purchased %s"):format(item.label or "vehicle"), newBal) + end) +end + +local function Execute(src, gangId, kind, itemId) + D(("EXEC src=%d gang=%d kind=%s item=%s"):format(src, gangId, tostring(kind), tostring(itemId))) + if kind == "weapon" then + HandleBuyWeapon(src, gangId, itemId) + elseif kind == "ammo" then + HandleBuyAmmo(src, gangId, itemId) + elseif kind == "vehicle" then + HandleBuyVehicle(src, gangId, itemId) + else + Finish(src, false, "Invalid purchase.", nil) + end +end + +local function BeginPurchase(src, kind, itemId) + local key = seenKey(src, kind, itemId) + + if isSeen(src, key) then + D(("IGNORED DUPLICATE src=%d kind=%s item=%s"):format(src, kind, tostring(itemId))) + return + end + markSeen(src, key) + + if inFlight[src] then + D(("BLOCKED IN-FLIGHT src=%d kind=%s item=%s"):format(src, kind, tostring(itemId))) + TriggerClientEvent("turfwar:shop:result", src, false, "Please wait…", nil) + return + end + + inFlight[src] = true + + if not isGangSyncFresh(src) then + local nonce = ("%d:%d:%d"):format(src, nowMs(), math.random(100000, 999999)) + pendingBuy[src] = { nonce = nonce, kind = kind, itemId = tostring(itemId or "") } + D(("SYNC-REQ src=%d nonce=%s kind=%s item=%s cachedGang=%d"):format( + src, nonce, kind, tostring(itemId), GetPlayerGangId(src) + )) + requestGangSync(src, nonce) + return + end + + Execute(src, GetPlayerGangId(src), kind, itemId) +end + +RegisterNetEvent("turfwar:shop:syncGangResponse", function(nonce, gangId) + local src = source + local p = pendingBuy[src] + if not p or p.nonce ~= nonce then return end + + gangId = tonumber(gangId) or 0 + SetGangFor(src, gangId) + freshSync[src] = nowMs() + pendingBuy[src] = nil + + D(("SYNC-OK src=%d gang=%d kind=%s item=%s"):format(src, gangId, p.kind, tostring(p.itemId))) + Execute(src, gangId, p.kind, p.itemId) +end) + +RegisterNetEvent("turfwar:shop:buyWeapon", function(itemId) BeginPurchase(source, "weapon", itemId) end) +RegisterNetEvent("turfwar:shop:buyAmmo", function(itemId) BeginPurchase(source, "ammo", itemId) end) +RegisterNetEvent("turfwar:shop:buyVehicle", function(itemId) BeginPurchase(source, "vehicle", itemId) end) diff --git a/server/vehicles.lua b/server/vehicles.lua new file mode 100644 index 0000000..7421c5f --- /dev/null +++ b/server/vehicles.lua @@ -0,0 +1,178 @@ +-- server/vehicles.lua +print("^2[turfwar]^7 server/vehicles.lua LOADED (config spawn pads)") + +VehicleSpawner = VehicleSpawner or {} + +-- ActiveVeh[src] = { netId=number, gangId=number } +local ActiveVeh = {} + +-- PendingSpawn[src] = { gangId=number, model=string, plate=string } +local PendingSpawn = {} + +local function gangFromSrc(src) + if PlayerGang and PlayerGang[src] then + return tonumber(PlayerGang[src]) or 0 + end + return 0 +end + +local function getGangRGB(gangId) + local g = Config and Config.GANGS and Config.GANGS[gangId] + local rgb = g and g.rgb + if rgb and rgb[1] and rgb[2] and rgb[3] then + return tonumber(rgb[1]) or 255, tonumber(rgb[2]) or 255, tonumber(rgb[3]) or 255 + end + return 255, 255, 255 +end + +local function getHQPosForGang(gangId) + for _, jp in ipairs(Config.JOIN_POINTS or {}) do + if tonumber(jp.gangId) == tonumber(gangId) then + return jp.pos + end + end + return nil +end + +local function getSpawnPadForGang(gangId) + local sp = Config and Config.GANG_VEHICLE_SPAWNS and Config.GANG_VEHICLE_SPAWNS[gangId] + if not sp then return nil end + return { + x = tonumber(sp.x) or 0.0, + y = tonumber(sp.y) or 0.0, + z = tonumber(sp.z) or 0.0, + h = tonumber(sp.w) or 0.0 + } +end + +local function dist(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 deleteVehicleFor(src) + local entry = ActiveVeh[src] + if not entry then return end + + if entry.netId and entry.netId ~= 0 then + local e = NetworkGetEntityFromNetworkId(entry.netId) + if e and DoesEntityExist(e) then + DeleteEntity(e) + end + end + + TriggerClientEvent("turfwar:veh:clearLocal", src) + ActiveVeh[src] = nil +end + +local function setPendingTimeout(src) + SetTimeout(8000, function() + if PendingSpawn[src] then + PendingSpawn[src] = nil + TriggerClientEvent("turfwar:veh:notify", src, "~y~Spawn timed out. Try again.~s~") + end + end) +end + +-- Called from server/main.lua on gang change: +function VehicleSpawner.OnGangChanged(src, oldGang, newGang) + oldGang = tonumber(oldGang) or 0 + newGang = tonumber(newGang) or 0 + if oldGang ~= newGang then + deleteVehicleFor(src) + end +end + +-- ========================================================= +-- EXTERNAL SPAWN (for vehicle shop purchases) +-- ========================================================= +function VehicleSpawner.SpawnModelFor(src, modelName, opts) + opts = opts or {} + + -- HARD BLOCK: prevent double spawns from double-trigger / spam + if PendingSpawn[src] then + TriggerClientEvent("turfwar:veh:notify", src, "~y~Spawn already in progress...~s~") + return false, "pending" + end + + local gangId = gangFromSrc(src) + if gangId == 0 then + TriggerClientEvent("turfwar:veh:notify", src, "~r~You are Neutral.~s~") + return false, "neutral" + end + + if gangId == 3 then + TriggerClientEvent("turfwar:veh:notify", src, "~r~Police can't use this system.~s~") + return false, "police" + end + + if type(modelName) ~= "string" or modelName == "" then + TriggerClientEvent("turfwar:veh:notify", src, "~r~Bad vehicle model.~s~") + return false, "bad_model" + end + + local pad = getSpawnPadForGang(gangId) + if not pad then + TriggerClientEvent("turfwar:veh:notify", src, "~r~No vehicle spawn pad set for your gang.~s~") + print(("^1[turfwar]^7 Missing Config.GANG_VEHICLE_SPAWNS[%s]"):format(gangId)) + return false, "no_pad" + end + + if ActiveVeh[src] then + deleteVehicleFor(src) + end + + local r, g, b = getGangRGB(gangId) + local prefix = opts.platePrefix + or ((Config.Vehicles and Config.Vehicles.platePrefix and Config.Vehicles.platePrefix[gangId]) or ("G"..tostring(gangId))) + local plate = (prefix .. tostring(src)):sub(1, 8) + + PendingSpawn[src] = { gangId = gangId, model = modelName, plate = plate } + setPendingTimeout(src) + + print(("^3[turfwar]^7 doSpawn(shop) -> src=%d gang=%d model=%s plate=%s"):format(src, gangId, modelName, plate)) + + TriggerClientEvent("turfwar:veh:doSpawn", src, { + gangId = gangId, + model = modelName, + x = pad.x, + y = pad.y, + z = pad.z, + heading = pad.h, + rgb = { r, g, b }, + plate = plate + }) + + return true +end + +RegisterNetEvent("turfwar:veh:spawned", function(netId) + local src = source + netId = tonumber(netId) or 0 + + local req = PendingSpawn[src] + PendingSpawn[src] = nil + + if not req then + TriggerClientEvent("turfwar:veh:notify", src, "~r~Spawn rejected (no pending request).~s~") + return + end + + if netId == 0 then + TriggerClientEvent("turfwar:veh:notify", src, "~r~Vehicle spawn failed (netId=0).~s~") + return + end + + ActiveVeh[src] = { netId = netId, gangId = req.gangId } + + TriggerClientEvent("turfwar:veh:setLocal", src, netId, req.gangId) + TriggerClientEvent("turfwar:veh:notify", src, "~g~Gang vehicle spawned.~s~") +end) + +AddEventHandler("playerDropped", function() + local src = source + deleteVehicleFor(src) + PendingSpawn[src] = nil +end)