Upload files to "server"

This commit is contained in:
tanthius 2026-02-12 04:18:25 +00:00
parent be328c9be4
commit ff2b8be533
5 changed files with 1285 additions and 0 deletions

823
server/main.lua Normal file
View File

@ -0,0 +1,823 @@
-- server/main.lua
-- Turfwar (single resource) - Turfs + Capture + Guard host + PlayerGang broadcast
-- Works with the guards.lua youre using (no GetPedCombatTarget).
--
-- REQUIRED Config tables in config.lua:
-- Config.GANGS
-- Config.JOIN_POINTS (ARRAY of { gangId=number, label=string, pos=vector3/4 })
-- Config.TURFS (keyed by turfId)
-- Optional:
-- Config.SECONDS_TO_CAPTURE
-- Config.INCOME = { intervalMinutes=60, ... }
--
-- Guard system:
-- - One client is elected "GuardHost" (AI owner)
-- - Server tells GuardHost to spawn/clear guards per turf
-- - When all guards die, GuardHost triggers turfwar:guardsEmpty -> server frees capture again
--
-- Loadout system:
-- - When player joins a gang -> give gang loadout
-- - When player leaves gang (gangId 0) -> strip ALL weapons and ammo
--
-- Leaderboard:
-- - "Most influence" = count of turfs currently owned per gang
-- - Broadcast on capture + clientReady + reload + resource start
print("^2[turfwar]^7 server/main.lua LOADED")
local SecondsToCapture = (Config and tonumber(Config.SECONDS_TO_CAPTURE)) or 60
-- Police "Restore Peace" (how long police must hold to neutralize a turf)
local PoliceRestoreToNeutral = (Config and Config.PlayerPolice and tonumber(Config.PlayerPolice.RESTORE_SECONDS)) or 30
-- Loadouts is a GLOBAL provided by server/loadouts.lua (loaded before this file via fxmanifest)
local Loadouts = Loadouts
if not (Loadouts and Loadouts.ApplyForGang) then
print("^1[turfwar]^7 WARNING: Loadouts not loaded! Check fxmanifest order (loadouts.lua before main.lua).")
end
local function applyLoadoutSafe(src, gangId)
if Loadouts and Loadouts.ApplyForGang then
Loadouts.ApplyForGang(src, gangId)
end
end
AddEventHandler("playerJoining", function()
local src = source
if not (TurfwarPersist and TurfwarPersist.Load) then
PlayerGang[src] = PlayerGang[src] or 0
return
end
TurfwarPersist.Load(src, function(gangId, rank)
PlayerGang[src] = tonumber(gangId) or 0
-- Push to the player + everyone so HUD/blips are correct right away
TriggerClientEvent("turfwar:gangUpdate", src, PlayerGang[src])
TriggerClientEvent("turfwar:setFaction", src, PlayerGang[src])
TriggerClientEvent("turfwar:playerGang", -1, src, PlayerGang[src])
-- Apply loadout for their restored gang
applyLoadoutSafe(src, PlayerGang[src])
end)
end)
-- ------------------------------------------------------------
-- Player gangs (server authoritative)
-- ------------------------------------------------------------
PlayerGang = PlayerGang or {} -- [src] = gangId
local function setPlayerGang(src, gangId)
gangId = tonumber(gangId) or 0
local oldGang = tonumber(PlayerGang[src]) or 0
PlayerGang[src] = gangId
-- Push gang bank balance (optional module)
if TurfwarPayouts and TurfwarPayouts.GetGangBalance then
local bal = (gangId ~= 0) and (TurfwarPayouts.GetGangBalance(gangId) or 0) or 0
TriggerClientEvent("turfwar:gangbank:update", src, gangId, bal)
end
-- Vehicle spawner hook (optional module)
if VehicleSpawner and VehicleSpawner.OnGangChanged then
VehicleSpawner.OnGangChanged(src, oldGang, gangId)
end
-- Apply loadout rules whenever gang changes
applyLoadoutSafe(src, gangId)
-- Send updates to the player + broadcast to all
TriggerClientEvent("turfwar:gangUpdate", src, gangId)
TriggerClientEvent("turfwar:setFaction", src, gangId) -- compatibility
TriggerClientEvent("turfwar:playerGang", -1, src, gangId) -- broadcast to all clients
-- Join announcement in chat
if gangId ~= 0 and gangId ~= oldGang then
local g = (Config and Config.GANGS and Config.GANGS[gangId]) or nil
local color = (g and g.chatColor) or "~s~"
local gangName = (g and g.name) or ("Gang " .. tostring(gangId))
local playerName = GetPlayerName(src) or ("ID " .. tostring(src))
TriggerClientEvent('chat:addMessage', -1, {
args = {
"[Turfwar]",
("%s has joined the %s%s~s~!"):format(playerName, color, gangName)
}
})
end
-- Persist gang for next login
if TurfwarPersist and TurfwarPersist.SaveGang then
TurfwarPersist.SaveGang(src, gangId)
AddEventHandler("playerJoining", function()
local src = source
if TurfwarPersist and TurfwarPersist.Load then
local gangId, rank = TurfwarPersist.Load(src)
PlayerGang[src] = tonumber(gangId) or 0
TriggerClientEvent("turfwar:gangUpdate", src, PlayerGang[src])
TriggerClientEvent("turfwar:setFaction", src, PlayerGang[src])
TriggerClientEvent("turfwar:playerGang", -1, src, PlayerGang[src])
applyLoadoutSafe(src, PlayerGang[src])
else
PlayerGang[src] = PlayerGang[src] or 0
end
end)
end
end
RegisterNetEvent("turfwar:requestFaction", function()
local src = source
TriggerClientEvent("turfwar:setFaction", src, tonumber(PlayerGang[src]) or 0)
end)
RegisterNetEvent("turfwar:setGang", function(gangId)
local src = source
setPlayerGang(src, gangId)
if TurfwarPayouts and TurfwarPayouts.PushGangBankToPlayer then
TurfwarPayouts.PushGangBankToPlayer(src, tonumber(gangId) or 0)
end
end)
RegisterNetEvent("turfwar:requestAllPlayerGangs", function()
local src = source
for id, gang in pairs(PlayerGang) do
TriggerClientEvent("turfwar:playerGang", src, id, tonumber(gang) or 0)
end
end)
RegisterCommand("joingang", function(src, args)
local gangId = tonumber(args[1]) or 0
setPlayerGang(src, gangId)
end, false)
-- ------------------------------------------------------------
-- HQ spawn lookup (used by client to teleport on respawn)
-- Config.JOIN_POINTS is an ARRAY (you use ipairs on client)
-- ------------------------------------------------------------
RegisterNetEvent('turfwar:getMyHQSpawn', function()
local src = source
local gangId = tonumber(PlayerGang[src]) or 0
local function findJoinPoint(forGang)
for i, jp in ipairs(Config.JOIN_POINTS or {}) do
if tonumber(jp.gangId) == tonumber(forGang) then
return jp, i
end
end
return nil, nil
end
local jp, idx = findJoinPoint(gangId)
if not jp then
print(("[turfwar] getMyHQSpawn src=%d gangId=%s -> NOT FOUND, fallback to 0"):format(src, tostring(gangId)))
gangId = 0
jp, idx = findJoinPoint(0)
end
if not jp or not jp.pos then
print(("[turfwar] getMyHQSpawn src=%d gangId=%s -> NO JOIN POINT / NO POS"):format(src, tostring(gangId)))
TriggerClientEvent('turfwar:myHQSpawn', src, nil)
return
end
local v = jp.pos
print(("[turfwar] getMyHQSpawn src=%d gangId=%d jpIndex=%s -> %.2f %.2f %.2f h=%.2f")
:format(src, gangId, tostring(idx), v.x, v.y, v.z, v.w or 0.0))
TriggerClientEvent('turfwar:myHQSpawn', src, {
x = v.x, y = v.y, z = v.z,
h = v.w or 0.0,
gangId = gangId
})
end)
-- ------------------------------------------------------------
-- Turfs runtime state (GLOBAL for other server files if needed)
-- ------------------------------------------------------------
Turfs = Turfs or {}
local function vecToTable(v)
return { x = v.x, y = v.y, z = v.z }
end
local function initTurfsFromConfig()
Turfs = {}
if not Config or not Config.TURFS then
print("^1[turfwar]^7 Config.TURFS missing/empty")
return
end
for turfId, t in pairs(Config.TURFS) do
Turfs[turfId] = {
name = t.name or turfId,
center = vecToTable(t.center),
radius = tonumber(t.radius) or 80.0,
owner = tonumber(t.owner) or 0,
progress = 0,
contestingGang = 0,
guardsAlive = false,
paused = false,
lastActivity = 0,
}
end
-- Load persisted owners into runtime if payout module provides it
if TurfwarPayouts and TurfwarPayouts.LoadTurfOwnersIntoRuntime then
TurfwarPayouts.LoadTurfOwnersIntoRuntime(Turfs)
end
local count = 0
for _ in pairs(Turfs) do count = count + 1 end
print(("^2[turfwar]^7 Turfs loaded: %d"):format(count))
end
initTurfsFromConfig()
local function snapshotForClient()
local payload = {}
for turfId, t in pairs(Turfs) do
payload[turfId] = {
name = t.name,
center = t.center,
radius = t.radius,
owner = t.owner,
progress = t.progress,
contestingGang = t.contestingGang,
}
end
return payload
end
-- ------------------------------------------------------------
-- Turf broadcast helpers (were missing before)
-- ------------------------------------------------------------
local function broadcastTurfUpdate(turfId)
local t = Turfs[turfId]
if not t then return end
TriggerClientEvent("turfwar:turfUpdate", -1, turfId, t.owner, t.progress, t.contestingGang)
end
local function broadcastTurfCaptured(turfId)
local t = Turfs[turfId]
if not t then return end
TriggerClientEvent("turfwar:turfCaptured", -1, turfId, t.owner)
end
-- ------------------------------------------------------------
-- Leaderboard (Most influence = most owned turfs)
-- ------------------------------------------------------------
local function buildLeaderboard()
local counts = {} -- [gangId] = owned turf count
for _, turf in pairs(Turfs) do
local owner = tonumber(turf.owner) or 0
if owner ~= 0 then
counts[owner] = (counts[owner] or 0) + 1
end
end
local rows = {}
for gangId, g in pairs(Config.GANGS or {}) do
gangId = tonumber(gangId) or 0
if gangId ~= 0 then
rows[#rows + 1] = {
gangId = gangId,
name = g.name or ("Gang " .. tostring(gangId)),
value = counts[gangId] or 0,
rgb = g.rgb or {255,255,255}
}
end
end
table.sort(rows, function(a, b)
if a.value == b.value then return a.name < b.name end
return a.value > b.value
end)
return { title = "Most influence", rows = rows }
end
local function broadcastLeaderboard(target)
TriggerClientEvent("turfwar:leaderboard:update", target or -1, buildLeaderboard())
end
RegisterNetEvent("turfwar:leaderboard:request", function()
local src = source
broadcastLeaderboard(src)
end)
CreateThread(function()
local announced = false
while true do
local interval = tonumber(Config?.INCOME?.intervalMinutes) or 60
if interval < 0.1 then interval = 0.1 end
if not announced then
print(("^3[turfwar]^7 Payout scheduler running (%.2f min interval)"):format(interval))
announced = true
end
Wait(math.floor(interval * 60 * 1000))
if TurfwarPayouts and TurfwarPayouts.DoPayoutTick then
TurfwarPayouts.DoPayoutTick(Turfs)
end
end
end)
-- ------------------------------------------------------------
-- Guard host election + guard spawn control
-- ------------------------------------------------------------
local GuardHost = nil
local function isValidSource(src)
return src and tonumber(src) and GetPlayerName(src) ~= nil
end
local function ensureGuardHost()
if isValidSource(GuardHost) then return GuardHost end
GuardHost = nil
for _, src in ipairs(GetPlayers()) do
local n = tonumber(src)
if isValidSource(n) then
GuardHost = n
print(("^2[turfwar]^7 GuardHost elected: %s"):format(GuardHost))
break
end
end
return GuardHost
end
RegisterNetEvent("turfwar:guardsClientReady", function()
local src = source
if not isValidSource(GuardHost) then
GuardHost = src
print(("^2[turfwar]^7 GuardHost assigned from guardsClientReady: %s"):format(GuardHost))
for turfId, t in pairs(Turfs) do
if t.owner ~= 0 then
local cfg = Config.TURFS and Config.TURFS[turfId]
if cfg and (tonumber(cfg.guardCount) or 0) > 0 then
t.guardsAlive = true
TriggerClientEvent("turfwar:spawnGuards", GuardHost, {
turfId = turfId,
ownerFaction = t.owner,
count = tonumber(cfg.guardCount) or 0,
spawns = cfg.guardSpawns or {},
model = cfg.guardModel or "g_m_y_lost_01",
weapon = cfg.guardWeapon or "WEAPON_PISTOL",
})
end
end
end
end
end)
local function clearGuardsForTurf(turfId)
local host = ensureGuardHost()
if host then
TriggerClientEvent("turfwar:clearGuards", host, turfId)
end
end
local function spawnGuardsForTurf(turfId)
local host = ensureGuardHost()
if not host then return end
local t = Turfs[turfId]
local cfg = Config.TURFS and Config.TURFS[turfId]
if not t or not cfg then return end
local count = tonumber(cfg.guardCount) or 0
if t.owner == 0 or count <= 0 then
t.guardsAlive = false
clearGuardsForTurf(turfId)
return
end
t.guardsAlive = true
TriggerClientEvent("turfwar:spawnGuards", host, {
turfId = turfId,
ownerFaction = t.owner,
count = count,
spawns = cfg.guardSpawns or {},
model = cfg.guardModel or "g_m_y_lost_01",
weapon = cfg.guardWeapon or "WEAPON_PISTOL",
})
end
RegisterNetEvent("turfwar:guardsEmpty", function(turfId)
turfId = tostring(turfId)
if Turfs[turfId] then
Turfs[turfId].guardsAlive = false
broadcastTurfUpdate(turfId)
end
end)
RegisterCommand("tw_respawn_guards", function(src, args)
local turfId = tostring(args[1] or "")
if turfId == "" or not Turfs[turfId] then return end
spawnGuardsForTurf(turfId)
end, true)
RegisterCommand("tw_clear_guards", function(src, args)
local turfId = tostring(args[1] or "")
if turfId == "" or not Turfs[turfId] then return end
Turfs[turfId].guardsAlive = false
clearGuardsForTurf(turfId)
end, true)
-- ------------------------------------------------------------
-- Client ready + snapshot
-- ------------------------------------------------------------
RegisterNetEvent("turfwar:clientReady", function()
local src = source
TriggerClientEvent("turfwar:snapshot", src, snapshotForClient(), SecondsToCapture)
for id, gang in pairs(PlayerGang) do
TriggerClientEvent("turfwar:playerGang", src, id, tonumber(gang) or 0)
end
TriggerClientEvent("turfwar:playerGang", -1, src, tonumber(PlayerGang[src]) or 0)
applyLoadoutSafe(src, tonumber(PlayerGang[src]) or 0)
-- send leaderboard to joining client
broadcastLeaderboard(src)
end)
-- ------------------------------------------------------------
-- Environment cash drop relay
-- ------------------------------------------------------------
RegisterNetEvent("environment:pedCashDrop", function(amount, coords)
if type(coords) == "table" and coords.x and coords.y and coords.z then
TriggerClientEvent("environment:spawnCashPickup", -1, amount, coords)
else
print("^3[turfwar]^7 environment:pedCashDrop received invalid coords payload")
end
end)
-- ------------------------------------------------------------
-- Capture logic (bulletproof)
-- ------------------------------------------------------------
local LastPing = {}
-- Client-reported movement state (cached server-side)
PlayerOnFoot = PlayerOnFoot or {} -- [src] = true/false
RegisterNetEvent("turfwar:setOnFoot", function(onFoot)
local src = source
PlayerOnFoot[src] = (onFoot == true)
end)
-- Presence tracking (pause rules)
local Presence = {} -- Presence[turfId][src] = expiresMs
local PRESENCE_TTL_MS = 3500
local CONTEST_IDLE_RESET_MS = 6000 -- if no capture pings for this long, reset contest
local function notePresence(turfId, src, gangId)
if tonumber(gangId) == 0 then return end -- neutral does NOT pause
Presence[turfId] = Presence[turfId] or {}
Presence[turfId][src] = GetGameTimer() + PRESENCE_TTL_MS
end
local function cleanupPresence(turfId)
local now = GetGameTimer()
local bucket = Presence[turfId]
if not bucket then return end
for src, exp in pairs(bucket) do
if (not exp) or exp < now or (not GetPlayerName(src)) then
bucket[src] = nil
end
end
end
-- =========================================================
-- Wanted stars on capture (anti-snipe)
-- =========================================================
local CAPTURE_WANTED_STARS = 2
local function awardWantedToPresence(turfId, stars)
stars = tonumber(stars) or CAPTURE_WANTED_STARS
cleanupPresence(turfId)
local bucket = Presence[turfId]
if not bucket then return end
local policeGang = (Config.PlayerPolice and tonumber(Config.PlayerPolice.POLICE_GANG_ID)) or 3
for src, exp in pairs(bucket) do
if exp and exp >= GetGameTimer() and GetPlayerName(src) then
local g = tonumber(PlayerGang[src]) or 0
if g ~= 0 and g ~= policeGang then
TriggerClientEvent("turfwar:wanted:setMin", src, stars)
end
end
end
end
-- Clients ping this while standing inside a turf zone so defenders also count as "present"
RegisterNetEvent("turfwar:notePresence", function(turfId)
local src = source
turfId = tostring(turfId or "")
if turfId == "" or not Turfs[turfId] then return end
local gangId = tonumber(PlayerGang[src]) or 0
notePresence(turfId, src, gangId)
end)
local function isCapturePausedByOthers(turfId, capturingGang)
cleanupPresence(turfId)
local bucket = Presence[turfId]
if not bucket then return false end
local cg = tonumber(capturingGang) or 0
for src, _ in pairs(bucket) do
local g = tonumber(PlayerGang[src]) or 0
if g ~= 0 and g ~= cg then
return true
end
end
return false
end
-- Hint throttle
local HintCooldown = {}
local function sendCaptureHint(src, msg)
local now = GetGameTimer()
local nextOk = HintCooldown[src] or 0
if now < nextOk then return end
HintCooldown[src] = now + 1500
TriggerClientEvent("turfwar:captureHint", src, msg)
end
local function canCapture(turfId, gangId)
local t = Turfs[turfId]
if not t then return false, "bad_turf" end
if gangId == 0 then return false, "neutral_player" end
local policeGang = (Config.PlayerPolice and tonumber(Config.PlayerPolice.POLICE_GANG_ID)) or 3
if gangId == policeGang then
return false, "police_cannot_capture"
end
if t.owner == gangId then return false, "already_owner" end
if t.owner ~= 0 and t.guardsAlive then return false, "guards_alive" end
return true, "ok"
end
local function setPausedState(turfId, paused)
local t = Turfs[turfId]
if not t then return end
paused = paused and true or false
if t.paused == paused then return end -- only broadcast on change
t.paused = paused
TriggerClientEvent("turfwar:capturePaused", -1, turfId, paused)
end
local function resetContest(turfId)
local t = Turfs[turfId]
if not t then return end
if (t.contestingGang or 0) == 0 and (t.progress or 0) == 0 then return end
t.contestingGang = 0
t.progress = 0
t.paused = false
t.lastActivity = 0
broadcastTurfUpdate(turfId)
TriggerClientEvent("turfwar:capturePaused", -1, turfId, false)
end
-- cleanup tick so contests can't get stuck forever
CreateThread(function()
while true do
Wait(1500)
local now = GetGameTimer()
for turfId, t in pairs(Turfs) do
if t.contestingGang and t.contestingGang ~= 0 then
local last = tonumber(t.lastActivity) or 0
if last > 0 and (now - last) > CONTEST_IDLE_RESET_MS then
resetContest(turfId)
end
end
end
end
end)
RegisterNetEvent("turfwar:attemptCapture", function(turfId)
local src = source
turfId = tostring(turfId)
local gangId = tonumber(PlayerGang[src]) or 0
local t = Turfs[turfId]
if not t then return end
-- Presence: any non-neutral ping counts for pause rules (including police)
notePresence(turfId, src, gangId)
-- Throttle per-player per-turf
LastPing[src] = LastPing[src] or {}
local now = GetGameTimer()
local nextOk = LastPing[src][turfId] or 0
if now < nextOk then return end
LastPing[src][turfId] = now + 900
if gangId == 0 then return end
local policeGang = (Config.PlayerPolice and tonumber(Config.PlayerPolice.POLICE_GANG_ID)) or 3
if gangId == policeGang then
-- =========================
-- POLICE: Restore the peace
-- =========================
-- Nothing to do if already neutral
if (tonumber(t.owner) or 0) == 0 then
sendCaptureHint(src, "This area is already neutral.")
return
end
-- Same guard rule as normal capture (prevents bypassing defenses)
if t.owner ~= 0 and t.guardsAlive then
sendCaptureHint(src, "Clear the guards before restoring peace.")
return
end
-- On-foot enforcement
if PlayerOnFoot[src] == false then
sendCaptureHint(src, "Exit your vehicle to restore peace.")
return
end
-- Mark activity so it doesn't idle-reset
t.lastActivity = now
-- If police take over the contest, reset contest state cleanly
if (tonumber(t.contestingGang) or 0) ~= policeGang then
t.contestingGang = policeGang
t.progress = math.max(1, tonumber(t.progress) or 0)
t.paused = false
broadcastTurfUpdate(turfId)
end
-- If other gangs are present, pause (same rule)
if isCapturePausedByOthers(turfId, t.contestingGang) then
if (tonumber(t.progress) or 0) < 1 then t.progress = 1 end
setPausedState(turfId, true)
broadcastTurfUpdate(turfId)
return
else
setPausedState(turfId, false)
end
-- Progress restore-peace
t.progress = (tonumber(t.progress) or 0) + 1
if t.progress >= PoliceRestoreToNeutral then
-- Neutralize turf
t.owner = 0
t.progress = 0
t.contestingGang = 0
t.paused = false
t.lastActivity = 0
-- Persist neutral owner
if TurfwarPayouts and TurfwarPayouts.SaveTurfOwner then
TurfwarPayouts.SaveTurfOwner(turfId, 0)
end
-- Clear guards (neutral turf should not have guards)
t.guardsAlive = false
clearGuardsForTurf(turfId)
-- Broadcast changes
broadcastTurfCaptured(turfId) -- clients will see owner=0
broadcastLeaderboard(-1)
setPausedState(turfId, false)
else
broadcastTurfUpdate(turfId)
end
return
end
-- On-foot enforcement: only block if we KNOW they're in a vehicle
if PlayerOnFoot[src] == false then
sendCaptureHint(src, "Exit your vehicle to capture.")
return
end
local ok, reason = canCapture(turfId, gangId)
if not ok then
if reason == "guards_alive" then
sendCaptureHint(src, "Clear the guards before capturing.")
end
return
end
-- Mark activity so contest doesn't stick forever
t.lastActivity = now
-- Start contest if none, or if different gang takes over
if (tonumber(t.contestingGang) or 0) ~= gangId then
t.contestingGang = gangId
t.progress = math.max(1, tonumber(t.progress) or 0) -- ensure >0 for HUD
t.paused = false
broadcastTurfUpdate(turfId) -- immediate HUD pop
end
-- Pause if any other non-neutral gang present
if isCapturePausedByOthers(turfId, t.contestingGang) then
if (tonumber(t.progress) or 0) < 1 then t.progress = 1 end
setPausedState(turfId, true)
broadcastTurfUpdate(turfId)
return
else
setPausedState(turfId, false)
end
-- Progress
t.progress = (tonumber(t.progress) or 0) + 1
if t.progress >= SecondsToCapture then
-- ✅ Award wanted stars to everyone currently in the zone (based on presence pings)
awardWantedToPresence(turfId, CAPTURE_WANTED_STARS)
t.owner = gangId
t.progress = 0
t.contestingGang = 0
t.paused = false
t.lastActivity = 0
if TurfwarPayouts and TurfwarPayouts.SaveTurfOwner then
TurfwarPayouts.SaveTurfOwner(turfId, gangId)
end
broadcastTurfCaptured(turfId)
-- leaderboard refresh on capture
broadcastLeaderboard(-1)
spawnGuardsForTurf(turfId)
setPausedState(turfId, false)
else
broadcastTurfUpdate(turfId)
end
end)
-- ------------------------------------------------------------
-- Resource start / reload support
-- ------------------------------------------------------------
AddEventHandler("onResourceStart", function(res)
if res ~= GetCurrentResourceName() then return end
SecondsToCapture = (Config and tonumber(Config.SECONDS_TO_CAPTURE)) or 60
initTurfsFromConfig()
-- leaderboard refresh on resource start
broadcastLeaderboard(-1)
end)
RegisterCommand("tw_reload_turfs", function(src)
initTurfsFromConfig()
TriggerClientEvent("turfwar:snapshot", -1, snapshotForClient(), SecondsToCapture)
-- leaderboard refresh on reload
broadcastLeaderboard(-1)
end, true)
-- ------------------------------------------------------------
-- Player dropped cleanup
-- ------------------------------------------------------------
AddEventHandler("playerDropped", function()
local src = source
PlayerGang[src] = nil
TriggerClientEvent("turfwar:playerGang", -1, src, -1)
if PlayerOnFoot then PlayerOnFoot[src] = nil end
if GuardHost == src then
print("^3[turfwar]^7 GuardHost dropped; will re-elect on next guardsClientReady")
GuardHost = nil
end
for turfId, bucket in pairs(Presence) do
if bucket then bucket[src] = nil end
end
LastPing[src] = nil
HintCooldown[src] = nil
end)

200
server/payouts.lua Normal file
View File

@ -0,0 +1,200 @@
-- server/payouts.lua
print("^2[turfwar]^7 server/payouts.lua LOADED (turf owners + gang bank + payouts)")
TurfwarPayouts = TurfwarPayouts or {}
-- ======================================================
-- CONFIG (matches your existing DB)
-- ======================================================
local GANGBANK_TABLE = "turfwar_gang_accounts" -- (gang_id, balance, updated_at)
local TURFOWNERS_TABLE = "turfwar_turf_owners" -- change if your table name differs
-- Ensure globals exist
PlayerGang = PlayerGang or {}
local function i(v) return math.floor(tonumber(v) or 0) end
local function dbReady()
return MySQL and MySQL.query and MySQL.update
end
-- ======================================================
-- Turf owner persistence (used by server/main.lua)
-- ======================================================
function TurfwarPayouts.SaveTurfOwner(turfId, gangId)
turfId = tostring(turfId or "")
gangId = i(gangId)
if turfId == "" then return end
if not dbReady() then
print("^1[turfwar]^7 SaveTurfOwner: MySQL not ready")
return
end
MySQL.update.await(
("INSERT INTO %s (turf_id, owner_gang) VALUES (?, ?) " ..
"ON DUPLICATE KEY UPDATE owner_gang = VALUES(owner_gang)"):format(TURFOWNERS_TABLE),
{ turfId, gangId }
)
end
function TurfwarPayouts.LoadTurfOwnersIntoRuntime(Turfs)
if not dbReady() then
print("^1[turfwar]^7 LoadTurfOwnersIntoRuntime: MySQL not ready")
return
end
local rows = MySQL.query.await(
("SELECT turf_id, owner_gang FROM %s"):format(TURFOWNERS_TABLE),
{}
) or {}
local applied = 0
for _, r in ipairs(rows) do
local id = tostring(r.turf_id)
local owner = i(r.owner_gang)
if Turfs and Turfs[id] then
Turfs[id].owner = owner
Turfs[id].progress = 0
Turfs[id].contestingGang = 0
applied = applied + 1
end
end
print(("^2[turfwar]^7 Turf owners loaded from DB: %d applied"):format(applied))
end
-- ======================================================
-- Gang bank helpers
-- ======================================================
local function ensureGangRow(gangId)
gangId = i(gangId)
if gangId <= 0 then return end
if not dbReady() then return end
-- requires gang_id to be UNIQUE/PK
MySQL.update.await(
("INSERT INTO %s (gang_id, balance) VALUES (?, 0) " ..
"ON DUPLICATE KEY UPDATE gang_id = gang_id"):format(GANGBANK_TABLE),
{ gangId }
)
end
local function getGangBalance(gangId)
gangId = i(gangId)
if gangId <= 0 then return 0 end
if not dbReady() then return 0 end
local rows = MySQL.query.await(
("SELECT balance FROM %s WHERE gang_id=? LIMIT 1"):format(GANGBANK_TABLE),
{ gangId }
) or {}
return tonumber(rows[1] and rows[1].balance) or 0
end
local function addGangMoney(gangId, amount)
gangId = i(gangId)
amount = i(amount)
if gangId <= 0 or amount == 0 then return getGangBalance(gangId) end
if not dbReady() then return 0 end
ensureGangRow(gangId)
-- Atomic increment
MySQL.update.await(
("UPDATE %s SET balance = balance + ? WHERE gang_id = ?"):format(GANGBANK_TABLE),
{ amount, gangId }
)
return getGangBalance(gangId)
end
-- Expose for other scripts if you want them
function TurfwarPayouts.GetGangBalance(gangId)
return getGangBalance(gangId)
end
function TurfwarPayouts.AddGangMoney(gangId, amount)
return addGangMoney(gangId, amount)
end
-- ======================================================
-- Push to HUD (server -> client)
-- ======================================================
function TurfwarPayouts.PushGangBankToPlayer(src, gangId)
gangId = i(gangId)
local bal = 0
if gangId > 0 then
bal = getGangBalance(gangId)
end
TriggerClientEvent("turfwar:gangbank:update", src, gangId, bal)
end
-- Client requests "my gang bank"
RegisterNetEvent("turfwar:gangbank:request", function()
local src = source
local gid = i(PlayerGang[src] or 0)
TurfwarPayouts.PushGangBankToPlayer(src, gid)
end)
-- ======================================================
-- Payout tick (called from server/main.lua)
-- ======================================================
function TurfwarPayouts.DoPayoutTick(Turfs)
if not dbReady() then
print("^1[turfwar]^7 DoPayoutTick: MySQL not ready")
return
end
local inc = (Config and Config.INCOME) or {}
local excluded = (inc and inc.excludedGangs) or {}
local defaultPayout = i(inc.defaultTurfPayout or 10)
local totals = {} -- [gangId] = payout this tick
for turfId, t in pairs(Turfs or {}) do
local owner = i(t.owner)
if owner ~= 0 and not excluded[owner] then
local cfg = (Config and Config.TURFS and Config.TURFS[turfId]) or nil
local payout = defaultPayout
if cfg and cfg.payout ~= nil then payout = i(cfg.payout) end
if payout > 0 then
totals[owner] = (totals[owner] or 0) + payout
end
end
end
for gangId, amount in pairs(totals) do
local newBal = addGangMoney(gangId, amount)
print(("^2[turfwar]^7 Payout: gang %d +$%d => $%d"):format(gangId, amount, newBal))
-- Push updated balance to online players in that gang
for _, pid in ipairs(GetPlayers()) do
local src = tonumber(pid)
if src and i(PlayerGang[src] or 0) == gangId then
TriggerClientEvent("turfwar:gangbank:update", src, gangId, newBal)
end
end
end
end
-- ======================================================
-- Debug
-- ======================================================
RegisterCommand("tw_gangbank_dump", function()
if not dbReady() then
print("^1[turfwar]^7 tw_gangbank_dump: MySQL not ready")
return
end
local rows = MySQL.query.await(
("SELECT gang_id, balance, updated_at FROM %s ORDER BY gang_id"):format(GANGBANK_TABLE),
{}
) or {}
print(("^2[turfwar]^7 Gang accounts (%d rows):"):format(#rows))
for _, r in ipairs(rows) do
print((" gang=%s balance=%s updated=%s"):format(r.gang_id, r.balance, r.updated_at))
end
end, true)

92
server/persistence.lua Normal file
View File

@ -0,0 +1,92 @@
-- server/persistence.lua
print("^2[turfwar]^7 persistence.lua loaded (MySQL await version)")
TurfwarPersist = TurfwarPersist or {}
local function getIdentifier(src)
local id = GetPlayerIdentifierByType(src, "license2")
if id and id ~= "" then return id end
id = GetPlayerIdentifierByType(src, "license")
if id and id ~= "" then return id end
local ids = GetPlayerIdentifiers(src)
return ids and ids[1] or nil
end
local function dbReady()
return MySQL ~= nil
end
-- Call once on resource start to confirm DB + table exists
CreateThread(function()
Wait(500)
if not dbReady() then
print("^1[turfwar]^7 ERROR: MySQL is nil. Did you add '@oxmysql/lib/MySQL.lua' BEFORE this file?")
return
end
-- Ensure table exists (auto-create)
local ok, err = pcall(function()
MySQL.query.await([[
CREATE TABLE IF NOT EXISTS turfwar_players (
identifier VARCHAR(64) NOT NULL PRIMARY KEY,
gang_id INT NOT NULL DEFAULT 0,
rank INT NOT NULL DEFAULT 0,
last_seen TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
)
]])
end)
if ok then
print("^2[turfwar]^7 DB ready: turfwar_players table ensured.")
else
print("^1[turfwar]^7 DB ERROR creating/ensuring table: " .. tostring(err))
end
end)
function TurfwarPersist.Load(src)
if not dbReady() then return 0, 0 end
local ident = getIdentifier(src)
if not ident then
print(("^3[turfwar]^7 Persist.Load: no identifier for src=%s"):format(src))
return 0, 0
end
local row = MySQL.single.await("SELECT gang_id, rank FROM turfwar_players WHERE identifier = ?", { ident })
if row then
return tonumber(row.gang_id) or 0, tonumber(row.rank) or 0
end
-- first time: insert
MySQL.insert.await("INSERT INTO turfwar_players (identifier, gang_id, rank) VALUES (?, 0, 0)", { ident })
return 0, 0
end
function TurfwarPersist.SaveGang(src, gangId)
if not dbReady() then
print("^1[turfwar]^7 Persist.SaveGang: MySQL not ready (MySQL=nil)")
return
end
local ident = getIdentifier(src)
if not ident then
print(("^1[turfwar]^7 Persist.SaveGang: no identifier for src=%s"):format(src))
return
end
gangId = tonumber(gangId) or 0
print(("^3[turfwar]^7 Persist.SaveGang: ident=%s gangId=%d"):format(ident, gangId))
local affected = MySQL.update.await([[
INSERT INTO turfwar_players (identifier, gang_id, rank)
VALUES (?, ?, 0)
ON DUPLICATE KEY UPDATE gang_id = VALUES(gang_id)
]], { ident, gangId })
print(("^2[turfwar]^7 Persist.SaveGang: DB affected=%s"):format(tostring(affected)))
end

74
server/player_police.lua Normal file
View File

@ -0,0 +1,74 @@
-- server/player_police.lua
print("^2[turfwar]^7 server/player_police.lua loaded (police tracking)")
local PP = Config.PlayerPolice or {}
local POLICE = tonumber(PP.POLICE_GANG_ID) or 3
local PREFIX = PP.CHAT_PREFIX or "^4[POLICE]^7 "
local ONLY_UP = (PP.ONLY_ANNOUNCE_ON_STAR_INCREASE ~= false)
local lastStars = {} -- [src] = stars
local function isPolice(src)
local g = (PlayerGang and PlayerGang[src]) or 0
return tonumber(g) == POLICE
end
local function sendToPolice(eventName, ...)
for _, sid in ipairs(GetPlayers()) do
local p = tonumber(sid)
if p and isPolice(p) then
TriggerClientEvent(eventName, p, ...)
end
end
end
local function chatToPolice(msg)
sendToPolice('turfwar:pp:chat', msg)
end
RegisterNetEvent('turfwar:pp:starsChanged', function(stars, interior)
local src = source
stars = tonumber(stars) or 0
interior = (interior == true)
local prev = lastStars[src] or 0
lastStars[src] = stars
if stars > 0 then
local announce = true
if ONLY_UP then announce = (stars > prev) end
if announce then
chatToPolice(("%sA %d star crime has been detected"):format(PREFIX, stars))
end
end
-- tell police the mode changed
sendToPolice('turfwar:pp:update', src, {
type = "mode",
stars = stars,
interior = interior
})
end)
RegisterNetEvent('turfwar:pp:posUpdate', function(stars, interior, x, y, z, heading)
local src = source
stars = tonumber(stars) or 0
interior = (interior == true)
x = tonumber(x); y = tonumber(y); z = tonumber(z)
heading = tonumber(heading) or 0.0
if not x or not y or not z then return end
sendToPolice('turfwar:pp:update', src, {
type = "pos",
stars = stars,
interior = interior,
x = x, y = y, z = z,
heading = heading
})
end)
AddEventHandler('playerDropped', function()
local src = source
lastStars[src] = nil
sendToPolice('turfwar:pp:clear', src)
end)

View File

@ -0,0 +1,96 @@
print("^2[turfwar]^7 police_wanted_escalate.lua loaded (server)")
local HEAT_WINDOW_SEC = 600
local POLICE_GANG_ID = 3
local MIN_STARS_FOR_COP_DAMAGE = 1
local function StarsForCount(n)
if n <= 0 then return 0 end
if n == 1 then return 2 end
if n == 2 then return 3 end
if n == 3 then return 4 end
return 5
end
-- heat[killerSrc] = { timestamps = { t1, t2, ... } }
local heat = {}
local function nowSec() return os.time() end
local function pruneOld(killerSrc)
local h = heat[killerSrc]
if not h then return 0 end
local cutoff = nowSec() - HEAT_WINDOW_SEC
local keep = {}
for _, t in ipairs(h.timestamps) do
if t >= cutoff then keep[#keep+1] = t end
end
h.timestamps = keep
return #keep
end
local function addCopKill(killerSrc)
heat[killerSrc] = heat[killerSrc] or { timestamps = {} }
heat[killerSrc].timestamps[#heat[killerSrc].timestamps + 1] = nowSec()
return pruneOld(killerSrc)
end
-- IMPORTANT: This is the #1 failure point.
-- Your PlayerGang table might not be global in THIS file.
local function IsPolicePlayer(src)
-- If PlayerGang is global, great:
if PlayerGang ~= nil then
return PlayerGang[src] == POLICE_GANG_ID
end
-- Fallback option: ACE permission check (enable if you use it)
-- return IsPlayerAceAllowed(src, "police")
return false
end
-- Debug helper
local function dbg(msg)
print(("^3[turfwar wanted]^7 %s"):format(msg))
end
-- --------------- TEST PIPELINE (temporary) ---------------
-- Set to true to bypass police checks while testing wiring
local BYPASS_POLICE_CHECK = true
-- --------------- /TEST PIPELINE --------------------------
RegisterNetEvent('turfwar:wanted:policeDamagedBy', function(killerSrc)
local victimSrc = source
killerSrc = tonumber(killerSrc) or 0
dbg(("policeDamagedBy victim=%d killer=%d"):format(victimSrc, killerSrc))
if killerSrc <= 0 or killerSrc == victimSrc then return end
if not BYPASS_POLICE_CHECK then
local isPolice = IsPolicePlayer(victimSrc)
dbg((" IsPolicePlayer(%d)=%s"):format(victimSrc, tostring(isPolice)))
if not isPolice then return end
end
TriggerClientEvent('turfwar:wanted:setMinimum', killerSrc, MIN_STARS_FOR_COP_DAMAGE)
dbg((" -> setMinimum %d stars to %d"):format(MIN_STARS_FOR_COP_DAMAGE, killerSrc))
end)
RegisterNetEvent('turfwar:wanted:policeKilledBy', function(killerSrc)
local victimSrc = source
killerSrc = tonumber(killerSrc) or 0
dbg(("policeKilledBy victim=%d killer=%d"):format(victimSrc, killerSrc))
if killerSrc <= 0 or killerSrc == victimSrc then return end
if not BYPASS_POLICE_CHECK then
local isPolice = IsPolicePlayer(victimSrc)
dbg((" IsPolicePlayer(%d)=%s"):format(victimSrc, tostring(isPolice)))
if not isPolice then return end
end
local count = addCopKill(killerSrc)
local stars = StarsForCount(count)
TriggerClientEvent('turfwar:wanted:setEscalated', killerSrc, stars, count, HEAT_WINDOW_SEC)
dbg((" -> setEscalated killer=%d heat=%d stars=%d"):format(killerSrc, count, stars))
end)