RivalityFX Docsrivalityfx.com ↗

Systems

Stability: Stable.

A system is a cross-cutting gameplay module — a shrinking zone, a loot layer, a hazard — that attaches to a mode and runs alongside its runtime. It is not part of the mode and not part of the engine core: it lives in its own resource, registers once with the engine, and any mode opts in by name.

Systems exist so a mechanic that spans modes isn't copy-pasted into each one or welded into the engine. A duel, a deathmatch, and a future battle-royale can all attach the same zone system; you can ship your own, or replace one wholesale by registering the same id.

System vs driver

A driver (or a tick hook) is the mode's runtime — it owns the whole match loop, and a mode has exactly one. A system composes alongside whatever runtime the mode uses, ticks independently, and a mode can attach several. Crucially, a system ticks even for a mode with no runtime at all (no rules, no driver, no tick) — which is exactly what lets a teamless, free-for-all mode get a working zone without writing a bespoke loop.

Driver / tickSystem
Owns the match loopyes (one per mode)no — composes alongside
How many per modeoneany (systems = { 'zone', 'loot' })
Ticks for a runtime-less moden/ayes
Registered withengine (registerDriver)engine (registerSystem)
Named by the mode viadriver = '…'systems = { '…' }

registerSystem(id, def)

Register a system from its own resource (it dependency-s on engine). Like registerDriver, this is an engine export — a system is imperative logic, not the pure data a mode descriptor is.

-- resources/[systems]/system_zone/server.lua
local STATE = {}   -- your per-scene state, keyed by sceneId — the engine keeps none for you
 
exports.engine:registerSystem('zone', {
    init = function(sceneId, snapshot, config)
        STATE[sceneId] = {}                       -- seed from snapshot.map + config
    end,
    tick = function(sceneId, snapshot)
        -- shrink the ring; push it to clients; hurt whoever is outside
        exports.engine:announce(sceneId, 'system_zone:update', { --[[ ring ]] })
    end,
    teardown = function(sceneId)
        STATE[sceneId] = nil
    end,
})

def is a table of lifecycle hooks (all optional; you must supply at least one). Each runs in your VM, receives a scene id plus read-only data, and acts on the scene through the write-exports — the same boundary as a mode hook. You keep your own per-scene state keyed by sceneId; the engine stores none for you.

HookFiredSignature
initscene created, after the mode's setup(sceneId, snapshot, config)
tickevery 500 ms, after the mode runtime — for every mode, runtime or not(sceneId, snapshot)
onSpawna combatant (re)spawned (ground spawn or air drop)(sceneId, src)
onDeatha combatant died (killerSrc nil = world / suicide / timeout)(sceneId, victimSrc, killerSrc)
teardownscene destroyed — free your per-scene state(sceneId)

A system whose owning resource stops is dropped automatically (a dead callback is never invoked).

Attaching a system to a mode

A mode names the systems it wants in its descriptor — pure data, exactly like driver = '…'. Behaviour stays in the system resource.

defineMode{
    id = 'br', label = 'Battle Royale',
    -- ...
    systems = {
        'loot',                                   -- bare id
        { id = 'zone', config = { dps = 2 } },    -- id + a per-mode config block
    },
}

Each entry is a system id string, or a { id, config } table. The optional config is handed back to that system's init for this mode — the same system serves several modes with different tuning. The id is validated for shape only at the mode's load (the SDK can't see the engine's system registry); if the named system isn't registered when the mode loads, the engine prints a "not registered yet" note and attaches it once its resource comes up — benign, like the unknown-driver case.

What a system reads and writes

Reads. init/tick get the scene snapshot. For a zone, the snapshot's map carries the arena placements so you can derive the play area:

tick = function(sceneId, snapshot)
    local areas = snapshot.map.areas   -- per-team drop circles { x, y, z, r, ... }
    -- average them for a centre; live ped positions come from the standard
    -- server natives in your VM: GetEntityCoords(GetPlayerPed(src))
end

Writes. Everything in write-exports — most often announce (push your own client event to the scene; your system's client script renders it) and the hazard primitives for a zone:

  • exports.engine:hurt(sceneId, src, hp) — environmental damage to a combatant.
  • exports.engine:kill(sceneId, src) — instant elimination.

Player peds are client-authoritative, so these route through the client and the resulting death flows through the engine's normal death funnel — elimination, killfeed, spectate and stats all fire unchanged; your system never special-cases dying. See Hazards.

Client side

A system renders client-side in its own resource. The server side fans a client event with announce; the system's client script listens and draws. Hook engine:left to clear your visuals/props when a player leaves a match (the engine fans it). Nothing engine-client is required.

Example skeletons

The repo ships two empty example systems under resources/[systems]/system_zone and system_loot. They register, attach, and receive the full lifecycle, but implement no behaviour: copy one as a starting point, or replace it with your own resource that registers the same id.

See also