# Anti combat log & restrictions

Players should not escape a fight, a death or a sentence by disconnecting or relogging. Everything on this page is decided on the server.

## How it works

:::steps

### Combat is tracked

When a player damages another player (any weapon or melee), both are marked as **in combat** for `Config.AntiCombatLog.CombatWindow` seconds.
Other scripts can mark players too — cuffing, robbing, a police chase — with [`MarkCombat`](#server-api).

### Leaving is recorded

On disconnect (`drop`) and on a character switch (`relog`), the server stores a **drop record** for the character: where they were, whether they were dead, whether they were in combat, and the disconnect reason.

### Combat loggers are exposed

When the player was in combat:

- Players within `Display.Radius` see a red marker where they left, with their name, server id and whether they were dead, for `Display.Duration` seconds.
- A Discord log is sent to `Config.AntiCombatLog.Webhook`.
- `ServerEditable.OnCombatLog` and the event `koja-spawnselector:server:combatLog` run, so you can jail, fine or ban.

### The next spawn is limited

| Situation | Default result |
| --- | --- |
| Combat logout or relog | "Last location" is locked ("Combat logout") for `LockMinutes` |
| Died, then left | "Last location" is locked ("You died here") and the hospital is forced |
| Dead on login (QB `isdead` / `inlaststand`, ESX `users.is_dead`) | The hospital is forced |
| In jail (QB `injail`, ESX `users.jail_time` or `users.jail`) | The prison is forced |
| Your own rules | See below |

A **forced** spawn is the only one that can be picked; the rest of the list stays visible but locked with the reason, the selector opens on it and ESC is disabled. Cancelling from the client is refused by the server and the selector opens again.

### The record is cleared

After the player lands, the drop record is removed. Locks you set with `LockLastLocation` stay until their own time runs out.

:::

## Config

```lua title="shared/config.lua"
Config.AntiCombatLog = {
    Enabled = true,
    CombatWindow = 60,
    LastLocation = 'lock',
    LockMinutes = 15,
    Display = { Enabled = true, Duration = 120, Radius = 80.0 },
    Webhook = '',
}

Config.Death = {
    Enabled = true,
    LastLocation = 'lock',
    ForceHospital = true,
    Hospital = { id = 'hospital', label = 'Pillbox Medical Center', category = 'job', icon = 'hospital', coords = vec4(298.62, -584.58, 43.26, 70.0) },
}

Config.Jail = {
    Enabled = true,
    Spawn = { id = 'prison', label = 'Bolingbroke Penitentiary', category = 'job', icon = 'lock', coords = vec4(1846.4, 2585.9, 45.67, 270.0) },
}
```

| Option | What it does |
| --- | --- |
| `CombatWindow` | Seconds a player stays "in combat" after the last damage |
| `LastLocation` | `'lock'` shows it locked, `'hide'` removes it, `'none'` leaves it alone |
| `LockMinutes` | How long after the logout the combat lock applies |
| `Display` | The red marker for nearby players |
| `Webhook` | Discord webhook URL, empty = off |
| `Death.ForceHospital` | Force `Death.Hospital` after dying / when dead on login |
| `Jail.Spawn` | The spawn forced while jailed |

Death and jail detection live in `server/editable.lua` (`IsDead`, `IsDeadOnLogin`, `IsJailed`). Change them if your ambulance or jail script stores this somewhere else.

## Server API

```lua
local spawn = exports['koja-spawnselector']
```

### Combat

```lua
spawn:MarkCombat(source, seconds)   -- seconds defaults to CombatWindow
spawn:IsInCombat(source)
```

```lua title="Mark both players when someone gets cuffed"
AddEventHandler('police:server:cuffed', function(officer, target)
    exports['koja-spawnselector']:MarkCombat(officer, 120)
    exports['koja-spawnselector']:MarkCombat(target, 300)
end)
```

### Drop record

```lua
local record = spawn:GetDropRecord(sourceOrIdentifier)
-- { kind = 'drop' | 'relog', reason = 'Exiting', at = 1793532461, dead = false, combat = true, coords = { x, y, z, w } } or nil
spawn:ClearDropRecord(sourceOrIdentifier)
```

### Lock the last location

```lua
spawn:LockLastLocation(sourceOrIdentifier, 'Inside the vault', 1800) -- seconds, nil = until unlocked
spawn:UnlockLastLocation(sourceOrIdentifier)
```

Use it from a heist, an event island or an interior so nobody logs back in there.

### Last location rules

A rule is asked every time the selector opens, after the built-in combat and death rules:

```lua
spawn:AddLastLocationRule('heist', function(source, ctx)
    if ctx.combat and ctx.sinceDrop and ctx.sinceDrop < 3600 then
        return { action = 'hide' }
    end
    if ctx.coords and #(vector3(ctx.coords.x, ctx.coords.y, ctx.coords.z) - vector3(255.0, 225.0, 101.0)) < 30.0 then
        return { action = 'lock', reason = 'Bank vault' }
    end
end)
spawn:RemoveLastLocationRule('heist')
```

| `ctx` field | Description |
| --- | --- |
| `identifier` | Character identifier |
| `coords`, `updated` | Saved last location and when it was saved |
| `drop` | The drop record table (or empty) |
| `dead`, `combat` | From the drop record |
| `kind`, `reason`, `droppedAt`, `sinceDrop` | Disconnect type, reason, unix time, seconds since |
| `now` | Server time |

Return `{ action = 'lock' | 'hide', reason = '...' }` or `nil`.

### Restrictions

A restriction limits the whole list. Persisted ones survive restarts and are stored per character:

```lua
spawn:SetRestriction(sourceOrIdentifier, 'community_service', {
    reason = 'Community service',
    spawn = { id = 'cs', label = 'Community service', category = 'job', coords = vec4(170.6, -991.3, 30.1, 160.0) },
    duration = 3600,
})
spawn:ClearRestriction(sourceOrIdentifier, 'community_service')
```

| Field | Description |
| --- | --- |
| `reason` | Shown on the banner and on every locked spot |
| `spawn` | A spawn definition (forced and added to the list) or the id of an existing spot |
| `allow` | Extra spawn ids that stay pickable |
| `duration` / `until` | Seconds from now / unix time. Omit for "until cleared" |

Live restrictions are computed on every open:

```lua
spawn:AddRestrictionHandler('wanted', function(source, ctx)
    if Player(source).state.wanted then
        return { reason = 'You are wanted', allow = { 'sandy', 'paleto' } }
    end
end)
spawn:RemoveRestrictionHandler('wanted')
```

Only the first active restriction applies (jail, then hospital, then saved ones, then handlers).

## Events

| Event | Side | Arguments |
| --- | --- | --- |
| `koja-spawnselector:server:combatLog` | Server | `identifier, record` (`name`, `character`, `kind`, `reason`, `dead`, `coords`, `source`) |
| `koja-spawnselector:client:combatLogged` | Client | `payload` of the marker shown to this player |

```lua title="Jail combat loggers on their next login"
AddEventHandler('koja-spawnselector:server:combatLog', function(identifier, record)
    exports['koja-spawnselector']:SetRestriction(identifier, 'combatlog', {
        reason = 'Combat logout — 10 minutes in prison',
        spawn = 'prison',
        duration = 600,
    })
end)
```
