Prerequisites: Basic Lua programming and familiarity with Red Alert 2 modding concepts
Difficulty: Beginner
Time: ~30 minutes
This tutorial builds a small LuaAPI mod that reads game objects, modifies units, spawns units, and responds to engine events.
⚠️ This tutorial describes the current implemented interface. For authoritative signatures and return values, seeAPI.md.
- Installation & Setup
- Creating Your First Mod
- Understanding the API
- Working with Units
- Responding to Events
- Testing Your Mod
- Common Pitfalls
- Quick Reference
- Next Steps
You need:
- Red Alert 2: Yuri's Revenge 1.001
- A LuaAPI release
- A text/code editor such as VS Code or Notepad++
Extract the release into the Yuri's Revenge directory. A typical installation contains:
Yuri's Revenge/
├── gamemd.exe
├── LuaAPI.dll
├── injector.exe
└── scripts/
├── init.lua
├── active_mods.txt
└── mods/
Run the injector according to the release instructions.
After starting the game, inspect LuaAPI.log if you need to diagnose loading or script errors.
Create:
scripts/mods/my_first_mod/
local MyFirstMod = {}
function MyFirstMod.OnScenarioStart()
Engine.PrintMessage("MyFirstMod loaded!")
end
function MyFirstMod.Update(frame)
-- Called once per logical game frame.
end
return MyFirstModA mod returns a Lua table. The loader calls lifecycle methods defined on that table.
Open:
scripts/active_mods.txt
Add:
my_first_mod
The entry must match the directory name. Lines beginning with # are comments.
Start a skirmish. The OnScenarioStart() callback should display the message through the engine message system.
LuaAPI exposes implemented functionality through namespaces and validated engine-backed objects.
| Namespace / Object | Purpose | Example |
|---|---|---|
House |
House/player access and economy | House.GetPlayer() |
World |
Unit, building, and map queries | World.GetUnits() |
Engine |
Engine/HUD helpers | Engine.PrintMessage("Hello") |
game |
Lower-level/legacy diagnostics and map helpers | game.GetEventHookOverrideCount() |
Techno |
Methods available on engine objects | unit:GetTypeName() |
The native layer validates engine objects before exposing or operating on them. Lua references should still be treated as short-lived because an engine object can become invalid after destruction or a session transition.
For gameplay timing, use the frame argument supplied to Update(frame). Do not substitute wall-clock time for deterministic gameplay logic.
World.GetUnits() returns mobile technos such as vehicles, infantry, and aircraft.
function MyFirstMod.Update(frame)
for _, unit in ipairs(World.GetUnits()) do
if unit:IsAlive() then
local typeName = unit:GetTypeName()
local owner = unit:GetOwner()
local hp = unit:GetHealth()
local pos = unit:GetPosition()
-- typeName: "APOC", "E1", "DRED", etc.
-- pos: { x = ..., y = ..., z = ... }
end
end
endFor a global techno scan that includes buildings, use World.GetAllUnits().
function MyFirstMod.Update(frame)
local player = House.GetPlayer()
if not player then
return
end
local mine = {}
for _, unit in ipairs(World.GetUnits()) do
if unit:IsAlive() and unit:GetOwner() == player then
table.insert(mine, unit)
end
end
Engine.PrintMessage("You control " .. #mine .. " mobile units")
endlocal nearby = World.GetUnitsInRadius(100, 100, 10)The radius is specified in map cells. RA2 uses 256 leptons per cell. Avoid unnecessarily large radius searches; for whole-map scans, prefer World.GetAllUnits().
Lifecycle callbacks are methods on the mod table returned from main.lua. OnDebugCommand is different: it is a global Lua callback.
function MyFirstMod.Update(frame)
if frame % 300 == 0 then
Engine.PrintMessage("Five seconds of logical game time")
end
endUse logical frames for deterministic gameplay timing.
Use it for post-scenario initialization:
function MyFirstMod.OnScenarioStart()
Engine.PrintMessage("Scenario initialized")
endDo not assume this callback restores runtime state after a savegame is loaded. Systems that require runtime state must handle that lifecycle explicitly.
This callback runs at the damage-processing boundary and can replace incoming damage:
function MyFirstMod.OnPreDamage(attacker, target, damage, dmgType, frame, subc)
local player = House.GetPlayer()
if player and target and target:GetOwner() == player then
return damage * 0.5
end
return nil
endReturn values:
number— replaces the incoming damage.0— cancels the damage.nil— leaves the original damage unchanged.
Never return negative damage values.
function MyFirstMod.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
)
endkiller may be nil for engine-side causes such as environmental damage or other non-unit sources.
OnDebugCommand is a global function and is not attached to the returned mod table.
function OnDebugCommand(text)
Engine.PrintMessage("Command: " .. text)
endOnly one active definition should normally exist. Multiple mods defining the same global callback can overwrite one another.
The current high-level spawn helper is:
house:SpawnUnit(typeId, count, x, y, facing, force, action)Example:
function MyFirstMod.OnScenarioStart()
local player = House.GetPlayer()
if not player then
return
end
local created = player:SpawnUnit(
"APOC",
5,
100,
100,
0,
false,
"hunt"
)
Engine.PrintMessage("Spawned " .. created .. " APOC")
endThe return value is the number of units actually created. Normal spawning can use the implementation's nearby-cell fallback when the requested location is unavailable.
Milestone 10 exposes explicit sub-turret state and split-salvo firing:
unit:AddSubTurret(1, 40, 0, 15, 12, 90)
unit:SetSplitTargets({enemy})
unit:FireSplitSalvo()For split-salvo behavior with multiple targets:
unit:SetSplitTargets({enemyA, enemyB, enemyC})
unit:FireSplitSalvo()Target acquisition remains Lua gameplay logic; the native layer maintains turret state and performs the explicit operation requested by Lua.
if unit and unit:IsAlive() then
local hp = unit:GetHealth()
endIsAlive() checks liveness at that moment. It does not make a stored engine reference permanently safe.
Pass damage through:
return nilReduce it:
return damage * 0.5Cancel it:
return 0Do not return negative damage.
Avoid using os.time() or os.clock() for gameplay decisions that must remain synchronized in multiplayer.
Use:
function MyFirstMod.Update(frame)
if frame % 300 == 0 then
-- deterministic frame-based logic
end
endCheck:
scripts/active_mods.txt
The entry must exactly match the directory:
scripts/mods/my_first_mod/
my_first_mod
Also inspect LuaAPI.log for loader or script errors.
Do not assume OnScenarioStart() runs after loading a saved game. If your mod creates runtime state, verify and restore that state as required during subsequent updates.
RA2 coordinates use 256 leptons per cell. Large squared-distance calculations can exceed 32-bit integer range. Prefer a reasonable radius or use World.GetAllUnits() for global scans.
It is not a method on the returned mod table. Avoid defining competing global implementations across multiple mods.
local player = House.GetPlayer()
local count = House.GetCount()
local house = House.GetByIndex(0)
house:GetName()
house:IsHuman()
house:IsAlliedWith(otherHouse)
house:GetCredits()
house:SetCredits(5000)
house:AddCredits(500)
house:GetPowerOutput()
house:GetPowerDrain()
house:SpawnUnit("APOC", 1, 100, 100, 0, false, "hunt")World.GetBuildings()
World.GetUnits()
World.GetAllUnits()
World.GetUnitsInRadius(x, y, radius)
World.GetWaypoint(id)unit:GetOwner()
unit:GetTypeName()
unit:GetHealth()
unit:GetMaxHealth()
unit:IsAlive()
unit:GetPosition()
unit:GetDistanceTo(other)
unit:GetId()
unit:GetKind()
unit:IsAttacking()
unit:GetTarget()
unit:IsIdle()
unit:MoveTo(x, y)
unit:Scatter()
unit:Hunt()
unit:TakeDamage(amount, warhead)
unit:Disable(frames)
unit:SetHealthRatio(ratio)
unit:AttachParticleSystem(name)unit:AddSubTurret(section, offX, offY, offZ, rot, rof)
unit:GetSubTurretCount()
unit:GetSubTurret(index)
unit:SetSplitTargets(targets)
unit:FireSplitSalvo()
unit:ClearSubTurrets()Engine.PrintMessage(text)
game.GetEventHookOverrideCount()
game.ClearEventHookOverrides()function MyMod.Update(frame)
end
function MyMod.OnScenarioStart()
end
function MyMod.OnPreDamage(attacker, target, damage, dmgType, frame, subc)
return nil
end
function MyMod.OnUnitDestroyed(victim, killer)
endfunction OnDebugCommand(text)
end- API Reference — authoritative interface and callback contract.
- Capabilities & Cookbook — verified mechanics and practical recipes.
- Sample mods in
scripts/mods/— working examples. - Architecture Roadmap — development status and milestones.
The recommended workflow is simple: start with a small script, test it in-game, verify the behavior, and document only what the current build actually supports.