RivalityFX Docsrivalityfx.com ↗

Match criteria

Stability: Stable. Contract version 1 (Sdk.CRITERION_API_VERSION).

A criterion is one knob on a match: the map, the weapon set, the best-of, stamina, whether people may watch. The platform declares a set of them; your mode can retune those and add its own.

A criterion is a declaration, not markup. From one descriptor the platform derives, with no UI work:

  • the control in the Create-arena form,
  • that form's Summary row,
  • the read-only spec block — the criteria columns on custom-arena rows and on the mode cover's queue cards,
  • the server-side validator for the untrusted create payload.

That last one matters: the form and the check read the same table, so they cannot drift apart.

The platform set

Every mode automatically gets these. You do not declare them:

idLabelKindWhere it comes from
mapMapchoiceThe live battleground pool for this mode, plus two rules: Rotation (the server's map order) and Random (drawn fresh each match)
loadoutWeaponschoiceYour loadouts table, plus Random when randomLoadout is set
roundsRoundsnumberYour roundsRangeomitted entirely when the mode declares none, so a round-less royale simply has no Rounds
staminaStaminanumberA global 0–100 knob (rfx_stamina is the default)
spectatorsSpectatorstoggleSession setting; deliberately absent from the spec block

Add your own

Put them on the mode descriptor under criteria:

shared_script '@sdk/shared/criteria.lua'   -- already in your manifest: defineMode delegates `criteria` validation here
criteria = {
    {
        id = 'dropHeight', label = 'Drop height', kind = 'number',
        min = 100, max = 600, step = 50, default = 300,
        format = '%dm',
        help = 'How high the drop plane flies.',
    },
    {
        id = 'stormSpeed', label = 'Storm', kind = 'choice',
        options = {
            { key = 'slow',  label = 'Slow',  sub = 'Long games' },
            { key = 'brisk', label = 'Brisk' },
            { key = 'brutal', label = 'Brutal', sub = 'Ten-minute matches' },
        },
        default = 'brisk',
    },
}

Both appear in the create form, in its summary, on every arena row and queue card for the mode, and in the arena's config — validated — with nothing else to write.

Use the same id as a platform criterion to override it — the fields you give are merged onto the platform one, so you can retune a label, a range or a default without restating the whole thing.

Fields

FieldTypeRequiredNotes
idstringyesStable wire key: the create-payload field and the arena config key
labelstringyesThe one human label — form, summary and spec block all use it
kindstringyeschoice | toggle | number | text
groupstringnomatch (what the fight is — default) or session (how it is run)
ordernumbernoSort within the group (default 100)
defaultanynoThe value the form opens on
helpstringnoOne line under the control; also the tooltip on read-only surfaces
optionsarraychoice{ key, label, sub?, present?, roll? }
optionsFromstringchoiceA server-resolved provider: battlegrounds | loadouts
min / max / stepnumbernumbermin/max are required for number; the server clamps and snaps to step
formatstringnoOne formatter for every surface, e.g. 'Bo%d', '%d%%'. A single %d/%s, plus %% for a literal percent
valueLabelstablenoWords for the values where a bare number misleads, keyed by value-as-string — e.g. { ['0'] = 'None', ['100'] = 'Full' }
secretbooleantextRender as a password field
tierstringnoall (default) | vip | staff — the tier required to change it
specbooleannoShow in the read-only spec block (default: true for match, false for session)
enabledWhentableno{ id = '<other criterion>', is = <value> } — the one dependency primitive

Option fields

FieldNotes
keyThe stored value
labelDisplay name (defaults to the key, title-cased)
subSecondary line — a weapon class, a map's character
presentA presentation block. Its card (else hero) turns the option into a media card with a thumbnail
rollThis value is not a fixed pick — it is drawn at match start. Rendered muted everywhere, so "Random" never reads as a thing literally named Random

You do not choose the control

kind describes the value shape the server validates. How it is drawn is a scaling policy that lives in one place:

OptionsControl
≤ 4, no artSegmented control
more than 4, or any option has artWrapping card grid
≥ 10Card grid with a filter and its own scroll box

This is deliberate. An author who could pick the control would eventually pick one that looks right at four options and hides the seventh — which is exactly the bug this contract replaced.

Gating a criterion

tier marks a knob as a perk. Below that tier the control renders disabled at its default with the requirement named rather than hidden, and the server ignores whatever the client sent for it. Advertising a perk sells it; hiding it does not. Use it sparingly — one locked control reads as an upsell, four read as nagware.

Validation

The server sanitises every value against your descriptor before an arena is opened:

  • choice — the key must be one of the options, or the request is refused with a reason;
  • number — clamped to [min, max] and snapped to step;
  • toggle — coerced to a boolean;
  • text — trimmed, control characters and colour codes stripped, length-capped.

Errors in the descriptor itself are caught at your resource's load, tied to your resource, by defineMode / defineCriterion — an option default that is not in options, a number with no range, a duplicate id, an unknown optionsFrom.

Limits, on purpose

There are four kinds, one dependency primitive, and no layout fields. This is a declaration format, not a form language: the moment a knob needs conditional visibility trees or cross-field validation, hand-write that surface beside the generated form. An inner-platform form builder would be worse than the code it replaced.

Keep a mode at five or fewer spec criteria — the spec block's whole virtue is fitting on a browser row and a queue card. The SDK warns past that.

See also