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_PLUGIN or TITAN_PLUGIN_META macros
  • 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

VirtualWhen 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

VirtualCadenceDescription
onClientTick()~every frameFires on every client tick (high frequency). Use for responsive state checks.
onGameTick(int32_t tick)~600msFires 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:

SpawnDespawn
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

VirtualDescription
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 fields
  • consume() — 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 content
  • sender() — clan or friends chat channel name, if applicable
  • gameTick() — the tick the message was received

VarbitChangedEvent — only fires when a value actually changes (no-op writes are filtered):

  • varbitId() — the varbit that changed
  • oldValue(), newValue() — before and after
  • delta() — convenience for newValue() - 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 count
  • items() — vector of (itemId, quantity) pairs
  • slot(i) — access a specific slot index

ScriptEvent — fires when a CS2 client script executes:

  • scriptId() — the script being run
  • args() — the arguments passed to the script
  • results() — 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();
FactoryReturnsKey Filters
npcs()NpcQueryid, ids, hasAction, interactingWith*, animation*, overheadActive, isDead/isAlive, withHealthBar, + locatable filters
players()PlayerQueryexcludingSelf, interactingWith*, animation*, isIdle, isSkulled, combatLevel*, isDead/isAlive, + locatable filters
objects(radius)ObjectQueryid, ids, hasAction, ofType
groundItems(radius)GroundItemQueryid, ids, minQuantity, maxQuantity, canLoot() (SDK v50+), + locatable filters
inventory()InventoryQueryid, ids, slot, slotsBetween
projectiles()ProjectileQueryspotAnim, 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);
FactoryHighlights
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:

TypeDescriptionKey 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 represent clickable options in the right-click menu. The MenuAction::Id enum defines the major opcode families:

Opcode FamilyDescription
NPC_FIRST .. NPC_FIFTHRight-click NPC options (Attack, Talk-to, Pickpocket, etc.)
OBJECT_FIRST .. OBJECT_FIFTHRight-click object options (Chop, Mine, Open, etc.)
PLAYER_FIRST .. PLAYER_EIGHTHRight-click player options (Follow, Trade, Report, etc.)
GROUND_ITEM_FIRST .. GROUND_ITEM_FIFTHGround item options (Take, Examine, etc.)
WIDGET_*Interface interactions (buttons, item-on-item, spell targets)
WALKWalk-here click
CANCELCancel the menu
EXAMINE_NPC, EXAMINE_OBJECT, EXAMINE_ITEMExamine 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:

CallbackThreadQueries safe?Notes
onGameTick, onClientTickGameYesPrimary logic hooks
Entity spawn/despawn eventsGameYes
onMenuOptionClicked, onChatMessage, etc.Game (inside detour)YesAvoid heavy work; dispatch via runOnClientTick
Overlay onRender callbacksRenderYes for readsDispatch mutations via runOnClientTick
buildPanel, onPanelActionController processNoPure UI; no titan::* queries
onEnable, onDisableGameYes

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::logf for diagnostics — output appears in the controller's log tab.
  • Functions that may fail return bool or std::optional<T> in the fluent facades.