Для разработчиков
Условные соглашения
-
Если модуль имеет конфиг, он должен лежать в
addons/sourcemod/data/vip/modules/
-
Если модуль имеет квары, то конфиг должен создаваться в
cfg/vip
:
AutoExecConfig(true, "Имя модуля", "vip");
- VIP предоставляет доступ к хэш-карте (adt_trie -> StringMap) с настройками VIP-игрока. Эта хэш-карта хранит в себе функции к которым имеет доступ игрок, а так же некоторую служебную информацию (VIP-группы, сроки и т.д.). Вы можете использовать её в своих целях. Например, вместо создания глобальных пременных для хранения выбранного игроком цвета, предмета и т.д. данные можно записать в хэш-карту и они будут хранится до тех пор пока игрок на сервере.
Для избежания конфликтов все ключи используемые ядром имеют вид Core->Ключ
.
Следовательно если модуль хочет сохранить какую-то информацию в хэш-карту, он должен делать это по ключу который должен иметь вид Имя_модуля->Ключ
.
Примеры:
Skins->Model
- путь к скину игрока (.mdl
)Skins->ArmsModel
- путь к скину рук игрока (.mdl
)Tracers->Color
- цвет трассеров игрокаTrails->Material
- индекс кешированной модели трейла игрока-
Trails->Width
- ширину трейла игрока -
Источник VIP-статуса.
В старых версиях ядра была такая вещь, как тип авторизации
. Она показывала каким образом игрок получил VIP-статус (SteamID, IP-адрес, Ник, Админ-флаги, Админ-группа).
В 3.0 она была удалена. Поэтому для возможности определения происхождения VIP-статуса игрока предлагается после выдачи записывать в хэш-карту игрока следующее:
- Ключ:
"Core->Owner"
-
Значение:
"Core"
или отсутствует - Выдано ядром (обычный VIP-игрок загруженный из базы)."AdminFlag"
- Выдано модулем Admin по Админ-флагам."AdminGroup"
- Выдано модулем Admin по Админ-группе."Test"
- Выдано модулем Test....
- Используйте свои значения.
Таким образом можно будет легко узнать каким образом игрок получил VIP-статус и разрешить конфликтные ситуации.
Привязка плагинов к VIP
Зачем это нужно? Ну например добавить VIP-игроку иммунитет от чего-то. Либо сделать модуль из какого-то плагина.
Подключаем библиотеку:
#include <vip_core>
Далее решаем хотим мы это сделать для всех VIP-игроков или только для тех, кому выдан доступ через группу.
Если для всех то достаточно добавить в нужных местах проверку:
if(VIP_IsClientVIP(iClient))
{
// Это VIP-игрок
}
Если же только по доступу то:
static const char g_sFeature[] = "MyFunc"; // Уникальное имя ф-и
public void VIP_OnVIPLoaded()
{
VIP_RegisterFeature(g_sFeature, BOOL, HIDE);
// Если пишем BOOL то в параметрах 1/0
// HIDE значит что ф-я скрыта, она не будет показана у игрока в меню, её нельзя отключить и не нужно добавлять в перевод.
// Если HIDE не писать то ф-ю можно будет вкл/выкл и нужно добавить фразу в перевод.
}
А дальше просто в нужном месте проверяем:
if(VIP_IsClientVIP(iClient) && VIP_IsClientFeatureUse(iClient, g_sFeature))
{
// Это VIP-игрок и он имеет доступ к ф-и.
}
Начиная с версии ядра 3.0
делать VIP_IsClientVIP(iClient)
перед VIP_IsClientFeatureUse(iClient, g_sFeature)
стало не обязательно.
Написание модулей
Для написания модуля нам понадобится всё что нужно для написания любого другого плагина + библиотека випа vip_core.inc.
Как пример будем писать модуль телепорта к игроку или куда смотрит прицел.
Подключаем необходимые библиотеки:
#pragma semicolon 1
#include <sourcemod>
#include <sdktools>
#include <vip_core>
Первым делом нужно зарегистрировать функцию:
native VIP_RegisterFeature(
const String szFeature[], // Уникальное имя ф-и
VIP_ValueType ValType = VIP_NULL, // Тип данных ф-и
VIP_FeatureType FeatureType = TOGGLABLE, // Тип ф-и
ItemSelectCallback Item_select_callback = INVALID_FUNCTION, // Каллбэк нажатия на пункт в меню
ItemDisplayCallback Item_display_callback = INVALID_FUNCTION, // Каллбэк отображения текста пункта меню
ItemDrawCallback Item_draw_callback = INVALID_FUNCTION, // Каллбэк отображения стиля пункта меню
VIP_ToggleState eDefStatus = NO_ACCESS // Значение ф-и по-умолчанию
);
У нас есть 5 типов данных VIP-функций:
VIP_NULL
- Нет данныхINT
- Целое числоFLOAT
- Число с точкой (дробное)BOOL
- 1/0 (true/false, истина/ложь)STRING
- Строка
Поскольку в данном модуле мне нужно только разрешить/запретить использование логично было бы использовать BOOL
, но поскольку я хочу позволить ограничивать количество телепортов за раунд - буду использовать INT
, чтобы указывать количество раз.
Типы VIP-функций:
TOGGLABLE
- В VIP-меню можно будет включать/выключать ф-юSELECTABLE
- В VIP-меню можно будет только нажимать на пунктHIDE
- Скрытый. Ф-я не добавляется в VIP-меню
Поскольку телепорт не имеет смысла включать выключать, а нужно при нажатии в VIP-меню показывать другое меню - буду использовать тип `SELECTABLE`.
Изначально я предполагаю что если Teleport
указано:
0
- нет доступа> 0
- кол-во использований за раунд.-1
- безлимит
new g_iClientTeleports[MAXPLAYERS+1]; // Массив переменных для подсчета количества телепортаций
static const char g_sFeature[] = "Teleport"; // Создаем константу, которая будет уникальным именем нашей функции.
public void VIP_OnVIPLoaded() // Событие когда ядро VIP-плагина загрузилось и готово к работе.
{
VIP_RegisterFeature(g_sFeature, INT, SELECTABLE, OnSelectItem, OnDisplayItem, OnDrawItem); // Регистрируем
/*
OnSelectItem - Функция будет вызыватся при нажатии на пункт
OnDisplayItem - Функция будет вызыватся при отображении текста пункта
OnDrawItem - Функция будет вызыватся при отображении стиля пункта
*/
}
public OnPluginStart() // Плагин запустился
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy); // Ловим начало раунд для обнуления кол-ва телепортаций
LoadTranslations("vip_modules.phrases"); // Подключаем файл перевода модулей
if(VIP_IsVIPLoaded()) // Если ядро VIP уже загружено
{
VIP_OnVIPLoaded(); // Имитируем вызов форварда
}
}
public OnPluginEnd() // Плагин остановлен
{
if(CanTestFeatures() && GetFeatureStatus(FeatureType_Native, "VIP_UnregisterFeature") == FeatureStatus_Available) // Если ядро VIP еще запущено
{
VIP_UnregisterFeature(g_sFeature); // Удаляем ф-ю
}
}
Нужно чтобы при нажатии на пункт открывалось другое меню.
Помним что имя ф-и при нажатии - OnSelectItem
.
Исходя из прототипа:
typeset ItemSelectCallback
{
// Используется когда тип ф-и TOGGLABLE
function Action (int iClient, const char[] szFeature, VIP_ToggleState eOldStatus, VIP_ToggleState &eNewStatus);
// Используется когда тип ф-и SELECTABLE
function bool (int iClient, const char[] szFeature);
};
Получаем:
public bool OnSelectItem(int iClient, const char[] szFeature)
{
DisplayTeleportMenu(iClient); // Отправляем игроку наше меню
// Если вернуть true игроку снова откроется VIP-меню, нам это не нужно.
return false;
}
void DisplayTeleportMenu(int iClient)
{
// Создаем меню
Menu hMenu = new Menu(MenuHandler_TeleportMenu);
hMenu.SetTitle("Телепорт\n ");
hMenu.ExitBackButton = true;
hMenu.AddItem("", "На позицию прицела\n "); // Тут думаю всё ясно
hMenu.AddItem("", "К игроку под прицелом\n ");
// Добавляем игроков
char szUserID[16], String:szName[MAX_NAME_LENGTH];
for (int i = 1; i <= MaxClients; ++i)
{
if (i != iClient && IsClientInGame(i) && IsPlayerAlive(i) && GetClientName(i, szName, sizeof(szName)))
{
IntToString(GetClientUserId(i), szUserID, sizeof(szUserID));
hMenu.AddItem(szUserID, szName);
}
}
hMenu.Display(iClient, MENU_TIME_FOREVER); // Отправляем меню
}
public int MenuHandler_TeleportMenu(Menu hMenu, MenuAction eAction, int iClient, int Item)
{
switch(eAction)
{
case MenuAction_End: // Меню было завершено
{
delete hMenu; // Удаляем Handle меню
}
case MenuAction_Cancel: // Меню было закрыто
{
if(Item == MenuCancel_ExitBack) // Причина - игрок нажал "Назад"
{
// Открываем ему обратно VIP-меню
VIP_SendClientVIPMenu(iClient);
}
}
case MenuAction_Select: // Игрок нажал на пукнт меню
{
switch(Item)
{
case 0: // Игрок выбрал 1-й пункт
{
float fPos[3];
GetAimPos(iClient, fPos);
TeleportEntity(iClient, fPos, NULL_VECTOR, NULL_VECTOR);
++g_iClientTeleports[iClient];
}
case 1: // Игрок выбрал 2-й пункт
{
int iTarget = GetClientAimTarget(iClient, true);
if (iTarget > 0)
{
float fPos[3];
GetClientAbsOrigin(iTarget, fPos);
TeleportEntity(iClient, fPos, NULL_VECTOR, NULL_VECTOR);
++g_iClientTeleports[iClient];
}
else
{
VIP_PrintToChatClient(iClient, "Наведите прицел на игрока!");
}
}
default: // Игрок выбрал другого игрока
{
char szUserID[16];
hMenu.GetItem(Item, sUserID, sizeof(sUserID));
int iTarget = GetClientOfUserId(StringToInt(sUserID));
if(iTarget != 0 && IsPlayerAlive(iTarget)) // Цель еще на сервера и жива
{
float fPos[3];
GetClientAbsOrigin(iTarget, fPos);
TeleportEntity(iClient, fPos, NULL_VECTOR, NULL_VECTOR);
++g_iClientTeleports[iClient];
}
else
{
VIP_PrintToChatClient(iClient, "Игрок больше недоступен!");
}
}
}
DisplayTeleportMenu(iClient);
}
}
return 0;
}
// Ф-я получения координат, куда смотрит игрок
void GetAimPos(int iClient, float fPos[3])
{
float fAngles[3], fDirection[3];
GetClientEyeAngles(iClient, fAngles);
GetClientEyePosition(iClient, fPos);
TR_TraceRayFilter(fPos, fAngles, MASK_SOLID, RayType_Infinite, FilterGetAim, iClient);
TR_GetEndPosition(fPos);
GetVectorAngles(fPos, fAngles);
fAngles[0] = fAngles[2] = 0.0;
fAngles[1] += 180.0;
GetAngleVectors(fAngles, fDirection, NULL_VECTOR, NULL_VECTOR);
fPos[0] = fPos[0] + fDirection[0] * 30.0;
fPos[1] = fPos[1] + fDirection[1] * 30.0;
fPos[2] += 15.0;
}
public bool FilterGetAim(int iTraceEnt, int iMask, any iEntity)
{
return iTraceEnt != iEntity;
}
Дальше я хочу чтобы отображении пункта показывалось сколько телепортаций осталось. Смотрим прототип функции:
typedef ItemDisplayCallback = function bool (int iClient, const char[] szFeature, char[] szDisplay, int iMaxLength);
Помним что ф-я отображения текста пункта называлась OnDisplayItem
public bool OnDisplayItem(int iClient, const char[] szFeature, char[] szDisplay, int iMaxLength)
{
if(VIP_IsClientFeatureUse(iClient, szFeature)) // Проверяем что функция включена у игрока
{
int iTeleports = VIP_GetClientFeatureInt(iClient, szFeature);
// VIP_GetClientFeatureInt(iClient, szFeature) - Получит количество доступных телепортов (То что в конфиге указано в "Teleport")
if(iTeleports > 0) // Если кол-во телепортов ограничено
{
// Выводим кол-во оставшихся телепортов.
FormatEx(szDisplay, iMaxLength, "%T [Осталось: %d]", szFeature, iClient, VIP_GetClientFeatureInt(iClient, szFeature)-g_iClientTeleports[iClient]);
// Если вернуть true то будет выводиться то что в sDisplay.
return true;
}
}
// А во всех остальных случаях нужно выводить без изменений
return false;
}
Теперь я хочу чтобы когда у игрока не осталось доступных телепортов то пункт становился не активным (белым, его нельзя будет нажать) Согласно прототипу:
typedef ItemDrawCallback = function int (int iClient, const char[] szFeature, int iStyle);
Получаем:
public int OnDrawItem(int iClient, const char[] szFeature, int iStyle)
{
if(VIP_IsClientFeatureUse(iClient, szFeature)) // Проверяем у игрока есть доступ
{
int iTeleports = VIP_GetClientFeatureInt(iClient, szFeature);
if(iTeleports > 0 && iTeleports == g_iClientTeleports[iClient]) // Если кол-во телепортов ограничено и достигнут лимит
{
return ITEMDRAW_DISABLED;
}
}
return iStyle;
}
Далее мелочи.
Очищаем переменную при выходе игрока:
public void OnClientDisconnect(int iClient)
{
g_iClientTeleports[iClient] = 0;
}
Обнуляем кол-во телепортаций в начале раунда:
public void Event_RoundStart(Event hEvent, const char[] szEventName, bool bDontBroadcast)
{
for (int i = 1; i <= MaxClients; ++i)
{
g_iClientTeleports[i] = 0;
}
}