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.

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.
| Function | Returns |
|---|---|
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:
| Field | Type | Description |
|---|---|---|
index | integer | Entity index |
ptr | integer | Pointer value for the entity. row:get reads through it, and glow.set takes it |
handle | integer | Entity handle. Stable while the entity lives |
class | string | The class name exactly as you passed it to by_class or watch |
class_name | string or nil | The raw C++ class name, present only when known |
x, y, z | number | World position |
position | vec3 | The same position as a vec3, ready to hand to a 3D draw call |
team | integer | Team number |
health | integer | Current HP |
max_health | integer | Maximum HP |
owner_index | integer | Index of the owning entity, -1 for none |
distance_m | number | Distance from the local player, in meters |
alive_seq | integer | The 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 counts | How |
|---|---|
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 removedCalls 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.
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:
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.
| Field | Meaning |
|---|---|
wants | Classes currently being read |
refused | Calls turned away because 8 classes were already wanted |
capped | Entities dropped past the 64 per class limit |
stale | Rows dropped because the entity slot was reused by another entity |
added, removed | Entity add and remove counts |
events_delivered, events_pending | Delivered 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:
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
endBoth 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:
| Field | Type | Description |
|---|---|---|
type | string | Curated entity type (see list below). Never empty. Anything outside the curated set arrives as "entity" (parsed but unclassified) or "unknown" |
class_name | string | Engine class name (e.g. npc_neutral_sinners_sacrifice, citadel_gravestone_blocker) |
x, y, z | number | World position |
health | integer | Current HP (if applicable) |
max_health | integer | Maximum HP (if applicable) |
team | integer | Team number. 0=none, 1=spectator, 2=amber, 3=sapphire, 4=neutral (jungle, props) |
distance | number | Distance from the local player, in meters |
currency_value | integer | Soul value (for dropped souls) |
pickup_name | string or nil | Pickup identifier, only present on powerup, pickup_modifier, pickup_gold, and pickup_health types. See Pickup entities. |
active | boolean | For 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. |
subclass | integer | Tells 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_in | number | rejuv 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_taken | boolean | rejuv rows only: true once it has been taken |
rejuv_hits | integer | rejuv 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
| Type | Description |
|---|---|
trooper | Lane creeps |
trooper_boss | Boss / super troopers |
camp | Jungle camp neutrals and bugs |
sinner | Sinner sacrifice camps |
tower | Guardians, walkers, shrines |
midboss | Mid boss |
soul_orb | Denial/last-hit soul orbs |
soul_urn | Soul urn pickups |
dropped_souls | Dropped souls from kills |
powerup | Map powerups (casting, movement, survival, gun) |
pickup_gold | Small gold boxes |
pickup_health | Health pickups |
pickup_modifier | Stat modifier pickups (hp, wp, cd, spirit, firerate) |
guided_owl | Grey Talon guided arrow projectile |
doorman_cart | Doorman luggage cart projectile |
necro_ghoul | Graves' ghouls |
necro_skull | Graves' floating skull |
rift | The Rift spawner |
familiar_helper | Rem's Helping Hands |
rejuv | The Rejuvenator shard |
trap | Grey Talon's and Venator's traps |
healing_snack | A Healing Snack pickup |
chess_piece | A chess piece placed on the map by a hero's ability. It blocks line of sight |
shop | Shop trigger volumes |
entity | Unclassified entities (props, spawners, items on ground) |
unknown | Anything 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_name | Buff |
|---|---|
survival_powerup_pickup | Survival |
casting_powerup_pickup | Casting |
movement_powerup_pickup | Movement |
gun_powerup_pickup | Gun |
Modifier variants (type "pickup_modifier", permanent stat bonuses from breakable props):
pickup_name | Stat |
|---|---|
hp_permanent_pickup_label | Max health |
wp_permanent_pickup_label | Weapon damage |
cd_permanent_pickup_label | Cooldown reduction |
spirit_permanent_pickup_label | Spirit power |
firerate_permanent_pickup_label | Fire rate |
ammo_permanent_pickup_label | Ammo |
Names ending in _lv2 are upgraded versions of the same stat.
Basic powerup ESP example. This labels the four powerup variants by name:
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
endFor a fuller version that handles all four pickup types (powerups, modifiers, gold, health) with color-coded categories, see Powerup ESP.
Examples
-- Total count
local all = get_world_entities()
print("Total entities: " .. #all)-- 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-- 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-- 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-- 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-- 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
endentity.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:
| Name | Works |
|---|---|
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 typo | No |
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_ratewith"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:
-- 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).
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.
for _, c in ipairs(entities.camps() or {}) do
if not c.alive then
-- not on the map: cleared, or being fought
end
end| Field | Meaning |
|---|---|
key | Stable id for the camp, the same across sessions |
kind | "small", "medium", "large", "casino" (the Sinner's Sacrifice) or "super" |
gx, gy | Map grid cell, 0 to 255. Pass to hud.minimap_grid_to_screen |
x, y, z, position | World position of the camp centre |
alive | Whether the game currently shows it on the map |
n_alive, n_total | n_total is always 1, and n_alive is 1 while the camp shows on the map. They do not count units, so use alive |
type | The camp kind as a number, 34 (small) to 38 (super). kind is the same thing as a name |
seq | Goes up each time the camp list is refreshed |
health_pct | Camp health as a percentage |
lane | Which lane it belongs to |
ent | Entity index |
distance_m | Distance 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.
| Type | Default Rate | Notes |
|---|---|---|
trooper, trooper_boss | Per-frame when trooper ESP/aimbot is on, otherwise slow | High count (~100+) |
soul_orb | Per-frame when soul aimbot/triggerbot is on, otherwise slow | High count |
guided_owl | Per-frame | Always fast when present |
sinner | 200 ms | Always on |
tower | 200 ms | Always on |
midboss | 200 ms | Always on |
camp | 200 ms | Always on |
powerup | 200 ms | Always on |
pickup_gold | 200 ms | Always on |
pickup_health | 200 ms | Always on |
pickup_modifier | 200 ms | Always on |
dropped_souls | 200 ms | Always on |
soul_urn | 200 ms | Always on |
doorman_cart | 200 ms | Always on |
necro_ghoul, necro_skull | Per-frame when trooper ESP/aimbot is on, otherwise slow | |
familiar_helper | 200 ms | Always on |
rift | ~1 second | Always on |
rejuv | 200 ms | While the world ESP and its Rejuvenator toggle are on. Otherwise only when a script sets a rate for it, at that rate |
trap | 200 ms | While 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 second | While 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_piece | 200 ms | Always on |
entity | ~1 second | Static 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.