Skip to content

FAQ#

Common questions and pitfalls when developing Xenon plugins.

Why can't I use #include <string> or <vector>?#

Xenon plugins compile to freestanding WASM with -nostdlib. The C and C++ standard libraries are not available. Use only <xenon/SDK.hpp> and the types it provides.

If you need dynamic containers, you'll have to implement them yourself or use a library plugin.

How do I format numbers into strings?#

Use TextBuilder — it's included automatically via SDK.hpp:

// Integer
TextBuilder<32> buf;
buf.putInt(123);
Log(buf.c_str());  // "123"

// Float with 2 decimal places
TextBuilder<32> buf2;
buf2.put("Value: ").putFloat(12.345f, 2);
Log(buf2.c_str());  // "Value: 12.35"

// Combined text + numbers
TextBuilder<64> hp;
hp.put("HP: ").putInt(static_cast<int>(enemy.GetHealth()))
  .put("/").putInt(static_cast<int>(enemy.GetHealthMax()));
Draw::Text(x, y, Color::White(), hp.c_str());

You can also use the lower-level fmt::int_to_str and fmt::float_to_str functions if you prefer working with raw buffers. See the Format & Strings API for details.

Why does my Combo show nothing / only the first item?#

Combo items must be a null-separated string, not a C array.

// CORRECT:
ImGui::Combo("Mode", &mode, "Head\0Body\0Nearest\0");

// WRONG — will not compile or will show only one item:
const char* items[] = {"Head", "Body", "Nearest"};
ImGui::Combo("Mode", &mode, items, 3);

The SDK's ImGui::Combo takes a single const char* where items are separated by \0 bytes.

IsKeyDown fires every frame — how do I detect a single press?#

Use IsKeyPressed — it returns true only on the frame the key goes down:

extern "C" void on_frame(float dt)
{
    if (IsKeyPressed(VK::F1))
        g_enabled = !g_enabled;
}

There is also IsKeyReleased for detecting when a key is let go. See the Core API for details.

GetBonePos returns (0, 0, 0) — what's wrong?#

Bone data may not be available for all entities at all times. Always check for zero and use a fallback:

Vector3 headWorld = enemy.GetBonePos(Bone::Head);
if (headWorld.x == 0.f && headWorld.y == 0.f && headWorld.z == 0.f)
{
    // Fallback: root position + estimated head offset
    Vector3 root = enemy.GetPosition();
    headWorld = Vector3(root.x, root.y + 1.8f, root.z);
}

My plugin doesn't load — no errors shown#

Check these common issues:

  1. Missing on_get_info: Every plugin needs XENON_PLUGIN_INFO (or equivalent macro). Without it, the host can't identify your plugin.

  2. Wrong build mode: If it's a library plugin, use build.bat --library. If it's a dependent plugin, build normally but make sure the library .wasm is also present.

  3. Dependency not found: If you use XENON_PLUGIN_INFO_DEPS, the library WASM must be loaded too. The host can't resolve lib_* imports without it.

Can I use new or malloc?#

Not by default — there's no allocator in freestanding WASM. All data must be stack-allocated or use static globals.

If you need dynamic allocation, you'd have to implement your own allocator (bump allocator, etc.) in a library plugin.

How large can my plugin be?#

Typical plugins compile to 1-5 KB of WASM. The host loads them into memory, so there's no hard limit, but keep them small for fast loading. The -O2 flag is already applied by the build script.

Can I split a plugin across multiple files?#

No — each plugin is a single .cpp file compiled to a single .wasm. If you need shared code, use the dependency system to create a library plugin.

Where are config files stored?#

Config is managed by the host. Each plugin gets its own key-value namespace. You don't need to worry about file paths — just use Config::Get* and Config::Set*.

What hero ID should I use for a universal plugin?#

Set targetHeroId = 0. This makes the plugin active for all heroes.

XENON_PLUGIN_INFO("my_plugin", "My Plugin", "Me", "Works on all heroes", "1.0",
    0,  // <-- universal
    PluginFlags::HasOverlay | PluginFlags::HasMenu)