Version: 1.1.0 Milestone: 11 Target:
gamemd.exe— Yuri's Revenge 1.001 Last Updated: 2026-08-31 Safety: Protected by RTTI (WhatAmI()) validation and SEH against0xC0000005.
This document is the authoritative reference for the LuaAPI public scripting interface.
It documents the namespaces, object methods, callbacks, game-state queries, multiplayer requirements, CnCNet integration, and mod-loading behavior available in the current implementation.
LuaAPI uses a namespace-based architecture:
House— Player and house managementWorld— Global queries for units, buildings, and spatial searchesEngine— Game engine functions such as HUD messagesGame— Game state and logical frame informationAI— AI control functions (planned; not currently available)
Object methods operate on validated game objects exposed to Lua.
Important: This document describes the implemented public API. Planned or experimental functionality must not be treated as stable API.
LuaAPI crosses the boundary between Lua and the native RA2 engine. Native pointers can become invalid when game objects are destroyed or when the game session changes.
LuaAPI therefore performs defensive validation at the native boundary.
Bindings validate native objects before accessing them.
For TechnoClass objects, validation includes:
nullptrchecks- RTTI/type validation through
WhatAmI() - object lifecycle validation
- health/liveness checks where applicable
Invalid or destroyed objects must not be dereferenced by Lua code.
When a native object is no longer valid, API functions may return nil or another documented failure value instead of dereferencing the invalid pointer.
Lua scripts should still perform normal validity checks:
local unit = World.GetUnits()[1]
if unit and unit:IsAlive() then
local health = unit:GetHealth()
endPointer validation is a native safety mechanism. It does not make stale Lua references permanently valid.
LuaAPI resets Lua callback state and related session data when the game scenario/session lifecycle requires it.
This prevents callback references and Lua state from surviving into an incompatible game session.
Unit and building objects are exposed through validated TechnoClass bindings.
Returns the house that owns the object.
local owner = unit:GetOwner()Returns: HouseClass* or nil.
Returns the object's INI type identifier.
local typeName = unit:GetTypeName()Returns: string
Examples:
HTNK
E1
DRED
APOC
Returns the current health value.
local health = unit:GetHealth()Returns: number
Returns the object's maximum health.
local maxHealth = unit:GetMaxHealth()Returns: number
Checks whether the object is currently valid and alive.
if unit:IsAlive() then
-- Safe to continue working with the object
endReturns: boolean
Returns the object's map position.
local pos = unit:GetPosition()
print(pos.x, pos.y)Returns:
{
x = number,
y = number
}Coordinates are map cell coordinates.
Returns the Euclidean distance between two objects in cells.
local distance = unit:GetDistanceTo(otherUnit)Returns: number
Checks whether the object is currently executing an Attack mission.
if unit:IsAttacking() then
-- Unit is attacking
endReturns: boolean
Applies damage to the object.
local success = unit:TakeDamage(100, "SA")Parameters:
amount— damage amountwarhead— optional warhead identifier
Returns: boolean
Temporarily disables the object.
local success = unit:Disable(90)Parameters:
frames— duration in logical frames
Returns: boolean
LuaAPI provides native support for adding and controlling additional weapon turrets.
Adds a sub-turret to a unit.
unit:AddSubTurret(1, 40, 0, 15, 12, 90)Parameters:
| Parameter | Type | Description |
|---|---|---|
section |
number |
Voxel section index |
offX |
number |
X offset in leptons |
offY |
number |
Y offset in leptons |
offZ |
number |
Z offset in leptons |
rot |
number |
Rotation speed |
rof |
number |
Rate of fire in logical frames |
Returns: boolean
Assigns targets to the unit's sub-turrets.
unit:SetSplitTargets(targets)Parameters:
targets— Lua table containingTechnoClassobjects.
The implementation assigns the supplied targets to the available sub-turrets according to the current split-target logic.
Returns: boolean
Fires the configured sub-turrets at their assigned targets.
unit:FireSplitSalvo()Returns: boolean
Sets the object's health using a normalized ratio.
unit:SetHealthRatio(0.35)Example:
0.35 = 35% health
1.00 = 100% health
Returns: boolean
Attaches a particle system to the object.
unit:AttachParticleSystem("DamageSmokeSys")Examples include:
DamageSmokeSys
DamageFireSys
Returns: boolean
House provides access to player and house objects.
Returns the local human player's house.
local player = House.GetPlayer()Returns: HouseClass* or nil.
Returns the number of available houses.
local count = House.GetCount()Returns: number
Returns a house by index.
local house = House.GetByIndex(index)Returns: HouseClass* or nil.
Returns the house name.
local name = house:GetName()Returns: string
Checks whether the house is controlled by a human player.
if house:IsHuman() then
-- Human-controlled
endReturns: boolean
Checks alliance status between two houses.
if house:IsAlliedWith(otherHouse) then
-- Allied
endReturns: boolean
Returns the current credits balance.
local credits = house:GetCredits()Returns: number
Adds or subtracts credits.
house:AddCredits(1000)
house:AddCredits(-500)Parameters:
amount— positive to add, negative to subtract
Returns: boolean
Returns total power production.
local power = house:GetPowerOutput()Returns: number
Returns total power consumption.
local drain = house:GetPowerDrain()Returns: number
Spawns units at the specified map position.
local spawned = player:SpawnUnit(
"APOC",
5,
100,
100,
0,
false,
"hunt"
)| Parameter | Description |
|---|---|
typeId |
INI unit type identifier, e.g. "APOC" |
count |
Number of units to spawn |
x |
X map cell |
y |
Y map cell |
facing |
Facing direction, 0–255 |
force |
Whether to bypass normal spawn/pathfinding checks |
action |
Optional action/mission identifier |
When force=false, the implementation attempts to find a valid nearby cell if the requested cell cannot be used.
The spawn search is bounded by the implementation's configured search radius.
If the requested coordinates are outside the valid map area, spawning fails for those units.
If an action such as "hunt" is supplied and supported, the spawned unit receives that action after creation.
Returns: number — number of successfully created units.
World provides global queries over objects currently present in the game world.
Returns buildings currently available to the API.
local buildings = World.GetBuildings()
for _, building in ipairs(buildings) do
-- ...
endReturns: table
Returns units currently available to the API.
local units = World.GetUnits()Returns: table
Returns all supported unit objects, including infantry.
local units = World.GetAllUnits()Returns: table
Returns units located within the specified radius.
local units = World.GetUnitsInRadius(100, 100, 15)Parameters:
x— center X celly— center Y cellradius— radius in cells
The query uses Euclidean distance and filters invalid/dead objects according to the native validation rules.
Returns: table
Returns the coordinates of a map waypoint.
local pos = World.GetWaypoint(5)
if pos then
print(pos.x, pos.y)
endParameters:
id— waypoint identifier
Returns:
{
x = number,
y = number
}or nil if the waypoint does not exist.
Displays a message through the standard in-game message system.
Engine.PrintMessage("Hello, Commander!", 1)Parameters:
text— message stringcolorIndex— message color index
Returns: boolean
Returns the current logical game frame.
local frame = Game.GetFrame()The value corresponds to the engine's logical frame counter.
Returns: number
Checks whether a match is currently active.
if Game.IsInMatch() then
-- Match is active
endReturns: boolean
LuaAPI exposes two kinds of callbacks.
Lifecycle callbacks are methods on the mod table returned by main.lua. They are dispatched independently for each enabled mod.
Global callbacks are Lua global functions looked up by name. They are not methods of the mod table.
| Callback | Called by | Lookup | Arguments | Return value | If undefined |
|---|---|---|---|---|---|
Update(frame) |
init.lua |
Mod-table method | frame: number |
Ignored | Skipped |
OnScenarioStart() |
init.lua |
Mod-table method | None | Ignored | Skipped |
OnPreDamage(...) |
C++ event bridge | Registered callback reference | attacker, target, damage, dmgType, frame, subc |
number or nil |
Skipped |
OnUnitDestroyed(victim, killer) |
C++ event bridge | Registered callback reference | victim, killer |
Ignored | Skipped |
OnDebugCommand(text) |
C++ debug input layer | Global lua_getglobal lookup |
text: string |
Ignored | No-op |
Implementation note:
OnPreDamageandOnUnitDestroyedare registered as Lua callback references by the native event system. Their dispatch path must remain consistent with the implementation.
A mod exposes lifecycle callbacks by returning a Lua table:
local MyMod = {}
function MyMod.Update(frame)
-- Called once per logical frame.
end
function MyMod.OnScenarioStart()
-- Scenario initialization.
end
function MyMod.OnPreDamage(attacker, target, damage, dmgType, frame, subc)
-- Damage interception.
return nil
end
function MyMod.OnUnitDestroyed(victim, killer)
-- Destruction event.
end
return MyModCalled once per logical game frame.
Arguments:
frame—number, current logical frame.
Return value: ignored.
Use this callback for continuous gameplay logic, polling, timers, and state management.
The callback is gated against the game's logical frame rather than the render frame.
Called when the scenario starts.
Arguments: none.
Return value: ignored.
Use this callback for scenario initialization.
Called during the damage pipeline before incoming damage is applied.
| Argument | Type | Description |
|---|---|---|
attacker |
TechnoClass* or nil |
Attacking object, if available |
target |
TechnoClass* |
Object receiving damage |
damage |
number |
Incoming damage |
dmgType |
string |
Damage/warhead type |
frame |
number |
Current logical frame |
subc |
number |
Sub-cell index |
number— replaces the incoming damage.0— cancels the damage completely.nil— passes the original damage through unchanged.- Negative damage values must not be returned.
Example:
function MyMod.OnPreDamage(attacker, target, damage, dmgType, frame, subc)
local player = House.GetPlayer()
if not player or not target then
return nil
end
if target:GetOwner() == player then
return damage * 0.5
end
return nil
endCalled when a unit or building is destroyed.
| Argument | Type | Description |
|---|---|---|
victim |
TechnoClass* |
Destroyed object |
killer |
TechnoClass* or nil |
Object responsible for the destruction, if available |
Return value: ignored.
Example:
function MyMod.OnUnitDestroyed(victim, killer)
if not victim then
return
end
local victimType = victim:GetTypeName()
local killerType = killer and killer:GetTypeName() or "unknown"
Engine.PrintMessage(
victimType .. " destroyed by " .. killerType,
2
)
endOnDebugCommand is a global Lua function, not a lifecycle callback on the mod table.
The debug input layer looks up this function through the Lua global environment.
Define it as:
function OnDebugCommand(text)
Engine.PrintMessage("[DEBUG] " .. text, 1)
endDo not define it as:
local MyMod = {}
function MyMod.OnDebugCommand(text)
-- This is not the global debug callback.
end
return MyModtext—string, command entered by the user.
Ignored.
If no global OnDebugCommand exists, the debug event is ignored.
Because this callback is global rather than mod-scoped, only one active definition should be used.
LuaAPI gameplay callbacks execute as part of the game's logical simulation.
Lua scripts used in multiplayer must therefore avoid nondeterministic behavior.
Do not use wall-clock time for gameplay decisions:
-- Do not use in deterministic gameplay logic:
os.time()
os.clock()Use the logical game frame instead:
function MyMod.Update(frame)
if frame % 300 == 0 then
-- Deterministic frame-based logic
end
endLuaAPI's frame-based callback mechanism does not automatically make arbitrary Lua code deterministic.
The mod itself must also avoid nondeterministic inputs and behavior.
Random behavior must be handled consistently across clients.
Do not base gameplay decisions on local wall-clock time or other client-specific values.
Prefer deterministic state derived from the logical game state and frame sequence.
For example:
function MyMod.Update(frame)
if frame % 60 == 0 then
-- Deterministic timing point
end
endUpdate(frame) uses the game's logical frame counter rather than the render frame rate.
This means scripts should use the supplied frame argument for frame-based timing.
Do not implement render-rate timers using wall-clock time.
When testing LuaAPI through CnCNet:
- All players must use the same LuaAPI binary/version.
- All players must use identical gameplay scripts.
- All players must use the same
scripts/active_mods.txt. - All players must use the same mod configuration.
- Monitor the match for Out-of-Sync (OOS) errors.
LuaAPI can be attached to a CnCNet-launched Yuri's Revenge process.
Recommended workflow:
- Place
LuaAPI.dll,injector.exe, andscripts/in the game directory. - Launch the game through the CnCNet client.
- CnCNet starts the game process.
- Run:
injector.exe --attach
- The injector waits for the target game process and performs the required attachment/injection sequence.
The exact process behavior depends on the current injector implementation.
For automated testing or integration:
injector.exe --withcncnet
The injector operates without an interactive console and waits for the CnCNet game process.
Mods are enabled through:
scripts/active_mods.txt
Example:
# scripts/active_mods.txt
multi_turret_battleship
shield_overload
my_custom_mod
One mod name is specified per line.
The name corresponds to the mod directory under:
scripts/mods/
Comments begin with #.
Example:
scripts/
└── mods/
├── multi_turret_battleship/
├── shield_overload/
└── my_custom_mod/
-- scripts/mods/multi_turret_battleship/main.lua
local MultiTurretMod = {}
function MultiTurretMod.OnScenarioStart()
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
if unit:GetTypeName() == "DRED" then
unit:AddSubTurret(1, 40, 0, 15, 12, 90)
unit:AddSubTurret(2, -40, 0, 15, 12, 90)
unit:AddSubTurret(3, 0, 40, 15, 12, 90)
end
end
end
end
function MultiTurretMod.Update(frame)
local player = House.GetPlayer()
if not player then
return
end
if frame % 30 ~= 0 then
return
end
for _, unit in ipairs(World.GetUnits()) do
if unit:IsAlive() and unit:GetOwner() == player then
if unit:GetTypeName() == "DRED" then
local pos = unit:GetPosition()
local nearbyUnits =
World.GetUnitsInRadius(pos.x, pos.y, 15)
local targets = {}
for _, enemy in ipairs(nearbyUnits) 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
end
return MultiTurretModBecause OnDebugCommand is a global callback, a debug console implementation must define it globally:
-- scripts/mods/debug_console/main.lua
function OnDebugCommand(text)
local count, typeId =
text:match("^(%d+)%s+(%u+)$")
if not count or not typeId then
Engine.PrintMessage(
"[DEBUG] Invalid command: " .. text,
2
)
return
end
count = tonumber(count)
local player = House.GetPlayer()
if not player then
Engine.PrintMessage(
"[DEBUG] No player house",
2
)
return
end
local baseX, baseY = nil, nil
for _, building in ipairs(World.GetBuildings()) do
if building:IsAlive()
and building:GetOwner() == player then
local pos = building:GetPosition()
baseX = math.floor(pos.x)
baseY = math.floor(pos.y)
break
end
end
if not baseX or not baseY then
Engine.PrintMessage(
"[DEBUG] No player building",
2
)
return
end
local spawned = player:SpawnUnit(
typeId,
count,
baseX + 5,
baseY + 5,
0,
false,
"hunt"
)
Engine.PrintMessage(
"[DEBUG] Spawned "
.. spawned
.. " "
.. typeId,
1
)
endHouse.GetPlayer()
House.GetCount()
House.GetByIndex(index)
World.GetBuildings()
World.GetUnits()
World.GetAllUnits()
World.GetUnitsInRadius(x, y, radius)
World.GetWaypoint(id)
Engine.PrintMessage(text, colorIndex)
Game.GetFrame()
Game.IsInMatch()unit:GetOwner()
unit:GetTypeName()
unit:GetHealth()
unit:GetMaxHealth()
unit:IsAlive()
unit:GetPosition()
unit:GetDistanceTo(otherUnit)
unit:IsAttacking()
unit:TakeDamage(amount, warhead)
unit:Disable(frames)
unit:AddSubTurret(section, offX, offY, offZ, rot, rof)
unit:SetSplitTargets(targets)
unit:FireSplitSalvo()
unit:SetHealthRatio(ratio)
unit:AttachParticleSystem(sysName)house:GetName()
house:IsHuman()
house:IsAlliedWith(otherHouse)
house:GetCredits()
house:AddCredits(amount)
house:GetPowerOutput()
house:GetPowerDrain()
house:SpawnUnit(
typeId,
count,
x,
y,
facing,
force,
action
)function MyMod.Update(frame)
end
function MyMod.OnScenarioStart()
end
function MyMod.OnPreDamage(
attacker,
target,
damage,
dmgType,
frame,
subc
)
end
function MyMod.OnUnitDestroyed(
victim,
killer
)
endfunction OnDebugCommand(text)
endNative game objects can be destroyed between frames.
Always validate an object before using it:
if unit and unit:IsAlive() then
local health = unit:GetHealth()
endIncorrect:
function MyMod.OnDebugCommand(text)
endCorrect:
function OnDebugCommand(text)
endOnDebugCommand is a global callback.
If a mod implements OnPreDamage, explicitly return the intended result:
function MyMod.OnPreDamage(attacker, target, damage, dmgType, frame, subc)
return nil
endnil means that the original damage passes through unchanged.
Avoid:
os.time()
os.clock()for deterministic gameplay logic.
Use the logical frame:
if frame % 300 == 0 then
-- Logic
endLuaAPI provides logical-frame-based callbacks, but arbitrary Lua code can still introduce nondeterminism.
Multiplayer-safe behavior remains the responsibility of the mod author.
- TUTORIAL.md — Step-by-step guide for creating a LuaAPI mod
- CAPABILITIES_AND_COOKBOOK.md — Proven mechanics and implementation recipes
- ENGINEERING_LESSONS.md — Technical deep-dives, limitations, and engineering lessons
- MOD_MANAGER.md — Mod structure, loading, and distribution
- ROADMAP.md — Project milestones and development status