Skip to content

Traces

Both calls need the internal running and needs_internal = true in script_info.

trace.line(from, to [, {id = 0, mask = 0, skip_local = true}]) -> id | nil
trace.get(id) -> table | nil

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
mask0Which collision set to trace against
skip_localtrueIgnore your own pawn

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. One 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, kept for scripts ported from other loaders
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 and its id freed. The result table is reused between calls; entry_seq changes when the line was actually re-traced.

Limits

  • 32 requests across all scripts. The 33rd returns nil and logs once.
  • Lines are re-traced within 50 ms, not every tick.
  • Ids are never 0.

Returns nil when

  • trace.line: the flag is missing, an endpoint is not a vec3 or {x, y, z} table, 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

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

This answers the same question as p:is_visible, on your own schedule. p:is_visible and p:is_bone_visible on the Player page need neither the flag nor the internal.