If RimBridgeServer adds a human-friendly scripting language, the best fit is:
- Lua syntax
- MoonSharp as the embedded interpreter or parser layer
- the existing script runner as the execution backend
The important constraint is architectural: Lua should not become a second direct automation runtime that talks to RimWorld on its own. It should lower into, or delegate through, the same registry-backed execution/reporting path already used by rimbridge/run_script.
As of 2026-03-17, the core Lua front-end and its discoverability slices are implemented. rimbridge/run_lua, rimbridge/compile_lua, rimbridge/run_lua_file, and rimbridge/compile_lua_file now compile a narrow Lua v1 subset into the shared script runner instead of introducing a second automation runtime. rimbridge/get_lua_reference exposes that supported subset, including the read-only params binding and file-backed execution, in machine-readable form for fresh agents. The remaining work is around richer reusable examples and broader scenario self-planning, not around the core Lua execution path itself.
Lua is small, readable, and good at the kind of glue logic these scripts need:
- local variables
- tables
- arithmetic
- loops
- conditionals
- small helper abstractions
For this repo, MoonSharp is the right Lua implementation shape because it is pure C# and does not require native runtime packaging. That matters for a RimWorld mod targeting net472 and running inside Unity/Mono.
Rejected alternatives:
- NLua/KeraLua: pulls in native runtime complexity and cross-platform packaging risk that this mod does not need
- Jint: technically viable and actively maintained, but JavaScript is less compact for this style of game automation and is not the smallest language that solves the problem
- Move from JSON-only ordered scripts to a small readable language suitable for real scenario logic.
- Preserve the current capability model so every registered capability remains scriptable automatically.
- Preserve the current reporting model: per-step operation ids, timings, success/failure, warnings, and optional results.
- Support the minimum missing control flow needed for dynamic scenarios such as:
- starting from a connected RimWorld session
- resetting to main menu
- starting a fresh debug colony
- choosing cells dynamically
- iterating over colonists or wall segments
- waiting until a generic condition becomes true
- Host-level process control inside Lua.
games.startandgames.connectremain outside the in-game capability registry. - Arbitrary CLR access from Lua.
- File I/O, OS access, networking, module loading, or debug-library access from Lua.
- Replacing the existing JSON script format.
- Replacing the current
CapabilityScriptRunnerwith a separate scripting engine.
The current backend is already the right execution core:
It already provides the hard operational guarantees we care about:
- execution through the shared capability registry
- uniform child operation metadata
- per-step success/failure reporting
- value passing with
$ref - bounded polling with
continueUntil - predictable halt-on-failure behavior
Lua should be introduced as a front-end over that backend, not as a new automation stack.
Recommended layering:
Lua source
-> Lua front-end
-> extended script AST / lowered script model
-> CapabilityScriptRunner
-> CapabilityRegistry
-> existing capability implementations
This keeps one execution model and one report model.
Add a sibling tool rather than overloading rimbridge/run_script:
rimbridge/run_luarimbridge/compile_luarimbridge/run_lua_filerimbridge/compile_lua_file
Suggested first signature:
rimbridge/run_lua(
luaSource: string,
parameters?: object,
includeStepResults: bool = true
)
File-backed runtime/debug tools:
rimbridge/run_lua_file(
scriptPath: string,
parameters?: object,
includeStepResults: bool = true
)
rimbridge/compile_lua(
luaSource: string,
parameters?: object
)
rimbridge/compile_lua_file(
scriptPath: string,
parameters?: object
)
The parameters object is injected into Lua as a top-level read-only params table. compile_lua and compile_lua_file are useful for debugging lowering errors and for verifying that Lua remains a front-end over the shared script model.
The current JSON runner is intentionally step-oriented. To support meaningful Lua control flow cleanly, the internal script model needs a small control-flow expansion.
Recommended statement kinds:
callblockletifforeachwhile
Recommended expression kinds:
- literal values
- variable lookup
- prior-step reference
- object/table construction
- array construction
- property access
- index access
- unary operators such as
notand unary minus - binary operators:
- arithmetic
- comparison
- boolean
and/or
Only capability calls should produce ordinary step reports.
Control statements should not flood the existing report format. If extra visibility is needed later, add a lightweight trace channel, but keep the main report focused on concrete capability executions.
Every loop path must remain bounded. Recommended guards:
- maximum lowered statement count
- maximum loop iteration count
- maximum script wall-clock duration
- maximum nested control depth
Lua should make scripts easier to write, not make it possible to hang RimWorld with an unbounded loop.
The first Lua slice should be intentionally narrow.
localvariables- table literals
- field and index access
- arithmetic and comparisons
- boolean operators
if/elseif/else- numeric
for - array iteration via
ipairs while- calls to a narrow host API under a single namespace such as
rb
require- metatables
- coroutines
- user-provided global mutation outside the script scope
- direct CLR interop
io,os,package, anddebuglibraries- arbitrary library import
This is still a real language, just one constrained to automation needs.
The host API exposed to Lua should stay narrow and explicit. A single namespace is preferable:
rb.call("rimworld/go_to_main_menu")
rb.call("rimworld/start_debug_game")
status = rb.call("rimbridge/get_bridge_status")
rb.poll("rimbridge/get_bridge_status", {}, {
timeoutMs = 30000,
pollIntervalMs = 100,
condition = {
all = {
{ path = "result.state.inEntryScene", equals = true },
{ path = "result.state.programState", equals = "Entry" }
}
}
})Initial host helpers should stay close to the current runner semantics:
rb.call(alias, args?)rb.poll(alias, args?, policy)rb.ref(stepId, path?)only if needed after AST support is extended
Important constraint:
- Lua must never receive direct RimWorld objects
- Lua only sees plain projected values and plain dictionaries/lists
This is the kind of script the system should support after the first Lua slice:
rb.call("rimworld/go_to_main_menu")
rb.poll("rimbridge/get_bridge_status", {}, {
timeoutMs = 30000,
pollIntervalMs = 100,
condition = {
all = {
{ path = "result.state.inEntryScene", equals = true },
{ path = "result.state.programState", equals = "Entry" },
{ path = "result.state.hasCurrentGame", equals = false },
{ path = "result.state.longEventPending", equals = false }
}
}
})
rb.call("rimworld/start_debug_game")
rb.call("rimbridge/wait_for_game_loaded", {
timeoutMs = 60000,
pollIntervalMs = 100,
waitForScreenFade = true,
pauseIfNeeded = true
})
local colonists = rb.call("rimworld/list_colonists", { currentMapOnly = true })
for i, colonist in ipairs(colonists.result.colonists) do
rb.call("rimworld/select_pawn", {
pawnName = colonist.name,
append = i > 1
})
endThis example is intentionally close to the current JSON semantics. Lua adds readability and normal control flow; it should not invent a separate automation model.
Refactor the current runner so the per-call execution/reporting path can be reused by richer script forms.
Deliverables:
- extract the call-execution/reporting logic out of the current monolithic loop in
CapabilityScriptRunner.cs - preserve current JSON behavior exactly
- add tests proving no regression in
run_script
Add the smallest control-flow model needed for dynamic scripts.
Deliverables:
- new statement and expression contracts in
RimBridgeServer.Contracts - runner support for
let,if,foreach, and boundedwhile - tests for variable scope, loop bounds, and report shape
This slice should land before Lua. It is useful on its own and makes the execution model explicit.
Introduce MoonSharp and build a narrow front-end that lowers supported Lua into the extended script model.
Deliverables:
LuaScriptCompiler- syntax/lowering diagnostics
- compile-only tests
- no new direct capability execution path
Status:
- completed on 2026-03-17
Once lowering is stable, expose the public tool.
Deliverables:
rimbridge/run_luarimbridge/compile_lua- README examples
- safety limits on script size and complexity
Status:
- completed on 2026-03-17
Use the user-provided prison flow as the first serious smoke case.
Success criteria:
- one Lua script starts from a connected RimWorld session
- it resets to main menu
- starts a fresh debug colony
- drafts and groups colonists
- builds the enclosing wall
- undrafts them
- captures a screenshot
- returns the same high-quality report shape as
run_script
If Lua grows its own execution semantics instead of lowering into the shared backend, the project will split into two automation systems. That should be treated as a failure mode.
Any accidental CLR exposure or broad standard-library enablement turns Lua from “small helper language” into “arbitrary code execution inside the game process”.
The language must be pleasant, but it cannot be allowed to hang the game. Limits are part of the design, not a later hardening pass.
If every control node becomes a report row, script output will become noisy and harder to consume. Preserve step reports for capability calls.
Recommended path:
- keep the current JSON runner as the execution core
- extend the internal script model with minimal control flow
- add MoonSharp-based Lua as a front-end over that model
- expose
rimbridge/run_lua - prove it with the prison scenario
This is the smallest path that gives real scripting power without discarding the work already done on rimbridge/run_script.