Ingame menu for server plugins (CS:S only): Difference between revisions
(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 | 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, | 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 | '''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), " | 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 | helpers->ClientCommand(pEntity, "say Hello"); | ||
break; | break; | ||
case 2: | case 2: | ||
helpers->ClientCommand(pEntity, "say Bye | 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.