Welcome!

By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!

Система регистрации и авторизации с IQ-тестом (SA-MP + MySQL)

Янв
268
39
Пользователь

🎯 Назначение​

Система обеспечивает:
  • Регистрацию новых игроков с обязательным прохождением IQ-теста (минимум > 40 баллов).
  • Авторизацию существующих игроков по паролю.
  • Безопасное хранение паролей с использованием SHA256.
  • Загрузку игрока в игровой мир только после успешной авторизации.
  • Защиту от несанкционированного доступа.



🛠️ Требования​

Плагины​


ПлагинНазначение
mysql.so / mysql.dllРабота с базой данных (BlueG R41+)
sha256.so / sha256.dllХеширование паролей

Include-файлы​

  • <a_samp>
  • <mysql>
  • <sha256>

Настройка server.cfg​

plugins mysql sha256

База данных (MySQL)​

Создайте таблицу:

CREATE TABLE `players` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(24) NOT NULL UNIQUE,
`password` CHAR(64) NOT NULL, -- SHA256 хеш
`iq` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`spawned` TINYINT(1) NOT NULL DEFAULT 0
);

🔐 Безопасность​

  • Пароли никогда не хранятся в открытом виде — используются SHA256-хеши.
  • SQL-запросы защищены через mysql_real_escape_string.
  • Игрок не может спавниться, пока не пройдёт авторизацию.
  • При провале IQ-теста игрок автоматически кикается.

⚠️ Примечание: SHA256 — это улучшение по сравнению с plaintext, но не замена bcrypt/scrypt. Для продакшена рекомендуется использовать более стойкие алгоритмы хеширования.



🔄 Логика работы​

При подключении игрока (OnPlayerConnect)​

  1. Система проверяет наличие игрока в таблице players по нику.
  2. Если игрок новый → показывается диалог регистрации.
  3. Если игрок известен → показывается диалог входа.



Регистрация​

  1. Игрок вводит пароль (мин. 6 символов).
  2. Запускается IQ-тест: 5 случайных вопросов из пула 10.
  3. За каждый правильный ответ — +20 баллов (макс. 100).
  4. Если итоговый IQ ≤ 40 → игрок кикается.
  5. Если IQ > 40 → данные сохраняются в БД, игрок спавнится.



Авторизация​

  1. Игрок вводит пароль.
  2. Система хеширует введённый пароль и сравнивает с записью в БД.
  3. При совпадении — игрок помечается как авторизованный и спавнится.
  4. При ошибке — повторный запрос пароля.



❓ IQ-тест​

  • Формат вопроса: текст + 4 варианта (A, B, C, D).
  • Выборка: 5 уникальных вопросов выбираются случайно при каждой регистрации.
  • Оценка:
    • Правильный ответ = +20 баллов
    • Максимум = 100
    • Минимум для допуска = 41
Пример вопроса:
«Сколько будет 2 + 2?»
A) 3 B) 4 C) 5 D) рыба
Правильный ответ: B

🧩 Состояния игрока​

Система отслеживает состояние каждого игрока через enum PlayerState:



ПолеОписание
pLoggedАвторизован ли игрок
pRegisteredСуществует ли аккаунт в БД
pInIqTestПроходит ли тест (регистрация)
pSpawnedВыполнен ли спавн в мире
Это предотвращает дублирование действий и несанкционированный доступ.




🚫 Защита от обхода​

  • Переопределены OnPlayerSpawn и OnPlayerRequestSpawn:
    • Если игрок не авторизован — спавн заблокирован.
    • Игрок остаётся в "чёрном экране" до ввода корректных данных.
  • При выходе из диалога без подтверждения — игрок кикается.



📂 Файлы и зависимости​


ФайлНазначение
gamemodes/yourmode.pwnОсновной код системы
plugins/mysql.dllMySQL-плагин (BlueG)
plugins/sha256.dllПлагин хеширования
pawno/include/sha256.incInclude для SHA256


🛠️ Настройка подключения к БД​

В функции OnGameModeInit() измените строку:


connection = mysql_connect("localhost", "samp", "root", "password");


Параметры:

  • "localhost" — хост БД
  • "samp" — имя базы данных
  • "root" — пользователь MySQL
  • "password" — пароль пользователя


Ну и наконец код самой системы:
Pawn:
#include <a_samp>
#include <mysql>
#include <sha256>

// Диалоги
#define DIALOG_LOGIN            1
#define DIALOG_REGISTER         2
#define DIALOG_IQ_TEST_BASE     100

// Константы
#define MAX_IQ_QUESTIONS        10
#define QUESTIONS_TO_ASK        5
#define COLOR_RED               0xFF0000AA
#define COLOR_GREEN             0x00FF00AA
#define COLOR_YELLOW            0xFFFF00AA

// Состояния игрока
enum PlayerState {
    pLogged = 0,
    pRegistered,
    pTryingLogin,
    pInIqTest,
    pSpawned
}
new PlayerData[MAX_PLAYERS][PlayerState];

// Временные данные
new PlayerTempPass[MAX_PLAYERS][65]; // SHA256
new PlayerIQScore[MAX_PLAYERS];
new PlayerCurrentQuestion[MAX_PLAYERS];
new PlayerSelectedQuestions[MAX_PLAYERS][QUESTIONS_TO_ASK];

// Вопросы (можно вынести в файл позже)
stock const IQ_Questions[MAX_IQ_QUESTIONS][2][] = {
    {"Сколько будет 2 + 2?", "B"},
    {"Какой месяц идёт после марта?", "B"},
    {"Если у вас 5 яблок и вы отдали 2, сколько осталось?", "B"},
    {"Столица Франции — это?", "A"},
    {"Какой элемент обозначается символом 'O'?", "C"},
    {"Сколько сторон у квадрата?", "D"},
    {"Какой год был следующим после 1999?", "B"},
    {"Что тяжелее: 1 кг железа или 1 кг пуха?", "C"},
    {"Сколько часов в сутках?", "D"},
    {"Какой цвет получится при смешивании синего и жёлтого?", "A"}
};

stock const IQ_Options[MAX_IQ_QUESTIONS][] = {
    "A) 3\nB) 4\nC) 5\nD) рыба",
    "A) февраль\nB) апрель\nC) май\nD) июнь",
    "A) 2\nB) 3\nC) 7\nD) 0",
    "A) Париж\nB) Лондон\nC) Берлин\nD) Рим",
    "A) Золото\nB) Азот\nC) Кислород\nD) Углерод",
    "A) 2\nB) 3\nC) 5\nD) 4",
    "A) 1900\nB) 2000\nC) 2001\nD) 1998",
    "A) Железо\nB) Пух\nC) Одинаково\nD) Невозможно определить",
    "A) 12\nB) 18\nC) 20\nD) 24",
    "A) Зелёный\nB) Оранжевый\nC) Фиолетовый\nD) Красный"
};

// Глобальные
new MySQL:connection;

// -------------------------------
// Вспомогательные функции
// -------------------------------

stock GetPlayerNameEx(playerid, name[] = "", len = sizeof(name))
{
    GetPlayerName(playerid, name, len);
    return name;
}

stock ShuffleQuestions(playerid)
{
    new used[MAX_IQ_QUESTIONS] = {0};
    new index;
    for (new i = 0; i < QUESTIONS_TO_ASK; i++)
    {
        do index = random(MAX_IQ_QUESTIONS);
        while (used[index]);
        used[index] = 1;
        PlayerSelectedQuestions[playerid][i] = index;
    }
}

stock ShowIQQuestion(playerid)
{
    new qNum = PlayerCurrentQuestion[playerid];
    if (qNum >= QUESTIONS_TO_ASK) return;

    new qIndex = PlayerSelectedQuestions[playerid][qNum];
    new title[64], content[256];
    format(title, sizeof(title), "Тест на IQ — Вопрос %d/%d", qNum + 1, QUESTIONS_TO_ASK);
    format(content, sizeof(content), "%s\n\n%s", IQ_Questions[qIndex][0], IQ_Options[qIndex]);

    ShowPlayerDialog(playerid, DIALOG_IQ_TEST_BASE + qNum, DIALOG_STYLE_MSGBOX, title, content, "Ответить", "Выход");
}

// -------------------------------
// Основные события
// -------------------------------

public OnGameModeInit()
{
    connection = mysql_connect("localhost", "samp", "root", "password");
    if (mysql_errno(connection))
    {
        print("❌ MySQL не подключён!");
        SendRconCommand("exit");
    }
    print("✅ MySQL подключён.");
    return 1;
}

public OnPlayerConnect(playerid)
{
    ResetPlayerData(playerid);
    new name[24];
    GetPlayerNameEx(playerid, name);
    mysql_real_escape_string(name, name);

    new query[128];
    format(query, sizeof(query), "SELECT `id`, `iq` FROM players WHERE name='%s'", name);
    mysql_tquery(connection, query, "CheckPlayerExist", "i", playerid);
    return 1;
}

forward CheckPlayerExist(playerid, MYSQL_ROW:row);
public CheckPlayerExist(playerid, MYSQL_ROW:row)
{
    if (cache_num_rows() == 0)
    {
        // Новый игрок → регистрация
        PlayerData[playerid][pRegistered] = 0;
        ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT,
            "Регистрация", "Введите пароль (мин. 6 символов):", "Зарегистрироваться", "Выход");
    }
    else
    {
        // Существующий → вход
        PlayerData[playerid][pRegistered] = 1;
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD,
            "Авторизация", "Введите ваш пароль:", "Войти", "Выход");
    }
    return 1;
}

public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
    if (!response) { Kick(playerid); return 1; }

    switch (dialogid)
    {
        case DIALOG_REGISTER:
        {
            if (strlen(inputtext) < 6)
            {
                SendClientMessage(playerid, COLOR_RED, "Пароль должен быть от 6 символов!");
                ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_INPUT,
                    "Регистрация", "Введите пароль:", "Зарегистрироваться", "Выход");
                return 1;
            }

            SHA256_PassHash(inputtext, "", PlayerTempPass[playerid], 65);
            PlayerData[playerid][pInIqTest] = 1;

            ShuffleQuestions(playerid);
            PlayerIQScore[playerid] = 0;
            PlayerCurrentQuestion[playerid] = 0;
            ShowIQQuestion(playerid);
        }

        case DIALOG_LOGIN:
        {
            if (strlen(inputtext) < 1)
            {
                ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD,
                    "Авторизация", "Введите ваш пароль:", "Войти", "Выход");
                return 1;
            }

            new hash[65];
            SHA256_PassHash(inputtext, "", hash, 65);

            new name[24];
            GetPlayerNameEx(playerid, name);
            mysql_real_escape_string(name, name);
            mysql_real_escape_string(hash, hash);

            new query[256];
            format(query, sizeof(query),
                "SELECT `id`, `iq` FROM players WHERE name='%s' AND password='%s'",
                name, hash
            );
            mysql_tquery(connection, query, "OnLoginAttempt", "i", playerid);
        }

        // Обработка вопросов
        case DIALOG_IQ_TEST_BASE .. DIALOG_IQ_TEST_BASE + QUESTIONS_TO_ASK - 1:
        {
            new qNum = dialogid - DIALOG_IQ_TEST_BASE;
            new qIndex = PlayerSelectedQuestions[playerid][qNum];
            new correctAnswer[2] = {0};
            format(correctAnswer, sizeof(correctAnswer), "%c", IQ_Questions[qIndex][1][0]);

            if (!strcmp(inputtext, correctAnswer, true))
                PlayerIQScore[playerid] += 20;

            PlayerCurrentQuestion[playerid]++;

            if (PlayerCurrentQuestion[playerid] < QUESTIONS_TO_ASK)
            {
                ShowIQQuestion(playerid);
            }
            else
            {
                // Завершение теста
                new iq = PlayerIQScore[playerid];
                if (iq <= 40)
                {
                    SendClientMessage(playerid, COLOR_RED, sprintf("Ваш IQ: %d. Требуется > 40. Доступ запрещён.", iq));
                    Kick(playerid);
                    return 1;
                }

                // Сохраняем игрока
                new name[24], query[300];
                GetPlayerNameEx(playerid, name);
                mysql_real_escape_string(name, name);
                mysql_real_escape_string(PlayerTempPass[playerid], PlayerTempPass[playerid]);

                format(query, sizeof(query),
                    "INSERT INTO players (name, password, iq) VALUES ('%s', '%s', %d)",
                    name, PlayerTempPass[playerid], iq
                );
                mysql_tquery(connection, query, "OnPlayerRegistered", "ii", playerid, iq);
            }
        }
    }
    return 1;
}

forward OnPlayerRegistered(playerid, iq);
public OnPlayerRegistered(playerid, iq)
{
    PlayerData[playerid][pLogged] = 1;
    PlayerData[playerid][pSpawned] = 0;
    SendClientMessage(playerid, COLOR_GREEN, sprintf("✅ Регистрация успешна! Ваш IQ: %d.", iq));
    SpawnPlayerForGame(playerid);
}

forward OnLoginAttempt(playerid, MYSQL_ROW:row);
public OnLoginAttempt(playerid, MYSQL_ROW:row)
{
    if (cache_num_rows() == 0)
    {
        SendClientMessage(playerid, COLOR_RED, "Неверный пароль!");
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD,
            "Авторизация", "Попробуйте снова:", "Войти", "Выход");
        return 1;
    }

    new iq = cache_get_field_content_int(0, "iq");
    PlayerData[playerid][pLogged] = 1;
    PlayerData[playerid][pSpawned] = 0;
    SendClientMessage(playerid, COLOR_GREEN, sprintf("✅ Добро пожаловать! Ваш IQ: %d.", iq));
    SpawnPlayerForGame(playerid);
    return 1;
}

stock SpawnPlayerForGame(playerid)
{
    if (PlayerData[playerid][pSpawned]) return;

    // Пример спавна
    SetSpawnInfo(playerid, 0, 0, 1958.33, 1343.12, 15.36, 0.0, -1, -1, -1, -1, -1, -1);
    SpawnPlayer(playerid);
    PlayerData[playerid][pSpawned] = 1;
}

stock ResetPlayerData(playerid)
{
    PlayerData[playerid][pLogged] = 0;
    PlayerData[playerid][pRegistered] = 0;
    PlayerData[playerid][pTryingLogin] = 0;
    PlayerData[playerid][pInIqTest] = 0;
    PlayerData[playerid][pSpawned] = 0;
    PlayerIQScore[playerid] = 0;
    PlayerCurrentQuestion[playerid] = 0;
    PlayerTempPass[playerid][0] = EOS;
}

public OnPlayerSpawn(playerid)
{
    if (!PlayerData[playerid][pLogged])
    {
        TogglePlayerControllable(playerid, 0);
        SendClientMessage(playerid, COLOR_YELLOW, "⚠️ Вы не авторизованы! Ожидайте...");
        return 0;
    }
    return 1;
}

public OnPlayerRequestSpawn(playerid)
{
    if (PlayerData[playerid][pLogged])
    {
        SpawnPlayerForGame(playerid);
        return 1;
    }
    return 0;
}

Специально для любимого форума и любимых пользователей ❤️
 
Сверху