Upload files to "server"
This commit is contained in:
parent
ff2b8be533
commit
bee27aeb84
448
server/shop.lua
Normal file
448
server/shop.lua
Normal file
|
|
@ -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)
|
||||||
178
server/vehicles.lua
Normal file
178
server/vehicles.lua
Normal file
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue
Block a user