From ff2b8be53337954caa248b59c064c711d3caa7e2 Mon Sep 17 00:00:00 2001 From: tanthius Date: Thu, 12 Feb 2026 04:18:25 +0000 Subject: [PATCH] Upload files to "server" --- server/main.lua | 823 ++++++++++++++++++++++++++++++ server/payouts.lua | 200 ++++++++ server/persistence.lua | 92 ++++ server/player_police.lua | 74 +++ server/police_wanted_escalate.lua | 96 ++++ 5 files changed, 1285 insertions(+) create mode 100644 server/main.lua create mode 100644 server/payouts.lua create mode 100644 server/persistence.lua create mode 100644 server/player_police.lua create mode 100644 server/police_wanted_escalate.lua diff --git a/server/main.lua b/server/main.lua new file mode 100644 index 0000000..1cc07e0 --- /dev/null +++ b/server/main.lua @@ -0,0 +1,823 @@ +-- 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) diff --git a/server/payouts.lua b/server/payouts.lua new file mode 100644 index 0000000..0443fe7 --- /dev/null +++ b/server/payouts.lua @@ -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) diff --git a/server/persistence.lua b/server/persistence.lua new file mode 100644 index 0000000..1c5f3ba --- /dev/null +++ b/server/persistence.lua @@ -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 diff --git a/server/player_police.lua b/server/player_police.lua new file mode 100644 index 0000000..a56361b --- /dev/null +++ b/server/player_police.lua @@ -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) diff --git a/server/police_wanted_escalate.lua b/server/police_wanted_escalate.lua new file mode 100644 index 0000000..2915241 --- /dev/null +++ b/server/police_wanted_escalate.lua @@ -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)