> For the complete documentation index, see [llms.txt](https://docs.lostdev.store/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lostdev.store/territories/architecture.md).

# architecture

## Overview

```mermaid
flowchart TB
    subgraph Shared
        Constants[shared/constants.lua]
        Utils[shared/utils.lua]
    end

    subgraph Server
        Bridge[server/bridge.lua]
        Validation[server/validation.lua]
        Cache[server/cache.lua]
        Persistence[server/persistence.lua]
        Capture[server/capture.lua]
        Admin[server/admin.lua]
        Events[server/events.lua]
        Main[server/main.lua<br/>TerritoryManager]
        Stats[server/stats.lua]
        MySQL[(MySQL<br/>oxmysql)]
    end

    subgraph Client
        CBridge[client/bridge.lua]
        Zones[client/zones.lua<br/>ox_lib zones]
        Blips[client/blips.lua]
        CMain[client/main.lua]
    end

    subgraph NUI [Tactical Tablet — React]
        App[App.jsx]
        Tablet[TacticalTablet.jsx]
    end

    Main --> Bridge
    Main --> Validation
    Main --> Persistence
    Persistence --> MySQL
    Capture --> Validation
    Admin --> Main
    Admin --> Persistence
    Events --> Main
    Stats --> Persistence

    CMain --> CBridge
    CMain --> Zones
    CMain --> Blips
    CMain -- SendNUIMessage --> App
    App --> Tablet
    Tablet -- postNUI/fetch --> CMain
    CMain -- TriggerServerEvent --> Events
    Events -- TriggerClientEvent --> CMain
```

## Lifecycle of a capture

```mermaid
sequenceDiagram
    participant P as Player (client)
    participant Z as ox_lib Zone
    participant S as Server (TerritoryManager)
    participant DB as MySQL

    P->>Z: Enters the territory radius
    Z->>P: onEnter (client/zones.lua)
    P->>S: TriggerServerEvent territories:playerEntered
    S->>S: Validation.CanPlayerCapture()
    alt Player can attack
        S->>S: Capture.StartCapture() -> state = CAPTURING
        S-->>P: territories:underAttack (to the owning faction, wherever they are)
    end
    loop Every Config.Performance.checkInterval (500ms)
        S->>S: TerritoryManager.ProcessTerritories()
        S->>S: CalculateProgressDelta() -> adjusts progress
        S-->>P: territories:updateTerritoryInfo (via client poll)
    end
    alt progress reaches 100
        S->>S: CompleteCapture() -> owner = attacker, state = COOLDOWN
        S->>DB: Persistence.LogHistoryEvent('captured')
        S->>DB: TerritoryManager.SaveTerritory()
        S-->>P: territories:captureResult (won/lost, to the whole faction)
    else progress reaches 0 (defender loses)
        S->>S: LoseOwnership() -> owner = nil, state = CAPTURING (neutral, continues)
        S->>DB: Persistence.LogHistoryEvent('lost')
    end
```

## Server authority

The server is the **single source of truth** for territory state (`TerritoryStates`, in `server/main.lua`). The client:

* Detects proximity **locally** (for responsiveness), but the server also validates the player's position before counting them as "inside" the zone (`TerritoryManager.IsPlayerValidInTerritory`, `Validation.IsPlayerInsideTerritory`).
* Never decides on its own who wins a capture — it only sends intents (`territories:playerEntered`, `territories:startCapture`) and receives updates.
* A player's identity (gang/job) is always resolved on the server (`Validation.GetPlayerGroupSync` → `PlayerCache`), never trusted from the client.

## Server main loop

`TerritoryManager.StartMonitoring()` runs a `CreateThread` with `Wait(0)` that, every `Config.Performance.checkInterval` ms, calls `ProcessTerritories()`:

{% stepper %}
{% step %}

## Skip inactive neutral territories

Skips neutral territories with nobody inside (optimization).
{% endstep %}

{% step %}

## Update players in zone

`UpdatePlayersInZone` rebuilds `playersInside`/`factionsInside` from `PlayerTracking` (populated by the `playerEntered`/`playerExited` events), validates the real position via `GetEntityCoords`, and detects the **contested** state.
{% endstep %}

{% step %}

## Process captures

If `state` is `CAPTURING` or `CONTESTED` → `ProcessCapture` (computes the progress delta).
{% endstep %}

{% step %}

## Process cooldowns

If `state` is `COOLDOWN` → `ProcessCooldown` (checks whether the time has expired).
{% endstep %}
{% endstepper %}

## Data distribution (client sync)

| Event                                                                  | Scope                                                | Frequency                                                    | Content                                                      |
| ---------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `territories:receiveTerritoryData`                                     | Broadcast (`-1`) or single player                    | On request / after structural changes (zone created/deleted) | Full state of every territory                                |
| `territories:blipSync`                                                 | Broadcast (`-1`)                                     | Only when `owner`/`state`/`isContested` change               | owner, state, isContested, contestedFactions (no `progress`) |
| `territories:updateTerritoryInfo`                                      | Single player (whoever is inside)                    | Polled every `Config.UI.updateInterval` (100ms)              | Detailed state incl. `progress`, used by the HUD             |
| `territories:syncTerritoryState` / `ownerChanged` / `captureCompleted` | Only players inside the territory (`PlayerTracking`) | On event                                                     | Incremental state update                                     |

This split exists for performance: `progress` (which changes every tick) is only sent to whoever is physically inside the zone; the rest of the map only gets the "visual essentials" (blip color/sprite) when something actually changes.

## Immediate persistence vs autosave

Since the fix documented in [`ADMIN_AUTH_GUIDE.md`](broken://pages/b95ea9d3317d902fbca11ec2078e9e5653313ef6), every relevant state change is saved to the database **immediately** (in addition to the periodic 30s autosave and the save on `onResourceStop`):

* `Admin.SetOwner` / `ResetTerritory` / `SetCooldown` / `ClearCooldown`
* Capture completed, successful defense, ownership lost
* Cooldown ended

This avoids data loss when a `restart` happens right after a change, since the `onResourceStop` saves use async queries that may not finish in time.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.lostdev.store/territories/architecture.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
