Skip to content

Entities

Entities are everything in the world that is not a player: troopers, jungle camps, the midboss, crates, powerups, gravestones, skeletons, projectiles. Players have their own accessor; everything else is in the entity list.

You can also approach detection through particles, modifiers, or other systems, but checking the entity list is usually the first thing to try when designing a script, especially anything ESP or detection-related.

Discovery: Show All Entities ESP

TSUKI ships with a dev tool that overlays every entity it currently sees with its class name, health, and distance. Open Dev Studio from the bug icon in the pill above the menu, go to World, and turn on Show All Entities on ESP.

Show All Entities on ESP overlaying entity labels in game

In the screenshot above, Graves' Gravestone ultimate shows up as citadel_gravestone_blocker, and the skeletons it spawns are npc_necro_skele. The breakable props next to the gravestone are citadel_breakableprop. Calico's cat is npc_neutral_hideout_cat, and the souls dropped on the floor are citadel_pickup_gold. Toggle the ESP on to discover the exact class name for whatever entity you're trying to script around, then filter get_world_entities() by that class name.

Include All Entity Types

Include All Entity Types (below the ESP toggle, off by default) makes get_world_entities() also return engine entities such as env_sky, zipline nodes, func_brush and trigger volumes. Off, it returns only the curated types in the table below plus projectiles, gravestones and breakables.

Leave it off unless a script needs one of those classes. Scanning the full entity list every frame has measurable overhead, which is why the curated parse is the default.

entities.by_class

get_world_entities returns a curated list filtered by type name and carries the pickup fields. entities.by_class returns every entity of one class by name, only for classes you ask for, and its rows can read a game field by name through the internal.

entities.classes() maps every class alive right now to a count. Use it to find the spelling to pass to by_class.

FunctionReturns
entities.by_class(class_name)Array of row tables, {}, or nil
entities.classes(){[class_name] = count} of everything alive, or nil
entities.diag()Counters, or nil

All three work in any script.

Row fields

Each element of the array by_class returns is a row table with these fields:

FieldTypeDescription
indexintegerEntity index
ptrintegerOpaque pointer value for the entity. row:get reads through it, scripts have no other use for it
handleintegerEntity handle. Stable while the entity lives
classstringThe class you asked for, in its normalised form
class_namestring or nilThe raw C++ class name, present only when known
x, y, znumberWorld position
positionvec3The same position as a vec3, ready to hand to a 3D draw call
teamintegerTeam number
healthintegerCurrent HP
max_healthintegerMaximum HP
owner_indexintegerIndex of the owning entity, -1 for none
distance_mnumberDistance from the local player, in meters
alive_seqintegerThe refresh this row came from

Plus one method, row:get(name), which reads a schema field off the entity behind the row.

row:get needs the internal

Every field above works with nothing loaded. row:get needs the internal running and needs_internal = true in script_info, and returns nil without them. See Entity Properties.

A row is a plain table with one method. It has no bones, modifiers or visibility.

nil and an empty table mean different things

nil means no answer yet: the first call for a new class always returns nil, and the rows arrive a tick or two later. {} means answered and none alive. by_class(c) or {} hides the difference.

Asking is what keeps a class alive

A class is read only while something asks for it. After 2 seconds without a call it is dropped. Call by_class every tick while you need it.

Limits

At most 8 classes at once. A ninth returns nil and logs once; a slot frees when a class idles for 2 seconds. At most 64 entities per class; the rest are not read, and entities.diag().capped counts them.

Class names are normalised

A leading CCitadel_ or C_ is stripped and the rest lowercased, so C_CitadelPlayerPawn, CCitadelPlayerPawn and citadelplayerpawn are the same class, and C_NPC_Trooper is npc_trooper.

Rows are reused and shared

Row tables and the array are reused between calls and shared between scripts. Copy the values you need; do not modify a row or keep it across ticks.

Entity events

Calling by_class on a class is also what registers you for on_entity_added and on_entity_removed on that class. See Events.

Counts differ from the player list

The pawn class sees every pawn in the match; get_all_players() holds only the teams your ESP shows. Compare positions, not counts.

entities.diag()

Counters, no game values.

FieldMeaning
wantsClasses currently being read
refusedCalls turned away because 8 classes were already wanted
cappedEntities dropped past the 64 per class limit
staleRows dropped because the entity slot was reused by another entity
added, removedEntity add and remove counts
events_delivered, events_pendingDelivered and queued entity events

Example

A killsteal marker from a class query and each trooper's health. my_team, max_m, limit and COL are set earlier in the script:

lua
local rows = entities.by_class("npc_trooper")
if rows then
    for _, e in ipairs(rows) do
        if e.team ~= my_team and e.health > 0 and e.distance_m <= max_m then
            if e.health <= limit then
                draw.circle3d(e.position, 14, COL, 2, 20)
                draw.text3d(e.position, string.format("KS %d hp", e.health), COL)
            end
        end
    end
end

Both draw calls last one tick, so this belongs in on_tick. To read a game field off a row, see Entity Properties.

get_world_entities([filter])

Returns a table of all world entities currently tracked. Optionally filter by a curated type name.

Arguments:

  • filter (string, optional). Matches either the curated type name ("sinner") or the exact engine class name ("citadel_gravestone_blocker"). The match is exact and case-sensitive. There is no substring matching.

Returns: Table of entity tables, each with these fields:

FieldTypeDescription
typestringCurated entity type (see list below). Never empty. Anything outside the curated set arrives as "entity" (parsed but unclassified) or "unknown"
class_namestringInternal RTTI class name (e.g. npc_neutral_sinners_sacrifice, citadel_gravestone_blocker)
x, y, znumberWorld position
healthintegerCurrent HP (if applicable)
max_healthintegerMaximum HP (if applicable)
teamintegerTeam number. 0=none, 1=spectator, 2=amber, 3=sapphire, 4=neutral (jungle, props)
distancenumberDistance from the local player, in meters
currency_valueintegerSoul value (for dropped souls)
pickup_namestring or nilPickup identifier, only present on powerup, pickup_modifier, pickup_gold, and pickup_health types. See Pickup entities.
activebooleanFor pickup entities, whether this variant is currently spawned and collectible. Multiple pickup entities can stack at the same position, only one is active at a time.
subclassintegerm_nSubclassID - distinguishes entities that share the same class name. For example, golden statues and soul crates are both citadel_breakableprop but have different subclass values (0xDDAC9D93 and 0xEDA33BFB respectively).

This class_name is not the same field as class_name on an entities.by_class row. The two rows are produced by different readers and are not interchangeable, so pick one of the two and stay with it.

Curated types

TypeDescription
trooperLane creeps
trooper_bossBoss / super troopers
campJungle camp neutrals and bugs
sinnerSinner sacrifice camps
towerGuardians, walkers, shrines
midbossMid boss
soul_orbDenial/last-hit soul orbs
soul_urnSoul urn pickups
dropped_soulsDropped souls from kills
powerupMap powerups (casting, movement, survival, gun)
pickup_goldSmall gold boxes
pickup_healthHealth pickups
pickup_modifierStat modifier pickups (hp, wp, cd, spirit, firerate)
guided_owlGrey Talon guided arrow projectile
doorman_cartDoorman luggage cart projectile
shopShop trigger volumes
entityUnclassified entities (props, spawners, items on ground)
unknownAnything the scanner parsed but could not classify, notably the exotic engine classes surfaced by Include All Entity Types

You can filter entities outside the curated list by class name directly: get_world_entities("citadel_gravestone_blocker"). This covers gravestones, breakable props, ability projectiles, and skeletons. Use the no-argument form to discover class names, not when you already know the one you want.

When the local player position isn't valid yet (spectating, pre-spawn, between rounds) get_world_entities() returns an empty table. If you got an entity back, distance is always a valid number. No nil check needed.

Pickup entities

Entities of type powerup, pickup_modifier, pickup_gold, and pickup_health carry the extra pickup_name and active fields. Each powerup spawn location has four entities stacked at the same position, one per buff variant, and only one is active at a time. Always filter by active to avoid rendering duplicate labels at the same world point.

Powerup variants (type "powerup", temporary buffs from bridge spawn points):

pickup_nameBuff
survival_powerup_pickupSurvival
casting_powerup_pickupCasting
movement_powerup_pickupMovement
gun_powerup_pickupGun

Modifier variants (type "pickup_modifier", permanent stat bonuses from breakable props):

pickup_nameStat
hp_permanent_pickup_labelMax health
wp_permanent_pickup_labelWeapon damage
cd_permanent_pickup_labelCooldown reduction
spirit_permanent_pickup_labelSpirit power
firerate_permanent_pickup_labelFire rate
ammo_permanent_pickup_labelAmmo

Names ending in _lv2 are upgraded versions of the same stat.

Basic powerup ESP example, labels the four powerup variants by name:

lua
function on_tick()
    for _, p in ipairs(get_world_entities("powerup")) do
        if p.active and p.distance < 200 then
            local sp = draw.world_to_screen(vec3(p.x, p.y, p.z))
            if sp then
                local label = "POWERUP"
                if p.pickup_name then
                    if p.pickup_name:find("survival") then label = "SURVIVAL"
                    elseif p.pickup_name:find("casting") then label = "CASTING"
                    elseif p.pickup_name:find("movement") then label = "MOVEMENT"
                    elseif p.pickup_name:find("gun") then label = "GUN"
                    end
                end
                draw.text(sp.x, sp.y, label, draw.color(255, 0, 255))
            end
        end
    end
end

For a fuller version that handles all four pickup types (powerups, modifiers, gold, health) with color-coded categories, see Powerup ESP.

Examples

lua
-- Total count
local all = get_world_entities()
print("Total entities: " .. #all)
lua
-- All sinners with distance
for _, s in ipairs(get_world_entities("sinner")) do
    print("Sinner at " .. s.x .. ", " .. s.y .. " - " .. s.distance .. "m away")
end
lua
-- Mark nearby powerups on screen
for _, p in ipairs(get_world_entities("powerup")) do
    if p.distance < 30 then
        local sp = draw.world_to_screen(vec3(p.x, p.y, p.z))
        if sp then
            draw.text(sp.x, sp.y, "POWERUP", draw.color(255, 255, 0, 255))
        end
    end
end
lua
-- Show camp HP bars
for _, c in ipairs(get_world_entities("camp")) do
    if c.health > 0 and c.distance < 100 then
        local sp = draw.world_to_screen(vec3(c.x, c.y, c.z))
        if sp then
            draw.text(sp.x, sp.y, c.health .. "/" .. c.max_health, draw.color(120, 120, 255, 255))
        end
    end
end
lua
-- Gravestone marker (filter straight on the class name)
for _, e in ipairs(get_world_entities("citadel_gravestone_blocker")) do
    draw.box3d(vec3(e.x, e.y, e.z), 50, 50, 80, draw.color(0, 255, 0, 255), 2)
end
lua
-- Discover unknown entity classes
local seen = {}
function on_tick()
    for _, e in ipairs(get_world_entities()) do
        if not seen[e.class_name] then
            seen[e.class_name] = true
            print("class:", e.class_name)
        end
    end
end

entity.set_rate(type, tier)

Controls how frequently entity positions are updated. Entities have default update rates based on their type; see the table below. Use this to promote entities to faster updates when your script needs precise, real-time tracking.

Arguments:

  • type (string). Entity type name (e.g. "sinner", "powerup") or internal class name (e.g. "npc_neutral_sinners_sacrifice", "citadel_punchablepowerup").
  • tier (string). One of:
    • "fast". Updated every frame (~16ms). Use for time-critical features like custom parry or precision aiming.
    • "medium". Updated every 200ms. Good for ESP or general tracking.
    • "slow". Updated every ~1 second. For entities you just need to know exist.
    • "default". Revert to the system's default rate for this type.

Returns: nothing.

An unrecognised tier raises a Lua error instead of being ignored: entity.set_rate: tier must be 'fast', 'medium', 'slow', or 'default'. A typo aborts the calling script unless you wrap the call in pcall. Both arguments must be strings.

Notes:

  • You can only promote entities to faster speeds, not demote below their default. For example, sinners default to "medium", setting them to "slow" has no effect.
  • Overrides persist until you call set_rate with "default" or the script is unloaded.
  • Multiple scripts can set different rates for the same type. The fastest rate wins.

Examples:

lua
-- Make powerup positions update every frame for precision tracking
entity.set_rate("powerup", "fast")

-- Revert to default
entity.set_rate("powerup", "default")

-- For a custom auto-parry against a specific projectile, promote it to per-frame
entity.set_rate("citadel_punchablepowerup", "fast")

entity.get_rate(type)

Returns the current Lua rate override for an entity type.

Arguments:

  • type (string). Entity type name or class name.

Returns: string: "fast", "medium", "slow", or "default" (no override set).

lua
print(entity.get_rate("sinner"))  -- "default"
entity.set_rate("sinner", "fast")
print(entity.get_rate("sinner"))  -- "fast"
entity.set_rate("sinner", "default")
print(entity.get_rate("sinner"))  -- "default"

Default update rates

Entities update at different speeds depending on their importance. You don't need to call set_rate unless you need faster updates than the defaults.

TypeDefault RateNotes
trooperPer-frame when trooper ESP/aimbot is on, otherwise slowHigh count (~100+)
soul_orbPer-frame when soul aimbot/triggerbot is on, otherwise slowHigh count
guided_owlPer-frameAlways fast when present
sinner200 msAlways on
tower200 msAlways on
midboss200 msAlways on
camp200 msAlways on
powerup200 msAlways on
pickup_gold200 msAlways on
pickup_health200 msAlways on
pickup_modifier200 msAlways on
dropped_souls200 msAlways on
soul_urn200 msAlways on
doorman_cart200 msAlways on
entity~1 secondStatic entities (props, items)

Position freshness in get_world_entities() depends on the rate, per-frame entities have positions ~16 ms old, 200 ms entities up to ~200 ms old, slow entities up to ~1 s old. For static entities like props and items, the position never changes so staleness doesn't matter.