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

From Valve Developer Community
Jump to navigation Jump to search
(Cleaned up the entire article, used a simpler example and used gpGlobals for maxClients retrieval. Explained the whole process a lot more clearly and simply to make it easier for beginners to understa)
(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.)
 
Line 103: Line 103:
To simplify sending the menu, the below helper writes the '''usermessage''' data to a <code>bf_write</code>:
To simplify sending the menu, the below helper writes the '''usermessage''' data to a <code>bf_write</code>:
<source lang="cpp">
<source lang="cpp">
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions=10, int iSecondsToStayOpen=-1)
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions, int iSecondsToStayOpen)
{
{
Assert(pBuffer);
Assert(pBuffer);
Line 121: Line 121:
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:
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">
<source lang="cpp">
void CreateMenu(bf_write*, const char*, int, int);
void CreateMenu(bf_write* pBuffer, const char* szMessage, int nOptions=10, int iSecondsToStayOpen=-1);
</source>
</source>


Line 142: Line 142:


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


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


return PLUGIN_STOP;
return PLUGIN_STOP;
Line 151: Line 153:
</source>
</source>


'''Note:''' Lines in a menu are split up by passing <code>\n</code>.<br />
'''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>
'''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>


Line 168: Line 170:
Find the function <code>CServerPluginEmpty::ClientCommand</code> in your <code>serverplugin_empty.cpp</code> file and insert the following below the last '''return''':
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">
<source lang="cpp">
if(FStrEq(engine->Cmd_Argv(0), "testmenu"))
if(FStrEq(engine->Cmd_Argv(0), "menuselect"))
{
{
switch(atoi(engine->Cmd_Argv(1)))
switch(atoi(engine->Cmd_Argv(1)))
{
{
case 1:
case 1:
helpers->ClientCommand(pEntity, "say Hello\n");
helpers->ClientCommand(pEntity, "say Hello");
break;
break;
case 2:
case 2:
helpers->ClientCommand(pEntity, "say Bye\n");
helpers->ClientCommand(pEntity, "say Bye");
break;
break;
case 3:
case 3:

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.