Menu API (SourceMod)/ru

From AlliedModders Wiki
Jump to: navigation, search
Language: English  • русский

SourceMod обладает большим API для создания и отображений меню для клиентов. В отличии от AMX Mod X, API SourceMod'a обладает широким функционалом. Меню базируются на обратных вызовах (callbacks), которые гарантированно будут вызваны.

Для C++, API меню может быть найдено в public/IMenuManager.h. Для SourcePawn - scripting/include/menus.inc.

Объекты

Система меню SourceMod основана на объектно ориентированной иерархии. Понимание этой иерархии, даже для скриптинга, является ключевым моментом для эффективного использования меню.

Стили

Высший уровень объектов называется MenuStyle (IMenuStyle в C++). Стили описывают уникальную систему меню. Всего есть два стиля, встроенных в SourceMod:

  • Valve Styly, так же называемым "ESC" меню; 8 пунктов на странице, нельзя добавить текст без занятия слота или спрятать (отключить) текст.
  • Radio Style, так же называемый "AMX" меню; 10 пунктов на странице, можно добавить любой текст.

Каждый MenuStyle обладает своими правилами и свойствами. Вы можете считать, что каждый из них существует на раздельных "каналах". Например, два разных меню могут быть на экране игрока, будучи Valve и Radio меню одновременно, и SourceMod сможет обработать их обоих без всяких проблем. Это всё возможно благодаря тому, что каждый стиль отслеживает свои меню отдельно.

Панели

Меню низшего уровня называется Панелью (IMenuPanel в C++). Панель - один 'кусок' текста. Пункты и текст могут добавляться к панели столько раз, сколько поддерживает родительский стиль. Например, Valve стиль не поддерживает отображение текста без занятия слота или отключенных пунктов. Однако с Radio стилем панели это становится возможным, и вы можете отобразить большое количество текста так, как вам это захочется.

Панель считается временным объектом. Она создается, рендерится, отображается, и затем удаляется. Хотя она может быть сохранена на неопределенное время, если в этом есть необходимость.

Правила и ограничения Vavle стиля:

  • Максимум пунктов на странице: 8
  • Отключенный текст не отображается.
  • Нельзя написать текст, не заняв слота.
  • Пробелы не добавляются к пробелам\новым линиям, добавляя чувство "ограниченности".
  • Пользователь должен нажать "ESC" или быть в своей консоли, чтобы увидеть меню.

Правила и ограничения Radio стиля:

  • 10 пунктов на странице
  • Заглавия белые; пункты желтые, если не отключены. Если отключены, то белого цвета.
  • Нулевой элемент всегда белый. Для согласованности это значит, что навигационное управление всегда белое и находится в следующей секции, и просто не отрисовывается, если отсутствует.

Меню

Наконец, есть просто Меню (IBaseMenu in C++). Это вспомогательный объект, созданный для хранения базированных на меню выбираемых пунктов. В отличии от низкоуровневых панелей, меню содержит пункты, и может содержать только те пункты, которые можно выбрать (т.е. занимающие слоты). Они делятся на две категории:

  • Не нумерованные: эти меню могут иметь определенное число пунктов, и не имеют навигационных опций, кроме кнопки "Выход", которая всегда добавляется на последнюю позицию, поддерживаемую стилем.
    • Valve Стиль: максимум пунктов: 8
    • Radio Стиль: максимум пунктов: 10
  • Нумерованные: такие меню могут содержать сколь угодно пунктов. Во время отображения будет показано только определенное число пунктов. Автоматически будут добавлены навигационные пункты, таким образом, игроки легко могут возвращаться назад и вперед к определенным "страницам" пунктов меню.
    • "Назад" всегда отображается как первый навигационный пункт, третий с конца поддерживаемых позиций. Не будет добавлен, если меню содержит только одну страницу. Если нет никаких предыдущих страниц, текст никогда не будет отображен, ни в каком стиле. Если возможно, то меню будет дополнено необходимыми пробелами.
      • Valve Стиль, позиция: 6
      • Radio Стиль, позиция: 8
    • "Вперед" всегда отображается как второй навигационный пункт, второй с конца из доступных позиций. Не отображается, если меню состоит из одной страницы. Если нет следующих страниц, то текс не будет отображен при любом стиле. Если возможно, то меню будет дополнено необходимыми пробелами.
      • Valve Стиль, позиция: 7
      • Radio Стиль, позиция: 9
    • "Выход" отображается, если у меню включена кнопка "выход". Всегда занимает последнюю из возможных позиций
      • Valve Стиль, позиция: 8
      • Radio Стиль, позиция: 10

Цель меню - упростить процедуру хранения, отображения, и вычисления выбранного пункта. Таким образом, меню не позволяет добавлять пустой текст, так как это значительно усложняет алгоритм отображения.

Внутренне, меню отображается через RenderMenu алогритм. Этот алгоритм создает временную панель и заполняет её пунктами из меню. Эта панель отображается клиенту. Алгоритм пытается создать свободное перемещение по всем меню, и по всем стилям. Таким образом, все меню отображаются через IBaseMenu класс, или Menu дескрипторы (Handles), и будет выглядеть и действовать так же, как и меню API, основанное на панельном API.

Обратные вызовы (callbacks)

Краткий обзор

Меню базируются на системе обратных вызовов. Каждый обратный вызов предоставляет собой действие, которые происходят во время цикла отображения меню. Цикл состоит из множества уведомлений:

  • Начало уведомления.
    • Уведомление об отображении, если меню может быть отображено клиенту.
    • Уведомление о выборе пункта или кнопки отмены.
  • Конец уведомления.

Так как End обозначает конец полного цикла отображения, то обычно его используют для уничтожения временных меню.

Спецификация

Детальное объяснение этих событий ниже. Для C++, IBaseMenu указатель всегда доступен. Для SourcePawn, Menu хандл и MenuAction всегда установлены в MenuHandler callback. В отличии от C++, SourcePawn API позволяет вызвать лишь некоторые действия, если они были запрошены во время создания меню. Это сделано в целях оптимизации. Однако, эти действия не могут быть не вызваны.

  • Start. Меню было признано. Это не означает, что меню будет отображено; однако, это гарантирует, что событие "OnMenuEnd" будет вызвано.
    • OnMenuStart() в C++.
    • MenuAction_Start в SourcePawn. Не будет вызвано, если не было запрошено.
      • param1: Игнорируется (всегда 0).
      • param2: Игнорируется (всегда 0).
  • Display. Меню отобразилось клиенту.
    • OnMenuDisplay() в C++. IMenuPanel доступны указатель и индекс клиента.
    • MenuAction_Display в SourcePawn. Не будет вызвано, если не было запрошено.
      • param1: Индекс клиента.
      • param2: Хандл меню.
  • Select. Пункт был выбран. Будет дана позиция пункта в меню, а не нажатая кнопка (если меню не является панелью)
    • OnMenuSelect() в C++. Передаются индекс клиента и позиция пункта.
    • MenuAction_Select в SourcePawn. Это действие всегда вызывается, вне зависимости от запроса.
      • param1: Индекс клиента.
      • param2: Позиция пункта.
  • Cancel. Отображение меню у одного из клиентов было закрыто.
    • OnMenuCancel() в C++. Доступна причина закрытия.
    • MenuAction_Cancel в SourcePawn. Это действие всегда вызывается, вне зависимости от запроса.
      • param1: Индекс клиента.
      • param2: Код причины закрытия меню.
  • End. Цикл отображения меню закончился; это означает, что "Start" действие было, и одно из действий "Select" или "Cancel" так же произошло. Обычно в этом месте отчищаются ресурсы меню или само меню удаляется.
    • OnMenuEnd() в C++.
    • MenuAction_End в SourcePawn. Это действие всегда вызывается, вне зависимости от запроса.
      • param1: Причина завершения меню.
      • param2: если param1 == MenuEnd_Cancelled, то здесь содержится код причины закрытия меню.

Панели

Для панелей цикл жизни другой. Панели могут вызывать лишь два callback'а из написанных выше, и гарантируется, что один из них точно будет вызван для данного цикла отображения. Для C++, IBaseMenu указатель будет всегда NULL. Для SourcePawn, меню Handle будет всегда INVALID_HANDLE.

  • Select. Кнопка была нажата. Этой кнопкой может быть любое число. Например, если в панели у вас 2 пункта, то клиент может вызвать это событие, нажав "43".
    • OnMenuSelect() в C++. Передаются индекс клиента и нажатая кнопка.
    • MenuAction_Select в SourcePawn.
      • param1: Индекс клиента.
      • param2: Номер нажатой клавиши.
  • Cancel. Отображение меню у одного из клиентов было закрыто.
    • OnMenuCancel() в C++. Доступна причина закрытия.
    • MenuAction_Cancel в SourcePawn.
      • param1: Индекс клиента.
      • param2: Код причины закрытия меню.


Примеры

Для начала попробуем сделать очень простое меню:

Вы любите яблоки?
1. Да
2. Нет

Сделаем это как меню и как панель, чтобы увидеть различия в их API.

Простое меню

Сперва напишем наш пример через API Меню. Для пошагового руководства смотрите Menus Step By Step (SourceMod Scripting).

public void OnPluginStart()
{
	// зарегистрировали консольную команду menu_test1 для открытия меню.
	RegConsoleCmd("menu_test1", Menu_Test1);
}
 
// хандл меню
int MenuHandler1(Menu menu, MenuAction action, int param1, int param2)
{
	/* Если был выбран какой-либо пункт, то сообщим клиенту о его выборе. */
	if (action == MenuAction_Select)
	{
		char info[32]; // переменная для хранения выбора
		bool found = GetMenuItem(menu, param2, info, sizeof(info)); // получаем информацию о выбранном в меню пункте
		PrintToConsole(param1, "Вы нажали: %d (найдено? %d информация: %s)", param2, found, info); // пишем клиенту в консоль
	}
	/* Если меню было отменено, то сообщим об этом серверу. */
	else if (action == MenuAction_Cancel)
	{
		PrintToServer("Клиент %d' закрыл меню.  Причина: %d", param1, param2);
	}
	/* Если меню "закончилось", то удалим его из памяти */
	else if (action == MenuAction_End)
	{
		delete menu;
	}
}
 
Action Menu_Test1(int client, int args)
{
	new Handle:menu = CreateMenu(MenuHandler1);
	SetMenuTitle(menu, "Вы любите яблоки?");
	AddMenuItem(menu, "да", "да");
	AddMenuItem(menu, "нет", "нет");
	SetMenuExitButton(menu, false);
	DisplayMenu(menu, client, 20);
 
	return Plugin_Handled;
}

Несколько очень важных замечаний о этом примере:

  • Одно из Select или Cancel событий всегда будет отослано обработчику действий (action handler).
  • End всегда будет отослано обработчику действий (action handler).
  • Мы удаляем наше меню в End действии, потому что наш дескриптор (Handle) нам больше не нужен. Если бы мы удалили его после DisplayMenu, это бы отменило отображение меню клиенту.
  • Меню по умолчанию имеет кнопку выхода. В нашем примере мы его отключили.
  • Наше меню будет отображаться 20 секунд. Это означает, что если клиент ничего не выбрал в течении 20 секунд, то меню будет закрыто. Это обычно необходимо для меню с голосованиями. В отличии от AMX Mod X, вам не нужно создавать таймер, чтобы быть увереным, что меню "закончилось".
  • Мы создали и уничтожили новый дескриптор меню (Menu Handle), однако мы можем этого и не делать. Вполне допустимо создать дескриптор (Handle) один раз для всего времени жизни плагина.
  • Чтобы нормально отображались русские буквы, кодировка файла должна быть UTF-8 (без BOOM), иначе будут квадратики вместо русских букв.

Наше законченное меню и приложенный вывод в консоли выглядит как на картинке ниже (был выбран ответ "Да", и картинка взята с англ вики, прим. переводчика).

Basic menu 1.PNG

Базовая панель

Теперь давайте перепишем наш пример с использованием Панели вместо Меню.

public OnPluginStart()
{
	// зарегистрировали консольную команду panel_test1 для открытия меню.
	RegConsoleCmd("panel_test1", Panel_Test1);
}
 
public PanelHandler1(Handle:menu, MenuAction:action, param1, param2)
{
	if (action == MenuAction_Select)
	{
		PrintToConsole(param1, "Вы выбрали: %d", param2);
	} else if (action == MenuAction_Cancel) {
		PrintToServer("Клиент %d' закрыл панель. Причина: %d", param1, param2);
	}
}
 
public Action:Panel_Test1(client, args)
{
	new Handle:panel = CreatePanel();
	SetPanelTitle(panel, "Вы любите яблоки?");
	DrawPanelItem(panel, "Да");
	DrawPanelItem(panel, "Нет");
 
	SendPanelToClient(panel, client, PanelHandler1, 20);
 
	CloseHandle(panel);
 
	return Plugin_Handled;
}

Как вы можете заметить, Панели сильно отличаются от Меню:

  • Мы можем удалить Панель как только закончим его отображать. Мы можем создать Панель один раз, и продолжать использовать его много раз, однако мы можем удалить его в любой момент, без нарушения цикла отображения клиентского меню.
  • Обработчик Панели (Handler) получает гораздо меньше информации. Так как Панели разрабатывались как грубый вывод текста, была сохранена возможность добавления информации без занятия слота. Поэтому обработчик может знать лишь об отмене или о нажатии любой клавиши с номером.
  • Отсутствует автоматизация. Вы не можете добавить больше определенного числа выбираемых пунктов и добавить нумерацию. Автоматический контроль требует использования тяжелого объектного API Меню.

Наше законченная панель и приложеный вывод в консоли выглядит как на картинке ниже (был выбран ответ "Да", и картинка взята с англ вики, прим. переводчика):

Basic panel 1.PNG

Базовое меню с нумерацией

Теперь сделаем пример с большим функционалом -- нумерацией. Попробуем сделать меню для смены карты. Самый простой путь - прочесть maplist.txt файл в начале плагина и создать меню по этому файла.

Так как чтение и парсинг файла довольно долгая операция, мы будем делать это только раз за карту. Поэтому мы построим меню в OnMapStart, и мы не будем вызывать CloseHandle до события OnMapEnd.

Исходный код:

new Handle:g_MapMenu = INVALID_HANDLE
 
public OnPluginStart()
{
	RegConsoleCmd("menu_changemap", Command_ChangeMap);
}
 
public OnMapStart()
{
	g_MapMenu = BuildMapMenu();
}
 
public OnMapEnd()
{
	if (g_MapMenu != INVALID_HANDLE)
	{
		CloseHandle(g_MapMenu);
		g_MapMenu = INVALID_HANDLE;
	}
}
 
Handle:BuildMapMenu()
{
	/* открываем файл */
	new Handle:file = OpenFile("maplist.txt", "rt");
	if (file == INVALID_HANDLE)
	{
		return INVALID_HANDLE;
	}
 
	/* создаем хандл меню */
	new Handle:menu = CreateMenu(Menu_ChangeMap);
	new String:mapname[255];
	while (!IsEndOfFile(file) && ReadFileLine(file, mapname, sizeof(mapname)))
	{
		if (mapname[0] == ';' || !IsCharAlpha(mapname[0]))
		{
			continue;
		}
		/* выбрасываем все пробелы из названия */
		new len = strlen(mapname);
		for (new i=0; i<len; i++)
		{
			if (IsCharSpace(mapname[i]))
			{
				mapname[i] = '\0';
				break;
			}
		}
		/* проверяем карту на валидность */
		if (!IsMapValid(mapname))
		{
			continue;
		}
		/* добавляем карту в меню */
		AddMenuItem(menu, mapname, mapname);
	}
	/* не забываем закрыть файл! */
	CloseHandle(file);
 
	/* наконец, устанавливаем заглавие меню */
	SetMenuTitle(menu, "Выберите карту:");
 
	return menu;
}
 
public Menu_ChangeMap(Handle:menu, MenuAction:action, param1, param2)
{
	if (action == MenuAction_Select)
	{
		new String:info[32];
 
		/* получаем информацию о пункте */
		new bool:found = GetMenuItem(menu, param2, info, sizeof(info));
 
		/* говорим об этом клиенту */
		PrintToConsole(param1, "Вы выбрали: %d (найдено? %d информация: %s)", param2, found, info);
 
		/* меняем карту */
		ServerCommand("changelevel %s", info);
	}
}
 
public Action:Command_ChangeMap(client, args)
{
	if (g_MapMenu == INVALID_HANDLE)
	{
		PrintToConsole(client, "Файл maplist.txt не был найден!");
		return Plugin_Handled;
	}	
 
	DisplayMenu(g_MapMenu, client, MENU_TIME_FOREVER);
 
	return Plugin_Handled;
}

Результатом такого меню будет множество пунктов (maplist.txt файл содержал примерно 18 карт). Таким образом, конечное меню содержит три страницы, которые идут друг за другом, и выглядят вот так:

Basic menu 2 page1.PNG Basic menu 2 page2.PNG Basic menu 2 page3.PNG

Сообщение в консоль будет выведено до смены карты, например, если выбрали cs_office:

Вы выбрали: 8 (найдено? 1 информация: cs_office)

Отображение и разработка этого меню через ShowMenu сообщение или Панельный API занимает много времени и является довольно сложной задачей. Придется отслеживать все пункты в массиве большого размера, страницы, которые пользователь просматривает, и написать функцию, которая будет вычислять, на основе текущей странице, пункт, который был выбран и какую кнопку нажали. Меню системы это всё делает за вас.

Заметки:

  • Пункты управления, которые не доступны, не рисуются. Например, на первой странице вы не можете нажать "назад", а на последней странице вы не можете нажать "вперед". Несмотря на это, меню пытается удерживать каждый интерфейс последовательно. Таким образом, визуально, каждая навигационная клавиша всегда на одной и той же позиции.
  • Если указать таймаут меню, то переключение между страницами не воздействует на общее время открытия меню. Например, если мы установим таймаут на 20 секунд, то каждое листание страницы будет устанавливать таймайт равный (20 - время, уже затраченное на просмотр). Только переоткрытие меню позволит вернуть таймаут времени на 20 секунд.
  • Если отключить кнопку выхода, то не смотря на это, пункты 8 и 9 будут всё так же "назад" и вперед, соответственно.
  • Мы не освобождаем дескриптор (Handle) меню в MenuAction_End, потому что наше меню глобальное/статическое, и нам не нужно пересоздавать его каждый раз.
  • На изображении отображена кнопка "Назад". В SourceMod версии 1011 и выше, кнопка "Назад" была переименована в "Предыдующая", и "Назад" зарезервированно для "ExitBack" функциональности. (актуально для ENG версии, на ру вроде бы оба пункта "назад". прим. переводчик)

Голосование

SourceMod также имеет API для отображения меню как голосования для больше чем одного клиента. SourceMod автоматически обрабатываем выбранные пункты and randomly picking a tie-breaker (не знаю как перевести. прим. переводчика). Так же API голосования добавляет два новых значения для MenuAction, которые во время отображения меню всегда вызываются.

  • MenuAction_VoteStart: Вызывается после MenuAction_Start, когда голосование оффициально началось.
  • MenuAction_VoteEnd: Вызывается после того, как все клиенты проголосовали или отменили свои меню голосований. Выбранный предмет может быть получен через param1. Вызывается перед MenuAction_End. Важно заметить, что это не замена MenuAction_End, это разные вещи. Нельзя освобождать меню в MenuAction_VoteEnd. Заметка: не вызывается, если используется SetVoteResultCallback().
  • MenuAction_VoteCancel: Вызывается, если меню было отмеено в процессе голосования. Если вызвалось, то MenuAction_VoteEnd или обратный вызов (callback) для результата голосований не будет вызваны, но MenuAction_End будет после этого. Причина отмены голосования находится в param1.

Система меню - это то же самое меню, только с двумя дополнительными свойствами:

  • Только одно голосование может быть активно. Вы должны проверять, не запущено ли голосование (IsVoteInProgress()), иначе VoteMenu() не сработает.
  • Если клиент во время голосования отключится, то его голос будет недействительным.

Пример внизу показывает, как создать функцию DoVoteMenu(), которая будет спрашивать, хотят ли они сменить карту на предложенную.

Простое Голосование

public Handle_VoteMenu(Handle:menu, MenuAction:action, param1, param2)
{
	if (action == MenuAction_End)
	{
		/* Вызывается после VoteEnd */
		CloseHandle(menu);
	} else if (action == MenuAction_VoteEnd) {
		/* 0=да, 1=нет */
		if (param1 == 0)
		{
			new String:map[64];
			GetMenuItem(menu, param1, map, sizeof(map));
			ServerCommand("changelevel %s", map);
		}
	}
}
 
DoVoteMenu(const String:map[])
{
	if (IsVoteInProgress())
	{
		return;
	}
 
	new Handle:menu = CreateMenu(Handle_VoteMenu);
	SetMenuTitle(menu, "Сменить карту на: %s?", map);
	AddMenuItem(menu, map, "да");
	AddMenuItem(menu, "no", "нет");
	SetMenuExitButton(menu, false);
	VoteMenuToAll(menu, 20);
}

Продвинутое Голосование

Если вам нужно больше информации о результатах голосовании, чем вам дает MenuAction_VoteEnd, вы можете указать отдельный обратный вызов (callback). Новый обратный вызов будет давать больше информации, но за опреденную цену: MenuAction_VoteEnd не будет вызвано, и вам придется самим решать, как обработать результат. Это делается с помощью SetVoteResultCallback().

Пример:

public Handle_VoteMenu(Handle:menu, MenuAction:action, param1, param2)
{
	if (action == MenuAction_End)
	{
		/* Вызывается после VoteEnd */
		CloseHandle(menu);
	}
}
 
public Handle_VoteResults(Handle:menu, 
			num_votes, 
			num_clients, 
			const client_info[][2], 
			num_items, 
			const item_info[][2])
{
	/* Проверяем, нет ли сразу двух победителей */
	new winner = 0;
	if (num_items > 1
	    && (item_info[0][VOTEINFO_ITEM_VOTES] == item_info[1][VOTEINFO_ITEM_VOTES]))
	{
		winner = GetRandomInt(0, 1);
	}
 
	new String:map[64];
	GetMenuItem(menu, item_info[winner][VOTEINFO_ITEM_INDEX], map, sizeof(map));
	ServerCommand("changelevel %s", map);
}
 
DoVoteMenu(const String:map[])
{
	if (IsVoteInProgress())
	{
		return;
	}
 
	new Handle:menu = CreateMenu(Handle_VoteMenu);
	SetVoteResultCallback(menu, Handle_VoteResults);
	SetMenuTitle(menu, "Сменить карту на: %s?", map);
	AddMenuItem(menu, map, "Yes");
	AddMenuItem(menu, "no", "No");
	SetMenuExitButton(menu, false);
	VoteMenuToAll(menu, 20);
}

ExitBack

ExitBack - специальный термин для обозначения "ExitBack Кнопки". По умолчанию эта кнопка отключена. Обычно нумерованные меню не имеют пункта "Назад" для первой страници. Если "ExitBack" кнопка включена, появится кнопка "Назад".

Выбор опции "ExitBack" для меню в обратном вызове будет в действии MenuCancel_ExitBack и MenuEnd_ExitBack. Функциональность этого такая же, как и у простого выхода из меню; дополнительная функциональность должна быть определена через обратный вызов.

Освобождение Дескрипторов (Handle) меню

Важно закрывать дескриптор меню в MenuAction_End. MenuAction_End вызывается когда меню закрылось и больше не нужно.

Переводы

Есть возможность динамически переводить меню для каждого игрока через MenuAction_DisplayItem callback. Специальный натив, RedrawMenuItem, используется чтобы преобразовать текст внутри вызова. Давайте переделаем наш пример голосования с переводами.

public Handle_VoteMenu(Handle:menu, MenuAction:action, param1, param2)
{
	if (action == MenuAction_End)
	{
		/* Вызывается после VoteEnd */
		CloseHandle(menu);
	} else if (action == MenuAction_VoteEnd) {
		/* 0=да, 1=нет */
		if (param1 == 0)
		{
			new String:map[64];
			GetMenuItem(menu, param1, map, sizeof(map));
			ServerCommand("changelevel %s", map);
		}
	} else if (action == MenuAction_DisplayItem) {
		/* Получаем отображаемую строку, будем использовать как фразу перевода */
		decl String:display[64];
		GetMenuItem(menu, param2, "", 0, _, display, sizeof(display));
 
		/* Переводим строку на язык клиента */
		decl String:buffer[255];
		Format(buffer, sizeof(buffer), "%T", display, param1);
 
		/* Перерисовываем текст */
		return RedrawMenuItem(buffer);
	} else if (action == MenuAction_Display) {
		/* Дескриптор панели будет вторым параметром */
		new Handle:panel = Handle:param2;
 
		/* Получаем название карты, на которую меняем, из первого пункта */
		decl String:map[64];
		GetMenuItem(menu, 0, map, sizeof(map));
 
		/* Переводим нашу фразу */
		decl String:buffer[255];
		Format(buffer, sizeof(buffer), "%T", "Change map to?", client, map);
 
		SetPanelTitle(panel, buffer);
	}
}
 
DoVoteMenu(const String:map[])
{
	if (IsVoteInProgress())
	{
		return;
	}
 
	new Handle:menu = CreateMenu(Handle_VoteMenu, MenuAction_DisplayItem|MenuAction_Display);
	SetMenuTitle(menu, "Сменить карту на: %s?", map);
	AddMenuItem(menu, map, "Yes");
	AddMenuItem(menu, "no", "No");
	SetMenuExitButton(menu, false);
	VoteMenuToAll(menu, 20);
}