Архитектура - NikitaFoxze/Offensive-Core GitHub Wiki

HomeАрхитектура

[!TIP] Этот раздел описывает ключевые архитектурные решения проекта, включая модульность, структуру файлов и организацию кода.

Модульность

Всё, о чём здесь говорится - итог кропотливого и напряжённого труда. Архитектуру проекта уже будет сложно полностью переделать и очень долго по времени. Результат этой работы представляется весьма удачным и легко адаптируемым.

Эволюция архитектуры

Изначально весь код проекта разрабатывался в единственном .pwn файле. Однако по мере роста (добавления различных режимов, сложных систем и дополнительных возможностей) возникла необходимость в модульной архитектуре. В какой-то момент стало очень тяжело продолжать писать код в одном файле.

Преимущества модульного подхода

  • Упрощение отладки: Проблемы локализуются в конкретных модулях, а не в едином файле
  • Гибкость: Возможность подключения модулей (например, weapon-config, nex-ac) через механизм "хуков"
  • Масштабируемость: Упрощенное добавление нового функционала

В процессе разработки появлялось ощущение, что Pawn вообще не создан для модульности, поэтому в коде сервера есть специфичные моменты, но плюсы перевешивают минусы.

Структура файлов

Эта структура кажется наиболее практичной и гибкой. Папки легко перестраивать, а имена файлов позволяют создавать новые модули, если потребуется.

Папки

Название папки Назначение
sources Корневая директория проекта
core Основная папка с базовыми модулями (админка, античит, БД, ...)
modes Содержит поддиректории с игровыми режимами (DM, TDM, ...)
modules Дополнительные модули (дина, промокоды, торговая площадка)

На данный момент это все основные папки с системами.

Файлы

Название файла Назначение
name_head.inc Файл содержащий основные макросы, функции, ...
name_td.inc Файл содержащий все TextDraw's системы
name_main.pwn Файл содержащий весь функционал системы

Сейчас это ключевые файлы в любой системе. Если вы хотите изменить архитектуру, просто создайте новые файлы. Например, можно создать файл name_dialogs.inc, который будет содержать все диалоги. Не забудьте подключить его.

Пояснение

[!IMPORTANT] Почти все файлы подключаются в offensive-core.pwn в папке gamemodes. Исключением являются файлы в папке modes, где находятся все режимы и их функционал. Они подключаются в файлах includes_head.inc - для заголовочных и includes_main.inc - для основных файлов. Эти файлы также находятся в папке modes.

Все файлы в папке sources связаны между собой так или иначе, они имеют прямое влияние на геймплей игрока и не только, также "хукают" некоторые колбэки, но не все, некоторые объявляются как функция (например, stock Modes_OnGameModeInit()) и вызываются уже в файле offensive-core.pwn, чтобы контролировать очередность вызовов этих колбэков.

Например, нам нужно, чтобы колбэк public OnGameModeInit() в файле database_main.pwn вызывался первым, а public OnGameModeExit() - последним. Для этого мы создаем функции stock MySQL_OnGameModeInit() и stock MySQL_OnGameModeExit() в самом файле database_main.pwn и вызываем их в offensive-core.pwn как задумали.

Структура кода

В каждом файле имеются заголовки и подзаголовки блоков кода. Эти блоки кода могут быть, а могут и не быть, но у них есть очерёдность, которая соблюдается в каждом файле. В заголовочных (.inc) и в главных (.pwn) файлах они немного разнятся, но суть одна. Блоки помогают ориентироваться в коде, которого может быть 6000 строк и более.

Блоки кода

Например, сокращённая структура в файле player_main.pwn.

/*
 * |>======================<|
 * |   About: Player main   |
 * |   Author: Foxze        |
 * |>======================<|
 */

/*
 * |>--------------------------------------------<|
 * |                  Functions                   |
 * |>--------------------------------------------<|
 * Public:
	- OnPlayerConnect(playerid)
	- OnPlayerDisconnect(playerid, reason)
 * Stock:
	- GivePlayerRank(playerid, rank)
 * |>--------------------------------------------<|
 * |                  Enums                       |
 * |>--------------------------------------------<|
	- E_PLAYER_INFO
 * |>--------------------------------------------<|
 * |                  Commands                    |
 * |>--------------------------------------------<|
	- menu(playerid)
 * |>--------------------------------------------<|
 * |                  Dialogs                     |
 * |>--------------------------------------------<|
	- Login
 * |>--------------------------------------------<|
 * |                  Interfaces                  |
 * |>--------------------------------------------<|
	- (None)
 */

#if defined _INC_PLAYER_MAIN
	#endinput
#endif
#define _INC_PLAYER_MAIN

/*
 * |>-------------<|
 * |     Enums     |
 * |>-------------<|
 */

enum E_PLAYER_INFO {
	ORM:e_ORM_ID,
	e_ID,
	e_Name[MAX_PLAYER_NAME],
	e_Password[MAX_LENGTH_PASSWORD]
}

/*
 * |>------------<|
 * |     Vars     |
 * |>------------<|
 */

static
	pInfo[MAX_PLAYERS][E_PLAYER_INFO];

/*
 * |>-----------------<|
 * |     Functions     |
 * |>-----------------<|
 */

stock GivePlayerRank(playerid, rank)
{
	// Код...
	return 1;
}

/*
 * |>-------------<|
 * |     Reset     |
 * |>-------------<|
 */

static ResetPlayerGlobalData(playerid)
{
	pInfo[playerid][e_ORM_ID]		= ORM:0;
	pInfo[playerid][e_ID]			= -1;

	pInfo[playerid][e_Name][0]		=
	pInfo[playerid][e_Password][0]		= EOS;
	return 1;
}

/*
 * |>----------------<|
 * |     Commands     |
 * |>----------------<|
 */

CMD:menu(playerid)
{
	// Код...
}

/*
 * |>---------------<|
 * |     Dialogs     |
 * |>---------------<|
 */

DialogCreate:Login(playerid)
{
	// Код...
	return 1;
}

DialogResponse:Login(playerid, response, listitem, inputtext[])
{
	// Код...
	return 1;
}

/*
 * |>-------------<|
 * |     MySQL     |
 * |>-------------<|
 */

// Различные MySQL запросы, например, авторизация и т.д.

/*
 * |>-----------------<|
 * |     Callbacks     |
 * |>-----------------<|
 */

/*
 * |>-------------------<|
 * |   OnPlayerConnect   |
 * |>-------------------<|
 */

stock Player_OnPlayerConnect(playerid)
{
	new playerLastIP[MAX_LENGTH_IP], playerName[MAX_PLAYER_NAME];

	GetPlayerIp(playerid, playerLastIP, MAX_LENGTH_IP);
	GetPlayerName(playerid, playerName, MAX_PLAYER_NAME);
	return 0;
}

/*
 * |>----------------------<|
 * |   OnPlayerDisconnect   |
 * |>----------------------<|
 */

stock Player_OnPlayerDisconnect(playerid, reason)
{
	// Код...
	return 0;
}

/*
 * |>-------<|
 * |   ALS   |
 * |>-------<|
 */

// Например:
#if defined _ALS_OnPlayerConnect
	#undef OnPlayerConnect
#else
	#define _ALS_OnPlayerConnect
#endif
#define OnPlayerConnect Player_OnPlayerConnect
#if defined Player_OnPlayerConnect
	forward Player_OnPlayerConnect(playerid);
#endif

Пояснение

Обратите внимание на стиль именования переменных и функций. Глобальные и локальные переменные и функции следуют PascalCase, за исключением локальных переменных в самих функциях, которые начинаются со строчной буквы (например, playerName[MAX_PLAYER_NAME]), а также аргументов функций. Переменные с суффиксом "id" в конце, такие как playerid или modeid, также являются исключением.

Приставки имеют функции только в папке modes, например, создание сессии в TDM режиме - TDM_CreateSession(sessionid), это выглядит более читабельным и удобным.

Вы, наверное, заметили, что функция Player_OnPlayerConnect(playerid) хоть и является обычной функцией, но числиться колбэком. Это сделано для удобства, чтобы понимать, что эта функция вызывается в колбэке где-то в offensive-core.pwn. Последний "хук" написан лишь для примера блока.

Элемент Стиль Пример
Глобальные/Локальные переменные PascalCase PlayerTimerSpawn[MAX_PLAYERS]
Локальные переменные в функциях camelCase playerName[MAX_PLAYER_NAME]
Аргументы в функциях camelCase const stringDatetime[]
Глобальные/Локальные функции PascalCase GivePlayerRank
Функции в папке modes Префикс_Функция TDM_CreateSession

Итоги

Мы обсудили ключевые аспекты: структуру файлов и папок, код и стиль его написания. Важно помнить, что все эти элементы применяются по всему проекту. Их нужно соблюдать. Да, есть альтернативные подходы, но в этом проекте используются именно эти.