Перехват функций, часть 2: практика - пишем античит на HP
Оглавление:
Sup.
В этой части мы рассмотрим создание простого античита на HP, подходящего к любому моду*.
* За исключением тех, на которых уже есть аналогичный античит,
в этом случае два разных античита могут конфликтовать друг с другом.
Прежде, чем начать разбор примера, давайте рассмотрим несколько советов, которые помогут избежать самых распространённых ошибок, связанных с перехватами функций, и улучшить качество вашего кода.
Внимание: данные советы ещё не рассматривались ни в одной из известных мне статей по перехватам (в.т.ч. в статье Y_Less'а на оффе). Поэтому, даже если Вы уже знакомы с техникой перехватов, эти советы достойны Вашего внимания.- Внутри инклуда задавайте переменным и функциям (в т.ч. перехватываемым) один и тот же префикс, связанный с названием инклуда.
Главное, чтобы этот префикс и названия переменных и функций в этом скрипте были уникальными, т.е. не повторялись в других инклудах и в основном скрипте.
Также название префикса должно быть как можно менее общим. Плохие примеры: "ac", "fix".
Античиты и багфиксы могут быть разные, поэтому старайтесь, отразить суть своего багфикса или античита. Например, "ac_spect__" (античит на невидимость, получаемую имитацией перехода в спект) (на самом деле, после "ac_hp" только 1 символ подчёркивания - это из-за ограничения названий до 31 символов в Pawn) или "ac_hp__" (античит на читерское HP).
Если префиксы будут повторяться в разных перехватах, то эти перехваты будут конфликтовать друг с другом, что, в свою очередь, помешает компиляции скрипта.
Пример: если есть инклуд "afk_system.inc", в котором реализована система AFK, можно использовать в нём префикс "afk_sys__" для перехватов, переменных, констант и функций (кроме тех функций, которые будут видны за пределами инклуда, например IsPlayerAFK).
- Перехватчик не должен менять аргументы перехватываемой функции, её логику работы и предназначение.
Изменение возвращаемых значений допускается только в том случае, если это не нарушает совместимости с оригинальной функцией.
Например, в античите на HP перехватчик GetPlayerHealth возвращает не то значение, которое возвратит оригинальная функция, а кол-во HP игрока, хранящееся в античите. Но в то же время перехватчик должен возвращать именно HP, а не сумму HP и брони или ещё что-нибудь, что поменяет логику работы функции.
То же самое относится к изменению аргументов функции. Если с помощью перехвата добавить в какую-либо функцию дополнительные параметры, а потом убрать перехватчик, то компилятор будет выдавать ошибки и код не будет компилироваться без перехватчика, т.к. в оригинальной функции тех дополнительных параметров нет.
Иными словами, работа перехватчика должна быть незаметной для того кода, который использует перехватываемую функцию, как будто того перехватчика и нет.
Если же вам нужна функция, работающая по-другому - сделайте отдельную функцию, но не нужно путать её с оригиналом с помощью перехвата. Вмешательство в стандартную логику обычно приводит только к проблемам.
- Если название перехватываемой функции (в случае с коллбэками) или перехватчика (для нативных функций) длиннее 31 символов, его следует сократить до этого лимита. Сокращение производится путём отсечения лишних символов справа (например, ac_veh_hp__OnVehicleDamageStatusUpdate -> ac_veh_hp__OnVehicleDamageStatu), строго до длины в 31 символ (не больше и не меньше!).
Пример:
{
// ...
#if defined ac_veh_hp__OnVehicleDamageStatu // "урезаем" название до 31 символа
ac_veh_hp__OnVehicleDamageStatu(vehicleid, playerid);
#endif
return 1;
}
#if defined _ALS_OnVehicleDamageStatusUpdat // "урезаем" название до 31 символа
#undef OnVehicleDamageStatusUpdate
#else
#define _ALS_OnVehicleDamageStatusUpdat
#endif
#define OnVehicleDamageStatusUpdate ac_veh_hp__OnVehicleDamageStatu
#if defined ac_veh_hp__OnVehicleDamageStatu
forward ac_veh_hp__OnVehicleDamageStatu(vehicleid, playerid);
#endif
- Всё, что должно использоваться только внутри инклуда (в данном уроке это переменные для записи HP - нельзя допустить, чтобы они изменялись из мода), должно иметь атрибут static.
Пример:
static some_var; // переменная не будет видна из мода
static stock DoSomething() // функция не будет видна из мода
{
// ...
}
- Если перехватываемая функция не обязательно должна возвращать значение (пример: коллбэк OnPlayerConnect), вызывайте её из перехватчика следующим образом:
#if defined LibName__Func
LibName__Func();
#endif
return 1;
Благодаря этому удастся избежать варнингов, если перехватываемая функция ничего не возвращает.
- Если в коде внутри перехватчика используются локальные переменные, ограничивайте этот код локальным блоком.
public Func(param1, param2)
{
// локальные переменные ограничены локальным блоком
{
new string[256];
// ...
}
#if defined LibName__Func
return Libname_Func(param1, param2);
#endif
}
Таким образом локальные переменные перехватчика не будут занимать место в стеке при вызове перехватываемой функции, благодаря чему перехватчики почти не будут потреблять дополнительного стекового пространства в моде.
Перейдём обратно к заданию - написанию античита на HP.
Сначала нужно определиться, как мы назовём инклуд с античитом и какой префикс будем использовать для переменных/функций.
Для примера подойдёт название "ac_health.inc" или "ac_hp.inc". Соответственно, будем использовать префикс "ac_hp__".
Дальше создадим массив, в который будем записывать здоровье игроков, устанавливаемое сервером:
static Float:ac_hp__health[MAX_PLAYERS];
Вместо new массив объявлен с помощью ключевого слова static - так, если перенести весь античит в отдельный инклуд, массив будет виден только внутри инклуда и не будет мешаться в моде.
Дальше можно было бы найти все вызовы SetPlayerHealth и под ними дублировать устанавливаемое кол-во HP в ac_health__health:
ac_hp__health[playerid] = 100.0;
НО для этого вам придётся:- Выискивать вручную все вызовы и точно так же вручную "прицеплять" к каждому из них дублирование HP в массив. И ни в коем случае не пропустить ни одного из них!
- Всегда при использовании SetPlayerHealth держать в голове, что нужно добавить дублирование в ac_health.
Кроме того, стоит помнить, что чем больше сделано модификаций кода, тем больше вероятность того, что туда вкрадётся какая-нибудь ошибка. Человеческий фактор.
А может быть вы уже используете такой подход в своём RLS и даже не подозреваете, что в вашем античите на HP уже куча подобных ошибок, которые никогда не заметите ни вы, ни компилятор?
Причём одна такая ошибка - и в один прекрасный момент недоантичит начнёт банить ни в чём не повинных игроков, а репутация проекта будет испорчена. Если же попытаетесь отыскать источник ошибки, столкнётесь с новой проблемой: забаненный игрок может не помнить всех подробностей (да и не факт, что он вообще что-то захочет говорить скриптеру) - придётся перепроверять каждый вызов SetPlayerHealth и устраивать кучу тестов на локальном сервере.
Цитата:
Но ведь можно просто найти все вызовы SetPlayerHealth и под ними копировать устанавливаемое кол-во HP в ac_health!
И именно поэтому такой подход всегда был и будет считаться быдлокодерством - абсолютно никаких гарантий надёжности и куча проблем, причину которых очень трудно найти. От человеческого фактора нельзя полностью избавиться, лучшее, что можно сделать - минимизировать риск, избавившись от дублирования кода.
С техникой перехвата ситуация намного проще.
Поскольку перехватчик всего один, достаточно лишь адекватно протестировать хотя бы 1 случай его использования.
Затем во всех остальных случаях вызова SetPlayerHealth компилятор возьмёт всю рутинную работу по разбору перехватов на себя и перехватчик всегда будет вести себя так, как было запланировано.
В результате вероятность возникновения ошибки сводится к нулю. Такой код поддерживать намного проще.
Сделаем перехват функции SetPlayerHealth и в этом перехватчике осуществим дублирование выдаваемого кол-ва HP в массив:
stock ac_hp__SetPlayerHealth(playerid, &Float:health)
{
ac_hp__health[playerid] = health;
}
#if defined _ALS_SetPlayerHealth
#undef SetPlayerHealth
#else
#define _ALS_SetPlayerHealth
#endif
#define SetPlayerHealth ac_hp__SetPlayerHealth
Теперь нужно будет в функцию OnPlayerUpdate добавить проверку игрока на несанкционированное восстановление HP и последующую выдачу бана.
Всё это, как вы уже могли догадаться, будет сделано с помощью перехватов.
{
// код перехвата вынесен в отдельный блок,
{ // чтобы после его выполнения переменные не занимали место в стеке
new Float:health;
// если кол-во HP изменилось с момента предыдущего обновления
// сравниваемые значения трактуются, как целочисленные, чтобы избежать лишнего вызова floatcmp
// (внимание! такой оптимизационный приём можно применять только при сравнении
// с помощью знаков "==" и "!=", но ни в коем случае не с ">", "<", ">=" или "<=")
if(_:ac_hp__health[playerid] != _:health)
{
// если игрок потерял HP, упав с высоты или с помощью чита - запоминаем новое значение
// (пусть отнимает HP читами сколько угодно, всё равно восстановить его он уже не сможет)
if(ac_hp__health[playerid] > health)
{
ac_hp__health[playerid] = health;
}
// если HP больше, чем записано в античите - HAX detected!
else if(ac_hp__health[playerid] < health)
{
}
}
}
// на wiki.sa-mp.com написано, что возвращаемое коллбэком значение ни на что не влияет
// и может быть пропущено, поэтому не стоит "требовать" это значение у перехватываемой функции -
// вместо этого лучше использовать "return 1" отдельно от её вызова
#if defined ac_hp__OnPlayerUpdate
ac_hp__OnPlayerUpdate(playerid);
#endif
return 1;
}
#if defined _ALS_OnPlayerUpdate
#undef OnPlayerUpdate
#else
#define _ALS_OnPlayerUpdate
#endif
#define OnPlayerUpdate ac_hp__OnPlayerUpdate
#if defined ac_hp__OnPlayerUpdate
forward ac_hp__OnPlayerUpdate(playerid);
#endif
Теперь сделаем перехват GetPlayerHealth. По идее он не обязателен и никак не отразится на работе античита.
Но в случае с функцией GetPlayerHealth предоставляется возможность избавиться от вызова нативной функции, подменив её на функцию, написанную на Pawn - таким образом повысится производительность работы сервера при вызове функции.
При этом на возвращаемом функцией значении это никак не отразится, т.к. античит теперь имеет полный контроль над HP игрока.
stock ac_hp__GetPlayerHealth(playerid, &Float:health)
{
health = ac_hp__health[playerid];
return 1;
}
#if defined _ALS_GetPlayerHealth
#undef GetPlayerHealth
#else
#define _ALS_GetPlayerHealth
#endif
#define GetPlayerHealth ac_hp__GetPlayerHealth
Внимание! Перехват OnPlayerUpdate следует делать выше перехватов GetPlayerHealth/SetPlayerHealth, иначе они будут влиять на работу перехватчика в OnPlayerUpdate, если в нём используются те функции.
Но это ещё не всё. Если выдать игроку здоровье, оно выдастся ему не сразу из-за пинга.
Античит проверит здоровье со следующим вызовом OnPlayerUpdate ещё до того, как здоровье обновится у игрока, в результате сервер запишет то кол-во HP, которое было ещё до выдачи.
Как только у игрока обновится HP, в античите будет записано прежнее значение, и после ещё одной проверки в OnPlayerUpdate античит выдаст ложное срабатывание.
Нужно исправить эту ситуацию. Сделаем так, чтобы античит игнорировал уменьшение HP у игрока в течение 1 секунды после выдачи.
К переменным (под ac_hp__health):
static ac_hp__ignore_timestamp[MAX_PLAYERS];
Добавим перед переменными константу, в которой укажем время для игнорирования:
#if !defined AC_HP__IGNORE_TIME
#define AC_HP__IGNORE_TIME 1000
#endif
Почему вокруг объявления константы используется #if defined? Это мы разберём позже.
А пока что добавим игнорирование в перехвате OnPlayerUpdate.
Найдите строку:
if(ac_hp__health[playerid] > health)
и замените её на:
if((ac_hp__health[playerid] > health)
И остаётся лишь сделать запись времени, до которого античит будет игнорировать игрока.
В перехвате SetPlayerHealth найдите строки:
ac_hp__health[playerid] = health;
и замените их на:
ac_hp__ignore_timestamp
[playerid
] = GetTickCount()+AC_HP__IGNORE_TIME
; ac_hp__health[playerid] = health;
Теперь античит не будет выдавать ложных срабатываний при выдаче HP.
Но остаётся ещё одна проблема: игрок будет умирать во время спавна.
Суть в том, что при спавне у игрока всегда 100 HP, а в античите в это время записано 0.
Если сервер не выдаст игроку новое кол-во HP, античит выдаст ложное срабатывание и установит игроку 0 HP.
Чтобы исправить эту проблему, добавим под перехватом OnPlayerUpdate перехват OnPlayerSpawn, в котором запишем у игрока 100 HP.
{
ac_hp__ignore_timestamp
[playerid
] = GetTickCount()+AC_HP__IGNORE_TIME
; ac_hp__health[playerid] = 100.0;
#if defined ac_hp__OnPlayerSpawn
ac_hp__OnPlayerSpawn(playerid);
#endif
return 1;
}
#if defined _ALS_OnPlayerSpawn
#undef OnPlayerSpawn
#else
#define _ALS_OnPlayerSpawn
#endif
#define OnPlayerSpawn ac_hp__OnPlayerSpawn
#if defined ac_hp__OnPlayerSpawn
forward ac_hp__OnPlayerSpawn(playerid);
#endif
Итак, если игрок накручивает HP читами, античит будет понижать здоровье обратно.
Но что, если нам нужно не только нейтрализовать читера, но и оповестить администрацию?
Можно добавить после понижения здоровья вызов какой-нибудь функции из мода (например, SendAdminMessage) для вывода модераторам сообщения о читере, НО такая функция есть не везде, а потому модуль не будет работать на всех модах.
Поэтому мы поступим иначе: добавим в мод коллбэк OnHPCheatDetected и в нём будем записывать весь код, который "привязан" к тому моду.
Здесь же сделаем вызов коллбэка. В перехвате OnPlayerUpdate находим строку:
и заменяем её на:
#if defined OnHPCheatDetected
OnHPCheatDetected(playerid, ac_hp__health[playerid], health);
#endif
И не забудем добавить опережающее объявление коллбэка для мода. В конец инклуда:
#if defined OnHPCheatDetected
forward OnHPCheatDetected(playerid, Float:hp_expected, Float:hp_got);
#endif
Обратите внимание: в обоих отрывках присутствует "#if defined" - это сделано для того, чтобы убедиться, что коллбэк OnHPCheatDetected реализован в моде.
Если его в моде нет, он не будет вызван из инклуда - иначе были бы ошибки из-за вызова несуществующей функции.
Наконец, составим весь код воедино и вынесем его в отдельный инклуд (например, "ac__health.inc", здесь "ac" - сокращение от "AntiCheat").
Результат должен выглядеть примерно так, как в этой теме.
В итоге получается система, совершенно никак не привязанная к конкретному моду.
Поскольку она готова, остаётся лишь использовать её в вашем моде.
Сохраним инклуд в папке "pawno/include" и подключим его:
После этого добавим в мод коллбэк OnHPCheatDetected, который уже сделан в инклуде:
public OnHPCheatDetected(playerid, Float:hp_expected, Float:hp_got)
{
// здесь Ваш код для оповещения модераторов, бан игрока и т.д. и т.п.
}
Если у Вас получилось всё вышеперечисленное - поздравляю, Вы написали свой первый модуль на Pawn с использованием перехватов.
К следующей неделе придумаю ещё несколько примеров для 3-й части урока.
Автор: Daniel_Cortez
Копирование данной статьи на других ресурсах без разрешения автора запрещено!