Code3Arena

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 33 | Next >>

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    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!

    PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 33 | Next >>