Skip to content

Plugin Lifecycle#

Every Xenon plugin can implement up to 7 entry points. All are optional — implement only what you need.

Entry Points#

on_get_info#

extern "C" void on_get_info(xenon::PluginInfo* info);

Called once when the host discovers the plugin. Fill in the PluginInfo struct to declare your plugin's name, flags, and dependencies. Usually handled by a macro.

When: Before any other entry point.


on_load#

extern "C" void on_load();

Called once when the plugin is loaded and initialized. Use this to:

  • Load config values
  • Initialize state
  • Log a startup message
extern "C" void on_load()
{
    g_enabled = Config::GetBool("enabled", true);
    Log("Plugin loaded!");
}

on_unload#

extern "C" void on_unload();

Called once when the plugin is being unloaded. Use this to:

  • Save config values
  • Clean up state
extern "C" void on_unload()
{
    Config::SetBool("enabled", g_enabled);
    Config::Save();
    Log("Plugin unloaded!");
}

on_frame#

extern "C" void on_frame(float deltaTime);

Called every tick. Use this for game logic, state updates, and calculations.

Parameter Type Description
deltaTime float Time in seconds since the last frame
extern "C" void on_frame(float dt)
{
    g_timer += dt;
    if (g_timer > 1.0f)
    {
        Log("One second passed");
        g_timer = 0.f;
    }
}

Render-only APIs

The following APIs are only valid in on_render() — calling them from on_frame() returns zero/false and logs a one-shot warning per plugin to the debug console:

  • All Draw::* calls (Line, Circle, Rect, Text, etc.)
  • WorldToScreen / WorldToScreenUnclamped
  • GetCameraPosition / GetViewMatrix
  • FindBestTargetInFov
  • Entity::GetFovTo(boneId), Entity::GetScreenOffsetTo(boneId), Entity::GetClosestBoneInFov(...)
  • Entity::GetHitboxes(...), Entity::GetHitbox(...)
  • Entity::GetLerpHistory(...)

Reason: projected coordinates, render-only hitbox data, and the draw-list buffer are refreshed during the render phase. GetCameraForward() and its GetViewDirection() alias are exceptions: they use a locked update-side matrix snapshot from on_frame().

Phase-agnostic queries (IsEnemy, IsAlly, IsVisible, GetDistance, FindBestTarget, PredictPosition, GetCameraForward, GetViewDirection, AimGetDirection, AimHitsHitbox, AimAtPositionEx, AimAtBoneEx, and AimHitTest) work in both phases.

Coordinating aim and input#

on_frame is the correct place to advance AimAtPositionEx / AimAtBoneEx and poll tracked input receipts. Their commandTick values share the game command counter exposed by GetGameCommandTick().

Do not treat command ticks as shot or projectile-spawn confirmation. In particular, InputRequestStateV1::InputConsumed means only that the GameMemory backend observed the input dispatcher consume the injected edge. The KernelDriver backend cannot observe this milestone. The weapon can still reject the action, and an accepted ability can have a later cast or projectile-spawn frame.

Receipt milestone ticks are bracketed observations. Zero can mean the milestone has not occurred or that the event crossed a command boundary / no stable nonzero tick was available. Superseded is terminal and means the request was coalesced or replaced, but it may have emitted or still affect a live held/timed action; do not automatically retry it.

For aimed casts:

  1. Call the Aim*Ex function every command tick.
  2. Require AimStepResultV1::IsSettled() and an appropriate settledCommandTicks count.
  3. Apply AimHitTest and weapon/ability readiness gates when relevant. Require the hit result's nonzero commandTick to equal the settled aim result's commandTick; if the tick changed between queries, continue aiming and evaluate again.
  4. Submit one tracked press or pulse and retain its request ID.
  5. Keep advancing aim after InputConsumed until a hero-specific cooldown/active transition, projectile event, or known cast-startup window confirms it is safe to release aim ownership.

AimHitResultV1::fireReady is geometric and visibility-based. It is not shot confirmation.


on_render#

extern "C" void on_render();

Called every frame during the render phase. All Draw calls go here.

extern "C" void on_render()
{
    for (Entity player : Players())
    {
        if (!player.IsAlive() || !player.IsEnemy()) continue;
        Vector2 screen;
        if (WorldToScreen(player.GetBonePos(Bone::Head), screen))
            Draw::CircleFilled(screen, 5.f, Color::Red());
    }
}

on_menu#

extern "C" void on_menu();

Called when the Xenon menu is open. All ImGui calls go here.

extern "C" void on_menu()
{
    if (ImGui::CollapsingHeader("My Plugin"))
    {
        ImGui::Checkbox("Enabled", &g_enabled);
        ImGui::SliderFloat("Range", &g_range, 10.f, 200.f);
    }
}

on_hero_changed#

extern "C" void on_hero_changed(uint64_t heroPoolId);

Called when the local player's hero changes. Useful for hero-specific plugins that need to update state.

Parameter Type Description
heroPoolId uint64_t The new hero's pool ID (compare with HeroId)
extern "C" void on_hero_changed(uint64_t heroId)
{
    if (heroId == HeroId::Widowmaker)
        Log("Switched to Widowmaker");
}

Execution Order#

Each frame, the host calls entry points in this order:

  1. on_frame(dt) — game logic
  2. on_render() — drawing
  3. on_menu() — UI (only if menu is open)

on_load and on_unload are called once at plugin start/stop. on_hero_changed is called whenever the local player switches heroes. on_get_info is called during plugin discovery.

Minimal Plugin#

The absolute minimum is on_get_info (or a macro that generates it):

#include <xenon/SDK.hpp>
XENON_PLUGIN("noop", "No-Op", "Does nothing")

This is a valid plugin that loads and does nothing.