game
Live data (read-only, writes raise a Lua error). Accessing a field = the latest published snapshot for the current frame.
Update strategy
The field tables carry an Update strategy column — not every field is read from the game each frame. The backend re-paces reads by state, and a skipped frame reuses the previous value. Understanding this column saves the "why does an off-screen enemy's HP update so slowly" / "why are bones zero off-screen" confusion.
- Enemy players / loot / projectiles down-throttle off-screen: off-screen, farther away, and non-alive all read slower. Any field marked "on-screen only" does not refresh while off-screen — you read the last value seen while it was on-screen.
- One exception: the aimbot's currently locked target is exempt from all throttling and is read every frame (so aiming never depends on ESP toggles).
- The local player is never throttled — you're always relevant, so its default is "every frame".
- "Computed each update" = the value is not read from memory directly; it's derived locally each cycle from the freshest other fields, so its freshness follows whatever it depends on.
- "Every frame / every N frames" refers to backend read cycles, not a fixed 60 Hz.
game.localplayer
The local player has no on-screen / distance throttling; default is "every frame".
| Field | Type | Update strategy | Description |
|---|---|---|---|
index | int | Every frame | entity_list index |
base / self_base | uint64 | Every frame | Entity memory base — combine with offsets + mem.read/write to self-service read/write any attribute. The two diverge while dead-spectating: base follows the spectated player, self_base is always the real local machine — use self_base to read "my own" attributes |
health | int | Every frame | Current HP |
max_health | int | Low frequency | Max HP (only changes on armor pickup / gold-knockdown revive) |
shield | int | Every frame | Current shield |
max_shield | int | Low frequency | Max shield (changes on armor swap) |
origin / camera_origin | Vec3 | Every frame | Entity / camera position (origin follows the spectated player while dead) |
view_angle | Vec2 | Every frame | {pitch, yaw} |
sway_angle | Vec2 | Every frame | Bullet firing direction including breath/sway |
view_offset / punch_angle | Vec3 | Every frame, alive & not knocked | View offset / recoil; keeps last value when dead/knocked |
abs_velocity | Vec3 | Every frame | |
flags_raw | uint32 | Every frame | Raw m_fFlags bits (FL_ONGROUND etc.) |
team_num / squad_id | int | Read once on create | |
platform_uid | string | Read once on create | uint64 → string |
weapon_id | int | Every frame, alive & armed | Weapon string-table index; blank when empty-handed |
weapon_enum | int | Resolved on weapon swap | sdk::ItemId value, more reliable than the weapon string |
weapon | string | Resolved on weapon swap | Weapon name shown in ESP |
weapon_speed / weapon_scale | number | Every frame, alive & armed | Fire rate / charge-up |
target_zoom_fov | number | Every frame, alive & armed | ADS fov of the current weapon |
weapon_next_ready_time | number | Every frame, alive & armed | |
weap_state / burst_fire_index / ammo_in_clip | int | Every frame, alive & armed | Internal ammo state |
is_semi_auto | bool | Every frame, alive & armed | |
is_zooming | bool | Every frame, alive & not knocked | |
is_dead / is_down / is_on_ground | bool | Every frame | |
is_grenade / is_hands | bool | Resolved on weapon swap | |
is_skydive | bool | Low frequency (~every 6 frames) | |
time_base | number | Every frame | World time base |
spec_index | int | Every frame | Spectated target (only set while dead-spectating; -1 when alive) |
backpack_tier | int | Low frequency (~1 Hz) | 0=None / 1=White / 2=Blue / 3=Purple / 4=Gold |
consumables | table | Low frequency (~every 15 frames) | 16-slot consumable inventory (each slot {item: uint16, count: uint16}; count==0 = empty) |
game.entities.players (iterable via pairs)
Per-PlayerEntity fields. The update strategy here stacks two layers: the entity itself is read every frame on-screen, throttled off-screen, and a field may add its own gate (bones need on-screen, identity is read once on create). The effective refresh rate is the slower of the two.
| Field | Type | Update strategy | Description |
|---|---|---|---|
index / base / is_npc | int/uint64/bool | Fixed on create | Not read from memory |
name / platform_uid / xp | string/string/int | Read once on create | platform_uid retries a few times in-match if the first read fails |
team_num / squad_id / rank_source | int | Read once on create | |
grade / rank / rank_icon_key / rank_color | int/string | Locked for the match | Computed from score on the first frame, fixed after one cloud-rank resolve |
legend / legend_icon_key | string | Resolved on legend change | Model name sampled ~every 64 frames; resolves ~once per match |
kills / damage | int | Low frequency (~every 64 frames) | Read on- and off-screen, diluted by entity throttling |
weapon_id | int | Short lag on weapon swap | Weapon pointer refreshes ~every 12 frames on-screen; the underlying handle is sampled very infrequently |
weapon | string | Resolved on weapon swap | |
origin | Vec3 | Every frame on-screen, throttled off-screen | Not read after death |
abs_velocity | Vec3 | On-screen & in valid range only | Falls back to position-delta estimation off-screen |
head_pos | Vec3 | On-screen only | Derived from the head bone each update (needs bones resolved) |
yaw | number | <300m ~every 6 frames, slower beyond | Read on- and off-screen |
health / shield / max_shield | int | On-screen only (~every 12 frames) | |
flags | int | On-screen only (~every 3 frames) | m_fFlags state bits |
is_down / cloak_endtime | bool/number | On-screen only (~every 3 frames) | |
is_dead | bool | Low frequency (~every 8 frames) | |
spec_index | int | Dead players only (~every 8 frames) | Whom this player is spectating; -1 for the living |
last_visible_time | number | On-screen & in valid range only | Visibility timestamp |
vec_min / vec_max | Vec3 | On-screen (~every 3 frames), needs dynamic-box/map-ident | Collision AABB; not read if neither is enabled |
is_glow / is_wall | bool | On-screen (~every 12 frames), needs glow enabled | |
roster | uint8 | Roster lookup each update | Roster bitmask (pro/star/cheater/friends); not re-queried unless the roster changes |
distance | number | Computed each update | Meters |
is_visible / is_teammate / is_cloak | bool | Computed each update | Not read from memory |
in_screen / can_draw / is_model_blocked | bool | Computed each update | Rendering-related state; static occlusion like Gibraltar's bubble |
has_bones / has_fresh_bones() | bool | On-screen only; true only when all 17 bones were read this frame | false off-screen; calling it triggers a full-bone read (see below) |
What you get is a plain 1-based Lua table, so
#,ipairsandpairsall work.Every read materialises a fresh table. Touching one several times in the same block builds several identical tables, which is not free in a match with hundreds of loot items — read it once into a local:
local players = game.entities.players. The table is good for the current frame only, do not hold it across frames.
Skeleton
PlayerEntity exposes 17 bone world positions; pair with game.BONE (part constants) and game.BONE_LINKS (connection table) to draw skeleton ESP.
| Member / constant | Description |
|---|---|
player:bone(idx) | World-space Vec3 of body part idx (0-based body part id); returns a zero vector if out of range or bones not ready |
player:has_fresh_bones() | Whether all 17 bones were actually read this frame (when false, bone() is likely zero — skip) |
game.BONE | Named part constants: head=0, upper_chest, chest, waist, hip, l_shoulder, l_elbow, l_hand, r_shoulder, r_elbow, r_hand, l_thigh, l_knee, l_foot, r_thigh, r_knee, r_foot=16 |
game.BONE_LINKS | Array of {a, b} part-id pairs (matches the native skeleton, 16 segments) |
Update strategy: bones are read on-screen only (
bone()returns zero off-screen). By default the backend reads only the head bone; all 17 are read when one of these holds: skeleton ESP is on, the player is the aimbot's locked target, or it's used as a chams far-enemy anchor. Callingbone()orhas_fresh_bones()makes the backend read all 17 bones for on-screen players for the next ~0.5s — so drawing your own skeleton does not need the menu's skeleton-ESP toggle; just call it.
math.WorldToScreen(world)returns 3 values(sx, sy, ok), not a Vec2.
-- Skeleton ESP: iterate links, project each bone to screen, draw a line
local LINKS = game.BONE_LINKS
event.on("frame_update", function()
for _, p in pairs(game.entities.players) do
if p.is_visible and not p.is_dead and not p.is_teammate and p:has_fresh_bones() then
for _, link in ipairs(LINKS) do
local ax, ay, ok1 = math.WorldToScreen(p:bone(link[1]))
local bx, by, ok2 = math.WorldToScreen(p:bone(link[2]))
if ok1 and ok2 then
draw.line(ax, ay, bx, by, draw.u8(255, 255, 255, 200), 1.5)
end
end
end
end
end)game.entities.loots (iterable via pairs)
Per-LootEntity fields. Loot properties are essentially fixed at create; only origin keeps re-pacing by state. A newly dropped item is discovered after ~0.25s (discovery lag, unrelated to field refresh).
| Field | Type | Update strategy | Description |
|---|---|---|---|
index / base | int/uint64 | Fixed on create | Entity memory base — combine with offsets + mem.read/write for self-service reads |
model_name | string | Read once on create | |
model_hash | uint32 | Read once on create | hash of model_name |
quality_level | int | Read once on create | 0=unknown, 1..5=COMMON..HEIRLOOM |
weapon_name_index | int | Read once on create | Weapon string-table index |
custom_script_int / context_id | int | Read once on create | |
classified_id | int | Computed each update | ItemId enum value |
classified_name / item_id_str / base_name | string | Computed each update | Classified names |
origin | Vec3 | Low-frequency polling | High when just moved; static on-screen ~4 Hz; static off-screen ~1 Hz; beyond loot display range ~every 4s. Never fully stops |
distance | number | Computed each update | Meters |
game.entities.projectiles (iterable via pairs)
Per-ProjectileEntity fields. The core job is high-frequency trajectory drawing: origin is read every frame for nearby local/enemy projectiles. Lifetime-constant fields (owner/team/radius/weapon class) are read once when the projectile appears.
| Field | Type | Update strategy | Description |
|---|---|---|---|
index / base | int/uint64 | Fixed on create | Entity memory base — combine with offsets + mem.read/write for self-service reads |
owner_handle_raw | uint32 | Read once on first appearance | Raw EHandle bits of m_hOwnerEntity |
team_num | int | Read once on first appearance | |
dmg_radius | number | Read once on first appearance | m_DmgRadius |
weapon_class_index | uint16 | Read once on first appearance | m_weaponClassIndex; pair with game.world to look up weapon name |
creation_time | number | Stamped once on first appearance | Entity birth timestamp |
kind | string | Typed once when weapon name resolves | "frag" / "thermite" / "arc_star" / "other" / "unknown" |
origin | Vec3 | Every frame (nearby local/enemy) | ~1 Hz beyond trajectory display range; not read for teammate or static map props once confirmed |
is_throwable | bool | Computed each update | Real player grenade (filters static map props) |
is_enemy / is_local | bool | Computed each update |
game.aimbot (read-only)
| Field | Type | Description |
|---|---|---|
target_index | int | Entity index of the current aimbot target (-1 when no target) |
target_distance | number (meters) | Distance to the current target |
predict_pos | Vec3 | nil | The aimbot's internal predicted target position |
trigger_busy | bool | Whether the trigger decision is currently in a busy state (for custom trigger HUDs) |
trigger | table | Detailed trigger state snapshot (see below, for trigger HUDs) |
weapon_next_ready_timeis ongame.localplayer; trigger state is not its own namespace to avoid duplicate paths.
game.aimbot.trigger
All timestamps are engine-clock seconds (QPC, base::get_time), not time.game()/time.now(). The table includes a same-clock now; use it for countdowns (e.g. ready_in = t.weapon_ready_at - t.now).
| Field | Type | Description |
|---|---|---|
now | number | Current engine clock (QPC seconds), same clock as the timestamps below; use it for countdowns |
busy | bool | In a hold/release-window countdown |
target_index | int | Target being evaluated (0 = none) |
pressed | bool | Already mouse_down |
last_hit | bool | Whether the last evaluation hit |
snapshot_time | number | Game time of this snapshot |
release_at / again_at | number | Planned mouse_up / next-allowed-fire time |
visible_at | number | Target visible start (react timing origin) |
weapon_ready_at | number | Weapon ready time |
react_window_ms / again_window_ms / release_window_ms | int | React / re-fire / hold-duration windows, in milliseconds (same unit as the menu) |
radscale | number | Hit-radius multiplier |
aim_offset | Vec3 | Offset of the aim part relative to spine_head |
event.on("frame_update", function()
local t = game.aimbot.trigger
if t.target_index ~= 0 then
local ready_in = math.max(0, t.weapon_ready_at - t.now)
draw.text(20, 200, draw.u8(255,255,255,255),
string.format("trigger: tgt=%d hit=%s ready_in=%.2f",
t.target_index, tostring(t.last_hit), ready_in))
end
end)Custom aim algorithms (movement/smoothing + prediction override)
Scripts can register their own movement algorithm (added to the dropdown) and prediction algorithm (global override). Execution model: callbacks run on the render thread (same thread as the Lua VM — no lock). Smoothing cadence = min(aim publish rate, render FPS) (uncapped render → high rate); prediction is slow-changing, so the render thread computes the aim point and the aim thread consumes it at 500Hz with no cadence loss. The native Normal/PID paths are unaffected.
game.aimbot.solve(shooter, target_pos, target_vel, v0, gravity) → {pitch, yaw, time} | nil
Native ballistic solver (stateless, callable anywhere). pitch/yaw are Source angles (degrees, pitch +down), time is flight time (seconds). v0 ≤ 1 is treated as hitscan (time=0); out of range returns nil. Coordinates are world-space Vec3, velocity u/s, gravity u/s².
game.aimbot.register_algorithm(name, fn)
Register a movement algorithm. name is appended to the aim / trigger Movement Algorithm dropdown; when selected, this algorithm drives mouse smoothing. fn(state) returns dx, dy (mickey delta; the engine accumulates sub-pixel residue). Auto-unregistered on script unload.
Must be called from the script's top-level load body — deferred callbacks (
event.on("frame_update", ...),Delay(),gui.Buttoncallbacks, etc.) are rejected with a throttled log warning, because the owner identity is cleared after load completion and cannot be reliably attributed (would silently leak across scripts). For lazy enable, keepregister_algorithmin the body and gate behavior with a Lua-side flag.
state fields:
| field | type | meaning |
|---|---|---|
err_pitch / err_yaw | number (deg) | current aim error (prediction-compensated + dead-zoned) |
dt | number (s) | time since this algorithm was last called |
distance | number (m) | target distance |
zooming | bool | ADS or not |
mouse_sens | number | in-game sensitivity (for screen-consistent scaling: err/0.022/mouse_sens → mickey) |
target_index | int | target entity index |
shooter / sway | Vec3 / Vec2 | shooter origin / current reference view |
target / velocity | Vec3 | world hit point / target velocity (u/s) |
Sign convention matches native:
dx = -err_yaw·k,dy = err_pitch·k.
game.aimbot.set_predictor(fn)
Globally override the prediction algorithm. fn(target) returns a world aim point Vec3; the aim thread consumes it at 500Hz in place of native solve. Pass nil to clear (fall back to native). target fields: index, shooter, aim_point, head, origin, velocity (Vec3), distance, v0, gravity.
aim_pointvshead:aim_pointis the current actual aim point — in upper-body / full-body modes it rides the body part nearest the crosshair and moves with it, so compute the lead against it.headis the stable real head-bone world point, only for head-specific logic (e.g. fixed head-aim compensation). Always lead withaim_point, nothead(otherwise the aim point gets pulled back to the head and cancels the tracking).
Same as
register_algorithm— load-body-only. Also one-predictor-per-process: if another script already owns the predictor, calls are rejected silently to Lua (throttled log only). Convention: one predictor per process, or unload the holding script first.
local tab = gui.tab("AimAlgo")
local g = tab:group("g", "Lua Aim Algo", 0, 0, 300, 0)
local gain = g:slider_float("gain", "Strength (mickey/deg)", 0.55, 0.05, 2.0)
local lead = g:checkbox("lead", "Prediction override", true)
-- Adaptive easing smoother: appears in the dropdown, active when selected
game.aimbot.register_algorithm("Lua Adaptive", function(s)
local mag = math.sqrt(s.err_pitch^2 + s.err_yaw^2)
local f = gain:get() * (1.0 - 0.4 * math.exp(-mag)) -- soft near target, full far
return -s.err_yaw * f, s.err_pitch * f
end)
-- Prediction override: reuse native solve for flight time, extrapolate by velocity
game.aimbot.set_predictor(function(t)
if not lead:get() then return t.aim_point end
local sol = game.aimbot.solve(t.shooter, t.aim_point, t.velocity, t.v0, t.gravity)
if not sol then return t.aim_point end
return Vec3(t.aim_point.x + t.velocity.x * sol.time,
t.aim_point.y + t.velocity.y * sol.time,
t.aim_point.z + t.velocity.z * sol.time)
end)game.world
| Field | Type | Description |
|---|---|---|
ring | table | {origin=Vec3, radius_start=N, radius_end=N, time_start=N, time_end=N, is_active=bool}; radius / origin are in game units (≈ inches), divide by 39.37 for meters |
map_name | string | Current map string |
game top-level methods
| Method | Returns | Description |
|---|---|---|
game:is_in_game() | bool | Whether currently in a match (signon_state + not in lobby/match-making) |
game:signon_state() | int | Raw client_state.signon_state |
game:smart_filter() | table | nil | Internals of the smart item/attachment filter, see below |
game:smart_filter()
Diagnostics: exposes the intermediate state of the smart item filter — which attachment slots the game offers for the held weapon, what each one was recognized as, and which items ended up selected. This is what answers "why isn't this attachment showing up".
Built on demand: each call renews a 1-second lease; with no script reading, nothing is built at all. So the very first call always returns nil (the background pass only produces data on its next recompute) — calling it every frame makes that a non-issue. One second after the last call it goes back to zero cost. Also nil when not in a match.
Returned table:
| Field | Type | Description |
|---|---|---|
recompute | int | Recompute counter. Not moving = loadout/inventory unchanged, not a stalled feed |
weapons | table | The two primary weapon slots, see below (empty slot has weapon == "") |
desired | table | Array of the finally selected items, each {name, id, group, checked} |
weapons[i]:
| Field | Type | Description |
|---|---|---|
slot | int | 1 / 2 |
weapon / weapon_id | string | Debug short name / item-list key; "" for an empty slot |
weapon_icon / ammo_icon | string | Icon resource names, feed straight into file.image:get() (see Icons below) |
ammo | string | Ammo key from the item spec table (e.g. ammo_energy). Magazines are filtered by this |
mods_read | bool | Whether the attachment list was read. false = neither attachments nor ammo will be suggested for this weapon |
care_package | bool | Care-package weapon (fully kitted; nothing is suggested) |
dual_ammo | bool | Dual-ammo weapon (C.A.R.) |
mag_offered | table | Magazine series the game offers for this weapon ("light"/"heavy"/"energy"/"sniper") |
mag_equipped | table | Of those, the ones currently equipped |
mag_mismatch | bool | true = the spec table's ammo is not among mag_offered, so no magazine will ever be shown for this weapon (spec table needs fixing) |
wanted | table | Attachments contributed by this weapon, same shape as desired. This is the value before the global optic rules run — don't diff it against desired |
mods | table | Per-slot attachment rows, see below |
weapons[i].mods[k]:
| Field | Type | Description |
|---|---|---|
index | int | Index in the game's attachment list (0-based) |
name | string | Raw attachment name from the game, e.g. stock_sniper_l3 |
item / item_id | string | Recognition result (debug short name / item key); "" = not recognized |
icon / tier | string / int | Icon resource name / rarity tier (0=single 1 white 2 blue 3 purple 4 gold 5 red) |
equipped | bool | Currently equipped |
marker | string | "" / "dual_ammo" / "care_package" — the latter two are markers, not real attachments |
desired[k] / wanted[k]:
| Field | Type | Description |
|---|---|---|
name / id | string | Debug short name / item-list key |
icon | string | Icon resource name (see below); "" when the item has no icon |
tier | int | Rarity: 0=single tier / 1 white / 2 blue / 3 purple / 4 gold / 5 red. All tiers of one attachment share a single icon — use this to color them apart |
group | string | weapon / weapon_mod / optic / ammo / heal / gear … |
checked | bool | Whether it is checked in the item list. Being checked is a hard prerequisite for drawing: selected but unchecked still draws nothing |
Icons
icon / weapon_icon / ammo_icon are resource names (e.g. icon_attachments/Extended_Energy_Mag), not file paths. The app preloads the whole resources/icon_* tree into the texture table at startup, so scripts just fetch them — no file lookup, no re-rasterizing the SVG:
local tex = file.image:get(item.icon)
if tex then draw.image(tex, x, y, 28, 28) endReturns nil when that item has no icon. Don't use file.image:load() for these — it re-reads and rasterizes a second copy, wasting VRAM.
-- Find weapons the spec table got wrong: ammo mismatch, or two stock series offered at once
local sf = game:smart_filter()
if sf then
for _, w in ipairs(sf.weapons) do
if w.mag_mismatch then
log.warn(w.weapon .. ": wrong ammo in the spec table, its magazines will never show")
end
end
endA full example ships with the app: scripts/_test_16_smart_filter.lua (window + console dump + automatic flagging).
Example
local lp = game.localplayer
if lp.is_dead then return end
for _, p in pairs(game.entities.players) do
if (not p.is_teammate) and p.is_visible and p.distance < 50 then
log.info("Enemy " .. (p.name or "?") .. " at " .. p.distance .. "m")
end
end
if game.world.map_name == "mp_rr_arena_skygarden" then
-- ...
end