Pushistik↯❤
Команда форума
- Регистрация
- 6 Июл 2017
- Сообщения
- 393
- Реакции
- 97
- Баллы
- 28
[SourcePawn] Урок 15 - API (natives, forwards, functions)
<= К содержанию
В этом уроке разберем как создать свой inc файл.В качестве примера будет 2 плагина:
- Плагин предоставляющий API (Им будет наш плагин статистики base_stats.sp из урока
[SourcePawn] Урок 13 - Работа с базами данных (MySQL, SQLite)) - Плагин использующий API первого (назовем его api_example.sp)
Сам инклюд назовем base_stats.inc
Из чего состоит inc файл:
- Первое что следует сделать - это проверку на повторное подключение инклюда:
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
И после условия она будет объявлена:PHP:#define _base_stats_included
PHP:#if defined _base_stats_included
- Далее обычно идет объявление директив (констант), переменных, перечислений если они есть. Давайте объявим несколько:
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 }
- Если у нас объемное API разумно было бы его разбить на части (например, как в shop) и если это так то сейчас самое время подключить наши части:
PHP:#include <myplugin/part1> #include <myplugin/part2>
- include:
- base_stats.inc
- myplugin:
- part1.inc
- part2.inc
- include:
- Думаю суть ясна. Поскольку наш inc не большой то в разбиении его на отдельные файлы нет смысла.
- Для упрощения объяснений начнем с более простого - нативов.
Существует средство позволяющее вызывать функции одного плагина с помощью других.
Например один плагин создает натив AddNumbers, а другие могут его использовать в своих целях. В этом и есть суть нативов - обмен данными между несколькими плагинами.
Нативы могут использоваться как для получения информации из плагина создащего натив так и для её передачи в него.
Давайте решим какие нам нужны будут нативы:- Получение указателя (Database) базы данных, для работы непосредственно с ней.
- Получение уникального ID игрока (По индексу - т.е. когда игрок на сервере)
- Получение данных об игроке из базы (По индексу - т.е. когда игрок на сервере)
- Получение позиции в топе по отношению Убийств/Смертей (По уникальному ID)
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);
Вызывается перед 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; }
- Теперь мы подошли к функциям.
Каждая ф-я в плагине имеет имя, адрес, список параметров и их типов. Например ф-я имеет имя 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(); }
- Плагин предоставляющий API (base_stats.sp)
- Плагин использующий API первого (api_example.sp)
-
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
- В какой-то момент работы api_example.sp нам потребовалось получить позицию игрока в топе.
- Теперь можно перейти к форвардам. По своей сути форвады это те же ф-и но с некоторыми отличиями.
Например, мы хотим чтобы после подключения игрока и загрузки его данных из базы вызывалася какая-то ф-я, уведомляющая о том что игрок успешно загружен. Для этой цели идеально подходит форвард.
Форвады бывают 2-х видов:- Глобальные - имеют постоянное имя и вызываются во всех плагинах, где используются
- Частные - вызываются не во всех плагинах, а только в заранее указанных и могут иметь разные имена
- По своей сути форвады это те же ф-и но вызываются сразу во всех или заранее заданных плагинах и имеют постоянное имя.
Алгоритм вызова форвардов примерно такой:
PHP:for(...) // Цикл по всем плагинам { if(...) // Поиск адреса ф-и по имени. Т.е. мы проверяем отслеживает ли плагин этот форвард { // Вызов форварда } }
PHP:forward void BS_OnClientLoaded(int iClient); forward void BS_OnClientRankChanged(int iClient, int iOldRank, int iNewRank);
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 файла.
Вложения
-
7 KB Просмотры: 13
Последнее редактирование: