- Янв
- 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)
- Система проверяет наличие игрока в таблице players по нику.
- Если игрок новый → показывается диалог регистрации.
- Если игрок известен → показывается диалог входа.
Регистрация
- Игрок вводит пароль (мин. 6 символов).
- Запускается IQ-тест: 5 случайных вопросов из пула 10.
- За каждый правильный ответ — +20 баллов (макс. 100).
- Если итоговый IQ ≤ 40 → игрок кикается.
- Если IQ > 40 → данные сохраняются в БД, игрок спавнится.
Авторизация
- Игрок вводит пароль.
- Система хеширует введённый пароль и сравнивает с записью в БД.
- При совпадении — игрок помечается как авторизованный и спавнится.
- При ошибке — повторный запрос пароля.
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.dll | MySQL-плагин (BlueG) |
| plugins/sha256.dll | Плагин хеширования |
| pawno/include/sha256.inc | Include для 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;
}
Специально для любимого форума и любимых пользователей