extensions/topmenus/TopMenu.cpp
author David Anderson <dvander@alliedmods.net>
Wed Sep 26 21:44:12 2007 +0000 (2007-09-26)
changeset 1496 4145ca042242
parent 1495 5e85546abc33
child 1497 1cab582efa5f
permissions -rw-r--r--
- various improvements to the plugin-level topmenu api
- categories are no longer displayed if they have no items
     1 /**
     2  * vim: set ts=4 :
     3  * =============================================================================
     4  * SourceMod Sample Extension
     5  * Copyright (C) 2004-2007 AlliedModders LLC.  All rights reserved.
     6  * =============================================================================
     7  *
     8  * This program is free software; you can redistribute it and/or modify it under
     9  * the terms of the GNU General Public License, version 3.0, as published by the
    10  * Free Software Foundation.
    11  * 
    12  * This program is distributed in the hope that it will be useful, but WITHOUT
    13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    14  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    15  * details.
    16  *
    17  * You should have received a copy of the GNU General Public License along with
    18  * this program.  If not, see <http://www.gnu.org/licenses/>.
    19  *
    20  * As a special exception, AlliedModders LLC gives you permission to link the
    21  * code of this program (as well as its derivative works) to "Half-Life 2," the
    22  * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
    23  * by the Valve Corporation.  You must obey the GNU General Public License in
    24  * all respects for all other code used.  Additionally, AlliedModders LLC grants
    25  * this exception to all derivative works.  AlliedModders LLC defines further
    26  * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
    27  * or <http://www.sourcemod.net/license.php>.
    28  *
    29  * Version: $Id$
    30  */
    31 
    32 #include <stdlib.h>
    33 #include <stdarg.h>
    34 #include "TopMenu.h"
    35 
    36 struct obj_by_name_t
    37 {
    38 	unsigned int obj_index;
    39 	char name[64];
    40 };
    41 
    42 int _SortObjectNamesDescending(const void *ptr1, const void *ptr2);
    43 unsigned int strncopy(char *dest, const char *src, size_t count);
    44 size_t UTIL_Format(char *buffer, size_t maxlength, const char *fmt, ...);
    45 
    46 TopMenu::TopMenu(ITopMenuObjectCallbacks *callbacks)
    47 {
    48 	m_clients = NULL;
    49 	m_SerialNo = 1;
    50 	m_pTitle = callbacks;
    51 	m_max_clients = 0;
    52 
    53 	if (playerhelpers->IsServerActivated())
    54 	{
    55 		CreatePlayers(playerhelpers->GetMaxClients());
    56 	}
    57 }
    58 
    59 TopMenu::~TopMenu()
    60 {
    61 	/* Delete all categories */
    62 	size_t i = 1;
    63 	while (i >= m_Categories.size())
    64 	{
    65 		RemoveFromMenu(m_Categories[i - 1]->obj->object_id);
    66 	}
    67 
    68 	/* Remove all objects */
    69 	for (i = 0; i < m_Objects.size(); i++)
    70 	{
    71 		assert(m_Objects[i]->is_free == true);
    72 		delete m_Objects[i];
    73 	}
    74 
    75 	m_pTitle->OnTopMenuObjectRemoved(this, 0);
    76 
    77 	/* Delete all cached config entries */
    78 	for (i = 0; i < m_Config.cats.size(); i++)
    79 	{
    80 		delete m_Config.cats[i];
    81 	}
    82 
    83 	/* Sweep players */
    84 	for (i = 0; i <= (size_t)m_max_clients; i++)
    85 	{
    86 		TearDownClient(&m_clients[i]);
    87 	}
    88 	delete [] m_clients;
    89 }
    90 
    91 void TopMenu::OnClientConnected(int client)
    92 {
    93 	if (m_clients == NULL)
    94 	{
    95 		return;
    96 	}
    97 
    98 	topmenu_player_t *player = &m_clients[client];
    99 	TearDownClient(player);
   100 }
   101 
   102 void TopMenu::OnClientDisconnected(int client)
   103 {
   104 	if (m_clients == NULL)
   105 	{
   106 		return;
   107 	}
   108 
   109 	topmenu_player_t *player = &m_clients[client];
   110 	TearDownClient(player);
   111 }
   112 
   113 void TopMenu::OnServerActivated(int max_clients)
   114 {
   115 	if (m_clients == NULL)
   116 	{
   117 		CreatePlayers(max_clients);
   118 	}
   119 }
   120 
   121 unsigned int TopMenu::AddToMenu(const char *name,
   122 								TopMenuObjectType type,
   123 								ITopMenuObjectCallbacks *callbacks,
   124 								IdentityToken_t *owner,
   125 								const char *cmdname,
   126 								FlagBits flags,
   127 								unsigned int parent)
   128 {
   129 	/* Sanity checks */
   130 	if (type == TopMenuObject_Category && parent != 0)
   131 	{
   132 		return 0;
   133 	}
   134 	else if (type == TopMenuObject_Item && parent == 0)
   135 	{
   136 		return 0;
   137 	}
   138 	else if (m_ObjLookup.retrieve(name) != NULL)
   139 	{
   140 		return 0;
   141 	}
   142 	else if (type != TopMenuObject_Item && type != TopMenuObject_Category)
   143 	{
   144 		return 0;
   145 	}
   146 
   147 	/* If we're adding an item, make sure the parent is valid, 
   148 	 * and that the parent is a category.
   149 	 */
   150 	topmenu_object_t *parent_obj = NULL;
   151 	topmenu_category_t *parent_cat = NULL;
   152 	if (type == TopMenuObject_Item)
   153 	{
   154 		/* Check parent index.  Note it will be >= 1 here. */
   155 		if (parent > m_Objects.size() || m_Objects[parent - 1]->is_free)
   156 		{
   157 			return 0;
   158 		}
   159 		parent_obj = m_Objects[parent - 1];
   160 
   161 		/* Find an equivalent pointer in the category array. */
   162 		for (size_t i = 0; i < m_Categories.size(); i++)
   163 		{
   164 			if (m_Categories[i]->obj == parent_obj)
   165 			{
   166 				parent_cat = m_Categories[i];
   167 				break;
   168 			}
   169 		}
   170 
   171 		/* If none was found, leave. */
   172 		if (parent_cat == NULL)
   173 		{
   174 			return 0;
   175 		}
   176 	}
   177 
   178 	/* Re-use an old object pointer if we can. */
   179 	topmenu_object_t *obj = NULL;
   180 	for (size_t i = 0; i < m_Objects.size(); i++)
   181 	{
   182 		if (m_Objects[i]->is_free == true)
   183 		{
   184 			obj = m_Objects[i];
   185 			break;
   186 		}
   187 	}
   188 
   189 	/* Otherwise, allocate a new one. */
   190 	if (obj == NULL)
   191 	{
   192 		obj = new topmenu_object_t;
   193 		obj->object_id = ((unsigned int)m_Objects.size()) + 1;
   194 		m_Objects.push_back(obj);
   195 	}
   196 
   197 	/* Initialize the object's properties. */
   198 	obj->callbacks = callbacks;
   199 	obj->flags = flags;
   200 	obj->owner = owner;
   201 	obj->type = type;
   202 	obj->is_free = false;
   203 	obj->parent = parent_obj;
   204 	strncopy(obj->name, name, sizeof(obj->name));
   205 	strncopy(obj->cmdname, cmdname ? cmdname : "", sizeof(obj->cmdname));
   206 
   207 	if (obj->type == TopMenuObject_Category)
   208 	{
   209 		/* Create a new category entry */
   210 		topmenu_category_t *cat = new topmenu_category_t;
   211 		cat->obj = obj;
   212 		cat->reorder = false;
   213 		cat->serial = 1;
   214 
   215 		/* Add it, then update our serial change number. */
   216 		m_Categories.push_back(cat);
   217 		m_SerialNo++;
   218 
   219 		/* Updating sorting info */
   220 		m_bCatsNeedResort = true;
   221 	}
   222 	else if (obj->type == TopMenuObject_Item)
   223 	{
   224 		/* Update the category, mark it as needing changes */
   225 		parent_cat->obj_list.push_back(obj);
   226 		parent_cat->reorder = true;
   227 		parent_cat->serial++;
   228 
   229 		/* If the category just went from 0 to 1 items, mark it as 
   230 		 * changed, so clients get the category drawn.
   231 		 */
   232 		if (parent_cat->obj_list.size() == 1)
   233 		{
   234 			m_SerialNo++;
   235 		}
   236 	}
   237 
   238 	m_ObjLookup.insert(name, obj);
   239 
   240 	return obj->object_id;
   241 }
   242 
   243 void TopMenu::RemoveFromMenu(unsigned int object_id)
   244 {
   245 	if (object_id == 0 
   246 		|| object_id > m_Objects.size() 
   247 		|| m_Objects[object_id - 1]->is_free)
   248 	{
   249 		return;
   250 	}
   251 
   252 	topmenu_object_t *obj = m_Objects[object_id - 1];
   253 
   254 	m_ObjLookup.remove(obj->name);
   255 
   256 	if (obj->type == TopMenuObject_Category)
   257 	{
   258 		/* Find it in the category list. */
   259 		for (size_t i = 0; i < m_Categories.size(); i++)
   260 		{
   261 			if (m_Categories[i]->obj == obj)
   262 			{
   263 				/* Mark all children as removed + free.  Note we could
   264 				 * call into RemoveMenuItem() for this, but it'd be very 
   265 				 * inefficient!
   266 				 */
   267 				topmenu_category_t *cat = m_Categories[i];
   268 				for (size_t j = 0; j < m_Categories[i]->obj_list.size(); j++)
   269 				{
   270 					cat->obj_list[j]->callbacks->OnTopMenuObjectRemoved(this, cat->obj_list[j]->object_id);
   271 					cat->obj_list[j]->is_free = true;
   272 				}
   273 				
   274 				/* Remove the category from the list, then delete it. */
   275 				m_Categories.erase(m_Categories.iterAt(i));
   276 				delete cat;
   277 				break;
   278 			}
   279 		}
   280 
   281 		/* Update the root as changed. */
   282 		m_SerialNo++;
   283 		m_bCatsNeedResort = true;
   284 	}
   285 	else if (obj->type == TopMenuObject_Item)
   286 	{
   287 		/* Find the category this item is in. */
   288 		topmenu_category_t *parent_cat = NULL;
   289 		for (size_t i = 0; i < m_Categories.size(); i++)
   290 		{
   291 			if (m_Categories[i]->obj == obj->parent)
   292 			{
   293 				parent_cat = m_Categories[i];
   294 				break;
   295 			}
   296 		}
   297 
   298 		/* Erase it from the category's lists. */
   299 		if (parent_cat)
   300 		{
   301 			for (size_t i = 0; i < parent_cat->obj_list.size(); i++)
   302 			{
   303 				if (parent_cat->obj_list[i] == obj)
   304 				{
   305 					parent_cat->obj_list.erase(parent_cat->obj_list.iterAt(i));
   306 
   307 					/* If this category now has no items, mark root as changed 
   308 					 * so clients won't get the category drawn anymore.
   309 					 */
   310 					if (parent_cat->obj_list.size() == 0)
   311 					{
   312 						m_SerialNo++;
   313 					}
   314 					break;
   315 				}
   316 			}
   317 
   318 			/* Update the category as changed. */
   319 			parent_cat->reorder = true;
   320 			parent_cat->serial++;
   321 		}
   322 	}
   323 
   324 	/* Finally, mark the object as free. */
   325 	obj->is_free = true;
   326 
   327 	/* The callbacks pointer is still valid, so fire away! */
   328 	obj->callbacks->OnTopMenuObjectRemoved(this, object_id);
   329 }
   330 
   331 bool TopMenu::DisplayMenu(int client, unsigned int hold_time, TopMenuPosition position)
   332 {
   333 	if (m_clients == NULL)
   334 	{
   335 		return false;
   336 	}
   337 
   338 	IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
   339 	if (!pPlayer->IsInGame())
   340 	{
   341 		return false;
   342 	}
   343 
   344 	UpdateClientRoot(client, pPlayer);
   345 
   346 	topmenu_player_t *pClient = &m_clients[client];
   347 	if (pClient->root == NULL)
   348 	{
   349 		return false;
   350 	}
   351 
   352 	bool return_value = false;
   353 
   354 	if (position == TopMenuPosition_LastCategory && 
   355 		pClient->last_category < m_Categories.size())
   356 	{
   357 		return_value = DisplayCategory(client, pClient->last_category, hold_time, true);
   358 	}
   359 	else if (position == TopMenuPosition_LastRoot)
   360 	{
   361 		pClient->root->DisplayAtItem(client, hold_time, pClient->last_root_pos);
   362 	}
   363 	else if (position == TopMenuPosition_Start)
   364 	{
   365 		pClient->last_position = 0;
   366 		pClient->last_category = 0;
   367 		return_value = pClient->root->Display(client, hold_time);
   368 	}
   369 
   370 	return return_value;
   371 }
   372 
   373 bool TopMenu::DisplayCategory(int client, unsigned int category, unsigned int hold_time, bool last_position)
   374 {
   375 	UpdateClientCategory(client, category);
   376 
   377 	topmenu_player_t *pClient = &m_clients[client];
   378 	if (category >= pClient->cat_count
   379 		|| pClient->cats[category].menu == NULL)
   380 	{
   381 		return false;
   382 	}
   383 
   384 	bool return_value = false;
   385 
   386 	topmenu_player_category_t *player_cat = &(pClient->cats[category]);
   387 
   388 	pClient->last_category = category;
   389 	if (last_position)
   390 	{
   391 		return_value = player_cat->menu->DisplayAtItem(client, hold_time, pClient->last_position);
   392 	}
   393 	else
   394 	{
   395 		return_value = player_cat->menu->Display(client, hold_time);
   396 	}
   397 
   398 	return return_value;
   399 }
   400 
   401 void TopMenu::OnMenuSelect2(IBaseMenu *menu, int client, unsigned int item, unsigned int item_on_page)
   402 {
   403 	const char *item_name = menu->GetItemInfo(item, NULL);
   404 	if (!item_name)
   405 	{
   406 		return;
   407 	}
   408 
   409 	topmenu_object_t *obj;
   410 	topmenu_player_t *pClient = &m_clients[client];
   411 	topmenu_object_t **pObject = m_ObjLookup.retrieve(item_name);
   412 	if (pObject == NULL)
   413 	{
   414 		return;
   415 	}
   416 
   417 	obj = *pObject;
   418 
   419 	/* We now have the object... what do we do with it? */
   420 	if (obj->type == TopMenuObject_Category)
   421 	{
   422 		/* If it's a category, the user wants to view it.. */
   423 		for (size_t i = 0; i < m_Categories.size(); i++)
   424 		{
   425 			if (m_Categories[i]->obj == obj)
   426 			{
   427 				pClient->last_root_pos = item_on_page;
   428 				DisplayCategory(client, (unsigned int)i, MENU_TIME_FOREVER, false);
   429 				break;
   430 			}
   431 		}
   432 	}
   433 	else
   434 	{
   435 		pClient->last_position = item_on_page;
   436 		
   437 		/* Re-check access in case this user had their credentials revoked */
   438 		if (obj->cmdname[0] != '\0' && !adminsys->CheckAccess(client, obj->cmdname, obj->flags, false))
   439 		{
   440 			DisplayMenu(client, 0, TopMenuPosition_LastCategory);
   441 			return;
   442 		}
   443 		
   444 		/* Pass the information on to the callback */
   445 		obj->callbacks->OnTopMenuSelectOption(this, client, obj->object_id);
   446 	}
   447 }
   448 
   449 void TopMenu::OnMenuDrawItem(IBaseMenu *menu, int client, unsigned int item, unsigned int &style)
   450 {
   451 	const char *item_name = menu->GetItemInfo(item, NULL);
   452 	if (!item_name)
   453 	{
   454 		return;
   455 	}
   456 
   457 	topmenu_object_t *obj;
   458 	topmenu_object_t **pObject = m_ObjLookup.retrieve(item_name);
   459 	if (pObject == NULL)
   460 	{
   461 		return;
   462 	}
   463 
   464 	obj = *pObject;
   465 
   466 	if (obj->cmdname[0] == '\0')
   467 	{
   468 		return;
   469 	}
   470 
   471 	if (!adminsys->CheckAccess(client, obj->cmdname, obj->flags, false))
   472 	{
   473 		style = ITEMDRAW_IGNORE;
   474 	}
   475 }
   476 
   477 unsigned int TopMenu::OnMenuDisplayItem(IBaseMenu *menu,
   478 								int client,
   479 								IMenuPanel *panel,
   480 								unsigned int item,
   481 								const ItemDrawInfo &dr)
   482 {
   483 	const char *item_name = menu->GetItemInfo(item, NULL);
   484 	if (!item_name)
   485 	{
   486 		return 0;
   487 	}
   488 
   489 	topmenu_object_t *obj;
   490 	topmenu_object_t **pObject = m_ObjLookup.retrieve(item_name);
   491 	if (pObject == NULL)
   492 	{
   493 		return 0;
   494 	}
   495 
   496 	obj = *pObject;
   497 
   498 	/* Ask the object to render the text for this client */
   499 	char renderbuf[64];
   500 	obj->callbacks->OnTopMenuDrawOption(this, client, obj->object_id, renderbuf, sizeof(renderbuf));
   501 
   502 	/* Build the new draw info */
   503 	ItemDrawInfo new_dr = dr;
   504 	new_dr.display = renderbuf;
   505 
   506 	/* Man I love the menu API.  Ask the panel to draw the item and give the position 
   507 	 * back to Core's renderer.  This way we don't have to worry about keeping the 
   508 	 * render buffer static!
   509 	 */
   510 	return panel->DrawItem(new_dr);
   511 }
   512 
   513 void TopMenu::OnMenuCancel(IBaseMenu *menu, int client, MenuCancelReason reason)
   514 {
   515 	if (reason == MenuCancel_ExitBack)
   516 	{
   517 		/* If the client chose exit back, they were on a category menu, and we can
   518 		 * now display the root menu from the last known position.
   519 		 */
   520 		DisplayMenu(client, 0, TopMenuPosition_LastRoot);
   521 	}
   522 }
   523 
   524 void TopMenu::UpdateClientRoot(int client, IGamePlayer *pGamePlayer)
   525 {
   526 	topmenu_player_t *pClient = &m_clients[client];
   527 	IGamePlayer *pPlayer = pGamePlayer ? pGamePlayer : playerhelpers->GetGamePlayer(client);
   528 
   529 	/* Determine if an update is necessary */
   530 	bool is_update_needed = false;
   531 	if (pClient->menu_serial != m_SerialNo)
   532 	{
   533 		is_update_needed = true;
   534 	}
   535 	else if (pPlayer->GetUserId() != pClient->user_id)
   536 	{
   537 		is_update_needed = true;
   538 	}
   539 
   540 	/* If no update is needed at the root level, just leave now */
   541 	if (!is_update_needed)
   542 	{
   543 		return;
   544 	}
   545 
   546 	/* First we need to flush the cache... */
   547 	TearDownClient(pClient);
   548 
   549 	/* Now, rebuild the category list, but don't create menus */
   550 	if (m_Categories.size() == 0)
   551 	{
   552 		pClient->cat_count = 0;
   553 		pClient->cats = NULL;
   554 	}
   555 	else
   556 	{
   557 		pClient->cat_count = (unsigned int)m_Categories.size();
   558 		pClient->cats = new topmenu_player_category_t[pClient->cat_count];
   559 		memset(pClient->cats, 0, sizeof(topmenu_player_category_t) * pClient->cat_count);
   560 	}
   561 
   562 	/* Re-sort the root categories if needed */
   563 	SortCategoriesIfNeeded();
   564 
   565 	/* Build the root menu */
   566 	IBaseMenu *root_menu = menus->GetDefaultStyle()->CreateMenu(this, myself->GetIdentity());
   567 
   568 	/* Add the sorted items */
   569 	for (size_t i = 0; i < m_SortedCats.size(); i++)
   570 	{
   571 		if (m_Categories[m_SortedCats[i]]->obj_list.size() == 0)
   572 		{
   573 			continue;
   574 		}
   575 		root_menu->AppendItem(m_Categories[m_SortedCats[i]]->obj->name, ItemDrawInfo(""));
   576 	}
   577 
   578 	/* Now we need to handle un-sorted items.  This is slightly trickier, as we need to 
   579 	 * pre-render each category name, and cache those names.  Phew!
   580 	 */
   581 	if (m_UnsortedCats.size())
   582 	{
   583 		obj_by_name_t *item_list = new obj_by_name_t[m_UnsortedCats.size()];
   584 		for (size_t i = 0; i < m_UnsortedCats.size(); i++)
   585 		{
   586 			obj_by_name_t *temp_obj = &item_list[i];
   587 			topmenu_object_t *obj = m_Categories[m_UnsortedCats[i]]->obj;
   588 			obj->callbacks->OnTopMenuDrawOption(this,
   589 				client, 
   590 				obj->object_id,
   591 				temp_obj->name,
   592 				sizeof(temp_obj->name));
   593 			temp_obj->obj_index = m_UnsortedCats[i];
   594 		}
   595 
   596 		/* Sort our temp list */
   597 		qsort(item_list, m_UnsortedCats.size(), sizeof(obj_by_name_t), _SortObjectNamesDescending);
   598 
   599 		/* Add the new sorted categories */
   600 		for (size_t i = 0; i < m_SortedCats.size(); i++)
   601 		{
   602 			if (m_Categories[item_list[i].obj_index]->obj_list.size() == 0)
   603 			{
   604 				continue;
   605 			}
   606 			root_menu->AppendItem(m_Categories[item_list[i].obj_index]->obj->name, ItemDrawInfo(""));
   607 		}
   608 
   609 		delete [] item_list;
   610 	}
   611 
   612 	/* Set the menu's title */
   613 	char renderbuf[128];
   614 	m_pTitle->OnTopMenuDrawTitle(this, client, 0, renderbuf, sizeof(renderbuf));
   615 	root_menu->SetDefaultTitle(renderbuf);
   616 
   617 	/* The client is now fully updated */
   618 	pClient->root = root_menu;
   619 	pClient->user_id = pPlayer->GetUserId();
   620 	pClient->menu_serial = m_SerialNo;
   621 	pClient->last_position = 0;
   622 	pClient->last_category = 0;
   623 	pClient->last_root_pos = 0;
   624 }
   625 
   626 void TopMenu::UpdateClientCategory(int client, unsigned int category)
   627 {
   628 	/* Update the client's root menu just in case it needs it.  This 
   629 	 * will validate that we have both a valid client and a valid
   630 	 * category structure for that client.
   631 	 */
   632 	UpdateClientRoot(client);
   633 
   634 	/* Now it's guaranteed that our category tables will be usable */
   635 	topmenu_player_t *pClient = &m_clients[client];
   636 	topmenu_category_t *cat = m_Categories[category];
   637 	topmenu_player_category_t *player_cat = &(pClient->cats[category]);
   638 
   639 	/* Does the category actually need updating? */
   640 	if (player_cat->serial == cat->serial)
   641 	{
   642 		return;
   643 	}
   644 
   645 	/* Destroy any existing menu */
   646 	if (player_cat->menu)
   647 	{
   648 		player_cat->menu->Destroy();
   649 		player_cat->menu = NULL;
   650 	}
   651 
   652 	if (pClient->last_category == category)
   653 	{
   654 		pClient->last_position = 0;
   655 	}
   656 
   657 	IBaseMenu *cat_menu = menus->GetDefaultStyle()->CreateMenu(this, myself->GetIdentity());
   658 	
   659 	/* Categories get an "exit back" button */
   660 	cat_menu->SetMenuOptionFlags(cat_menu->GetMenuOptionFlags() | MENUFLAG_BUTTON_EXITBACK);
   661 
   662 	/* Re-sort the category if needed */
   663 	SortCategoryIfNeeded(category);
   664 
   665 	/* Build the menu with the sorted items first */
   666 	for (size_t i = 0; i < cat->sorted.size(); i++)
   667 	{
   668 		cat_menu->AppendItem(cat->sorted[i]->name, ItemDrawInfo(""));
   669 	}
   670 
   671 	/* Now handle unsorted items */
   672 	if (cat->unsorted.size())
   673 	{
   674 		/* Build a list of the item names */
   675 		obj_by_name_t *item_list = new obj_by_name_t[cat->unsorted.size()];
   676 		for (size_t i = 0; i < cat->unsorted.size(); i++)
   677 		{
   678 			obj_by_name_t *item = &item_list[i];
   679 			topmenu_object_t *obj = cat->unsorted[i];
   680 			obj->callbacks->OnTopMenuDrawOption(this,
   681 				client,
   682 				obj->object_id,
   683 				item->name,
   684 				sizeof(item->name));
   685 			item->obj_index = (unsigned int)i;
   686 		}
   687 
   688 		/* Sort the names */
   689 		qsort(item_list, cat->unsorted.size(), sizeof(obj_by_name_t), _SortObjectNamesDescending);
   690 
   691 		/* Add to the menu */
   692 		for (size_t i = 0; i < cat->unsorted.size(); i++)
   693 		{
   694 			cat_menu->AppendItem(cat->unsorted[item_list[i].obj_index]->name, ItemDrawInfo(""));
   695 		}
   696 
   697 		delete [] item_list;
   698 	}
   699 
   700 	/* Set the menu's title */
   701 	char renderbuf[128];
   702 	cat->obj->callbacks->OnTopMenuDrawTitle(this, 
   703 		client, 
   704 		cat->obj->object_id, 
   705 		renderbuf, 
   706 		sizeof(renderbuf));
   707 	cat_menu->SetDefaultTitle(renderbuf);
   708 
   709 	/* We're done! */
   710 	player_cat->menu = cat_menu;
   711 	player_cat->serial = cat->serial;
   712 }
   713 
   714 void TopMenu::SortCategoryIfNeeded(unsigned int category)
   715 {
   716 	topmenu_category_t *cat = m_Categories[category];
   717 	if (!cat->reorder)
   718 	{
   719 		return;
   720 	}
   721 
   722 	cat->sorted.clear();
   723 	cat->unsorted.clear();
   724 
   725 	if (cat->obj_list.size() == 0)
   726 	{
   727 		cat->reorder = false;
   728 		return;
   729 	}
   730 
   731 	CVector<unsigned int> to_sort;
   732 	for (size_t i = 0; i < cat->obj_list.size(); i++)
   733 	{
   734 		to_sort.push_back(i);
   735 	}
   736 
   737 	/* Find a matching category in the configs */
   738 	config_category_t *config_cat = NULL; 
   739 	for (size_t i = 0; i < m_Config.cats.size(); i++)
   740 	{
   741 		if (strcmp(m_Config.strings.GetString(m_Config.cats[i]->name), cat->obj->name) == 0)
   742 		{
   743 			config_cat = m_Config.cats[i];
   744 			break;
   745 		}
   746 	}
   747 
   748 	/* If there is a matching category, build a pre-sorted item list */
   749 	if (config_cat != NULL)
   750 	{
   751 		/* Find matching objects in this category */
   752 		for (size_t i = 0; i < config_cat->commands.size(); i++)
   753 		{
   754 			const char *config_name = m_Config.strings.GetString(config_cat->commands[i]);
   755 			for (size_t j = 0; j < to_sort.size(); j++)
   756 			{
   757 				if (strcmp(config_name, cat->obj_list[to_sort[j]]->name) == 0)
   758 				{
   759 					/* Place in the final list, then remove from the temporary list */
   760 					cat->sorted.push_back(cat->obj_list[to_sort[j]]);
   761 					to_sort.erase(to_sort.iterAt(j));
   762 					break;
   763 				}
   764 			}
   765 		}
   766 	}
   767 
   768 	/* Push any remaining items onto the unsorted list */
   769 	for (size_t i = 0; i < to_sort.size(); i++)
   770 	{
   771 		cat->unsorted.push_back(cat->obj_list[to_sort[i]]);
   772 	}
   773 
   774 	cat->reorder = false;
   775 }
   776 
   777 void TopMenu::SortCategoriesIfNeeded()
   778 {
   779 	if (!m_bCatsNeedResort)
   780 	{
   781 		return;
   782 	}
   783 
   784 	/* Clear sort results */
   785 	m_SortedCats.clear();
   786 	m_UnsortedCats.clear();
   787 
   788 	if (m_Categories.size() == 0)
   789 	{
   790 		m_bCatsNeedResort = false;
   791 		return;
   792 	}
   793 
   794 	CVector<unsigned int> to_sort;
   795 	for (unsigned int i = 0; i < (unsigned int)m_Categories.size(); i++)
   796 	{
   797 		to_sort.push_back(i);
   798 	}
   799 
   800 	/* If we have any predefined categories, add them in as they appear. */
   801 	for (size_t i= 0; i < m_Config.cats.size(); i++)
   802 	{
   803 		/* Find this category and map it in if we can */
   804 		for (size_t j = 0; j < to_sort.size(); j++)
   805 		{
   806 			if (strcmp(m_Config.strings.GetString(m_Config.cats[i]->name), 
   807 					   m_Categories[to_sort[j]]->obj->name) == 0)
   808 			{
   809 				/* Add to the real list and remove from the temporary */
   810 				m_SortedCats.push_back(to_sort[j]);
   811 				to_sort.erase(to_sort.iterAt(j));
   812 				break;
   813 			}
   814 		}
   815 	}
   816 
   817 	/* Push any remaining items onto the unsorted list */
   818 	for (size_t i = 0; i < to_sort.size(); i++)
   819 	{
   820 		m_UnsortedCats.push_back(to_sort[i]);
   821 	}
   822 
   823 	m_bCatsNeedResort = false;
   824 }
   825 
   826 void TopMenu::CreatePlayers(int max_clients)
   827 {
   828 	m_max_clients = max_clients;
   829 	m_clients = (topmenu_player_t *)malloc(sizeof(topmenu_player_t) * (max_clients + 1));
   830 	memset(m_clients, 0, sizeof(topmenu_player_t) * (max_clients + 1));
   831 }
   832 
   833 void TopMenu::TearDownClient(topmenu_player_t *player)
   834 {
   835 	if (player->cats != NULL)
   836 	{
   837 		for (unsigned int i = 0; i < player->cat_count; i++)
   838 		{
   839 			topmenu_player_category_t *player_cat = &(player->cats[i]);
   840 			if (player_cat->menu != NULL)
   841 			{
   842 				player_cat->menu->Destroy();
   843 			}
   844 		}
   845 		delete [] player->cats;
   846 	}
   847 
   848 	if (player->root != NULL)
   849 	{
   850 		player->root->Destroy();
   851 	}
   852 
   853 	memset(player, 0, sizeof(topmenu_player_t));
   854 }
   855 
   856 bool TopMenu::LoadConfiguration(const char *file, char *error, size_t maxlength)
   857 {
   858 	SMCParseError err;
   859 	unsigned int line = 0, col = 0;
   860 
   861 	if ((err = textparsers->ParseFile_SMC(file, this, &line, &col))
   862 		!= SMCParse_Okay)
   863 	{
   864 		const char *err_string = textparsers->GetSMCErrorString(err);
   865 		if (!err_string)
   866 		{
   867 			err_string = "Unknown";
   868 		}
   869 
   870 		UTIL_Format(error, maxlength, "%s", err_string);
   871 
   872 		return false;
   873 	}
   874 
   875 	return true;
   876 }
   877 
   878 bool TopMenu::OnIdentityRemoval(IdentityToken_t *owner)
   879 {
   880 	/* First sweep the categories owned by us */
   881 	CVector<unsigned int> obj_list;
   882 	for (size_t i = 0; i < m_Categories.size(); i++)
   883 	{
   884 		if (m_Categories[i]->obj->owner == owner)
   885 		{
   886 			obj_list.push_back(m_Categories[i]->obj->object_id);
   887 		}
   888 	}
   889 
   890 	for (size_t i = 0; i < obj_list.size(); i++)
   891 	{
   892 		RemoveFromMenu(obj_list[i]);
   893 	}
   894 
   895 	/* Now we can look for actual items */
   896 	for (size_t i = 0; i < m_Objects.size(); i++)
   897 	{
   898 		if (m_Objects[i]->is_free)
   899 		{
   900 			continue;
   901 		}
   902 		if (m_Objects[i]->owner == owner)
   903 		{
   904 			assert(m_Objects[i]->type != TopMenuObject_Category);
   905 			RemoveFromMenu(m_Objects[i]->object_id);
   906 		}
   907 	}
   908 
   909 	return true;
   910 }
   911 
   912 #define PARSE_STATE_NONE		0
   913 #define PARSE_STATE_CATEGORY	1
   914 unsigned int ignore_parse_level = 0;
   915 unsigned int current_parse_state = 0;
   916 config_category_t *cur_cat = NULL;
   917 
   918 void TopMenu::ReadSMC_ParseStart()
   919 {
   920 	current_parse_state = PARSE_STATE_NONE;
   921 	ignore_parse_level = 0;
   922 	cur_cat = NULL;
   923 
   924 	/* Reset the old config */
   925 	m_Config.strings.Reset();
   926 	for (size_t i = 0; i < m_Config.cats.size(); i++)
   927 	{
   928 		delete m_Config.cats[i];
   929 	}
   930 	m_Config.cats.clear();
   931 }
   932 
   933 SMCParseResult TopMenu::ReadSMC_NewSection(const char *name, bool opt_quotes)
   934 {
   935 	if (ignore_parse_level)
   936 	{
   937 		ignore_parse_level++;
   938 	}
   939 	else
   940 	{
   941 		if (current_parse_state == PARSE_STATE_NONE)
   942 		{
   943 			cur_cat = new config_category_t;
   944 			cur_cat->name = m_Config.strings.AddString(name);
   945 			m_Config.cats.push_back(cur_cat);
   946 			current_parse_state = PARSE_STATE_CATEGORY;
   947 		}
   948 		else
   949 		{
   950 			ignore_parse_level = 1;
   951 		}
   952 	}
   953 
   954 	return SMCParse_Continue;
   955 }
   956 
   957 SMCParseResult TopMenu::ReadSMC_KeyValue(const char *key, const char *value, bool key_quotes, bool value_quotes)
   958 {
   959 	if (ignore_parse_level > 0
   960 		|| current_parse_state != PARSE_STATE_CATEGORY
   961 		|| cur_cat == NULL)
   962 	{
   963 		return SMCParse_Continue;
   964 	}
   965 
   966 	if (strcmp(key, "item") == 0)
   967 	{
   968 		cur_cat->commands.push_back(m_Config.strings.AddString(value));
   969 	}
   970 
   971 	return SMCParse_Continue;
   972 }
   973 
   974 SMCParseResult TopMenu::ReadSMC_LeavingSection()
   975 {
   976 	if (ignore_parse_level)
   977 	{
   978 		ignore_parse_level--;
   979 	}
   980 	else
   981 	{
   982 		if (current_parse_state == PARSE_STATE_CATEGORY)
   983 		{
   984 			cur_cat = NULL;
   985 			current_parse_state = PARSE_STATE_NONE;
   986 		}
   987 	}
   988 
   989 	return SMCParse_Continue;
   990 }
   991 
   992 unsigned int TopMenu::FindCategory(const char *name)
   993 {
   994 	topmenu_object_t **p_obj = m_ObjLookup.retrieve(name);
   995 	if (!p_obj)
   996 	{
   997 		return 0;
   998 	}
   999 
  1000 	topmenu_object_t *obj = *p_obj;
  1001 	if (obj->type != TopMenuObject_Category)
  1002 	{
  1003 		return 0;
  1004 	}
  1005 
  1006 	return obj->object_id;
  1007 }
  1008 
  1009 int _SortObjectNamesDescending(const void *ptr1, const void *ptr2)
  1010 {
  1011 	obj_by_name_t *obj1 = (obj_by_name_t *)ptr1;
  1012 	obj_by_name_t *obj2 = (obj_by_name_t *)ptr2;
  1013 	return strcmp(obj1->name, obj2->name);
  1014 }
  1015 
  1016 unsigned int strncopy(char *dest, const char *src, size_t count)
  1017 {
  1018 	if (!count)
  1019 	{
  1020 		return 0;
  1021 	}
  1022 
  1023 	char *start = dest;
  1024 	while ((*src) && (--count))
  1025 	{
  1026 		*dest++ = *src++;
  1027 	}
  1028 	*dest = '\0';
  1029 
  1030 	return (dest - start);
  1031 }
  1032 
  1033 size_t UTIL_Format(char *buffer, size_t maxlength, const char *fmt, ...)
  1034 {
  1035 	va_list ap;
  1036 	va_start(ap, fmt);
  1037 	size_t len = vsnprintf(buffer, maxlength, fmt, ap);
  1038 	va_end(ap);
  1039 
  1040 	if (len >= maxlength)
  1041 	{
  1042 		buffer[maxlength - 1] = '\0';
  1043 		return (maxlength - 1);
  1044 	}
  1045 	else
  1046 	{
  1047 		return len;
  1048 	}
  1049 }