Ingame menu for server plugins (CS:S only)
Contents
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.