-- server/main.lua -- Turfwar (single resource) - Turfs + Capture + Guard host + PlayerGang broadcast -- Works with the guards.lua you’re 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)