extensions/topmenus/TopMenu.cpp
author David Anderson <dvander@alliedmods.net>
Wed Jan 07 02:08:14 2009 -0500 (2009-01-07)
changeset 2510 3b2349600557
parent 2323 efa6bfdd7571
child 4078 483b617ef4f2
permissions -rw-r--r--
Fixed top menu categories being available even if not usable (bug 3256, r=me).
     1 /**
     2  * vim: set ts=4 :
     3  * =============================================================================
     4  * SourceMod TopMenus Extension
     5  * Copyright (C) 2004-2008 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 	while (m_Categories.size())
    63 	{
    64 		RemoveFromMenu(m_Categories[0]->obj->object_id);
    65 	}
    66 
    67 	/* Remove all objects */
    68 	for (size_t i = 0; i < m_Objects.size(); i++)
    69 	{
    70 		assert(m_Objects[i]->is_free == true);
    71 		delete m_Objects[i];
    72 	}
    73 
    74 	m_pTitle->OnTopMenuObjectRemoved(this, 0);
    75 
    76 	/* Delete all cached config entries */
    77 	for (size_t i = 0; i < m_Config.cats.size(); i++)
    78 	{
    79 		delete m_Config.cats[i];
    80 	}
    81 
    82 	/* Sweep players */
    83 	for (size_t i = 0; i <= (size_t)m_max_clients; i++)
    84 	{
    85 		TearDownClient(&m_clients[i]);
    86 	}
    87 	delete [] m_clients;
    88 }
    89 
    90 unsigned int TopMenu::CalcMemUsage()
    91 {
    92 	unsigned int size = sizeof(TopMenu);
    93 
    94 	size += m_Config.strings.GetMemTable()->MemUsage();
    95 	size += (m_Config.cats.size() * sizeof(int));
    96 	size += (sizeof(topmenu_player_t) * (ABSOLUTE_PLAYER_LIMIT + 1));
    97 	size += (m_SortedCats.size() * sizeof(unsigned int));
    98 	size += (m_UnsortedCats.size() * sizeof(unsigned int));
    99 	size += (m_Categories.size() * (sizeof(topmenu_category_t *) + sizeof(topmenu_category_t)));
   100 	size += (m_Objects.size() * (sizeof(topmenu_object_t *) + sizeof(topmenu_object_t)));
   101 	size += m_ObjLookup.mem_usage();
   102 
   103 	for (size_t i = 0; i < m_Categories.size(); i++)
   104 	{
   105 		size += m_Categories[i]->obj_list.size() * sizeof(topmenu_object_t *);
   106 		size += m_Categories[i]->sorted.size() * sizeof(topmenu_object_t *);
   107 		size += m_Categories[i]->unsorted.size() * sizeof(topmenu_object_t *);
   108 	}
   109 
   110 	return size;
   111 }
   112 
   113 void TopMenu::OnClientConnected(int client)
   114 {
   115 	if (m_clients == NULL)
   116 	{
   117 		return;
   118 	}
   119 
   120 	topmenu_player_t *player = &m_clients[client];
   121 	TearDownClient(player);
   122 }
   123 
   124 void TopMenu::OnClientDisconnected(int client)
   125 {
   126 	if (m_clients == NULL)
   127 	{
   128 		return;
   129 	}
   130 
   131 	topmenu_player_t *player = &m_clients[client];
   132 	TearDownClient(player);
   133 }
   134 
   135 void TopMenu::OnServerActivated(int max_clients)
   136 {
   137 	if (m_clients == NULL)
   138 	{
   139 		CreatePlayers(max_clients);
   140 	}
   141 }
   142 
   143 unsigned int TopMenu::AddToMenu(const char *name,
   144 								TopMenuObjectType type,
   145 								ITopMenuObjectCallbacks *callbacks,
   146 								IdentityToken_t *owner,
   147 								const char *cmdname,
   148 								FlagBits flags,
   149 								unsigned int parent)
   150 {
   151 	return AddToMenu2(name, type, callbacks, owner, cmdname, flags, parent, NULL);
   152 }
   153 
   154 unsigned int TopMenu::AddToMenu2(const char *name,
   155 								 TopMenuObjectType type,
   156 								 ITopMenuObjectCallbacks *callbacks,
   157 								 IdentityToken_t *owner,
   158 								 const char *cmdname,
   159 								 FlagBits flags,
   160 								 unsigned int parent,
   161 								 const char *info_string)
   162 {
   163 	/* Sanity checks */
   164 	if (type == TopMenuObject_Category && parent != 0)
   165 	{
   166 		return 0;
   167 	}
   168 	else if (type == TopMenuObject_Item && parent == 0)
   169 	{
   170 		return 0;
   171 	}
   172 	else if (m_ObjLookup.retrieve(name) != NULL)
   173 	{
   174 		return 0;
   175 	}
   176 	else if (type != TopMenuObject_Item && type != TopMenuObject_Category)
   177 	{
   178 		return 0;
   179 	}
   180 
   181 	/* If we're adding an item, make sure the parent is valid, 
   182 	 * and that the parent is a category.
   183 	 */
   184 	topmenu_object_t *parent_obj = NULL;
   185 	topmenu_category_t *parent_cat = NULL;
   186 	if (type == TopMenuObject_Item)
   187 	{
   188 		/* Check parent index.  Note it will be >= 1 here. */
   189 		if (parent > m_Objects.size() || m_Objects[parent - 1]->is_free)
   190 		{
   191 			return 0;
   192 		}
   193 		parent_obj = m_Objects[parent - 1];
   194 
   195 		/* Find an equivalent pointer in the category array. */
   196 		for (size_t i = 0; i < m_Categories.size(); i++)
   197 		{
   198 			if (m_Categories[i]->obj == parent_obj)
   199 			{
   200 				parent_cat = m_Categories[i];
   201 				break;
   202 			}
   203 		}
   204 
   205 		/* If none was found, leave. */
   206 		if (parent_cat == NULL)
   207 		{
   208 			return 0;
   209 		}
   210 	}
   211 
   212 	/* Re-use an old object pointer if we can. */
   213 	topmenu_object_t *obj = NULL;
   214 	for (size_t i = 0; i < m_Objects.size(); i++)
   215 	{
   216 		if (m_Objects[i]->is_free == true)
   217 		{
   218 			obj = m_Objects[i];
   219 			break;
   220 		}
   221 	}
   222 
   223 	/* Otherwise, allocate a new one. */
   224 	if (obj == NULL)
   225 	{
   226 		obj = new topmenu_object_t;
   227 		obj->object_id = ((unsigned int)m_Objects.size()) + 1;
   228 		m_Objects.push_back(obj);
   229 	}
   230 
   231 	/* Initialize the object's properties. */
   232 	obj->callbacks = callbacks;
   233 	obj->flags = flags;
   234 	obj->owner = owner;
   235 	obj->type = type;
   236 	obj->is_free = false;
   237 	obj->parent = parent_obj;
   238 	strncopy(obj->name, name, sizeof(obj->name));
   239 	strncopy(obj->cmdname, cmdname ? cmdname : "", sizeof(obj->cmdname));
   240 	strncopy(obj->info, info_string ? info_string : "", sizeof(obj->info));
   241 
   242 	if (obj->type == TopMenuObject_Category)
   243 	{
   244 		/* Create a new category entry */
   245 		topmenu_category_t *cat = new topmenu_category_t;
   246 		cat->obj = obj;
   247 		cat->reorder = false;
   248 		cat->serial = 1;
   249 
   250 		/* Add it, then update our serial change number. */
   251 		obj->cat_id = m_Categories.size();
   252 		m_Categories.push_back(cat);
   253 		m_SerialNo++;
   254 
   255 		/* Updating sorting info */
   256 		m_bCatsNeedResort = true;
   257 	}
   258 	else if (obj->type == TopMenuObject_Item)
   259 	{
   260 		/* Update the category, mark it as needing changes */
   261 		obj->cat_id = 0;
   262 		parent_cat->obj_list.push_back(obj);
   263 		parent_cat->reorder = true;
   264 		parent_cat->serial++;
   265 
   266 		/* If the category just went from 0 to 1 items, mark it as 
   267 		 * changed, so clients get the category drawn.
   268 		 */
   269 		if (parent_cat->obj_list.size() == 1)
   270 		{
   271 			m_SerialNo++;
   272 		}
   273 	}
   274 
   275 	m_ObjLookup.insert(name, obj);
   276 
   277 	return obj->object_id;
   278 }
   279 
   280 const char *TopMenu::GetObjectInfoString(unsigned int object_id)
   281 {
   282 	if (object_id == 0 
   283 		|| object_id > m_Objects.size() 
   284 		|| m_Objects[object_id - 1]->is_free)
   285 	{
   286 		return NULL;
   287 	}
   288 
   289 	topmenu_object_t *obj = m_Objects[object_id - 1];
   290 
   291 	return obj->info;
   292 }
   293 
   294 const char *TopMenu::GetObjectName(unsigned int object_id)
   295 {
   296 	if (object_id == 0 
   297 		|| object_id > m_Objects.size() 
   298 		|| m_Objects[object_id - 1]->is_free)
   299 	{
   300 		return NULL;
   301 	}
   302 
   303 	topmenu_object_t *obj = m_Objects[object_id - 1];
   304 
   305 	return obj->name;
   306 }
   307 
   308 void TopMenu::RemoveFromMenu(unsigned int object_id)
   309 {
   310 	if (object_id == 0 
   311 		|| object_id > m_Objects.size() 
   312 		|| m_Objects[object_id - 1]->is_free)
   313 	{
   314 		return;
   315 	}
   316 
   317 	topmenu_object_t *obj = m_Objects[object_id - 1];
   318 
   319 	m_ObjLookup.remove(obj->name);
   320 
   321 	if (obj->type == TopMenuObject_Category)
   322 	{
   323 		/* Find it in the category list. */
   324 		for (size_t i = 0; i < m_Categories.size(); i++)
   325 		{
   326 			if (m_Categories[i]->obj == obj)
   327 			{
   328 				/* Mark all children as removed + free.  Note we could
   329 				 * call into RemoveMenuItem() for this, but it'd be very 
   330 				 * inefficient!
   331 				 */
   332 				topmenu_category_t *cat = m_Categories[i];
   333 				for (size_t j = 0; j < m_Categories[i]->obj_list.size(); j++)
   334 				{
   335 					m_ObjLookup.remove(cat->obj_list[j]->name);
   336 					cat->obj_list[j]->callbacks->OnTopMenuObjectRemoved(this, cat->obj_list[j]->object_id);
   337 					cat->obj_list[j]->is_free = true;
   338 				}
   339 				
   340 				/* Remove the category from the list, then delete it. */
   341 				m_Categories.erase(m_Categories.iterAt(i));
   342 				delete cat;
   343 				break;
   344 			}
   345 		}
   346 
   347 		/* Update the root as changed. */
   348 		m_SerialNo++;
   349 		m_bCatsNeedResort = true;
   350 	}
   351 	else if (obj->type == TopMenuObject_Item)
   352 	{
   353 		/* Find the category this item is in. */
   354 		topmenu_category_t *parent_cat = NULL;
   355 		for (size_t i = 0; i < m_Categories.size(); i++)
   356 		{
   357 			if (m_Categories[i]->obj == obj->parent)
   358 			{
   359 				parent_cat = m_Categories[i];
   360 				break;
   361 			}
   362 		}
   363 
   364 		/* Erase it from the category's lists. */
   365 		if (parent_cat)
   366 		{
   367 			for (size_t i = 0; i < parent_cat->obj_list.size(); i++)
   368 			{
   369 				if (parent_cat->obj_list[i] == obj)
   370 				{
   371 					parent_cat->obj_list.erase(parent_cat->obj_list.iterAt(i));
   372 
   373 					/* If this category now has no items, mark root as changed 
   374 					 * so clients won't get the category drawn anymore.
   375 					 */
   376 					if (parent_cat->obj_list.size() == 0)
   377 					{
   378 						m_SerialNo++;
   379 					}
   380 					break;
   381 				}
   382 			}
   383 
   384 			/* Update the category as changed. */
   385 			parent_cat->reorder = true;
   386 			parent_cat->serial++;
   387 		}
   388 	}
   389 
   390 	/* The callbacks pointer is still valid, so fire away! */
   391 	obj->callbacks->OnTopMenuObjectRemoved(this, object_id);
   392 
   393 	/* Finally, mark the object as free. */
   394 	obj->is_free = true;
   395 }
   396 
   397 bool TopMenu::DisplayMenu(int client, unsigned int hold_time, TopMenuPosition position)
   398 {
   399 	if (m_clients == NULL)
   400 	{
   401 		return false;
   402 	}
   403 
   404 	IGamePlayer *pPlayer = playerhelpers->GetGamePlayer(client);
   405 	if (!pPlayer->IsInGame())
   406 	{
   407 		return false;
   408 	}
   409 
   410 	UpdateClientRoot(client, pPlayer);
   411 
   412 	/* This is unfortunate but it's the best solution. */
   413 	for (size_t i = 0; i < m_Categories.size(); i++)
   414 	{
   415 		UpdateClientCategory(client, i, true);
   416 	}
   417 
   418 	topmenu_player_t *pClient = &m_clients[client];
   419 	if (pClient->root == NULL)
   420 	{
   421 		return false;
   422 	}
   423 
   424 	bool return_value = false;
   425 
   426 	if (position == TopMenuPosition_LastCategory && 
   427 		pClient->last_category < m_Categories.size())
   428 	{
   429 		return_value = DisplayCategory(client, pClient->last_category, hold_time, true);
   430 		if (!return_value)
   431 		{
   432 			return_value = pClient->root->DisplayAtItem(client, hold_time, pClient->last_root_pos);
   433 		}
   434 	}
   435 	else if (position == TopMenuPosition_LastRoot)
   436 	{
   437 		pClient->root->DisplayAtItem(client, hold_time, pClient->last_root_pos);
   438 	}
   439 	else if (position == TopMenuPosition_Start)
   440 	{
   441 		pClient->last_position = 0;
   442 		pClient->last_category = 0;
   443 		return_value = pClient->root->Display(client, hold_time);
   444 	}
   445 
   446 	return return_value;
   447 }
   448 
   449 bool TopMenu::DisplayCategory(int client, unsigned int category, unsigned int hold_time, bool last_position)
   450 {
   451 	UpdateClientCategory(client, category);
   452 
   453 	topmenu_player_t *pClient = &m_clients[client];
   454 	if (category >= pClient->cat_count || pClient->cats[category].menu == NULL)
   455 	{
   456 		return false;
   457 	}
   458 
   459 	bool return_value = false;
   460 
   461 	topmenu_player_category_t *player_cat = &(pClient->cats[category]);
   462 
   463 	pClient->last_category = category;
   464 	if (last_position)
   465 	{
   466 		return_value = player_cat->menu->DisplayAtItem(client, hold_time, pClient->last_position);
   467 	}
   468 	else
   469 	{
   470 		return_value = player_cat->menu->Display(client, hold_time);
   471 	}
   472 
   473 	return return_value;
   474 }
   475 
   476 void TopMenu::OnMenuSelect2(IBaseMenu *menu, int client, unsigned int item, unsigned int item_on_page)
   477 {
   478 	const char *item_name = menu->GetItemInfo(item, NULL);
   479 	if (!item_name)
   480 	{
   481 		return;
   482 	}
   483 
   484 	topmenu_object_t *obj;
   485 	topmenu_player_t *pClient = &m_clients[client];
   486 	topmenu_object_t **pObject = m_ObjLookup.retrieve(item_name);
   487 	if (pObject == NULL)
   488 	{
   489 		return;
   490 	}
   491 
   492 	obj = *pObject;
   493 
   494 	/* We now have the object... what do we do with it? */
   495 	if (obj->type == TopMenuObject_Category)
   496 	{
   497 		/* If it's a category, the user wants to view it.. */
   498 		assert(obj->cat_id < m_Categories.size());
   499 		assert(m_Categories[obj->cat_id]->obj == obj);
   500 		pClient->last_root_pos = item_on_page;
   501 		if (!DisplayCategory(client, obj->cat_id, MENU_TIME_FOREVER, false))
   502 		{
   503 			/* If we can't display the category, re-display the root menu.
   504 			 * This code should be dead as of bug 3256, which disables categories 
   505 			 * that cannot be displayed.
   506 			 */
   507 			DisplayMenu(client, MENU_TIME_FOREVER, TopMenuPosition_LastRoot);
   508 		}
   509 	}
   510 	else
   511 	{
   512 		pClient->last_position = item_on_page;
   513 		
   514 		/* Re-check access in case this user had their credentials revoked */
   515 		if (obj->cmdname[0] != '\0' && !adminsys->CheckAccess(client, obj->cmdname, obj->flags, false))
   516 		{
   517 			DisplayMenu(client, 0, TopMenuPosition_LastCategory);
   518 			return;
   519 		}
   520 		
   521 		/* Pass the information on to the callback */
   522 		obj->callbacks->OnTopMenuSelectOption(this, client, obj->object_id);
   523 	}
   524 }
   525 
   526 void TopMenu::OnMenuDrawItem(IBaseMenu *menu, int client, unsigned int item, unsigned int &style)
   527 {
   528 	const char *item_name = menu->GetItemInfo(item, NULL);
   529 	if (!item_name)
   530 	{
   531 		return;
   532 	}
   533 
   534 	topmenu_object_t *obj;
   535 	topmenu_object_t **pObject = m_ObjLookup.retrieve(item_name);
   536 	if (pObject == NULL)
   537 	{
   538 		return;
   539 	}
   540 
   541 	obj = *pObject;
   542 
   543 	/* If the category has nothing to display, disable it. */
   544 	if (obj->type == TopMenuObject_Category)
   545 	{
   546 		assert(obj->cat_id < m_Categories.size());
   547 		assert(m_Categories[obj->cat_id]->obj == obj);
   548 		topmenu_player_t *pClient = &m_clients[client];
   549 		if (obj->cat_id >= pClient->cat_count || pClient->cats[obj->cat_id].menu == NULL)
   550 		{
   551 			style = ITEMDRAW_IGNORE;
   552 			return;
   553 		}
   554 	}
   555 
   556 	style = obj->callbacks->OnTopMenuDrawOption(this, client, obj->object_id);
   557 	if (style != ITEMDRAW_DEFAULT)
   558 	{
   559 		return;
   560 	}
   561 
   562 	if (obj->cmdname[0] == '\0')
   563 	{
   564 		return;
   565 	}
   566 
   567 	if (!adminsys->CheckAccess(client, obj->cmdname, obj->flags, false))
   568 	{
   569 		style = ITEMDRAW_IGNORE;
   570 	}
   571 }
   572 
   573 unsigned int TopMenu::OnMenuDisplayItem(IBaseMenu *menu,
   574 								int client,
   575 								IMenuPanel *panel,
   576 								unsigned int item,
   577 								const ItemDrawInfo &dr)
   578 {
   579 	const char *item_name = menu->GetItemInfo(item, NULL);
   580 	if (!item_name)
   581 	{
   582 		return 0;
   583 	}
   584 
   585 	topmenu_object_t *obj;
   586 	topmenu_object_t **pObject = m_ObjLookup.retrieve(item_name);
   587 	if (pObject == NULL)
   588 	{
   589 		return 0;
   590 	}
   591 
   592 	obj = *pObject;
   593 
   594 	/* Ask the object to render the text for this client */
   595 	char renderbuf[64];
   596 	obj->callbacks->OnTopMenuDisplayOption(this, client, obj->object_id, renderbuf, sizeof(renderbuf));
   597 
   598 	/* Build the new draw info */
   599 	ItemDrawInfo new_dr = dr;
   600 	new_dr.display = renderbuf;
   601 
   602 	/* Man I love the menu API.  Ask the panel to draw the item and give the position 
   603 	 * back to Core's renderer.  This way we don't have to worry about keeping the 
   604 	 * render buffer static!
   605 	 */
   606 	return panel->DrawItem(new_dr);
   607 }
   608 
   609 void TopMenu::OnMenuCancel(IBaseMenu *menu, int client, MenuCancelReason reason)
   610 {
   611 	if (reason == MenuCancel_ExitBack)
   612 	{
   613 		/* If the client chose exit back, they were on a category menu, and we can
   614 		 * now display the root menu from the last known position.
   615 		 */
   616 		DisplayMenu(client, 0, TopMenuPosition_LastRoot);
   617 	}
   618 }
   619 
   620 void TopMenu::UpdateClientRoot(int client, IGamePlayer *pGamePlayer)
   621 {
   622 	topmenu_player_t *pClient = &m_clients[client];
   623 	IGamePlayer *pPlayer = pGamePlayer ? pGamePlayer : playerhelpers->GetGamePlayer(client);
   624 
   625 	/* Determine if an update is necessary */
   626 	bool is_update_needed = false;
   627 	if (pClient->menu_serial != m_SerialNo)
   628 	{
   629 		is_update_needed = true;
   630 	}
   631 	else if (pPlayer->GetUserId() != pClient->user_id)
   632 	{
   633 		is_update_needed = true;
   634 	}
   635 
   636 	/* If no update is needed at the root level, just leave now */
   637 	if (!is_update_needed)
   638 	{
   639 		return;
   640 	}
   641 
   642 	/* First we need to flush the cache... */
   643 	TearDownClient(pClient);
   644 
   645 	/* Now, rebuild the category list, but don't create menus */
   646 	if (m_Categories.size() == 0)
   647 	{
   648 		pClient->cat_count = 0;
   649 		pClient->cats = NULL;
   650 	}
   651 	else
   652 	{
   653 		pClient->cat_count = (unsigned int)m_Categories.size();
   654 		pClient->cats = new topmenu_player_category_t[pClient->cat_count];
   655 		memset(pClient->cats, 0, sizeof(topmenu_player_category_t) * pClient->cat_count);
   656 	}
   657 
   658 	/* Re-sort the root categories if needed */
   659 	SortCategoriesIfNeeded();
   660 
   661 	/* Build the root menu */
   662 	IBaseMenu *root_menu = menus->GetDefaultStyle()->CreateMenu(this, myself->GetIdentity());
   663 
   664 	/* Add the sorted items */
   665 	for (size_t i = 0; i < m_SortedCats.size(); i++)
   666 	{
   667 		if (m_Categories[m_SortedCats[i]]->obj_list.size() == 0)
   668 		{
   669 			continue;
   670 		}
   671 		root_menu->AppendItem(m_Categories[m_SortedCats[i]]->obj->name, ItemDrawInfo(""));
   672 	}
   673 
   674 	/* Now we need to handle un-sorted items.  This is slightly trickier, as we need to 
   675 	 * pre-render each category name, and cache those names.  Phew!
   676 	 */
   677 	if (m_UnsortedCats.size())
   678 	{
   679 		obj_by_name_t *item_list = new obj_by_name_t[m_UnsortedCats.size()];
   680 		for (size_t i = 0; i < m_UnsortedCats.size(); i++)
   681 		{
   682 			obj_by_name_t *temp_obj = &item_list[i];
   683 			topmenu_object_t *obj = m_Categories[m_UnsortedCats[i]]->obj;
   684 			obj->callbacks->OnTopMenuDisplayOption(this,
   685 				client, 
   686 				obj->object_id,
   687 				temp_obj->name,
   688 				sizeof(temp_obj->name));
   689 			temp_obj->obj_index = m_UnsortedCats[i];
   690 		}
   691 
   692 		/* Sort our temp list */
   693 		qsort(item_list, m_UnsortedCats.size(), sizeof(obj_by_name_t), _SortObjectNamesDescending);
   694 
   695 		/* Add the new sorted categories */
   696 		for (size_t i = 0; i < m_UnsortedCats.size(); i++)
   697 		{
   698 			if (m_Categories[item_list[i].obj_index]->obj_list.size() == 0)
   699 			{
   700 				continue;
   701 			}
   702 			root_menu->AppendItem(m_Categories[item_list[i].obj_index]->obj->name, ItemDrawInfo(""));
   703 		}
   704 
   705 		delete [] item_list;
   706 	}
   707 
   708 	/* Set the menu's title */
   709 	char renderbuf[128];
   710 	m_pTitle->OnTopMenuDisplayTitle(this, client, 0, renderbuf, sizeof(renderbuf));
   711 	root_menu->SetDefaultTitle(renderbuf);
   712 
   713 	/* The client is now fully updated */
   714 	pClient->root = root_menu;
   715 	pClient->user_id = pPlayer->GetUserId();
   716 	pClient->menu_serial = m_SerialNo;
   717 	pClient->last_position = 0;
   718 	pClient->last_category = 0;
   719 	pClient->last_root_pos = 0;
   720 }
   721 
   722 void TopMenu::UpdateClientCategory(int client, unsigned int category, bool bSkipRootCheck)
   723 {
   724 	bool has_access = false;
   725 
   726 	/* Update the client's root menu just in case it needs it.  This 
   727 	 * will validate that we have both a valid client and a valid
   728 	 * category structure for that client.
   729 	 */
   730 	if (!bSkipRootCheck)
   731 	{
   732 		UpdateClientRoot(client);
   733 	}
   734 
   735 	/* Now it's guaranteed that our category tables will be usable */
   736 	topmenu_player_t *pClient = &m_clients[client];
   737 	topmenu_category_t *cat = m_Categories[category];
   738 	topmenu_player_category_t *player_cat = &(pClient->cats[category]);
   739 
   740 	/* Does the category actually need updating? */
   741 	if (player_cat->serial == cat->serial)
   742 	{
   743 		return;
   744 	}
   745 
   746 	/* Destroy any existing menu */
   747 	if (player_cat->menu)
   748 	{
   749 		player_cat->menu->Destroy();
   750 		player_cat->menu = NULL;
   751 	}
   752 
   753 	if (pClient->last_category == category)
   754 	{
   755 		pClient->last_position = 0;
   756 	}
   757 
   758 	IBaseMenu *cat_menu = menus->GetDefaultStyle()->CreateMenu(this, myself->GetIdentity());
   759 	
   760 	/* Categories get an "exit back" button */
   761 	cat_menu->SetMenuOptionFlags(cat_menu->GetMenuOptionFlags() | MENUFLAG_BUTTON_EXITBACK);
   762 
   763 	/* Re-sort the category if needed */
   764 	SortCategoryIfNeeded(category);
   765 
   766 	/* Build the menu with the sorted items first */
   767 	for (size_t i = 0; i < cat->sorted.size(); i++)
   768 	{
   769 		cat_menu->AppendItem(cat->sorted[i]->name, ItemDrawInfo(""));
   770 		if (!has_access && adminsys->CheckAccess(client,
   771 												 cat->sorted[i]->cmdname,
   772 												 cat->sorted[i]->flags,
   773 												 false))
   774 		{
   775 			has_access = true;
   776 		}
   777 	}
   778 
   779 	/* Now handle unsorted items */
   780 	if (cat->unsorted.size())
   781 	{
   782 		/* Build a list of the item names */
   783 		obj_by_name_t *item_list = new obj_by_name_t[cat->unsorted.size()];
   784 		for (size_t i = 0; i < cat->unsorted.size(); i++)
   785 		{
   786 			obj_by_name_t *item = &item_list[i];
   787 			topmenu_object_t *obj = cat->unsorted[i];
   788 			obj->callbacks->OnTopMenuDisplayOption(this,
   789 				client,
   790 				obj->object_id,
   791 				item->name,
   792 				sizeof(item->name));
   793 			item->obj_index = (unsigned int)i;
   794 			if (!has_access && adminsys->CheckAccess(client, obj->cmdname, obj->flags, false))
   795 			{
   796 				has_access = true;
   797 			}
   798 		}
   799 
   800 		if (has_access)
   801 		{
   802 			/* Sort the names */
   803 			qsort(item_list,
   804 				cat->unsorted.size(),
   805 				sizeof(obj_by_name_t),
   806 				_SortObjectNamesDescending);
   807 
   808 			/* Add to the menu */
   809 			for (size_t i = 0; i < cat->unsorted.size(); i++)
   810 			{
   811 				cat_menu->AppendItem(cat->unsorted[item_list[i].obj_index]->name, ItemDrawInfo(""));
   812 			}
   813 		}
   814 
   815 		delete [] item_list;
   816 	}
   817 
   818 	/* If the player has access to no items, don't draw a menu. */
   819 	if (!has_access)
   820 	{
   821 		cat_menu->Destroy();
   822 		player_cat->menu = NULL;
   823 		player_cat->serial = cat->serial;
   824 		return;
   825 	}
   826 
   827 	/* Set the menu's title */
   828 	char renderbuf[128];
   829 	cat->obj->callbacks->OnTopMenuDisplayTitle(this, 
   830 		client, 
   831 		cat->obj->object_id, 
   832 		renderbuf, 
   833 		sizeof(renderbuf));
   834 	cat_menu->SetDefaultTitle(renderbuf);
   835 
   836 	/* We're done! */
   837 	player_cat->menu = cat_menu;
   838 	player_cat->serial = cat->serial;
   839 }
   840 
   841 void TopMenu::SortCategoryIfNeeded(unsigned int category)
   842 {
   843 	topmenu_category_t *cat = m_Categories[category];
   844 	if (!cat->reorder)
   845 	{
   846 		return;
   847 	}
   848 
   849 	cat->sorted.clear();
   850 	cat->unsorted.clear();
   851 
   852 	if (cat->obj_list.size() == 0)
   853 	{
   854 		cat->reorder = false;
   855 		return;
   856 	}
   857 
   858 	CVector<unsigned int> to_sort;
   859 	for (size_t i = 0; i < cat->obj_list.size(); i++)
   860 	{
   861 		to_sort.push_back(i);
   862 	}
   863 
   864 	/* Find a matching category in the configs */
   865 	config_category_t *config_cat = NULL; 
   866 	for (size_t i = 0; i < m_Config.cats.size(); i++)
   867 	{
   868 		if (strcmp(m_Config.strings.GetString(m_Config.cats[i]->name), cat->obj->name) == 0)
   869 		{
   870 			config_cat = m_Config.cats[i];
   871 			break;
   872 		}
   873 	}
   874 
   875 	/* If there is a matching category, build a pre-sorted item list */
   876 	if (config_cat != NULL)
   877 	{
   878 		/* Find matching objects in this category */
   879 		for (size_t i = 0; i < config_cat->commands.size(); i++)
   880 		{
   881 			const char *config_name = m_Config.strings.GetString(config_cat->commands[i]);
   882 			for (size_t j = 0; j < to_sort.size(); j++)
   883 			{
   884 				if (strcmp(config_name, cat->obj_list[to_sort[j]]->name) == 0)
   885 				{
   886 					/* Place in the final list, then remove from the temporary list */
   887 					cat->sorted.push_back(cat->obj_list[to_sort[j]]);
   888 					to_sort.erase(to_sort.iterAt(j));
   889 					break;
   890 				}
   891 			}
   892 		}
   893 	}
   894 
   895 	/* Push any remaining items onto the unsorted list */
   896 	for (size_t i = 0; i < to_sort.size(); i++)
   897 	{
   898 		cat->unsorted.push_back(cat->obj_list[to_sort[i]]);
   899 	}
   900 
   901 	cat->reorder = false;
   902 }
   903 
   904 void TopMenu::SortCategoriesIfNeeded()
   905 {
   906 	if (!m_bCatsNeedResort)
   907 	{
   908 		return;
   909 	}
   910 
   911 	/* Clear sort results */
   912 	m_SortedCats.clear();
   913 	m_UnsortedCats.clear();
   914 
   915 	if (m_Categories.size() == 0)
   916 	{
   917 		m_bCatsNeedResort = false;
   918 		return;
   919 	}
   920 
   921 	CVector<unsigned int> to_sort;
   922 	for (unsigned int i = 0; i < (unsigned int)m_Categories.size(); i++)
   923 	{
   924 		to_sort.push_back(i);
   925 	}
   926 
   927 	/* If we have any predefined categories, add them in as they appear. */
   928 	for (size_t i= 0; i < m_Config.cats.size(); i++)
   929 	{
   930 		/* Find this category and map it in if we can */
   931 		for (size_t j = 0; j < to_sort.size(); j++)
   932 		{
   933 			if (strcmp(m_Config.strings.GetString(m_Config.cats[i]->name), 
   934 					   m_Categories[to_sort[j]]->obj->name) == 0)
   935 			{
   936 				/* Add to the real list and remove from the temporary */
   937 				m_SortedCats.push_back(to_sort[j]);
   938 				to_sort.erase(to_sort.iterAt(j));
   939 				break;
   940 			}
   941 		}
   942 	}
   943 
   944 	/* Push any remaining items onto the unsorted list */
   945 	for (size_t i = 0; i < to_sort.size(); i++)
   946 	{
   947 		m_UnsortedCats.push_back(to_sort[i]);
   948 	}
   949 
   950 	m_bCatsNeedResort = false;
   951 }
   952 
   953 void TopMenu::CreatePlayers(int max_clients)
   954 {
   955 	m_max_clients = max_clients;
   956 	m_clients = (topmenu_player_t *)malloc(sizeof(topmenu_player_t) * (ABSOLUTE_PLAYER_LIMIT + 1));
   957 	memset(m_clients, 0, sizeof(topmenu_player_t) * (ABSOLUTE_PLAYER_LIMIT + 1));
   958 }
   959 
   960 void TopMenu::TearDownClient(topmenu_player_t *player)
   961 {
   962 	if (player->cats != NULL)
   963 	{
   964 		for (unsigned int i = 0; i < player->cat_count; i++)
   965 		{
   966 			topmenu_player_category_t *player_cat = &(player->cats[i]);
   967 			if (player_cat->menu != NULL)
   968 			{
   969 				player_cat->menu->Destroy();
   970 			}
   971 		}
   972 		delete [] player->cats;
   973 	}
   974 
   975 	if (player->root != NULL)
   976 	{
   977 		player->root->Destroy();
   978 	}
   979 
   980 	memset(player, 0, sizeof(topmenu_player_t));
   981 }
   982 
   983 bool TopMenu::LoadConfiguration(const char *file, char *error, size_t maxlength)
   984 {
   985 	SMCError err;
   986 	SMCStates states;
   987 
   988 	if ((err = textparsers->ParseFile_SMC(file, this, &states))
   989 		!= SMCError_Okay)
   990 	{
   991 		const char *err_string = textparsers->GetSMCErrorString(err);
   992 		if (!err_string)
   993 		{
   994 			err_string = "Unknown";
   995 		}
   996 
   997 		UTIL_Format(error, maxlength, "%s", err_string);
   998 
   999 		return false;
  1000 	}
  1001 
  1002 	return true;
  1003 }
  1004 
  1005 bool TopMenu::OnIdentityRemoval(IdentityToken_t *owner)
  1006 {
  1007 	/* First sweep the categories owned by us */
  1008 	CVector<unsigned int> obj_list;
  1009 	for (size_t i = 0; i < m_Categories.size(); i++)
  1010 	{
  1011 		if (m_Categories[i]->obj->owner == owner)
  1012 		{
  1013 			obj_list.push_back(m_Categories[i]->obj->object_id);
  1014 		}
  1015 	}
  1016 
  1017 	for (size_t i = 0; i < obj_list.size(); i++)
  1018 	{
  1019 		RemoveFromMenu(obj_list[i]);
  1020 	}
  1021 
  1022 	/* Now we can look for actual items */
  1023 	for (size_t i = 0; i < m_Objects.size(); i++)
  1024 	{
  1025 		if (m_Objects[i]->is_free)
  1026 		{
  1027 			continue;
  1028 		}
  1029 		if (m_Objects[i]->owner == owner)
  1030 		{
  1031 			assert(m_Objects[i]->type != TopMenuObject_Category);
  1032 			RemoveFromMenu(m_Objects[i]->object_id);
  1033 		}
  1034 	}
  1035 
  1036 	return true;
  1037 }
  1038 
  1039 #define PARSE_STATE_NONE		0
  1040 #define PARSE_STATE_MAIN		1
  1041 #define PARSE_STATE_CATEGORY	2
  1042 unsigned int ignore_parse_level = 0;
  1043 unsigned int current_parse_state = 0;
  1044 config_category_t *cur_cat = NULL;
  1045 
  1046 void TopMenu::ReadSMC_ParseStart()
  1047 {
  1048 	current_parse_state = PARSE_STATE_NONE;
  1049 	ignore_parse_level = 0;
  1050 	cur_cat = NULL;
  1051 
  1052 	/* Reset the old config */
  1053 	m_Config.strings.Reset();
  1054 	for (size_t i = 0; i < m_Config.cats.size(); i++)
  1055 	{
  1056 		delete m_Config.cats[i];
  1057 	}
  1058 	m_Config.cats.clear();
  1059 }
  1060 
  1061 SMCResult TopMenu::ReadSMC_NewSection(const SMCStates *states, const char *name)
  1062 {
  1063 	if (ignore_parse_level)
  1064 	{
  1065 		ignore_parse_level++;
  1066 	}
  1067 	else
  1068 	{
  1069 		if (current_parse_state == PARSE_STATE_NONE)
  1070 		{
  1071 			if (strcmp(name, "Menu") == 0)
  1072 			{
  1073 				current_parse_state = PARSE_STATE_MAIN;
  1074 			}
  1075 			else
  1076 			{
  1077 				ignore_parse_level = 1;
  1078 			}
  1079 		}
  1080 		else if (current_parse_state == PARSE_STATE_MAIN)
  1081 		{
  1082 			cur_cat = new config_category_t;
  1083 			cur_cat->name = m_Config.strings.AddString(name);
  1084 			m_Config.cats.push_back(cur_cat);
  1085 			current_parse_state = PARSE_STATE_CATEGORY;
  1086 		}
  1087 		else
  1088 		{
  1089 			ignore_parse_level = 1;
  1090 		}
  1091 	}
  1092 
  1093 	return SMCResult_Continue;
  1094 }
  1095 
  1096 SMCResult TopMenu::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value)
  1097 {
  1098 	if (ignore_parse_level > 0
  1099 		|| current_parse_state != PARSE_STATE_CATEGORY
  1100 		|| cur_cat == NULL)
  1101 	{
  1102 		return SMCResult_Continue;
  1103 	}
  1104 
  1105 	if (strcmp(key, "item") == 0)
  1106 	{
  1107 		cur_cat->commands.push_back(m_Config.strings.AddString(value));
  1108 	}
  1109 
  1110 	return SMCResult_Continue;
  1111 }
  1112 
  1113 SMCResult TopMenu::ReadSMC_LeavingSection(const SMCStates *states)
  1114 {
  1115 	if (ignore_parse_level)
  1116 	{
  1117 		ignore_parse_level--;
  1118 	}
  1119 	else
  1120 	{
  1121 		if (current_parse_state == PARSE_STATE_CATEGORY)
  1122 		{
  1123 			cur_cat = NULL;
  1124 			current_parse_state = PARSE_STATE_MAIN;
  1125 		}
  1126 		else if (current_parse_state == PARSE_STATE_MAIN)
  1127 		{
  1128 			current_parse_state = PARSE_STATE_NONE;
  1129 		}
  1130 	}
  1131 
  1132 	return SMCResult_Continue;
  1133 }
  1134 
  1135 unsigned int TopMenu::FindCategory(const char *name)
  1136 {
  1137 	topmenu_object_t **p_obj = m_ObjLookup.retrieve(name);
  1138 	if (!p_obj)
  1139 	{
  1140 		return 0;
  1141 	}
  1142 
  1143 	topmenu_object_t *obj = *p_obj;
  1144 	if (obj->type != TopMenuObject_Category)
  1145 	{
  1146 		return 0;
  1147 	}
  1148 
  1149 	return obj->object_id;
  1150 }
  1151 
  1152 void TopMenu::OnMaxPlayersChanged( int newvalue )
  1153 {
  1154 	m_max_clients = newvalue;
  1155 }
  1156 
  1157 int _SortObjectNamesDescending(const void *ptr1, const void *ptr2)
  1158 {
  1159 	obj_by_name_t *obj1 = (obj_by_name_t *)ptr1;
  1160 	obj_by_name_t *obj2 = (obj_by_name_t *)ptr2;
  1161 	return strcmp(obj1->name, obj2->name);
  1162 }
  1163 
  1164 unsigned int strncopy(char *dest, const char *src, size_t count)
  1165 {
  1166 	if (!count)
  1167 	{
  1168 		return 0;
  1169 	}
  1170 
  1171 	char *start = dest;
  1172 	while ((*src) && (--count))
  1173 	{
  1174 		*dest++ = *src++;
  1175 	}
  1176 	*dest = '\0';
  1177 
  1178 	return (dest - start);
  1179 }
  1180 
  1181 size_t UTIL_Format(char *buffer, size_t maxlength, const char *fmt, ...)
  1182 {
  1183 	va_list ap;
  1184 	va_start(ap, fmt);
  1185 	size_t len = vsnprintf(buffer, maxlength, fmt, ap);
  1186 	va_end(ap);
  1187 
  1188 	if (len >= maxlength)
  1189 	{
  1190 		buffer[maxlength - 1] = '\0';
  1191 		return (maxlength - 1);
  1192 	}
  1193 	else
  1194 	{
  1195 		return len;
  1196 	}
  1197 }