Ingame menu for server plugins (CS:S only): Difference between revisions
m (Robot: fixing template case.) |
(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.) |
||
(One intermediate revision by the same user not shown) | |||
Line 1: | Line 1: | ||
== 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 [[Server Plugins|IServerPluginHelpers]] interface is preferred. | ||
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: | |||
== | === MRecipientFilter.h === | ||
<source lang="cpp"> | |||
#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 | |||
</source> | |||
=== MRecipientFilter.cpp === | === MRecipientFilter.cpp === | ||
< | <source lang="cpp"> | ||
#include "MRecipientFilter.h" | #include "MRecipientFilter.h" | ||
#include "interface.h" | #include "interface.h" | ||
Line 28: | Line 53: | ||
extern IPlayerInfoManager *playerinfomanager; | extern IPlayerInfoManager *playerinfomanager; | ||
extern IServerPluginHelpers *helpers; | extern IServerPluginHelpers *helpers; | ||
extern CGlobalVars *gpGlobals; | |||
// memdbgon must be the last include file in a .cpp file!!! | // memdbgon must be the last include file in a .cpp file!!! | ||
#include "tier0/memdbgon.h" | #include "tier0/memdbgon.h" | ||
int MRecipientFilter::GetRecipientCount() const | int MRecipientFilter::GetRecipientCount() const | ||
Line 47: | Line 65: | ||
int MRecipientFilter::GetRecipientIndex(int slot) const | int MRecipientFilter::GetRecipientIndex(int slot) const | ||
{ | { | ||
if ( slot < 0 || slot >= GetRecipientCount() ) | if(slot < 0 || slot >= GetRecipientCount()) | ||
return -1; | return -1; | ||
return m_Recipients[ slot ] | return m_Recipients[slot]; | ||
} | } | ||
void MRecipientFilter::AddAllPlayers( | void MRecipientFilter::AddAllPlayers() | ||
{ | { | ||
m_Recipients.RemoveAll(); | m_Recipients.RemoveAll(); | ||
for ( int i = 1; i <= maxClients; i++ ) | |||
for(int i = 1; i <= gpGlobals->maxClients; i++) | |||
{ | { | ||
edict_t *pPlayer = engine->PEntityOfEntIndex(i); | edict_t *pPlayer = engine->PEntityOfEntIndex(i); | ||
if ( !pPlayer || pPlayer->IsFree()) | |||
if(!pPlayer || pPlayer->IsFree()) | |||
continue; | continue; | ||
m_Recipients.AddToTail(i); | m_Recipients.AddToTail(i); | ||
} | } | ||
} | } | ||
void MRecipientFilter::AddRecipient( int iPlayer ) | void MRecipientFilter::AddRecipient(int iPlayer) | ||
{ | { | ||
// | // Return if the recipient is already in the vector | ||
if ( m_Recipients.Find( iPlayer ) != m_Recipients.InvalidIndex() ) | 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; | return; | ||
m_Recipients.AddToTail( iPlayer ); | m_Recipients.AddToTail(iPlayer); | ||
} | } | ||
</ | </source> | ||
=== CreateMenu helper === | |||
To simplify sending the menu, the below helper writes the '''usermessage''' data to a <code>bf_write</code>: | |||
<source lang="cpp"> | |||
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 | |||
} | |||
</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> | |||
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 <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. | |||
''' | 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), "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; | return PLUGIN_STOP; | ||
} | } | ||
</source> | |||
</ | '''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> | |||
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 | |||
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. | |||
The | |||
< | 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: | case 1: | ||
helpers->ClientCommand(pEntity, "say Hello"); | |||
break; | break; | ||
case 2: | case 2: | ||
helpers->ClientCommand(pEntity, "say Bye"); | |||
break; | break; | ||
case 3: | case 3: | ||
engine->ClientPrintf(pEntity, "Menu | engine->ClientPrintf(pEntity, "Menu exited\n"); | ||
break; | break; | ||
} | } | ||
return PLUGIN_STOP; | 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. | |||
[[Category:Tutorials]] | [[Category:Tutorials]] | ||
[[Category:Programming]] | [[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.