Target Platform:
gamemd.exe— Yuri's Revenge 1.001
API Version:2.0.0— Milestone 11
Purpose: Proven mechanics, real-world case studies, reusable Lua recipes, and engineering lessons learned while developing LuaAPI.
This document describes functionality that has been implemented and verified in LuaAPI.
It focuses on practical examples rather than API definitions:
- What can LuaAPI currently do?
- How was each mechanic implemented?
- What problems were encountered?
- What architectural decisions solved them?
- How can modders reproduce the same functionality?
⚠️ Important: Only mechanics explicitly marked as VERIFIED should be treated as proven capabilities of the current implementation.Evidence authority (2026-09-20): per-claim evidence grades live in
FSM/VERIFICATION.md; the project gate model and pass/fail criteria live inPROJECT/GATES.md. Where a case study below and the ledger disagree, the ledger wins.
- Case Study 1 — Sub-Frame Reactive Energy Shields
- Case Study 2 — Dynamic Bounties & Economy
- Case Study 3 — Battle-Damaged Starting Fleet
- Case Study 4 — Multi-Turret Batteries
- Case Study 5 — CnCNet Multiplayer & Dev Tools
- Case Study 6 — Lua Gameplay Framework (Milestone 14)
- Universal Engineering Principles
- Verification Policy
- Related Documentation
Status:
Reference implementation: shield_overload (archived; uses a
registration call that has no binding and a table-method callback the
engine never dispatches)
If OnPreDamage were wired, LuaAPI could intercept incoming damage before
the engine applies it to the target.
This makes it possible to implement:
- Energy shields
- Damage reduction
- Armor systems
- Directional mitigation
- Damage filters
- Custom defensive mechanics
Polling damage from Update() is too late.
By the time a regular frame callback runs, the engine may have already resolved the damage.
OnPreDamage operates at the damage-processing boundary instead.
The intended flow is:
Engine damage event
↓
OnPreDamage
↓
Lua
↓
┌──────┼────────┐
↓ ↓ ↓
nil number 0
↓ ↓ ↓
Pass Modify Cancel
damage damage damage
-- Intended form (NOT live: the engine never invokes this today).
-- Must be GLOBAL: table-method callbacks are never dispatched.
function OnPreDamage(attacker, target, damage, dmgType, frame, subc)
if dmgType == "energy" or dmgType == "explosive" then
return damage * 0.5
end
return nil
endThe engine collects the global reference each frame but has no
ReceiveDamagehook, so live damage never reaches Lua in this build. Keep this recipe as the intended contract, not a working mechanic.
- Never return negative damage values.
- Avoid triggering additional damage from inside
OnPreDamagewithout a re-entrancy guard. - Always validate
attackerandtargetbefore accessing their methods.
Status: bounty_hunter mod is inert
(it defines a table-method OnPreDamage the engine never dispatches,
and its Update no-ops). Engine-credit writes themselves
(AddCredits) are implemented; the event-driven bounty loop below is
aspirational until damage events are wired.
Reference implementation: bounty_hunter (see note)
LuaAPI can implement dynamic credit rewards based on observed combat
(aftermath polling, Command Authority pattern). Engine-credit writes
(AddCredits) are implemented.
This enables:
- Kill rewards
- Combat bounties
- Economy modifiers
- Dynamic income systems
- HUD notifications
The general flow is:
- Receive a combat callback.
- Identify the attacker's house.
- Modify the house's credits.
- Display a message through the HUD.
-- Intended form (NOT live: damage events are not wired; also note the
-- current PrintMessage takes text only, no color argument).
function OnPreDamage(attacker, target, damage, dmgType, frame, subc)
if attacker and attacker.GetOwner then
local ownerHouse = attacker:GetOwner()
local player = House.GetPlayer()
if ownerHouse and player and ownerHouse == player then
ownerHouse:AddCredits(50)
Engine.PrintMessage("[Bounty] +$50 combat reward")
end
end
return nil
endCombat callbacks may receive a nil attacker.
This can happen when damage or destruction originates from:
- Environmental effects
- Triggers
- Crushing
- Other engine-side causes
Always validate the attacker before accessing its owner.
Status:
- it uses the table-method
OnScenarioStart, and the current engine dispatches only the globalOnScenarioStart(seeAPI.mdCallback Model) — convert to a global before reuse; - it calls
unit:SetHealthRatio(0.35)— the current binding takes a percent scale (API.md: pass35, not0.35; fractional inputs truncate toward ~0%); - the reference mod lives in
scripts/mods_archive/damaged_fleet/, not in the active tree. The health/visual modifications themselves remain proven mechanics; the callback wiring and the scale in the recipe are what went stale.
Reference implementation:damaged_fleet(archived)
LuaAPI can modify units immediately after scenario initialization, allowing mods to create pre-damaged starting forces.
Example uses:
- Battle-damaged fleets
- Damaged starting armies
- Custom scenario states
- Visual damage effects
OnScenarioStart() runs after scenario initialization.
The mod can then:
- Query units.
- Filter them by owner and type.
- Modify their health.
- Attach visual effects.
-- scripts/mods_archive/damaged_fleet/main.lua (archived; global form required)
function OnScenarioStart() -- GLOBAL: a MyMod.OnScenarioStart method never fires
local player = House.GetPlayer()
if not player then
return
end
for _, unit in ipairs(World.GetUnits()) do
if unit:IsAlive()
and unit:GetOwner() == player
and unit:GetTypeName() == "DEST" then
unit:SetHealthRatio(35) -- percent scale (API.md); NOT 0.35
unit:AttachParticleSystem("DamageSmokeSys")
end
end
endDo not access scenario-dependent game objects before the scenario has finished initializing.
Use OnScenarioStart() for post-load initialization.
Savegame note:
OnScenarioStart()does not fire when loading a savegame. Systems that require persistent runtime state must account for the savegame lifecycle separately.
Status: 🔴 REMOVED — SubTurretManager (src/sub_turret.*), its
bindings, the spawned-missile decoupling, BulletHook, EventHook, and
FireProjectile were deleted (zero live consumers; per-frame sweep + hook
overhead). The record below stands as history: bindings WERE verified in
their time, the showcase mod was already absent from the tree.
Reference implementation: multi_turret_battleship (absent; see
FSM/MODDB_ALPHA_RELEASE.md Small 3)
LuaAPI can extend units with additional turret systems and split targeting.
This enables:
- Multi-turret ships
- Multiple simultaneous targets
- Independent turret facing
- Split-salvo attacks
- Custom capital-ship weapon systems
The multi-turret system consists of a C++ state layer and Lua-controlled gameplay logic.
The C++ side manages:
- Turret state
- Target references
- Reload timers
- Turret offsets
- Safe pointer handling
Lua controls:
- Target selection
- Target allocation
- When a salvo is fired
C++ manages engine state. Lua decides gameplay behavior.
The C++ layer should not autonomously decide when units attack.
Target acquisition and firing inside the C++ tick caused unintended attacks while units were idle or moving.
The solution was to move firing decisions into explicit Lua calls.
Never erase entries from a container while iterating over it.
Use deferred cleanup instead.
Target references must be invalidated when their underlying game objects are destroyed.
OnScenarioStart() does not run after loading a savegame.
Systems that attach runtime state should detect and restore missing state during Update().
Red Alert 2 uses 256 leptons per cell.
Squared distance calculations can overflow 32-bit integers.
Use 64-bit arithmetic for large coordinate differences.
-- (was scripts/mods/multi_turret_battleship/main.lua — absent from the tree)
function OnScenarioStart() -- GLOBAL: a mod-table method never fires
local player = House.GetPlayer()
if not player then
return
end
for _, unit in ipairs(World.GetUnits()) do
if unit:IsAlive() and unit:GetOwner() == player then
local typeName = unit:GetTypeName()
if typeName == "DRED" or typeName == "DEST" then
unit:AddSubTurret(1, 40, 0, 15, 12, 90)
unit:AddSubTurret(2, -40, 0, 15, 12, 90)
end
end
end
end
local MultiTurretMod = {}
function MultiTurretMod.Update(frame)
if frame % 30 ~= 0 then
return
end
local player = House.GetPlayer()
if not player then
return
end
for _, unit in ipairs(World.GetUnits()) do
if unit:IsAlive()
and unit:GetOwner() == player
and unit:GetTypeName() == "DRED" then
local pos = unit:GetPosition()
local targets = {}
for _, enemy in ipairs(
World.GetUnitsInRadius(pos.x, pos.y, 15)
) do
if enemy:IsAlive() and enemy:GetOwner() ~= player then
table.insert(targets, enemy)
end
end
if #targets > 0 then
unit:SetSplitTargets(targets)
unit:FireSplitSalvo()
end
end
end
end
return MultiTurretModStatus: 🟡 PRIMITIVES VERIFIED, BUNDLE NOT RE-RUN —
CnCNet attach, SpawnUnit, GetSelectedUnits each have live sessions on
record (Gate 11/12.2/CA); the OnDebugCommand console path is source-wired
(CallDebugCommand, pcall) with no live consumer on record; both reference
implementations are archived/absent (see above). Re-run before showcasing
as a bundle.
Reference implementations: spawn_test (scripts/mods_archive/), debug
console (global OnDebugCommand; the debug_console mod file is not in the
tree — the recipe below is the contract)
LuaAPI supports CnCNet environments and provides development-oriented tooling for rapid mod testing.
This includes:
- CnCNet process attachment
- Compatibility with existing hooks
- Logical-frame-based callbacks
- Runtime unit spawning
- Debug commands
CnCNet can launch gamemd-spawn.exe rather than the standard gamemd.exe.
The injector therefore resolves the actual game module from the running process instead of assuming a fixed executable name.
LuaAPI may encounter an existing hook at the game's main loop.
A signature mismatch does not necessarily mean that injection failed.
When another modification such as Ares or Phobos has already hooked the function, LuaAPI can chain its hook through MinHook.
Gameplay callbacks should be synchronized to the game's logical frame rather than the render rate.
Conceptually:
Render frames
↓
┌────┴────┐
↓ ↓ ↓
120 90 60 FPS
└────┬────┘
↓
Logical game frame
↓
LuaAPI.Update(frame)
This prevents gameplay logic from executing multiple times simply because one client renders at a higher FPS.
-- scripts/mods_archive/spawn_test/main.lua
local SpawnTest = {}
function SpawnTest.Update(frame)
if frame ~= 300 then
return
end
local player = House.GetPlayer()
if not player then
return
end
local count = player:SpawnUnit(
"APOC",
5,
100,
100,
0,
false,
"hunt"
)
Engine.PrintMessage(
"Spawned " .. count .. " APOC"
)
end
return SpawnTestOnDebugCommand is a global callback.
-- Global OnDebugCommand contract (no debug_console mod file in the tree).
function OnDebugCommand(text)
local count, typeId =
text:match("^(%d+)%s+(%u+)$")
if not count or not typeId then
return
end
local player = House.GetPlayer()
if not player then
return
end
player:SpawnUnit(
typeId,
tonumber(count),
100,
100,
0,
false,
"hunt"
)
endStatus: 🔵 FRAMEWORK LOGIC VERIFIED (in-game runtime verification pending)
Reference implementation: tactical_patrol (scripts/mods_archive/; the
scripts/mods/ path in older records is stale) + docs/FRAMEWORK.md
Doc-sync note: the recipe below previously started from
require("framework.init")(Framework.update/Framework.enableUnitEvents/Framework.UnitController). That module does not exist in the tree — there is noFramework.updatedriver and nounit_created/unit_destroyedemitter;CombatStateTrackeremitscombat_unit_invalidated/combat_state_changed. Compose the leaf modules directly until and unless a framework entry module lands.
LuaAPI exposes a thin, composable Lua-side gameplay framework on top of the native bindings. It is pure Lua — no new native bindings — and provides:
- EventBus — multiple listeners, ordered dispatch, error isolation.
- Timer — frame-based
after/every/atscheduling, cancellation. - Query —
enemies_in_range,nearest_enemy,units_by_house, etc. - Task —
MoveTo/Attack/Waitnodes withSequence/Loopcomposites. - CombatStateTracker — id-keyed snapshots,
combat_state_changed/combat_unit_invalidatedemissions (polled, not native).
The goal is API composability, not API surface: a modder composes
UnitController:patrol(...), Query.nearest_enemy(...), and an onTaskDone
hook to build a patrol, rather than hand-rolling a state machine.
Gameplay need
↓
Check existing native primitive
↓
Build Lua abstraction if possible ← M14 does this
↓
Only add native primitive if missing ← M14 added none
-- Compose leaf modules directly (no UnitController module exists in-tree;
-- scripts/framework/unit_controller.lua is the TacticalPatrol demo itself).
local EventBus = require("framework.event_bus")
local Query = require("framework.query")
local Timer = require("framework.timer")
function MyMod.Update(frame)
Timer.update(frame)
-- per-unit logic here: Query.nearest_enemy(...) -> unit:attack(e),
-- tracked by id and re-resolved every frame (see CombatStateTracker).
end- Do not add a native binding "for convenience." M14 added zero; every
requirement was already solvable by composing
MoveTo/Attack/GetUnitsInRadius. - Never retain a stale
TechnoClass*. Track units by id and re-resolve them each frame; death reports are value snapshots, not pointers. - Session reset is handled by the VM. Recreating the Lua state clears all listeners/timers/controllers; no framework cross-session leak.
- Timers must use logical frames (not
os.time) or you risk CnCNet OOS. - The native event callbacks are not wired in the current build. The
framework drives itself from
Update()rather than depending onOnScenarioStart/OnUnitDestroyed.
These principles emerged from the development and debugging of LuaAPI.
The C++ layer should provide safe access to the engine and maintain runtime state.
Lua should decide gameplay behavior.
Engine objects can become invalid after destruction, map transitions, and other lifecycle events.
Never assume a previously obtained object remains valid.
Use the API's validity checks before accessing engine-backed objects.
Do not erase objects from containers while iterating over those containers.
Queue removals and process them after iteration.
Any system storing references to engine objects must clear references when those objects are destroyed.
A system that works on a fresh scenario may fail after loading a save.
Do not assume scenario-start callbacks cover the entire lifecycle.
Large squared distances can overflow 32-bit integers.
Use appropriate integer widths for coordinate calculations.
Gameplay state changes should be tied to logical game frames rather than render-frame frequency.
An existing hook or signature mismatch does not automatically indicate an injection failure.
Compatibility with other engine modifications must be evaluated before treating the condition as fatal.
This document intentionally distinguishes between implemented functionality and speculative API possibilities.
A capability should only be marked ✅ VERIFIED when it has been implemented and tested against the current LuaAPI build.
Examples and recipes in this document are intended to reflect the current API contract.
If an API name or behavior changes, this document must be updated together with API.md and TUTORIAL.md.
API.md— Complete LuaAPI referenceTUTORIAL.md— Beginner tutorialREADME.md— Project overview and installationENGINEERING_LESSONS.md— Engineering notes and debugging history
LuaAPI development follows a verification-first workflow:
Understand the API
↓
Build a small Lua prototype
↓
Verify the behavior
↓
Move unsafe engine work into C++
↓
Expose a safe Lua interface
↓
Document the proven capability
Build small. Test frequently. Verify before documenting.