TUTORIAL 33 - Popup Menus II
by HypoThermia
In this part we're going to take full advantage of the menu framework we've added in part I,
and implement a fully featured bot command menu for use in Team DM and CTF games. There are a number
of useful functions here that show how to access parameters stored on or by the server.
For the second part of this tutorial, you must have implemented all of the code
in the first part. You should pay particular attention to the parts of
the code that have been commented as being required for this second part to work.
The bot commands implemented here can all be found in the BotCommands.htm file
in the Docs/Help directory under your Q3 install.
This is a fairly large tutorial to digest, with a few subtleties in the coding.
The key points can be summarized:
- Helper function for adding bots/players
- Helper function for getting items on the map
- Using submenus
- Implementing commands
- Commands based on earlier menu selections
- Use of run-time sanity checking
1. About the menu structure
You'll get the greatest benefit from this tutorial if you know what to expect
from this code. There's a lot of interaction between code, often by the use of pointers
to functions, and if you're not careful you might lose sight of the objectives and things
that can be learnt.
You can see the menu code in action by looking at my user interface mod,
UI Enhanced (ok, ok, that was
the last shameless plug!) Many of the final commands depend on the previous menu choices,
and this complicates things further.
We'll start by looking at the help functions used to create some of the more complicated
menus. Then we'll build up the menu in groups of related commands.
There's nothing special about how the commands are executed. They simply create the
required message text in the private team chat channel. The bots understand and respond
to them. Unfortunately the CTF bots are much less responsive, this is because they have
a conflicting and over-riding urge to go get the flag!
We're continuing making code modifications to ui_ingame.c. By default static data
and constants should be placed near the beginning so that all following functions can see
and use them without a forward declaration. You can also order all of these functions so that
no function declaration is needed, i.e. the function is defined before first use.
2. Building a list of bots and/or players
One function covers all in this case. We're going to
iterate over all the players on the server, and see if they pass one or more criterion
for acceptance or rejection.
These are the flags that govern how bots and players are added to the list. Most are
self explanatory and can be used in limited combination. Use either PT_FRIENDLY
or PT_ENEMY as needed. Use either PT_BOTONLY or PT_PLAYERONLY.
The last two flags, PT_EXCLUDEPARENT and PT_EXCLUDEGRANDPARENT,
need special explanation. Some menu commands only make sense when they exclude the
bot or player who will receive the command. "Anarki follow Anarki" is a good
example... its nonsense! Provided the sanity checks are passed, we can access these earlier
menus, compare and exclude their contents.
#define PT_FRIENDLY 1
#define PT_ENEMY 2
#define PT_BOTONLY 4
#define PT_PLAYERONLY 8
#define PT_EXCLUDEPARENT 16
#define PT_EXCLUDEGRANDPARENT 32
To check the list of players we first grab all the details in the
CS_SERVERINFO string. The player's team is identified for future
comparison.
Bots can be identified by their use of a "skill" setting, while
the cleaned name string (colour codes are removed) is compared and rejected if required.
Note that we are in the middle of creating a menu, and the depth has count has already
been increased. We need to take that into account when examining earlier menus.
Provided the player name has passed, its added into the menu with the provided
sub-menu creation or event handling functions.
/*
=================
DynamicMenu_AddListOfPlayers
=================
*/
static void DynamicMenu_AddListOfPlayers( int type,
createHandler crh, eventHandler evh )
{
uiClientState_t cs;
int numPlayers;
int isBot;
int n;
char info[MAX_INFO_STRING];
char name[64];
int playerTeam, team;
int depth;
trap_GetConfigString( CS_SERVERINFO, info, sizeof(info) );
numPlayers = atoi( Info_ValueForKey( info, "sv_maxclients" ) );
trap_GetClientState( &cs );
trap_GetConfigString( CS_PLAYERS
+ cs.clientNum, info, MAX_INFO_STRING );
playerTeam = atoi(Info_ValueForKey(info, "t"));
depth = s_dynamic.depth - 1;
for( n = 0; n < numPlayers; n++ ) {
trap_GetConfigString( CS_PLAYERS + n,
info, MAX_INFO_STRING );
if (n == cs.clientNum)
continue;
isBot = atoi( Info_ValueForKey( info, "skill" ) );
if( (type & PT_BOTONLY) && !isBot ) {
continue;
}
if( (type & PT_PLAYERONLY) && isBot ) {
continue;
}
team = atoi(Info_ValueForKey(info, "t"));
if ((type & PT_FRIENDLY) && team != playerTeam)
continue;
if ((type & PT_ENEMY) && team == playerTeam)
continue;
Q_strncpyz(name, Info_ValueForKey(info, "n"), 64);
Q_CleanStr(name);
if (!name[0])
continue;
if (type & PT_EXCLUDEPARENT && depth >= 1)
{
// depth has been increased by init of (sub)menu
if (!Q_stricmp(name, s_dynamic.data[
s_dynamic.active[depth - 1]].text))
continue;
}
if (type & PT_EXCLUDEGRANDPARENT && depth >= 2)
{
// depth has been increased by init of (sub)menu
if (!Q_stricmp(name, s_dynamic.data[
s_dynamic.active[depth - 2]].text))
continue;
}
DynamicMenu_AddItem(name, 0, crh, evh);
}
}
3. Building a list of items on the map
As quite a few of the bot commands are centered around items on the map, we need
a way to add a list of them to the menu. For greatest efficiency, we'd like to add those
that only appear on the map itself.
Fortunately we can do this through the use of a system wide string set by the server.
This string is also used to pre-load items when changing levels, so those of you who've looked
through the client code might recognize some of this.
Some assumptions have been made about items. This is required because the only
way to distinguish between the different health and armour items is by the
amount of benefit they can give. If you've changed these values in bg_misc.c then
you'll have to update the values used here so they match.
First we'll set up all of the data we'll be using, put this near the beginning of the changes
you've already made from the first part of the tutorial.
Each item has a name used in the menu (longname), the name used to command the bots
(shortname), the item type identified in bg_misc.h, and a value that
uniquely identifies it. The boolean loaded is qtrue if the item is on the map.
The last parameter was originally intended to represent the CTF specific flags, but
I never used it in the end. You can put a gametype value here, and the object will only
appear on the menu if that game type is running as well.
typedef struct {
const char* longname;
const char* shortname;
itemType_t type;
int tag;
qboolean loaded;
int game;
} itemList_t;
// if you've changed the armour or mega strength values in
// bg_misc.c, then it won't be identified here
//
// The machine gun is excluded from the list because it
// is the default weapon, maps usually don't have it as
// available for pickup.
static itemList_t dm_itemList[] = {
{ "Red Armour", "ra", IT_ARMOR, 100, qfalse, 0 },
{ "Yel Armour", "ya", IT_ARMOR, 50, qfalse, 0 },
{ "Mega", "mh", IT_HEALTH, 100, qfalse, 0},
// { "Machine G", "mg", IT_WEAPON, WP_MACHINEGUN, qfalse, 0 },
{ "Shotgun", "sg", IT_WEAPON, WP_SHOTGUN, qfalse, 0 },
{ "Grenade L", "gl", IT_WEAPON, WP_GRENADE_LAUNCHER, qfalse, 0 },
{ "Rocket L", "rl", IT_WEAPON, WP_ROCKET_LAUNCHER, qfalse, 0 },
{ "Plasma G", "pg", IT_WEAPON, WP_PLASMAGUN, qfalse, 0 },
{ "Lightning", "lg", IT_WEAPON, WP_LIGHTNING, qfalse, 0 },
{ "Railgun", "rg", IT_WEAPON, WP_RAILGUN, qfalse, 0 },
{ "BFG10k", "bfg", IT_WEAPON, WP_BFG, qfalse, 0 },
{ "Quad", "quad", IT_POWERUP, PW_QUAD, qfalse, 0 },
{ "Regen", "regen", IT_POWERUP, PW_REGEN, qfalse, 0 },
{ "Invis", "invis", IT_POWERUP, PW_INVIS, qfalse, 0 },
{ "Btl Suit", "bs", IT_POWERUP, PW_BATTLESUIT, qfalse, 0 },
{ "Haste", "haste", IT_POWERUP, PW_HASTE, qfalse, 0 }
};
static int dm_numMenuItems = sizeof(dm_itemList)/sizeof(dm_itemList[0]);
Now we'll actually add the items to the menu. We can specify that a particular item
is excluded (in this case grayed out), by its position on the list. Note that we sanity
check the addition of the menu item so we aren't graying out the wrong menu item.
If you remember from the first part of the tutorial, I made a fuss about when and where
the menu grayed state was changed. Menu items are initialized without the flag when the
popup menu is created before first use. The grayed flag is removed when a sub-menu is closed.
This creates a window in which we can add the QMF_GRAYED flag during the
DynamicMenu_AddItem() phase, that exists for the duration of the sub-menu, but is removed
safely so the next sub-menu isn't affected. When data structures are re-used dynamically, you must
make sure that they're in a safe state for next use.
/*
=================
DynamicMenu_AddListOfItems
=================
*/
static void DynamicMenu_AddListOfItems( int exclude, createHandler crh, eventHandler evh )
{
int i;
int lastitem;
for (i = 0; i < dm_numMenuItems; i++)
{
if (!dm_itemList[i].loaded)
continue;
if (dm_itemList[i].game && dm_itemList[i].game != s_dynamic.gametype)
continue;
if (!DynamicMenu_AddItem(dm_itemList[i].longname, i, crh, evh))
continue;
if (i == exclude)
{
// gray the item
lastitem = s_dynamic.end[ s_dynamic.depth - 1] - 1;
s_dynamic.item[lastitem].generic.flags |= QMF_GRAYED;
}
}
}
But before we can do this, we need to detect the presence of items on the map. This is done
every time the menu is first created (you did uncomment the call to this function, didn't you?),
so no assumption is made about its persistance in memory.
The CS_ITEMS configuration string consists of an array of 1's and 0's, matching
the listed order of items in bg_itemlist[] in bg_misc.c. We just need to match
up the identifying parameters in our dm_itemlist[] to confirm that the item is present on
the map.
/*
=================
DynamicMenu_InitMapItems
=================
*/
static void DynamicMenu_InitMapItems( void )
{
int i, j;
char items[MAX_ITEMS+1];
int type;
for (i = 0; i < dm_numMenuItems; i++)
dm_itemList[i].loaded = qfalse;
trap_GetConfigString( CS_ITEMS, items, sizeof(items) );
for ( i = 1; i < bg_numItems; i++)
{
if (items[i] != '1')
continue;
// locate item on our list
type = bg_itemlist[i].giType;
for (j = 0 ; j < dm_numMenuItems; j++)
{
if (dm_itemList[j].type != bg_itemlist[i].giType)
continue;
// mark as loaded if we've found the item
if (type == IT_WEAPON || type == IT_POWERUP)
{
if (bg_itemlist[i].giTag == dm_itemList[j].tag)
{
dm_itemList[j].loaded = qtrue;
break;
}
continue;
}
if (bg_itemlist[i].quantity == dm_itemList[j].tag)
{
dm_itemList[j].loaded = qtrue;
break;
}
}
}
}
4. Building up the menu
With two useful helper functions now in place, we can start work on
creating the menu itself. I'll split this up into two parts to try and
make things easier to follow.
The code in this section is responsible for creating the menu, and
each of the sub-menus. Once implemented, all that remains is to add
the code that will execute the commands. This is done in section 5.
Refresh your memory about how DynamicMenu_AddItem() is used. The four arguments
are the displayed text string, a unique identifier, the function that will create
a sub menu, and an event handler function that performs the selected task.
It's important to note, for future reference, that many of the text strings here
are used as part of the command issued. It's therefore important that they're entered exactly
and not changed.
Don't forget that to reduce compiler warnings and errors you should try and
place functions before they're called.
I've tried to order these menu commands in a way that matches frequent use.
Your mileage may vary.
4.1 The primary menu
The enum should be placed near the start of the dynamic menu code additions,
it identifies the unique commands that pass through the same handler DM_Command_Event().
The function DynamicMenu_InitPrimaryMenu() replaces the very simple one used in the first
part of the tutorial. You shouldn't replace the DM_Close_Event() as its also used here.
enum {
COM_WHOLEADER,
COM_IAMLEADER,
COM_QUITLEADER,
COM_MYTASK
} commandId;
/*
=================
DynamicMenu_InitPrimaryMenu
=================
*/
static void DynamicMenu_InitPrimaryMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddItem("Close!", 0, NULL, DM_Close_Event);
DynamicMenu_AddItem("Everyone", 0, DM_CommandList_SubMenu, NULL);
DynamicMenu_AddListOfPlayers(PT_FRIENDLY|PT_BOTONLY,
DM_CommandList_SubMenu, NULL);
DynamicMenu_AddItem("Leader?", COM_WHOLEADER, NULL, DM_Command_Event);
if (s_dynamic.gametype == GT_CTF)
{
DynamicMenu_AddItem("My task?", COM_MYTASK, NULL, DM_Command_Event);
}
DynamicMenu_AddItem("Lead", COM_IAMLEADER, NULL, DM_Command_Event);
DynamicMenu_AddItem("Resign", COM_QUITLEADER, NULL, DM_Command_Event);
DynamicMenu_FinishSubMenuInit();
}
4.2 The bot command list
This creates a list of commands for controlling the bot, each command being uniquely
identified by the enumeration.
Two extra commands are added for CTF games - their use is self explanatory.
The Point+ command is slightly unusual, in that you then have to issue a second
command. The bot will (in theory) lead and wait for you, rather than follow you around.
enum {
BC_NULL,
BC_FOLLOW,
BC_HELP,
BC_GET,
BC_PATROL,
BC_CAMP,
BC_HUNT,
BC_DISMISS,
BC_REPORT,
BC_POINT,
BC_GETFLAG,
BC_DEFENDBASE
} botCommandId;
/*
=================
DM_CommandList_SubMenu
=================
*/
static void DM_CommandList_SubMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddItem("Report", BC_REPORT, NULL, DM_BotCommand_Event);
DynamicMenu_AddItem("Help", BC_HELP, DM_TeamList_SubMenu, NULL);
if (s_dynamic.gametype == GT_CTF)
{
DynamicMenu_AddItem("Capture Flag",
BC_GETFLAG, NULL, DM_BotCommand_Event);
DynamicMenu_AddItem("Defend Base",
BC_DEFENDBASE, NULL, DM_BotCommand_Event);
}
DynamicMenu_AddItem("Follow", BC_FOLLOW, DM_TeamList_SubMenu, NULL);
DynamicMenu_AddItem("Get", BC_GET, DM_ItemList_SubMenu, NULL);
DynamicMenu_AddItem("Patrol", BC_PATROL, DM_ItemPatrol_SubMenu, NULL);
DynamicMenu_AddItem("Camp", BC_CAMP, DM_CampItemList_SubMenu, NULL);
DynamicMenu_AddItem("Hunt", BC_HUNT, DM_EnemyList_SubMenu, NULL);
DynamicMenu_AddItem("Point+", BC_POINT, NULL, DM_BotCommand_Event);
DynamicMenu_AddItem("Dismiss", BC_DISMISS, NULL, DM_BotCommand_Event);
DynamicMenu_FinishSubMenuInit();
}
4.3 Commands issued to bots using one target
These commands are used to build a single submenu that gives a valid
target. This might be an item, another team mate, or an enemy player.
This first one provides a list of bots on the same team, excluding the bot that will receive
the command. Note the special case that refers to the player ("follow me").
/*
=================
DM_TeamList_SubMenu
=================
*/
static void DM_TeamList_SubMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddItem("me", 0, NULL, DM_BotPlayerTarget_Event);
DynamicMenu_AddListOfPlayers(PT_FRIENDLY|PT_EXCLUDEGRANDPARENT,
NULL, DM_BotPlayerTarget_Event);
DynamicMenu_FinishSubMenuInit();
}
Sets up a list of items that are on the map. The use of -1 in the exclude argument means
that all items will appear, none grayed out.
/*
=================
DM_ItemList_SubMenu
=================
*/
static void DM_ItemList_SubMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddListOfItems(-1, NULL, DM_BotItemTarget_Event);
DynamicMenu_FinishSubMenuInit();
}
Implementing the camp command, this is essentially the same as the item list,
with the addition of the "camp here" and "camp there" commands.
The use of -1 for the id ends up using the menu text instead of finding
a special contracted form of the item name.
/*
=================
DM_CampItemList_SubMenu
=================
*/
static void DM_CampItemList_SubMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddItem("here", -1, NULL, DM_BotItemTarget_Event);
DynamicMenu_AddItem("there", -1, NULL, DM_BotItemTarget_Event);
DynamicMenu_AddListOfItems(-1, NULL, DM_BotItemTarget_Event);
DynamicMenu_FinishSubMenuInit();
}
We finish off with a submenu that lists enemy players. Used to create the
list of targets for the hunt command.
/*
=================
DM_EnemyList_SubMenu
=================
*/
static void DM_EnemyList_SubMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddListOfPlayers(PT_ENEMY, NULL, DM_BotPlayerTarget_Event);
DynamicMenu_FinishSubMenuInit();
}
4.4 More complex commands requiring two targets
There's only one command that uses two targets: ordering a patrol between two items. The second
item list DM_ItemPatrol2_SubMenu() needs to exclude the item selected on the previous menu,
as it doesn't make sense to patrol between the same item.
/*
=================
DM_ItemList2_SubMenu
=================
*/
static void DM_ItemPatrol2_SubMenu( void )
{
int exclude;
int index;
int depth;
DynamicMenu_SubMenuInit();
depth = s_dynamic.depth - 1;
index = s_dynamic.active[depth - 1]; // previous menu level
exclude = s_dynamic.data[index].id;
DynamicMenu_AddListOfItems(exclude, NULL, DM_BotItemItemTarget_Event);
DynamicMenu_FinishSubMenuInit();
}
/*
=================
DM_ItemPatrol_SubMenu
=================
*/
static void DM_ItemPatrol_SubMenu( void )
{
DynamicMenu_SubMenuInit();
DynamicMenu_AddListOfItems(-1, DM_ItemPatrol2_SubMenu, NULL);
DynamicMenu_FinishSubMenuInit();
}
5. Adding the menu functionality
With everything set for creating the menu, we now need to add the code that will
issue the commands to the bot(s). The method for doing this is very simple: just create
the command text as a player would type it, and send it into the team chat channel. The bots
will parse the command and (hopefully) respond to it.
Although there is a lot of code here, most of it's sanity checking to make sure
everything is behaving as expected. For a final and fully debugged product this code can be
commented out, but in the mean time it helped me catch some mistakes I'd made while
writing the code.
The sanity checking falls into two parts. Ensuring that we're deep enough in the menu structure
for the rest of the code to access valid data (essential). And checking
that the command matches the one we've written the function for (optional).
5.1 Commands that have no target
Very straight forward, the command is a already fully formed as a simple string.
It's important that the strings match the ordering of the COM_* enums.
static char* commandString[] = {
"Who is the leader", // COM_WHOLEADER
"I am the leader", // COM_IAMLEADER
"I quit being the leader", // COM_QUITLEADER
"What is my job", // COM_MYTASK
0
};
/*
=================
DM_Command_Event
Issues a command without target
=================
*/
static void DM_Command_Event( int index )
{
int depth;
int cmd;
const char* s;
depth = DynamicMenu_IndexDepth(index);
if (depth != s_dynamic.depth)
{
Com_Printf("Command_Event: index %i"
" at wrong depth (%i)\n", index, depth);
DynamicMenu_Close();
return;
}
// validate command
cmd = s_dynamic.data[index].id;
switch (cmd) {
case COM_WHOLEADER:
case COM_IAMLEADER:
case COM_QUITLEADER:
case COM_MYTASK:
break;
default:
Com_Printf("Command_Event: unknown command (%i)\n", cmd);
DynamicMenu_Close();
return;
};
// issue the command
DynamicMenu_Close();
trap_Cmd_ExecuteText( EXEC_APPEND, va("say_team \"%s\"\n",
commandString[cmd]));
}
5.2 Simple commands given to bots
Introducing the next level of complexity, we need to include the name of the bot
that the command is targetted at. The complete list of commands is introduced here,
even though this event handler only uses a few of them.
You need to remember that s_dynamic.depth is a count of the number of menus open,
not an index into data arrays like s_dynamic.active[]. The menu structure is
BotName|Command, so we need to use active[s_dynamic.depth-2] to get the bot name.
static char* botCommandStrings[] = {
"", // BC_NULL
"%s follow %s", // BC_FOLLOW
"%s help %s", // BC_HELP
"%s get %s", // BC_GET
"%s patrol from %s to %s", // BC_PATROL
"%s camp %s", // BC_CAMP
"%s kill %s", // BC_HUNT
"%s dismissed", // BC_DISMISS
"%s report", // BC_REPORT
"%s lead the way", // BC_POINT
"%s get the flag", // BC_GETFLAG
"%s defend the base", // BC_DEFENDBASE
0
};
/*
=================
DM_BotCommand_Event
Issues a command to a bot
=================
*/
static void DM_BotCommand_Event( int index )
{
int depth;
int bot, cmd;
const char* s;
depth = DynamicMenu_IndexDepth(index);
if (depth != s_dynamic.depth || depth < 2)
{
Com_Printf("BotCommand_Event: index %i"
" at wrong depth (%i)\n", index, depth);
DynamicMenu_Close();
return;
}
// validate command
cmd = s_dynamic.data[index].id;
switch (cmd) {
case BC_DISMISS:
case BC_REPORT:
case BC_POINT:
case BC_GETFLAG:
case BC_DEFENDBASE:
break;
default:
Com_Printf("BotCommand_Event: unknown command (%i)\n", cmd);
DynamicMenu_Close();
return;
};
// get the parent bot name, insert into command string
bot = s_dynamic.active[ depth - 2 ];
s = va(botCommandStrings[cmd], s_dynamic.data[bot].text);
// issue the command
DynamicMenu_Close();
trap_Cmd_ExecuteText( EXEC_APPEND, va("say_team \"%s\"\n", s));
}
5.3 Commands that have a single item as target
These are two variations on the same event code. The first
DM_BotPlayerTarget_Event() uses the text drawn in the menu
to act as the target. The command is in the form
BotName|Command|Target, and so we again use active[] to pull out
the previous choices.
/*
=================
DM_BotPlayerTarget_Event
Issues a command to a bot that needs a target
Assumes index is the object, parent is the command,
and parent of parent is the bot
=================
*/
static void DM_BotPlayerTarget_Event( int index)
{
int depth;
int bot, cmd;
const char* s;
depth = DynamicMenu_IndexDepth(index);
if (depth != s_dynamic.depth || depth < 3)
{
Com_Printf("BotPlayerTarget_Event: index %i"
" at wrong depth (%i)\n", index, depth);
DynamicMenu_Close();
return;
}
// validate command
cmd = s_dynamic.data[s_dynamic.active[depth - 2]].id;
switch (cmd) {
case BC_FOLLOW:
case BC_HELP:
case BC_HUNT:
break;
default:
Com_Printf("BotPlayerTarget_Event: unknown command id %i\n", cmd);
DynamicMenu_Close();
return;
};
// get the parent bot, insert it and item into command string
bot = s_dynamic.active[ depth - 3 ];
s = va(botCommandStrings[cmd], s_dynamic.data[bot].text,
s_dynamic.data[index].text);
// issue the command
DynamicMenu_Close();
trap_Cmd_ExecuteText( EXEC_APPEND, va("say_team \"%s\"\n", s));
}
This second event handler works slightly differently. We've had to shorten
the names of objects in dm_ItemList[] (section 3) to fit them within
menu text limits. While these are still very human readable, the bots
don't understand these contractions. Instead of pulling the menu text and inserting it
into a command, we need to use a name the bot does understand.
This code is almost identical to DM_PlayerItemTarget_Event, especially when you
notice that item==-1 gives the same result. I don't have a strong over-riding reason for
doing things this way. I chose to use the same event function for each command, and then
needed a special case where the menu text was used instead of the special. To merge
DM_BotItemTarget_Event() with DM_BotPlayerTarget_Event() would require
modification to DynamicMenu_AddListOfPlayers(). You can do this if you think that is
a *better* way.
/*
=================
DM_BotItemTarget_Event
Issues a command to a bot that needs a target
Assumes index is the object, parent is the command,
and parent of parent is the bot
=================
*/
static void DM_BotItemTarget_Event( int index)
{
int depth;
int bot, cmd, item;
const char* s;
const char* item_str;
depth = DynamicMenu_IndexDepth(index);
if (depth != s_dynamic.depth || depth < 3)
{
Com_Printf("BotItemTarget_Event: index %i"
" at wrong depth (%i)\n", index, depth);
DynamicMenu_Close();
return;
}
// validate command
cmd = s_dynamic.data[s_dynamic.active[depth - 2]].id;
switch (cmd) {
case BC_GET:
case BC_CAMP:
break;
default:
Com_Printf("BotItemTarget_Event: unknown command id %i\n", cmd);
DynamicMenu_Close();
return;
};
// get the parent bot, insert it and item into command string
bot = s_dynamic.active[ depth - 3 ];
item = s_dynamic.data[index].id;
if (item < 0)
item_str = s_dynamic.data[index].text;
else
item_str = dm_itemList[item].shortname;
s = va(botCommandStrings[cmd], s_dynamic.data[bot].text, item_str);
// issue the command
DynamicMenu_Close();
trap_Cmd_ExecuteText( EXEC_APPEND, va("say_team \"%s\"\n", s));
}
5.4 Commands that need two targets
The last type of command is only used for giving a patrol command between two objects.
There's nothing here you haven't already seen, we just need to go deeper into the menu structure
in order to get all the parameters. The menu command looks like BotName|Command|Item|Item2.
/*
=================
DM_BotItemItemTarget_Event
Issues a command to a bot that needs two targets
Assumes index and parent are the objects, grandparent
is the command, and great-grandparent is the bot
=================
*/
static void DM_BotItemItemTarget_Event( int index)
{
int depth;
int bot, cmd, item, item2;
const char* s;
depth = DynamicMenu_IndexDepth(index);
if (depth != s_dynamic.depth || depth < 4)
{
Com_Printf("BotItemItemTarget_Event: index %i"
" at wrong depth (%i)\n", index, depth);
DynamicMenu_Close();
return;
}
// validate command
cmd = s_dynamic.data[s_dynamic.active[depth - 3]].id;
switch (cmd) {
case BC_PATROL:
break;
default:
Com_Printf("BotItemItemTarget_Event: unknown command id %i\n", cmd);
DynamicMenu_Close();
return;
};
// get the parent bot, insert it and item into command string
bot = s_dynamic.active[ depth - 4 ];
item = s_dynamic.data[s_dynamic.active[depth - 2]].id;
item2 = s_dynamic.data[index].id;
s = va(botCommandStrings[cmd], s_dynamic.data[bot].text,
dm_itemList[item].shortname, dm_itemList[item2].shortname);
// issue the command
DynamicMenu_Close();
trap_Cmd_ExecuteText( EXEC_APPEND, va("say_team \"%s\"\n", s));
}
6. Tying up loose ends
There isn't much left to do now, the code should already compile and run as
advertised.
If you're going to include this code in your mod then there are two things that you might
want to look at. At the moment you can access a (broken) bot command menu through the in-game
command menu or by pressing F3.
You could change the in-game menu so it moves to this popup menu
system instead. Changing the F3 key is also easily done. Instead of adding a custom command for
this menu, you can change the ui_teamOrders command, UI_ConsoleCommands() in
ui_atoms.c, to point to this menu system instead.
With the new 1.25 source code release just around the corner, this might become
"obsolete" very quickly. At the same time, it should easily port to the new
source code and co-exist with the (promised) bot command menu system. I can't make promises
for code I haven't seen.
Whether this code lasts as long as a mayfly, or proves useful in the 1.25 source code, I
don't know. In any case, enjoy!
|