IPC API
The IPC API connects Lua modules to external processes through Windows named pipes and JSON strings.
Functions
| Function | Signature | Returns | Description |
|---|---|---|---|
IPC.StartServer |
(pipeName) |
boolean |
Starts or joins a named pipe server. |
IPC.StopServer |
() |
nil |
Stops the current module instance's server endpoint. |
IPC.Send |
(message) |
boolean |
Sends a string or Lua value to all connected pipe clients. Non-string values are serialized to JSON automatically. |
IPC.HasMessages |
() |
boolean |
Returns whether queued messages are waiting for this module instance. |
IPC.GetMessages |
() |
string[] |
Returns queued messages for this module instance. |
JSON Helpers
| Function | Signature | Returns | Description |
|---|---|---|---|
ParseJSON |
(str) |
table |
Parses a JSON string into Lua values. Returns nil on parse error. |
ToJSON |
(obj) |
string |
Serializes a Lua value to JSON. Returns "{}" on serialization error. |
Routing Model
Multiple module instances can share one pipe name. Incoming messages can target specific module instances.
Accepted routing fields:
instanceIdorassignedInstanceIdassignedPlayerIdorplayerIdmoduleNamesettingsGroup
These fields can appear either:
- at the root object, or
- inside a root
targetobject
When routing fields are present, CONTROL only delivers the message to matching module instances. The Lua side receives the message payload without the routing wrapper.
Outgoing Envelope
IPC.Send(message) wraps your payload before sending it to the client:
{
"type": "module_message",
"pipeName": "AoE_ML_Pipe",
"source": {
"instanceId": 3,
"assignedPlayerId": 2,
"moduleName": "my_module",
"settingsGroup": "my_module [P1]"
},
"payload": {
"...": "your data"
}
}
If the original message is plain text instead of JSON, the payload stays a string. If you pass a Lua table or other non-string Lua value, CONTROL serializes it to JSON first.
Snapshot Buffers For IPC / ML
GetMapTilesPtr() and GetObjectsPtr() are part of the game API, not IPC.*, but they exist mainly for IPC users who want efficient bulk transfer instead of serializing thousands of Lua objects.
Both functions return two Lua values:
ptr: the address of an engine-owned packed buffercount: the number of elements in that buffer
Important behavior:
- The buffer is rebuilt on demand every time you call the function.
- The pointer is transient. Copy or read the buffer immediately.
countis an element count, not a byte count.- Byte size is
count * sizeof(Tile)orcount * sizeof(Object). GetObjectsPtr()is dead-inclusive.- Snapshot visibility follows the same fog-aware access rules used by the Lua API for object inclusion, tile visibility, and tile-derived flags.
Exact engine layouts:
#pragma pack(push, 1)
namespace game::snapshot {
struct Tile {
uint16_t x;
uint16_t y;
uint8_t terrain;
uint8_t elevation;
uint8_t isVisible;
uint8_t flags;
};
struct Object {
uint32_t id;
uint16_t unitObjectType;
uint16_t x;
uint16_t y;
uint8_t playerId;
uint8_t flags;
};
}
#pragma pack(pop)
static_assert(sizeof(game::snapshot::Tile) == 8);
static_assert(sizeof(game::snapshot::Object) == 12);
Flag bits:
Tile.flagsbit0: walkableTile.flagsbit1: navigatableObject.flagsbit0: alive
Recommended flow:
- Lua calls
GetMapTilesPtr()/GetObjectsPtr(). - Lua sends the returned pointer and count through IPC.
- The external reader uses
ReadProcessMemoryagainst the game process and decodes the packed structs.
Lua example:
function Update()
local tilesPtr, tileCount = GetMapTilesPtr()
local objectsPtr, objectCount = GetObjectsPtr()
IPC.Send({
type = "snapshot_meta",
tilesPtr = tilesPtr,
tileCount = tileCount,
objectsPtr = objectsPtr,
objectCount = objectCount
})
end
Python ctypes definitions:
import ctypes
class Tile(ctypes.Structure):
_pack_ = 1
_fields_ = [
("x", ctypes.c_uint16),
("y", ctypes.c_uint16),
("terrain", ctypes.c_uint8),
("elevation", ctypes.c_uint8),
("isVisible", ctypes.c_uint8),
("flags", ctypes.c_uint8),
]
class Object(ctypes.Structure):
_pack_ = 1
_fields_ = [
("id", ctypes.c_uint32),
("unitObjectType", ctypes.c_uint16),
("x", ctypes.c_uint16),
("y", ctypes.c_uint16),
("playerId", ctypes.c_uint8),
("flags", ctypes.c_uint8),
]
Reading example after you received ptr and count through IPC:
def decode_snapshot(process_handle, ptr, count, struct_type):
byte_count = count * ctypes.sizeof(struct_type)
raw = read_process_memory(process_handle, ptr, byte_count)
return (struct_type * count).from_buffer_copy(raw)
Lua Example
local pipeName = "AoE_ML_Pipe"
function Init()
IPC.StartServer(pipeName)
Log("IPC online for player " .. tostring(GetAssignedPlayerId()))
end
function Update()
if not IPC.HasMessages() then
return
end
for _, raw in ipairs(IPC.GetMessages()) do
local msg = ParseJSON(raw)
if msg and msg.action == "ping" then
IPC.Send({
action = "pong",
assignedPlayerId = GetAssignedPlayerId(),
time = GetGameTime()
})
end
end
end
function Unload()
-- Not required in theory: CONTROL stops the server automatically
-- after the module is unloaded. Keeping this is still fine as
-- explicit cleanup.
IPC.StopServer()
end
Python Example
import json
import time
import pywintypes
import win32file
PIPE_NAME = r"\\.\pipe\AoE_ML_Pipe"
def connect():
while True:
try:
return win32file.CreateFile(
PIPE_NAME,
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
0,
None,
win32file.OPEN_EXISTING,
0,
None,
)
except pywintypes.error as exc:
if exc.winerror in (2, 231):
time.sleep(0.5)
continue
raise
handle = connect()
targeted_ping = {
"target": {
"assignedPlayerId": 2,
"moduleName": "my_module"
},
"payload": {
"action": "ping"
}
}
win32file.WriteFile(handle, (json.dumps(targeted_ping) + "\n").encode("utf-8"))
result, data = win32file.ReadFile(handle, 4096)
print(data.decode("utf-8"))
Notes
- Pipe names are normalized automatically. Passing
"AoE_ML_Pipe"is enough. IPC.Sendreturnsfalseif no client is connected.IPC.HasMessages()is useful when polling every update and you want to skip empty queue drains.IPC.GetMessages()returns strings. UseParseJSON()when you expect JSON payloads.IPC.Send()andIPC.GetMessages()are safe to poll continuously; they return without waiting for a pipe close event.GetMapTilesPtr()andGetObjectsPtr()are intended for high-throughput IPC / ML workflows, not normal in-Lua iteration.- Explicit
IPC.StopServer()inUnload()is optional in practice because CONTROL also stops the server automatically after module unload.