Traces
There are two kinds of line trace:
- The game's own trace:
trace.line,trace.get,trace.statsandtrace.release. These need the internal running andneeds_internal = trueinscript_info. The answer arrives a tick or two later. - TSUKI's map trace:
trace.bvh. It works in any script, without the internal, and answers right away. It is described further down this page.
trace.line(from, to [, {id = 0, mask = 0, skip_local = true, rate_ms = 50}]) -> id | nil
trace.get(id) -> table | nil
trace.stats() -> table | nil
trace.release(id) -> booleantrace.line registers a line to trace through the game's collision and returns an id. trace.get(id) returns the latest result for that id, or nil until the first result lands, usually a tick or two later. from and to are vec3 or {x=, y=, z=} in world units.
| Option | Default | Meaning |
|---|---|---|
id | 0 | Refresh the request with this id instead of creating a new one. A whole number |
mask | 0 | Which collision set to trace against. A whole number: 1.0 is fine, 1.5 is an error |
skip_local | true | Ignore your own pawn |
rate_ms | 50 | How often this line is traced again, 25 to 1000. It only affects this line. Your other lines keep their own rate. A whole number: 50.0 is fine, 25.5 is an error |
Masks
mask | Set |
|---|---|
0 | Visibility. Player pawns are not solid, so a line that reaches the far point (fraction 1.0, no entity) means the point is visible |
1 | Bullets. What a shot collides with. Players come back in entity |
2 | Bullets, swept as a 16 unit sphere |
Other values are treated as 0. Each request has one mask. For both a visibility and a bullet answer, register two traces.
Result fields
| Field | Meaning |
|---|---|
id | Request id |
entry_seq | Changes each time the line was re-traced |
tick | Tick the line was traced on |
fraction | How far along the line the trace got, 0 to 1 |
hit | True when the line stopped short, hit an entity, or started inside solid |
start_solid | The start point was inside a solid |
x, y, z | End point, world units |
nx, ny, nz | Surface normal at the hit |
contents | Contents bits of the surface hit |
surface_flags | Same value as contents |
hitgroup | Hit group, -1 for none |
entity_ptr | 0 for nothing or the world |
entity_class | Designer name of the entity hit, "" for none |
entity | Player object when the entity hit is a player |
On mask 0, hit against world geometry means the line is blocked.
Keeping a trace alive
Pass the id back in opts.id on the next call to refresh the same request with new endpoints. A request not refreshed for 2 seconds is dropped. Passing its id again registers it again under the same id. trace.get returns the same table until the line is traced again, then a new table with a new entry_seq.
trace.release(id)
Removes one line now. Returns true if the line existed. After that, trace.get(id) returns nil.
A line you stop refreshing stays in the table for 2 seconds. It keeps being traced that whole time and holds a shared slot. Release a line as soon as you are done with it.
trace.bvh and trace.source
trace.bvh(from, to) -> table | nil
trace.source() -> "engine" | "bvh"trace.bvh checks a line against TSUKI's own copy of the map, plus the chess pieces a hero can place, and answers in the same call. It needs no internal, no flag and no id. from and to are a vec3 or a table with x, y and z fields ({x = 1, y = 2, z = 3}, not {1, 2, 3}). Anything else raises an error. It returns nil when TSUKI has no map data for the current map.
| Field | Meaning |
|---|---|
hit | true when something blocks the line |
fraction | How far along the line it got, 0 to 1 |
x, y, z | Where it stopped, or to when nothing blocked it |
source | Always "bvh" |
blocker | "map" or "chess_piece", only when hit is true |
It has no surface normal and no entity. Only the map and chess pieces block it: players, doors and ability walls do not. A chess piece with either end of the line inside it is ignored.
trace.source() tells you which kind of check TSUKI's aimbot uses for player visibility right now: "engine" when the internal is running with Use Engine Visibility on (it is on by default), otherwise "bvh". p:is_visible uses the same check. Use it to make your script agree with the aimbot. The "engine" branch calls trace.line, so it also needs needs_internal = true:
-- cam and head are vec3 positions, and id is kept from the last tick
local clear
if trace.source() == "bvh" then
local r = trace.bvh(cam, head)
clear = r and not r.hit
else
id = trace.line(cam, head, { id = id })
local r = id and trace.get(id)
clear = r and r.fraction >= 0.97
endtrace.stats()
Returns what the shared table holds right now, or nil when the internal is not running.
| Field | Meaning |
|---|---|
used | lines in the table |
max | table size, 32, shared by every script |
per_slice | lines traced in one 25 ms pass, 16 |
interval_ms | how often each live line is traced again, 50 by default |
refused | trace.line calls turned away this session because the table was full |
Limits
- 32 requests across all scripts. The 33rd returns
nil. - With 32 live lines, each line refreshes about every 50 ms.
- A refusal prints to the Lua console and names your script, at most once every 5 seconds.
- Ids are never
0. internal.trace_diag()returns the trace feed's counters, ornilwhen the internal is not running.requestsis the number of lines registered,countthe number answered, andoverflowthe ones turned away.
Keep one id per line and pass it back in. Keep the number of lines steady, and release lines you no longer need.
Returns nil when
trace.line: the flag is missing, an endpoint is neither avec3nor a table (a missingx,yorzfield reads as0), the internal is not running, or 32 requests are already livetrace.get: the flag is missing, the id is0, the internal is not running, or no result has landed yet
With the flag set, trace.get(nil) and trace.release(nil) raise an error. trace.line returns nil when it fails, so check the id first.
Example
One ray per enemy head, kept alive by its id:
script_info = { needs_internal = true }
local rays = {} -- key -> trace id
function on_tick()
local me = local_player()
if not me or not internal.is_loaded() then return end
local cam = camera.get_position()
for _, p in ipairs(get_players() or {}) do
if p:is_alive() and p:is_enemy() then
local b = p:get_bone_position("head")
if b then
local key = p:entity_ptr() .. ":head"
rays[key] = trace.line(cam, { x = b.x, y = b.y, z = b.z },
{ id = rays[key], mask = 0, skip_local = true })
local r = rays[key] and trace.get(rays[key]) or nil
if r then
local visible = (r.entity ~= nil and r.entity_ptr == p:entity_ptr())
or r.fraction >= 0.97
-- draw or act on `visible`
end
end
end
end
endAn arc drawn as 16 segments, one fixed id per segment, released when it is no longer needed:
local ids = {}
local function trace_arc(pts) -- pts[1..17]
for i = 1, 16 do
ids[i] = trace.line(pts[i], pts[i + 1], { id = ids[i], mask = 1, skip_local = true })
end
end
local function first_hit()
for i = 1, 16 do
local r = ids[i] and trace.get(ids[i])
if r and r.hit and not r.start_solid then return r end
end
end
local function stop()
for i = 1, 16 do
if ids[i] then trace.release(ids[i]); ids[i] = nil end
end
endA visibility trace answers the same question as p:is_visible, on your own schedule. p:is_visible and p:is_bone_visible on the Player page do not need the flag or the internal.