Source 2013 MP/Scripting/VScript Examples: Difference between revisions
Line 1,121: | Line 1,121: | ||
The built-in performance counter requires special considerations to show function names correctly. Otherwise, perf warnings will show <code>main</code> or <code><lambda or free run script></code> instead of the correct function name more often than not. | The built-in performance counter requires special considerations to show function names correctly. Otherwise, perf warnings will show <code>main</code> or <code><lambda or free run script></code> instead of the correct function name more often than not. | ||
A good rule of thumb to largely avoid this problem is to never use | A good rule of thumb to largely avoid this problem is to never use <code>name <- function(args)</code> syntax. This will create an anonymous function that will always print <code><lambda or free run script></code>. | ||
=== Think Functions === | === Think Functions === |
Revision as of 12:37, 11 August 2025
This page contains examples of VScripts for games on the modern (2025) SDK 2013 branch (,
,
,
,
)
Iterating Through Entities
With awhile
loop and a Entities.FindByClassname() function, you can iterate through all entities of a matching classname, based on your arguments.
The first parameter of Entities.FindByClassname() is named 'previous' which accepts a script handle (if an entity inherits 'CBaseEntity' specifically), which verifies if the matching entity it finds has an entity index that's higher than the current one in the 'previous' argument. If it turns out to be not, then its ignored.
local entity = null
while (entity = Entities.FindByClassname(entity, "prop_physics"))
{
printl(entity)
}
Alternatively, entity iteration loops can also be written in an identical but more compact manner as shown here:
for (local entity; entity = Entities.FindByClassname(entity, "prop_physics");)
{
printl(entity)
}
Iterating Through Players
It can be useful to iterate through players only. However, doing this with Entities.FindByClassname() is inefficient as it needs to search every entity. A quirk can be utilised to efficiently iterate players. Each networked entity has an associated 'entity index', which ranges from 0 to MAX_EDICTS. Usually these are unpredictable, however there is 2 groups of entities that have reserved entity indexes: worldspawn and players. Worldspawn is always reserved at entity index 0, and players are reserved from entity index 1 to maxplayers + 1. Using this fact, players can be simply iterated like shown below:

Constants.FServers.MAX_PLAYERS
. This is no longer recommended. Instead use MaxClients().tointeger() as shown below (only needs to be defined once in any file). MaxClients() only iterates the player limit set in the server rather than the maximum possible amount, which is more efficient, especially now that 
::MaxPlayers <- MaxClients().tointeger()
for (local i = 1; i <= MaxPlayers; i++)
{
local player = PlayerInstanceFromIndex(i)
if (player == null)
continue
printl(player)
}
Iterating Through Player's Weapons
The maximum amount of weapons a player can hold under normal gameplay scenarios is 8. (As Engineer: primary, secondary, melee, construction PDA, destruction PDA, the toolbox, grappling hook and the 'passtime gun' when holding passtime_ball)
const MAX_WEAPONS = 8
for (local i = 0; i < MAX_WEAPONS; i++)
{
local weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i)
if (weapon == null)
continue
printl(weapon)
}


m_hMyWeapons
array looks like it might be sorted by slot numbers, but this is NOT always true. Items being removed (such as equipping a boots primary as demoman) will shift this array back by one. Weapons being stripped by other code or added via Weapon_Equip
will also shift the array. Therefore, do not access m_hMyWeapons
with hardcoded slot numbers.Spawning Entities
VScript offers four different ways to spawn entities, each with their own unique considerations.
Spawning an Entity
SpawnEntityFromTable
The simplest method. Accepts a classname and a table of key-values.
The following code will spawn a rocket projectile that does nothing. The rocket will be spawned from the host's eyes.
const MOVETYPE_NOCLIP = 8
const MOVECOLLIDE_DEFAULT = 0
local player = GetListenServerHost()
local rocket = SpawnEntityFromTable("prop_dynamic_override",
{
// This is a table of keyvalues, which is the same way as keyvalues that are defined in Hammer
// Key Value
origin = player.EyePosition() + player.EyeAngles().Forward() * 32.0
angles = player.EyeAngles()
teamnum = player.GetTeam()
model = "models/weapons/w_missile.mdl"
})
rocket.SetMoveType(MOVETYPE_NOCLIP, MOVECOLLIDE_DEFAULT)
rocket.SetOwner(player) // make it not collide with owner
rocket.SetAbsVelocity(player.EyeAngles().Forward() * 250.0)
EntFireByHandle(rocket, "Kill", "", 5.0, null, null) // remove after 5 seconds

mins
and maxs
key-values must be set after the entity is spawned, either using KeyValueFromString/KeyValueFromVector
, or the SetSize
functions.
See the next example for spawning triggers.CreateByClassname
In situations where you only need to spawn an entity temporarily, spawn a dummy entity for your script (e.g. attaching think functions), or you are spawning the entity in an already expensive function and are concerned about performance, CreateByClassname
is more performant and generally recommended for these one-off situations.
The same example above using CreateByClassname
instead:
local player = GetListenServerHost()
// certain models may not be precached and will crash the game with an error if not done here
// many models are already precached by the game and do not need this (e.g. player/weapon models)
PrecacheModel("models/weapons/w_missile.mdl")
local rocket = Entities.CreateByClassname("prop_dynamic_override")
rocket.SetOrigin(player.EyePosition() + (player.EyeAngles().Forward() * 32.0))
rocket.SetAbsAngles(player.EyeAngles())
rocket.SetTeam(player.GetTeam())
rocket.SetModel("models/weapons/w_missile.mdl")
rocket.SetAbsVelocity(player.EyeAngles().Forward() * 250.0)
rocket.DispatchSpawn() // not strictly necessary for this example, but recommended
Spawning multiple entities
If you need to spawn multiple entities at the same time to preserve parent hierarchy across multiple entities or for performance/efficiency reasons, there are two options.
point_script_template
point_script_template is the most robust method of spawning multiple entities. It preserves parent hierarchy and has a callback function for when entities are spawned. It is also more performant than SpawnEntityGroupFromTable
. A notable use case for this entity aside from parenting is spawning multiple brush entities at the same time or spawning brush entities alongside other entities, as the PostSpawn
callback will allow you to set mins/maxs values easily after spawning.
local template = Entities.CreateByClassname("point_script_template")
template.DispatchSpawn()
template.ValidateScriptScope()
local template_scope = template.GetScriptScope()
// Required for the PostSpawn callback function to work
template_scope.__EntityMakerResult <- {}
// PostSpawn fires on the ForceSpawn input and will output a table of all spawned entities.
template_scope.PostSpawn <- function(entities)
{
__DumpScope(0, entities)
}
template.AddTemplate("prop_dynamic",
{
targetname = "heavy_model",
model = "models/player/heavy.mdl",
origin = Vector(247, -281, -123)
})
template.AddTemplate("prop_dynamic",
{
targetname = "spy_model",
parentname = "heavy_model",
model = "models/player/spy.mdl",
origin = Vector(247, -281, -123)
})
template.AcceptInput("ForceSpawn", "", null, null)

OnPostSpawn
to work, as they are stored as keys in the __EntityMakerResult
table, See this page for a workaround.SpawnEntityGroupFromTable
This function will also allow you to spawn multiple entities at once with proper parenting, however it does not have a convenient callback method for targeting the entities that have been spawned.
//spawn origins are right outside of mvm_bigrock spawn
SpawnEntityGroupFromTable({
[0] = {
func_rotating =
{
message = "hl1/ambience/labdrone2.wav",
volume = 8,
targetname = "crystal_spin",
spawnflags = 65,
solidbsp = 0, // mins/maxs values are not required for non-solid func_rotating
rendermode = 10,
rendercolor = "255 255 255",
renderamt = 255,
maxspeed = 48,
fanfriction = 20,
origin = Vector(278.900513, -2033.692993, 516.067200)
}
},
[1] = {
tf_glow =
{
targetname = "crystalglow",
parentname = "crystal",
target = "crystal",
Mode = 2,
origin = Vector(278.900513, -2033.692993, 516.067200),
GlowColor = "0 78 255 255"
}
},
[2] = {
prop_dynamic =
{
targetname = "crystal",
solid = 6,
renderfx = 15,
rendercolor = "255 255 255",
renderamt = 255,
physdamagescale = 1.0,
parentname = "crystal_spin",
modelscale = 1.3,
model = "models/props_moonbase/moon_gravel_crystal_blue.mdl",
MinAnimTime = 5,
MaxAnimTime = 10,
fadescale = 1.0,
fademindist = -1.0,
origin = Vector(278.900513, -2033.692993, 516.067200),
angles = QAngle(45, 0, 0)
}
},
})

Spawning a Trigger
Triggers can be spawned like other entities, but they require more setup. This involves using SetSize
to define the cuboid as 2 corners (relative to trigger's origin), then SetSolid
to tell the engine to use the bounds as the collision. No other shape is possible. mins
and maxs
keyvalues are discarded in SpawnEntityFromTable
, which is why SetSize
is necessary afterwards.
AABB (non-rotated box)
Example of spawning a trigger_push that isn't rotated.
const SOLID_BBOX = 2
local trigger = SpawnEntityFromTable("trigger_push",
{
origin = Vector(128, 500, 0)
pushdir = QAngle(0, 90, 0)
speed = 500
spawnflags = 1 // allow players only
})
trigger.SetSize(Vector(-32, -32, 0), Vector(32, 32, 64))
trigger.SetSolid(SOLID_BBOX)
OBB (rotated box)
Example of spawning a trigger_push that is rotated. It is more expensive to calculate rotated triggers, so only use these if you really need to.

const SOLID_OBB = 3
local trigger = SpawnEntityFromTable("trigger_push",
{
origin = Vector(128, 500, 0)
angles = QAngle(0, 45, 0)
pushdir = QAngle(0, 90, 0)
speed = 500
spawnflags = 1 // allow players only
})
trigger.SetSize(Vector(-16, -16, 0), Vector(32, 32, 64))
trigger.SetSolid(SOLID_OBB)
Visualizing triggers
This utility function allows you to see what a trigger looks like.
const SOLID_BBOX = 2
const SOLID_OBB = 3
function DebugDrawTrigger(trigger, r, g, b, alpha, duration)
{
local origin = trigger.GetOrigin()
local mins = NetProps.GetPropVector(trigger, "m_Collision.m_vecMins")
local maxs = NetProps.GetPropVector(trigger, "m_Collision.m_vecMaxs")
if (trigger.GetSolid() == SOLID_BBOX)
DebugDrawBox(origin, mins, maxs, r, g, b, alpha, duration)
else if (trigger.GetSolid() == SOLID_OBB)
DebugDrawBoxAngles(origin, mins, maxs, trigger.GetAbsAngles(), Vector(r, g, b), alpha, duration)
}
Map brush
It is also possible to spawn a brush entity that re-uses an existing bmodel in the map. Every brush entity has a special model name in the format *N
, where N is a continuous number starting from 1 (1 is worldspawn).
There is no easy way to tell what these brushes look like, other than simply incrementing the number and inspecting it.

cl_precacheinfo modelprecache
to see the maximum number possible.local brush = SpawnEntityFromTable("func_brush",
{
model = "*4"
origin = Vector(128, 500, 0)
})

Advanced Entity I/O Manipulation
The EntityOutputs
class has a variety of useful functions for modifying entity I/O. Notably, GetNumElements
and GetOutputTable
can be used to collect all of the entity's I/O in code for easier manipulation.
Example: Strip all outputs from a given entity.
function RemoveOutputAll(ent, output)
{
// store ent outputs in an array to operate on
local outputs = []
// iterate over all entity outputs
// TODO: iirc there is a legitimate reason for iterating backwards but I don't remember why
for (local i = EntityOutputs.GetNumElements(ent, output); i >= 0; i--)
{
// store each output in a table
local t = {}
EntityOutputs.GetOutputTable(ent, output, t, i)
// array of output tables
outputs.append(t)
}
// iterate over all collected outputs and remove them.
foreach (o in outputs)
foreach(_ in o)
EntityOutputs.RemoveOutput(ent, output, o.target, o.input, o.parameter)
printf("Removed all '%s' outputs from %s\n", output, ent.tostring())
}
// Remove all OnStartTouchAll outputs from every trigger_multiple on the map
for (local trigger; trigger = Entities.FindByClassname(trigger, "trigger_multiple");)
RemoveOutputAll(trigger, "OnStartTouchAll")

RemoveOutput
Spawning a Particle
There is various methods to spawn particles, and picking the right one depends on the use case.
DispatchParticleEffect
This is the simplest method. It spawns a particle at a given location and rotation. It is not possible to stop the particle nor parent it to any entity. Useful for one-off effects such as explosions.
Do not use it for looping particle as they will persist forever.
DispatchParticleEffect("asplode_hoodoo", Vector(123, 400, 0), QAngle(0, 45, 0).Forward())

function PrecacheParticle(name)
{
PrecacheEntityFromTable({ classname = "info_particle_system", effect_name = name })
}
SpawnEntityFromTable
This spawns a info_particle_system entity. It can move and rotate arbitrarily, be parented to entities and stopped/started at any time.
The downsides are that it consumes an edict and adds networking overhead, and it does not use the parent's hitbox data for positioning (therefore particles such as player burning do not show up right).
local particle = SpawnEntityFromTable("info_particle_system",
{
origin = Vector(100, 900, 0)
angles = QAngle(0, 0, 0)
effect_name = "superrare_beams1"
start_active = true // set to false if you don't want particle to start initially
})
// parent it to a player's head
particle.AcceptInput("SetParent", "!activator", GetListenServerHost(), null)
particle.AcceptInput("SetParentAttachment", "head", null, null)
// remove particle entity after a delay
EntFireByHandle(particle, "Kill", "", 5.0, null, null)

trigger_particle
Team Fortress 2 only!
This utilizes a trigger_particle entity. It spawns a particle at a given entity's location, optionally made to follow the entity. If following the entity, it can use the hitbox data for positioning (useful for particles like unusual taunts to show up correctly). Unlike info_particle_system, this does not use an entity per particle. It only requires 1 singular edict for the
trigger_particle
itself. See trigger_particle page for a list of attachment modes.
Disadvantages of this method are that the spawned particle requires another entity to be present, and the spawned particle cannot be controlled directly, therefore looping particles will not stop until the entity is deleted. However particles can be stopped by firing the DispatchEffect
input on the entity with ParticleEffectStop
as the parameter. Note that this will stop all particles on the entity. Alternatively, spawn another entity like a prop_dynamic and bonemerge it to the entity, and attach particles to this instead. Killing the entity will stop the particles.
ParticleSpawner <- Entities.CreateByClassname("trigger_particle")
ParticleSpawner.KeyValueFromInt("spawnflags", 64)
function SpawnParticle(entity, name, attach_name, attach_type)
{
NetProps.SetPropString(ParticleSpawner, "m_iszParticleName", name)
NetProps.SetPropString(ParticleSpawner, "m_iszAttachmentName", attach_name)
NetProps.SetPropInt(ParticleSpawner, "m_nAttachType", attach_type)
ParticleSpawner.AcceptInput("StartTouch", "", entity, entity)
}
// example usage:
const TF_TEAM_RED = 2
const PATTACH_ABSORIGIN_FOLLOW = 1
local player = GetListenServerHost()
local particle_name = player.GetTeam() == TF_TEAM_RED ? "spy_start_disguise_red" : "spy_start_disguise_blue"
SpawnParticle(player, particle_name, "", PATTACH_ABSORIGIN_FOLLOW)
$keyvalues
If custom models are an option, particles can be embedded into the model instead via the $keyvalues block. This approach is near identical to the trigger_particle one, except it does not require any code and is done automatically when the model is set.
Creating a timer
EntFire
or EntFireByHandle
can be used to create simple delays or timers. It runs a given input on an entity name or handle after a delay. By using CallScriptFunction, a EntFire can be chained to repeatedly run itself. The worldspawn entity always exists so its an easy one to target. Note that a delay of 0 will run at the end of the game tick.
function MyTimerFunction()
{
printl("EntFire executed")
// runs again in 0.5 seconds
EntFire("worldspawn", "CallScriptFunction", "MyTimerFunction", 0.5)
}
// runs the timer after a second
EntFire("worldspawn", "CallScriptFunction", "MyTimerFunction", 1.0)
By using this knowledge we can create a convenience functions, such as RunWithDelay
and CreateTimer
. Note that unlike running RunScriptCode
inputs you can catch local variables which makes it integrate seamlessly and without additional overhead of compiling your code and flooding the string table. Lambda functions (@
) are really useful when combined with these functions since they provide a concise way to create an input function that runs a single expression, beware that they also implicitly return the result of the executed expression, so when used with CreateTimer
the returned value that's not null could unintentionally be interpreted as a delay which either leads to an exception by trying to pass non-number value to the delay parameter of the EntFireByHandle
or to involuntary continuous execution. Also, 2 additional functions are provided that allow you to fire the delayed function on-spot or discard the execution.
// Implementation
local world_spawn = Entities.FindByClassname(null, "worldspawn")
world_spawn.ValidateScriptScope()
local world_spawn_scope = world_spawn.GetScriptScope()
function RunWithDelay(func, delay = 0.0)
{
local func_name = UniqueString()
world_spawn_scope[func_name] <- function[this]()
{
delete world_spawn_scope[func_name]
func()
}
EntFireByHandle(world_spawn, "CallScriptFunction", func_name, delay, null, null)
return func_name
}
function CreateTimer(on_timer_func, first_delay = 0.0)
{
local func_name = UniqueString()
world_spawn_scope[func_name] <- function[this]()
{
try
{
local delay = on_timer_func()
if (delay == null)
{
delete world_spawn_scope[func_name]
return
}
// Delays which are less or equal to 0 will be executed in the current tick which leads to an infinite loop
if (delay <= 0.0)
delay = 0.01
EntFireByHandle(world_spawn, "CallScriptFunction", func_name, delay, null, null)
}
catch (err)
{
delete world_spawn_scope[func_name]
throw err
}
}
EntFireByHandle(world_spawn, "CallScriptFunction", func_name, first_delay, null, null)
return func_name
}
function KillTimer(func_name)
{
if (func_name in world_spawn_scope)
delete world_spawn_scope[func_name]
}
function FireTimer(func_name)
{
if (func_name in world_spawn_scope)
{
world_spawn_scope[func_name]()
KillTimer(func_name)
}
}
// Example
local fired = false
local timer = CreateTimer(function()
{
if (!fired)
{
printl("This is the first fire")
fired = true
// Repeat after 1 second
return 1.0
}
else
{
printl("This is not a first fire")
// repeat after 2 seconds
return 2.0
}
// First fire will be after 1 second
}, 1.0)
// Fire and kill the timer after 7 seconds
RunWithDelay(@() printl("Firing and killing a timer..."), 7.0)
RunWithDelay(@() FireTimer(timer), 7.0)
Listening for Events
In the game, many actions trigger events to notify other parts of the code. For instance, when a player dies or when an objective is captured, an event is fired. Each event can carry specific data, such as the index of an involved entity.
VScript allows you to capture these events, process the data, and execute code in response (this is called a 'callback'). You can find a list of game events on the game events page.
The code below demonstrates how to listen for the // This handles all the dirty work, just copy paste it into your code
function CollectEventsInScope(events)
{
local events_id = UniqueString()
getroottable()[events_id] <- events
foreach (name, callback in events)
events[name] = callback.bindenv(this)
local cleanup_user_func, cleanup_event = "OnGameEvent_scorestats_accumulated_update"
if (cleanup_event in events)
cleanup_user_func = events[cleanup_event]
events[cleanup_event] <- function(params)
{
if (cleanup_user_func)
cleanup_user_func(params)
delete getroottable()[events_id]
}
__CollectGameEventCallbacks(events)
}
CollectEventsInScope
({
OnGameEvent_post_inventory_application = function(params)
{
local player = GetPlayerFromUserID(params.userid)
// add uber protection to the player for 2 seconds.
player.AddCondEx(Constants.ETFCond.TF_COND_INVULNERABLE_USER_BUFF, 2.0, null)
}
})
![]() scorestats_accumulated_update ). Otherwise, the event callbacks won't function after the 1st round.![]() ClearGameEventCallbacks for cleaning up events is now obsolete. Do not use it, as it often caused conflicts with other scripts. The updated method shown above stores event callback functions in a table, which can be deleted to safely clean up the events without causing issues.
Backwards compatibility: If you have plugin-like scripts (independent of the map), and have a problem with outdated maps using // Override ClearGameEventCallbacks to wipe events from the root table or from entities only
// This way, backwards compatibility is preserved with maps using this deprecated function
// Events that are namespaced and not tied to the entity (e.g. for script plugins) are preserved
function ClearGameEventCallbacks()
{
local root = getroottable()
foreach (callbacks in [GameEventCallbacks, ScriptEventCallbacks, ScriptHookCallbacks])
{
foreach (event_name, scopes in callbacks)
{
for (local i = scopes.len() - 1; i >= 0; i--)
{
local scope = scopes[i]
if (scope == null || scope == root || "__vrefs" in scope)
scopes.remove(i)
}
}
}
}
|
The code below demonstrates how to listen for the // This handles all the dirty work, just copy paste it into your code
function CollectEventsInScope(events)
{
local events_id = UniqueString()
getroottable()[events_id] <- events
local events_table = getroottable()[events_id]
local Instance = self
foreach (name, callback in events)
{
local callback_binded = callback.bindenv(this)
events_table[name] = @(params) Instance.IsValid() ? callback_binded(params) : delete getroottable()[events_id]
}
__CollectGameEventCallbacks(events_table)
}
CollectEventsInScope
({
OnGameEvent_round_start = function(params)
{
printf("\tRound start\n")
}
OnGameEvent_player_spawn = function(params)
{
local player = GetPlayerFromUserID(params.userid)
printf("\tplayer_spawn '%s' on team '%d'\n", NetProps.GetPropString(player, "m_szNetname"), player.GetTeam())
}
})
|
The code below demonstrates how to listen for the // This handles all the dirty work, just copy paste it into your code
function CollectEventsInScope(events)
{
local events_id = UniqueString()
getroottable()[events_id] <- events
local events_table = getroottable()[events_id]
local Instance = self
foreach (name, callback in events)
{
local callback_binded = callback.bindenv(this)
events_table[name] = @(params) Instance.IsValid() ? callback_binded(params) : delete getroottable()[events_id]
}
__CollectGameEventCallbacks(events_table)
}
CollectEventsInScope
({
OnGameEvent_dod_round_start = function(params)
{
printf("\tRound start\n")
}
OnGameEvent_player_spawn = function(params)
{
local player = GetPlayerFromUserID(params.userid)
printf("\tplayer_spawn '%s' on team '%d'\n", NetProps.GetPropString(player, "m_szNetname"), player.GetTeam())
}
})
|

tf/scripts/vscripts/
folder and run script_execute showevents
in console.

CollectEventsInScope
function with the matching one! It is slightly different and not doing this will cause unexpected issues such as events stacking up each round.Toggling HUD Elements
Some HUD elements can be hidden away for players by using the appropriate HudHideFlags
functions.
The available flags can be found on the Constants page.
Since these are bit flags, elements to hide or show can be combined using the OR |
operator.
Example usage:
player.AddHudHideFlags(
Constants.FHideHUD.HIDEHUD_CROSSHAIR |
Constants.FHideHUD.HIDEHUD_HEALTH |
Constants.FHideHUD.HIDEHUD_WEAPONSELECTION
) // hides hud elements
player.RemoveHudHideFlags(player,
Constants.FHideHUD.HIDEHUD_CROSSHAIR |
Constants.FHideHUD.HIDEHUD_HEALTH |
Constants.FHideHUD.HIDEHUD_WEAPONSELECTION
) // makes hud elements visible again
Line 2 would add bits to hide the crosshair, health, and disabling weapon switching from the player. Line 3 would remove bits to show the crosshair, health, and re-enable weapon switching for the player.
Fetching player name or Steam ID
Steam IDs are stored in SteamID3 format, e.g. [U:1:53275741]
::GetPlayerName <- function(player)
{
return NetProps.GetPropString(player, "m_szNetname")
}
::GetPlayerSteamID <- function(player)
{
return NetProps.GetPropString(player, "m_szNetworkIDString")
}

player_activate
event in the player_spawn
event when the 'team' parameter is 0. This will make the SteamID3 immediately available afterwards.
const TEAM_UNASSIGNED = 0
function OnGameEvent_player_spawn(params)
{
local player = GetPlayerFromUserID(params.userid)
if (player.GetTeam() == TEAM_UNASSIGNED)
SendGlobalGameEvent("player_activate", {userid = params.userid})
}
Fetching and setting entity color
::SetEntityColor <- function(entity, r, g, b, a)
{
local color = (r) | (g << 8) | (b << 16) | (a << 24)
NetProps.SetPropInt(entity, "m_clrRender", color)
}
::GetEntityColor <- function(entity)
{
local color = NetProps.GetPropInt(entity, "m_clrRender")
return {
r = color & 0xFF,
g = (color >> 8) & 0xFF,
b = (color >> 16) & 0xFF,
a = (color >> 24) & 0xFF,
}
}
Think functions
A think function is a function on an entity that runs repeatedly on a timer. How it works:
- Define a function with no parameters
- Specify the think function name using the
Script Think Function
or add it to the entity usingAddThinkToEnt
- The return value of the function specifies the delay before running it again. By default, this is 0.1 seconds. The fastest possible delay is per-tick by returning -1.
runs at 66 ticks per second, so this is effectively a 0.015 interval. In
, servers may run at different tickrates so don't assume it's 66 by default.
Reading players' input
The example below when executed will show a message when the player presses reload.

const IN_RELOAD = 8192
function PlayerThink()
{
local buttons = NetProps.GetPropInt(self, "m_nButtons")
if (buttons & IN_RELOAD)
{
printl("Player is reloading")
}
return -1
}
local player = GetListenServerHost()
AddThinkToEnt(player, "PlayerThink")

logic_script
because the PlayerThink
function would not exist outside of the assigned entity. To fix this, either
- Make
PlayerThink
global, e.g. rewrite as::PlayerThink <- function()
or
- Add
PlayerThink
to the entity's scope, e.g.player.GetScriptScope().PlayerThink <- PlayerThink
It may be useful to only detect when a key is pressed (or released) rather than when it's being helded. This can be achieved by storing the last button state and comparing it against the new one. The example below prints a message when the player left-clicks.
const IN_ATTACK = 1
function PlayerThink()
{
local buttons = NetProps.GetPropInt(self, "m_nButtons")
local buttons_changed = buttons_last ^ buttons
local buttons_pressed = buttons_changed & buttons
local buttons_released = buttons_changed & (~buttons)
if (buttons_pressed & IN_ATTACK)
{
printl("Player pressed left-click")
}
buttons_last = buttons
return -1
}
local player = GetListenServerHost()
player.ValidateScriptScope()
player.GetScriptScope().buttons_last <- 0
AddThinkToEnt(player, "PlayerThink")
Timer
Another use case for think functions is running a function at certain delays so it can be used as a simple timer. A limitation is that there can only be one think function per entity.
function MyThinkFunction()
{
printl("Think executed")
// runs again in 0.5 seconds
return 0.5
}
local thinker = Entities.CreateByClassname("logic_relay")
AddThinkToEnt(thinker, "MyThinkFunction")
Conditional Think function
If you want a timer to be conditionally skipped depending on an input value, you can set the think to run every tick and use the Time()
function to get the current server time, then use a cooldown value to decide when to run the think again if the condition fails.
local cooldown = 0.0
local random_value = RandomInt(0, 10)
function MyThinkWithCooldown()
{
// skip cooldown if random_value is => 5
if (random_value < 5 && cooldown > Time())
return -1
printf("Think executed, value is %d\n", random_value)
// get a new random value
random_value = RandomInt(0, 10)
// random value is < 5, wait for 3 seconds
cooldown = Time() + 3.0
// actual think runs every tick
return -1
}
local thinker = Entities.CreateByClassname("logic_relay")
AddThinkToEnt(thinker, "MyThinkWithCooldown")
Multiple thinks on a single entity
Entities can only have one think function applied to them at any given time, attempting to call `AddThinkToEnt` when one is already running will cause the original think to be replaced entirely. As a workaround, you can assign a table to the entities scope and have the think function iterate over this table to call them, this will let you to dynamically add and remove functions as needed. Use an array instead of a table if execution order is important.
An issue that might arise is that the performance counter will not print an actual culprit function name, instead it will always print MultipleThinks
by summing up all the executed functions runtime.
The code below also provides a way to execute the thinks at different intervals based on the return value, the same ability you would have without a think table. No return value or null return value will result in a 0 delay instead of the usual 0.1 second delay however.
// Implementation
function MultipleThinks()
{
local time = Time()
foreach (name, func in ThinkTable)
{
if (ThinkDelays[name] > time)
continue
local delay = func()
if (delay == null || delay < 0.0)
delay = 0.0
ThinkDelays[name] = time + delay
}
return -1
}
function AddThinkTable(entity)
{
entity.ValidateScriptScope()
local scope = entity.GetScriptScope()
scope.ThinkTable <- {}
scope.ThinkDelays <- {}
scope.MultipleThinks <- MultipleThinks
AddThinkToEnt(entity, "MultipleThinks")
return scope
}
function AddThink(entity, name, func)
{
local scope = entity.GetScriptScope()
scope.ThinkTable[name] <- func//.bindenv(scope) seemingly has no impact
scope.ThinkDelays[name] <- 0.0
}
function RemoveThink(entity, name)
{
local scope = entity.GetScriptScope()
delete scope.ThinkDelays[name]
delete scope.ThinkTable[name]
}
// Example
local player = GetListenServerHost()
AddThinkTable(player)
function FirstThink() {
printl("Think1")
return 1.0
}
AddThink(player, "FirstThink", FirstThink)
AddThink(player, "SecondThink", function()
{
printl("Think2")
return 2.0
})
Printing colored text from Hammer I/O
When attempting to print colored text in using entity I/O, the escape characters used will not be saved correctly. While
does support escape characters for newlines, the
\x07
sequence for colored text will crash the editor when loading the map. To work around this, you can make a utility function to replace a random character with \x07
at run-time.

\x08
instead of \x07
.const HUD_PRINTTALK = 3
//print colored text within hammer
::ClientPrintSafe <- function(player, text)
{
//replace ^ with \x07 at run-time
local escape = "^"
//just use the normal print function if there's no escape character
if (!startswith(text, escape))
{
ClientPrint(player, HUD_PRINTTALK, text)
return
}
//split text at the escape character
local split_text = split(text, escape, true)
//format into new string
local formatted = ""
foreach (part in split_text)
formatted += format("\x07%s", part)
//print formatted string
ClientPrint(player, HUD_PRINTTALK, formatted)
}
Example usage:
ClientPrintSafe(null, "^FF0000This text is red.^00FF00 This text is green")
Getting the userid from a player handle


tf_player_manager
with the manager classname for your game. E.g. cs_player_manager in 

::PlayerManager <- Entities.FindByClassname(null, "tf_player_manager")
::GetPlayerUserID <- function(player)
{
return NetProps.GetPropIntArray(PlayerManager, "m_iUserID", player.entindex())
}
Alternatively, store off the userid from the player_spawn
game event.
Checking if point or box is inside a trigger / tracing triggers
This abuses a quirk where triggers can be set solid temporarily so traces "hit" them, and reverted afterwards. The example below demonstrates this to detect if a point is inside a given trigger.
Example usage: printl(IsPointInRespawnRoom(GetListenServerHost().EyePosition(), Entities.FindByName(null, "mycooltrigger")))
const FSOLID_NOT_SOLID = 4
const CONTENTS_SOLID = 1
function IsPointInTrigger(point, trigger)
{
trigger.RemoveSolidFlags(FSOLID_NOT_SOLID)
local trace =
{
start = point
end = point
mask = CONTENTS_SOLID
}
TraceLineEx(trace)
trigger.AddSolidFlags(FSOLID_NOT_SOLID)
return trace.hit && trace.enthit == trigger
}


Finding changed netprops
It may be useful to see what netprops have changed after an event occurs on an entity. The following example shows the changed sendprops (networked changes) and datamaps (server-specific changes) after the local player is killed.
function DumpChangedProps(ent)
{
local prev_sendprops = {}
local prev_datamaps = {}
local cur_sendprops = {}
local cur_datamaps = {}
NetProps.GetTable(ent, 0, prev_sendprops)
NetProps.GetTable(ent, 1, prev_datamaps)
ent.TakeDamage(999.9, 32, null)
NetProps.GetTable(ent, 0, cur_sendprops)
NetProps.GetTable(ent, 1, cur_datamaps)
local recursive_compare
recursive_compare = function(prev_table, cur_table, indent="")
{
foreach (k, v in cur_table)
{
local prev_v = prev_table[k]
if (typeof(v) == "table")
{
printl(indent + "TABLE: " + k)
recursive_compare(prev_table[k], v, indent + "\t")
}
else
{
if (typeof(v) == "Vector" || typeof(v) == "QAngle")
{
if (v.x != prev_v.x || v.y != prev_v.y || v.z != prev_v.z)
printl(indent + k + " changed: " + prev_v + " -> " + v)
}
else
{
if (prev_v != v)
printl(indent + k + " changed: " + prev_v + " -> " + v)
}
}
}
}
recursive_compare(prev_sendprops, cur_sendprops)
recursive_compare(prev_datamaps, cur_datamaps)
}
DumpChangedProps(GetListenServerHost())
Moving a player into spectating state without death
Avoids death effects such as kill feed or screams from appearing if killed by conventional means, and the player still remains on the team.
function ForceSpectate(player)
{
NetProps.SetPropInt(player, "m_iObserverLastMode", 5)
local team = player.GetTeam()
NetProps.SetPropInt(player, "m_iTeamNum", 1)
player.DispatchSpawn()
NetProps.SetPropInt(player, "m_iTeamNum", team)
}
Getting the players active soundscape
If you would like to determine if a player is in a certain area of the map (notably if they are inside a building or not), and the maps soundscapes are configured correctly, you can check the players active soundscape to get a rough idea of where they are without needing to manually place trigger_multiples or using other tedious detection methods.
Every soundscape has a unique index that can be found by typing soundscape_dumpclient
in console. Setting the developer
console command to 1 will print the string name of your active soundscape whenever one is played.
::InSoundscapeIndex <- function(player, index)
{
// uncomment to print soundscape indexes to console, enable "developer 1" to match with the name of the soundscape
// printf("Soundscape Index: %d \n", NetProps.GetPropInt(player, "m_Local.m_audio.soundscapeIndex"))
return NetProps.GetPropInt(player, "m_Local.m_audio.soundscapeIndex") == index
}
// prints true for soundscape index 34 on a listen server (Sawmill.Outside in TF2)
for (local soundscape; soundscape = Entities.FindByClassname(soundscape, "env_soundscape*");)
{
EntityOutputs.AddOutput(soundscape, "OnPlay", "!self", "RunScriptCode", "printl(InSoundscapeIndex(GetListenServerHost(), 34))", 0, -1)
}
Error telemetry
It may be difficult to debug script errors when testing on multiplayer servers. The following code when executed sets up error telemetry, where all script errors are sent to a specific player by their matching SteamID3. Each script error will be printed to chat in red text, with full error information printed to console.
const TelemetrySteamID3 = "[U:1:369156816]"
const HUD_PRINTCONSOLE = 2
const HUD_PRINTTALK = 3
seterrorhandler(function(error)
{
for (local player; player = Entities.FindByClassname(player, "player");)
{
if (NetProps.GetPropString(player, "m_szNetworkIDString") != TelemetrySteamID3)
continue
local Chat = @(message) (printl(message), ClientPrint(player, HUD_PRINTCONSOLE, message))
ClientPrint(player, HUD_PRINTTALK, format("\x07FF0000AN ERROR HAS OCCURRED [%s].\nCheck console for details", error))
Chat(format("\n====== TIMESTAMP: %g ======\nAN ERROR HAS OCCURRED [%s]", Time(), error))
Chat("CALLSTACK")
for (local stack, level = 2; stack = getstackinfos(level); level++)
Chat(format("*FUNCTION [%s()] %s line [%d]", stack.func, stack.src, stack.line))
Chat("LOCALS")
local stack = getstackinfos(2)
if (stack)
{
foreach (name, value in stack.locals)
{
local type = type(v)
type == "null" ? Chat(format("[%s] NULL" , name)) :
type == "integer" ? Chat(format("[%s] %d" , name, value)) :
type == "float" ? Chat(format("[%s] %.14g" , name, value)) :
type == "string" ? Chat(format("[%s] \"%s\"", name, value)) :
Chat(format("[%s] %s %s", name, type, value.tostring()))
}
}
return
}
})
Perf Counter Usage
The built-in performance counter requires special considerations to show function names correctly. Otherwise, perf warnings will show main
or <lambda or free run script>
instead of the correct function name more often than not.
A good rule of thumb to largely avoid this problem is to never use name <- function(args)
syntax. This will create an anonymous function that will always print <lambda or free run script>
.
Think Functions
The first two think functions in this example will print the correct function name.
// dummy entity for think function
local test_ent = Entities.CreateByClassname("info_target")
test_ent.KeyValueFromString("targetname", "__test_ent")
test_ent.ValidateScriptScope()
::TestScope <- test_ent.GetScriptScope() // Use this entities scope as a 'namespace'
// WORKS: Define the function in entity scope, prints "Test1" in the perf counter
function TestScope::Test1() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
// AddThinkToEnt( test_ent, "Test1" ) //uncomment to test
// WORKS: Define the function, then add a reference to entity scope
function Test2() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
TestScope.Test2 <- Test2
// AddThinkToEnt( test_ent, "Test2" ) //uncomment to test
// DOES NOT WORK: This will print '<lambda or free run script>'
TestScope.Test3 <- function() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
// AddThinkToEnt( test_ent, "Test3" ) //uncomment to test
A table of functions in entity scope will also correctly print their names as expected, as well as the base think function iterating over the table,
// ... same test entity code from previous example
TestScope.ThinkTable <- {
// WORKS: prints "TableTest1" in the perf counter
function TableTest1() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
// DOES NOT WORK: Perf counter will print "<lambda or free run script>" instead of "TableTest2"
TableTest2 = function() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
}
// main think function that iterates over the table
function TestScope::ThinkTableTest() {
foreach (name, func in TestScope.ThinkTable)
func.call(TestScope)
}
AddThinkToEnt(test_ent, "ThinkTableTest")
// This will also work with functions added to the table after the think function has started.
// WORKS: Function defined in ThinkTable scope, prints "TableTest3" in the perf counter
function TestScope::ThinkTable::TableTest3() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
// DOES NOT WORK: prints "<lambda or free run script>" instead of "TableTest4"
TestScope.ThinkTable.TableTest4 <- function() {
for (local i = 0; i < 100000; i++) {
local a = 1
local b = 2
local c = a + b
}
}
Other Functions
Additionally, you should always call functions from the scope that function is defined in, otherwise the perf counter will almost always print main
.
// ... same test entity code from previous example
// Shows "main" in the perf counter
TestScope.Test1()
// Shows "Test1" in the perf counter
TestScope.Test1.call(this) // NOTE: .call is expensive and will cause perf warnings
// Shows "main" in the perf counter
test_ent.AcceptInput("RunScriptCode", "Test1()", null, null)
// Shows "Test1" in the perf counter
test_ent.AcceptInput("CallScriptFunction", "Test1", null, null)