Skip to content

Chat camera input and more

Alexandre 'kidev' Poumaroux edited this page Oct 21, 2026 · 5 revisions

Chat, camera, input and more

This page covers the smaller modules of the API, one section each. The API reference lists every function and class.

Module For
chat Chat commands and messages
timers Code that runs later or every frame
tasks Work on other threads
game Your slot, raw unit records, the game version
actions Acting as the player
sfx Sounds
camera Moving the camera, first and third person views
input Keys and clicks
server The server kind and its capabilities
advanced Harmony patches and .NET types

Players, units and game data have their own page: Players, units and game data.

Contents

chat

from proleak import chat


@chat.command("hello")
def hello(cmd):
    cmd.reply(f"hi, args={cmd.args}", color="#33ff33")

A command stays on this client: teammates never see !hello. cmd.name, cmd.args and cmd.raw describe what was typed, and cmd.reply(text, color=None) answers in your own chat.

  • send=True (@chat.command("hello", send=True)) lets the message reach the server as typed and still runs the handler.
  • When two plugins register the same name, the first keeps !name, and both stay reachable as !<plugin id>:name.
  • chat.say(text, color=None) shows a line in your own chat.
  • chat.send(text) sends a message as the player and needs actions.

Colors are "#rrggbb". The prefix (! by default) is chat_prefix in Settings.

timers

from proleak import timers


@timers.every(5)
def poll():
    ...


timers.after(1.5, lambda: print("later"))

timers.every(seconds, fn), timers.after(seconds, fn) and timers.on_frame(fn) work as decorators or calls and return a timer with cancel(). Callbacks run on the main thread.

tasks

Call Does
tasks.run_in_thread(fn, *args) Runs fn on a worker thread and returns a concurrent.futures.Future.
tasks.call_soon_main(fn, *args) Queues fn for the main thread at the next frame. Works from any thread.
tasks.spawn(coro) Runs a coroutine on a shared asyncio loop.

Worker threads receive PluginUnloaded when the plugin unloads.

from proleak import chat, tasks


def fetch():
    ...                             # slow work, off the main thread
    tasks.call_soon_main(chat.say, "done")


tasks.run_in_thread(fetch)

game

Call Returns
game.local_player() Your player slot (1 to 8). world.player() gives the Player object.
game.unit(unit_id) {id, type, owner, x, z, alive, placed, wave_fighter}, or None.
game.units() Every unit, as those records.
game.version() The running game's version.
game.built_for() The game version this ProLeak was built for.
game.available(name) True when an event, an action (actions.hire) or a feature (world) runs on this game; otherwise the reason, as a string.

wave_fighter and placed link a placed fighter and the unit that fights for it in a wave (see Units); both are -1 for other units.

actions

Call Acts
actions.build(builder_unit, unit_type, x, z) Places a fighter at map point (x, z).
actions.train(unit, unit_type) Upgrades a fighter, or trains a unit.
actions.hire(unit, unit_type) Hires a mercenary from your king or barracks.
actions.upgrade(upgrade_id, level) Buys a king upgrade or Upgrade Supply.

They need actions, so they work in local games only (see Plugin permissions), and return whether the game accepted the request; the server may still refuse it.

Note

The game's data calls upgrades researches: upgrade_id is a research id such as king_attack_research_id. data.king_upgrades() lists the king's.

Sounds

Sounds play on this client only, on every server kind.

from proleak import sfx

sfx.play_ui("goodjob")            # a UI sound by name; sfx.names() lists them
sfx.announce("round-start")       # the same sounds, on the announcer's channel
sfx.what(unit)                    # the line a unit says when selected
sfx.play_from(path, unit)         # a sound bank path, coming from a unit
sfx.play_at(path, x, z)           # a sound bank path, at a map point
sfx.play_global(path)             # a sound bank path, heard everywhere

play_ui and announce refuse a name the game does not have and suggest close ones.

camera

camera.get() returns {x, y, z, yaw, pitch, fov}. camera.set(...), camera.focus_unit(unit_id), camera.reset(), camera.pov(...) and camera.third_person(...) need actions.

First person

camera.pov(unit, offset=(0, 0, 0), pitch=0, show_unit=False, look=None)

Rides a unit in first person: the camera sits at the unit's head, moved by offset ((right, up, forward) in the unit's own frame), and looks where the unit faces. With look=(horizontal, vertical) the player can look around by dragging with the middle mouse button, within those angles in degrees.

from proleak import camera, input, world

view = None


@input.key("V")
def toggle(ev):
    global view
    if view is not None and view.alive:
        view.end()
        return
    selected = world.player().selected()
    if selected:
        view = camera.pov(selected[0], offset=(0, 0.2, 0.1))

Third person

camera.third_person(unit, distance=6, yaw=None, pitch=25, show_unit=True, orbit=True)

Follows a unit from distance world units away, looking at its head pitch degrees down; yaw=None starts behind the unit. With orbit=True a middle-button drag turns the camera around the unit and the mouse wheel moves it closer or further.

The view object

Both return a Pov:

Member Meaning
mode "first" or "third".
offset, pitch, show_unit, look First person; can change while the view lasts.
yaw, pitch, distance Third person: as the player left them, and can be set.
alive False once the view has ended.
end() Ends the view.

One plugin holds the view at a time. It ends with view.end(), when its unit leaves the game, when the plugin unloads, or when the player moves the camera with the minimap or a camera key. The camera then goes back to where it was.

input

from proleak import input


@input.key("F8", ctrl=True)
def toggle_overlay(ev):
    ...


@input.click(button=1)
def right_click(ev):
    print(ev.world_x, ev.world_z, ev.unit)
Decorator Fires
input.key(name, shift=False, ctrl=False, alt=False) For a Unity KeyCode name with exactly the given modifiers, also while the chat box has focus.
input.click(button=0) For a click of that mouse button (0 left, 1 right); ev has the screen and map positions (ev.world_x, ev.world_z).
input.hover(fn) When the cursor moves onto a unit.

ev.unit is a world Unit, or None when the cursor is over no unit. Handlers observe; the game still handles the input.

server

server.kind is menu, official_ranked, official_casual, local or unknown. server.allows(capability) and server.require(capability) check a capability. Server permissions describes both.

advanced

Warning

Names in both calls follow the game's code and change between game versions.

advanced.patch("Namespace.Type:Method", pre=None, post=None) installs a Harmony patch with Python callbacks: pre(instance, args) may return False to skip the original, and post(instance, args, result) sees the return value. It needs actions, at install and at every call, so it works in local games only, and it refuses the types of ProLeak and its mod loader.

advanced.type("Assets.Api.UnitApi") returns a .NET class to call directly and works on local servers only.


Previous: Purchases, signals and pings | Next: Game assets

Clone this wiki locally