Ingame menu for server plugins (CS:S only): Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
m (Robot: fixing template case.)
(Fixed CreateMenu prototype. Fixed ShowMenu usermessage index. Added MessageEnd(). Fixed string comparison typo-- should have been "menuselect" not "testmenu". Now confirmed to be fully functional.)
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
{{Tutpov}}
== Introduction and Preparation ==
'''It should be noted, before you start, that this method is mod dependent and that this menu-system is not enabled in the standard SDK. This menu only seems to function in [[Counter-Strike: Source]]. Use of the simpler [[Server Plugins|IServerPluginHelpers]] interface is preferred.'''
'''Note:''' This method of sending menus to clients is mod-dependent and that this '''usermessage''' is not enabled in the SDK. It has been confirmed to function on [[Counter-Strike: Source]] and games running on the Episode 2 engine ([[Team Fortress 2]], [[Day of Defeat: Source]], etc. Also note the menus on Episode 2 games close after 4 seconds-- a fix is yet to be released). Use of the simpler [[Server Plugins|IServerPluginHelpers]] interface is preferred.


An in-game menu is a '''usermessage''' that is distributed to clients using the <code>[[IVEngineServer]]::UserMessageBegin</code> function. This function returns a pointer to a <code>bf_write</code>. To use <code>bf_write</code>, <code>tier1/bitbuf.h</code> needs to be included. One of the parameters of <code>[[IVEngineServer]]::UserMessageBegin</code> is a pointer to a <code>IRecipientFilter</code>. <code>IRecipientFilter</code> is an abstract class, this means that we cannot instantiate (create instances of) it and use it. A class needs to be derived from it and then implemented, the code of which is below:


== Creating an in-game menu for server plugins ==
=== MRecipientFilter.h ===
<source lang="cpp">
#ifndef _MRECIPIENT_FILTER_H
#define _MRECIPIENT_FILTER_H
 
#include "irecipientfilter.h"
#include "bitvec.h"
#include "tier1/utlvector.h"
 
class MRecipientFilter : public IRecipientFilter
{
public:
MRecipientFilter(void) {};
~MRecipientFilter(void) {};
 
virtual bool IsReliable( void ) const { return false; }
virtual bool IsInitMessage( void ) const { return false; }
 
virtual int GetRecipientCount( void ) const;
virtual int GetRecipientIndex( int slot ) const;
void AddAllPlayers();
void AddRecipient(int iPlayer);


The in-game menu is a usermessage so we will need to include <code>src/tier1/bitbuf.cpp</code> into the project. This needs to be added to the project and then bitbuf.h needs to be included in <code>serverplugin_empty.cpp</code>
private:
CUtlVector<int> m_Recipients;
};


We will also need a Recipient Filter, you can use the one from mosca.br at [http://www.hl2coding.com/forums/viewtopic.php?t=31 HL2Coding]
#endif
</source>


=== MRecipientFilter.cpp ===
=== MRecipientFilter.cpp ===
<pre>
<source lang="cpp">
#include "MRecipientFilter.h"
#include "MRecipientFilter.h"
#include "interface.h"
#include "interface.h"
Line 28: Line 53:
extern IPlayerInfoManager *playerinfomanager;
extern IPlayerInfoManager *playerinfomanager;
extern IServerPluginHelpers *helpers;
extern IServerPluginHelpers *helpers;
extern CGlobalVars *gpGlobals;


// memdbgon must be the last include file in a .cpp file!!!
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#include "tier0/memdbgon.h"
MRecipientFilter::MRecipientFilter(void)
{
}
MRecipientFilter::~MRecipientFilter(void)
{
}


int MRecipientFilter::GetRecipientCount() const
int MRecipientFilter::GetRecipientCount() const
Line 47: Line 65:
int MRecipientFilter::GetRecipientIndex(int slot) const
int MRecipientFilter::GetRecipientIndex(int slot) const
{
{
if ( slot < 0 || slot >= GetRecipientCount() )
if(slot < 0 || slot >= GetRecipientCount())
return -1;
return -1;


return m_Recipients[ slot ];
return m_Recipients[slot];
}
 
bool MRecipientFilter::IsInitMessage() const
{
return false;
}
 
bool MRecipientFilter::IsReliable() const
{
return false;
}
}


void MRecipientFilter::AddAllPlayers(int maxClients)
void MRecipientFilter::AddAllPlayers()
{
{
m_Recipients.RemoveAll();
m_Recipients.RemoveAll();
for ( int i = 1; i <= maxClients; i++ )
for(int i = 1; i <= gpGlobals->maxClients; i++)
{
{
edict_t *pPlayer = engine->PEntityOfEntIndex(i);
edict_t *pPlayer = engine->PEntityOfEntIndex(i);
if ( !pPlayer || pPlayer->IsFree())
if(!pPlayer || pPlayer->IsFree())
continue;
continue;
//AddRecipient( pPlayer );
m_Recipients.AddToTail(i);
m_Recipients.AddToTail(i);
}
}
}  
}  
void MRecipientFilter::AddRecipient( int iPlayer )
void MRecipientFilter::AddRecipient(int iPlayer)
{
{
// Already in list
// Return if the recipient is already in the vector
if ( m_Recipients.Find( iPlayer ) != m_Recipients.InvalidIndex() )
if(m_Recipients.Find(iPlayer) != m_Recipients.InvalidIndex())
return;
// Make sure the player is valid
edict_t* pPlayer = engine->PEntityOfEntIndex(iPlayer);
if(!pPlayer || pPlayer->IsFree())
return;
return;
 
m_Recipients.AddToTail( iPlayer );
m_Recipients.AddToTail(iPlayer);
}
}
</pre>
</source>
 
=== MRecipientFilter.h ===
<pre>
#ifndef _MRECIPIENT_FILTER_H
#define _MRECIPIENT_FILTER_H
#include "irecipientfilter.h"
#include "bitvec.h"
#include "tier1/utlvector.h"


class MRecipientFilter : public IRecipientFilter
=== CreateMenu helper ===
To simplify sending the menu, the below helper writes the '''usermessage''' data to a <code>bf_write</code>:
<source lang="cpp">
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions, int iSecondsToStayOpen)
{
{
public:
Assert(pBuffer);
MRecipientFilter(void);
~MRecipientFilter(void);


virtual bool IsReliable( void ) const;
// Add option to bits
virtual bool IsInitMessage( void ) const;
int optionBits = 0;
for(int i = 0; i < nOptions; i++)
optionBits |= (1<<i);


virtual int GetRecipientCount( void ) const;
pBuffer->WriteShort(optionBits); // Write options
virtual int GetRecipientIndex( int slot ) const;
pBuffer->WriteChar(iSecondsToStayOpen); // Seconds to stay open
void AddAllPlayers( int maxClients );
pBuffer->WriteByte(false); // We don't need to receive any more of this menu
void AddRecipient (int iPlayer );
pBuffer->WriteString(szMessage); // Write the menu message
}
</source>


private:
Simply add it to the end of your <code>serverplugin_empty.cpp</code> file and the following just below the include's of the file:
bool m_bReliable;
<source lang="cpp">
bool m_bInitMessage;
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions=10, int iSecondsToStayOpen=-1);
CUtlVector< int > m_Recipients;
</source>
};


#endif
An example on how to use the function is shown in the next section.
</pre>


=== Usage ===
== Example usage ==
These both need adding the project and have the line in the includes at the top or <code>serverplugin_empty.cpp</code>
Add the above to files to the project and add the following line to <code>serverplugin_empty.cpp</code>:
#include "MRecipientFilter.h"
<source lang="cpp">
We also need a function to get the clients index from their userid.
#include "MRecipientFilter.h"
</source>
It will include the <code>MRecipientFilter</code> class definition so you can use it in your plugin.


'''Edit: '''This is not actually needed, valve have already got a function: engine->IndexOfEdict(edict_t *)
Find the function <code>CServerPluginEmpty::ClientCommand</code> in your <code>serverplugin_empty.cpp</code> file and insert the following below the last '''return''':
<pre>int getIndexFromUserID(int userid)
<source lang="cpp">
{
if(FStrEq(engine->Cmd_Argv(0), "testmenu"))
edict_t *player;
IPlayerInfo *info;
for(int i = 1; i <= maxplayers; i++) //int maxplayers; has to be added after the includes and maxplayers=clientMax; in the ServerActivate function
{
{
player = engine->PEntityOfEntIndex(i);
// Create a filter and add this client to it
if(!player || player->IsFree() )
MRecipientFilter filter;
continue;
filter.AddRecipient(engine->IndexOfEdict(pEntity));
info = playerinfomanager->GetPlayerInfo(player);


if(info->GetUserID() == userid)
// Start the usermessage and get a bf_write
return i;
bf_write* pBuffer = engine->UserMessageBegin(&filter, 10);
}
return -1;
}</pre>


== Making the menu ==
// Send the menu
 
CreateMenu(pBuffer, "->1. Say hello\n->2. Say bye\n->3. Exit", 3);
I have used made the menu as a client command activated on the command a_menu.
 
engine->MessageEnd();
<pre>
if ( FStrEq( pcmd, "a_menu" ) )
{
/*IPlayerInfo *playerInfo = playerinfomanager->GetPlayerInfo(pEntity);
MRecipientFilter filter;
filter.AddRecipient(getIndexFromUserID(playerInfo->GetUserID()));
                No need what so ever of The Custom function Valve have there own
                engine->IndexOfEdict(pEntity);
                */


MRecipientFilter filter;
filter.AddRecipient(engine->IndexOfEdict(pEntity));
bf_write *pBuffer = engine->UserMessageBegin( &filter, 10 );
pBuffer->WriteShort( (1<<0) | (1<<1) | (1<<2) ); //Sets how many options the menu has
pBuffer->WriteChar( -1 ); //Sets how long the menu stays open -1 for stay until option selected
pBuffer->WriteByte( false ); //true if there is more string yet to be received before displaying the menu, false otherwise
pBuffer->WriteString( "1.Assault Rifle\n2.AWP\n3.Exit" ); //The text shown on the menu
engine->MessageEnd();
return PLUGIN_STOP;
return PLUGIN_STOP;
}
}
</source>


</pre>
'''Note:''' Lines in a menu are split up by using the <code>\n</code> character.<br />
 
'''Tip:''' You can color an option in on a menu by starting the line with <code>->1.</code>, <code>->2.</code>, <code>->3.</code> through <code>->9.</code>
This menu will be an alternative buy menu that will buy ammo, armor and grenades, as well as the primary weapon.
 
The WriteShort contains a number of bits which tell the game which options are enabled for the menu.


*(1<<0) enables 1
When in-game, executing <code>testmenu</code> in the client console will result in a menu of 3 menus, structured like:
*(1<<1) enables 2
1. Say hello
*etc..
2. Say bye
3. Exit


To enable 4 and 7, one would use:
Selecting any option will simply result in the menu disappearing-- this is not useful. The next section will focus on processing the results from 1-9 key presses.
(1<<3) | (1<<6)


If you have done this correcly so far then you should have the menu show up in game when the client types a_menu.
=== Processing the client commands ===
Pressing a key while a menu is open will result in a '''menuselect''' command automatically being executed by the client and eventually making its way to the plugin where we can process it.


== Making commands for the menu ==
The command has only one parameter, a number, which is the respective menu option the client selected.
The menu run the command menuselect with the parameter as the option selected, so for this menu the commands would be <code>menuselect 1</code>, <code>menuselect 2</code>, and <code>menuselect 3</code>


<pre>
Find the function <code>CServerPluginEmpty::ClientCommand</code> in your <code>serverplugin_empty.cpp</code> file and insert the following below the last '''return''':
else if ( FStrEq( pcmd, "menuselect" ) )
<source lang="cpp">
if(FStrEq(engine->Cmd_Argv(0), "menuselect"))
{
{
const char *parameter = engine->Cmd_Argv(1);
switch(atoi(engine->Cmd_Argv(1)))
switch(atoi(parameter))
{
{
case 1:
case 1:
engine->ClientCommand (pEntity, "buy m4a1;buy ak47;buy primammo;buy vesthelm;buy deagle;buy secammo;buy flashbang;buy hegrenade;buy flashbang\n");
helpers->ClientCommand(pEntity, "say Hello");
break;
break;
case 2:
case 2:
engine->ClientCommand (pEntity, "buy awp;buy primammo;buy vesthelm;buy deagle;buy secammo;buy flashbang;buy hegrenade;buy flashbang\n");
helpers->ClientCommand(pEntity, "say Bye");
break;
break;
case 3:
case 3:
engine->ClientPrintf(pEntity, "Menu Exited");
engine->ClientPrintf(pEntity, "Menu exited\n");
break;
break;
}
}
return PLUGIN_STOP;
return PLUGIN_STOP;
}   
}   
</pre>
</source>


This will make the client run the commands to buy weapons ammo armor and grenades. The menu runs the commands.
The above code is trivial: <code>atoi</code> converts the first argument from '''a'''SCII '''to''' an '''i'''nteger which we can perform conditional operations upon.
If the value is '''1''', we make the client <code>say Hello</code>. If the value is '''2''', we make the client <code>say Bye</code>. If the value is '''3''' then we print '''Menu exited''' in the client's console.


[[Category:Tutorials]]
[[Category:Tutorials]]
[[Category:Programming]]
[[Category:Programming]]

Latest revision as of 07:03, 15 April 2009

Introduction and Preparation

Note: This method of sending menus to clients is mod-dependent and that this usermessage is not enabled in the SDK. It has been confirmed to function on Counter-Strike: Source and games running on the Episode 2 engine (Team Fortress 2, Day of Defeat: Source, etc. Also note the menus on Episode 2 games close after 4 seconds-- a fix is yet to be released). Use of the simpler IServerPluginHelpers interface is preferred.

An in-game menu is a usermessage that is distributed to clients using the IVEngineServer::UserMessageBegin function. This function returns a pointer to a bf_write. To use bf_write, tier1/bitbuf.h needs to be included. One of the parameters of IVEngineServer::UserMessageBegin is a pointer to a IRecipientFilter. IRecipientFilter is an abstract class, this means that we cannot instantiate (create instances of) it and use it. A class needs to be derived from it and then implemented, the code of which is below:

MRecipientFilter.h

#ifndef _MRECIPIENT_FILTER_H
#define _MRECIPIENT_FILTER_H

#include "irecipientfilter.h"
#include "bitvec.h"
#include "tier1/utlvector.h"

class MRecipientFilter : public IRecipientFilter
{
public:
	MRecipientFilter(void) {};
	~MRecipientFilter(void) {};

	virtual bool IsReliable( void ) const { return false; }
	virtual bool IsInitMessage( void ) const { return false; }

	virtual int GetRecipientCount( void ) const;
	virtual int GetRecipientIndex( int slot ) const;
	void AddAllPlayers();
	void AddRecipient(int iPlayer);

private:
	CUtlVector<int> m_Recipients;
};

#endif

MRecipientFilter.cpp

#include "MRecipientFilter.h"
#include "interface.h"
#include "filesystem.h"
#include "engine/iserverplugin.h"
#include "dlls/iplayerinfo.h"
#include "eiface.h"
#include "igameevents.h"
#include "convar.h"
#include "Color.h"

#include "shake.h"
#include "IEffects.h"
#include "engine/IEngineSound.h"

extern IVEngineServer		*engine;
extern IPlayerInfoManager	*playerinfomanager;
extern IServerPluginHelpers	*helpers;
extern CGlobalVars		*gpGlobals;

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

int MRecipientFilter::GetRecipientCount() const
{
	return m_Recipients.Size();
}

int MRecipientFilter::GetRecipientIndex(int slot) const
{
	if(slot < 0 || slot >= GetRecipientCount())
		return -1;

	return m_Recipients[slot];
}

void MRecipientFilter::AddAllPlayers()
{
	m_Recipients.RemoveAll();
	
	for(int i = 1; i <= gpGlobals->maxClients; i++)
	{
		edict_t *pPlayer = engine->PEntityOfEntIndex(i);
		
		if(!pPlayer || pPlayer->IsFree())
			continue;
		
		m_Recipients.AddToTail(i);
	}
} 
void MRecipientFilter::AddRecipient(int iPlayer)
{
	// Return if the recipient is already in the vector
	if(m_Recipients.Find(iPlayer) != m_Recipients.InvalidIndex())
		return;
	
	// Make sure the player is valid
	edict_t* pPlayer = engine->PEntityOfEntIndex(iPlayer);
	if(!pPlayer || pPlayer->IsFree())
		return;
	
	m_Recipients.AddToTail(iPlayer);
}

CreateMenu helper

To simplify sending the menu, the below helper writes the usermessage data to a bf_write:

void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions, int iSecondsToStayOpen)
{
	Assert(pBuffer);

	// Add option to bits
	int optionBits = 0;
	for(int i = 0; i < nOptions; i++)
		optionBits |= (1<<i);

	pBuffer->WriteShort(optionBits); // Write options
	pBuffer->WriteChar(iSecondsToStayOpen); // Seconds to stay open
	pBuffer->WriteByte(false); // We don't need to receive any more of this menu
	pBuffer->WriteString(szMessage); // Write the menu message
}

Simply add it to the end of your serverplugin_empty.cpp file and the following just below the include's of the file:

void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions=10, int iSecondsToStayOpen=-1);

An example on how to use the function is shown in the next section.

Example usage

Add the above to files to the project and add the following line to serverplugin_empty.cpp:

#include "MRecipientFilter.h"

It will include the MRecipientFilter class definition so you can use it in your plugin.

Find the function CServerPluginEmpty::ClientCommand in your serverplugin_empty.cpp file and insert the following below the last return:

	if(FStrEq(engine->Cmd_Argv(0), "testmenu"))
	{
		// Create a filter and add this client to it
		MRecipientFilter filter;
		filter.AddRecipient(engine->IndexOfEdict(pEntity));

		// Start the usermessage and get a bf_write
		bf_write* pBuffer = engine->UserMessageBegin(&filter, 10);

		// Send the menu
		CreateMenu(pBuffer, "->1. Say hello\n->2. Say bye\n->3. Exit", 3);
		
		engine->MessageEnd();

		return PLUGIN_STOP;
	}

Note: Lines in a menu are split up by using the \n character.
Tip: You can color an option in on a menu by starting the line with ->1., ->2., ->3. through ->9.

When in-game, executing testmenu in the client console will result in a menu of 3 menus, structured like:

1. Say hello
2. Say bye
3. Exit

Selecting any option will simply result in the menu disappearing-- this is not useful. The next section will focus on processing the results from 1-9 key presses.

Processing the client commands

Pressing a key while a menu is open will result in a menuselect command automatically being executed by the client and eventually making its way to the plugin where we can process it.

The command has only one parameter, a number, which is the respective menu option the client selected.

Find the function CServerPluginEmpty::ClientCommand in your serverplugin_empty.cpp file and insert the following below the last return:

	if(FStrEq(engine->Cmd_Argv(0), "menuselect"))
	{
		switch(atoi(engine->Cmd_Argv(1)))
		{
		case 1:
			helpers->ClientCommand(pEntity, "say Hello");
			break;
		case 2:
			helpers->ClientCommand(pEntity, "say Bye");
			break;
		case 3:
			engine->ClientPrintf(pEntity, "Menu exited\n");
			break;
		}

		return PLUGIN_STOP;
	}

The above code is trivial: atoi converts the first argument from aSCII to an integer which we can perform conditional operations upon. If the value is 1, we make the client say Hello. If the value is 2, we make the client say Bye. If the value is 3 then we print Menu exited in the client's console.