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: Difference between revisions

From Fightorder
Created page with "{{DISPLAYTITLE:RmlUI Starter Guide}} __TOC__ = RmlUI Starter Guide = '''''For RecoilEngine game developers — build reactive, HTML/CSS-style game UIs in Lua.''''' {| class="wikitable" ! Attribute !! Value |- | Engine || RecoilEngine (Spring) |- | Framework || RmlUi ([https://github.com/mikke89/RmlUi mikke89/RmlUi]) |- | Language || Lua (via sol2 bindings) |- | Availability || LuaUI (LuaIntro / LuaMenu planned) |} ---- == 1. Introduction == RmlUI is a UI framework..."
 
No edit summary
Tag: Reverted
Line 1: Line 1:
{{DISPLAYTITLE:RmlUI Starter Guide}}
{{DISPLAYTITLE:Making A Basic Recoil Game}}
__TOC__
__TOC__


= RmlUI Starter Guide =
= Making A Basic Recoil Game =


'''''For RecoilEngine game developers — build reactive, HTML/CSS-style game UIs in Lua.'''''
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.


{| class="wikitable"
{{warning|1=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.}}
! Attribute !! Value
|-
| Engine || RecoilEngine (Spring)
|-
| Framework || RmlUi ([https://github.com/mikke89/RmlUi mikke89/RmlUi])
|-
| Language || Lua (via sol2 bindings)
|-
| Availability || LuaUI (LuaIntro / LuaMenu planned)
|}


----
----


== 1. Introduction ==
== 1. What is Recoil? ==


RmlUI is a UI framework that lets you build reactive game interfaces using an HTML/CSS-style workflow — with Lua instead of JavaScript. If you have ever built a web page, the learning curve is gentle. If you haven't, the concepts are still approachable because the mental model (mark-up + style + logic) is widely documented.
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:


RecoilEngine ships with a full RmlUI integration including:
* '''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


* An OpenGL 3 renderer tightly coupled to the engine's rendering pipeline
Your game is a Lua + asset package that rides on top of the engine.
* A virtual-filesystem-aware file loader so your <code>.rml</code> and <code>.rcss</code> files live inside game archives
You control the rules, the units, the UI, and everything else; the engine provides the platform.
* Complete Lua bindings via sol2 so every RmlUI object is accessible from Lua
* Custom elements: <code><nowiki><texture></nowiki></code> for engine textures and <code><nowiki><svg></nowiki></code> for vector art
* A built-in DOM debugger for interactive inspection


This guide walks you through everything you need to get a working, reactive widget running in LuaUI. The examples are game-engine-agnostic and safe to copy into any Recoil-based game.
=== Key terminology ===
 
{{Note|RmlUI is currently available in '''LuaUI''' (the in-game UI). Support for LuaIntro and LuaMenu is planned for a future release.}}
 
----
 
== 2. Key Concepts ==
 
Before writing any code it helps to understand the five core objects:


{| class="wikitable"
{| class="wikitable"
! Concept !! Description
! Term !! Meaning
|-
|-
| '''Context''' || A named container that holds documents and data models. Multiple contexts can exist simultaneously, each rendered independently.
| '''Game''' || Your content archive loaded by the engine (units, Lua scripts, textures, etc.)
|-
|-
| '''Document''' || A parsed RML file representing a DOM tree. Documents live inside a Context and can be shown, hidden, or closed.
| '''Map''' || The terrain archive (.smf height data + textures). Distributed separately from the game.
|-
|-
| '''RML''' || The markup language for documents. Very similar to XHTML (well-formed XML). The root tag is <code><nowiki><rml></nowiki></code> instead of <code><nowiki><html></nowiki></code>.
| '''Gadget''' || A Lua script that runs game logic (synced or unsynced). Lives in <code>luarules/gadgets/</code>.
|-
|-
| '''RCSS''' || The styling language. Similar to CSS2 with some differences (see section 11). File extension: <code>.rcss</code>.
| '''Widget''' || A Lua script that runs UI logic. Lives in <code>luaui/widgets/</code>.
|-
|-
| '''Data Model''' || A Lua table exposed to the RML document via reactive data bindings. Changes to the model automatically update the rendered UI.
| '''Wupget''' || Informal collective term for widgets and gadgets.
|-
| '''Callin''' || A function in your Lua code that the engine calls (e.g. <code>gadget:UnitCreated</code>).
|-
| '''Callout''' || A function the engine exposes to Lua (e.g. <code>Spring.GetUnitPosition</code>).
|-
| '''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 <code>.sdd</code>). Equivalent to an archive but uncompressed.
|-
| '''.sdz''' || Production game archive (renamed .zip). Faster to distribute and load.
|}
|}
On the '''Lua side''' you create contexts, attach data models to them, and load documents. On the '''RML side''' you declare your UI structure and reference data-model values through data bindings.
Each widget will typically consist of at least three files: a <code>.lua</code>, a <code>.rml</code>, and optionally a <code>.rcss</code> file — grouping them in a folder per widget keeps things tidy.


----
----


== 3. Setup ==
== 2. Prerequisites ==


RecoilEngine ships a minimal setup script at <code>cont/LuaUI/rml_setup.lua</code>. It is included automatically by the base content handler but you should understand what it does so you can extend it for your game.
Before starting you need:


=== 3.1 The rml_setup.lua script ===
# '''The Recoil engine binary.''' Download a pre-built release from [https://github.com/beyond-all-reason/RecoilEngine/releases GitHub Releases] or build from source. The main executable is <code>spring</code> (or <code>spring.exe</code> on Windows).
# '''A map archive''' (<code>.smf</code> / <code>.sd7</code>) placed in the <code>maps/</code> 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.
# '''A text editor.''' Any editor works. For the best experience, set up the [[Recoil:Lua Language Server|Lua Language Server]] with Recoil type stubs so you get autocomplete on all <code>Spring.*</code> functions.


The script performs three tasks: guards against double-initialisation, loads font faces, and registers mouse-cursor aliases. Below is a more complete version that also creates a shared context and configures dp scaling:
Your Recoil data directory typically looks like:


<syntaxhighlight lang="lua">
<pre>
--  luaui/rml_setup.lua
recoil-data/
-- author:  lov + ChrisFloofyKitsune
├── spring          ← engine binary (linux) or spring.exe (windows)
--  License: GNU GPL, v2 or later
├── 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)
</pre>


if (RmlGuard or not RmlUi) then
----
    return
end
-- Prevent this from running more than once
RmlGuard = true


-- Patch CreateContext to set dp_ratio automatically
== 3. Game Archive Structure ==
local oldCreateContext = RmlUi.CreateContext
local function NewCreateContext(name)
    local context = oldCreateContext(name)
    local viewSizeX, viewSizeY = Spring.GetViewGeometry()
    local userScale = Spring.GetConfigFloat("ui_scale", 1)
    local baseWidth, baseHeight = 1920, 1080
    local resFactor = math.min(viewSizeX / baseWidth, viewSizeY / baseHeight)
    -- Floor to 2 decimal places to avoid floating point drift
    context.dp_ratio = math.floor(resFactor * userScale * 100) / 100
    return context
end
RmlUi.CreateContext = NewCreateContext


-- Load fonts (list your .ttf files here)
During development use an <code>.sdd</code> folder — it's just a normal directory the engine treats as an archive.
local font_files = {
Create it inside your <code>games/</code> directory:
    -- "Fonts/MyFont-Regular.ttf",
}
for _, file in ipairs(font_files) do
    RmlUi.LoadFontFace(file, true)
end


-- Map CSS cursor names to engine cursor names
<pre>
-- CSS cursor list: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
games/
RmlUi.SetMouseCursorAlias("default", 'cursornormal')
└── mygame.sdd/
RmlUi.SetMouseCursorAlias("move",    'uimove')
    ├── modinfo.lua          ← required: identifies your game to the engine
RmlUi.SetMouseCursorAlias("pointer", 'Move')
    ├── 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)
</pre>


-- Create the shared context used by all widgets
{{warning|1=The <code>.sdd</code> extension on the folder name is mandatory. Without it the engine will not recognise it as a game archive.}}
RmlUi.CreateContext("shared")
</syntaxhighlight>


=== 3.2 Including the setup in main.lua ===
For production, compress the contents into a <code>.sdz</code> (zip) archive or <code>.sd7</code> (7-zip) archive.
The folder contents become the archive root — <code>modinfo.lua</code> should be at the top level.


Add a single line to <code>luaui/main.lua</code> to run the setup before any widgets load:
=== VFS and dependencies ===


<syntaxhighlight lang="lua">
Your game archive can declare '''dependencies''' on other archives (including other games) via <code>modinfo.lua</code>.
-- luaui/main.lua
The engine merges all archives into the VFS.
VFS.Include(LUAUI_DIRNAME .. "rml_setup.lua", nil, VFS.ZIP)
Files in your game take priority over dependencies.
</syntaxhighlight>
This means you can build on an existing game without copying its assets.


{{Note|The base-content <code>rml_setup.lua</code> only loads a font and sets cursor aliases; it does '''not''' create a context. If you are building a new game you should add context creation here or let each widget create its own context.}}
See [[Recoil:VFS Basics]] for full details on VFS modes and the load order.


----
----


== 4. Writing Your First Document (.rml) ==
== 4. modinfo.lua ==
 
<code>modinfo.lua</code> is the single most important file — without it the engine will not recognise your archive as a game.


Create a file at <code>luaui/widgets/my_widget/my_widget.rml</code>. RML is well-formed XML, so every tag must be closed and attribute values must be quoted.
<syntaxhighlight lang="lua">
-- games/mygame.sdd/modinfo.lua


=== 4.1 Root structure ===
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
    },
}


<syntaxhighlight lang="xml">
return modinfo
<rml>
<head>
    <title>My Widget</title>
    <!-- External stylesheet (optional) -->
    <link type="text/rcss" href="my_widget.rcss"/>
    <!-- Inline styles -->
    <style>
        body { margin: 0; padding: 0; }
    </style>
</head>
<body>
    <!-- Your content here -->
</body>
</rml>
</syntaxhighlight>
</syntaxhighlight>


=== 4.2 Styles (inline and .rcss) ===
{{warning|1=<code>shortName</code> must be unique across all games the engine knows about. Pick something distinctive. Changing it later breaks lobby integrations and replays.}}


RmlUI starts with '''no default styles''' at all — not even block/inline defaults. The RmlUI docs provide an HTML4 base stylesheet you can copy in, but you will need to add form element styles yourself.
=== ModOptions.lua ===


Use <code>dp</code> units instead of <code>px</code> so your UI scales correctly with the player's display and <code>ui_scale</code> setting.
To expose configurable options in lobbies (e.g. starting resources, game mode), add a <code>gamedata/ModOptions.lua</code>:


<syntaxhighlight lang="css">
<syntaxhighlight lang="lua">
/* Typical widget container */
-- gamedata/ModOptions.lua
#my-widget {
-- Returned options appear as sliders/checkboxes/dropdowns in lobby UIs.
    position: absolute;
    width: 400dp;
    right: 10dp;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: auto;  /* required to receive mouse input */
}


#my-widget .panel {
return {
     padding: 10dp;
    {
     border-radius: 8dp;
        key    = "startmetal",
     border: 1dp #6c7086;  /* note: no 'solid' keyword – see section 11 */
        name    = "Starting Metal",
     background-color: rgba(30, 30, 46, 200);
        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>
</syntaxhighlight>


=== 4.3 Data bindings in RML ===
Options are readable in gadgets via <code>Spring.GetModOptions()</code>.
 
----
 
== 5. Your First Unit Definition ==
 
Unit defs are Lua files in <code>units/</code> 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.


Attach a data model to a root element with <code>data-model="model_name"</code>. All data binding attributes inside that element can reference the model.
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",


<syntaxhighlight lang="xml">
        -- ── Model ────────────────────────────────────────────────────
<body>
        -- Path inside the archive (objects3d/commander.dae)
  <!-- data-model scopes all bindings inside this div -->
        objectName  = "commander.dae",
  <div id="my-widget" data-model="my_model">


    <!-- Text interpolation -->
        -- ── Physics ──────────────────────────────────────────────────
    <p>Status: {{status_message}}</p>
        -- 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",


    <!-- Conditional visibility -->
        -- ── Movement ─────────────────────────────────────────────────
    <div data-if="show_details">
        -- Movement class name; references a MoveClass def (see MoveClasses below)
         <p>Extra details go here.</p>
        moveDef = {
    </div>
            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,


    <!-- Dynamic class based on model value -->
        -- ── Construction ─────────────────────────────────────────────
     <div class="panel" data-class-active="is_active">
        -- How many build power units this unit provides
         Active panel
        workerTime  = 200,
    </div>
        -- 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,


    <!-- Loop over an array -->
        -- ── Energy and Metal Production ───────────────────────────────
    <ul>
        metalMake  = 1,      -- passively trickles 1 metal/second
         <li data-for="item, i: items">{{i}}: {{item.label}}</li>
         energyMake  = 20,    -- passively generates 20 energy/second
     </ul>


    <!-- Two-way checkbox binding -->
        -- ── Weapons ──────────────────────────────────────────────────
    <input type="checkbox" data-checked="is_enabled"/>
        -- Up to 3 weapons can be listed; name references a weapon def
        weapons = {
            { def = "COMMANDER_LASER" },
        },


    <!-- Data event: calls model function on click -->
        -- ── Self-Destruct ─────────────────────────────────────────────
    <button data-click="on_button_click()">Click me</button>
        selfDExplosion = "commander_sdc",  -- optional; a weapon def name


   </div>
        -- ── Misc ─────────────────────────────────────────────────────
</body>
        -- 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>
</syntaxhighlight>


'''Data binding reference:'''
{{warning|1=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 <code>lowerkeys()</code> in <code>gamedata/unitdefs_post.lua</code> if you mix styles.}}
 
=== Notable unit def keys ===


{| class="wikitable"
{| class="wikitable"
! Binding !! Effect
! Key !! Type !! Notes
|-
| <code>name</code> || string || Display name in UI. Not the def name (the Lua table key).
|-
| <code>health</code> || number || Max HP.
|-
|-
| <code><nowiki>{{value}}</nowiki></code> || Interpolate model value as text
| <code>moveDef</code> || table || Inline move class definition. See engine docs for all fields.
|-
|-
| <code>data-model="name"</code> || Attach named data model to this element and its children
| <code>maxVelocity</code> || number || Elmos per second. 1 elmo ≈ 8 map height units.
|-
|-
| <code>data-if="flag"</code> || Show element only when flag is truthy
| <code>buildCostMetal</code> || number || Metal cost. Constructor must have enough resources to start building.
|-
|-
| <code>data-class-X="flag"</code> || Add class X when flag is truthy
| <code>buildTime</code> || number || Seconds to build at 1 build power. Actual time = buildTime / workerTime.
|-
|-
| <code>data-for="v, i: arr"</code> || Repeat element for each item in array arr
| <code>workerTime</code> || number || Build power this unit contributes when constructing.
|-
|-
| <code>data-checked="val"</code> || Two-way bind checkbox to boolean model value
| <code>weapons</code> || table || Up to 3 entries; each entry is a table with a <code>def</code> key naming a WeaponDef.
|-
|-
| <code>data-click="fn()"</code> || Call model function on click (data event)
| <code>commander</code> || bool || Marks this unit as a Commander. Lobby UI and engine use this.
|-
|-
| <code>data-attr-src="val"</code> || Dynamically set the src attribute from model value
| <code>customParams</code> || 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 [https://beyond-all-reason.github.io/spring/ldoc/ Recoil Lua API documentation].


== 5. The Lua Widget ==
=== Def pre- and post-processing ===


Each widget is a Lua file that the handler loads. The <code>widget</code> global is injected by the widget handler and provides the lifecycle hooks.
The engine reads <code>gamedata/defs.lua</code> (provided by basecontent) which orchestrates def loading.
 
It first executes <code>gamedata/unitdefs_pre.lua</code> (if present) to populate a <code>Shared</code> table visible to all unit def files, then loads every <code>units/*.lua</code> file, and finally runs <code>gamedata/unitdefs_post.lua</code>.
=== 5.1 Widget skeleton ===


<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
-- luaui/widgets/my_widget/my_widget.lua
-- gamedata/unitdefs_post.lua
if not RmlUi then return end  -- guard: RmlUi not available
-- Normalize key casing and apply game-wide tweaks.


local widget = widget ---@type Widget
for _, unitDef in pairs(UnitDefs) do
    -- lowerkeys() is provided by basecontent; makes all keys lowercase
    lowerkeys(unitDef)
end


function widget:GetInfo()
-- Example: give all units 10% extra health
     return {
for _, unitDef in pairs(UnitDefs) do
         name    = "My Widget",
     if unitDef.health then
        desc    = "Demonstrates RmlUI basics.",
         unitDef.health = unitDef.health * 1.1
        author  = "YourName",
     end
        date    = "2025",
        license = "GNU GPL, v2 or later",
        layer  = 0,
        enabled = true,
     }
end
end
</syntaxhighlight>
</syntaxhighlight>


=== 5.2 Loading the document ===
{{warning|1=The <code>UnitDefs</code> 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 <code>weapons/</code> returning tables.


<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
local RML_FILE  = "luaui/widgets/my_widget/my_widget.rml"
-- weapons/laser.lua
local MODEL_NAME = "my_model"
 
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,


local rml_ctx    -- the RmlUi Context
        -- ── Ballistics ────────────────────────────────────────────────
local dm_handle -- the DataModel handle (MUST be kept – see section 6)
        -- Projectile/beam speed in elmos/second
local document  -- the loaded Document
        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,


-- Initial data model state
        -- ── Damage ────────────────────────────────────────────────────
local init_model = {
        damage = {
    status_message = "Ready",
            -- 'default' applies to all armour types not explicitly listed
    is_enabled    = false,
            default = 50,
     show_details  = false,
        },
    items = {
        -- Area of effect in elmos (0 = single target)
         { label = "Alpha", value = 1 },
        areaOfEffect     = 8,
         { label = "Beta",  value = 2 },
        -- Impulse applied to targets on hit (0 = none)
         impulse          = 0,
         -- Does this weapon set things on fire?
        firestarter      = 0,
     },
     },
    -- Functions can live in the model too (callable from data-events)
    on_button_click = function()
        Spring.Echo("Button was clicked!")
    end,
}
}
</syntaxhighlight>


function widget:Initialize()
Like unit defs, you can post-process weapon defs via <code>gamedata/weapondefs_post.lua</code>.
    rml_ctx = RmlUi.GetContext("shared")


    -- IMPORTANT: assign dm_handle or the engine WILL crash on RML render
----
    dm_handle = rml_ctx:OpenDataModel(MODEL_NAME, init_model)
    if not dm_handle then
        Spring.Echo("[my_widget] Failed to open data model")
        return
    end
 
    document = rml_ctx:LoadDocument(RML_FILE, widget)
    if not document then
        Spring.Echo("[my_widget] Failed to load document")
        return
    end


    document:ReloadStyleSheet()
== 7. Feature Definitions ==
    document:Show()
end
</syntaxhighlight>


=== 5.3 Shutting down cleanly ===
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">
<syntaxhighlight lang="lua">
function widget:Shutdown()
-- features/trees.lua
     if document then
 
         document:Close()
return {
         document = nil
     tree = {
    end
        description    = "A tree.",
    -- Remove the model from the context; frees memory
        -- Model to render
     rml_ctx:RemoveDataModel(MODEL_NAME)
        object        = "tree.dae",
     dm_handle = nil
        -- Health before the feature is destroyed
end
        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>
</syntaxhighlight>


----
----


== 6. Data Models in Depth ==
== 8. Game Rules (modrules.lua) ==
 
<code>gamedata/modrules.lua</code> is read by basecontent's <code>defs.lua</code> to configure fundamental engine behaviour that is not driven by unit/weapon defs.
 
<syntaxhighlight lang="lua">
-- gamedata/modrules.lua


=== 6.1 Opening a data model ===
return {
    system = {
        -- Starting resources (also overridable via ModOptions / start script)
        startMetal  = 1000,
        startEnergy = 1000,


<code>context:OpenDataModel(name, table)</code> copies the structure of the table into a reactive proxy. The name must match the <code>data-model="name"</code> attribute in your RML. It must be unique within the context.
        -- Maximum units per team (0 = unlimited)
        maxUnits    = 1000,


{{Warning|You '''must''' store the return value of <code>OpenDataModel</code> in a variable. Letting it be garbage-collected while the document is open will crash the engine the next time RmlUI tries to render a data binding.}}
        -- Commander handling: 0 = game continues, 1 = game ends, 2 = lineage
        gameMode    = 1,


=== 6.2 Reading and writing values ===
        -- Limit on repair speed (% of max health per second; 0 = unlimited)
        repairSpeed = 0,


For simple string/number/boolean values at the top level of the model you can use dot-notation directly on the handle:
        -- Whether resurrect restores health
        resurrectHealth = false,


<syntaxhighlight lang="lua">
        -- Resource multiplier for reclaiming metal from living units
-- Read a value
        reclaimUnitMethod = 1,
local msg = dm_handle.status_message  -- works


-- Write a value (auto-marks the field dirty; UI updates next frame)
        -- Whether ground features leave wrecks
dm_handle.status_message = "Unit selected"
        featureDeathResurrect = true,
dm_handle.is_enabled     = true
    },
     sound = {
        -- Optional: override specific engine sound events
    },
}
</syntaxhighlight>
</syntaxhighlight>


=== 6.3 Iterating arrays ===
----
 
== 9. Running Your Game ==


Because the handle is a sol2 proxy, you cannot iterate it directly with <code>pairs</code> or <code>ipairs</code>. Call <code>dm_handle:__GetTable()</code> to get the underlying Lua table:
The engine is launched with a '''start script''' — a plain text file (conventionally <code>.sdf</code>) that describes who is playing, which game, and which map.


<syntaxhighlight lang="lua">
=== Minimal start script ===
local tbl = dm_handle:__GetTable()


for i, item in ipairs(tbl.items) do
<syntaxhighlight lang="ini">
     Spring.Echo(i, item.label, item.value)
[GAME]
     item.value = item.value + 1  -- modify in place
{
end
     GameType = My Recoil Game;  -- must match modinfo.lua name or shortName
</syntaxhighlight>
     MapName  = my_map.smf;      -- the .smf file inside the map archive


=== 6.4 Marking dirty ===
    IsHost  = 1;
    MyPlayerName = Dev;


When you modify nested values (e.g. elements inside an array) through <code>__GetTable()</code>, you must tell the model which top-level key changed so that RmlUI re-renders the bound elements:
    [PLAYER0]
    {
        Name      = Dev;
        Password  = ;
        Spectator = 0;
        Team      = 0;
    }


<syntaxhighlight lang="lua">
    [TEAM0]
-- After modifying tbl.items …
    {
dm_handle:__SetDirty("items")
        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;
    }


-- For a simple top-level assignment via dm_handle.key = value,
    [ALLYTEAM0]
-- dirty-marking is done automatically.
    {
        NumAllies = 0;
    }
}
</syntaxhighlight>
</syntaxhighlight>


----
Save this as <code>game.sdf</code> in your Recoil data directory, then launch:
 
<pre>
./spring game.sdf
</pre>


== 7. Events ==
On Windows:


Recoil's RmlUI supports two distinct event families. They look similar but have different scopes — mixing them up is a common source of confusion.
<pre>
spring.exe game.sdf
</pre>


=== 7.1 Normal events (on*) ===
The engine will:
# Read the start script
# Mount your game archive from <code>games/mygame.sdd</code>
# Mount the map archive from <code>maps/my_map.sd7</code>
# Load basecontent (default widget handlers, etc.)
# Start the simulation and call into your Lua scripts


Written as <code>onclick="myFunc()"</code>. The function is resolved from the '''second argument''' passed to <code>LoadDocument</code> the widget table. These events do '''not''' have access to the data model.
{{warning|1=If the engine cannot find your game archive, check that the folder name ends in exactly <code>.sdd</code> (case-sensitive on Linux) and that <code>modinfo.lua</code> is at the root of that folder, '''not''' in a subfolder.}}


<syntaxhighlight lang="lua">
=== Quick test with headless ===
-- Lua
local doc_scope = {
    greet = function(name)
        Spring.Echo("Hello", name)
    end,
}
document = rml_ctx:LoadDocument(RML_FILE, doc_scope)
</syntaxhighlight>


<syntaxhighlight lang="xml">
For automated testing (or when you don't need graphics), use the headless binary:
<!-- RML -->
<button onclick="greet('World')">Say hello</button>
</syntaxhighlight>


=== 7.2 Data events (data-*) ===
<pre>
./spring_headless game.sdf
</pre>


Written as <code>data-click="fn()"</code>. The function is resolved from the '''data model'''. These events have full access to all model values.
Widgets still run, so you can run AI vs AI matches or data-gathering scripts without a window.


<syntaxhighlight lang="lua">
----
-- Lua model
local init_model = {
    counter = 0,
    increment = function()
        -- dm_handle is captured in the closure
        dm_handle.counter = dm_handle.counter + 1
    end,
}
</syntaxhighlight>


<syntaxhighlight lang="xml">
== 10. Lua Scripting Basics ==
<!-- RML -->
<p>Count: {{counter}}</p>
<button data-click="increment()">+1</button>
</syntaxhighlight>


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


{| class="wikitable"
{| class="wikitable"
! Type !! Syntax example !! Scope
! Environment !! Location !! Purpose
|-
| '''LuaRules''' (synced) || <code>luarules/main.lua</code> || Authoritative game logic. Desync if misused.
|-
| '''LuaRules''' (unsynced) || <code>luarules/draw.lua</code> || Unsynced effects and UI driven by game logic.
|-
|-
| Normal || <code>onclick="fn()"</code> || <code>LoadDocument</code> second argument (widget table)
| '''LuaUI''' || <code>luaui/main.lua</code> || Player-facing UI: HUD, minimap overlays, etc.
|-
|-
| Data || <code>data-click="fn()"</code> || Data model table
| '''LuaGaia''' || <code>luagaia/main.lua</code> || Logic for the neutral Gaia team (wrecks, features, world events).
|-
|-
| Normal || <code>onmouseover="fn()"</code> || <code>LoadDocument</code> second argument
| '''LuaIntro''' || <code>luaintro/main.lua</code> || Loading screen, briefings.
|-
|-
| Data || <code>data-mouseover="fn()"</code> || Data model table
| '''LuaMenu''' || <code>luamenu/main.lua</code> || Main menu (pre-game). Requires a menu archive.
|}
|}


----
Basecontent provides default <code>main.lua</code> entry points and wupget handler boilerplate so you can start writing gadgets and widgets without building the handler yourself.
 
=== Your first gadget ===


== 8. Custom Recoil Elements ==
A gadget is a Lua file the gadget handler loads from <code>luarules/gadgets/</code>.


Beyond standard HTML elements, Recoil adds two custom RML elements.
<syntaxhighlight lang="lua">
-- luarules/gadgets/game_end.lua
-- Ends the game when a player's Commander is destroyed.


=== 8.1 The &lt;texture&gt; element ===
local gadget = gadget ---@type Gadget


Renders any engine texture — unit icons, map thumbnails, GL4 render targets, etc. Behaves identically to <code><nowiki><img></nowiki></code> except the <code>src</code> attribute takes a '''Recoil texture reference string''' (see engine docs for the full format).
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


<syntaxhighlight lang="xml">
-- Only run the logic in synced mode
<!-- Load a file from VFS -->
if not gadgetHandler:IsSyncedCode() then return end
<texture src="unitpics/armcom.png" width="64dp" height="64dp"/>


<!-- Reference a named GL texture -->
-- Track alive Commanders per team
<texture src="%luaui:myTextureName" width="128dp" height="128dp"/>
local commanderByTeam = {}
</syntaxhighlight>


=== 8.2 The &lt;svg&gt; element ===
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


Renders an SVG image. Unlike upstream RmlUI, the Recoil implementation accepts either a file path '''or raw inline SVG data''' in the <code>src</code> attribute:
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>


<syntaxhighlight lang="xml">
=== Your first widget ===
<!-- External file -->
<svg src="images/icon.svg" width="32dp" height="32dp"/>


<!-- Inline SVG data -->
A widget is a Lua file the widget handler loads from <code>luaui/widgets/</code>.
<svg src="<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'><circle cx='5' cy='5' r='4' fill='red'/></svg>"
    width="32dp" height="32dp"/>
</syntaxhighlight>


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


== 9. Debugging ==
local widget = widget ---@type Widget


RmlUI ships with a DOM debugger that you can open in-game to inspect elements, check computed styles, and view event logs.
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


<syntaxhighlight lang="lua">
function widget:DrawScreen()
-- Enable the debugger for the shared context.
    local metal, energy = Spring.GetTeamResources(Spring.GetMyTeamID())
-- Call this AFTER the context has been created.
    if not metal then return end
RmlUi.SetDebugContext('shared')


-- A handy pattern: toggle via a build flag in rml_setup.lua
    gl.Color(1, 1, 1, 1)
local DEBUG_RMLUI = true  -- set false before shipping
     gl.Text(string.format("Metal: %.0f  Energy: %.0f", metal, energy),
if DEBUG_RMLUI then
            10, 40, 16, "s")
     RmlUi.SetDebugContext('shared')
end
end
</syntaxhighlight>
</syntaxhighlight>


Once enabled, a small panel appears in the game window. Click '''Debug''' to open the inspector or '''Log''' for the event log.
=== Callins and callouts ===


{{Note|The debugger attaches to one context at a time. Using the shared context means you see all widgets in one place — very useful for tracking cross-widget interactions.}}
'''Callins''' are functions in your code the engine calls at the right moment: <code>UnitCreated</code>, <code>UnitDestroyed</code>, <code>GameOver</code>, <code>DrawScreen</code>, <code>Update</code>, etc.
Check the full list at runtime with <code>Script.GetCallInList()</code>.


----
'''Callouts''' are <code>Spring.*</code> functions you call out to the engine: <code>Spring.GetTeamResources</code>, <code>Spring.GiveOrderToUnit</code>, <code>Spring.Echo</code>, etc.
Full documentation is at [https://beyond-all-reason.github.io/spring/ldoc/ beyond-all-reason.github.io/spring/ldoc/].
 
{{warning|1=Synced gadgets have strict rules — only call synced-safe functions. Calling unsynced functions from synced code causes a desync error. When in doubt, check <code>gadgetHandler:IsSyncedCode()</code> and gate appropriately.}}


== 10. Best Practices ==
=== Communicating between environments ===


; Use dp units everywhere
The Lua environments are isolated from each other by design.
: The <code>dp</code> unit scales with <code>context.dp_ratio</code>, so your UI automatically adapts to different resolutions and the player's <code>ui_scale</code> setting.
Common bridges:


; One shared context per game
{| class="wikitable"
: Beyond All Reason and most community projects use a single <code>'shared'</code> context. Documents in the same context can reference the same data models and are Z-ordered relative to each other.
! Method !! Direction !! Notes
|-
| <code>WG</code> / <code>GG</code> table || Within one environment || Widgets share <code>WG</code>; gadgets share <code>GG</code>.
|-
| <code>SendToUnsynced</code> / <code>RecvFromSynced</code> || Synced → Unsynced || Pass data from game logic to rendering/UI.
|-
| <code>Spring.SendLuaRulesMsg</code> / <code>gadget:RecvLuaMsg</code> || LuaUI → LuaRules || UI triggers a game action beyond unit orders.
|-
| <code>Spring.SendLuaUIMsg</code> / <code>widget:RecvLuaMsg</code> || LuaUI ↔ LuaUI (all players) || Broadcast UI state to other players.
|-
| Rules params (<code>Spring.SetRulesParam</code>) || LuaRules → all || Expose synced data as read-only key/values.
|}


; Keep data models flat when possible
See [[Recoil:Wupget Communication]] for detailed examples.
: Top-level writes via <code>dm_handle.key = value</code> are automatically dirty-marked. Deeply nested structures require manual <code>__SetDirty</code> calls — simpler models mean fewer bugs.


; Separate per-document and shared styles
----
: Put styles unique to one document in a <code><nowiki><style></nowiki></code> block inside the <code>.rml</code> file. Put styles shared across multiple documents in a <code>.rcss</code> file and link it.


; Guard against missing RmlUi
== 11. Useful Debug Commands ==
: Always start widgets with <code>if not RmlUi then return end</code> to prevent errors in environments where RmlUI is unavailable.


; Check return values
In-game chat commands prefixed with <code>/</code> control engine and game state.
: Both <code>OpenDataModel</code> and <code>LoadDocument</code> can fail silently if paths are wrong. Always check for nil and log an error so you know immediately what happened.
With cheats enabled (<code>/cheat</code>) additional commands become available:


; Organise by widget folder
{| class="wikitable"
: Group <code>luaui/widgets/my_widget/my_widget.lua</code>, <code>.rml</code>, and <code>.rcss</code> together. This makes the project navigable and each widget self-contained.
! Command !! Effect
|-
| <code>/cheat</code> || Toggle cheats on/off. Required for most debug commands.
|-
| <code>/give [unitname]</code> || Spawn a unit at the cursor position.
|-
| <code>/destroy</code> || Destroy selected units immediately.
|-
| <code>/invincible</code> || Toggle invincibility for selected units.
|-
| <code>/godmode</code> || Toggle god mode (all units obey all players).
|-
| <code>/speed [x]</code> || Set game speed multiplier (e.g. <code>/speed 5</code>).
|-
| <code>/nopause</code> || Disallow pausing.
|-
| <code>/luarules reload</code> || Reload LuaRules scripts without restarting.
|-
| <code>/luaui reload</code> || Reload LuaUI scripts without restarting.
|-
| <code>/editdefs</code> || Enter def-editing mode (requires cheats). See unit def docs.
|-
| <code>Spring.Echo("msg")</code> || Print a message to the in-game console from Lua.
|}


----
----


== 11. RCSS Gotchas ==
== 12. Maps and Map Integration ==
 
Maps are separate archives (<code>.sd7</code> or <code>.sdz</code>) placed in <code>maps/</code>.
They are not shipped with your game.
The map defines the terrain height data (<code>.smf</code>), sky, water, and can optionally define starting metal spots and features.


RCSS closely follows CSS2 but has several important differences that will trip up web developers:
=== Controlling what maps do to your game ===


; No default stylesheet
Maps can include a <code>mapinfo.lua</code> which may set atmosphere, gravity, and other properties.
: RmlUI ships with zero default styles. Block/inline, heading sizes, list bullets — none of it exists unless you add it. Copy the HTML4 base sheet from the RmlUI docs as a starting point.
A map can also override unit def values via <code>MapOptions.lua</code>, which your game can read with <code>Spring.GetMapOptions()</code>.


; RGBA alpha is 0–255
You can '''restrict which maps are compatible''' by checking map properties in a gadget at startup and calling <code>Spring.GameOver</code> if requirements are not met.
: <code>rgba(255, 0, 0, 128)</code> means 50% transparent red. CSS uses 0.0–1.0 for alpha. The <code>opacity</code> property still uses 0.0–1.0.


; Border shorthand has no style keyword
=== Metal spots ===
: <code>border: 1dp #6c7086;</code> works. <code>border: 1dp solid #6c7086;</code> does '''not''' work. Only solid borders are supported; <code>border-style</code> is unavailable.


; background-* properties use decorators
Metal spots (mexes) are typically defined in the map as SMF/SMD metal map data, or via a Lua file in the map archive.
: <code>background-color</code> works normally. <code>background-image</code>, <code>background-size</code>, etc. are replaced by the decorator system. See the RmlUI docs for syntax.
Your gadgets can read them with <code>Spring.GetMetalMapSize()</code> and <code>Spring.ExtractorInRangeMetalMap()</code>.
Extractor units that generate metal must have a <code>extractsMetal</code> unit def key > 0 and be placed over a metal spot.


; No list styling
----
: <code>list-style-type</code> and friends are not supported. <code>ul</code>/<code>ol</code>/<code>li</code> have no special rendering — treat them as plain div-like elements.


; Input text is set by content, not value
== 13. Packaging and Distribution ==
: For <code><nowiki><input type="button"></nowiki></code> and <code><nowiki><input type="submit"></nowiki></code> the displayed text comes from the element's text content, not the <code>value</code> attribute (unlike HTML).


; pointer-events: auto required
When your game is ready for distribution, compress the <code>.sdd</code> folder contents into an <code>.sdz</code> (zip) or <code>.sd7</code> (7-zip) file.
: By default elements do not receive mouse events. Add <code>pointer-events: auto</code> to any element that needs to be interactive.


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


== 12. Differences from Upstream RmlUI ==
# Linux: create .sd7 (7-zip, better compression)
cd games/mygame.sdd
7z a ../mygame-0.1.0.sd7 .
</pre>


The Recoil implementation is based on the official RmlUI library but adds engine-specific features and changes some defaults:
{{warning|1=Make sure <code>modinfo.lua</code> ends up at the '''root''' of the archive, not inside a subfolder. Some zip tools create an extra folder level automatically.}}


{| class="wikitable"
=== Rapid ===
! Feature !! Description
|-
| '''<nowiki><texture></nowiki> element''' || A custom element that renders engine-managed textures via Recoil texture reference strings. Not present in upstream.
|-
| '''<nowiki><svg></nowiki> inline data''' || The <code>src</code> attribute accepts raw SVG markup in addition to file paths. Upstream only supports file paths.
|-
| '''VFS file loader''' || All file paths are resolved through Recoil's Virtual File System, so <code>.rml</code> and <code>.rcss</code> files work inside <code>.sdz</code> game archives transparently.
|-
| '''Lua bindings via sol2''' || The Lua API is provided by RmlSolLua (derived from [https://github.com/LoneBoco/RmlSolLua LoneBoco/RmlSolLua]) rather than the upstream Lua plugin, offering tighter engine integration.
|-
| '''dp_ratio via context''' || <code>context.dp_ratio</code> is set from code rather than the system DPI, allowing games to honour the player's <code>ui_scale</code> preference.
|}


{{Note|For everything not listed above, the upstream RmlUI documentation at [https://mikke89.github.io/RmlUiDoc/ mikke89.github.io/RmlUiDoc] applies directly.}}
[https://github.com/beyond-all-reason/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 [https://github.com/beyond-all-reason/RecoilEngine/blob/master/doc/site/content/articles/start-script-format.md start script format] for integration details.


----
----


== 13. IDE Setup ==
== 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 <code>units/</code>.
 
; '''Resource extractors'''
: A unit with <code>extractsMetal > 0</code> placed over a metal spot generates metal. Add an energy converter unit to turn metal into energy and vice versa.


Configuring your editor for <code>.rml</code> and <code>.rcss</code> file extensions dramatically improves the authoring experience.
; '''Pathfinding and MoveClasses'''
: Define <code>movedefs</code> in <code>gamedata/movedefs.lua</code> to control how different unit types traverse terrain (hover, boat, amphibious, kbot, tank, aircraft).


=== VS Code ===
; '''AI support'''
: The Skirmish AI interface lets players add bot opponents without any extra gadget work. Beyond that, you can write a [[https://springrts.com/wiki/Lua_AI Lua AI]] for fully custom bot behaviour inside your game's Lua scripting.


# Open any <code>.rml</code> file in VS Code.
; '''Full UI'''
# Click the language mode indicator in the status bar (usually shows ''Plain Text'').
: The basecontent widget handler gives you a basic HUD for free. For a polished interface use [[Recoil:RmlUI Starter Guide|RmlUI]] for HTML/CSS-style widgets, or raw OpenGL via <code>gl.*</code> callouts.
# Choose '''Configure File Association for '.rml''''
# Select '''HTML''' from the list.
# Repeat for <code>.rcss</code> files, selecting '''CSS'''.


=== Lua Language Server (lua-ls) ===
; '''Maps'''
: Creating your own map requires tools like [https://github.com/beyond-all-reason/MapConv MapConv] (height and texture compilation) and [https://springrts.com/wiki/SpringMapEdit SpringMapEdit]. See the Recoil map documentation for details.


Add the Recoil type annotations to your workspace for autocomplete on <code>RmlUi</code>, <code>Spring</code>, and all widget APIs. See the Recoil docs guide on the Lua Language Server for details.
; '''Replays and dedicated servers'''
: Replays are recorded automatically when <code>RecordDemo=1</code> is set in the start script. For large-scale multiplayer use the dedicated binary (<code>spring_dedicated</code>) and an autohost such as [https://springrts.com/wiki/SPADS SPADS] or [https://springrts.com/wiki/Springie Springie].


----
----


== 14. Quick Reference ==
== 15. Further Reading ==


=== Lua API ===
=== On this wiki ===


{| class="wikitable"
* [[Recoil:VFS Basics]] — full documentation of the Virtual File System, modes, and load order
! Call !! Description
* [[Recoil:Unit Types Basics]] — deep dive into unit defs, post-processing, and the wupget-facing UnitDefs table
|-
* [[Recoil:Widgets and Gadgets]] — the addon (wupget) system in full detail
| <code>RmlUi.CreateContext(name)</code> || Create a new rendering context
* [[Recoil:Wupget Communication]] — how to pass data between Lua environments
|-
* [[Recoil:RmlUI Starter Guide]] — reactive HTML/CSS-style UI for your game
| <code>RmlUi.GetContext(name)</code> || Retrieve an existing context by name
* [[Recoil:Headless and Dedicated]] — server binaries and automated testing
|-
* [[Recoil:Migrating From Spring]] — for existing Spring 105 games
| <code>RmlUi.LoadFontFace(path, fallback)</code> || Register a <code>.ttf</code> font (<code>fallback=true</code> recommended)
|-
| <code>RmlUi.SetMouseCursorAlias(css, engine)</code> || Map a CSS cursor name to an engine cursor name
|-
| <code>RmlUi.SetDebugContext(name)</code> || Attach the DOM debugger to a named context
|-
| <code>ctx:OpenDataModel(name, tbl)</code> || Create reactive data model from Lua table; '''store the return value!'''
|-
| <code>ctx:RemoveDataModel(name)</code> || Destroy a data model and free its memory
|-
| <code>ctx:LoadDocument(path, scope)</code> || Parse an RML file; scope is used for normal (<code>on*</code>) events
|-
| <code>doc:Show()</code> || Make the document visible
|-
| <code>doc:Hide()</code> || Hide the document (keeps it in memory)
|-
| <code>doc:Close()</code> || Destroy the document
|-
| <code>doc:ReloadStyleSheet()</code> || Reload linked <code>.rcss</code> files (useful during development)
|-
| <code>dm:__GetTable()</code> || Get the underlying Lua table for iteration
|-
| <code>dm:__SetDirty(key)</code> || Mark a top-level model key as changed; triggers re-render
|-
| <code>dm.key = value</code> || Write a top-level value; auto-marks dirty
|}


=== Useful Links ===
=== External resources ===


* [https://mikke89.github.io/RmlUiDoc/ RmlUI official documentation]
* [https://beyond-all-reason.github.io/spring/ldoc/ Recoil Lua API reference] — complete callout/callin documentation
* [https://mikke89.github.io/RmlUiDoc/pages/data_bindings/views_and_controllers.html RmlUI data bindings reference]
* [https://github.com/beyond-all-reason/RecoilEngine RecoilEngine on GitHub] — source, releases, and issue tracker
* [https://mikke89.github.io/RmlUiDoc/pages/rcss.html RmlUI RCSS reference]
* [https://recoilengine.org/ recoilengine.org] — official website with downloads and changelog
* [https://mikke89.github.io/RmlUiDoc/pages/rml/html4_style_sheet.html RmlUI HTML4 base stylesheet]
* [https://discord.gg/recoil Recoil Discord] — community support and engine developer discussions
* [https://mikke89.github.io/RmlUiDoc/pages/rcss/decorators.html RmlUI decorators (backgrounds)]
* [https://springrts.com/wiki/Simple_Game_Tutorial Spring Simple Game Tutorial] — older but still broadly applicable tutorial (Spring and Recoil are highly compatible)
* [https://github.com/mikke89/RmlUi RmlUI GitHub]
* [https://github.com/beyond-all-reason/RecoilEngine/tree/master/rts/Rml RecoilEngine source (rts/Rml/)]
* Recoil texture reference strings — see engine docs: <code>articles/texture-reference-strings</code>


[[Category:Guides]]
[[Category:Guides]]
[[Category:LuaUI]]
[[Category:Recoil]]
[[Category:RmlUI]]
[[Category:Game Development]]

Revision as of 23:02, 5 March 2026

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