Koja Scripts

Anti combat log & restrictions

How Koja Spawn Selector handles combat logging, relogging, death, jail and forced spawns, with the config and the server API to add your own rules.

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

  1. 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.
  2. 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.
  3. 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.
  4. The next spawn is limited

    SituationDefault 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 rulesSee 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.
  5. 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

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) },
}
OptionWhat it does
CombatWindowSeconds a player stays "in combat" after the last damage
LastLocation'lock' shows it locked, 'hide' removes it, 'none' leaves it alone
LockMinutesHow long after the logout the combat lock applies
DisplayThe red marker for nearby players
WebhookDiscord webhook URL, empty = off
Death.ForceHospitalForce Death.Hospital after dying / when dead on login
Jail.SpawnThe 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

local spawn = exports['koja-spawnselector']

Combat

spawn:MarkCombat(source, seconds)   -- seconds defaults to CombatWindow
spawn:IsInCombat(source)
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

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

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:

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 fieldDescription
identifierCharacter identifier
coords, updatedSaved last location and when it was saved
dropThe drop record table (or empty)
dead, combatFrom the drop record
kind, reason, droppedAt, sinceDropDisconnect type, reason, unix time, seconds since
nowServer time

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

Restrictions

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

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')
FieldDescription
reasonShown on the banner and on every locked spot
spawnA spawn definition (forced and added to the list) or the id of an existing spot
allowExtra spawn ids that stay pickable
duration / untilSeconds from now / unix time. Omit for "until cleared"

Live restrictions are computed on every open:

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

EventSideArguments
koja-spawnselector:server:combatLogServeridentifier, record (name, character, kind, reason, dead, coords, source)
koja-spawnselector:client:combatLoggedClientpayload of the marker shown to this player
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)