Всем привет.
Начну с того, что это не совсем обычный урок - скорее, повествование. Но нигде на pro-pawn я не видел статей подобного жанра, так что...
Всё началось с того, что Untonyst, проверяя работу своего инклуда fix_K2BEx.inc, наткнулся на странный баг: при использовании функции BanEx в samp.ban вместо текста бана записывалась какая-то "каша" из букв.
Об этом он вскоре написал мне и, как оказалось, баг наблюдался и в моём инклуде dc_kickfix.inc, поэтому я решил разобраться.
Для начала я взял тестовый мод, в котором использовался инклуд dc_kickfix.inc, и ввёл в нём команду "/ban 0 проверка":
А вот содержимое файла samp.ban после бана:
Пример кода, с помощью которого можно воспроизвести этот баг:
PHP код:
SetPVarString(playerid, "ban_reason", "проверка");
new reason[128];
GetPVarString(playerid, "ban_reason", reason, sizeof(reason));
DeletePVar(playerid, "ban_reason");
BanEx(playerid, reason);
Поскольку задачей инклудов fix_K2BEx.inc и dc_kickfix.inc было сделать кик/бан с задержкой, чтобы успеть показать игроку причину, функции Kick, Ban и BanEx вызывались с помощью таймера.
А чтобы как-то передать функции BanEx строку с причиной бана (в функции SetTimerEx спецификатор "s" не работает), эта строка предварительно сохранялась в PVar.
Можно было бы сделать сохранение в одномерном массиве, но если забанить 2 игроков за полсекунды, то последний игрок перезапишет причину бана первого (массив-то общий!) и первому игроку будет показана та же причина бана, что и последнему.
Если же сделать двухмерный массив, то его придётся делать размером MAX_PLAYERS * 128 ячеек (вместо 128 может быть и меньшее число - главное, чтобы вместилась причина бана), но резервировать участок памяти сразу под всех игроков и никогда не высвобождать его тоже не целесообразно.
Именно поэтому было решено использовать PVar'ы - с ними память выделяется только тогда, когда это нужно, а после использования её можно высвободить, удалив PVar.
Со слов Untonyst, баг проявлялся только в тех случаях, когда в причине бана были русские буквы, и если в коде выше поставить "print(reason);" перед вызовом BanEx, то причина бана с символами кириллицы будет выведена, как ни в чём не бывало, но в samp.ban текст запишется неправильно. Очень странно.
С его же слов, если вместо хранения причины в PVar сделать двухмерный массив и сохранять причину бана в нём, то никаких проблем с сохранением русского текста в samp.ban не будет.
Значит причина, скорее всего, кроется в PVar'ах.
Я решил проверить эту догадку, но только на SVar'ах, чтобы можно было провести тесты на пустом сервере. Всё равно механизм хранения в SVar'ах примерно тот же самый, разве что без привязки к игрокам.
PHP код:
#include <a_samp>
main()
{
static str[] = "AaBbZzАаБбЯя";
new str1[20], str2[20];
str1[0] = '\0', strcat(str1, str, sizeof(str1));
SetSVarString("reason", str);
GetSVarString("reason", str2, sizeof(str2));
print(str1);
print(str2);
for (new i = 0;; ++i)
{
printf("[%d]\t%d\t%08x\t%c", i, str1[i], str1[i], str1[i]);
if (str1[i] == '\0')
break;
}
for (new i = 0;; ++i)
{
printf("[%d]\t%d\t%08x\t%c", i, str2[i], str2[i], str2[i]);
if (str2[i] == '\0')
break;
}
}
Здесь в один массив строка копируется прямиком из строковой константы, а в другой она копируется с промежуточным сохранением в SVar.
После этого содержимое массивов выводится сначала полностью, а затем и посимвольно (с номерами позиций в строке и кодами символов).
Вывод:
Открыть/закрыть
Код:
AaBbZzАаБбЯя
AaBbZzАаБбЯя
[0] 65 00000041 A
[1] 97 00000061 a
[2] 66 00000042 B
[3] 98 00000062 b
[4] 90 0000005A Z
[5] 122 0000007A z
[6] 192 000000C0 А
[7] 224 000000E0 а
[8] 193 000000C1 Б
[9] 225 000000E1 б
[10] 223 000000DF Я
[11] 255 000000FF я
[12] 0 00000000
[0] 65 00000041 A
[1] 97 00000061 a
[2] 66 00000042 B
[3] 98 00000062 b
[4] 90 0000005A Z
[5] 122 0000007A z
[6] -64 FFFFFFC0 А
[7] -32 FFFFFFE0 а
[8] -63 FFFFFFC1 Б
[9] -31 FFFFFFE1 б
[10] -33 FFFFFFDF Я
[11] -1 FFFFFFFF я
[12] 0 00000000
Как и говорил Untonyst, второй массив после сохранения в SVar'е с помощью print и printf выводится нормально, но посмотрите внимательно на коды его последних символов:
Код:
[4] 90 0000005A Z
[5] 122 0000007A z
[6] -64 FFFFFFC0 А
[7] -32 FFFFFFE0 а
[8] -63 FFFFFFC1 Б
[9] -31 FFFFFFE1 б
[10] -33 FFFFFFDF Я
[11] -1 FFFFFFFF я
[12] 0 00000000
Ага, у русских символов старшие байты ячеек установлены в FF (255) вместо 00 !
Неудивительно, что они некорректно сохранялись в файл.
Опытным путём удалось выяснить, что все символы с кодом от 0 до 127 обрабатываются корректно.
Сам баг проявляется только в символах с кодами от 128 до 255 - среди них как раз и находятся символы кириллицы.
Скорее всего, Kalcor перед возвратом строки в GetPVarString конвертировал символы из char в cell (т.е. с расширением знакового бита, т.к. оба типа данных не беззнаковые), из-за чего коды символов больше 127 после получения из PVar/SVar'а становились неправильными.
Чтобы исправить символ, достаточно лишь установить старшие байты ячейки в 0, при этом оставив младший байт, как есть.
Это можно легко сделать, выполнив побитовое "И" кода символа с числом 0x000000FF, т.к. будут действовать следующие правила:
X & 0xFF = X
X & 0x00 = 0
В итоге получаем функцию:
PHP код:
FixSVarString(str[], size = sizeof(str))
for (new i = 0; ((str[i] &= 0xFF) != '\0') && (++i != size);) {}
(Ещё вместо побитового "И" можно было вычислить остаток от деления на 256, но для процессора эта операция куда более дорогостоящая.)
Проверка. В отрывок кода, приведённый выше, ставим вызов получившейся функции:
PHP код:
SetPVarString(playerid, "ban_reason", "проверка");
new reason[128];
GetPVarString(playerid, "ban_reason", reason, sizeof(reason));
DeletePVar(playerid, "ban_reason");
FixSVarString(reason);
BanEx(playerid, reason);
Вывод:
Открыть/закрыть
Код:
AaBbZzАаБбЯя
AaBbZzАаБбЯя
[0] 65 00000041 A
[1] 97 00000061 a
[2] 66 00000042 B
[3] 98 00000062 b
[4] 90 0000005A Z
[5] 122 0000007A z
[6] 192 000000C0 А
[7] 224 000000E0 а
[8] 193 000000C1 Б
[9] 225 000000E1 б
[10] 223 000000DF Я
[11] 255 000000FF я
[12] 0 00000000
[0] 65 00000041 A
[1] 97 00000061 a
[2] 66 00000042 B
[3] 98 00000062 b
[4] 90 0000005A Z
[5] 122 0000007A z
[6] 192 000000C0 А
[7] 224 000000E0 а
[8] 193 000000C1 Б
[9] 225 000000E1 б
[10] 223 000000DF Я
[11] 255 000000FF я
[12] 0 00000000
Всё работает так, как и должно.
Примерно то же самое я сделал в инклуде dc_kickfix.inc.
Для проверки в тестовом моде снова ввёл команду /ban. В игре отличий никаких, но другое дело в samp.ban:
Символы кириллицы записались так, как и должны были. Проблема решена.
P.S.: Инклуд dc_kickfix.inc обновлён, баг с записью символов кириллицы исправлен.
UPD: Инклуд fix_K2BEx.inc тоже дождался обновления.
UPD[2]: Фикс принят в fixes.inc (вернее, в форк fixes.inc от ziggi - насколько я понял, он единственный, кто сейчас работает над обновлением инклуда):
UPD (10.05.18): Как удалось выяснить, причина бага кроется во всё той же самопальной функции set_amxstring, уже успевшей отметиться в баге с выходом за пределы массива - видимо, создатели SA-MP решили использовать эту кривую самоделку, не осилив стандартную функцию amx_SetString.
int set_amxstring(AMX* amx, cell amx_addr, const char* source, int max)
{
cell* dest = (cell *)(amx->base + (int)(((AMX_HEADER *)amx->base)->dat + amx_addr));
cell* start = dest;
while (max--&&*source)
*dest++=(cell)*source++;
*dest = 0;
return dest-start;
}
Обратите внимание на эту строку:
Как и предполагалось, при записи символа в массив Pawn происходит конверсия из char в cell, т.е. с расширением знакового бита. Значения больше 127 воспринимаются как отрицательные, например 128 => -128 (0x80), 129 => -127 (0x81), 130 => -126 (0x82), ..., 255 => -1 (0xFF), соответственно при конверсии в cell эти значения всё так же получаются отрицательными (0x80 => 0xFFFFFF80, 0x81 => 0xFFFFFF81, ..., 0xFF => 0xFFFFFFFF), со старшими байтами равными FF - отсюда и баг.
Чтобы устранить эту проблему, достаточно указанную выше строку заменить на:
*dest++=(cell)(unsigned char)*source++;
После этого конверсия будет происходить из беззнакового типа (без расширения знакового бита) и баг будет устранён.
Автор: Daniel_Cortez
Копирование данной статьи на других ресурсах без разрешения автора запрещено!