Распространённые ошибки в процессе кодинга

Dragokas

Меценат
Сообщения
200
Реакции
162
10. Распространённые ошибки в процессе кодинга:
Writing Sane Plugins - Множество советов и наиболее частые ошибки.​
XY Problem - Нужна помощь? Когда вы задаёте вопрос, убедитесь, что описали реальную проблему, а не то, что по вашему мнению поможет её исправить.​


Ошибки из логов sourcemod/logs/errors_<date>.log:
  • Unable to load plugin (no debug string table) - вам нужно скомпилировать плагин с помощью более старой версии SM, например 1.9 (а лучше обновите версию SourceMod на вашем сервере).

  • Illegal disk size - smx файл повреждён во время загрузки на FTP. Загрузите плагин ещё раз.

  • Client X is not connected или Client X is not in game - потеряна проверка IsClientInGame() в исходном коде.
    - Возможно, недостаточно безопасно хранятся индексы клиентов. См. ниже "Хранение индексов клиентов и сущностей".

  • Invalid entity или Invalid edict - потеряна проверка IsValidEntity() или IsValidEdict() в исходном коде.
    - Возможно, недостаточно безопасно хранятся индексы сущностей. См. ниже "Хранение индексов клиентов и сущностей".

  • "Native XXX is not bound"- зависимость плагина не установлена, не загружена или возникла ошибка при её загрузке.
    • если зависимость опциональна, прежде чем вызывать натив, всегда проверяйте, доступен ли он, используя функцию GetFeatureStatus() в форварде OnAllPluginsLoaded.
    • Либо проверяйте присутствие библиотеки с помощью LibraryExists() в форварде OnAllPluginsLoaded в случае, если зависимость регистрирует такую библиотеку. (Прим. переводчика: вместо LibraryExists лучше использовать пару форвардов OnLibraryLoaded / OnLibraryUnloaded, т.к. операция LibraryExists более ресурсоёмка).

  • "Instruction contained invalid parameter" - исходный код был успешно скомпилирован, несмотря на серьезную синтаксическую или структурную ошибку в связи. Компилятор упустил это, т.к. его парсер не является идеальным. Пример: не-инициализированная переменная.
    - В данном случае, лог ошибки может указывать даже на неверное имя плагина, который не виновен, но приводит к появлению ошибки из-за обращения к другому - виновному плагину.


Ошибки при компиляции:
  • "error 075: input line too long - Файл вероятно имеет неверную кодировку. Преобразуйте её в UTF-8.
    - Альтернативно, продублируйте рабочий скрипт, замените вашим кодом, удалите проблемный файл и переименуйте дубликат в ваше имя файла.

  • Array-based enum structs will be removed in 1.11 / Array-based enum structs have been removed- примеры исправления:
    • через добавление MAX_ITEM в enum: пример. Примечание.: В разных билдах SM 1.11 поддержка именованных enum то убиралась, то добавлялась, поэтому если на момент релиза SM 1.11 она будет отсустствовать, тогда можно посоветовать также удалить имя у enum и везде по коду использовать тип "int" вместо типа имени enum.
    • через преобразование в enum struct: пример

  • Local variable "v" shadows a variable at a preceding level - означает, что вы объявили переменную "v" дважды.



Ошибки в клиентской консоли:
  • KeyValues Error: RecursiveLoadFromBuffer: got EOF instead of keyname in file materials/effects/tankwall.vmt UnlitGeneric, - неверный синтаксис в указанном файле (например, потеряна кавычка или скобка "}" ).



Ошибки в серверной консоли:
  • Entity 393 (class 'infected') reported ENTITY_CHANGE_NONE but 'm_hEffectEntity' changed. - при изменении некоторых свойств сущности необходимо явно оповещать об этом движок с помощью функции ChangeEdictState. Пример: ChangeEdictState(ent, FindSendPropInfo("infected", "m_hEffectEntity"));


Логические ошибки:
  • Статические переменные - Знайте, когда использовать; предназначены для удержания данных при последующих вызовах функции. Не ожидайте, что такая переменная будет инициализирована со значением по умолчанию при последующем вызове, например, не полагайтесь на то, что строка будет пустой.
    • не рекомендуется для локальных массивов. Пример использования: когда вам не нужна глобальная переменная, можно воспользоваться к примеру статической переменной типа bool для определения, установлен ли хук на функцию, так что вы можете переключать или вызывать функцию множество раз без проблем.
    • если к примеру вы пытаетесь вызвать функцию множество раз в попытке хукнуть что-то и только статический bool оберегает от множественных дублирований хуков, вам вероятно стоит иначе продумать логику скрипта.

  • Если вы пользуетесь транзакциями в базе данных, убедитесь, что создали таблицу с Transaction-Safe типом, например "InnoDB": Руководство.

  • Не используйте FakeClientCommand для вызова вашей зарегистрированной команды в том же самом плагине. Вызывайте сразу по имени колбека, например CmdTest(client, 0);

  • Некоторые люди полагают, что OnPlayerRunCmd использует больше циклов процессора, чем OnGameFrame; это не так. Используйте правильную последовательность проверки; подробности см. ниже.

  • Неверная (не-эффективная) последовательность валидации. Делая битовые операции либо проверку GetGameTime, вы тратите значительно меньше циклов процессора, чем при родных вызовах, таких как IsValidEntity или IsClientInGame. Сначала сделайте первое, прежде чем переходить к более дорогостоящим вызовам. Это особенно важно для хуков Think и OnGameFrame.

  • Неверный порядок проверки клиента. Верным будет: if (client && client <= MaxClients) для проверки на 0 (не поддерживается listen серверами) и клиентского индекса, IsClientConnected (в большинстве случаев не требуется, т.к. следующий вызов покрывает её), IsClientInGame, затем уже всё остальное, например, GetClientTeam, IsPlayerAlive и т.д.

  • Вызов FindConVar множество раз для одного и того же квара. Вместо этого, используйте данную команду в OnPluginStart один раз и сохраняйте возвращенный хендл для последующего использования.

  • Используйте GetEngineVersion вместо GetGameFolderName. Последнее использовалось в старых скриптах до того, как появилась данная функция.

  • Использование strlen для проверки того, что строка пустая. Более быстрый способ - проверить первый знак: blah[0] == 0 или blah[0] == '\0' или !blah[0]

  • Использование только одного события round_end для сброса переменных и т.д. Вы должны учесть, что люди могу сменить карту посреди игры, а в таком случае событие не срабатывает. Для сброса используйте дополнительно OnMapEnd или событие round_start.

  • Вам необходимо проверять, запущена ли карта, прежде чем создавать сущности.

  • Прежде чем искать и изменять сущности в OnMapStart(), вам необходимо подождать немного, пока сущности корректно инициализируются. Лучше всего использовать событие "round_freeze_end" (не доступно в некоторых играх). Или попробуйте воспользоваться RequestFrame. Прим. переводчика: некоторые сущности требуют еще большего времени ожидания, например, машины. Для гарантированного доступа на запись к их свойствам советую подождать первого из событий player_first_spawn, проверив что оно сработало для не-фейкового клиента (т.е. не бота).

  • Отправка большого числа UserMessages или панелей (SendPanelToClient) слишком часто за короткий промежуток времени может спровоцировать краш клиента с ошибкой "Reliable buffer overflow".

  • Меню и панели, а также их пункты не появляются, когда первым знаком строки является "[" (в некоторых играх, например L4D). Вместо этого используйте " [" (т.е. добавив лидирующий пробел). Учтите, что это может повлиять на случай перечисления в меню имён игроков, которые начинаются на символ квадратной скобки "[".
    - Делайте: ReplaceString(ClientUserName, sizeof(ClientUserName), "[", ""); ИЛИ Format(text_client, sizeof(text_client), " %s", ClientUserName);

  • Передача заранее неизвестных данных (например, никнейма) в команды вида ServerCommand в аргумент "format". Вы рано или поздно получите ошибку: "String formatted incorrectly", например, если ник клиента будет содержать символ %. Правильным вариантом будет использование "%s" в качестве форматного аргумента: ServerCommand("%s", buffer);

  • HookEvent не вызывает событие по причине потерянного модификатора const в прототипе колбека: public Action Event_RoundEnd(Event event, [b]const[/b] char[] name, bool dontBroadcast) При этом, код нормально компилируется. Более того, некоторые события вполня нормально работают с таким прототипом (до определённого стечения обстоятельств).


Unknown Command: - Потерян return Plugin_Handled;
  • Если вы создаёте команду и получаете "Unknown command" в консоль, вам необходимо завершить колбек через: return Plugin_Handled;

PHP:
public void OnPluginStart()
{
    RegConsoleCmd("sm_test", CmdTest);
}

public Action CmdTest(int client, int args)
{
    // Действия
    return Plugin_Handled;
}

Хранение индексов клиентов и сущностей либо передача их в Таймеры:
  • Чтобы не затронуть неверного клиента, когда он отключается, либо неверную сущность, когда она удаляется, мы используем следующие команды для получения уникального идентификатора для них:
  • GetClientUserId для получения клиентского userid (каждый новый подключаемый клиент будет иметь userid + 1 от предыдущего человека, который зашел на сервер).
  • GetClientOfUserId для обратной операции - получения индекса клиента по его userid. Если он отключился, значение будет 0. Должно использоваться вместе с IsClientInGame. Прим. переводчика: на самом деле, в процессе отключения человек всё еще будет иметь не нулевой индекс, при этом часть команд для такого клиента будут более навалидны. Поэтому проверка IsClientInGame не является опциональной, как могло показаться.
  • EntIndexToEntRef для хранения индекса сущности путем её преобразования в уникальную порядковую ссылку.
  • EntRefToEntIndex для обратной операции - получения индекса сущности из её ссылки. Если сущность более не существует, значение будет равно -1 (INVALID_ENT_REFERENCE).
  • Альтернативой к GetClientUserId и GetClientOfUserId являются: GetClientSerial and GetClientFromSerial.
  • Вот пример хранения, получения и проверки индексов клиента и сущности.

Сущности:
PHP:
public void OnEntityCreated(int entity, const char[] classname)
{
    // Сущность создалась, и её индекс между 0 и 2048 или 4096 для не распространяющихся по сети сущностей.
    if( strcmp(classname "molotov_projectile") == 0 )
    {
        // Получаем ссылку, чтобы избежать случайного влияния на сущность, которая могла бы появится с тем же индексом после удаления нашей
        CreateTimer(5.0, TimerDelete, EntIndexToEntRef(entity));
    }
}

public Action TimerDelete(Handle timer, any entity)
{
    // Преобразовуем ссылку обратно в индекс сущности
    entity = EntRefToEntIndex(entity);

    // Проверяем, что индекс не равен -1 и действителен для использования
    if( entity != INVALID_ENT_REFERENCE )
    {
        RemoveEntity(entity); // Безопасно удаляем эту сущность, зная, что это в точности тот самый исходный оригинал
    } else {
        // Сущность более не существует и мы в безопасности, потому что не повлияем по неосторожности на другую сущность, которая могла бы повторно воспользоваться таким же индексом.
    }
}

Клиенты:
PHP:
int g_iClientList[MAXPLAYERS+1];
public void OnClientPutInServer(int client)
{
    // Всегда передаём UserID в таймеры и проверяем его в колбеке таймера
    CreateTimer(10.0, TimerMessage, GetClientUserId(client));

    // Иной пример: вы могли бы сохранять клиентские UserID в массив, чтобы затем использовав в другой функции проверить, влияем ли мы на тот же самый клиент, что существовал в момент сохранения его UserId.
    g_iClientList[client] = GetClientUserId(client);
}

public Action TimerMessage(Handle timer, any client)
{
    // Получаем клиентский индекс (должен быть в диапазоне от 1 до MaxClients на выделенном сервере)
    client = GetClientOfUserId(client);
    if( client != 0 && IsClientInGame(client) )
    {
        // Это тот же самый клиент, на который мы хотим повлиять
    } else {
        // Должно быть он отключился
    }
}

// Другой пример, допустим, вызываем функцию, которая использует сохранённый ранее список клиентских UserID:
public void SomeFunction(int client)
{
    int client = GetClientOfUserId(g_iClientList[client]);
    if( client && IsClientInGame(client) )
    {
        // Это тот же самый клиент, что и был ранее (при сохранении списка UserId)
    } else {
        // Должно быть он отключился
    }
}

OnEntityCreated - Получение данных:
  • Попытка получить имя модели в форварде OnEntityCreated может не сработать. Правильный способ - использование SDKHooks SpawnPost или RequestFrame.
  • Некоторые сущности всё ещё не будут иметь корректных данных о позиции или скорости. Вам необходимо также воспользоваться SpawnPost и возможно, дополнительно RequestFrame после него.

PHP:
public void OnEntityCreated(int entity, const char[] classname)
{
    if( strcmp(classname "molotov_projectile") == 0 )
    {
        SDKHook(entity, SDKHook_SpawnPost, SpawnPost);
    }
}

public void SpawnPost(int entity)
{
    // Верификация
    if( !IsValidEntity(entity) ) return;

    // Теперь модель действительна
    char sModel[64];
    GetEntPropString(entity, Prop_Data, "m_ModelName", sModel, sizeof(sModel));

    // В зависимости от сущности, скорость может быть некорректна до следующего фрейма
    RequestFrame(nextFrame, EntIndexToEntRef(entity)); // Вторым параметром передаём переменную в колбек. Она опциональна, и в этом примере мы её используем.
}

public void nextFrame(int entity)
{
    // Верифицируем
    if( (entity = EntRefToEntIndex(entity)) != INVALID_ENT_REFERENCE )
    {
        // Теперь данные о скорости корректны
        float vVel[3];
        GetEntPropVector(entity, Prop_Send, "m_vInitialVelocity", vVel);
    }
}

Объявление переменных:
  • Объявляйте переменные за пределами цикла. Указание их внутри является очень неэффективным, особенно для строк. К примеру:

PHP:
// Это правильно:
    char temp[64];
    int test;
    for( int i = 1; i < 2048; i++ )
    {
        // Прочее
    }

// Это плохо:
    for( int i = 1; i < 2048; i++ )
    {
        char temp[64];
        int test;
        // Прочее
    }

Смешивание разных типов переменных (int вместе с float):
PHP:
// Неверно:
float f = 1.5;
int i = 2;
i += f;
PrintToServer("wrong: %i", i); // Ответ: 1080033280

// Правильным будет сперва привести float в int через округление:
float f = 1.5;
int i = 2;
i += RoundToCeil(f); // или RoundToFloor, RoundToZero, RoundFloat, RoundToNearest
PrintToServer("correct: %i", i); // Ответ: 4
 
Последнее редактирование:

Kruzya

Главный уборщик говнокода
Меценат
Сообщения
11,375
Реакции
9,481
исходный код был успешно скомпилирован, несмотря на серьезную синтаксическую или структурную ошибку в связи с тем, что парсер не является идеальным
В совсем редких случаях может помочь банальная перекомпиляция плагина актуальным компилятором. Иногда имеет смысл вообще попробовать зарепортить разработчикам.
Из своей практики как-то ловил на SourcePawn 1.11 такого же рода ошибку, и отрепортил разработчикам. Они признали ошибку, исправили её, и с тех пор её больше ни разу не видел. Это, кстати, был первый и последний раз, когда я её увидел. 😀

Ошибки при компиляции
Сюда я бы добавил ошибку, когда при компиляции через compile.exe вылезает информация о том, что spcomp.exe упал. Чаще всего причиной ошибки является отсутствие папки compiled в папке с исходником.
Суть баги в том, что compile.exe проверяет наличие этой папки и создаёт её в случае отсутствия - только в папке, где он сам и располагается, а в папке с исходником даже не проверяет её, но всё равно указывает её настоящему компилятору (Сишному) как "место, куда нужно поместить бинарь". Тот не справляется и падает с сегфолтом.
Но это в каких-то старых компиляторах вылезает. На свежем вроде исправили:
1610538126013.png
 

Grey83

Ленивая и невнимательная жопа
Сообщения
5,436
Реакции
3,232
Меню и панели, а также их пункты не появляются, когда первым знаком строки является "[".
Не везде (так вот почему у меня менюшка в l4d2 не отображалась, получается?!).
В ксс и ксго прокатывает.
 

_wS_

Участник
Сообщения
356
Реакции
714
До сих пор на автомате допускаю эту ошибку (скорее всего многие тоже) + не вижу её в списке.

PHP:
public void OnPluginStart()
{
    char s[] = " 234 ";
    PrintToServer("%d, %d", strlen(s), TrimString(s));
}

Результат: 3, 3
Хотели: 5, 3

Выходит, сначала выполняется красное, затем зелёное (всегда справа налево).
PrintToServer("%d, %d", strlen(s), TrimString(s));

Порядок в if (1 && 2 && 3 ..) чётко слева направо, и код выполняется сверху вниз, а тут хрн пойми откуда и куда 😢
 
Сверху