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#
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#
Called once when the plugin is loaded and initialized. Use this to:
- Load config values
- Initialize state
- Log a startup message
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#
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/WorldToScreenUnclampedGetCameraPosition/GetViewMatrixFindBestTargetInFovEntity::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:
- Call the
Aim*Exfunction every command tick. - Require
AimStepResultV1::IsSettled()and an appropriatesettledCommandTickscount. - Apply
AimHitTestand weapon/ability readiness gates when relevant. Require the hit result's nonzerocommandTickto equal the settled aim result'scommandTick; if the tick changed between queries, continue aiming and evaluate again. - Submit one tracked press or pulse and retain its request ID.
- Keep advancing aim after
InputConsumeduntil 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#
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#
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#
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:
on_frame(dt)— game logicon_render()— drawingon_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):
This is a valid plugin that loads and does nothing.