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, and everything else is in the entity list.

Particles and modifiers can also tell you things, but for ESP or detection, start with the entity list.

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. Turn the ESP on to find the exact class name of the entity you want to script, 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. When it is 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 costs performance.

entities.by_class ​

get_world_entities returns a curated list, filtered by type name, with the pickup fields. entities.by_class returns every entity of one class by name, only for classes you ask for. 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
ptrintegerPointer value for the entity. row:get reads through it, and glow.set takes it
handleintegerEntity handle. Stable while the entity lives
classstringThe class name exactly as you passed it to by_class or watch
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

Rows also have one method, row:get(name), which reads a schema field from 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 there is no answer yet. The first call for a new class always returns nil, and the rows arrive a tick or two later. {} means the answer is in and none are 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.

New entities show up within about 100 ms ​

While something is asking for a class, a new entity of that class appears within about 100 ms of spawning. Two things count as asking:

What countsHow
entities.by_class(name)you called it in the last 2 seconds
entity.set_rate(name, "fast")a "fast" override is active

With neither, a new entity of that class can take up to about a second to show up.

You can ask for a class before anything of it exists. entities.by_class("some_projectile") returns {} until one spawns, then the row appears on the next call.

Limits ​

You can ask for at most 8 classes at once, counted across every script. entities.camps() and TSUKI's own camp ESP, minimap timers and camp notifications share one of them while they are on. A ninth returns nil, and a watch on a ninth class gets no events until a slot frees up. Both write a line to C:\TSUKI\Deadlock\logs\tsuki.log, not to the console. A slot frees up when nothing has asked for a class for 2 seconds. At most 64 entities per class are read. The rest are skipped, 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 also registers you for on_entity_added and on_entity_removed on that class. See Events.

Watching a class ​

entities.watch(class, fn)   -> true
entities.watch(class, nil)  -> true if a watch was removed

Calls fn(event, row) whenever an entity of that class appears or goes away. Calling watch again for the same class replaces the callback. event is "added" or "removed". You do not need to call by_class every tick to keep the events coming. The watch does that for you.

lua
entities.watch("npc_trooper", function(event, row)
    if event == "added" then print("trooper", row.index, "at", row.distance_m, "m") end
end)

A watch counts toward the 8 classes you can ask for at once. "added" means added since you started watching. Entities that already exist when you start do not fire, so start the watch before the thing you want to see appears.

A watch is cheaper than calling by_class every tick.

Your callback can change watches, including its own. A one-shot watch stops itself:

lua
entities.watch("citadelprojectile_immobilizetrap", function(event, row)
    entities.watch("citadelprojectile_immobilizetrap", nil)   -- only the first one
    print("trap", event, "at", row.distance_m, "m")
end)

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 instead of counts.

entities.diag() ​

Returns counters only, 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 built 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_namestringEngine 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. Several pickup entities can stack at the same position, and only one is active at a time.
subclassintegerTells apart 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).
rejuv_innumberrejuv rows only: seconds until the Rejuvenator can be taken. Above 0 while it is still dropping in, 0 or below once it can be taken. -1 when not known and on every other row, so check type first
rejuv_takenbooleanrejuv rows only: true once it has been taken
rejuv_hitsintegerrejuv rows only: punches the crystal has left, 3 to 0, or -1 when not known. Other rows always read 3, so check type first

This class_name is not the same field as class_name on an entities.by_class row. The two kinds of row are not interchangeable, so pick one 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
necro_ghoulGraves' ghouls
necro_skullGraves' floating skull
riftThe Rift spawner
familiar_helperRem's Helping Hands
rejuvThe Rejuvenator shard
trapGrey Talon's and Venator's traps
healing_snackA Healing Snack pickup
chess_pieceA chess piece placed on the map by a hero's ability. It blocks line of sight
shopShop trigger volumes
entityUnclassified entities (props, spawners, items on ground)
unknownAnything that was parsed but could not be classified, including the engine classes added by Include All Entity Types

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

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

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 so you don't draw duplicate labels at the same spot.

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. This 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 by 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) ​

Sets how often the positions of one entity type are updated. Each type has its own default rate, listed under Default update rates below. Use it when your script needs a type updated faster.

Arguments:

  • type (string). A type name or a designer name. Not every name works:
NameWorks
A type name: "sinner", "soul_orb", "trooper", "camp"Yes
A designer name: "npc_neutral_sinners_sacrifice", "citadel_punchablepowerup"Yes
A C++ class name with no designer name, such as "ability_slide"No. Use entities.by_class
A filtered class such as "env_sky" or "zip_line", or a typoNo

A name that never matches anything logs one warning to the Lua console. Names that match, and type names, never log a warning.

  • tier (string). One of:
    • "fast". Updated every tick, about every 4 ms. 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 only need to know exist.
    • "default". Goes back to the 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 make an entity update faster than its default, not slower. For example, sinners default to "medium", so setting them to "slow" has no effect.
  • Overrides last until you call set_rate with "default", or your script is turned off, errors or reloads.
  • Multiple scripts can set different rates for the same type. The fastest rate wins, and "default" only removes your own script's override.

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 fastest rate any script has set for a name, or "default" when none has. It does not tell you whether the name matched anything.

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"

entities.camps() ​

The neutral camps the game tracks for your team, from the same list the minimap uses. Returns an array, {} when there are none, or nil until the first read answers.

lua
for _, c in ipairs(entities.camps() or {}) do
    if not c.alive then
        -- not on the map: cleared, or being fought
    end
end
FieldMeaning
keyStable id for the camp, the same across sessions
kind"small", "medium", "large", "casino" (the Sinner's Sacrifice) or "super"
gx, gyMap grid cell, 0 to 255. Pass to hud.minimap_grid_to_screen
x, y, z, positionWorld position of the camp centre
aliveWhether the game currently shows it on the map
n_alive, n_totaln_total is always 1, and n_alive is 1 while the camp shows on the map. They do not count units, so use alive
typeThe camp kind as a number, 34 (small) to 38 (super). kind is the same thing as a name
seqGoes up each time the camp list is refreshed
health_pctCamp health as a percentage
laneWhich lane it belongs to
entEntity index
distance_mDistance from you in meters

If a camp disappears from the list, treat it as cleared. alive also reads false while the camp is being fought, and stays true after a Sinner's Sacrifice is finished, so check that camp's vault_state instead.

name is empty for camps

The name field exists, but the game leaves it blank on every camp. Use kind and the grid cell as the key instead. They are fixed for a given map.

The grid cell is exact. The world position gets more accurate once the game has shown you troopers resting at the camp. Before that, z is 0 and x/y come from the grid, which is within about 150 units of the real spot.

Camps are read only while a script is asking for them, at most four times a second.

For the Sinner's Sacrifice mini-game, entities.by_class("C_NPC_Neutral_SinnersSacrifice") rows carry vault_state along with health and max_health. 2 means the mini-game is up and 4 means it is finished.

Default update rates ​

Each type has its own default rate. Call set_rate only when you need a type faster than this.

TypeDefault RateNotes
trooper, trooper_bossPer-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
necro_ghoul, necro_skullPer-frame when trooper ESP/aimbot is on, otherwise slow
familiar_helper200 msAlways on
rift~1 secondAlways on
rejuv200 msWhile the world ESP and its Rejuvenator toggle are on. Otherwise only when a script sets a rate for it, at that rate
trap200 msWhile the world ESP and its Traps toggle are on. Otherwise only when a script sets a rate for it, at that rate
healing_snack~1 secondWhile the world ESP and its Healing Snacks toggle or Healing Snacks event timer are on. Otherwise only when a script sets a rate for it, at that rate
chess_piece200 msAlways on
entity~1 secondStatic entities (props, items)

How fresh the positions in get_world_entities() are depends on the rate. Per-frame entities are at most one tick old, 200 ms entities up to about 200 ms old, and slow entities up to about 1 s old. Static entities like props and items never move, so for them the age doesn't matter.