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:
| id | Label | Kind | Where it comes from |
|---|---|---|---|
map | Map | choice | The live battleground pool for this mode, plus two rules: Rotation (the server's map order) and Random (drawn fresh each match) |
loadout | Weapons | choice | Your loadouts table, plus Random when randomLoadout is set |
rounds | Rounds | number | Your roundsRange — omitted entirely when the mode declares none, so a round-less royale simply has no Rounds |
stamina | Stamina | number | A global 0–100 knob (rfx_stamina is the default) |
spectators | Spectators | toggle | Session 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 herecriteria = {
{
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
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Stable wire key: the create-payload field and the arena config key |
label | string | yes | The one human label — form, summary and spec block all use it |
kind | string | yes | choice | toggle | number | text |
group | string | no | match (what the fight is — default) or session (how it is run) |
order | number | no | Sort within the group (default 100) |
default | any | no | The value the form opens on |
help | string | no | One line under the control; also the tooltip on read-only surfaces |
options | array | choice | { key, label, sub?, present?, roll? } |
optionsFrom | string | choice | A server-resolved provider: battlegrounds | loadouts |
min / max / step | number | number | min/max are required for number; the server clamps and snaps to step |
format | string | no | One formatter for every surface, e.g. 'Bo%d', '%d%%'. A single %d/%s, plus %% for a literal percent |
valueLabels | table | no | Words for the values where a bare number misleads, keyed by value-as-string — e.g. { ['0'] = 'None', ['100'] = 'Full' } |
secret | boolean | text | Render as a password field |
tier | string | no | all (default) | vip | staff — the tier required to change it |
spec | boolean | no | Show in the read-only spec block (default: true for match, false for session) |
enabledWhen | table | no | { id = '<other criterion>', is = <value> } — the one dependency primitive |
Option fields
| Field | Notes |
|---|---|
key | The stored value |
label | Display name (defaults to the key, title-cased) |
sub | Secondary line — a weapon class, a map's character |
present | A presentation block. Its card (else hero) turns the option into a media card with a thumbnail |
roll | This 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:
| Options | Control |
|---|---|
| ≤ 4, no art | Segmented control |
| more than 4, or any option has art | Wrapping card grid |
| ≥ 10 | Card 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 tostep; - 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
- Mode descriptor —
loadouts,roundsRange,randomLoadout - Battleground SDK — where map options come from, and their art
- Presentation block —
card,hero,sub