Battleground SDK — building battlegrounds (the shared map pool)
The battleground side of the RivalityFX SDK. A battleground is a self-contained resource that adds a location to the shared map pool by registering a plain-data descriptor with the SDK. It is the third domain beside the mode SDK and the cosmetics SDK — the same registry pattern, a third contract.
One platform SDK, three contracts:
exports.sdk:registerMode(gameplay),exports.sdk:registerPack(cosmetics), andexports.sdk:registerBattleground(maps). Each is versioned independently (MODE_API_VERSION/PACK_API_VERSION/BATTLEGROUND_API_VERSION).
Naming (battleground vs Arena). In this doc a battleground is a map / location in the shared map pool (
exports.sdk:registerBattleground,battleground_pack_base). Don't confuse it with the engine's match Arena — the runtime match container players browse and join. Two layers: the battleground is the place, the match Arena is the session that runs a mode on it.
The model — ownership inversion
A mode no longer owns its maps inline. Battlegrounds register independently, form a shared pool, and a mode consumes the battlegrounds that declare a block for it. So the same battleground serves any mode that fits it, and the same mode draws from every compatible battleground.
- The
sdkresource owns the contract, the authoritative battleground registry (server-side — spawn coords are server-authoritative, unlike a per-player cosmetic), and thedefineBattlegroundbuilder. engineis a consumer: it mirrors the registry into the per-mode map-pool view (Engine.battlegroundsForMode) used by rotation and the match-Arena create-form.- A battleground pack is its own resource that depends on
sdkand registers viaexports.sdk:registerBattleground.
The base battlegrounds ship as an OSS example (battleground_pack_base, base-game
locations, MIT). A paid battleground ships its own streamed MLO under
stream/ and registers through the same public SDK — the mirror of
cosmetic_pack_base vs cosmetic_pack_neon.
Compatibility is "has a block"
A battleground ships spawn data per supported mode, as arrays indexed by team
number (teams are anonymous — entry 1 is where team 1 enters, entry 2 team 2).
Compatibility is simply the presence of a modes.<id> block — there is no
abstract slot-mapping layer. A battleground with no modes.duel block is never
offered to the duel; the engine hides it from that mode's rotation and
create-form.
The entrance: ground spawn or air drop
A block carries one or both placement shapes, and that choice is the entrance — the battleground decides how a round opens on it, not the mode and not the operator:
spawns— GROUND SPAWN: static points, one per seat, per team. Players start standing on their point facingw(heading, degrees). No parachute, no descent — the arena entrance, and what a 1v1 or 2v2 wants.areas— AIR DROP: a centre + radius (r) per team. The round opens with a parachute descent scattered inside the circle;faceis the landing heading (degrees, or'center'to face the area centre). One area serves any team size — the battle-royale entrance.
Declare both and spawns wins for every team it covers; areas is then the fallback
for teams it doesn't.
spawns is indexed by team, and each team's entry is a list of points — one per seat,
so a 2v2 places all four players individually. For a one-player team you may write the
point directly instead of a one-element list; it is normalised for you.
defineBattleground{
id = 'warehouse', apiVersion = 2, label = 'Warehouse',
-- a base-game location streams nothing; a proprietary battleground adds `stream = '...'`
-- (a streamed-MLO resource) or `ipl = '...'` (a base-game interior to request).
modes = {
duel = { -- supports 1v1 Duel (perTeam = 1)
spawns = {
{ { x = 1209.0, y = -3115.0, z = 5.5, w = 270.0 } }, -- team 1, seat 1
{ { x = 1234.0, y = -3115.0, z = 5.5, w = 90.0 } }, -- team 2, seat 1
},
},
squad = { -- supports 2v2 Duel (perTeam = 2)
spawns = {
{ { x = 1209.0, y = -3117.0, z = 5.5, w = 270.0 }, -- team 1, seat 1
{ x = 1209.0, y = -3113.0, z = 5.5, w = 270.0 } }, -- team 1, seat 2
{ { x = 1234.0, y = -3117.0, z = 5.5, w = 90.0 },
{ x = 1234.0, y = -3113.0, z = 5.5, w = 90.0 } },
},
},
royale = { -- parachute in instead
areas = { { x = 1209.0, y = -3115.0, z = 5.5, r = 300.0, face = 'center' } },
},
-- no `myothermode` key -> this battleground is never offered to that mode
},
}The engine warns at consume time if a block holds fewer entries than a mode's
teamCount (that side would have nowhere to spawn), or fewer ground points than its
perTeam (teammates would share a spot — extra seats wrap onto an earlier point).
Capturing coords
Spawn points are exact positions you can only really get by standing on them. With
set rfx_lab true, two admin commands close that loop:
/rfx lab here # prints your position as `{ x = .., y = .., z = .., w = .. },`
/rfx lab goto <bgId> <modeId> [team] [seat] # teleport onto a declared point to check itz is forgiving — the client re-probes the ground under x/y on spawn, so a point
captured slightly above or below the floor still puts the player on the surface. w is
not: it decides which way a player is looking when the round opens.
Give it a thumbnail
A battleground may carry the optional present block — the
shared presentation block every descriptor uses. The
map picker in the Create-arena form renders it:
present = {
card = 'nui://my_battleground_pack/media/warehouse.webp', -- 16:9 thumbnail, ~640x360
sub = 'Tight indoor lanes', -- one line under the name
}cardis the picker's thumbnail. Author it at roughly 640×360 (16:9) — the picker draws many cells at once, and full-bleed 1920×1080 splash art means that many full-resolution decodes when the menu opens. Ifcardis absent the picker falls back tohero.subis a one-line hint shown under the name, on the card and wherever the map is named on a read-only surface.
Art is optional. A battleground with none still renders as a designed typographic card — its name set large over an ink wash — so a pool where only some battlegrounds ship thumbnails looks deliberate rather than broken. Add art when you have it.
Build a battleground in 3 steps
-
Scaffold a resource that depends on the SDK:
-- fxmanifest.lua dependencies { 'sdk' } shared_scripts { '@sdk/shared/kit.lua', -- SDK helpers the contract uses (load first) '@sdk/shared/battlegrounds.lua', -- defineBattleground + LuaCATS types } server_script 'server.lua' -- battlegrounds register SERVER-side -
Author the battleground and register it (server-side, once the SDK is up):
local BG = defineBattleground{ id = 'mybattleground', apiVersion = 2, label = 'My Battleground', modes = { duel = { spawns = { {...}, {...} } } }, -- arrays: entry N = team N } CreateThread(function() while GetResourceState('sdk') ~= 'started' do Wait(100) end exports.sdk:registerBattleground(BG) end) -
Ship geometry (proprietary battlegrounds only): drop the streamed MLO files under
stream/and pointstream/iplat it. Base-game locations stream nothing.
Then ensure your battleground pack in server.cfg. It joins the pool and is offered to
every mode it declares a block for. Narrow the rotation with the engine's
rfx_battlegrounds allowlist if needed (see config).
Bundling a mode with its map
The shared pool decouples maps from modes by default — but you can still ship a mode and an exclusive map together, as one sellable resource. A bundle is simply a resource that registers both contracts from one VM:
- it depends on
sdkandengine(the mode half needs the runtime; the battleground half needs onlysdk, a subset), - includes both
@sdk/shared/descriptor.luaand@sdk/shared/battlegrounds.lua, - and calls both
exports.sdk:registerModeandexports.sdk:registerBattleground— each into its own SDK store; the engine mirrors each and pairs them by id at consume time.
-- server/main.lua
Sdk.autoRegister(function()
exports.sdk:registerMode(Config.mode) -- id = 'rooftop'
exports.sdk:registerBattleground(Config.battleground) -- modes = { rooftop = { areas = {...} } }
end)Exclusivity is free, by construction. Give the mode a unique id and have the
battleground declare only that id's block. Compatibility is an exact id match
(above), so the pool offers the map to your mode and
to nothing else — and since no other battleground declares your id, your mode in practice
only ever runs on your map. This is a convention, not an engine-enforced lock: the
contract has no owner check, so it does not stop a third party from declaring the same
id. For a base-game open-ground map that is all you need; a proprietary bundle adds its
streamed MLO under stream/ exactly like any paid battleground.
See resources/[modes]/mode_rooftop — the reference bundle (a Rooftop Duel + its
exclusive Neon Rooftop battleground: one resource, two registrations).
See also
- cosmetics-sdk — the sibling content domain this mirrors.
- engine-sdk — the gameplay sibling (modes).
- config —
rfx_battlegrounds+ the static/admin tiers.