Ingame menu for server plugins (CS:S only)

From Valve Developer Community
Jump to navigation Jump to search

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.