Эффективная реализация API инклуда с возможностью вызова функций из фильтрскриптов
В этом уроке я расскажу, как умно сделать функционал (API) вашего инклуда, чтобы пользователи могли подключать инклуд к фильтрскриптам/моду и вызывать его внешние функции как из фильтрскриптов (но сами функции обрабатывались бы лишь в инклуде, подключенному к моду), так и из мода, но в этом случае с такой скоростью, как если бы они вызывались без поддержки их вызова из фильтрскриптов.
В общем-то, вся суть алгоритма уже разложена в первом абзаце и при желании понимающим людям можно сразу переходить к последнему примеру реализации, а для всех остальных начну с самого начала.
Прежде всего стоит прояснить, зачем вообще инклуду иметь API и для чего всё это нужно. API (внешние функции) некой системы в инклуде представляют собой функции, которые делаются, чтобы их могли использовать за пределами этого инклуда (в моде, фильтрскриптах, других инклудах). Простой пример - mxINI, easyDialogs/mdialog и т.д. Все эти инклуды имеют внутри себя функции, например ini_openFile или Dialog_Close, созданные специально для их использования извне. Таким образом, мы имеем движок нашего алгоритма (реализацию некой системы, её части) в отдельном файле, а из мода или других инклудов уже просто вызываем готовые функции в одну строчку. Либо же мы имеем в инклуде независимую систему, а функции нужны другим скриптам для доступа к её переменным (их узнавания/изменения) или любым другим данным. В случае с доступом к переменным можно, конечно, и не делать отдельных функций, а изменять эти переменные вне инклуда напрямую, но так обычно не принято по ряду причин и потому выделяемую память держат в зоне видимости лишь файла инклуда, давая к ней доступ другим скриптам через API функции.
Хорошо. С тем, что такое и зачем нужно API нашему инклуду (если оно вам действительно нужно) разобрались. Теперь рассмотрим то, как на практике оно реализуется в большинстве случаев. Самый простой пример - в нашем инклуде создаётся stock функция -> в ней нужный нам код -> всё, функция готова для вызова из мода или любых других инклудов, также к нему подключенных. Однако в данном случае у нас не получится каким-либо образом вызывать эту функцию из любого другого фильтрскрипта. Вернее получится, если вы подключите этот же инклуд отдельно и к фильтрскрипту, но тем самым вы просто продублируете весь код инклуда ещё и для этого фильтрскрипта и в итоге будете иметь параллельную работу двух (всё равно не связанных между собой) систем из вашего инклуда в фильтрскрипте и в моде.
Если в качестве api функций вы имеете что-то вроде этого:
PHP код:
stock IsValidVehicleModel(skinid) return (400 <= skinid <= 611);
stock IsValidSkinModel(skinid) return (0 <= skinid <= 73 || 75 <= skinid <= 311);
stock IsValidMapIconType(markertype) return (0 <= markertype <= 63);
//И любые другие несложные функции, которые не обращаются к памяти и результат выполнения которых определяется проверками/вычислениями только лишь относительно заданных ей аргументов и ничего больше
То в данном случае, конечно, нет ничего критичного в том, что они будут продублированы отдельно (при подключении этого инклуда) для каждого фильтрскрипта, поскольку нет нужды выполнения этих функций в каком-то одном месте (например в моде), а остальным фильтрскриптам также нет нужды перенаправлять их вызовы в мод.
Однако если вы имеете какую-либо более-менее полноценную систему в вашем инклуде с подобным функционалом:
PHP код:
new bool:IsChatEnabled[MAX_PLAYERS];
public OnPlayerConnect(playerid)
{
IsChatEnabled[playerid] = true;
//Hook
#if defined myinc_OnPlayerConnect
return myinc_OnPlayerConnect(playerid);
#else
return 1;
#endif
}
//Hook
#if defined _ALS_OnPlayerConnect
#undef OnPlayerConnect
#else
#define _ALS_OnPlayerConnect
#endif
#define OnPlayerConnect myinc_OnPlayerConnect
#if defined myinc_OnPlayerConnect
forward myinc_OnPlayerConnect(playerid);
#endif
public OnPlayerText(playerid, text[])
{
//Если IsChatEnabled равна false - блокируем отправку сообщений игрока в чат
if(!IsChatEnabled[playerid]) return 0;
//Hook
#if defined myinc_OnPlayerText
return myinc_OnPlayerText(playerid, text);
#else
return 1;
#endif
}
//Hook
#if defined _ALS_OnPlayerText
#undef OnPlayerText
#else
#define _ALS_OnPlayerText
#endif
#define OnPlayerText myinc_OnPlayerText
#if defined myinc_OnPlayerText
forward myinc_OnPlayerText(playerid, text[]);
#endif
stock EnableChatForPlayer(playerid, bool:enable)
{
if(!IsPlayerConnected(playerid)) return 0;
IsChatEnabled[playerid] = enable;
return 1;
}
stock IsChatEnabledForPlayer(playerid)
return (IsPlayerConnected(playerid) ? IsChatEnabled[playerid] : 0);
То вы уже наверняка понимаете, что выполняться эти функции должны из одного места, иначе при параллельном выполнении инклуда в фильтрскриптах и в моде функция EnableChatForPlayer будет иметь эффект на блокировку чата лишь там, где выполняется копия такого инклуда (например, установив через неё запрет на отправку сообщий игроку в чат в одном из фильтрскриптов, а потом сняв запрет ею же в моде, блокировка чата продолжится, потому что это будут две параллельно работающие блокировки в фильтрскрипте и в моде, а по итогу вы отключаете её только в моде). Что же делать в таком случае - ответ ниже.
Второй, более сложный пример реализации API функций уже с возможностью перенаправления их вызовов из фильтрскриптов в мод (т.е. наши функции, вызываясь в фильтрскриптах, не выполняются тут же в них, а всего лишь шлют удалённый вызов в инклуд, подключенный к моду, в котором находится уже непосредственно код этих функций) происходит следующим образом: мы создаём stock функции-"обёртки", в которых есть всего лишь функция CallRemoteFunction (клик) с вызовом другой паблик функции -> далее мы создаём эти самые паблик функции, которые в себе уже и содержат фактический код наших функций. CallRemoteFunction позволяет вызывать именно паблик функции (и именно поэтому нам приходится делать реальную функцию пабликом) всего лишь по её названию/указанным аргументам, ища и вызывая её по всем фильтрскриптам/моду уже в процессе работы сервера. Таким образом, благодаря CallRemoteFunction мы имеем возможность, вызвав функцию-обёртку в фильтрскрипте, послать сигнал на выполнение паблик функции с реальным кодом нашей функции уже в моде, тем самым выполняя код системы лишь в одном, едином месте. Удобно, но взамен мы жертвуем скоростью вызова этих функций, потому как CallRemoteFunction, выполняя вызов функции не напрямую, делает это достаточно медленно. Тем не менее, вот сам способ реализации этого варианта кодом, который используют почти во всех инклудах, где есть возможность вызывать функции инклуда из фильтрскриптов, выполняя их при этом в моде:
PHP код:
//На этот раз пример содержит не сами api функции некого инклуда,
//а хуки обычных самповских нативов, которые также нам часто нужно перехватить и в моде и в скриптах,
//однако код хука выполнить только в моде, перенаправляя вызовы из фильтрскриптов
//Для начала давайте добавим те функции-обёртки, которые будут перенаправлять все вызовы в мод
stock myinc_SetPlayerHealth(playerid, Float:health)
return CallRemoteFunction("myGMinc_SetPlayerHealth", "if", playerid, health);
//Hook
#if defined _ALS_SetPlayerHealth
#undef SetPlayerHealth
#else
#define _ALS_SetPlayerHealth
#endif
#define SetPlayerHealth myinc_SetPlayerHealth
stock myinc_SetPlayerArmour(playerid, Float:armour)
return CallRemoteFunction("myGMinc_SetPlayerArmour", "if", playerid, armour);
//Hook
#if defined _ALS_SetPlayerArmour
#undef SetPlayerArmour
#else
#define _ALS_SetPlayerArmour
#endif
#define SetPlayerArmour myinc_SetPlayerArmour
//Теперь нам нужна часть инклуда, которая будет выполняться только в моде (т.е. только если не задефайнен FILTERSCRIPT)
//В ней мы и объявим логику самой системы и наши паблик функции с фактическим кодом,
//которые будут удалённо вызываться хукнутыми выше функциями
#if !defined FILTERSCRIPT
//Код системы, который должен выполняться только из мода
new
Float:PlayerHealth[MAX_PLAYERS],
Float:PlayerArmour[MAX_PLAYERS];
//Паблики с фактическим кодом перехваченных функций
forward myGMinc_SetPlayerHealth(playerid, Float:health);
public myGMinc_SetPlayerHealth(playerid, Float:health)
{
if(!SetPlayerHealth(playerid, health)) return 0; //Валидация аргументов самой самповской функцией + её выполнение, если валидация прошла
PlayerHealth[playerid] = health; //Весь наш код
return 1;
}
forward myGMinc_SetPlayerArmour(playerid, Float:armour);
public myGMinc_SetPlayerArmour(playerid, Float:armour)
{
if(!SetPlayerArmour(playerid, armour)) return 0; //Валидация аргументов самой самповской функцией + её выполнение, если валидация прошла
PlayerArmour[playerid] = armour; //Весь наш код
return 1;
}
//Ну а далее уже обнуление хп/брони при коннекте просто для демонстрации целостности системы из этого примера
//Однако стоит заметить, что эта часть системы (как и любые другие, если таковые бы были далее)
//также должна выполняться в части инклуда, доступной только из мода
public OnPlayerConnect(playerid)
{
PlayerHealth[playerid] = 100.0;
PlayerArmour[playerid] = 0.0;
//Hook
#if defined myinc_OnPlayerConnect
return myinc_OnPlayerConnect(playerid);
#else
return 1;
#endif
}
//Hook
#if defined _ALS_OnPlayerConnect
#undef OnPlayerConnect
#else
#define _ALS_OnPlayerConnect
#endif
#define OnPlayerConnect myinc_OnPlayerConnect
#if defined myinc_OnPlayerConnect
forward myinc_OnPlayerConnect(playerid);
#endif
#endif // !defined FILTERSCRIPT
Заметили, что реализация такого способа не потребовала создания двух отдельных инклудов: одного только для фильтрскриптов, а другого исключительно для мода? Всё в данном случае решила "#if !defined FILTERSCRIPT", тем самым один и тот же инклуд нам нужно просто подключить к моду и всем фильтрскриптам. Это тоже удобно и также используется многими достаточно давно, но при этом в таком случае важно иметь во всех фильтрскриптах перед подключением каких-либо инклудов дефайн "FILTERSCRIPT" в самом начале, таким образом давая этой системе работать правильно.
И, вроде как, вот он выход из ситуации и решение проблемы, которым все пользуются уже достаточно долгое время: если нам нужны функции, которые должны мочь вызываться из фильтрскриптов, то мы делаем их вторым способом, а если нет - первым. Но что, если мы имеем достаточно объёмную систему, которая например, перехватывает большинство стандартных самповских нативов или имеет десятки/сотни API функций, для которых просто необходимо иметь возможность вызова из FS'а, но при этом, используя для них всех медленный CallRemoteFunction, мы совсем не можем быть уверенными, что конечный пользователь нашего инклуда имеет у себя вообще хоть какие-то фильтрскрипты? Это достаточно серьёзная проблема, потому как активное использование таких "перенаправляемых" функций из самого мода будет впустую замедлять весь сервер, а фича из второго способа, тем самым, превращается в абузу.
Именно поэтому (+-) пару лет назад я сделал своё собственное решение на основе второго варианта, которое также позволяет вызывать наши API функции инклуда из фильтрскриптов (подключая инклуд к фильтрскриптам и перенаправляя функции в инклуд из мода) и при этом вызывая их напрямую из самого мода. Реальная оптимизация ощущается гораздо больше на практике, потому как чаще всего люди абсолютно не используют никакие фильтрскрипты либо используют их непродолжительное время, тем самым в таком большинстве случаев мы полностью снимаем нагрузку от CallRemoteFunction, хотя сама возможность вызова из фильтрскриптов никуда не пропадает и может быть добавлена в любом из подключенных фильтрскриптов даже прямо во время работы сервера.
Итак, вот сама реализация такого метода на примере всё тех же хуков стандартных функций сампа:
PHP код:
//Для начала давайте снова добавим функции-обёртки, которые будут перенаправлять все вызовы в мод, но на этот раз...
stock myinc_SetPlayerHealth(playerid, Float:health)
{
//мы тут же проверяем, где именно вызвалась наша функция-обёртка
#if defined FILTERSCRIPT
//и если это фильтрскрипт, то (куда деваться) перенаправляем вызов в мод через CallRemoteFunction
return CallRemoteFunction("myGMinc_SetPlayerHealth", "if", playerid, health);
#else
//а вот если мод, то вызов паблика делаем напрямую, тем самым не теряя в скорости
return myGMinc_SetPlayerHealth(playerid, health);
#endif
}
//Hook
#if defined _ALS_SetPlayerHealth
#undef SetPlayerHealth
#else
#define _ALS_SetPlayerHealth
#endif
#define SetPlayerHealth myinc_SetPlayerHealth
//Здесь всё то же самое
stock myinc_SetPlayerArmour(playerid, Float:armour)
{
#if defined FILTERSCRIPT
return CallRemoteFunction("myGMinc_SetPlayerArmour", "if", playerid, armour);
#else
return myGMinc_SetPlayerArmour(playerid, armour);
#endif
}
//Hook
#if defined _ALS_SetPlayerArmour
#undef SetPlayerArmour
#else
#define _ALS_SetPlayerArmour
#endif
#define SetPlayerArmour myinc_SetPlayerArmour
//Теперь нам нужна часть инклуда, которая будет выполняться только в моде (т.е. только если не задефайнен FILTERSCRIPT)
//В ней мы и объявим наши паблик функции с фактическим кодом и логику самой системы
#if !defined FILTERSCRIPT
//Код системы, который должен выполняться только из мода
new
Float:PlayerHealth[MAX_PLAYERS],
Float:PlayerArmour[MAX_PLAYERS];
//Паблики с фактическим кодом перехваченных функций
forward myGMinc_SetPlayerHealth(playerid, Float:health);
public myGMinc_SetPlayerHealth(playerid, Float:health)
{
if(!SetPlayerHealth(playerid, health)) return 0; //Валидация аргументов самой самповской функцией + её выполнение, если валидация прошла
PlayerHealth[playerid] = health; //Весь наш код
return 1;
}
forward myGMinc_SetPlayerArmour(playerid, Float:armour);
public myGMinc_SetPlayerArmour(playerid, Float:armour)
{
if(!SetPlayerArmour(playerid, armour)) return 0; //Валидация аргументов самой самповской функцией + её выполнение, если валидация прошла
PlayerArmour[playerid] = armour; //Весь наш код
return 1;
}
//Ну а далее уже обнуление хп/брони при коннекте просто для демонстрации целостности системы из этого примера
//Однако стоит заметить, что эта часть системы (как и любые другие, если таковые бы были далее)
//также должна выполняться в части инклуда, доступной только из мода
public OnPlayerConnect(playerid)
{
PlayerHealth[playerid] = 100.0;
PlayerArmour[playerid] = 0.0;
//Hook
#if defined myinc_OnPlayerConnect
return myinc_OnPlayerConnect(playerid);
#else
return 1;
#endif
}
//Hook
#if defined _ALS_OnPlayerConnect
#undef OnPlayerConnect
#else
#define _ALS_OnPlayerConnect
#endif
#define OnPlayerConnect myinc_OnPlayerConnect
#if defined myinc_OnPlayerConnect
forward myinc_OnPlayerConnect(playerid);
#endif
#endif // !defined FILTERSCRIPT
Как видите, изменения относительно примера из второго варианта реализации, по большому счёту, всего лишь в самих функциях-обёртках-хуках, где просто была добавлена одна дефайн проверка с двумя вариантами вызова каждой из наших функций. Ничего необычного, однако именно этот вариант следовало бы использовать большинству разработчиков инклудов, которые до сих пор в ста процентах случаев используют удалённый вызов даже там, где этого можно избежать и вызывать напрямую.
А вот ещё один пример, только уже с собственными API функциями:
PHP код:
//И снова добавим функции-обёртки
stock EnableChatForPlayer(playerid, bool:enable)
{
//Валидацию аргументов на этот раз можно вынести сразу сюда,
//чтобы не затрачивать время на перенаправление из фильтрскрипта функции, которая всё равно не выполнится
if(!IsPlayerConnected(playerid)) return 0;
#if defined FILTERSCRIPT
return CallRemoteFunction("gm_EnableChatForPlayer", "ii", playerid, enable);
#else
return gm_EnableChatForPlayer(playerid, enable);
#endif
}
stock IsChatEnabledForPlayer(playerid)
{
//Валидацию аргументов на этот раз можно вынести сразу сюда,
//чтобы не затрачивать время на перенаправление из фильтрскрипта функции, которая всё равно не выполнится
if(!IsPlayerConnected(playerid)) return 0;
#if defined FILTERSCRIPT
return CallRemoteFunction("gm_IsChatEnabledForPlayer", "i", playerid);
#else
return gm_IsChatEnabledForPlayer(playerid);
#endif
}
//Часть инклуда, которая будет выполняться только в моде (т.е. только если не задефайнен FILTERSCRIPT)
#if !defined FILTERSCRIPT
//Код системы, который должен выполняться только из мода
new bool:IsChatEnabled[MAX_PLAYERS];
//Паблики с фактическим кодом API функций
forward gm_EnableChatForPlayer(playerid, bool:enable);
public gm_EnableChatForPlayer(playerid, bool:enable)
{
IsChatEnabled[playerid] = enable;
return 1;
}
forward gm_IsChatEnabledForPlayer(playerid);
public gm_IsChatEnabledForPlayer(playerid) return IsChatEnabled[playerid];
//Продолжение основного кода системы
public OnPlayerConnect(playerid)
{
IsChatEnabled[playerid] = true;
//Hook
#if defined myinc_OnPlayerConnect
return myinc_OnPlayerConnect(playerid);
#else
return 1;
#endif
}
//Hook
#if defined _ALS_OnPlayerConnect
#undef OnPlayerConnect
#else
#define _ALS_OnPlayerConnect
#endif
#define OnPlayerConnect myinc_OnPlayerConnect
#if defined myinc_OnPlayerConnect
forward myinc_OnPlayerConnect(playerid);
#endif
public OnPlayerText(playerid, text[])
{
//Если IsChatEnabled равна false - блокируем отправку сообщений игрока в чат
if(!IsChatEnabled[playerid]) return 0;
//Hook
#if defined myinc_OnPlayerText
return myinc_OnPlayerText(playerid, text);
#else
return 1;
#endif
}
//Hook
#if defined _ALS_OnPlayerText
#undef OnPlayerText
#else
#define _ALS_OnPlayerText
#endif
#define OnPlayerText myinc_OnPlayerText
#if defined myinc_OnPlayerText
forward myinc_OnPlayerText(playerid, text[]);
#endif
#endif // !defined FILTERSCRIPT
Ну а вместо итога я хотел бы привести плюсы и минусы реализации API последним (моим) вариантом.
Плюсы:
* Все функции в вашем инклуде доступны для вызова как из мода, так и из фильтрскриптов (как в предыдущем методе);
* При этом нет большой нагрузки от вызова функций инклуда из мода, поскольку в этом случае они вызываются напрямую.
Минусы:
* Зависимость корректной работы всего этого алгоритма от того, добавил ли пользователь "#define FILTERSCRIPT" в начало каждого фильтрскрипта (как и в предыдущем методе);
* Для каждой функции вы должны иметь её "обёртку" и паблик с фактическим её кодом, т.е. в сумме технически две функции для одной фактической (как в предыдущем методе);
* Код становится ещё более некомпактным, чем в предыдущем методе, из-за постоянного дублирования проверок на фильтрскрипт внутри каждой функции-обёртки.
Тем не менее, как мне кажется, это вполне того стоит и эффективность с лихвой перевешивают косметические недостатки.