Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Recoil:RmlUI Starter Guide

From Fightorder
Revision as of 23:02, 5 March 2026 by Qrow (talk | contribs)

Making A Basic Recoil Game

This guide walks you through creating a minimal but functional game with the Recoil engine. By the end you will have a working game archive that loads in the engine, with a playable Commander unit, a simple weapon, and a basic Lua gadget. No prior Spring or Recoil experience is assumed, though familiarity with Lua is helpful.

<translate> Warning</translate> <translate> Warning:</translate> This guide targets the current Recoil engine. If you are migrating an existing Spring 105 game, see Recoil:Migrating From Spring for a list of breaking changes.

1. What is Recoil?

Recoil is an open-source, cross-platform RTS game engine descended from Spring (which itself descended from Total Annihilation). It provides out of the box:

  • Physics simulation — projectile trajectories, terrain collision, unit movement
  • Resource system — metal and energy by default, fully customisable
  • Networking — authoritative server, deterministic lockstep simulation, replays
  • Lua scripting — every mechanic is overridable via Lua gadgets and widgets
  • VFS — a virtual file system that loads content from game archives, maps, and basecontent

Your game is a Lua + asset package that rides on top of the engine. You control the rules, the units, the UI, and everything else; the engine provides the platform.

Key terminology

Term Meaning
Game Your content archive loaded by the engine (units, Lua scripts, textures, etc.)
Map The terrain archive (.smf height data + textures). Distributed separately from the game.
Gadget A Lua script that runs game logic (synced or unsynced). Lives in luarules/gadgets/.
Widget A Lua script that runs UI logic. Lives in luaui/widgets/.
Wupget Informal collective term for widgets and gadgets.
Callin A function in your Lua code that the engine calls (e.g. gadget:UnitCreated).
Callout A function the engine exposes to Lua (e.g. Spring.GetUnitPosition).
VFS Virtual File System — unified view of game archive, map archive, basecontent.
Basecontent Engine-bundled archive with default handlers, water textures, and other bare necessities.
.sdd Development game folder (name must end in .sdd). Equivalent to an archive but uncompressed.
.sdz Production game archive (renamed .zip). Faster to distribute and load.

2. Prerequisites

Before starting you need:

  1. The Recoil engine binary. Download a pre-built release from GitHub Releases or build from source. The main executable is spring (or spring.exe on Windows).
  2. A map archive (.smf / .sd7) placed in the maps/ subfolder of your Recoil data directory. Any map from an existing Recoil game works for testing. Map archives and game archives live in separate directories.
  3. A text editor. Any editor works. For the best experience, set up the Lua Language Server with Recoil type stubs so you get autocomplete on all Spring.* functions.

Your Recoil data directory typically looks like:

recoil-data/
├── spring           ← engine binary (linux) or spring.exe (windows)
├── spring_headless  ← headless binary (no rendering)
├── spring_dedicated ← dedicated server binary
├── games/           ← your game archives / folders go here
├── maps/            ← map archives go here
└── (engine config files)

3. Game Archive Structure

During development use an .sdd folder — it's just a normal directory the engine treats as an archive. Create it inside your games/ directory:

games/
└── mygame.sdd/
    ├── modinfo.lua          ← required: identifies your game to the engine
    ├── units/               ← unit definition files
    │   └── commander.lua
    ├── weapons/             ← weapon definition files
    │   └── laser.lua
    ├── features/            ← feature (prop/wreck) definitions
    ├── gamedata/            ← def pre/post processing, options
    │   ├── unitdefs_post.lua
    │   └── modrules.lua
    ├── luarules/
    │   └── gadgets/         ← synced and unsynced game logic
    │       └── game_end.lua
    ├── luaui/
    │   └── widgets/         ← UI scripts
    ├── objects3d/           ← unit models (.dae / .obj / .s3o)
    ├── unittextures/        ← unit icon textures (unitname.png)
    └── maps/                ← embedded map resources (optional)
<translate> Warning</translate> <translate> Warning:</translate> The .sdd extension on the folder name is mandatory. Without it the engine will not recognise it as a game archive.

For production, compress the contents into a .sdz (zip) archive or .sd7 (7-zip) archive. The folder contents become the archive root — modinfo.lua should be at the top level.

VFS and dependencies

Your game archive can declare dependencies on other archives (including other games) via modinfo.lua. The engine merges all archives into the VFS. Files in your game take priority over dependencies. This means you can build on an existing game without copying its assets.

See Recoil:VFS Basics for full details on VFS modes and the load order.


4. modinfo.lua

modinfo.lua is the single most important file — without it the engine will not recognise your archive as a game.

<syntaxhighlight lang="lua"> -- games/mygame.sdd/modinfo.lua

local modinfo = {

   -- Displayed in lobbies and the engine title bar
   name    = "My Recoil Game",
   -- Short unique identifier used internally; no spaces, keep it stable
   shortName = "MRG",
   -- Shown to players in lobby browsers and descriptions
   description = "A minimal Recoil game for learning purposes.",
   -- Semantic version string
   version = "0.1.0",
   -- Mutator name; leave empty for base games (not mods on top of other games)
   mutator = "",
   -- Archive names this game depends on; engine always loads basecontent itself
   depend  = {},
   -- Replaces list: names of games this game supersedes in rapid/lobby
   replace = {},
   -- URL shown in lobby if a player does not have the game
   modtype = 1,  -- 1 = game/mod, 0 = hidden
   -- Primary unit sides available to players
   -- These correspond to the "Side" key in start scripts and unit defs
   sides   = { "MyFaction" },
   -- For each side, which unit the engine spawns as the starting unit
   startscreens = {
       MyFaction = "commander",   -- references a unit def name
   },

}

return modinfo </syntaxhighlight>

<translate> Warning</translate> <translate> Warning:</translate> shortName must be unique across all games the engine knows about. Pick something distinctive. Changing it later breaks lobby integrations and replays.

ModOptions.lua

To expose configurable options in lobbies (e.g. starting resources, game mode), add a gamedata/ModOptions.lua:

<syntaxhighlight lang="lua"> -- gamedata/ModOptions.lua -- Returned options appear as sliders/checkboxes/dropdowns in lobby UIs.

return {

   {
       key     = "startmetal",
       name    = "Starting Metal",
       desc    = "Amount of metal each player starts with.",
       type    = "number",
       def     = 1000,
       min     = 0,
       max     = 10000,
       step    = 100,
   },
   {
       key     = "commanderdie",
       name    = "Commander Death Ends Game",
       desc    = "Lose when your Commander is destroyed.",
       type    = "bool",
       def     = true,
   },

} </syntaxhighlight>

Options are readable in gadgets via Spring.GetModOptions().


5. Your First Unit Definition

Unit defs are Lua files in units/ that return a table of unit type definitions. The convention is one unit per file, where the filename matches the unit def name.

<syntaxhighlight lang="lua"> -- units/commander.lua -- The starting unit for the MyFaction side.

return {

   commander = {
       -- ── Display ──────────────────────────────────────────────────
       name        = "Commander",
       description = "The starting unit. Its death ends the game.",
       -- Path to the icon shown in the unit HUD (unittextures/commander.png)
       buildPic    = "commander.png",
       -- ── Model ────────────────────────────────────────────────────
       -- Path inside the archive (objects3d/commander.dae)
       objectName  = "commander.dae",
       -- ── Physics ──────────────────────────────────────────────────
       -- Maximum health points
       health      = 3000,
       -- Armour class name; controls damage reduction from weapon types
       armorType   = "commander",
       -- Size of the footprint in map squares (8 elmos each)
       footprintX  = 2,
       footprintZ  = 2,
       -- The height of the collision sphere
       collisionVolumeScales = "30 50 30",
       collisionVolumeType   = "ellipsoid",
       -- ── Movement ─────────────────────────────────────────────────
       -- Movement class name; references a MoveClass def (see MoveClasses below)
       moveDef = {
           family      = "tank",
           -- Units above this slope can't be traversed
           maxSlope    = 36,
           -- Turns per second at max speed
           turnRate    = 1024,
       },
       -- Maximum speed in elmos/second
       maxVelocity = 42,
       -- Acceleration in elmos/second²
       maxAcc      = 0.5,
       -- Braking in elmos/second²
       maxDec      = 1.5,
       -- ── Construction ─────────────────────────────────────────────
       -- How many build power units this unit provides
       workerTime  = 200,
       -- How much metal it costs to build
       buildCostMetal  = 0,     -- Commanders are free (spawned by engine)
       buildCostEnergy = 0,
       -- Build time in seconds at 1 build power
       buildTime   = 1,
       -- ── Energy and Metal Production ───────────────────────────────
       metalMake   = 1,      -- passively trickles 1 metal/second
       energyMake  = 20,     -- passively generates 20 energy/second
       -- ── Weapons ──────────────────────────────────────────────────
       -- Up to 3 weapons can be listed; name references a weapon def
       weapons = {
           { def = "COMMANDER_LASER" },
       },
       -- ── Self-Destruct ─────────────────────────────────────────────
       selfDExplosion = "commander_sdc",  -- optional; a weapon def name
       -- ── Misc ─────────────────────────────────────────────────────
       -- Used in start scripts to assign this unit as Commander
       commander   = true,
       -- Sight radius in elmos
       sightDistance = 660,
       -- Radar range in elmos (0 = no radar)
       radarDistance = 0,
   },

} </syntaxhighlight>

<translate> Warning</translate> <translate> Warning:</translate> Key names in unit def files are case-insensitive as far as the engine is concerned, but Lua itself is case-sensitive. Adopt a consistent convention (all lowercase is common) and use lowerkeys() in gamedata/unitdefs_post.lua if you mix styles.

Notable unit def keys

Key Type Notes
name string Display name in UI. Not the def name (the Lua table key).
health number Max HP.
moveDef table Inline move class definition. See engine docs for all fields.
maxVelocity number Elmos per second. 1 elmo ≈ 8 map height units.
buildCostMetal number Metal cost. Constructor must have enough resources to start building.
buildTime number Seconds to build at 1 build power. Actual time = buildTime / workerTime.
workerTime number Build power this unit contributes when constructing.
weapons table Up to 3 entries; each entry is a table with a def key naming a WeaponDef.
commander bool Marks this unit as a Commander. Lobby UI and engine use this.
customParams table Arbitrary key/value pairs preserved by the engine for gadget use.

For the full list of unit def keys and their effects, see the Recoil Lua API documentation.

Def pre- and post-processing

The engine reads gamedata/defs.lua (provided by basecontent) which orchestrates def loading. It first executes gamedata/unitdefs_pre.lua (if present) to populate a Shared table visible to all unit def files, then loads every units/*.lua file, and finally runs gamedata/unitdefs_post.lua.

<syntaxhighlight lang="lua"> -- gamedata/unitdefs_post.lua -- Normalize key casing and apply game-wide tweaks.

for _, unitDef in pairs(UnitDefs) do

   -- lowerkeys() is provided by basecontent; makes all keys lowercase
   lowerkeys(unitDef)

end

-- Example: give all units 10% extra health for _, unitDef in pairs(UnitDefs) do

   if unitDef.health then
       unitDef.health = unitDef.health * 1.1
   end

end </syntaxhighlight>

<translate> Warning</translate> <translate> Warning:</translate> The UnitDefs table inside gadgets/widgets is not the same as the one in post-processing. After loading, the engine parses the def tables into internal structures and re-exposes them with different key names and value scales. Never copy-paste keys between def files and wupget code.

6. Weapon Definitions

Weapon defs follow the same pattern as unit defs — Lua files in weapons/ returning tables.

<syntaxhighlight lang="lua"> -- weapons/laser.lua

return {

   COMMANDER_LASER = {
       name        = "Commander Laser",
       description = "Short-range direct-fire laser.",
       -- Weapon category controls which targets it can engage
       -- Options: ground, air, sea, land, building, etc. (combinable)
       onlyTargetCategory = "SURFACE",
       -- ── Visuals ───────────────────────────────────────────────────
       -- Weapon class drives the physics model
       -- Options: Cannon, LaserCannon, MissileLauncher, AircraftBomb,
       --          BeamLaser, LightningCannon, Flame, TorpedoLauncher, ...
       weaponType    = "LaserCannon",
       -- Colour of the beam/tracer (R G B A each 0-255)
       rgbColor      = "1 0.2 0.2",
       -- Colour at the other end of the beam (optional)
       rgbColor2     = "1 0.9 0.9",
       -- Visual length of the laser beam in elmos
       beamtime      = 1,
       -- Thickness of the beam in pixels
       thickness     = 3,
       corethickness = 0.3,
       laserflaresize = 8,
       -- ── Ballistics ────────────────────────────────────────────────
       -- Projectile/beam speed in elmos/second
       projectilespeed  = 800,
       -- Range in elmos
       range            = 350,
       -- Reload time in seconds
       reloadtime       = 1.5,
       -- Burst: number of shots per fire command
       burst            = 1,
       -- Spread in degrees (0 = perfectly accurate)
       accuracy         = 0,
       -- ── Damage ────────────────────────────────────────────────────
       damage = {
           -- 'default' applies to all armour types not explicitly listed
           default = 50,
       },
       -- Area of effect in elmos (0 = single target)
       areaOfEffect     = 8,
       -- Impulse applied to targets on hit (0 = none)
       impulse          = 0,
       -- Does this weapon set things on fire?
       firestarter      = 0,
   },

} </syntaxhighlight>

Like unit defs, you can post-process weapon defs via gamedata/weapondefs_post.lua.


7. Feature Definitions

Features are non-commandable props: trees, rocks, wrecks, and so on. They occupy the map by default via map configuration, or can be spawned by Lua. A minimal feature def:

<syntaxhighlight lang="lua"> -- features/trees.lua

return {

   tree = {
       description    = "A tree.",
       -- Model to render
       object         = "tree.dae",
       -- Health before the feature is destroyed
       damage         = 50,
       -- Metal reclaimed when a constructor takes it apart
       metal          = 0,
       -- Energy reclaimed when destroyed or reclaimed
       energy         = 0,
       -- Blocks ground unit pathfinding
       blocking       = true,
       -- Visible on radar/sonar
       geoThermal     = false,
       -- What this feature becomes when destroyed (nil = nothing)
       deathFeature   = "",
       -- Whether this can be reclaimed by a constructor
       reclaimable    = true,
   },

} </syntaxhighlight>


8. Game Rules (modrules.lua)

gamedata/modrules.lua is read by basecontent's defs.lua to configure fundamental engine behaviour that is not driven by unit/weapon defs.

<syntaxhighlight lang="lua"> -- gamedata/modrules.lua

return {

   system = {
       -- Starting resources (also overridable via ModOptions / start script)
       startMetal  = 1000,
       startEnergy = 1000,
       -- Maximum units per team (0 = unlimited)
       maxUnits    = 1000,
       -- Commander handling: 0 = game continues, 1 = game ends, 2 = lineage
       gameMode    = 1,
       -- Limit on repair speed (% of max health per second; 0 = unlimited)
       repairSpeed = 0,
       -- Whether resurrect restores health
       resurrectHealth = false,
       -- Resource multiplier for reclaiming metal from living units
       reclaimUnitMethod = 1,
       -- Whether ground features leave wrecks
       featureDeathResurrect = true,
   },
   sound = {
       -- Optional: override specific engine sound events
   },

} </syntaxhighlight>


9. Running Your Game

The engine is launched with a start script — a plain text file (conventionally .sdf) that describes who is playing, which game, and which map.

Minimal start script

<syntaxhighlight lang="ini"> [GAME] {

   GameType = My Recoil Game;   -- must match modinfo.lua name or shortName
   MapName  = my_map.smf;       -- the .smf file inside the map archive
   IsHost   = 1;
   MyPlayerName = Dev;
   [PLAYER0]
   {
       Name      = Dev;
       Password  = ;
       Spectator = 0;
       Team      = 0;
   }
   [TEAM0]
   {
       TeamLeader = 0;
       AllyTeam   = 0;
       RgbColor   = 0.1 0.4 1.0;
       Side       = MyFaction;    -- must match a side declared in modinfo.lua
       StartPosX  = 4000;
       StartPosZ  = 4000;
   }
   [ALLYTEAM0]
   {
       NumAllies = 0;
   }

} </syntaxhighlight>

Save this as game.sdf in your Recoil data directory, then launch:

./spring game.sdf

On Windows:

spring.exe game.sdf

The engine will:

  1. Read the start script
  2. Mount your game archive from games/mygame.sdd
  3. Mount the map archive from maps/my_map.sd7
  4. Load basecontent (default widget handlers, etc.)
  5. Start the simulation and call into your Lua scripts
<translate> Warning</translate> <translate> Warning:</translate> If the engine cannot find your game archive, check that the folder name ends in exactly .sdd (case-sensitive on Linux) and that modinfo.lua is at the root of that folder, not in a subfolder.

Quick test with headless

For automated testing (or when you don't need graphics), use the headless binary:

./spring_headless game.sdf

Widgets still run, so you can run AI vs AI matches or data-gathering scripts without a window.


10. Lua Scripting Basics

Lua is the scripting language for everything above the C++ engine layer. The engine provides several isolated Lua environments:

Environment Location Purpose
LuaRules (synced) luarules/main.lua Authoritative game logic. Desync if misused.
LuaRules (unsynced) luarules/draw.lua Unsynced effects and UI driven by game logic.
LuaUI luaui/main.lua Player-facing UI: HUD, minimap overlays, etc.
LuaGaia luagaia/main.lua Logic for the neutral Gaia team (wrecks, features, world events).
LuaIntro luaintro/main.lua Loading screen, briefings.
LuaMenu luamenu/main.lua Main menu (pre-game). Requires a menu archive.

Basecontent provides default main.lua entry points and wupget handler boilerplate so you can start writing gadgets and widgets without building the handler yourself.

Your first gadget

A gadget is a Lua file the gadget handler loads from luarules/gadgets/.

<syntaxhighlight lang="lua"> -- luarules/gadgets/game_end.lua -- Ends the game when a player's Commander is destroyed.

local gadget = gadget ---@type Gadget

function gadget:GetInfo()

   return {
       name    = "Game End Conditions",
       desc    = "Ends the game on Commander death.",
       author  = "YourName",
       date    = "2025",
       license = "GNU GPL, v2 or later",
       layer   = 0,
       enabled = true,
   }

end

-- Only run the logic in synced mode if not gadgetHandler:IsSyncedCode() then return end

-- Track alive Commanders per team local commanderByTeam = {}

function gadget:UnitFinished(unitID, unitDefID, teamID)

   local def = UnitDefs[unitDefID]
   -- UnitDefs inside wupgets is indexed by numeric ID, not name
   if def and def.isCommander then
       commanderByTeam[teamID] = unitID
   end

end

function gadget:UnitDestroyed(unitID, unitDefID, teamID)

   if commanderByTeam[teamID] == unitID then
       commanderByTeam[teamID] = nil
       -- Check whether any other living Commander still belongs to this allyteam
       local allyTeamID = Spring.GetTeamAllyTeamID(teamID)
       local hasCommander = false
       for _, ally in ipairs(Spring.GetTeamList(allyTeamID)) do
           if commanderByTeam[ally] then
               hasCommander = true
               break
           end
       end
       if not hasCommander then
           -- GameOver sends the end-of-game event to all clients
           Spring.GameOver({ allyTeamID })
       end
   end

end </syntaxhighlight>

Your first widget

A widget is a Lua file the widget handler loads from luaui/widgets/.

<syntaxhighlight lang="lua"> -- luaui/widgets/resources_display.lua -- Draws current metal and energy income in the corner of the screen.

local widget = widget ---@type Widget

function widget:GetInfo()

   return {
       name    = "Resources Display",
       desc    = "Shows current resource income.",
       author  = "YourName",
       date    = "2025",
       license = "GNU GPL, v2 or later",
       layer   = 0,
       enabled = true,
   }

end

function widget:DrawScreen()

   local metal, energy = Spring.GetTeamResources(Spring.GetMyTeamID())
   if not metal then return end
   gl.Color(1, 1, 1, 1)
   gl.Text(string.format("Metal: %.0f  Energy: %.0f", metal, energy),
           10, 40, 16, "s")

end </syntaxhighlight>

Callins and callouts

Callins are functions in your code the engine calls at the right moment: UnitCreated, UnitDestroyed, GameOver, DrawScreen, Update, etc. Check the full list at runtime with Script.GetCallInList().

Callouts are Spring.* functions you call out to the engine: Spring.GetTeamResources, Spring.GiveOrderToUnit, Spring.Echo, etc. Full documentation is at beyond-all-reason.github.io/spring/ldoc/.

<translate> Warning</translate> <translate> Warning:</translate> Synced gadgets have strict rules — only call synced-safe functions. Calling unsynced functions from synced code causes a desync error. When in doubt, check gadgetHandler:IsSyncedCode() and gate appropriately.

Communicating between environments

The Lua environments are isolated from each other by design. Common bridges:

Method Direction Notes
WG / GG table Within one environment Widgets share WG; gadgets share GG.
SendToUnsynced / RecvFromSynced Synced → Unsynced Pass data from game logic to rendering/UI.
Spring.SendLuaRulesMsg / gadget:RecvLuaMsg LuaUI → LuaRules UI triggers a game action beyond unit orders.
Spring.SendLuaUIMsg / widget:RecvLuaMsg LuaUI ↔ LuaUI (all players) Broadcast UI state to other players.
Rules params (Spring.SetRulesParam) LuaRules → all Expose synced data as read-only key/values.

See Recoil:Wupget Communication for detailed examples.


11. Useful Debug Commands

In-game chat commands prefixed with / control engine and game state. With cheats enabled (/cheat) additional commands become available:

Command Effect
/cheat Toggle cheats on/off. Required for most debug commands.
/give [unitname] Spawn a unit at the cursor position.
/destroy Destroy selected units immediately.
/invincible Toggle invincibility for selected units.
/godmode Toggle god mode (all units obey all players).
/speed [x] Set game speed multiplier (e.g. /speed 5).
/nopause Disallow pausing.
/luarules reload Reload LuaRules scripts without restarting.
/luaui reload Reload LuaUI scripts without restarting.
/editdefs Enter def-editing mode (requires cheats). See unit def docs.
Spring.Echo("msg") Print a message to the in-game console from Lua.

12. Maps and Map Integration

Maps are separate archives (.sd7 or .sdz) placed in maps/. They are not shipped with your game. The map defines the terrain height data (.smf), sky, water, and can optionally define starting metal spots and features.

Controlling what maps do to your game

Maps can include a mapinfo.lua which may set atmosphere, gravity, and other properties. A map can also override unit def values via MapOptions.lua, which your game can read with Spring.GetMapOptions().

You can restrict which maps are compatible by checking map properties in a gadget at startup and calling Spring.GameOver if requirements are not met.

Metal spots

Metal spots (mexes) are typically defined in the map as SMF/SMD metal map data, or via a Lua file in the map archive. Your gadgets can read them with Spring.GetMetalMapSize() and Spring.ExtractorInRangeMetalMap(). Extractor units that generate metal must have a extractsMetal unit def key > 0 and be placed over a metal spot.


13. Packaging and Distribution

When your game is ready for distribution, compress the .sdd folder contents into an .sdz (zip) or .sd7 (7-zip) file.

# Linux: create .sdz (zip)
cd games/mygame.sdd
zip -r ../mygame-0.1.0.sdz .

# Linux: create .sd7 (7-zip, better compression)
cd games/mygame.sdd
7z a ../mygame-0.1.0.sd7 .
<translate> Warning</translate> <translate> Warning:</translate> Make sure modinfo.lua ends up at the root of the archive, not inside a subfolder. Some zip tools create an extra folder level automatically.

Rapid

Rapid is the standard package distribution system for Recoil games. It allows lobby clients (e.g. Chobby) to download and update games automatically. See the Rapid documentation and the start script format for integration details.


14. Next Steps

With a working minimal game, the typical next steps are:

More unit types
Add builders, resource extractors, power generators, turrets, and mobile combat units. Follow the same unit def pattern — each is a Lua file in units/.
Resource extractors
A unit with extractsMetal > 0 placed over a metal spot generates metal. Add an energy converter unit to turn metal into energy and vice versa.
Pathfinding and MoveClasses
Define movedefs in gamedata/movedefs.lua to control how different unit types traverse terrain (hover, boat, amphibious, kbot, tank, aircraft).
AI support
The Skirmish AI interface lets players add bot opponents without any extra gadget work. Beyond that, you can write a [Lua AI] for fully custom bot behaviour inside your game's Lua scripting.
Full UI
The basecontent widget handler gives you a basic HUD for free. For a polished interface use RmlUI for HTML/CSS-style widgets, or raw OpenGL via gl.* callouts.
Maps
Creating your own map requires tools like MapConv (height and texture compilation) and SpringMapEdit. See the Recoil map documentation for details.
Replays and dedicated servers
Replays are recorded automatically when RecordDemo=1 is set in the start script. For large-scale multiplayer use the dedicated binary (spring_dedicated) and an autohost such as SPADS or Springie.

15. Further Reading

On this wiki

External resources