RE:CORD
REC_HeistManager

Stracture

Installation, configuration, and usage for REC_HeistManager.
Added
Updated

The following configuration files and manifest are taken from the current source.

config/sh_config.lua

---@type REC_Utils.Shared.Enum
local shEnum = require "@REC_Utils.shared.sh_enum"
local placeEntityTypes = shEnum.placeentityTypes

---@class REC_HeistManager.Shared.Config
local config = {}

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


config.dev = {}

---[[
---
---]]
config.dev.entityPlacer = {

    default = {

        ---@type string
        model = "sf_prop_sf_art_basketball_01a",

        ---@type string[]
        defaultHelpText = {
            "~INPUT_CELLPHONE_CANCEL~ でキャンセル  ~INPUT_ATTACK~ で設置   ",
            "~INPUT_JUMP~ でモデル変更 ~INPUT_INTERACTION_MENU~ で設置モード切替",
            "~INPUT_SPRINT~ + ~INPUT_FRONTEND_RDOWN~ で出力",
        },
    },
}

---[[
---     Patrol guards
---
---     Every value here is the default a guard falls back to, so a heist only writes
---     what it wants different. A guard becomes a watcher by declaring patrol or vision,
---     guards that declare neither behave exactly as they did before.
---]]
config.guard = {

    ---[[
    ---     Walking the route
    ---]]
    patrol = {

        ---@type "idle" | "loop" | "pingpong" | "random"
        mode = "loop",

        ---@type "walk" | "run"
        speed = "walk",

        ---@type { min: integer, max: integer, } how long it stands at a waypoint, ms
        waitTime = { min = 3000, max = 8000, },

        ---@type number the waypoint counts as reached from here
        arriveDistance = 1.5,

        ---@type integer ms before a guard that cannot reach a waypoint is sent to the next one
        moveTimeout = 25000,
    },

    ---[[
    ---     What a guard notices
    ---]]
    vision = {

        ---@type number metres it can see
        distance = 22.0,

        ---@type number full cone in degrees
        fov = 100.0,

        ---@type number seconds inside the cone before the player is spotted
        detectSeconds = 1.8,

        ---@type number seconds it takes to forget somebody it lost sight of
        forgetSeconds = 6.0,

        ---@type number seen at once inside this radius, no matter where it looks
        proximity = 2.5,

        ---@type number sprinting and shooting inside this radius is heard
        hearing = 12.0,

        ---@type boolean ignore the player until the meter is full, which is what makes sneaking possible
        blockEvents = true,
    },

    ---[[
    ---     Day and night
    ---
    ---     Every multiplier is applied to a guard's vision as the game clock moves into
    ---     the night, and fades out again at dawn. A guard can bring its own set in
    ---     vision.timeOfDay, or opt out of it with vision.timeOfDay = false.
    ---]]
    timeOfDay = {

        ---@type boolean
        enabled = true,

        ---@type { from: integer, to: integer, } clock hours of the night, may wrap past midnight
        night = { from = 20, to = 6, },

        ---@type integer game minutes the fade takes after dusk and before dawn
        transitionMinutes = 60,

        ---[[
        ---     What the night does, 1.0 changes nothing
        ---]]
        multipliers = {

            ---@type number it sees shorter
            distance = 0.6,

            ---@type number and narrower
            fov = 0.85,

            ---@type number
            proximity = 1.0,

            ---@type number and takes longer to be sure
            detectSeconds = 1.3,

            ---@type number but hears further
            hearing = 1.6,
        },
    },

    ---[[
    ---     What happens once somebody is spotted
    ---]]
    alert = {

        ---@type "investigate" | "combat"
        escalate = "combat",

        ---@type number other guards of the same location that join in
        shareRadius = 60.0,

        ---@type boolean a guard going down raises the alarm on its own
        raiseOnDeath = true,

        ---@type integer quiet seconds before one level is dropped
        holdSeconds = 200,

        ---@type integer how often the server checks for that quiet time, ms
        tickInterval = 1000,

        ---[[
        ---     Losing the crew after an alarm. Once alerted drops to suspicious the
        ---     guards close in on a guess of where the crew went and look around from
        ---     a short distance, until the location calms down.
        ---]]
        search = {

            ---@type boolean
            enabled = true,

            ---@type number metres the guess is off by, so the crew is not walked straight into
            scatter = 4.0,

            ---@type number metres short of the guess the guard stops at
            stopDistance = 6.0,

            ---@type "walk" | "run"
            speed = "walk",

            ---@type number degrees it sweeps to either side while it looks around
            sweep = 60.0,

            ---@type integer ms between two glances
            glanceInterval = 2500,
        },

        ---[[
        ---     What knowing about an alarm does to a guard's senses.
        ---     Applied on top of the night, so a guard that was told something sees
        ---     further and needs less time to be sure of what it saw.
        ---]]
        senses = {

            ---@type boolean
            enabled = true,

            ---@type number
            distance = 1.2,

            ---@type number
            fov = 1.2,

            ---@type number
            proximity = 1.3,

            ---@type number lower is quicker
            detectSeconds = 0.65,

            ---@type number
            hearing = 1.5,
        },

        ---[[
        ---     How a guard carries itself once it knows something is wrong
        ---]]
        stance = {

            ---@type boolean
            enabled = true,

            ---@type "action" | "clipset" the game's own ready walk, or a movement clipset of your own
            mode = "action",

            ---@type string|false movement clipset while it is not calm, read on the clipset mode
            clipset = "move_ped_crouched",

            ---@type string|false the strafe that goes with it, false leaves the game's own
            strafeClipset = "move_ped_crouched_strafing",

            ---@type boolean drop the posture once it is a fight, the game carries a fight on its own
            uprightWhenAlerted = true,

            ---@type integer 0 to 3, how twitchy the game's own AI is about it
            alertness = 3,
        },

        ---[[
        ---     Walking into the body of another guard.
        ---     A quiet kill only stays quiet while nobody comes past.
        ---]]
        body = {

            ---@type boolean
            enabled = true,

            ---@type "suspicious" | "alerted" what a body does to the location
            level = "alerted",

            ---@type number metres a body is noticed from, never further than the guard sees
            distance = 14.0,

            ---@type boolean the body has to be in the cone, not only in range
            requireFov = false,

            ---@type integer ms between two rounds of looking, per guard
            interval = 1500,

            ---@type integer ms before the same body is worth reporting again
            reportInterval = 30000,
        },

        ---[[
        ---     How the alarm travels. The guard that noticed something tells the ones
        ---     around it, those tell the ones around them, and a guard that was told
        ---     nothing keeps walking its route.
        ---]]
        spread = {

            ---@type boolean false puts every guard of the location on alert at once
            enabled = true,

            ---@type integer ms before the guards that know pass it on one step further
            stepInterval = 1500,
        },
    },

    ---[[
    ---     How fast the meter fills. Every multiplier is applied to the fill rate.
    ---]]
    detection = {

        ---@type boolean only the crew is worth reacting to
        membersOnly = true,

        ---[[
        ---     A player the game is not drawing is not there as far as the guards are
        ---     concerned. Admins watching, cutscenes and anything else that hides the
        ---     ped stop filling the meter and stop making noise.
        ---]]
        ---@type boolean
        ignoreInvisible = true,

        ---@type number the meter counts as suspicious from here, 1.0 is spotted
        suspicionAt = 0.45,

        ---@type number
        sprintMultiplier = 1.6,

        ---@type number
        crouchMultiplier = 0.45,

        ---@type number
        aimMultiplier = 1.35,

        ---@type number
        vehicleMultiplier = 1.4,

        ---@type integer ms between two reports of the same client
        reportInterval = 1500,
    },

    ---[[
    ---     Noise the crew makes.
    ---
    ---     A noise never says where the crew is, only where the sound came from, so
    ---     the guards that heard it go and look there instead of walking into it.
    ---]]
    noise = {

        ---@type boolean
        enabled = true,

        ---@type "suspicious" | "alerted" what a noise does to the location
        level = "suspicious",

        ---@type integer ms between two noises of the same kind one client may report
        reportInterval = 3000,

        ---[[
        ---     Noise that keeps coming. One shot is worth a look, shot after shot is
        ---     worth the alarm.
        ---]]
        escalate = {

            ---@type boolean
            enabled = true,

            ---@type integer noises heard inside the window before the level goes up
            count = 3,

            ---@type integer ms the count is kept for
            window = 20000,

            ---@type "suspicious" | "alerted"
            level = "alerted",
        },

        gunshot = {

            ---@type boolean
            enabled = true,

            ---@type number metres the shot carries
            radius = 70.0,

            ---@type number applied to the radius while the weapon is suppressed
            silencedMultiplier = 0.3,

            ---@type boolean the guards walk over to look, false only turns them that way
            investigate = true,
        },

        horn = {

            ---@type boolean
            enabled = true,

            ---@type number metres a horn carries
            radius = 60.0,

            ---@type boolean
            investigate = true,
        },

        vehicle = {

            ---@type boolean
            enabled = true,

            ---@type number metres an engine carries
            radius = 32.0,

            ---@type number m/s from which a vehicle is loud at all
            minSpeed = 5.0,

            ---[[
            ---     An engine keeps making noise, so the guards only look that way.
            ---     Turning this on has them chase a car that is still driving.
            ---]]
            ---@type boolean
            investigate = false,
        },

        ---[[
        ---     A helicopter or a plane with its engine running, hovering or not.
        ---     Rotors carry far and straight down, so the radius has to cover the height too.
        ---]]
        aircraft = {

            ---@type boolean
            enabled = true,

            ---@type number metres the rotors carry
            radius = 150.0,

            ---@type boolean the guards only look up, they cannot walk over to it
            investigate = false,
        },

        ---[[
        ---     Anything going off nearby, whoever set it off
        ---]]
        explosion = {

            ---@type boolean
            enabled = true,

            ---@type number metres the blast is heard
            radius = 160.0,

            ---@type number metres around the player a blast is picked up at all
            detectRadius = 45.0,

            ---@type "suspicious" | "alerted" a blast is never just a noise
            level = "alerted",

            ---@type boolean
            investigate = true,
        },

        ---[[
        ---     Swimming. On the surface it splashes, under it almost nothing gets out.
        ---]]
        swim = {

            ---@type boolean
            enabled = true,

            ---@type number metres the splashing carries
            radius = 20.0,

            ---@type boolean
            investigate = true,
        },

        dive = {

            ---@type boolean
            enabled = true,

            ---@type number metres it carries, the surface swallows the rest
            radius = 7.0,

            ---@type boolean
            investigate = false,
        },
    },

    ---[[
    ---     Fire. A guard walks around what is burning instead of into it.
    ---]]
    fire = {

        ---@type boolean
        enabled = true,

        ---@type number metres of fire around a waypoint that has it skipped
        waypointRadius = 6.0,

        ---@type number metres of fire around the guard itself before it backs off
        fleeRadius = 7.0,

        ---@type number metres it backs off to
        fleeDistance = 14.0,

        ---@type integer ms between two looks, per guard
        checkInterval = 1000,
    },

    ---[[
    ---     What becomes of a guard once it is down.
    ---     A networked body keeps its network id and keeps changing owner for as long
    ---     as it lies there, so the server lets it go once nobody is close enough to
    ---     see the swap, and every client lays a local body in its place.
    ---]]
    corpse = {

        ---@type boolean false keeps the networked body until the phase ends
        replaceLocal = true,

        ---@type integer ms the networked body stays after the death, so the fall itself is seen
        delay = 8000,

        ---@type number metres, no player may stand closer to the body when it is swapped
        clearRadius = 40.0,

        ---@type integer ms between two rounds of looking at the bodies, per location
        checkInterval = 2000,
    },

    ---[[
    ---     Thread intervals. sense is what the meter is timed against.
    ---]]
    tick = {

        ---@type integer ms between two rounds of guard tasking
        drive = 500,

        ---@type integer ms between two vision checks
        sense = 150,
    },

    ---[[
    ---     The in-world route recorder (/heistroute)
    ---     Control ids, so they can be moved to keys that suit the server
    ---]]
    route = {

        controls = {

            ---@type integer INPUT_CONTEXT (E)
            add = 38,

            ---@type integer INPUT_CELLPHONE_CANCEL (BACKSPACE)
            undo = 177,

            ---@type integer INPUT_FRONTEND_RDOWN (ENTER)
            finish = 191,

            ---@type integer INPUT_FRONTEND_PAUSE_ALTERNATE (ESC)
            cancel = 200,
        },

        ---@type "loop" | "pingpong" | "random" written into the snippet, change it there when it is not what you want
        mode = "loop",

        ---@type "walk" | "run"
        speed = "walk",

        ---@type integer decimals of the recorded coordinates
        precision = 2,
    },

    ---[[
    ---     What the crew is told when the location changes its mind
    ---]]
    feedback = {

        ---@type boolean a notification to every member on each level change, also when a body is found or a noise is heard
        notify = true,

        ---[[
        ---     Frontend sound played on this client when the level changes, false for silence
        ---]]
        sounds = {
            ---@type { name: string, set: string }|false
            suspicious = { name = "TIMER_STOP", set = "HUD_MINI_GAME_SOUNDSET", },
            ---@type { name: string, set: string }|false
            alerted = { name = "Beep_Red", set = "DLC_HEIST_HACKING_SNAKE_SOUNDS", },
            ---@type { name: string, set: string }|false
            calm = false,
        },
    },

    ---[[
    ---     The detection meter, a REC_Library gauge drawn while a guard is filling it
    ---]]
    hud = {

        ---@type boolean false draws no meter at all, the guards still sense as usual
        enabled = true,

        ---[[
        ---     A marker over the guard that is filling the meter
        ---]]
        ---@type boolean
        marker = true,

        ---@type REC_Library.Shared.Enums.HelpTextPosition
        position = "bottom-center",

        ---@type "bar" | "ring"
        shape = "bar",

        ---@type "card" | "plain" plain is the bare bar and wording the native meter had
        variant = "plain",

        ---@type string|false CSS width of the bar, false for the REC_Library default
        width = "10vw",

        ---@type string|false font awesome icon, plain never draws it
        icon = false,

        ---@type boolean draw the percentage next to the wording
        showValue = false,

        ---[[
        ---     Fill colour for each alert level, suspicious also once the meter passes detection.suspicionAt
        ---]]
        colors = {
            ---@type string
            calm = "#ebebeb",
            ---@type string
            suspicious = "#e19822",
            ---@type string
            alerted = "#d63031",
        },
    },
}

---[[
---     Relationship groups
---
---     A heist that names nothing in registerClient gets these. member, whitelist and
---     guard are filled by the runtime, player is the group every other player already
---     stands in and cannot be renamed. relations read as [from][to], and a pair that
---     does not declare its reverse is applied both ways.
---]]
config.relationship = {

    ---@type table<string, string> appended to the heist key when it names no group of its own
    groupSuffixes = {
        member = "_PLAYER_MEMBER",
        whitelist = "_PLAYER_POLICE",
        guard = "_NPC_ENEMY",
    },

    ---@type table<string, table<string, REC_HeistManager.Shared.Enum.RelationshipTypes>>
    relations = {
        guard = {
            guard = "companion",
            whitelist = "companion",
            member = "hate",
            player = "neutral",
        },
    },

    ---[[
    ---     A member that is down is not a target. Every client publishes its own
    ---     downed state under this key, the clients driving the guards read it.
    ---]]
    ---@type string
    downedStateBagKey = "RECHM_isDowned",
}

---[[
---     Dropoff
---]]
config.dropoff = {

    ---[[
    ---     Played when a member hands the loot over.
    ---     A heist can override it per registration.
    ---]]
    ---@type string
    cutsceneName = "hs3f_all_drp3",
}

---[[
---     Cross-heist cooldowns
---
---     Both are shared by every heistKey, unlike timing.heistCooldown which stays
---     local to the location that registered it. 0 disables a check, which is the
---     default, so a server that never touches this file behaves exactly as before.
---]]
config.cooldown = {

    ---[[
    ---     One lockout the whole server shares. Started when any heist of any kind
    ---     completes, so nobody may order anything until it lifts.
    ---]]
    global = {

        ---@type integer seconds, 0 disables it
        seconds = 0,
    },

    ---[[
    ---     One lockout per citizenId, shared by every heistKey. Started for every
    ---     member of a crew, owner included, the moment its order is accepted.
    ---]]
    player = {

        ---@type integer seconds, 0 disables it
        seconds = 0,

        ---@type "memory" | "database" memory resets on a restart, database survives it
        persistence = "memory",
    },
}

return config
©2026 RE:CORD Development Group. All rights reserved.