Difference between revisions of "Listening to game events in CS:GO"

From Valve Developer Community
Jump to: navigation, search
m (Fix)
m
 
(8 intermediate revisions by the same user not shown)
Line 1: Line 1:
CS:GO vscripts API is missing crucial functions for event listening. However, you can still use the event data fetched from [[logic_eventlistener]]s in your custom maps. Fetching event data option was added to CS:GO in 2016.
+
CS:GO [[VScript|vscript]] API is missing crucial functions for event listening. However, event data fetched from [[logic_eventlistener]]s in custom maps can still be used.
  
There are multiple ways of acessing the script scope of an event listener, which is where the event data are dumped. Only one method will be demonstrated here.
+
There are multiple ways of acessing 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 <code>/resources/gameevents.res</code> and <code>/resources/modevents.res</code> files.
  
You can find event data contents in <code>/resources/*events.res</code> files.
 
  
 
== Setting up ==
 
== Setting up ==
 
Create your event listener in Hammer Editor, add targetname and enable the <code>FetchEventData</code> keyvalue. Add an output that executes an ''OnGameEvent_'' function in itself with the <code>event_data</code> parameter. Note that the targetname and function names are arbitrary, but it is always good practice to be consistent.
 
Create your event listener in Hammer Editor, add targetname and enable the <code>FetchEventData</code> keyvalue. Add an output that executes an ''OnGameEvent_'' function in itself with the <code>event_data</code> parameter. Note that the targetname and function names are arbitrary, but it is always good practice to be consistent.
  
Example event <code>item_purchase</code>  
+
Example event <code>player_say</code>
  
 
logic_eventlistener keyvalues:
 
logic_eventlistener keyvalues:
  
 
<pre>
 
<pre>
targetname: item_purchase
+
targetname: player_say
EventName:  item_purchase
+
EventName:  player_say
 
FetchEventData: Yes
 
FetchEventData: Yes
 
</pre>
 
</pre>
Line 21: Line 22:
  
 
<pre>
 
<pre>
OnEventFired > item_purchase > RunScriptCode > ::OnGameEvent_item_purchase(event_data)
+
OnEventFired > player_say > RunScriptCode > ::OnGameEvent_player_say(event_data)
 
</pre>
 
</pre>
  
In your script, create your event callback function and bind it using <code>.bindenv(this)</code>:
+
In your script, create your event callback function and bind it using <code>.bindenv(this)</code>. This binding will ensure the call environment of the event function is always your script, and allow you to call any function in the body of the script file it is put in.
  
 
<source lang=cpp>
 
<source lang=cpp>
::OnGameEvent_item_purchase <- function(data)
+
::OnGameEvent_player_say <- function( event )
 
{
 
{
ScriptPrintMessageChatAll(data.weapon + " is purchased.")
+
ScriptPrintMessageChatAll( event.userid + " says " + event.text )
 
}.bindenv(this) // environment binding
 
}.bindenv(this) // environment binding
 
</source>
 
</source>
  
Alternatively you can bind named functions. This would also let you dynamically change the event callback function in runtime.
+
Alternatively named functions can be bound and assigned. This would also let you dynamically change the event callback function back and forth in runtime.
  
 
<source lang=cpp>
 
<source lang=cpp>
 
// Your event function
 
// Your event function
function OnItemPurchase(data)
+
function OnPlayerSay( event )
 
{
 
{
ScriptPrintMessageChatAll(data.weapon + " is purchased.")
+
ScriptPrintMessageChatAll( event.userid + " says " + event.text )
 
}
 
}
  
::OnGameEvent_item_purchase <- OnItemPurchase.bindenv(this)
+
::OnGameEvent_player_say <- OnPlayerSay.bindenv(this)
  
// these can be done anywhere in the code
+
// these can be done anywhere in the code to change the event callback
// ::OnGameEvent_item_purchase = OnItemPurchase2.bindenv(this)
+
// ::OnGameEvent_player_say = OnPlayerSay2.bindenv(this)
// ::OnGameEvent_item_purchase = OnItemPurchase3.bindenv(this)
+
// ::OnGameEvent_player_say = OnPlayerSay3.bindenv(this)
 
</source>
 
</source>
 +
  
 
== Getting player userid, SteamID and Steam names ==
 
== Getting player userid, SteamID and Steam names ==
As mentioned before, the API to get these 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.
+
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.
  
You can simplify all of this by using [https://github.com/samisalreadytaken/vs_library vs_library], a third party vscript library that handles it for you.
+
This can be simplified by using [https://github.com/samisalreadytaken/vs_library vs_library], a third party vscript library that completely automates this, and adds Source 2 style dynamic event listening with <code>VS.ListenToGameEvent</code>.
  
After following instructions on installation and setting up required event listeners, you can get player info from their script scope, and get player script handles from their userids using <code>VS.GetPlayerByUserid</code>.
+
After following instructions on installation and setting up the required entities, player info can be got from their script scope, and player script handles from their userids using <code>VS.GetPlayerByUserid</code>.
  
Example code that prints Steam names of players in the <code>player_death</code> event:
+
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)
  
 
<source lang=cpp>
 
<source lang=cpp>
::OnGameEvent_player_death <- function(data)
+
IncludeScript("vs_events");
 +
 
 +
function OnPlayerSay( event )
 
{
 
{
local victim = VS.GetPlayerByUserid(data.userid)
+
// get the chat message
local attacker = VS.GetPlayerByUserid(data.attacker)
+
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)
 +
local argv = split( msg, " " );
 +
local argc = argv.len();
 +
 
 +
// 'argv[0]' is the command
 +
// values separated with " " can be accessed with 'argv[1]', 'argv[2]'...
  
// initial names
+
local value;
local vicName = "NULL"
+
if ( argc > 1 )
local attName = "NULL"
+
value = argv[1];
  
// either of players can be null if they were not found
+
// Your chat commands are string cases in this switch statement.
if( victim )
+
// 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() )
 
{
 
{
vicName = victim.GetScriptScope().name
+
// multiple chat messages can execute the same code
 +
case "!hp":
 +
case "!health":
 +
{
 +
CommandSetHealth( player, value );
 +
break;
 +
}
 +
// default:
 +
// Msg("Invalid chat command '"+msg+"'.\n");
 
}
 
}
 +
}
 +
 +
// register
 +
VS.ListenToGameEvent( "player_say", OnPlayerSay.bindenv(this), "" );
 +
 +
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(e){ return }
 +
 +
// clamp the value
 +
if ( health < 1 )
 +
health = 1;
 +
 +
player.SetHealth( health );
 +
 +
local sc = player.GetScriptScope();
 +
 +
ScriptPrintMessageChatAll(format( "%s (%s) set their health to %d", sc.name, sc.networkid, health ));
 +
}
 +
</source>
 +
 +
 +
=== 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.
 +
 +
 +
== 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 <code>VS.ListenToGameEvent</code>.
  
if( attacker )
+
<source lang=cpp>
{
+
VS.ListenToGameEvent( "bullet_impact", function( event )
attName = attacker.GetScriptScope().name
+
{
}
+
local pos = Vector( event.x, event.y, event.z );
 +
local ply = VS.GetPlayerByUserid( event.userid );
  
printl(attName + " KILLED " + vicName + " USING " + data.weapon)
+
DebugDrawLine( ply.EyePosition(), pos, 255,0,0,false, 2.0 );
}.bindenv(this)
+
DebugDrawBox( pos, Vector(-2,-2,-2), Vector(2,2,2), 255,0,255,127, 2.0 );
 +
}, "DrawImpact" );
 
</source>
 
</source>
 +
  
 
== External links ==
 
== External links ==
Line 90: Line 167:
 
* [[List of Counter-Strike: Global Offensive Script Functions]]
 
* [[List of Counter-Strike: Global Offensive Script Functions]]
 
* [[L4D2 EMS/Appendix: Game Events|L4D2 Game Events]]
 
* [[L4D2 EMS/Appendix: Game Events|L4D2 Game Events]]
* [[VScript]]
 

Latest revision as of 12:49, 30 September 2021

CS:GO vscript API 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 acessing 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 /resources/gameevents.res and /resources/modevents.res files.


Setting up

Create your event listener in Hammer Editor, add targetname and enable the FetchEventData keyvalue. Add an output that executes an OnGameEvent_ function in itself with the event_data parameter. Note that the targetname and function names are arbitrary, but it is always good practice to be consistent.

Example event player_say

logic_eventlistener keyvalues:

targetname: player_say
EventName:  player_say
FetchEventData: Yes

logic_eventlistener output:

OnEventFired > player_say > RunScriptCode > ::OnGameEvent_player_say(event_data)

In your script, create your event callback function and bind it using .bindenv(this). This binding will ensure the call environment of the event function is always your script, and allow you to call any function 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 function
function OnPlayerSay( event )
{
	ScriptPrintMessageChatAll( event.userid + " says " + event.text )
}

::OnGameEvent_player_say <- OnPlayerSay.bindenv(this)

// these can be done anywhere in the code to change the event callback
// ::OnGameEvent_player_say = OnPlayerSay2.bindenv(this)
// ::OnGameEvent_player_say = OnPlayerSay3.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 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)
	local argv = split( msg, " " );
	local argc = argv.len();

	// 'argv[0]' is the command
	// values separated with " " can be accessed with 'argv[1]', 'argv[2]'...

	local value;
	if ( argc > 1 )
		value = argv[1];

	// 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":
		{
			CommandSetHealth( player, value );
			break;
		}
	//	default:
	//		Msg("Invalid chat command '"+msg+"'.\n");
	}
}

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

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(e){ return }

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

	player.SetHealth( health );

	local sc = player.GetScriptScope();

	ScriptPrintMessageChatAll(format( "%s (%s) set their health to %d", sc.name, sc.networkid, health ));
}


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.


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.

VS.ListenToGameEvent( "bullet_impact", function( event )
{
	local pos = Vector( event.x, event.y, event.z );
	local ply = VS.GetPlayerByUserid( event.userid );

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


External links

See also