shape1
shape2
shape3
shape4
shape7
shape8

Фильтр имен (обучение работы с SQLite)


SHOROOP

Освоившийся
Пользователь
30.01.2014
58
56
0
29
Скриптер
Доброго времени суток.

Данный материал прежде всего предназначен для RP/RPG-серверов, однако если будет использоваться где-то еще - спорить не буду.

Итак, небольшое вступление.
На некоторых серверах существуют правила для ника игрока. Самый яркий пример - РП-сервера, где ник обычно требуют в формате %name%_%surname%. Более того, кое-где существует запрет на регистрацию с ником, имя в котором указано в уменьшительно-ласкательной или иной, отличной от официальной, формы (Sasha вместо Alexander и так далее). При достаточно большом количестве игроков уследить за подобными случаями становится все сложнее.
Но, как говорится, "кто ищет - тот всегда найдет!"

Перед скриптом ставились следующие задачи:

  • Хранить где-то базу некорректных ников. Это показалось проще, чем вести базу корректных.
  • Иметь удобные инструменты для добавления, удаления и проверки наличия записи.
  • Не допускать игрока к серверу в случае совпадения имени с базой и отключать от сервера в случае добавления новой записи в базу.
Первую задачу решено было решить, используя SQLite. Создавать что-то на файлах показалось мне не слишком удобным в реализации, а на MySQL такую ерунду делать не хотелось. Хотя можно и ее использовать, но зачем, если, по сути, нам нужна только одна колонка в БД?
Вторая решилась еще тривиальнее - три команды-три запроса к базе. Еще один запрос - в момент подключения.

Но - обо всем по порядку.

Итак, для начала необходимо подключить нужные библиотеки. В нашем случае - a_samp и a_sampdb.

#include <a_samp>
#include <a_sampdb>
Для удобства забьем имя файла с базой как директиву препроцессора.

#define db_name "WrongNicks.db"
Далее - создаем переменную типа DB для хранения указателя на открытую базу и переменную типа DBResult для хранения возвращаемых из базы результатов.

new DB:NicksDB;
new DBResult:Result;
Начнем с загрузки самой базы. Я использовал код в отдельном ФС, однако ничто не мешает зашить его в гейммод.
Первоначально с помощью команды db_open открываем файл, имя которого забито в db_name. Или создаем, если он не был создан:

NicksDB = db_open(db_name);
Далее - делаем запрос в базу. Необходимо, если таблицы Nicks не существует - создать ее с одной колонкой NICK типа varchar - символьной строки. Делается это простым запросом:

db_query(NicksDB,"CREATE TABLE IF NOT EXISTS Nicks (NICK varchar)");
Проверять отдельно, есть ли таблица в файле и есть ли файл вообще, нет необходимости, как нет необходимости и в дальнейших действиях при загрузке ГМ или ФС.
Конечный код в случае ФС будет выглядеть так:

public OnFilterScriptInit()
{
NicksDB = db_open(db_name);
db_query(NicksDB,"CREATE TABLE IF NOT EXISTS Nicks (NICK varchar)");
return 1;
}

После выгрузки ГМ или ФС базу нужно корректно закрыть. Тут все еще проще - db_close().

public OnFilterScriptExit ()
{
db_close(NicksDB);
return 1;
}
Далее я решил начать с реализации проверки на наличие ника в базе и, соответственно, запрещению доступа к серверу, если ник в базе есть.
Алгоритм действий таков:

  • Получаем ник игрока. Это достаточно просто, есть стандартная функция GetPlayerName(), описывать ее я смысла не вижу.
  • Выделение из ника имени и фамилии.
    Обычно разделителем выступает нижняя черта - ее проверять и будем. Берем с SA:MP Wiki функцию split:stock split(const strsrc[], strdest[][], delimiter)
    {
        new i, li;
        new aNum;
        new len;
        while(i <= strlen(strsrc))
        {
            if(strsrc == delimiter || i == strlen(strsrc))
            {
                len = strmid(strdest[aNum], strsrc, li, i, 128);
                strdest[aNum][len] = 0;
                li = i+1;
                aNum++;
            }
            i++;
        }
        return 1;
    }
    Создаем двумерный массив для хранения имени и фамилии и записываем в его элементы отдельно имя и отдельно фамилию, полученные после работы split().
    [*]Запрос в базу на наличие ника.
    Проверка организована просто - в базу посылается запрос, в ответе проверяем, сколько строк вернула база. Если не ноль - значит, записи есть и ник запрещен, а значит - игрока нужно кикнуть. Если ноль - записи нет, ник разрешен, ничего с игроком делать не нужно.

Берем OnPlayerConnect и производим все необходимые действия по алгоритму:

public OnPlayerConnect (playerid)
{
new Nick[MAX_PLAYER_NAME];//Получение ника
GetPlayerName (playerid, Nick, MAX_PLAYER_NAME);
new Name[2][24];//Получение имени и "остатков"
split (Nick, Name, '_');
new query [512];//Отсылка запроса к БД
format (query, sizeof(query), "SELECT * FROM Nicks WHERE NICK LIKE '%s'", Name[0]);
Result = db_query (NicksDB, query);//и прием результата запроса.
if (db_num_rows(Result))//Если строк результата больше, чем ноль - запись есть!
{
new Response[144];
SendClientMessage (playerid, 0xdc143cFF, "Вы были кикнуты сервером. Причина: В нике указано неполное имя.");
Kick(playerid);
format (Response, sizeof(Response), "Игрок %s кикнут сервером. Причина: В нике указано неполное имя.", Nick);
SendClientMessageToAll (0xdc143cFF, Response);
}
return 1;
}
Хочу обратить внимание на сам запрос в БД:

SELECT * FROM Nicks WHERE NICK LIKE '%s'
Оператор LIKE здесь используется как оператор нестрогого сравнения - то есть регистр символов может не совпадать. Однако с русскими символами в строке такой запрос не пройдет, что в MySQL, что в SQLite придется городить костыли. У меня не используются ники с русскими символами, поэтому меня такой запрос вполне устраивает в текущем виде.

Ну и осталось, собственно, только организовать удобство работы с такой базой.
Поскольку бросать идею не хотелось, а использовать для такого порыва покодить какие-то сторонние тулзы вроде командных процессоров было откровенно лень - все построено на стандартной функции OnPlayerCommandText и ненавистной многими strtok:

strtok(const string[], &index)
{
new length = strlen(string);
while ((index < length) && (string[index] <= ' '))
{
index++;
}

new offset = index;
new result[20];
while ((index < length) && (string[index] > ' ') && ((index - offset) < (sizeof(result) - 1)))
{
result[index - offset] = string[index];
index++;
}
result[index - offset] = EOS;
return result;
}
Однако, конечно, если захотите использовать другой командный процессор - пожалуйста, перетаскивайте.
Начнем с команды добавления записи в базу и одновременной проверки игроков на свежедобавленное имя. Алгоритм прост - из строки cmdtext получаем необходимый параметр и делаем запрос к базе, после чего циклом проверяем все имена игроков, полученные split().
Далее код с комментариями, который, конечно, нужно пихнуть в OnPlayerCommandText, в котором уже объявлены нужные переменные для strtok:

//Для strtok
new cmd[256], tmp[256], idx;
cmd = strtok(cmdtext, idx);
Код:
 if (!strcmp ("/addnick", cmd, true))
{
if (!IsPlayerAdmin(playerid)) //Если игрок не RCon - доступ к функции не даем.
{
SendClientMessage (playerid, 0xdc143cFF, "Вы не обладаете необходимыми правами доступа.");
return 1;
}
tmp = strtok(cmdtext, idx);
if (!strlen(tmp)) //Если параметр пустой - выводим сообщение об ошибке.
{
SendClientMessage (playerid, 0xdc143cFF, "Синтаксис команды: /addnick [Имя]");
return 1;
}
new query[512]; //Делаем запрос в базу.
format (query, sizeof(query), "INSERT INTO Nicks (NICK) VALUES ('%s')", tmp);
db_query (NicksDB, query);
SendClientMessage (playerid, 0xdc143cFF, "Ник добавлен в базу данных."); //..и оповещаем об этом пользователя.
new TempNick[MAX_PLAYER_NAME]; //Готовим переменную под ник.
for (new pid=0; pid<MAX_PLAYERS; pid++) //Для всех игроков
{
if (IsPlayerConnected(pid)) //Если проверяемый подключен -
{
GetPlayerName (pid, TempNick, MAX_PLAYER_NAME); //получаем его ник полностью
if (!strcmp (TempNick, tmp, true, strlen(tmp))) //и проверяем на соответствие с новой записью - вне зависимости от регистра и только количество символов, которое было в новой записи!
{
//Если условия соблюдены - выбрасываем игрока с сервера. Войти обратно ему не даст код в OnPlayerConnect.
new Response[144];
SendClientMessage (pid, 0xdc143cFF, "Вы были кикнуты сервером. Причина: В нике указано неполное имя.");
Kick(pid);
format (Response, sizeof(Response), "Игрок %s кикнут сервером. Причина: В нике указано неполное имя.", TempNick);
SendClientMessageToAll (0xdc143cFF, Response);
}
}
}
return 1;
}
Похожая ситуация - с удалением записи. Даже если похожей записи нет - запрос ничего лишнего не удалит, поэтому на проверку наличия в базе можно не заморачиваться.

if (!strcmp ("/removenick", cmd, true))
{
if (!IsPlayerAdmin(playerid))
{
SendClientMessage (playerid, 0xdc143cFF, "Вы не обладаете необходимыми правами доступа.");
return 1;
}
tmp = strtok(cmdtext, idx);
if (!strlen(tmp))
{
SendClientMessage (playerid, 0xdc143cFF, "Синтаксис команды: /removenick [Имя]");
return 1;
}
new query[512];
format (query, sizeof(query), "DELETE FROM Nicks WHERE NICK LIKE '%s'", tmp); //Запрос на удаление строк с похожим значением.
db_query (NicksDB, query);
SendClientMessage (playerid, 0xdc143cFF, "Ник удален из базы данных.");
return 1;
}
В конкретном случае вместо строгого сравнения в запросе опять используется оператор LIKE.

Ну, и функция проверки на наличие записи. Тут все аналогично проверке в OnPlayerConnect - если запрос вернет более нуля строк как результат, значит, запись есть. В противном случае - нет.

if (!strcmp ("/checknick", cmd, true))
{
if (!IsPlayerAdmin(playerid))
{
SendClientMessage (playerid, 0xdc143cFF, "Вы не обладаете необходимыми правами доступа.");
return 1;
}
tmp = strtok(cmdtext, idx);
if (!strlen(tmp))
{
SendClientMessage (playerid, 0xdc143cFF, "Синтаксис команды: /checknick [Имя]");
return 1;
}
new query[512];
format (query, sizeof(query), "SELECT * FROM Nicks WHERE NICK LIKE '%s'", tmp);
Result = db_query (NicksDB, query);
if (db_num_rows(Result)) SendClientMessage (playerid, 0xdc143cFF, "Данное имя есть в базе данных запрещенных имен.");
else SendClientMessage (playerid, 0xdc143cFF, "Данного имени в базе запрещенных имен нет.");
return 1;
}
На этом все! Система работоспособна и готова к наполнению невалидными никами.

#define FILTERSCRIPT
#define db_name "WrongNicks.db"

#include <a_samp>
#include <a_sampdb>

new DB:NicksDB;
new DBResult:Result;

public OnFilterScriptInit()
{
NicksDB = db_open(db_name);
db_query(NicksDB,"CREATE TABLE IF NOT EXISTS Nicks (NICK varchar)");
return 1;
}

public OnFilterScriptExit ()
{
db_close(NicksDB);
return 1;
}

public OnPlayerConnect (playerid)
{
new Nick[MAX_PLAYER_NAME];
GetPlayerName (playerid, Nick, MAX_PLAYER_NAME);
new Name[2][24];
split (Nick, Name, '_');
new query [512];
format (query, sizeof(query), "SELECT * FROM Nicks WHERE NICK LIKE '%s'", Name[0]);
Result = db_query (NicksDB, query);
if (db_num_rows(Result))
{
new Response[144];
SendClientMessage (playerid, 0xdc143cFF, "Вы были кикнуты сервером. Причина: В нике указано неполное имя.");
Kick(playerid);
format (Response, sizeof(Response), "Игрок %s кикнут сервером. Причина: В нике указано неполное имя.", Nick);
SendClientMessageToAll (0xdc143cFF, Response);
}
return 1;
}

public OnPlayerCommandText (playerid, cmdtext[])
{
new cmd[256], tmp[256], idx;
cmd = strtok(cmdtext, idx);
if (!strcmp ("/addnick", cmd, true))
{
if (!IsPlayerAdmin(playerid))
{
SendClientMessage (playerid, 0xdc143cFF, "Вы не обладаете необходимыми правами доступа.");
return 1;
}
tmp = strtok(cmdtext, idx);
if (!strlen(tmp))
{
SendClientMessage (playerid, 0xdc143cFF, "Синтаксис команды: /addnick [Имя]");
return 1;
}
new query[512];
format (query, sizeof(query), "INSERT INTO Nicks (NICK) VALUES ('%s')", tmp);
db_query (NicksDB, query);
SendClientMessage (playerid, 0xdc143cFF, "Ник добавлен в базу данных.");
new TempNick[MAX_PLAYER_NAME];
for (new pid=0; pid<MAX_PLAYERS; pid++)
{
if (IsPlayerConnected(pid))
{
GetPlayerName (pid, TempNick, MAX_PLAYER_NAME);
if (!strcmp (TempNick, tmp, true, strlen(tmp)))
{
new Response[144];
SendClientMessage (pid, 0xdc143cFF, "Вы были кикнуты сервером. Причина: В нике указано неполное имя.");
Kick(pid);
format (Response, sizeof(Response), "Игрок %s кикнут сервером. Причина: В нике указано неполное имя.", TempNick);
SendClientMessageToAll (0xdc143cFF, Response);
}
}
}
return 1;
}
if (!strcmp ("/removenick", cmd, true))
{
if (!IsPlayerAdmin(playerid))
{
SendClientMessage (playerid, 0xdc143cFF, "Вы не обладаете необходимыми правами доступа.");
return 1;
}
tmp = strtok(cmdtext, idx);
if (!strlen(tmp))
{
SendClientMessage (playerid, 0xdc143cFF, "Синтаксис команды: /removenick [Имя]");
return 1;
}
new query[512];
format (query, sizeof(query), "DELETE FROM Nicks WHERE NICK LIKE '%s'", tmp);
db_query (NicksDB, query);
SendClientMessage (playerid, 0xdc143cFF, "Ник удален из базы данных.");
return 1;
}
if (!strcmp ("/checknick", cmd, true))
{
if (!IsPlayerAdmin(playerid))
{
SendClientMessage (playerid, 0xdc143cFF, "Вы не обладаете необходимыми правами доступа.");
return 1;
}
tmp = strtok(cmdtext, idx);
if (!strlen(tmp))
{
SendClientMessage (playerid, 0xdc143cFF, "Синтаксис команды: /checknick [Имя]");
return 1;
}
new query[512];
format (query, sizeof(query), "SELECT * FROM Nicks WHERE NICK LIKE '%s'", tmp);
Result = db_query (NicksDB, query);
if (db_num_rows(Result)) SendClientMessage (playerid, 0xdc143cFF, "Данное имя есть в базе данных запрещенных имен.");
else SendClientMessage (playerid, 0xdc143cFF, "Данного имени в базе запрещенных имен нет.");
return 1;
}
return 0;
}

//Разделение
stock split(const strsrc[], strdest[][], delimiter)
{
new i, li;
new aNum;
new len;
while(i <= strlen(strsrc))
{
if(strsrc == delimiter || i == strlen(strsrc))
{
len = strmid(strdest[aNum], strsrc, li, i, 128);
strdest[aNum][len] = 0;
li = i+1;
aNum++;
}
i++;
}
return 1;
}

strtok(const string[], &index)
{
new length = strlen(string);
while ((index < length) && (string[index] <= ' '))
{
index++;
}

new offset = index;
new result[20];
while ((index < length) && (string[index] > ' ') && ((index - offset) < (sizeof(result) - 1)))
{
result[index - offset] = string[index];
index++;
}
result[index - offset] = EOS;
return result;
}


Исходник также доступен на пастбине.

Данное решение, возможно, не является суперсовершенным и суперфункциональным, но то, чего я хотел - я добился. Все работает, и работает как надо.
Вопросы и пожелания принимаются ниже.

 
Последнее редактирование модератором: