AI Primer
A single, dense block meant to be pasted into another LLMβs context (a chat, a system prompt, a project memory) to quickly ground it in this gameβs modding surface β what exists, how the pieces fit, concrete code shapes to reuse, and where to send you back to this wiki for anything more specific than βsurface level.β It is deliberately compressed; it is not a replacement for the rest of this site.
MERCENARIES 2 MODDING PRIMER β for AI assistants. Full wiki: https://wiki.mercs2.tools (this is a
compressed map of it, not a replacement β when a task needs more than this covers, ask the user to paste
the specific page named below rather than guessing).
RULES β READ FIRST
- ALWAYS use Loader.Printf(sMsg) for your own debug output. NEVER Debug.Printf for this purpose β that's
the engine's own internal log, called thousands of times a second by stock game scripts; anything you
print through it is buried in unreadable noise. Loader.Printf writes only to lua_loader_printf.log,
next to the game exe β nothing else lands there, so it's actually readable.
- Wrap any engine call that could plausibly fail (stale uGuid, despawned object) in pcall β see below.
- import("ModuleName") is FILE-SCOPED. It does not leak to other files, other scripts, or console chunks
β every file that calls into a resident module needs its own import() line, or you get
`attempt to index global 'X' (a nil value)`.
- Engine namespaces (Object, Event, Player, Vehicle, ...) are always global β never import() these.
- Match the casing already used throughout this primer for engine/module calls: PascalCase.PascalCase
(Object.GetPosition, MrxPmc.AddCashQty) β never lowercase these (object.getPosition is wrong and will
fail). Locals use Hungarian-ish prefixes: bOn, nCount, sName, tArgs, uGuid.
WHAT THIS IS
Mercenaries 2: World in Flames (PC). A statically-linked Lua 5.1 VM drives world objects, missions, GUI,
AI β no official modding API ever existed. lua-bridge (an ASI plugin loaded via the pmc_bb.dll Fan Build
loader) injects a live console + script loader into that VM. Everything below is reverse-engineered from
decompiled source + live testing, not official documentation β treat "confirmed"/"verified" language as
meaningful (someone actually checked), its absence as "probably right, not yet double-checked."
RUNNING CODE β Console (interactive) plus 3 script-file hooks. Full detail: /getting-started, /first-mod,
/first-menu
- RECOMMENDED for rapid testing: scripts/OnKey/*.lua β runs once per keypress, edge-triggered, and is
RE-READ FROM DISK ON EVERY PRESS: edit the file, save, press the key again in-game, see the result
immediately β no restart, no separate reload step, nothing to run except a text editor and the game
itself. This is the most beginner-friendly loop on offer, especially for anyone still getting
comfortable with the tooling. Bind via `local KEYVAL = "keyname"` in the first 10 lines, or
lua_loader.ini's [OnKey] section. A per-script reentrancy cooldown (~250ms) throttles rapid
double-presses of the same key so a stuck/bouncing key can't queue up two overlapping runs.
- Console: tools/lua_console.py, TCP 127.0.0.1:27050, send a chunk then `<<<RUN>>>` β good for quick
one-off queries/inspection (read a value, test a single call), but it's a separate tool with its own
wire protocol to learn, a bigger step for a newcomer than just editing a file and pressing a key.
- scripts/OnBoot/*.lua β runs once, earliest possible (bridge captures Lua state).
- scripts/OnLoad/*.lua β runs once per level load (GlobalExit-Complete milestone).
- A browser-based Lua IDE (ide.mercs2.tools) and a live player-position map (map.mercs2.tools) also exist,
both connecting live to a running game over lua-bridge's WebSocket transport β useful for a human
collaborator to test/inspect state visually. Not something your own generated code needs to call into.
Minimal OnKey skeleton:
local KEYVAL = "insert" -- must be in the first 10 lines
Loader.Printf("hello from OnKey")
MODULE SYSTEM β no `require`. Full detail: /resident/, /namespaces/, /glossary
- Every src/resident/*.lua file is one global module table, named after its filename (crate.lua -> Crate).
- `inherit("Name")` = prototype-inherit via setmetatable __index chain (self falls back to parent table).
- `import("Name")` = pull a resident module in as a callable namespace (file-scoped β see RULES above).
- Engine namespaces (Object, Event, Player, Vehicle, Ai, Marker, Sound, Human, Camera, Airstrike, Weapon,
Sys, Net, Gui, Hud, Controller, Junk, Pg, Graphics) are ALWAYS global, need no import, have no `.lua`
file behind them (can't read their source, only observed behavior).
- Per-instance pattern: `Inheritable.Create(oPrototype, uGuid, ...)` -> setmetatable + a tInstance[uGuid]
registry. NOT universal β many resident modules are stateless singletons using bare module-level
globals instead. Check for OnActivate/Awake/Create/tInstance/setmetatable before assuming either way.
- `uGuid` = opaque runtime object handle (a spawned vehicle, a character, ...), not stable across game
sessions, the first argument to most Object.*/Vehicle.* calls.
- 228 resident-module reference pages, categorized: Vehicles, Support & Airstrikes, Missions & Tasks,
GUI & HUD, World Objects & Props, Audio & Music, Core Engine & Utilities, Cheats & Dev Tools.
- 19 engine namespace pages: Ai, Airstrike, Camera, Controller, Event, Graphics, Gui, Hud, Human, Junk,
Marker, Net, Object, Pg, Player, Sound, Sys, Vehicle, Weapon.
QUICK FUNCTION REFERENCE β the ones nearly every script touches; full signatures/confirmation status on
each namespace's own page under /namespaces/<name> or /resident/<module>
Object.GetPosition(uGuid) -> x, y, z
Object.SetPosition(uGuid, x, y, z)
Object.GetYaw(uGuid) -> n Object.SetYaw(uGuid, n)
(unit/axis convention unconfirmed β some scripts assume degrees, not verified against source)
Object.IsAlive(uGuid) -> bool
Object.HasLabel(uGuid, sLabel) -> bool
Object.SetInvincible(uGuid, bOn, sReasonTag)
Object.SetInfiniteAmmo(uGuid, bOn)
Object.GetMass(uGuid) -> n Object.ApplyImpulse(uGuid, x, y, z, bLocalSpace)
Object.Remove(uGuid)
Player.GetLocalCharacter() -> uGuid (your own character, single-player-safe)
Player.GetPrimaryCharacter() / GetSecondaryCharacter() -> uGuid (co-op player 1 / player 2)
Player.GetLocalPlayer() -> uPlayerGuid (player-SLOT guid, distinct from character)
Pg.Spawn(sTemplateName, x, y, z, ...) -> uGuid e.g. Pg.Spawn("Veyron", x, y, z) β confirmed working;
see /hash-lookup for the full real-template-name list
Pg.GetGuidByName(sObjectName) -> uGuid (look up a placed/named object)
Vehicle.GetFromRider(uCharGuid) -> uVehicleGuid Vehicle.GetDriver(uVehicleGuid) -> uGuid
Event.Create(EventType, tArgs, fCallback, tCallbackArgs) -> handle Event.Delete(handle)
import("MrxPmc"); MrxPmc.AddCashQty(n) / AddFuelQty(n) (HUD-updating economy calls β plain
Player.SetCash/AddCash change the value but skip the HUD refresh)
Loader.Printf(sMsg) Loader.IsKeyDown(vk) -> bool
Loader.SaveVar(sKey, xValue) Loader.LoadVar(sKey) -> x | nil
LUA-BRIDGE ADDITIONS (not part of the game itself). Full detail: /lua-bridge-api/loader,
/lua-bridge-api/stdlib
- Loader.Printf(msg) β see RULES above.
- Loader.IsKeyDown(vk) / GetKeyboardState() / PopKeyEvents() / ClearKeyEvents() / IsGameFocused() β the
only general-purpose keyboard input (the game's own Lua surface has none). IsKeyDown/GetKeyboardState
for continuous/movement input; PopKeyEvents (edge-triggered ring buffer, focus-gated) for typed text β
NOT interchangeable, PopKeyEvents has no "still held" signal. All three take a numeric Windows VK code
β a DIFFERENT scheme from OnKey's `KEYVAL = "keyname"` string binding above; a keyname and a VK code are
not interchangeable with each other either. Measured sub-microsecond per call β safe to call thousands
of times in a single frame, no need to hand-roll throttling for these specifically.
- Loader.SaveVar(sKey, xValue) / Loader.LoadVar(sKey) β key-value persistence across game restarts
(numbers/strings/booleans, type preserved on read-back). Stored in lua_loader_data.ini next to the
.asi, human-readable/hand-editable. LoadVar returns nil for a key never saved β standard idiom:
`local n = Loader.LoadVar("MyMod_progress") or 0`. Flat namespace shared by every script β prefix keys
with your own script's name (MyMod_progress, not progress) to avoid colliding with another mod's data.
- Tcp.Send(host, port, msg) β fire-and-forget, localhost-only by design.
- Loader.WsSend(msg) β broadcasts to any connected browser client over a WebSocket transport sharing the
console's own port, on a channel separate from the log file; safe no-op with zero clients connected.
Full detail: /lua-bridge-api/websocket
- Full math.* stdlib (sin, cos, tan, asin/acos/atan/atan2, sinh/cosh/tanh, sqrt, log, log10, fmod, ldexp,
modf, frexp, random, randomseed, pi, huge) plus assert(v, msg) β polyfills, additive on top of the
engine's own math.floor/abs/max/min/etc. assert's error correctly points at whatever called it, not at
internal polyfill code. Same sub-microsecond hot-loop-safe cost as the input functions above. Older
scripts on this wiki hand-roll a Taylor-series sin/cos fallback from before these existed β no longer
necessary, harmless if left as-is.
- lua_Number is float, not double β precision-sensitive math can surprise you.
- Only math/assert were probed for completeness β os/io/coroutine/debug tables' presence isn't
individually confirmed either way on this wiki. Check `type(os) == "table"` (etc.) before relying on
something that "should" be there; don't assume stock-Lua-5.1 completeness beyond what's listed here.
ESSENTIALS (Ess) -- mod-authored framework, the recommended starting point for any new mod (not part of
the game or lua-bridge itself). Full detail: /ess/
- Deploy: 1_Ess.lua -> scripts/OnLoad/ with a low lua_loader.ini number (e.g. =5); also drop
data/vz-patch.wad into data/ if you'll use Ess.UI (skippable otherwise). Guard consumers:
`if not _G.Ess then Loader.Printf("load Ess first") return end`.
- Three tiers, reach for the highest that fits: `Ess.Easy.*` (intent-named one-liners) -> `Ess.*` (Core:
named params/sensible defaults) -> `Ess.Raw.*` (the primitives underneath, for composing something Ess
didn't anticipate). Not every namespace carries all three. `Ess.Easy.Console.open()` browses the whole
Easy.* surface in-game, searchable.
Ess.Easy.Vehicle.summon("UH1 Transport")
Ess.Easy.Spawn.explosion() / .crate() / .weapon() / .airstrike()
- Namespace groups: Core (Log/Safe/Table/Str/Color/Vec/Math/Guid/Name/State/SaveVar/RNG), Identity & World
Query (Player/Object/Vehicle/Probe/Human/Impulse), Timing & Input (Time/Loop/Input/TextConsole), Tracking
(Track/Event/Save), Mark (radar/PDA/ring/icon), UI (Menu/List/Panel/Bar/Toast/Confirm/Input/Chat/Board),
Camera/Bones/Points, Sound/Hud, Encounter Toolkit (AIOrders/Relations/Triggers/Sandbox/Layers), Cinematic,
Net, Contract, Override (safe function replacement, no tail-call crash), Support (combat call-ins), On
(reactive world hooks: death/area/health/hurt/vehicle/tick), Keys (multi-hotkey panel), Objective/Quest,
Easy.Debug (dev overlay).
- Absorbs four previously-standalone mod-authored frameworks as native code: uilib.lua -> Ess.UI,
ModNet.lua -> Ess.Net, ContractFramework.lua -> Ess.Contract, LayerFw.lua -> Ess.Layers. The first three
still exist as standalone .lua files and still work (see /deprecated-frameworks/) if you encounter
existing code using `_G.UI`/`_G.ModNet`/`_G.Contract` directly; Layer Framework was folded in without ever
getting its own standalone wiki page. New work should start on Ess instead β the two APIs are kept
close, e.g. `Ess.UI.Menu` is byte-for-byte compatible with the old `UI.Menu` (one caveat: `Menu` inherits
`Ess.UI.List`'s new wraparound cursor, so Down past the last entry now jumps to the top β `UI.Menu` never
did that):
local menu = Ess.UI.Menu{ title = "MY MENU", key = "F8" }
menu:entry("Do a thing", function(ctx) ctx:hint("done") end)
menu:category("Group", function(c) c:entry("Nested", function(ctx) ... end) end) -- nests freely
menu:toggle() -- put at the end of your OnKey file
CODE SAMPLES β reusable shapes, copy the pattern not necessarily the exact values
State that survives OnKey/OnLoad re-execution (_G is the only thing that persists between separate runs
of the same script within a session β plain `local`s don't):
_G.MyState = _G.MyState or {bOn = false}
local State = _G.MyState
State.bOn = not State.bOn
Safe engine call (never lets one bad uGuid kill the rest of the script):
local bOk, result = pcall(Object.SetInvincible, Player.GetLocalCharacter(), true, "mymod")
if not bOk then
Loader.Printf("SetInvincible failed: " .. tostring(result))
end
A menu (auto-paginates past ~8 options):
import("MrxMultiPageMenu")
MrxMultiPageMenu.Reset()
MrxMultiPageMenu.AddOption("Say hello", function() Loader.Printf("hi!") end)
MrxMultiPageMenu.AddOption("Close this menu", nil, nil, true, true) -- nil callback ONLY safe here,
-- bound to the cancel button
MrxMultiPageMenu.Display("Test Menu:")
Overriding existing game logic (resolves at CALL time, not definition time β this changes behavior for
every future call from anywhere, including from inside the original module's own other functions):
import("SomeModule")
SomeModule.SomeFunction = function(...)
-- your replacement body
end
COMMON GOTCHAS
- `X and A or B` is Lua's ternary-operator substitute β short-circuits to B if A itself is falsy, which
is the one real gotcha (only safe when A can never be false/nil). Same underlying "or returns the first
truthy value" behavior is what makes `_G.MyState = _G.MyState or {defaults}` work in CODE SAMPLES below.
- `pairs(t)` visits every key, any order; `ipairs(t)` only a plain 1..n array, in order, stopping at the
first gap β mixing them up silently drops data instead of erroring.
- Functions can return multiple values at once: `local a, b, c = f()`.
- No native free-text input widget exists β hand-roll it via Loader.PopKeyEvents() + a VK-code-to-
character table (see /snippets, /sample-scripts-onkey's CommonSpawnMenu.lua).
- This is Lua 5.1 specifically β no `+=`/`-=` compound assignment (no Lua version has ever had these), no
`//` floor division or bitwise operators (`&`, `|`, `<<`, ...) β both added in Lua 5.3, well after this
engine's runtime. Use `x = x + 1`, `math.floor(a / b)`, and don't reach for bit ops at all.
- An uncaught error in an OnKey/OnLoad/OnBoot script silently ends that run early β nothing gets printed
anywhere, unlike Console's automatic `[runtime]` report. Wrapping risky calls in `pcall` and
`Loader.Printf`-ing the error yourself (see CODE SAMPLES) is currently the only way to see what failed.
KNOWN HARD LIMITS β flag uncertainty rather than proposing confident workarounds for these
- No confirmed Lua touchpoint for firing a turret, or for a vehicle's camera while driving/gunning β
extensively tested (multiple recipe matrices, hardpoint probing, camera-lock recipes), all native-only.
See /namespaces/vehicle, /deep-dives/destroyer-vehicle.
- Object.ApplyImpulse/ApplyPointImpulse confirmed to affect vehicles/physics props; confirmed to NOT
affect a standing player character (tested live, no effect).
- Multi-player teleport helpers can crash the game if used while inside an interior cell β outdoor-only.
- Physics on some set-dressing "vehicle" templates can't be fixed from Lua alone β needs external
WAD-level editing tools that don't exist yet for this game.
IF YOU NEED MORE THAN THIS
This primer is intentionally shallow. For a specific module's full function list, exact call signatures,
event names, or a deep dive's full investigation, ask the user to paste the relevant page rather than
guessing β do NOT invent a URL path yourself; name the module/namespace/topic and let them find the page.
Good default ask: "what does wiki.mercs2.tools say about <ModuleName/Namespace>?"
Testing notes
Cold-tested against 10 different LLMs (this primer pasted in fresh, no other context, then given the same battery of questions/code requests). The restraint and escalation instructions β KNOWN HARD LIMITS, and βask rather than guessβ for anything not covered β held up broadly, including on smaller local models, so that part of the design seems to be working as intended. The one recurring weak spot: a bare namespace/module name listed with no function detail next to it (e.g. the 19-namespace list under MODULE SYSTEM) is an inviting surface for a model to confidently invent a plausible-sounding function that was never actually stated anywhere in this primer. Treat any function name a pasted-in LLM produces that isnβt in this primerβs own QUICK FUNCTION REFERENCE as unconfirmed until checked against the real wiki page.
Of the local/self-hosted options tested, Qwen2.5-coder-14b-instruct performed capably enough to be a viable choice if you want this running locally rather than against a cloud model.