RE:CORD
REC_PlayerManager

Stracture

Welcome to REC_PlayerManager documentation.
Added
Updated
config/sv_config.lua

---[[
---     REC_PlayerManager server config
---     What each setting does, and why the defaults are what they are:
---     https://docs.re-cord.dev/en/resources/rec_playermanager
---]]

---@type REC_Library.Server.Class.Staff.StaffConfig
local staffConfig = require "@REC_Library.server.class.staff.sv_staffConfig"

---@type REC_Library.Server.Class.Web.WebConfig
local webConfig = require "@REC_Library.server.class.web.sv_webConfig"

---@type REC_Library.Server.Class.Web.WebConfigStore
local WebConfigStore = require "@REC_Library.server.class.web.sv_webConfigStore"

---[[
---     Settings the admin panel owns
---     They are declared in server/sv_settings.lua (value, range and description) and
---     changed from the panel, so they are not repeated here.
---]]
---@type REC_PlayerManager.Server.Settings
local settings = require "@REC_PlayerManager.server.sv_settings"

---@class REC_PlayerManager.Server.Config
local config = {}

---[[
---    Debug mode
---]]
---@type boolean
config.debugMode = true

---[[
---     ACE permissions that get through while the server is closed
---     The window itself belongs to the admin panel. This list stays here because it
---     is a permission boundary, and the panel must not be able to widen it.
---     Used by the admin only window, maintenance mode and the reserved slots.
---]]
config.connectControl = {

    adminOnly = {

        ---@type string[]
        permissions = {
            "admin",
            "support",
        },
    },
}

---[[
---     Maintenance mode
---     Toggled at runtime from the admin UI, this is only the boot default.
---]]
config.maintenance = {

    ---@type boolean
    enabled = false,

    ---@type boolean
    persist = true, -- remember across restarts, needs rec_playermanager.sql
}

---[[
---     Admin action log webhook
---     Whether it records at all belongs to the admin panel. The url stays here,
---     because a read only token can see that page and a webhook url is a credential.
---         set REC_PlayerManager:webhook "https://discord.com/api/webhooks/..."
---]]
config.audit = {

    webhook = {

        ---@type string
        url = GetConvar(GetCurrentResourceName() .. ":webhook", ""),

        ---@type string
        userName = "REC_PlayerManager",

        ---@type string
        avatarUrl = "",
    },
}

---[[
---     Activity map geometry
---     The sampling and the cell size belong to the admin panel. These two stay here
---     because they are shapes rather than knobs, and changing them re-buckets every
---     row already stored.
---]]
config.heatmap = {

    ---[[
    ---     World rectangle the grid covers, minX and minY must be multiples of 100
    ---     Coordinates outside it are clamped to the edge.
    ---]]
    ---@type { minX: number, minY: number, maxX: number, maxY: number }
    worldBounds = {
        minX = -4000,
        minY = -4000,
        maxX = 4500,
        maxY = 8000,
    },

    ---[[
    ---     World rectangle an optional map image covers, nil assumes worldBounds
    ---     Drop a render into web/map/ as map.webp, map.jpg or map.png and it is drawn
    ---     under the heat (no image ships with the resource, the game map imagery is
    ---     Rockstar's). Nudge these numbers until the landmarks sit where they belong.
    ---]]
    ---@type { minX: number, minY: number, maxX: number, maxY: number }|nil
    mapImage = nil,
}

---[[
---     Characters left out of the activity figures
---     They are dropped from the heatmap samples and from the playtime ranking. The
---     session rows are still written: when an admin was on the server is what the
---     audit page is for, and the roster still lists them like anyone else.
---
---     The shared list lives in REC_Library server/sv_config.lua and covers every
---     REC_* resource at once. A convar does the same without touching a file:
---         set REC:staffCitizenIds "ABCDEFGH, K3PQ81ZR"
---         set REC_PlayerManager:staffCitizenIds "ABCDEFGH"   # this panel alone
---]]
config.staff = staffConfig:build({

    ---[[
    ---     A heatmap sample can land before the character is known, so these answer
    ---     for anyone holding the permission, not only for a listed character.
    ---     Replaces the default { "admin" } rather than adding to it, and an empty
    ---     one is taken at its word: the ACE half of the check then never matches.
    ---]]
    ---@type string[]
    aceGroups = {
        "admin",
        "support",
    },

    ---[[
    ---     Added on top of the shared list, never replacing it
    ---     An empty one changes nothing, so the shared list still applies.
    ---]]
    ---@type string[]
    -- citizenIds = {
    --     "ABCDEFGH",
    -- },

    -- false counts staff like anyone else, so the heatmap and the ranking include them
    -- enabled = false,

    -- false drops the shared list, leaving only the citizenIds above
    -- useSharedList = false,
})

---[[
---     Admin UI
---     The in-game UI (/playermanager) is always available and is gated by aceGroups.
---     http below only affects the browser route.
---
---     Tokens, dist dir and the address allowlist are shared defaults from
---     REC_Library. One convar covers every REC_* panel, so set these once:
---         set REC:adminToken "xxxxx"
---         set REC:supportToken "yyyyy"
---         set REC:allowedAddresses "192.168.1.0/24"   # opens the LAN everywhere
---
---     Naming this resource narrows one of them to this panel alone:
---         set REC_PlayerManager:supportToken "zzzzz"
---]]
config.web = webConfig:build(config.debugMode, {

    ---[[
    ---     Permission areas this resource understands, as "<area>:read" / "<area>:write"
    ---]]
    ---@type string[]
    areas = {
        "players",     -- the roster (write kicks, messages and moves buckets)
        "sessions",    -- the online history, sessions, playtime and heatmap
        "server",      -- the audit log (write flips maintenance)
        "settings",    -- the runtime config overrides (write changes them)
    },

    ---[[
    ---     Who opens the in-game panel, and what they may do once inside
    ---     Added on top of the shared list in REC_Library (admin, full access), so
    ---     naming one narrow grant here can never drop the row that lets an admin in.
    ---     Every entry that matches contributes its scopes, and a player holding two
    ---     of them gets the union. There is no deny form: to take something away,
    ---     stop granting it. Naming both an ace and a job means the player needs both.
    ---]]
    ---@type REC_Library.Server.Class.Web.WebConfig.Grant[]
    grants = {
        -- the old aceGroups.manage is a grant carrying writes, view is one without
        -- { ace = "support", scopes = { "read", }, },
    },

    -- false leaves the shared list out, so only the grants above count here
    -- useSharedGrants = false,

    http = {

        ---@type boolean
        enabled = true, -- RELEASE:false

        ---[[
        ---     Refuse every write over the browser route, the in-game UI is unaffected
        ---     Off by default: each token carries its own scopes, so hand a viewer a
        ---     read only token instead. Set true to block browser writes wholesale.
        ---]]
        ---@type boolean
        readOnly = false,

        -- where the built panel lives, relative to this resource
        -- distDir = "web/build",

        -- replaces the convar built admin/support pair entirely, which puts a literal
        -- token in this file: reach for customTokens below instead
        -- tokens = {
        --     ["a-literal-token"] = { label = "audit", scopes = { "read" }, },
        -- },

        -- named tokens beyond admin/support, each scoped on its own: https://docs.re-cord.dev/en/resources/rec_library/web-config
        -- customTokens = {
        --     { name = "playersOnly", scopes = { "players" }, },
        --     { name = "fullAccess", scopes = { "*" }, },
        -- },

        -- replaces the convar-built address list entirely, keep loopback in it
        -- allowedAddresses = {
        --     "127.0.0.0/8",
        --     "192.168.1.0/24",
        -- },

        -- trustedProxies = {
        --     "10.0.0.5",
        -- },
    },
}) --[[@as REC_PlayerManager.Server.Config.Web]]


-- put the panel owned defaults in place before anything reads them
WebConfigStore.seed(config, settings.list)

return config

---@class REC_PlayerManager.Server.Config.Web: REC_Library.Server.Class.Web.WebConfig.Result

---@class REC_PlayerManager.Server.Config
---@field staff REC_Library.Server.Class.Staff.Staff

---[[
---     The panel owned settings fill these in, so the shapes live here for completion
---]]

---@class REC_PlayerManager.Config.Heatmap
---@field enabled boolean
---@field sampleInterval integer
---@field flushInterval integer
---@field cellSize integer
---@field onlyDefaultBucket boolean
---@field idleSamples integer
---@field idleRadius number
---@field maxCells integer
---@field cacheTtl integer
---@field worldBounds { minX: number, minY: number, maxX: number, maxY: number }
---@field mapImage { minX: number, minY: number, maxX: number, maxY: number }|nil

---@class REC_PlayerManager.Config.Offline
---@field enabled boolean
---@field limit integer
---@field cacheTtl integer

---@class REC_PlayerManager.Config.Retention
---@field sessionDays integer
---@field heatmapDays integer
©2026 RE:CORD Development Group. All rights reserved.