C++ SDK Guide
The titan::Plugin base class, lifecycle callbacks, queries, state facades, and threading model.
The Plugin Base Class
Every native plugin inherits from titan::Plugin and overrides the lifecycle callbacks it needs.
The base class provides:
- Identity — via
TITAN_PLUGINorTITAN_PLUGIN_METAmacros - Lifecycle hooks — enable/disable, ticks, entity events, game events
- Self-registering members — settings, sections, and overlays declared as class members auto-register
- Host access — the full game-state API via fluent facades or the raw
HostApi
Identity Macros
Use TITAN_PLUGIN_META to declare all plugin metadata in one call. This supersedes the simpler
TITAN_PLUGIN macro and provides rich info in the controller UI:
class MyPlugin : public titan::Plugin {
TITAN_PLUGIN_META(
"my_plugin", // id (permanent, never change)
"My Plugin", // display name
"Does something useful.", // description
"Your Name", // author
"1.0.0", // version string
false) // default enabled on first install
// ...
};
The 6th parameter (defaultEnabled) controls whether the plugin starts enabled on a fresh install.
Default is false — plugins are opt-in by design.
Lifecycle
Enable / Disable
| Virtual | When it fires |
|---|---|
onEnabledChanged(bool) | Called when the controller toggles the plugin. Default implementation forwards to onEnable() / onDisable(). |
onEnable() | Plugin was just enabled. |
onDisable() | Plugin was just disabled. |
isEnabled() | Query the current state. |
The first onEnabledChanged call after construction is always fired — this is the initial-state
handshake from the controller. Plugins that wire up side effects (toggling UI, starting timers) can rely on this
guarantee.
Tick Callbacks
| Virtual | Cadence | Description |
|---|---|---|
onClientTick() | ~every frame | Fires on every client tick (high frequency). Use for responsive state checks. |
onGameTick(int32_t tick) | ~600ms | Fires on every game tick with the tick counter. Use for periodic logic. |
Entity Events
The plugin host diffs entity collections every tick and fires spawn/despawn callbacks:
| Spawn | Despawn |
|---|---|
onNpcSpawned(const Npc&) | onNpcDespawned(const Npc&) |
onPlayerSpawned(const Player&) | onPlayerDespawned(const Player&) |
onTileObjectSpawned(const TileObject&) | onTileObjectDespawned(const TileObject&) |
onProjectileSpawned(const Projectile&) | onProjectileDespawned(const Projectile&) |
Projectiles also have onProjectileMoved(const Projectile&) for position updates each frame.
Each entity reference is a live snapshot — you can query its position, name, ID, animations, and actions directly inside the callback:
void onNpcSpawned(const Npc& npc) override {
const auto t = npc.tile();
titan::logf("NPC spawned: {} (id={}) at ({}, {}, {})",
npc.name(), npc.id(),
t.x, t.y, t.plane);
}
void onProjectileMoved(const Projectile& proj) override {
titan::logf("Projectile moved: spotAnim={} targetEntity={}",
proj.spotAnimId(), proj.targetEntity());
}
Game Events
| Virtual | Description |
|---|---|
onMenuOptionClicked(MenuClickEvent&) |
Fires when the player clicks a menu option. Set .consumed = true to block the action. |
onScriptFired(const ScriptEvent&) |
Fires when a CS2 client script executes. |
onVarbitChanged(const VarbitChangedEvent&) |
Fires when a varbit value actually changes (no-op writes are filtered). |
onChatMessage(const ChatMessageEvent&) |
Fires for every chat line added to the chatbox (server, local, and plugin-injected). |
onItemContainerChanged(const ItemContainerChangedEvent&) |
Fires when item container contents differ from the previous tick. |
onSettingChanged(const std::string& key) |
Fires after a setting value is updated from the controller. |
Event Object APIs
MenuClickEvent — passed by mutable reference so you can cancel the click:
opcode(),identifier(),param0(),param1()— the raw menu entry fieldsconsume()— call this to swallow the click before the game processes it
ChatMessageEvent — read-only snapshot of a chat line:
type()— chat type (game message, public chat, clan, trade, etc.)name()— the speaker's display name (empty for server messages)message()— the chat text contentsender()— clan or friends chat channel name, if applicablegameTick()— the tick the message was received
VarbitChangedEvent — only fires when a value actually changes (no-op writes are filtered):
varbitId()— the varbit that changedoldValue(),newValue()— before and afterdelta()— convenience fornewValue() - oldValue()
ItemContainerChangedEvent — fires when a container's contents differ from the previous tick:
containerId()— the container ID (93 = inventory, 95 = bank, etc.)capacity()— total slot countitems()— vector of (itemId, quantity) pairsslot(i)— access a specific slot index
ScriptEvent — fires when a CS2 client script executes:
scriptId()— the script being runargs()— the arguments passed to the scriptresults()— the return values after execution
Comprehensive Lifecycle Example
This plugin tracks NPC spawns, reacts to chat messages, monitors inventory changes, and blocks certain menu clicks — showing several lifecycle callbacks working together:
class EventDemoPlugin : public titan::Plugin {
TITAN_PLUGIN_META("event_demo", "Event Demo",
"Demonstrates lifecycle callbacks.", "Dev", "1.0.0", false)
void onGameTick(int32_t tick) override {
if (titan::state::prayers().isActive(titan::Prayer::Protect_from_Melee)) {
titan::log("Melee prayer active");
}
}
void onNpcSpawned(const Npc& npc) override {
if (npc.name() == "Vorkath") {
titan::log("Vorkath phase started!");
}
}
void onChatMessage(const ChatMessageEvent& evt) override {
if (evt.message().find("You have been poisoned") != std::string::npos) {
titan::log("Poisoned! Consider drinking an antipoison.");
}
}
void onVarbitChanged(const VarbitChangedEvent& evt) override {
if (evt.varbitId() == 3955) {
titan::logf("Spec energy: {} -> {}", evt.oldValue(), evt.newValue());
}
}
void onItemContainerChanged(const ItemContainerChangedEvent& evt) override {
if (evt.containerId() == 93) {
titan::logf("Inventory updated ({} slots)", evt.capacity());
}
}
void onMenuOptionClicked(MenuClickEvent& evt) override {
if (evt.opcode() == titan::MenuAction::Id::ITEM_FIFTH_OPTION) {
evt.consume();
titan::log("Drop blocked by plugin.");
}
}
};
Queries (titan::queries::*)
Queries are chainable, filterable list views over live game collections. Import <titan/query.h>:
#include <titan/query.h>
// Find the nearest chicken to the local player
auto chicken = titan::queries::npcs()
.nameContains("Chicken")
.first();
if (chicken) {
chicken->interact("Attack");
}
// Count iron ore in inventory
int count = titan::queries::inventory()
.ids({440})
.totalQuantity();
| Factory | Returns | Key Filters |
|---|---|---|
npcs() | NpcQuery | id, ids, hasAction, interactingWith*, animation*, overheadActive, isDead/isAlive, withHealthBar, + locatable filters |
players() | PlayerQuery | excludingSelf, interactingWith*, animation*, isIdle, isSkulled, combatLevel*, isDead/isAlive, + locatable filters |
objects(radius) | ObjectQuery | id, ids, hasAction, ofType |
groundItems(radius) | GroundItemQuery | id, ids, minQuantity, maxQuantity, canLoot() (SDK v50+), + locatable filters |
inventory() | InventoryQuery | id, ids, slot, slotsBetween |
projectiles() | ProjectileQuery | spotAnim, targetingEntity, fromEntity, activeDuring, + locatable filters |
All queries support terminal operations: forEach, first, toVector,
count, any. Inventory query additionally exposes
totalQuantity() and exists().
State Facades (titan::state::*)
State facades provide read and write access to specific game subsystems. Import <titan/client.h>:
#include <titan/client.h>
// Read game state
int tick = titan::state::client().tick();
bool loggedIn = titan::state::client().loggedIn();
auto local = titan::state::client().localPlayer();
int energy = titan::state::client().runEnergy(); // 0-10000
int weightKg = titan::state::client().weight(); // signed kg
// Camera
float yaw = titan::state::camera().yaw();
// Skills
int hp = titan::state::skills().boosted(titan::Skill::Hitpoints);
// Vars
int questProgress = titan::state::vars().varbit(3456);
// Walk
titan::state::walk().toWorld(3200, 3200, 0);
// Invoke a menu action (returns true when queued for the next client tick)
bool queued = titan::state::client().invokeMenuAction(opcode, id, param0, param1);
| Factory | Highlights |
|---|---|
client() | tick(), plane(), loggedIn(), localPlayer(), runEnergy(), weight(), accountType(), isIronman()/isIronMan(), isGroupIronman()/isGroupIronMan(), invokeMenuAction(...) |
camera() | yaw(), pitch(), zoom(), posX()/posY()/posZ() |
skills() | boosted(Skill), real(Skill), experience(Skill) |
prayers() | isActive(Prayer) |
vars() | varbit(id), varp(id) |
walk() | toScene(sx, sy), toWorld(wx, wy, plane), to(Tile) |
widgets() | find(packedId), children(parentId), findByText(query), interact(...), setText(packedId, text) (SDK v51+) |
hider() | setPlayers(v), setNpcs(v), setSelf(v), setScene(v) |
cache() | item(id), npc(id), obj(id), varbit(id) |
idle() | remaining() (ms), reset() |
login() | isLoggedIn(), setUsername(...), setPassword(...) |
script() | run(id, args), runAndGetInt(id, args), questState(id) |
itemContainer(id) | Widget-backed container read. Returns std::optional. |
itemDef(id) | Runtime item definition with varbit transforms. |
collisions() | flag(plane,x,y), isBlocked(plane,x,y,dx,dy) |
Collision Map
titan::state::collisions() exposes the game's pathfinding collision flags for the current scene.
Useful for movement validation, custom pathfinding, and tile-level obstacle detection:
auto col = titan::state::collisions();
// Read the raw collision flag at a scene tile
uint32_t flag = col.flag(0, 50, 50);
// Check if movement from (x, y) in direction (dx, dy) is blocked
bool blocked = col.isBlocked(0, 50, 50, 1, 0); // moving east
The CollisionFlag namespace defines bitmask constants for walls, objects, floors, and
full-block tiles. Combine with bitwise AND to test specific obstacle types.
Cache Definitions
titan::state::cache() reads static definitions from the game cache. Returns
std::optional — empty if the ID doesn't exist:
auto cache = titan::state::cache();
if (auto item = cache.item(4151)) { // Abyssal whip
titan::logf("Item: {} (value={})", item->name, item->haValue);
}
if (auto npcDef = cache.npc(2042)) { // Zulrah
titan::logf("NPC: {} (combat={})", npcDef->name, npcDef->combatLevel);
}
if (auto objDef = cache.obj(11936)) { // Bank booth
titan::logf("Object: {}", objDef->name);
}
if (auto vb = cache.varbit(3955)) { // Spec energy varbit def
titan::logf("Varbit {} uses varp index {}", vb->id, vb->varpIndex);
}
Client Scripts
titan::state::script() executes CS2 client scripts at runtime. This is the same engine the
game uses for UI logic, so you can query computed values that aren't exposed as varbits:
auto script = titan::state::script();
// Fire-and-forget: run a script with integer args
script.run(2498, {1, 0});
// Run a script and read an int return value
int result = script.runAndGetInt(2498, 1, 0);
// Convenience: get quest completion state
// (0 = not started, 1 = in progress, 2 = complete)
int qs = script.questState(2);
Login State
titan::state::login() reads and controls the login screen. Useful for auto-login plugins
and session monitors:
auto login = titan::state::login();
auto snap = login.snapshot(); // full login state snapshot
int idx = login.index(); // login screen index
bool inGame = login.isLoggedIn();
// Credential setters (used by auto-login plugins)
login.setUsername("player@email.com");
login.setPassword("hunter2");
login.setAuthenticator("123456");
// OAuth2 / session token flows
login.setOAuth2Token("token_value");
login.setSessionId("session_id");
Idle Timer
titan::state::idle() tracks the client's idle logout timer. Call reset() to
prevent the 5-minute idle kick during automated tasks:
int ms = titan::state::idle().remaining();
titan::state::idle().reset();
World List & Hopping
titan::state::world is a namespace (not an instance method) that manages the world list
and hopping:
// Current world
auto current = titan::state::world::current();
titan::logf("World {} (members={}, activity={})",
current.id, current.members, current.activity);
// Enumerate all worlds
for (auto& w : titan::state::world::list()) {
if (w.members && w.playerCount < 500) {
titan::logf("Low-pop world: {}", w.id);
}
}
// Title-screen hop (must be on login screen)
titan::state::world::hop(302);
// In-game hop (drives the native world-switcher UI)
titan::state::world::hopIngame(330);
Each World struct contains: id, members, playerCount,
location, activity, and server address info.
Entity Hider
titan::state::hider() toggles rendering of entity categories. Hiding entities is purely visual
— they still exist in memory and appear in queries:
auto h = titan::state::hider();
h.setPlayers(true); // hide other players
h.setNpcs(true); // hide NPCs
h.setSelf(true); // hide the local player model
h.setScene(true); // hide scene objects (trees, rocks, etc.)
bool playersHidden = h.isPlayersHidden();
bool npcsHidden = h.isNpcsHidden();
bool selfHidden = h.isSelfHidden();
bool sceneHidden = h.isSceneHidden();
Item Containers
titan::state::itemContainer(id) returns a snapshot of a specific item container. Container IDs
are game constants (93 = inventory, 95 = bank, 94 = equipment):
if (auto inv = titan::state::itemContainer(93)) {
for (int i = 0; i < inv->capacity(); ++i) {
auto [itemId, qty] = inv->slot(i);
if (itemId > 0) {
titan::logf("Slot {}: item={} x{}", i, itemId, qty);
}
}
}
Item Definitions
titan::state::itemDef(id) returns a runtime item definition that resolves varbit-based
transformations (charged items, degraded states). Unlike cache().item(id) which returns the
raw cache entry, this reflects the item's current in-game state:
if (auto def = titan::state::itemDef(12931)) {
titan::logf("{}: noted={}, stackable={}", def->name, def->noted, def->stackable);
}
Utility Helpers (titan::utils::*)
Utils are header-only wrappers that compose queries and state facades into one-call APIs. No new ABI surface — pure convenience:
Magic catalog (SDK v49+): #include <titan/utils/magic.h> pulls in the
titan::utils::Magic nested namespace: spellbook enums, per-spell metadata, state predicates such as
canCast(...), and stubbed select/cast/castOn helpers that currently return
false (metadata is live; dispatch is not wired yet). Spellbook widget ids also live under
titan::InterfaceIds::MagicSpellbook in <titan/interface_ids.h>.
#include <titan/utils/inventory.h>
#include <titan/utils/dialogue.h>
#include <titan/utils/combat.h>
#include <titan/utils/equipment.h>
#include <titan/utils/bank.h>
#include <titan/utils/magic.h>
// Inventory
bool full = titan::utils::Inventory::isFull();
titan::utils::Inventory::drop("Iron ore");
// Dialogue
titan::utils::Dialogue::continueDialogue();
titan::utils::Dialogue::selectOption({"Yes"});
// Combat
titan::utils::Combat::enableSpecialAttack();
int spec = titan::utils::Combat::getSpecialAttackPercentage();
// Equipment
auto helm = titan::utils::Equipment::find(titan::EquipmentSlot::Head);
titan::utils::Equipment::unequip(titan::EquipmentSlot::Weapon);
// Bank
if (titan::utils::Bank::isOpen()) {
titan::utils::Bank::depositAll();
titan::utils::Bank::withdrawItem(itemId);
}
Keyboard Input (titan::keyboard::*)
The keyboard namespace simulates key input in the game window. Useful for chat automation, search boxes, and triggering hotkeys:
#include <titan/keyboard.h>
// Type a full string into the active text field (instant)
titan::keyboard::sendString("Hello world");
// Send a single key press with optional modifiers
titan::keyboard::sendKey(VK_RETURN, 0);
titan::keyboard::sendKey('A', titan::keyboard::MOD_SHIFT);
// Human-like typing with configurable delay between keystrokes
titan::keyboard::typeString("Buying rune scimitar 30k", {
.minDelay = 30,
.maxDelay = 80
});
// Cancel an in-progress typeString
titan::keyboard::cancelTypeString();
// Check if a typeString operation is still in progress
bool busy = titan::keyboard::isTyping();
sendString is instant — it sets the text field contents in one go. typeString
simulates individual keystrokes with randomized delays and is cancellable mid-sequence.
Geometry Types
The SDK provides value types for coordinates and spatial math. These appear throughout entity APIs, queries, and movement functions:
| Type | Description | Key Members |
|---|---|---|
Tile |
A world tile (absolute coordinates + plane). The primary coordinate type for game entities. | x(), y(), plane(), distanceTo(other) |
WorldPoint / WorldPos |
Alias for absolute world coordinates. Interchangeable with Tile in most APIs. |
x, y, plane |
LocalPoint |
Sub-tile scene coordinates (128 units per tile). Used for precise positioning and animation. | x(), y(), distanceTo(other) (Euclidean) |
WorldArea |
An axis-aligned rectangle of tiles with plane. Used for zone checks and proximity tests. | contains(tile), distanceTo(tile), distanceTo(area) |
ScreenPoint |
A pixel coordinate on the game viewport. Returned by world-to-screen projection. | x, y |
Tile myTile(3200, 3200, 0);
auto local = titan::state::client().localPlayer();
if (local) {
int dist = local->tile().distanceTo(myTile);
titan::logf("Distance to target: {} tiles", dist);
}
Menu Actions
Menu actions represent clickable options in the right-click menu. The MenuAction::Id enum
defines the major opcode families:
| Opcode Family | Description |
|---|---|
NPC_FIRST .. NPC_FIFTH | Right-click NPC options (Attack, Talk-to, Pickpocket, etc.) |
OBJECT_FIRST .. OBJECT_FIFTH | Right-click object options (Chop, Mine, Open, etc.) |
PLAYER_FIRST .. PLAYER_EIGHTH | Right-click player options (Follow, Trade, Report, etc.) |
GROUND_ITEM_FIRST .. GROUND_ITEM_FIFTH | Ground item options (Take, Examine, etc.) |
WIDGET_* | Interface interactions (buttons, item-on-item, spell targets) |
WALK | Walk-here click |
CANCEL | Cancel the menu |
EXAMINE_NPC, EXAMINE_OBJECT, EXAMINE_ITEM | Examine actions |
The MenuAction::Entry struct holds a complete menu option: opcode, target identifier,
and widget parameters. You can build entries manually and invoke them through the client facade:
// Invoke a menu action directly (bypasses the right-click menu)
bool queued = titan::state::client().invokeMenuAction(
titan::MenuAction::Id::NPC_FIRST, // opcode
npcIndex, // identifier (NPC scene index)
0, // param0
0 // param1
);
In practice, prefer the high-level interact() methods on entity objects — they build
the correct MenuAction::Entry for you. Direct invokeMenuAction is useful when
you need to forge custom entries or interact with widget elements programmatically.
Plugins Facade (titan::plugins())
titan::plugins() returns a PluginsFacade that lets your plugin discover and
interact with other loaded plugins:
auto pf = titan::plugins();
// List all loaded plugins
for (auto& p : pf.all()) {
titan::logf("Plugin: {} (enabled={})", p.id(), p.isEnabled());
}
// Look up a specific plugin by ID (throws if not found)
auto other = pf.get("ground_markers");
titan::logf("Ground Markers version: {}", other.version());
// Safe lookup (returns std::optional)
if (auto found = pf.find("ground_markers")) {
found->setEnabled(true);
}
// Get a reference to your own plugin descriptor
auto me = pf.self();
titan::logf("I am: {} v{}", me.name(), me.version());
This is useful for soft dependencies — check whether a companion plugin is loaded before using shared state, or build a “plugin manager” UI that toggles other plugins.
Overlay panels (titan::OverlayPanel, SDK v46+)
For structured in-game HUD panels (titles, lines, progress bars, anchor layout, Alt-drag repositioning),
include <titan/overlay_panel.h> and register panels from your constructor with
overlayPanel(name, titan::Anchor::..., [priority,] lambda). These extend titan::Overlay
but always dispatch on Layer::AboveWidgets. See Panels & Overlays
for examples and the full Anchor list.
Threading Model
All plugin callbacks run on the game thread unless otherwise noted. This means you can safely call
titan::* queries and state reads from any callback without synchronization:
| Callback | Thread | Queries safe? | Notes |
|---|---|---|---|
onGameTick, onClientTick | Game | Yes | Primary logic hooks |
| Entity spawn/despawn events | Game | Yes | |
onMenuOptionClicked, onChatMessage, etc. | Game (inside detour) | Yes | Avoid heavy work; dispatch via runOnClientTick |
Overlay onRender callbacks | Render | Yes for reads | Dispatch mutations via runOnClientTick |
buildPanel, onPanelAction | Controller process | No | Pure UI; no titan::* queries |
onEnable, onDisable | Game | Yes |
Re-entry protection: The chat and varbit hooks use thread-local depth guards. Calling
titan::addChatMessage from inside onChatMessage will not recursively
re-fire your own handler.
Error Handling
- Plugins must not throw exceptions across the C ABI boundary. The host wraps each callback in a try/catch and disables the plugin on fatal errors.
- Use
titan::log/titan::logffor diagnostics — output appears in the controller's log tab. - Functions that may fail return
boolorstd::optional<T>in the fluent facades.