Listening to game events in CS:GO

From Valve Developer Community
Jump to: navigation, search
Icon-delisted.png
This page documents information about a game, Counter-Strike: Global Offensive Counter-Strike: Global Offensive, that is no longer available for purchase or download digitally.
It is covered here for historical and technical reference.
English (en)
Edit

The VScript API in Counter-Strike: Global Offensive Counter-Strike: Global Offensive is missing crucial functions for event listening; however, event data fetched from logic_eventlisteners in custom maps can still be used.

There are multiple ways of accessing the script scope of an event listener, which is where the event data is dumped. Only one method will be demonstrated here.

Event data contents can be found in the files csgo/resources/gameevents.res and csgo/resources/modevents.res.


Setting up

Create your event listener in the Hammer Editor, add a targetname and enable the FetchEventData keyvalue. Add an output that executes your event function in itself with the event_data parameter.

Note.pngNote:The targetname and function names are arbitrary.

Example event player_say

Create a logic_eventlistener with the following keyvalues and outputs:

Property Name Value
Name player_say
EventName player_say
FetchEventData Yes
  My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnEventFired player_say RunScriptCode ::OnGameEvent_player_say(event_data) 0.00 No

In your script (e.g. an Entity Script of an entity), create your event callback function, make it global using :: and bind it using .bindenv(this) as seen in the following example. This binding will ensure the call environment of the event function is always your script, and allow you to access variables in the body of the script file it is put in.

::OnGameEvent_player_say <- function( event )
{
	ScriptPrintMessageChatAll( event.userid + " says " + event.text )
}.bindenv(this) // environment binding

Alternatively named functions can be bound and assigned. This would also let you dynamically change the event callback function back and forth in runtime.

// Your event callback
function OnPlayerSay1( event )
{
	printl( "callback 1 : " + event.text )
	ScriptPrintMessageChatAll( event.userid + " says " + event.text )

	// Change callback function
	::OnGameEvent_player_say = OnPlayerSay2.bindenv(this)
}

// Another event callback
function OnPlayerSay2( event )
{
	printl( "callback 2 : " + event.text )

	// Change callback function
	::OnGameEvent_player_say = OnPlayerSay1.bindenv(this)
}

// Initial setup
::OnGameEvent_player_say <- OnPlayerSay1.bindenv(this)


Getting player userid, SteamID and Steam names

The API to get this information is not available. The workaround to getting userids is listening to an event that dumps a userid which was triggered by a known player, then getting SteamIDs from the player_connect event data using these associated userids.

This can be simplified by using vs_library, a third party vscript library that completely automates this, and adds Source 2 style dynamic event listening with VS.ListenToGameEvent().

After following instructions on installation and manually setting up the required entities, player info can be got from their script scope, and player script handles from their userids using VS.GetPlayerByUserid().

Example code that sets the health of the player that types "!hp" in chat: (note that there is no need to create event listener entities in Hammer while using vs_library)

IncludeScript("vs_events");

function OnPlayerSay( event )
{
	// get the chat message
	local msg = event.text;

	// require all chat commands to be prepended with a symbol (!)
	// if the message is not a command, leave
	if ( msg[0] != '!' )
		return;

	// Get the player. Always valid, NULL only if disconnected
	local player = VS.GetPlayerByUserid( event.userid );

	// tokenise the message (split by spaces)
	// 'argv[0]' is the command
	// values separated with " " can be accessed with 'argv[1]', 'argv[2]'...
	local argv = split( msg, " " );
	local argc = argv.len();

	// Your chat commands are string cases in this switch statement.
	// Strings are case sensitive.
	// To make them insensitive, 'tolower' can be added to the command string.
	// In that case, every case string needs to be lower case.
	switch ( argv[0].tolower() )
	{
		// multiple chat messages can execute the same code
		case "!hp":
		case "!health":
		{
			local value;
			if ( argc > 1 )
				value = argv[1];

			CommandSetHealth( player, value );
			break;
		}
		case "!kill":
		{
			EntFireByHandle( player, "SetHealth", "0", 0, null, null );

			local name = player.GetScriptScope().name;
			ScriptPrintMessageChatAll( name + " bid farewell, cruel world!" );
			break;
		}
		default:
			Msg("Invalid chat command '"+msg+"'\n");
	}
}

function CommandSetHealth( player, health )
{
	// if health is null, the message did not have a value
	// if player is null, the player was not found
	if ( !health || !player )
		return;

	// 'value' is string, convert to int
	// invalid conversion throws excpetion
	try
	{
		health = health.tointeger();
	}
	// invalid value
	catch( err )
	{
		return;
	}

	// clamp the value
	if ( health < 1 )
		health = 1;

	player.SetHealth( health );

	// echo in chat
	local sc = player.GetScriptScope();
	ScriptPrintMessageChatAll(format( "%s (%s) set their health to %d", sc.name, sc.networkid, health ));
}

// register
VS.ListenToGameEvent( "player_say", OnPlayerSay.bindenv(this), "" );


Use on dedicated servers

It is not possible to get the Steam name and SteamIDs of human players that were connected to a server prior to a map change because the player_connect event is fired only once when a player connects to the server. This data will only be available for players that connect to the server while your map is running.

vs_library fixes this for listen servers, and players are guaranteed to have valid SteamIDs.


Listening for events fired multiple times in a frame

While event listeners dump the event data whenever events are fired, entity outputs are added to the event queue to be executed in the next frame. Because of this delay, when an event is fired multiple times before the output is fired - before the script function is executed via the output - previous events would be lost.

vs_library fixes this when using VS.ListenToGameEvent().

IncludeScript("vs_library");

//
// All players spawn in the same frame on round start
//
VS.ListenToGameEvent( "player_spawn", function( event )
{
	local player = ToExtendedPlayer( VS.GetPlayerByUserid( event.userid ) );
	if ( !player )
		return;

	local uid = player.GetNetworkIDString();
	local name = player.GetPlayerName();

	if ( player.IsBot() )
	{
		print(format( "\tBOT %s has spawned.\n", name ));
	}
	else
	{
		print(format( "\t%s has spawned. [%s]\n", name, uid ));
	}
}, "SpawnNotify" );

//
// Event fired for each impact point, including penetration points and shotgun pellets
//
VS.ListenToGameEvent( "bullet_impact", function( event )
{
	local position = Vector( event.x, event.y, event.z );
	local player = VS.GetPlayerByUserid( event.userid );

	DebugDrawLine( player.EyePosition(), position, 255, 0, 0, false, 2.0 );
	DebugDrawBox( position, Vector(-2,-2,-2), Vector(2,2,2), 255, 0, 255, 127, 2.0 );
}, "DrawImpact" );

//
// Assign a name label to each player to easily display it on player_death event below
//
VS.ListenToGameEvent( "player_spawn", function( event )
{
	local player = VS.GetPlayerByUserid( event.userid );
	if ( !player )
		return;

	local scope = player.GetScriptScope();

	// already done
	if ( "name_label" in scope )
		return;

	// 'networkid', 'name' and 'userid' are default variables in player script scopes that contain player info.
	if ( scope.networkid == "BOT" )
	{
		scope.name_label <- "BOT " + scope.name;
	}
	else
	{
		scope.name_label <- scope.name;
	}
}, "PlayerEvents" );

//
// Print death events in console
//
VS.ListenToGameEvent( "player_death", function( event )
{
	local victim = VS.GetPlayerByUserid( event.userid );
	local attacker = VS.GetPlayerByUserid( event.attacker );
	local assister = VS.GetPlayerByUserid( event.assister );

	// disconnected
	if ( !victim )
		return;

	local victimName = victim.GetScriptScope().name_label;

	// suicide
	if ( victim == attacker || event.weapon == "world" )
	{
		if ( assister )
		{
			print(format( "   %s finished off %s\n", assister.GetScriptScope().name_label, victimName ));
		}
		else
		{
			print(format( "   %s bid farewell, cruel world!\n", victimName ));
		}

		return;
	}

	// fall
	if ( event.weapon == "worldspawn" )
	{
		if ( assister )
		{
			print(format( "   %s finished off %s\n", assister.GetScriptScope().name_label, victimName ));
		}
		else
		{
			print(format( "   %s fell to a clumsy, painful death\n", victimName ));
		}

		return;
	}

	// Inject informational indicators in the text
	local infoAttacker = "";
	local infoWeapon = "";

	if ( event.attackerblind )
		infoAttacker += "[Ø] ";

	if ( event.penetrated )
		infoWeapon += "(P)";

	if ( event.headshot )
		infoWeapon += "(H)";

	if ( event.noscope )
		infoWeapon += "(N)";

	if ( event.thrusmoke )
		infoWeapon += "(S)";

	if ( event.assistedflash )
		infoWeapon += "(F)";

	if ( assister )
	{
		print(format( "   %s%s + %s [%s]%s %s\n",
			infoAttacker,
			attacker.GetScriptScope().name_label,
			assister.GetScriptScope().name_label,
			event.weapon.toupper(),
			infoWeapon,
			victimName ));
	}
	else
	{
		print(format( "   %s%s [%s]%s %s\n",
			infoAttacker,
			attacker.GetScriptScope().name_label,
			event.weapon.toupper(),
			infoWeapon,
			victimName ));
	}
}, "PlayerEvents" );

External links

See also