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

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
 
(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.)
 
(33 intermediate revisions by 12 users not shown)
Line 1: Line 1:
This page from [http://www.sourcewiki.org sourceWiki]
== Introduction and Preparation ==
== Creating an in-game menu for server plugins ==
'''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.


The ingame menu is a usermessage so we will need to include bitbuf.cpp into the project. This can be found at src/tier1/bitbuf.cpp This needs to be added to the project and have
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:
<pre>
#include "bitbuf.h"
</pre>
in the includes at the top of serverplugin_empty.cpp


We will also need a Recipient Filter, I have used one from mosca.br at [http://www.hl2coding.com HL2Coding]
=== MRecipientFilter.h ===
<source lang="cpp">
#ifndef _MRECIPIENT_FILTER_H
#define _MRECIPIENT_FILTER_H


MRecipientFilter.cpp
#include "irecipientfilter.h"
<pre>
#include "bitvec.h"
#include "MRecipientFilter.h"
#include "tier1/utlvector.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"
class MRecipientFilter : public IRecipientFilter
#include "IEffects.h"
{
#include "engine/IEngineSound.h"
public:
MRecipientFilter(void) {};
~MRecipientFilter(void) {};


extern IVEngineServer  *engine;  
virtual bool IsReliable( void ) const { return false; }
extern IPlayerInfoManager *playerinfomanager;
virtual bool IsInitMessage( void ) const { return false; }
extern IServerPluginHelpers *helpers;  


// memdbgon must be the last include file in a .cpp file!!!
virtual int GetRecipientCount( void ) const;
#include "tier0/memdbgon.h"
virtual int GetRecipientIndex( int slot ) const;
void AddAllPlayers();
void AddRecipient(int iPlayer);


MRecipientFilter::MRecipientFilter(void)
private:
{
CUtlVector<int> m_Recipients;
}  
};


MRecipientFilter::~MRecipientFilter(void)
#endif
{
</source>
}


int MRecipientFilter::GetRecipientCount() const
=== MRecipientFilter.cpp ===
{
<source lang="cpp">
  return m_Recipients.Size();
#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"


int MRecipientFilter::GetRecipientIndex(int slot) const
#include "shake.h"
{
#include "IEffects.h"
  if ( slot < 0 || slot >= GetRecipientCount() )
#include "engine/IEngineSound.h"
      return -1;


  return m_Recipients[ slot ];  
extern IVEngineServer *engine;
}
extern IPlayerInfoManager *playerinfomanager;
extern IServerPluginHelpers *helpers;
extern CGlobalVars *gpGlobals;


bool MRecipientFilter::IsInitMessage() const
// memdbgon must be the last include file in a .cpp file!!!
{
#include "tier0/memdbgon.h"
  return false;
}


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


void MRecipientFilter::AddAllPlayers(int maxClients)
int MRecipientFilter::GetRecipientIndex(int slot) const
{
  m_Recipients.RemoveAll();
  int i;
  for ( i = 1; i <= maxClients; i++ )
  {
      edict_t *pPlayer = engine->PEntityOfEntIndex(i);
      if ( !pPlayer || pPlayer->IsFree()) {
        continue;
      }
      //AddRecipient( pPlayer );
      m_Recipients.AddToTail(i);
  }
}
void MRecipientFilter::AddRecipient( int iPlayer )
{
{
  // Already in list
if(slot < 0 || slot >= GetRecipientCount())
  if ( m_Recipients.Find( iPlayer ) != m_Recipients.InvalidIndex() )
return -1;
      return;


  m_Recipients.AddToTail( iPlayer );
return m_Recipients[slot];
}
}
</pre>


and
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);
}
</source>


MRecipientFilter.h
=== CreateMenu helper ===
 
To simplify sending the menu, the below helper writes the '''usermessage''' data to a <code>bf_write</code>:
<pre>
<source lang="cpp">
#ifndef _MRECIIENT_FILTER_H
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions, int iSecondsToStayOpen)
#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;
  virtual bool IsInitMessage( void ) const;
 
  virtual int GetRecipientCount( void ) const;
  virtual int GetRecipientIndex( int slot ) const;
  void AddAllPlayers( int maxClients );
void AddRecipient (int iPlayer );
 
private:
  bool m_bReliable;
  bool m_bInitMessage;
  CUtlVector< int > m_Recipients;
};
 
#endif
</pre>
 
These both need adding the project and have the line in the includes at the top or serverplugin_empty.cpp
<pre>
#include "MRecipientFilter.h"
</pre>
 
We also need a function to get the clients index from their userid.
 
<pre>
int getIndexFromUserID(int userid)
{
{
  edict_t *player;
Assert(pBuffer);
  IPlayerInfo *info;
  for(int i = 1; i <= maxplayers; i++)  //int maxplayers; has to be added after the includes and clientMax=maxplayers; in the ServerActivate function
  {
      player = engine->PEntityOfEntIndex(i);
      if(!player || player->IsFree() )
        continue;
      info = playerinfomanager->GetPlayerInfo(player);


      if(info->GetUserID() == userid)
// Add option to bits
        return i;
int optionBits = 0;
  }
for(int i = 0; i < nOptions; i++)
optionBits |= (1<<i);


  return -1;
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
}
}
</pre>
</source>


----
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:
<source lang="cpp">
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions=10, int iSecondsToStayOpen=-1);
</source>


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


I have used made the menu as a client command activated on the command a_menu.
== Example usage ==
Add the above to files to the project and add the following line to <code>serverplugin_empty.cpp</code>:
<source lang="cpp">
#include "MRecipientFilter.h"
</source>
It will include the <code>MRecipientFilter</code> class definition so you can use it in your plugin.


<pre>
Find the function <code>CServerPluginEmpty::ClientCommand</code> in your <code>serverplugin_empty.cpp</code> file and insert the following below the last '''return''':
if ( FStrEq( pcmd, "a_menu" ) )
<source lang="cpp">
  {  
if(FStrEq(engine->Cmd_Argv(0), "testmenu"))
    IPlayerInfo *playerInfo = playerinfomanager->GetPlayerInfo(pEntity);
{
    MRecipientFilter filter;
// Create a filter and add this client to it
      filter.AddRecipient(getIndexFromUserID(playerInfo->GetUserID()));
MRecipientFilter filter;
    bf_write *pBuffer = engine->UserMessageBegin( &filter, 9 );
filter.AddRecipient(engine->IndexOfEdict(pEntity));
      pBuffer->WriteShort( 7 ); //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 );
      pBuffer->WriteString( "1.Assault Rifle\n2.AWP\n3.Exit" ); //The text shown on the menu
      engine->MessageEnd();
      return PLUGIN_STOP;
  }


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


This menu will be an alternative buy menu that will buy ammo, armor and grenades, as well as the primary weapon.
// Send the menu
CreateMenu(pBuffer, "->1. Say hello\n->2. Say bye\n->3. Exit", 3);
engine->MessageEnd();


The writeshort number is 7 dispite having 3 options because it goes up in powers of 2
return PLUGIN_STOP;
}
</source>


1 option: 2^0 = 1
'''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>


2 options: 2^0+2^1 = 3
When in-game, executing <code>testmenu</code> in the client console will result in a menu of 3 menus, structured like:
1. Say hello
2. Say bye
3. Exit


3 options: 2^0+2^1+2^2 = 7
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.


and so on, if you want more and arent very good at maths, google will work it out for you. [http://www.google.co.uk/search?hl=en&q=2%5E0%2B2%5E1%2B2%5E2%2B2%5E3%2B2%5E4%2B2%5E5%2B2%5E6%2B2%5E7%2B2%5E8%2B2%5E9&btnG=Google+Search&meta= 10 Options]
=== 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.


If you have done this correcly so far then you should have the menu show up in game when the client types a_menu but this menu doesnt do anything.
The command has only one parameter, a number, which is the respective menu option the client selected.


Find the function <code>CServerPluginEmpty::ClientCommand</code> in your <code>serverplugin_empty.cpp</code> file and insert the following below the last '''return''':
<source lang="cpp">
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;
</source>


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.


== Making commands for the menu ==
[[Category:Tutorials]]
 
[[Category:Programming]]
 
The menu run the command menuselect with the parameter as the option selected, so for this menu the commands would be menuselect 1 menuselect 2 and menuselect 3
 
<pre>
else if ( FStrEq( pcmd, "menuselect" ) )
  {
      const char *parameter = engine->Cmd_Argv(1);
      if ( FStrEq( parameter, "1" ) )
      {
        engine->ClientCommand (pEntity, "buy m4a1;buy ak47;buy primammo;buy vesthelm;buy deagle;buy secammo;buy flashbang;buy hegrenade;buy flashbang");
      }
      else if ( FStrEq( parameter, "2" ) )
      {
        engine->ClientCommand (pEntity, "buy awp;buy primammo;buy vesthelm;buy deagle;buy secammo;buy flashbang;buy hegrenade;buy flashbang");
      }
      else if ( FStrEq( parameter, "3" ) )
      {
  engine->ClientPrintf(pEntity, "Menu Exited")
      }
      return PLUGIN_STOP;
  } 
</pre>
 
This will make the client run the commands to buy weapons ammo armor and grenades. The menu runs the commands.
 
 
----
 
== Summary ==
 
All being right this should work, the whole code should be
 
<pre>
int getIndexFromUserID(int userid)
{
  edict_t *player;
  IPlayerInfo *info;
  for(int i = 1; i <= maxplayers; i++)
  {
      player = engine->PEntityOfEntIndex(i);
      if(!player || player->IsFree() )
        continue;
      info = playerinfomanager->GetPlayerInfo(player);
 
      if(info->GetUserID() == userid)
        return i;
  }
 
  return -1;
}
//---------------------------------------------------------------------------------
// Purpose: called when a client types in a command (only a subset of commands however, not CON_COMMAND's)
//---------------------------------------------------------------------------------
PLUGIN_RESULT CEmptyServerPlugin::ClientCommand( edict_t *pEntity )
{
const char *pcmd = engine->Cmd_Argv(0);
 
if ( !pEntity || pEntity->IsFree() )
{
return PLUGIN_CONTINUE;
}
 
if ( FStrEq( pcmd, "a_menu" ) )
  {  
    IPlayerInfo *playerInfo = playerinfomanager->GetPlayerInfo(pEntity);
    MRecipientFilter filter;
      filter.AddRecipient(getIndexFromUserID(playerInfo->GetUserID()));
    bf_write *pBuffer = engine->UserMessageBegin( &filter, 9 );
      pBuffer->WriteShort( 7 );
      pBuffer->WriteChar( -1 );
      pBuffer->WriteByte( false );
      pBuffer->WriteString( "1.Assault Rifle\n2.AWP\n3.Exit" );
      engine->MessageEnd();
      return PLUGIN_STOP;
  }
  else if ( FStrEq( pcmd, "menuselect" ) )
  {
      const char *parameter = engine->Cmd_Argv(1);
      if ( FStrEq( parameter, "1" ) )
      {
        engine->ClientCommand (pEntity, "buy m4a1;buy ak47;buy primammo;buy vesthelm;buy deagle;buy secammo;buy flashbang;buy hegrenade;buy flashbang");
      }
      else if ( FStrEq( parameter, "2" ) )
      {
        engine->ClientCommand (pEntity, "buy awp;buy primammo;buy vesthelm;buy deagle;buy secammo;buy flashbang;buy hegrenade;buy flashbang");
      }
      else if ( FStrEq( parameter, "3" ) )
      {
  engine->ClientPrintf(pEntity, "Menu Exited");
      }
      return PLUGIN_STOP;
  } 
 
return PLUGIN_CONTINUE;
}
</pre>
[[category:Tutorials]][[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.