SourcePawn [SourcePawn] Урок 15 - API (natives, forwards, functions)

Pushistik↯❤

Команда форума
Регистрация
6 Июл 2017
Сообщения
393
Реакции
97
Баллы
28
[SourcePawn] Урок 15 - API (natives, forwards, functions)
<= К содержанию
В этом уроке разберем как создать свой inc файл.

В качестве примера будет 2 плагина:

Сам инклюд назовем base_stats.inc

Из чего состоит inc файл:
  1. Первое что следует сделать - это проверку на повторное подключение инклюда:
    PHP:
    #if defined _base_stats_included    // Если директива  _base_stats_included объявлена
        #endinput // Прекращаем чтение файла (Если компилятор встречает #endinput в файле, то он игнорирует весь ниженаписанный код)
    #endif // Окончание условия
    #define _base_stats_included    // Объявляем директиву _base_stats_included
    На всякий случай поясню:
    Когда мы подключим этот inc в плагин и при его компиляции компилятор встречает условие
    PHP:
    #if defined _base_stats_included
    То оно будет ложным т.к. _base_stats_included еще не объявлена.
    И после условия она будет объявлена:
    PHP:
    #define _base_stats_included
    Если же мы повторно подключим этот inc, либо же он будет подключен в каком-то другом inc (которым мы так же подключим), либо в одном из файлов плагина, то при подключении inc компилятор опять проверит условие:
    PHP:
    #if defined _base_stats_included
    и на этот раз оно будет истинным т.к. _base_stats_included уже была объявлена и повторное подключение будет проигнорировано.
  2. Далее обычно идет объявление директив (констант), переменных, перечислений если они есть. Давайте объявим несколько:
    PHP:
    #define API_VERSION    10 // Версия нашего API (1.0). Сделаем его целым числом для упрощения проверок
    
    // Еще несколько, просто для примера
    #define MY_CONST1 64
    #define MY_CONST2 128
    #define MY_CONST3 256
    
    // Сделаем перечисление для удобного получения данных об игроке (подробнее будет далее)
    enum
    {
        BS_Kills = 0,
        BS_Kills_HS,
        BS_Deaths,
        BS_Hits,
        BS_Hits_HS,
        BS_DatSize
    }
  3. Если у нас объемное API разумно было бы его разбить на части (например, как в shop) и если это так то сейчас самое время подключить наши части:
    PHP:
    #include <myplugin/part1>
    #include <myplugin/part2>
    Т.е. иерархия файлов будет такой:
    • include:
      • base_stats.inc
      • myplugin:
        • part1.inc
        • part2.inc
  4. Думаю суть ясна. Поскольку наш inc не большой то в разбиении его на отдельные файлы нет смысла.
  5. Для упрощения объяснений начнем с более простого - нативов.
    Существует средство позволяющее вызывать функции одного плагина с помощью других.
    Например один плагин создает натив AddNumbers, а другие могут его использовать в своих целях. В этом и есть суть нативов - обмен данными между несколькими плагинами.
    Нативы могут использоваться как для получения информации из плагина создащего натив так и для её передачи в него.

    Давайте решим какие нам нужны будут нативы:
    • Получение указателя (Database) базы данных, для работы непосредственно с ней.
    • Получение уникального ID игрока (По индексу - т.е. когда игрок на сервере)
    • Получение данных об игроке из базы (По индексу - т.е. когда игрок на сервере)
    • Получение позиции в топе по отношению Убийств/Смертей (По уникальному ID)

  6. base_stats.inc:
    PHP:
    /*
    Получение указателя (Database) базы данных, для работы непосредственно с ней.
    */
    native Database BS_GetDatabase();
    /*
    Получение уникального ID игрока из базы (По индексу - т.е. когда игрок на сервере)
    int iClient - Индекс игрока
    */
    native int BS_GetClientID(int iClient);
    /*
    Получение данных об игроке из базы (По индексу - т.е. когда игрок на сервере)
    int iClient - Индекс игрока
    GetDataCallback callback - имя обратного вызова (каллбэка)
    any iData - данные, которые хотим передать в каллбэк
    */
    native void BS_GetClientData(int iClient, GetDataCallback callback, any iData = 0);
    /*
    Получение позиции в топе по отношению Убийств/Смертей (По уникальному ID)
    int iClient - Индекс игрока
    GetTopPosCallback callback - имя обратного вызова (каллбэка)
    any iData - данные, которые хотим передать в каллбэк
    */
    native void BS_GetClientTopPos(int iClient, GetTopPosCallback callback, any iData = 0);
    Сразу же объявим прототипы каллбэков. Что это поясню позже.
    PHP:
    typedef GetDataCallback = function void (int iClientID, int iData[BS_DatSize], any iData);
    
    typedef GetTopPosCallback = function void (int iClientID, int iTopPos, float fScore, any iData);
    Создавать нативы следует в событии AskPluginLoad2.
    Вызывается перед OnPluginStart, непосредственно перед загрузкой плагина.

    base_stats.sp:
    PHP:
    public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] sError, int iErr_max)
    {
        // Для создания натива используем ф-ю CreateNative
        CreateNative("BS_GetDatabase", Native_GetDatabase);
        /*
            BS_GetDatabase - Это имя натива. Именно его будут использовать другие плагины.
            Native_GetDatabase - Ф-я которая будет вызвана в нашем плагине, когда какой-то плагин будет вызывать этот натив.
        */
        CreateNative("BS_GetClientID", Native_GetClientID);
        CreateNative("BS_GetClientData", Native_GetClientData);
        CreateNative("BS_GetClientTopPos", Native_GetClientTopPos);
    
        RegPluginLibrary("base_stats");
        /*
        Ф-я RegPluginLibrary регистрирует имя библиотеки.
        Это нужно чтобы другие плагины, которые для работы требуют этот плагин могли определить загружен ли он.
        */
     
        return APLRes_Success; // Для продолжения загрузки плагина нужно вернуть APLRes_Success
    }
    
    /*
    Получение указателя (Database) базы данных, для работы непосредственно с ней.
    */
    public int Native_GetDatabase(Handle hPlugin, int iNumParams)
    {
        /*
        Когда какой-то плагин будет вызывать натив BS_GetDatabase то в нашем плагине будет вызываться эта ф-я.
        Обратите внимание что она имеет тип int возвращать должна значение типа int.
        Дальше у нас 2 варианта:
            1) Вернуть указатель на соединение с бд, которое мы используем.
            2) Создать копию соединения и вернуть её. В этом случае плагин после завершения работы с базой должен будет закрыть соеденение.
        Пойдем по 2-му пути.
        Убеждаемся что тип Database можно клонировать: https://wiki.alliedmods.net/Handles_(SourceMod_Scripting)#Databases
        Cloneable:    Yes - Значит можно
        Клонируем с помощью ф-и CloneHandle.
        1-й аргумент - собственно указатель, который нужно клонировать
        2-й аргумент является опциональным. Если он указан то он будет назначен новым владельцем копии.
     
        Т.к. Возвращаемый тип должен быть int то приводим результат к типу int
        Таким образом плагин вызвавший натив BS_GetDatabase получит копию нашего соеденения с базой g_hDatabase
        */
        return view_as<int>(CloneHandle(g_hDatabase, hPlugin));
    }
    
    /*
    Получение уникального ID игрока из базы (По индексу - т.е. когда игрок на сервере)
    int iClient - Индекс игрока
    */
    public int Native_GetClientID(Handle hPlugin, int iNumParams)
    {
        /*
            hPlugin - Указатель на плагин, который вызвал натив.
            iNumParams - Количество переданных аргументов.
         
            Исходя из прототипа:
            native int BS_GetClientID(int iClient);
            В натив передается 1 аргумент.
            Нумерация аргументов начинается с 1
         
            Для получения разных значений разных типов используются разные функции:
                int GetNativeArray(int param, any[] local, int size)    - Получение массива
                any GetNativeCell(int param)                            - Получение ячейки (обычно int, bool, float)
                any GetNativeCellRef(int param)                            - Получение ячейки при передаче по адресу (обычно int, bool, float)
                function GetNativeFunction(int param)                    - Получение адреса функции (каллбека)
                int GetNativeString(int param, char[] buffer, int maxlength, int &bytes)    - Получение строки
                int GetNativeStringLength(int param, int &length)        - Получение длины передаваемой строки
         
            Во всех ф-ях: int param это номер аргумента
         
            У нас тип int, поэтому используем GetNativeCell
        */
        int iClient = GetNativeCell(1);
        /*
        Тут мы имеем индекс игрока.
        Дальше опять 2 варианта:
            1) Проверить валиден ли он (в адекватных ли пределах, есть ли он на сервере, не бот ли)
            2) Переложить эти проверки на плагин, использующий натив и надеятся что всё будет хорошо.
        Для надежности пойдем по 1-му пути.
        */
        if(iClient > 0 && iClient <= MaxClients && IsClientInGame(iClient) && !IsClientInGame(iClient) && g_iClientID[iClient])
        {
            // Игрок валиден
            return g_iClientID[iClient]; // Вернем его ID
        }
    
        // Во всех остальных случаях будет возвращен 0
        return 0;
    }
  7. Теперь мы подошли к функциям.
    Каждая ф-я в плагине имеет имя, адрес, список параметров и их типов. Например ф-я имеет имя OnPluginStart, а её адрес 0x4f0a0019. При каждом запуске плагина эта цифра разная.
    Внутри самого SourceMod вызов ф-й происходит по адресу. Но адрес можно получить по имени с помощью ф-и GetFunctionByName.
    Если же мы передаем ф-ю в другие ф-и или нативы как аргумент то на самом деле мы передаем её адрес.
    Таким образом зная имя ф-и и список параметров, а так же их типов мы можем вызвать любую ф-ю внутри любого плагина с помощью другого плагина.
    Чтобы вызвать ф-ю нам нужно знать Handle плагина, в котором её необходимо вызвать и её адрес либо имя.
    base_stats.sp:
    PHP:
    /*
    Получение данных об игроке из базы (По индексу - т.е. когда игрок на сервере)
    int iClient - Индекс игрока
    GetDataCallback callback - имя обратного вызова (каллбэка)
    any iData - данные, которые хотим передать в каллбэк
    */
    public int Native_GetClientData(Handle hPlugin, int iNumParams)
    {
        // Получаем клиент
        int iClient = GetNativeCell(1);
        // Проверяем валидность клиента
        if(iClient > 0 && iClient <= MaxClients && IsClientInGame(iClient) && !IsClientInGame(iClient) && g_iClientID[iClient])
        {
            // Получаем данные, которые будут переданы в каллбэк
            int iData = GetNativeCell(3);
    
            // Получаем адрес каллбека
            Function fncCallback = GetNativeFunction(2);
         
            /*
            Далее нам нужно выполнить запрос на выборку и отправить результат в каллбек.
            В каллбек запроса нужно передать iData, Handle плагина вызвавшего натив и  fncCallback. Для этого будем использовать DataPack
            */
    
            DataPack hPack = new DataPack(); // Создаем DataPack
            hPack.WriteCell(hPlugin); // Записываем в него Handle плагина вызвавшего натив
            hPack.WriteFunction(fncCallback); // Записываем в него адрес каллбека
            hPack.WriteCell(iData); // Записываем в него данные, которые будут переданы в каллбэк
         
            char szQuery[256];
            /*
            Здесь вспоминаем наш список:
            enum StatsData
            {
                BS_Kills = 0,
                BS_Kills_HS,
                BS_Deaths,
                BS_Hits,
                BS_Hits_HS
            }
         
            Для упрощения получения данных выбирать значения из базы будем в том же порядке: убийств, убийств в голову, смертей, попаданий, попаданий в голову
    
            */
            FormatEx(szQuery, sizeof(szQuery), "SELECT `id`, `kills`, `kills_hs`, `deaths`, `hits`, `hits_hs` FROM `table_stats` WHERE `id` = %d;", g_iClientID[iClient]);    // Формируем запрос
            g_hDatabase.Query(SQL_Callback_NtvGetClientData, szQuery, hPack); // Отправляем запрос
        }
    
        return 0;
    }
    
    // Пришел ответ на запрос
    public void SQL_Callback_NtvGetClientData(Database hDatabase, DBResultSet hResults, const char[] sError, any iDataPack)
    {
        if(sError[0]) // Если произошла ошибка
        {
            LogError("SQL_Callback_NtvGetClientData: %s", sError); // Выводим в лог
            return; // Прекращаем выполнение ф-и
        }
     
        DataPack hPack = view_as<DataPack>(iDataPack); // Получаем наш DataPack
        hPack.Reset(); // Переводим указатель на начало датапака
     
        Handle hPlugin = view_as<Handle>(hPack.ReadCell());
        Function fncCallback = hPack.ReadFunction();
        int iData = hPack.ReadCell();
     
        delete hPack;
    
        int iClientID, iStatsData[BS_DatSize]; // Создаем переменные. По умолчанию они будут инициализированы нолями.
    
        if(hResults.FetchRow())    // Игрок есть в базе
        {
            // Получаем значения из результата
            iClientID = hResults.FetchInt(0);    // id
         
            for(int i = 1, j = 0; i < 6; ++i)
            {
                iStatsData[j++] = hResults.FetchInt(i);
            }
        }
     
        /*
        Вот здесь и происходит вызов ф-и
        Нужно указать Handle плагина и адрес ф-и
        */
        Call_StartFunction(hPlugin, fncCallback);
        /*
        Теперь нужно передать необходимые параметры в определенном порядке.
        Мы объявляли прототип обратного вызова
        typedef GetDataCallback = function void (int iClientID, int iStatsData[BS_DatSize], any iData);
        Это как раз и есть вид ф-и которая будет вызвана.
        */
        // Передаем int iClientID
        Call_PushCell(iClientID);
        // Передаем int iStatsData[BS_DatSize]
        Call_PushArray(iStatsData, 5);
        // Передаем any iData
        Call_PushCell(iData);
        // И собственно вызываем ф-ю
        Call_Finish();
     
        // Теперь в плагине, который вызвал натив была вызвана указанная ф-я
    }
    
    /*
    Получение позиции в топе по отношению Убийств/Смертей (По уникальному ID)
    int iClientID - Уникальный ID игрока
    GetTopPosCallback callback - имя обратного вызова (каллбэка)
    any iData - данные, которые хотим передать в каллбэк
    */
    public int Native_GetClientTopPos(Handle hPlugin, int iNumParams)
    {
        // Получаем клиент
        int iClient = GetNativeCell(1);
        // Проверяем валидность клиента
        if(iClient > 0 && iClient <= MaxClients && IsClientInGame(iClient) && !IsClientInGame(iClient) && g_iClientID[iClient])
        {
            // Получаем данные, которые будут переданы в каллбэк
            int iData = GetNativeCell(3);
    
            // Получаем адрес каллбека
            Function fncCallback = GetNativeFunction(2);
    
            DataPack hPack = new DataPack(); // Создаем DataPack
            hPack.WriteCell(hPlugin); // Записываем в него Handle плагина вызвавшего натив
            hPack.WriteFunction(fncCallback); // Записываем в него адрес каллбека
            hPack.WriteCell(iData); // Записываем в него данные, которые будут переданы в каллбэк
         
            char szQuery[256];
            FormatEx(szQuery, sizeof(szQuery), "SELECT DISTINCT ((`kills` + `kills_hs`) / `deaths`) as `score`, `id` FROM `table_stats` WHERE ((`kills` + `kills_hs`) / `deaths`) >= %.2f ORDER BY `score` ASC;", (float(g_iKills[iClient])+float(g_iKills_hs[iClient]))/float(g_iDeaths[iClient]));    // Формируем запрос
            g_hDatabase.Query(SQL_Callback_NtvGetClientTopPos, szQuery, hPack); // Отправляем запрос
        }
    
        return 0;
    }
    
    // Пришел ответ на запрос
    public void SQL_Callback_NtvGetClientTopPos(Database hDatabase, DBResultSet hResults, const char[] sError, any iDataPack)
    {
        if(sError[0]) // Если произошла ошибка
        {
            LogError("SQL_Callback_NtvGetClientTopPos: %s", sError); // Выводим в лог
            return; // Прекращаем выполнение ф-и
        }
     
        DataPack hPack = view_as<DataPack>(iDataPack); // Получаем наш DataPack
        hPack.Reset(); // Переводим указатель на начало датапака
     
        Handle hPlugin = view_as<Handle>(hPack.ReadCell());
        Function fncCallback = hPack.ReadFunction();
        int iData = hPack.ReadCell();
     
        delete hPack;
    
        int iPos = hResults.RowCount, iClientID = 0;
        float fScore = 0.0;
     
        if(hResults.FetchRow())    // Игрок есть в базе
        {
            // Получаем значения из результата
            fScore = hResults.FetchFloat(0);    // id
            iClientID = hResults.FetchInt(1);    // id
        }
    
        Call_StartFunction(hPlugin, fncCallback);
        /*
        Теперь нужно передать необходимые параметры в определенном порядке.
        Мы объявляли прототип обратного вызова
        typedef GetTopPosCallback = function void (int iClientID, int iTopPos, float fScore, any iData);
        Это как раз и есть вид ф-и которая будет вызвана.
        */
        Call_PushCell(iClientID);
        Call_PushCell(iPos);
        Call_PushFloat(fScore);
        Call_PushCell(iData);
        Call_Finish();
    }
    Попробую описать последовательность вызовов. Имеем 2 плагина:
    • Плагин предоставляющий API (base_stats.sp)
    • Плагин использующий API первого (api_example.sp)
  8. PHP:
    #pragma semicolon 1
    #include <sourcemod>
    #include <base_stats>
    #pragma newdecls required
    
    void GetPlayerTopPos(int iClient)
    {
        BS_GetClientTopPos(iClient, Example_PlayerTopPos, GetClientUserId(iClient));
    }
    
    public void Example_PlayerTopPos(int iClientID, int iTopPos, float fScore, any iData);
    {
        if(iClientID)
        {
            int iClient = GetClientOfUserId(iData);
            if(iClient)
            {
                /*
             
             
                */
            }
        }
    }
    • В какой-то момент работы api_example.sp нам потребовалось получить позицию игрока в топе.
      Мы вызовем ф-ю GetPlayerTopPos либо напрямую BS_GetClientTopPos
    • В base_stats.sp вызывается Native_GetClientTopPos и выполняется запрос в базу.
    • Когда приходит ответ от базы в base_stats.sp вызывается SQL_Callback_NtvGetClientTopPos.
    • Из результата запроса получаются нужные данные и передаются в ф-ю Example_PlayerTopPos с её последующим вызовом в api_example.sp
  9. Теперь можно перейти к форвардам. По своей сути форвады это те же ф-и но с некоторыми отличиями.
    Например, мы хотим чтобы после подключения игрока и загрузки его данных из базы вызывалася какая-то ф-я, уведомляющая о том что игрок успешно загружен. Для этой цели идеально подходит форвард.

    Форвады бывают 2-х видов:
    • Глобальные - имеют постоянное имя и вызываются во всех плагинах, где используются
    • Частные - вызываются не во всех плагинах, а только в заранее указанных и могут иметь разные имена
  10. По своей сути форвады это те же ф-и но вызываются сразу во всех или заранее заданных плагинах и имеют постоянное имя.

    Алгоритм вызова форвардов примерно такой:
    PHP:
    for(...) // Цикл по всем плагинам
    {
        if(...) // Поиск адреса ф-и по имени. Т.е. мы проверяем отслеживает ли плагин этот форвард
        {
            // Вызов форварда
        }
    }
    base_stats.inc:
    PHP:
    forward void BS_OnClientLoaded(int iClient);
    forward void BS_OnClientRankChanged(int iClient, int iOldRank, int iNewRank);
    base_stats.sp:
    PHP:
    Handle g_hGFwd_OnClientLoaded; // Указатель для вызова глобального форварда
    Handle g_hPFwd_OnClientRankChanged; // Указатель для вызова частного форварда
    
    // Создавать форварды можно как в AskPluginLoad2 так и в OnPluginStart, хотя можно и в других событиях которые вызываются единожды (либо блокировать повторное создание)
    
    public APLRes AskPluginLoad2(Handle hMyself, bool bLate, char[] sError, int iErr_max)
    {
        // ...
        // Здесь все нативы что мы создавали ранее
    
        CreateNative("BS_HookPlayerRankChange", Native_HookPlayerRankChange);
    
        g_hGFwd_OnClientLoaded = CreateGlobalForward("BS_OnClientLoaded", ET_Ignore, Param_Cell);
        g_hPFwd_OnClientRankChanged = CreateForward(ET_Ignore, Param_Cell, Param_Cell, Param_Cell);
        /*
        Параметры у CreateGlobalForward:
        1-м параметром указывается имя форварда. Т.е. это имя ф-и которая будет вызвана
        2-й параметр дает SourceMod понять как интерпретировать возвращаемое значение.
    
        Существует четыре предопределенных метода:
    
        ET_Ignore - Все возвращаемые значения будут игнорироваться; 0 будет возвращено в конце.
        ET_Single - Возвращается только последнее возвращаемое значение.
        ET_Event - Функция должна возвращать значение Action (core.inc). Plugin_Stop действует как Plugin_Handled. Возвращается самое высокое значение.
        ET_Hook - Функция должна вернуть значение Action. Plugin_Stop немедленно завершает прямой вызов.
     
        Нам нужно всего лишь уведомить другие плагины о некоторых событиях не обращая внимания на возвращенный результат.
        Поэтому используем ET_Ignore
     
        3-й и далее определяет количество параметров и их тип.
        Может быть до 32-х параметров.
     
        Тип параметра:
    
        Param_Any - Любой тип параметра
        Param_Cell - Целое число (bool, int, Handle и его прозводные, Function и другие)
        Param_Float - Число с плавающей точкой
        Param_String - Строка
        Param_Array - Массив
        Param_VarArgs - Параметр может быть любого типа, но будет передаваться по ссылке. Это не может быть первый тип параметра, и если он используется, он должен быть последним типом параметра.
        Param_CellByRef - Тоже что и Param_Cell но с передачей по ссылке, а не по значению (будет изменятся исходное значение, а не копия)
        Param_FloatByRef - Тоже что и Param_Float но с передачей по ссылке
    
        Строки и массивы являются неявными ссылками.
     
        У CreateForward всё точно так же за исключением 1-го параметра - имени форварда
        */
    
        RegPluginLibrary("base_stats");
    
        return APLRes_Success;
    }
    
    public int Native_HookPlayerRankChange(Handle hPlugin, int iNumParams)
    {
        /*
        Этот натив нужен для добавления плагина в список тех плагинов, в которых будет вызываться форвард, а так же указывается адрес ф-и которая будет вызывана в качестве форварда.
        */
        AddToForward(g_hPFwd_OnClientRankChanged, hPlugin, GetNativeFunction(1));
    }
    
    // Для удобства создадим несколько вспомогательных ф-й
    void FwdClientLoaded(int iClient)
    {
        //    Начинаем создавать вызов форварда
        Call_StartForward(g_hGFwd_OnClientLoaded); // Передаем Handle форварда
    
        //    Теперь передаем параметры. У нас он 1
        Call_PushCell(iClient);
    
        // Завершаем вызов форварда
        Call_Finish();
    }
    
    void FwdClientRankChanged(int iClient, int iOldRank, int iNewRank)
    {
        //    Начинаем создавать вызов форварда
        Call_StartForward(g_hPFwd_OnClientRankChanged); // Передаем Handle форварда
    
        //    Теперь передаем параметры.
        Call_PushCell(iClient);
        Call_PushCell(iOldRank);
        Call_PushCell(iNewRank);
    
        // Завершаем вызов форварда
        Call_Finish();
    }
    
    // Теперь нам остается только вызывать вспомогательные функции там где это нужно.
    // Например: FwdClientLoaded(iClient);
    С частными форвардами немного сложнее.
    Сначала с помощью созданного нами натива BS_HookPlayerRankChange необходимо добавить форвард.

    api_example.inc:
    PHP:
    public void OnPluginStart()
    {
        // Добавляем наш форвард
        BS_HookPlayerRankChange(OnClientRankChanged);
    }
    
    /*
    Теперь при вызове FwdClientRankChanged в base_stats.sp будет вызываться OnClientRankChanged этом плагине
    */
    public void OnClientRankChanged(int iClient, int iOldRank, int iNewRank)
    {
        // Ранк игрока изменился
    }
<= К содержанию

Во вложениях прилагается zip файл с финальные версии плагина, предоставляющего API и inc файла.
 

Вложения

Последнее редактирование:
Сверху Снизу