Skip to content

Traces ​

There are two kinds of line trace:

  • The game's own trace: trace.line, trace.get, trace.stats and trace.release. These need the internal running and needs_internal = true in script_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) -> boolean

trace.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.

OptionDefaultMeaning
id0Refresh the request with this id instead of creating a new one. A whole number
mask0Which collision set to trace against. A whole number: 1.0 is fine, 1.5 is an error
skip_localtrueIgnore your own pawn
rate_ms50How 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 ​

maskSet
0Visibility. Player pawns are not solid, so a line that reaches the far point (fraction 1.0, no entity) means the point is visible
1Bullets. What a shot collides with. Players come back in entity
2Bullets, 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 ​

FieldMeaning
idRequest id
entry_seqChanges each time the line was re-traced
tickTick the line was traced on
fractionHow far along the line the trace got, 0 to 1
hitTrue when the line stopped short, hit an entity, or started inside solid
start_solidThe start point was inside a solid
x, y, zEnd point, world units
nx, ny, nzSurface normal at the hit
contentsContents bits of the surface hit
surface_flagsSame value as contents
hitgroupHit group, -1 for none
entity_ptr0 for nothing or the world
entity_classDesigner name of the entity hit, "" for none
entityPlayer 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.

FieldMeaning
hittrue when something blocks the line
fractionHow far along the line it got, 0 to 1
x, y, zWhere it stopped, or to when nothing blocked it
sourceAlways "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:

lua
-- 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
end

trace.stats() ​

Returns what the shared table holds right now, or nil when the internal is not running.

FieldMeaning
usedlines in the table
maxtable size, 32, shared by every script
per_slicelines traced in one 25 ms pass, 16
interval_mshow often each live line is traced again, 50 by default
refusedtrace.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, or nil when the internal is not running. requests is the number of lines registered, count the number answered, and overflow the 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 a vec3 nor a table (a missing x, y or z field reads as 0), the internal is not running, or 32 requests are already live
  • trace.get: the flag is missing, the id is 0, 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:

lua
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
end

An arc drawn as 16 segments, one fixed id per segment, released when it is no longer needed:

lua
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
end

A 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.