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:
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:
-
Missing
on_get_info: Every plugin needsXENON_PLUGIN_INFO(or equivalent macro). Without it, the host can't identify your plugin. -
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.wasmis also present. -
Dependency not found: If you use
XENON_PLUGIN_INFO_DEPS, the library WASM must be loaded too. The host can't resolvelib_*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.