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
No edit summary
Tag: Reverted
Undo revision 2705 by Qrow (talk)
Tag: Undo
 
(4 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{DISPLAYTITLE:Making A Basic Recoil Game}}
{{DISPLAYTITLE:RmlUI Starter Guide}}
__TOC__
__TOC__


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


This guide walks you through creating a minimal but functional game with the Recoil engine.
'''''For RecoilEngine game developers — build reactive, HTML/CSS-style game UIs in Lua.'''''
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.


{{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.}}
{| 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. What is Recoil? ==
== 1. Introduction ==


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


* '''Physics simulation''' — projectile trajectories, terrain collision, unit movement
RecoilEngine ships with a full RmlUI integration including:
* '''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.
* An OpenGL 3 renderer tightly coupled to the engine's rendering pipeline
You control the rules, the units, the UI, and everything else; the engine provides the platform.
* A virtual-filesystem-aware file loader so your <code>.rml</code> and <code>.rcss</code> files live inside game archives
* 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


=== Key terminology ===
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.
 
{{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"
! Term !! Meaning
! Concept !! Description
|-
|-
| '''Game''' || Your content archive loaded by the engine (units, Lua scripts, textures, etc.)
| '''Context''' || A named container that holds documents and data models. Multiple contexts can exist simultaneously, each rendered independently.
|-
|-
| '''Map''' || The terrain archive (.smf height data + textures). Distributed separately from the game.
| '''Document''' || A parsed RML file representing a DOM tree. Documents live inside a Context and can be shown, hidden, or closed.
|-
|-
| '''Gadget''' || A Lua script that runs game logic (synced or unsynced). Lives in <code>luarules/gadgets/</code>.
| '''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>.
|-
|-
| '''Widget''' || A Lua script that runs UI logic. Lives in <code>luaui/widgets/</code>.
| '''RCSS''' || The styling language. Similar to CSS2 with some differences (see section 11). File extension: <code>.rcss</code>.
|-
|-
| '''Wupget''' || Informal collective term for widgets and gadgets.
| '''Data Model''' || A Lua table exposed to the RML document via reactive data bindings. Changes to the model automatically update the rendered UI.
|-
| '''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.


----
----


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


Before starting you need:
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.


# '''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).
=== 3.1 The rml_setup.lua script ===
# '''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.


Your Recoil data directory typically looks like:
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:


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


== 3. Game Archive Structure ==
-- Patch CreateContext to set dp_ratio automatically
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


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


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


{{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.}}
-- Create the shared context used by all widgets
RmlUi.CreateContext("shared")
</syntaxhighlight>


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


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


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


See [[Recoil:VFS Basics]] for full details on VFS modes and the load order.
{{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.}}


----
----


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


<syntaxhighlight lang="lua">
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.
-- games/mygame.sdd/modinfo.lua


local modinfo = {
=== 4.1 Root structure ===
    -- 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 lang="xml">
<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>


{{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.}}
=== 4.2 Styles (inline and .rcss) ===


=== ModOptions.lua ===
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.


To expose configurable options in lobbies (e.g. starting resources, game mode), add a <code>gamedata/ModOptions.lua</code>:
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.


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


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


Options are readable in gadgets via <code>Spring.GetModOptions()</code>.
=== 4.3 Data bindings in RML ===
 
----
 
== 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.


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


        -- ── Physics ──────────────────────────────────────────────────
    <!-- Text interpolation -->
        -- Maximum health points
    <p>Status: {{status_message}}</p>
        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 ─────────────────────────────────────────────────
    <!-- Conditional visibility -->
        -- Movement class name; references a MoveClass def (see MoveClasses below)
    <div data-if="show_details">
        moveDef = {
         <p>Extra details go here.</p>
            family      = "tank",
    </div>
            -- 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 ─────────────────────────────────────────────
    <!-- Dynamic class based on model value -->
        -- How many build power units this unit provides
    <div class="panel" data-class-active="is_active">
        workerTime  = 200,
         Active panel
        -- How much metal it costs to build
    </div>
        buildCostMetal  = 0,    -- Commanders are free (spawned by engine)
         buildCostEnergy = 0,
        -- Build time in seconds at 1 build power
        buildTime  = 1,


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


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


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


        -- ── Misc ─────────────────────────────────────────────────────
   </div>
        -- Used in start scripts to assign this unit as Commander
</body>
        commander   = true,
        -- Sight radius in elmos
        sightDistance = 660,
        -- Radar range in elmos (0 = no radar)
        radarDistance = 0,
    },
}
</syntaxhighlight>
</syntaxhighlight>


{{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.}}
'''Data binding reference:'''
 
=== Notable unit def keys ===


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


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].
----


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


The engine reads <code>gamedata/defs.lua</code> (provided by basecontent) which orchestrates def loading.
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.
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">
-- gamedata/unitdefs_post.lua
-- luaui/widgets/my_widget/my_widget.lua
-- Normalize key casing and apply game-wide tweaks.
if not RmlUi then return end  -- guard: RmlUi not available


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


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


{{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.}}
=== 5.2 Loading the document ===
 
----
 
== 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">
-- weapons/laser.lua
local RML_FILE  = "luaui/widgets/my_widget/my_widget.rml"
 
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,


        -- ── Ballistics ────────────────────────────────────────────────
local rml_ctx    -- the RmlUi Context
        -- Projectile/beam speed in elmos/second
local dm_handle -- the DataModel handle (MUST be kept – see section 6)
        projectilespeed = 800,
local document  -- the loaded Document
        -- 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 ────────────────────────────────────────────────────
-- Initial data model state
        damage = {
local init_model = {
            -- 'default' applies to all armour types not explicitly listed
    status_message = "Ready",
            default = 50,
    is_enabled    = false,
        },
    show_details  = false,
        -- Area of effect in elmos (0 = single target)
    items = {
         areaOfEffect    = 8,
         { label = "Alpha", value = 1 },
        -- Impulse applied to targets on hit (0 = none)
         { label = "Beta", value = 2 },
         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>


Like unit defs, you can post-process weapon defs via <code>gamedata/weapondefs_post.lua</code>.
function widget:Initialize()
    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


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


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


----
----


== 8. Game Rules (modrules.lua) ==
== 6. Data Models in Depth ==


<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.
=== 6.1 Opening a data model ===


<syntaxhighlight lang="lua">
<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.
-- gamedata/modrules.lua


return {
{{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.}}
    system = {
        -- Starting resources (also overridable via ModOptions / start script)
        startMetal  = 1000,
        startEnergy = 1000,


        -- Maximum units per team (0 = unlimited)
=== 6.2 Reading and writing values ===
        maxUnits    = 1000,


        -- Commander handling: 0 = game continues, 1 = game ends, 2 = lineage
For simple string/number/boolean values at the top level of the model you can use dot-notation directly on the handle:
        gameMode    = 1,


        -- Limit on repair speed (% of max health per second; 0 = unlimited)
<syntaxhighlight lang="lua">
        repairSpeed = 0,
-- Read a value
 
local msg = dm_handle.status_message  -- works
        -- Whether resurrect restores health
        resurrectHealth = false,
 
        -- Resource multiplier for reclaiming metal from living units
        reclaimUnitMethod = 1,


        -- Whether ground features leave wrecks
-- Write a value (auto-marks the field dirty; UI updates next frame)
        featureDeathResurrect = true,
dm_handle.status_message = "Unit selected"
    },
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">
local tbl = dm_handle:__GetTable()


=== Minimal start script ===
for i, item in ipairs(tbl.items) do
    Spring.Echo(i, item.label, item.value)
    item.value = item.value + 1  -- modify in place
end
</syntaxhighlight>


<syntaxhighlight lang="ini">
=== 6.4 Marking dirty ===
[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;
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:
    MyPlayerName = Dev;


    [PLAYER0]
<syntaxhighlight lang="lua">
    {
-- After modifying tbl.items …
        Name      = Dev;
dm_handle:__SetDirty("items")
        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]
-- For a simple top-level assignment via dm_handle.key = value,
    {
-- dirty-marking is done automatically.
        NumAllies = 0;
    }
}
</syntaxhighlight>
</syntaxhighlight>


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


<pre>
== 7. Events ==
./spring game.sdf
</pre>


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>
=== 7.1 Normal events (on*) ===
spring.exe game.sdf
</pre>


The engine will:
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.
# 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


{{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">
-- Lua
local doc_scope = {
    greet = function(name)
        Spring.Echo("Hello", name)
    end,
}
document = rml_ctx:LoadDocument(RML_FILE, doc_scope)
</syntaxhighlight>


=== Quick test with headless ===
<syntaxhighlight lang="xml">
<!-- RML -->
<button onclick="greet('World')">Say hello</button>
</syntaxhighlight>


For automated testing (or when you don't need graphics), use the headless binary:
=== 7.2 Data events (data-*) ===


<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.
./spring_headless game.sdf
</pre>


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


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


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


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.
----
 
== 8. Custom Recoil Elements ==


=== Your first gadget ===
Beyond standard HTML elements, Recoil adds two custom RML elements.


A gadget is a Lua file the gadget handler loads from <code>luarules/gadgets/</code>.
=== 8.1 The &lt;texture&gt; element ===


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


local gadget = gadget ---@type Gadget
<syntaxhighlight lang="xml">
<!-- Load a file from VFS -->
<texture src="unitpics/armcom.png" width="64dp" height="64dp"/>


function gadget:GetInfo()
<!-- Reference a named GL texture -->
    return {
<texture src="%luaui:myTextureName" width="128dp" height="128dp"/>
        name    = "Game End Conditions",
</syntaxhighlight>
        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
=== 8.2 The &lt;svg&gt; element ===
if not gadgetHandler:IsSyncedCode() then return end


-- Track alive Commanders per team
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:
local commanderByTeam = {}


function gadget:UnitFinished(unitID, unitDefID, teamID)
<syntaxhighlight lang="xml">
    local def = UnitDefs[unitDefID]
<!-- External file -->
    -- UnitDefs inside wupgets is indexed by numeric ID, not name
<svg src="images/icon.svg" width="32dp" height="32dp"/>
    if def and def.isCommander then
        commanderByTeam[teamID] = unitID
    end
end


function gadget:UnitDestroyed(unitID, unitDefID, teamID)
<!-- Inline SVG data -->
    if commanderByTeam[teamID] == unitID then
<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>"
        commanderByTeam[teamID] = nil
    width="32dp" height="32dp"/>
        -- 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>


=== Your first widget ===
----
 
== 9. Debugging ==


A widget is a Lua file the widget handler loads from <code>luaui/widgets/</code>.
RmlUI ships with a DOM debugger that you can open in-game to inspect elements, check computed styles, and view event logs.


<syntaxhighlight lang="lua">
<syntaxhighlight lang="lua">
-- luaui/widgets/resources_display.lua
-- Enable the debugger for the shared context.
-- Draws current metal and energy income in the corner of the screen.
-- Call this AFTER the context has been created.
 
RmlUi.SetDebugContext('shared')
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)
-- A handy pattern: toggle via a build flag in rml_setup.lua
     gl.Text(string.format("Metal: %.0f  Energy: %.0f", metal, energy),
local DEBUG_RMLUI = true  -- set false before shipping
            10, 40, 16, "s")
if DEBUG_RMLUI then
     RmlUi.SetDebugContext('shared')
end
end
</syntaxhighlight>
</syntaxhighlight>


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


'''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.
{{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.}}
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.}}


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


The Lua environments are isolated from each other by design.
; Use dp units everywhere
Common bridges:
: 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.


{| class="wikitable"
; One shared context per game
! Method !! Direction !! Notes
: 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.
|-
| <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.
|}


See [[Recoil:Wupget Communication]] for detailed examples.
; Keep data models flat when possible
: 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.


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


In-game chat commands prefixed with <code>/</code> control engine and game state.
; Check return values
With cheats enabled (<code>/cheat</code>) additional commands become available:
: 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.


{| class="wikitable"
; Organise by widget folder
! Command !! Effect
: 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.
|-
| <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.
|}


----
----


== 12. Maps and Map Integration ==
== 11. RCSS Gotchas ==


Maps are separate archives (<code>.sd7</code> or <code>.sdz</code>) placed in <code>maps/</code>.
RCSS closely follows CSS2 but has several important differences that will trip up web developers:
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.


=== Controlling what maps do to your game ===
; No default stylesheet
: 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.


Maps can include a <code>mapinfo.lua</code> which may set atmosphere, gravity, and other properties.
; RGBA alpha is 0–255
A map can also override unit def values via <code>MapOptions.lua</code>, which your game can read with <code>Spring.GetMapOptions()</code>.
: <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.


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.
; Border shorthand has no style keyword
: <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.


=== Metal spots ===
; background-* properties use decorators
: <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.


Metal spots (mexes) are typically defined in the map as SMF/SMD metal map data, or via a Lua file in the map archive.
; No list styling
Your gadgets can read them with <code>Spring.GetMetalMapSize()</code> and <code>Spring.ExtractorInRangeMetalMap()</code>.
: <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.
Extractor units that generate metal must have a <code>extractsMetal</code> unit def key > 0 and be placed over a metal spot.


----
; Input text is set by content, not value
: 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).


== 13. Packaging and Distribution ==
; pointer-events: auto required
: By default elements do not receive mouse events. Add <code>pointer-events: auto</code> to any element that needs to be interactive.


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.
----


<pre>
== 12. Differences from Upstream RmlUI ==
# Linux: create .sdz (zip)
cd games/mygame.sdd
zip -r ../mygame-0.1.0.sdz .


# Linux: create .sd7 (7-zip, better compression)
The Recoil implementation is based on the official RmlUI library but adds engine-specific features and changes some defaults:
cd games/mygame.sdd
7z a ../mygame-0.1.0.sd7 .
</pre>


{{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"
 
! Feature !! Description
=== Rapid ===
|-
| '''<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.
|}


[https://github.com/beyond-all-reason/rapid Rapid] is the standard package distribution system for Recoil games.
{{Note|For everything not listed above, the upstream RmlUI documentation at [https://mikke89.github.io/RmlUiDoc/ mikke89.github.io/RmlUiDoc] applies directly.}}
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.


----
----


== 14. Next Steps ==
== 13. IDE Setup ==
 
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.


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


; '''AI support'''
=== VS Code ===
: 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.


; '''Full UI'''
# Open any <code>.rml</code> file in VS Code.
: 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.
# Click the language mode indicator in the status bar (usually shows ''Plain Text'').
# Choose '''Configure File Association for '.rml''''
# Select '''HTML''' from the list.
# Repeat for <code>.rcss</code> files, selecting '''CSS'''.


; '''Maps'''
=== Lua Language Server (lua-ls) ===
: 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.


; '''Replays and dedicated servers'''
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 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].


----
----


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


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


* [[Recoil:VFS Basics]] — full documentation of the Virtual File System, modes, and load order
{| class="wikitable"
* [[Recoil:Unit Types Basics]] — deep dive into unit defs, post-processing, and the wupget-facing UnitDefs table
! Call !! Description
* [[Recoil:Widgets and Gadgets]] — the addon (wupget) system in full detail
|-
* [[Recoil:Wupget Communication]] — how to pass data between Lua environments
| <code>RmlUi.CreateContext(name)</code> || Create a new rendering context
* [[Recoil:RmlUI Starter Guide]] — reactive HTML/CSS-style UI for your game
|-
* [[Recoil:Headless and Dedicated]] — server binaries and automated testing
| <code>RmlUi.GetContext(name)</code> || Retrieve an existing context by name
* [[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
|}


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


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

Latest revision as of 23:18, 5 March 2026

RmlUI Starter Guide

For RecoilEngine game developers — build reactive, HTML/CSS-style game UIs in Lua.

Attribute Value
Engine RecoilEngine (Spring)
Framework RmlUi (mikke89/RmlUi)
Language Lua (via sol2 bindings)
Availability LuaUI (LuaIntro / LuaMenu planned)

1. Introduction

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.

RecoilEngine ships with a full RmlUI integration including:

  • An OpenGL 3 renderer tightly coupled to the engine's rendering pipeline
  • A virtual-filesystem-aware file loader so your .rml and .rcss files live inside game archives
  • Complete Lua bindings via sol2 so every RmlUI object is accessible from Lua
  • Custom elements: <texture> for engine textures and <svg> 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.

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:

Concept Description
Context A named container that holds documents and data models. Multiple contexts can exist simultaneously, each rendered independently.
Document A parsed RML file representing a DOM tree. Documents live inside a Context and can be shown, hidden, or closed.
RML The markup language for documents. Very similar to XHTML (well-formed XML). The root tag is <rml> instead of <html>.
RCSS The styling language. Similar to CSS2 with some differences (see section 11). File extension: .rcss.
Data Model A Lua table exposed to the RML document via reactive data bindings. Changes to the model automatically update the rendered UI.

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 .lua, a .rml, and optionally a .rcss file — grouping them in a folder per widget keeps things tidy.


3. Setup

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

3.1 The rml_setup.lua script

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:

<syntaxhighlight lang="lua"> -- luaui/rml_setup.lua -- author: lov + ChrisFloofyKitsune -- License: GNU GPL, v2 or later

if (RmlGuard or not RmlUi) then

   return

end -- Prevent this from running more than once RmlGuard = true

-- Patch CreateContext to set dp_ratio automatically 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) local font_files = {

   -- "Fonts/MyFont-Regular.ttf",

} for _, file in ipairs(font_files) do

   RmlUi.LoadFontFace(file, true)

end

-- Map CSS cursor names to engine cursor names -- CSS cursor list: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor RmlUi.SetMouseCursorAlias("default", 'cursornormal') RmlUi.SetMouseCursorAlias("move", 'uimove') RmlUi.SetMouseCursorAlias("pointer", 'Move')

-- Create the shared context used by all widgets RmlUi.CreateContext("shared") </syntaxhighlight>

3.2 Including the setup in main.lua

Add a single line to luaui/main.lua to run the setup before any widgets load:

<syntaxhighlight lang="lua"> -- luaui/main.lua VFS.Include(LUAUI_DIRNAME .. "rml_setup.lua", nil, VFS.ZIP) </syntaxhighlight>

The base-content rml_setup.lua 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.

4. Writing Your First Document (.rml)

Create a file at luaui/widgets/my_widget/my_widget.rml. RML is well-formed XML, so every tag must be closed and attribute values must be quoted.

4.1 Root structure

<syntaxhighlight lang="xml"> <rml> <head>

   <title>My Widget</title>
   <link type="text/rcss" href="my_widget.rcss"/>
   <style>
       body { margin: 0; padding: 0; }
   </style>

</head> <body> </body> </rml> </syntaxhighlight>

4.2 Styles (inline and .rcss)

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.

Use dp units instead of px so your UI scales correctly with the player's display and ui_scale setting.

<syntaxhighlight lang="css"> /* Typical widget container */

  1. my-widget {
   position: absolute;
   width: 400dp;
   right: 10dp;
   top: 50%;
   transform: translateY(-50%);
   pointer-events: auto;   /* required to receive mouse input */

}

  1. my-widget .panel {
   padding: 10dp;
   border-radius: 8dp;
   border: 1dp #6c7086;   /* note: no 'solid' keyword – see section 11 */
   background-color: rgba(30, 30, 46, 200);

} </syntaxhighlight>

4.3 Data bindings in RML

Attach a data model to a root element with data-model="model_name". All data binding attributes inside that element can reference the model.

<syntaxhighlight lang="xml"> <body>

Status: Template:Status message

Extra details go here.

       Active panel
   <input type="checkbox" data-checked="is_enabled"/>
   <button data-click="on_button_click()">Click me</button>

</body> </syntaxhighlight>

Data binding reference:

Binding Effect
{{value}} Interpolate model value as text
data-model="name" Attach named data model to this element and its children
data-if="flag" Show element only when flag is truthy
data-class-X="flag" Add class X when flag is truthy
data-for="v, i: arr" Repeat element for each item in array arr
data-checked="val" Two-way bind checkbox to boolean model value
data-click="fn()" Call model function on click (data event)
data-attr-src="val" Dynamically set the src attribute from model value

5. The Lua Widget

Each widget is a Lua file that the handler loads. The widget global is injected by the widget handler and provides the lifecycle hooks.

5.1 Widget skeleton

<syntaxhighlight lang="lua"> -- luaui/widgets/my_widget/my_widget.lua if not RmlUi then return end -- guard: RmlUi not available

local widget = widget ---@type Widget

function widget:GetInfo()

   return {
       name    = "My Widget",
       desc    = "Demonstrates RmlUI basics.",
       author  = "YourName",
       date    = "2025",
       license = "GNU GPL, v2 or later",
       layer   = 0,
       enabled = true,
   }

end </syntaxhighlight>

5.2 Loading the document

<syntaxhighlight lang="lua"> local RML_FILE = "luaui/widgets/my_widget/my_widget.rml" local MODEL_NAME = "my_model"

local rml_ctx -- the RmlUi Context local dm_handle -- the DataModel handle (MUST be kept – see section 6) local document -- the loaded Document

-- Initial data model state local init_model = {

   status_message = "Ready",
   is_enabled     = false,
   show_details   = false,
   items = {
       { label = "Alpha", value = 1 },
       { label = "Beta",  value = 2 },
   },
   -- Functions can live in the model too (callable from data-events)
   on_button_click = function()
       Spring.Echo("Button was clicked!")
   end,

}

function widget:Initialize()

   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()
   document:Show()

end </syntaxhighlight>

5.3 Shutting down cleanly

<syntaxhighlight lang="lua"> function widget:Shutdown()

   if document then
       document:Close()
       document = nil
   end
   -- Remove the model from the context; frees memory
   rml_ctx:RemoveDataModel(MODEL_NAME)
   dm_handle = nil

end </syntaxhighlight>


6. Data Models in Depth

6.1 Opening a data model

context:OpenDataModel(name, table) copies the structure of the table into a reactive proxy. The name must match the data-model="name" attribute in your RML. It must be unique within the context.

<translate> Warning</translate> <translate> Warning:</translate> You must store the return value of OpenDataModel 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.

6.2 Reading and writing values

For simple string/number/boolean values at the top level of the model you can use dot-notation directly on the handle:

<syntaxhighlight lang="lua"> -- Read a value local msg = dm_handle.status_message -- works

-- Write a value (auto-marks the field dirty; UI updates next frame) dm_handle.status_message = "Unit selected" dm_handle.is_enabled = true </syntaxhighlight>

6.3 Iterating arrays

Because the handle is a sol2 proxy, you cannot iterate it directly with pairs or ipairs. Call dm_handle:__GetTable() to get the underlying Lua table:

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

for i, item in ipairs(tbl.items) do

   Spring.Echo(i, item.label, item.value)
   item.value = item.value + 1   -- modify in place

end </syntaxhighlight>

6.4 Marking dirty

When you modify nested values (e.g. elements inside an array) through __GetTable(), you must tell the model which top-level key changed so that RmlUI re-renders the bound elements:

<syntaxhighlight lang="lua"> -- After modifying tbl.items … dm_handle:__SetDirty("items")

-- For a simple top-level assignment via dm_handle.key = value, -- dirty-marking is done automatically. </syntaxhighlight>


7. Events

Recoil's RmlUI supports two distinct event families. They look similar but have different scopes — mixing them up is a common source of confusion.

7.1 Normal events (on*)

Written as onclick="myFunc()". The function is resolved from the second argument passed to LoadDocument — the widget table. These events do not have access to the data model.

<syntaxhighlight lang="lua"> -- Lua local doc_scope = {

   greet = function(name)
       Spring.Echo("Hello", name)
   end,

} document = rml_ctx:LoadDocument(RML_FILE, doc_scope) </syntaxhighlight>

<syntaxhighlight lang="xml"> <button onclick="greet('World')">Say hello</button> </syntaxhighlight>

7.2 Data events (data-*)

Written as data-click="fn()". The function is resolved from the data model. These events have full access to all model values.

<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">

Count: 108

<button data-click="increment()">+1</button> </syntaxhighlight>

Event type comparison:

Type Syntax example Scope
Normal onclick="fn()" LoadDocument second argument (widget table)
Data data-click="fn()" Data model table
Normal onmouseover="fn()" LoadDocument second argument
Data data-mouseover="fn()" Data model table

8. Custom Recoil Elements

Beyond standard HTML elements, Recoil adds two custom RML elements.

8.1 The <texture> element

Renders any engine texture — unit icons, map thumbnails, GL4 render targets, etc. Behaves identically to <img> except the src attribute takes a Recoil texture reference string (see engine docs for the full format).

<syntaxhighlight lang="xml"> <texture src="unitpics/armcom.png" width="64dp" height="64dp"/>

<texture src="%luaui:myTextureName" width="128dp" height="128dp"/> </syntaxhighlight>

8.2 The <svg> element

Renders an SVG image. Unlike upstream RmlUI, the Recoil implementation accepts either a file path or raw inline SVG data in the src attribute:

<syntaxhighlight lang="xml"> <svg src="images/icon.svg" width="32dp" height="32dp"/>

<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>


9. Debugging

RmlUI ships with a DOM debugger that you can open in-game to inspect elements, check computed styles, and view event logs.

<syntaxhighlight lang="lua"> -- Enable the debugger for the shared context. -- Call this AFTER the context has been created. RmlUi.SetDebugContext('shared')

-- A handy pattern: toggle via a build flag in rml_setup.lua local DEBUG_RMLUI = true -- set false before shipping if DEBUG_RMLUI then

   RmlUi.SetDebugContext('shared')

end </syntaxhighlight>

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

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.

10. Best Practices

Use dp units everywhere
The dp unit scales with context.dp_ratio, so your UI automatically adapts to different resolutions and the player's ui_scale setting.
One shared context per game
Beyond All Reason and most community projects use a single 'shared' context. Documents in the same context can reference the same data models and are Z-ordered relative to each other.
Keep data models flat when possible
Top-level writes via dm_handle.key = value are automatically dirty-marked. Deeply nested structures require manual __SetDirty calls — simpler models mean fewer bugs.
Separate per-document and shared styles
Put styles unique to one document in a <style> block inside the .rml file. Put styles shared across multiple documents in a .rcss file and link it.
Guard against missing RmlUi
Always start widgets with if not RmlUi then return end to prevent errors in environments where RmlUI is unavailable.
Check return values
Both OpenDataModel and LoadDocument can fail silently if paths are wrong. Always check for nil and log an error so you know immediately what happened.
Organise by widget folder
Group luaui/widgets/my_widget/my_widget.lua, .rml, and .rcss together. This makes the project navigable and each widget self-contained.

11. RCSS Gotchas

RCSS closely follows CSS2 but has several important differences that will trip up web developers:

No default stylesheet
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.
RGBA alpha is 0–255
rgba(255, 0, 0, 128) means 50% transparent red. CSS uses 0.0–1.0 for alpha. The opacity property still uses 0.0–1.0.
Border shorthand has no style keyword
border: 1dp #6c7086; works. border: 1dp solid #6c7086; does not work. Only solid borders are supported; border-style is unavailable.
background-* properties use decorators
background-color works normally. background-image, background-size, etc. are replaced by the decorator system. See the RmlUI docs for syntax.
No list styling
list-style-type and friends are not supported. ul/ol/li have no special rendering — treat them as plain div-like elements.
Input text is set by content, not value
For <input type="button"> and <input type="submit"> the displayed text comes from the element's text content, not the value attribute (unlike HTML).
pointer-events
auto required
By default elements do not receive mouse events. Add pointer-events: auto to any element that needs to be interactive.

12. Differences from Upstream RmlUI

The Recoil implementation is based on the official RmlUI library but adds engine-specific features and changes some defaults:

Feature Description
<texture> element A custom element that renders engine-managed textures via Recoil texture reference strings. Not present in upstream.
<svg> inline data The src 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 .rml and .rcss files work inside .sdz game archives transparently.
Lua bindings via sol2 The Lua API is provided by RmlSolLua (derived from LoneBoco/RmlSolLua) rather than the upstream Lua plugin, offering tighter engine integration.
dp_ratio via context context.dp_ratio is set from code rather than the system DPI, allowing games to honour the player's ui_scale preference.
For everything not listed above, the upstream RmlUI documentation at mikke89.github.io/RmlUiDoc applies directly.

13. IDE Setup

Configuring your editor for .rml and .rcss file extensions dramatically improves the authoring experience.

VS Code

  1. Open any .rml file in VS Code.
  2. Click the language mode indicator in the status bar (usually shows Plain Text).
  3. Choose Configure File Association for '.rml'
  4. Select HTML from the list.
  5. Repeat for .rcss files, selecting CSS.

Lua Language Server (lua-ls)

Add the Recoil type annotations to your workspace for autocomplete on RmlUi, Spring, and all widget APIs. See the Recoil docs guide on the Lua Language Server for details.


14. Quick Reference

Lua API

Call Description
RmlUi.CreateContext(name) Create a new rendering context
RmlUi.GetContext(name) Retrieve an existing context by name
RmlUi.LoadFontFace(path, fallback) Register a .ttf font (fallback=true recommended)
RmlUi.SetMouseCursorAlias(css, engine) Map a CSS cursor name to an engine cursor name
RmlUi.SetDebugContext(name) Attach the DOM debugger to a named context
ctx:OpenDataModel(name, tbl) Create reactive data model from Lua table; store the return value!
ctx:RemoveDataModel(name) Destroy a data model and free its memory
ctx:LoadDocument(path, scope) Parse an RML file; scope is used for normal (on*) events
doc:Show() Make the document visible
doc:Hide() Hide the document (keeps it in memory)
doc:Close() Destroy the document
doc:ReloadStyleSheet() Reload linked .rcss files (useful during development)
dm:__GetTable() Get the underlying Lua table for iteration
dm:__SetDirty(key) Mark a top-level model key as changed; triggers re-render
dm.key = value Write a top-level value; auto-marks dirty