Skip to content

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

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.