Examples¶
Each example uses global functions and methods on API values. Save an example
as one .lua file in %APPDATA%\WololoLabs\scripts.
Show assigned-player resources¶
This script displays the four main resources. It reads fresh values in Update
and draws only in Render.
local label = "Waiting for a match"
local function resourceLabel(player, attribute)
local value = player:GetAttribute(attribute)
if value == nil then return "unavailable" end
return string.format("%.0f", value)
end
function Update()
local player = GetAssignedPlayer()
if player == nil then
label = "Waiting for a match"
return
end
label = string.format(
"Food %s | Wood %s | Gold %s | Stone %s",
resourceLabel(player, PlayerAttribute.FOOD),
resourceLabel(player, PlayerAttribute.WOOD),
resourceLabel(player, PlayerAttribute.GOLD),
resourceLabel(player, PlayerAttribute.STONE)
)
end
function Render()
RenderTopRightText(label, 12, 60, 14, Color("#FFFFFFFF"), true)
end
See Enumerations for PlayerAttribute values.
Mark the selected object¶
This script outlines your selected object and displays its name. It has no
Update callback because it reads the current selection while it draws.
function Render()
local player = GetAssignedPlayer()
local selected = player and player:GetSelectedObject() or nil
if selected == nil or selected:IsAlive() ~= true then
return
end
local position = selected:GetPosition()
if position == nil then
return
end
RenderObjectBounds(selected, Color("#00FF00"), 2)
RenderWorldText(
selected:GetName(),
position,
14,
Color("#FFFFFFFF"),
true,
true
)
end
Read the whole selection¶
This displays the selected-object count without issuing orders. The assigned player must be the local player; otherwise the query explains why it is unavailable.
local label = "Waiting for a match"
function Update()
local player = GetAssignedPlayer()
if player == nil then
label = "Waiting for a match"
return
end
local objects, reason = player:GetSelectedObjects()
if objects == nil then
label = "Selection unavailable: " .. reason
return
end
label = "Selected: " .. #objects
end
function Render()
RenderTopRightText(label, 12, 60, 14, Color("#FFFFFFFF"), true)
end
The array retains native selection order and includes selected enemy objects. Commands still enforce their own actor ownership requirements.
Inspect selected-building production¶
This reads the selected building's native queue without changing it. Entries represent batches, so one entry may contain several units. A batch ID is scoped to that building; it is not a command receipt or proof of completed production.
local label = "Select a production building"
function Update()
local player = GetAssignedPlayer()
local building = player and player:GetSelectedObject()
if building == nil then
label = "Select a production building"
return
end
local queue, reason = building:GetProductionQueue()
if queue == nil then
label = "Queue unavailable: " .. reason
return
end
local parts = {}
for _, entry in ipairs(queue) do
parts[#parts + 1] = string.format("%s %d x%d", entry.kind, entry.typeId, entry.quantity)
end
label = #parts == 0 and "Queue empty" or table.concat(parts, " | ")
end
function Render()
RenderTopRightText(label, 12, 60, 14, Color("#FFFFFFFF"), true)
end
Send one move order¶
This example moves the selected object once. Change enabled to true only in
a safe test match. Change destination to a valid map position.
local enabled = false
local issued = false
local destination = Vector2(42, 42)
function Init()
issued = false
end
function Update()
if not enabled or issued then
return
end
local player = GetAssignedPlayer()
local selected = player and player:GetSelectedObject() or nil
if selected == nil or selected:IsAlive() ~= true then
return
end
issued = UnitsMove({ selected }, destination) == true
end
true means that WololoLabs admitted the request to its command queue. Check fresh game state in a
later Update before you send another dependent order.
Next steps¶
- Find command signatures in Commands.
- Learn safe update patterns in Write reliable scripts.
- Use Lua values and game objects to inspect a player or object.
Issue a patrol route¶
Select owned units capable of patrolling and choose valid map positions. This example issues one route and retains its receipt until terminal metadata can be released. Keep the script loaded until cleanup finishes.
local issued = false
local pending_id = nil
function Update()
if pending_id then
local receipt = CommandReceipt(pending_id)
if receipt and receipt.state ~= "queued" and receipt.state ~= "executing" then
LogInfo("Patrol transport result", receipt.state, receipt.reason or "")
if ReleaseCommandReceipt(pending_id) then pending_id = nil end
end
return
end
if issued then return end
local player = GetAssignedPlayer()
local units = player and player:GetSelectedObjects()
if not units or #units == 0 then return end
issued = true
local accepted, receipt = UnitsPatrolRouteTracked(units, {
{x = 40, y = 40},
{x = 60, y = 40},
{x = 60, y = 60},
})
if accepted then
pending_id = receipt.id
else
LogWarn("Patrol admission rejected", receipt.reason)
end
end
Submission does not prove arrival. UnitsAttackMoveRouteTracked accepts the
same position array for an attack-move route. Both calls submit immediately;
appending orders to an existing route remains unavailable.
Attack a ground position with selected units¶
Call this helper once from your script's chosen trigger, with a valid ground position and owned units selected. The native game determines attack-ground eligibility, such as whether a selected siege unit supports the action.
local function attack_selected_ground(x, y)
local player = GetAssignedPlayer()
local units = player and player:GetSelectedObjects()
if not units or #units == 0 then return false end
return UnitsAttackGround(units, {x = x, y = y})
end
The boolean reports admission. For transport evidence, use
UnitsAttackGroundTracked with the receipt lifecycle shown in the patrol
example above. Observe the game separately to establish firing or impact.
Unload one known occupant¶
Use freshly queried objects and invoke the helper from your chosen script trigger. The native layer checks the parent relationship again at dispatch.
local function unload_one(container, occupant)
return UngarrisonUnit(container, occupant)
end
This targets only occupant. Use UngarrisonUnitTracked and retain/release its
receipt as in the patrol example when you need transport evidence. Observe the
occupant's garrison state separately to confirm it actually left.
Unload toward a destination¶
Invoke this helper once from your chosen trigger with freshly queried owned containers and a valid map position. It submits one native group action.
local function unload_toward(containers, x, y)
return UngarrisonTo(containers, {x = x, y = y})
end
Use UngarrisonToTracked with the receipt lifecycle above for submission
feedback. Check fresh game state before treating the unload as complete.
Request packing or unpacking¶
Call the appropriate action once from your script's chosen trigger, with fresh owned objects that support packing. The two actions are explicit, not toggles.
local function request_pack(units)
return UnitsPack(units)
end
local function request_unpack(units)
return UnitsUnpack(units)
end
Use the tracked variants and the receipt lifecycle above for submission evidence. Observe fresh unit state before assuming the requested transition has completed.
Toggle one gate¶
Call once for an explicit script trigger, with a freshly queried owned gate. Do not call every update or automatically retry an uncertain result.
local function toggle_gate_once(gate)
return ToggleGate(gate)
end
For native submission feedback, use ToggleGateTracked and retain/release the
receipt as shown above. The final locked state still needs game observation.
Cancel one item from the last production batch¶
Use an owned building as building. Run once; do not repeat every frame.
local queue, reason = building:GetProductionQueue()
if queue and #queue > 0 then
local accepted = CancelProduction(building, queue[#queue].batchId)
-- accepted means queued locally, not cancelled in the game.
-- Observe the building before deciding whether another request is needed.
end
Request a diplomacy change once¶
Run this once for the intended player; do not send it every frame. Choose an existing player other than yourself. Locked-team games reject the request.
local targetPlayerId = 2 -- Replace with the intended player's ID.
local accepted, receipt = SetDiplomacyTracked(targetPlayerId, "ally")
if not accepted then
Log("Diplomacy request was not queued")
else
Log("Diplomacy request queued; submission is not confirmation of an alliance")
end
Keep receipt if you want to inspect its lifecycle using the command-receipt API.
Append a movement destination¶
Run once for the intended group. The second request appends a destination after its existing native orders, including the first request if that was accepted by the game.
local player = GetAssignedPlayer()
local units = player and player:GetSelectedObjects()
if units and #units > 0 then
if UnitsMove(units, Vector2(40, 40)) then
local queued = UnitsMove(units, Vector2(50, 40), true)
if not queued then Log("Second movement request was not queued") end
end
end
Queue admission is not gameplay confirmation. Use tracked variants when the script needs to inspect rejection or submission later.
Queue a second object target¶
Call this once with the intended actors and two live target objects. The native game chooses the appropriate default action for each target.
local function orderTargets(units, firstTarget, secondTarget)
if not UnitsTargetObject(units, firstTarget) then return false end
return UnitsTargetObject(units, secondTarget, true)
end
The return value reports queue admission. A disappearing target can still reject before submission; tracked variants expose that rejection.
Detect native append support¶
local capability = GetApiCapability(UnitsMove)
if capability.supportsAppend == true and capability.available == true then
Log("Native movement append is supported and the command session is ready")
end
An absent supportsAppend field means unknown, including on older builds.
Readiness does not validate particular actors or guarantee execution.
Observe a target separately from submission¶
Use a later update to read the actor's current native target. Keep this evidence separate from the command receipt: matching targets do not prove that a particular request caused the state or that its action finished.
local function observeTarget(actorId, expectedTargetId)
local exists = XsDoesUnitExist(actorId)
if exists == nil then return "unavailable" end
if not exists then return "actor-missing" end
local targetId = XsGetUnitTargetUnitId(actorId)
if targetId == nil then return "unavailable" end
if targetId == expectedTargetId then return "target-observed" end
return "target-not-observed"
end
A non-match can mean the order is waiting, was superseded, or has already ended. Do not automatically retry from that observation alone.
Observe a tracked diplomacy request¶
Keep the receipt between script updates; call this poll once per update rather than blocking the game thread. A match reports current state only. The command may not have caused it. No automatic retry is safe from a mismatch alone.
local capability = GetApiCapability(SetDiplomacyTracked)
if capability.supportsObservation ~= true then return end
local admitted, request = SetDiplomacyTracked(2, "ally")
local pendingDiplomacy = admitted and request.id or nil
local function pollDiplomacy()
if not pendingDiplomacy then return end
local receipt = CommandReceipt(pendingDiplomacy)
if not receipt then
pendingDiplomacy = nil
return "unknown"
end
local observation = receipt.observation
local result = observation and observation.state or receipt.state
if receipt.sessionEnded or receipt.state == "rejected"
or receipt.state == "cancelled"
or (observation and observation.state == "matched") then
if ReleaseCommandReceipt(pendingDiplomacy) then pendingDiplomacy = nil end
end
return result
end
An owner that stops polling must release its terminal receipt. For a still-queued request, cancel it first and inspect the returned state before releasing it.
Poll the current target of a tracked group order¶
Call this helper from a later script update with the receipt ID returned by
UnitsTargetObjectTracked. It does not wait, release the receipt, or retry.
The caller keeps ownership of receipt cleanup. A match says all actors currently
have the requested target; it does not say gathering, attacking, or building is
complete, nor that an appended order has entered the native queue.
local function observeTargetReceipt(id)
local receipt = CommandReceipt(id)
if not receipt then return "unknown" end
if receipt.sessionEnded then return "session-ended" end
local observation = receipt.observation
if observation and observation.kind == "object-target" then
return observation.state
end
return receipt.state
end
Submit tribute with native fees¶
local capability = GetApiCapability(TributeToPlayerTracked)
if capability.available == true then
local accepted, receipt = TributeToPlayerTracked(2, 3, 100) -- 100 gold
if accepted then
-- Keep receipt.id and poll CommandReceipt on later updates.
-- Release terminal receipts when finished inspecting them.
-- Submitted does not prove that player 2 received all 100 gold.
else
print(receipt.reason)
end
end
The game applies its market requirements and may reduce the transfer for insufficient funds including fees. Never retry solely because a balance did not change immediately; the command can still be in flight.
Lock a gate explicitly¶
local function requestGateLock(gate)
if GetApiCapability(LockGatesTracked).available ~= true then
return false, {state="rejected", reason="unavailable"}
end
return LockGatesTracked(gate)
end
Retain the admitted receipt ID, inspect it on later updates and release terminal metadata when finished. A submitted receipt does not establish observed lock state. This operation requests locked state; it does not use a read-then-toggle.