Для разработчиков

Условные соглашения

  1. Если модуль имеет конфиг, он должен лежать в addons/sourcemod/data/vip/modules/

  2. Если модуль имеет квары, то конфиг должен создаваться в cfg/vip:

AutoExecConfig(true, "Имя модуля", "vip");
  1. 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;
    }
}