Глава 7.2: Модульные / плагинные системы
Главная | << Предыдущая: Паттерн Singleton | Модульные / плагинные системы | Следующая: Паттерны RPC >>
Введение
Каждый серьёзный фреймворк для моддинга DayZ использует модульную или плагинную систему для организации кода в самостоятельные блоки с определёнными хуками жизненного цикла. Вместо того чтобы разбрасывать логику инициализации по modded-классам миссий, модули регистрируются в центральном менеджере, который рассылает события жизненного цикла --- OnInit, OnMissionStart, OnUpdate, OnMissionFinish --- каждому модулю в предсказуемом порядке.
В этой главе рассматриваются четыре реальных подхода: CF_ModuleCore из Community Framework, PluginBase / ConfigurablePlugin из VPP, регистрация на основе атрибутов из Dabs Framework и собственный статический менеджер модулей. Каждый решает одну и ту же задачу по-своему; понимание всех четырёх поможет вам выбрать подходящий паттерн для своего мода или корректно интегрироваться с существующим фреймворком.
Содержание
- Зачем нужны модули?
- CF_ModuleCore (COT / Expansion)
- VPP PluginBase / ConfigurablePlugin
- Регистрация через атрибуты (Dabs)
- Собственный статический менеджер модулей
- Жизненный цикл модуля: универсальный контракт
- Лучшие практики проектирования модулей
- Сравнительная таблица
Зачем нужны модули?
Без модульной системы DayZ-мод обычно превращается в монолитный modded-класс MissionServer или MissionGameplay, который разрастается до неуправляемых размеров:
// ПЛОХО: всё запихнуто в один modded-класс
modded class MissionServer
{
override void OnInit()
{
super.OnInit();
InitLootSystem();
InitVehicleTracker();
InitBanManager();
InitWeatherController();
InitAdminPanel();
InitKillfeedHUD();
// ... ещё 20 систем
}
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
TickLootSystem(timeslice);
TickVehicleTracker(timeslice);
TickWeatherController(timeslice);
// ... ещё 20 тиков
}
};Модульная система заменяет это единой стабильной точкой подключения:
modded class MissionServer
{
override void OnInit()
{
super.OnInit();
MyModuleManager.Register(new LootModule());
MyModuleManager.Register(new VehicleModule());
MyModuleManager.Register(new WeatherModule());
}
override void OnMissionStart()
{
super.OnMissionStart();
MyModuleManager.OnMissionStart(); // Рассылает вызов всем модулям
}
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
MyModuleManager.OnServerUpdate(timeslice); // Рассылает вызов всем модулям
}
};Каждый модуль --- это независимый класс со своим файлом, своим состоянием и своими хуками жизненного цикла. Добавление новой функции означает добавление нового модуля --- а не редактирование класса миссии на 3000 строк.
CF_ModuleCore (COT / Expansion)
Community Framework (CF) предоставляет наиболее широко используемую модульную систему в экосистеме моддинга DayZ. И COT, и Expansion построены на ней.
Как это работает
- Вы объявляете класс модуля, наследующий один из базовых классов CF
- Регистрируете его в
config.cppв секцииCfgPatches/CfgMods CF_ModuleCoreManagerиз CF автоматически обнаруживает и создаёт экземпляры всех зарегистрированных классов модулей при запуске- События жизненного цикла рассылаются автоматически
Базовые классы модулей
CF предоставляет три базовых класса, соответствующих слоям скриптов DayZ:
| Базовый класс | Слой | Типичное использование |
|---|---|---|
CF_ModuleGame | 3_Game | Ранняя инициализация, регистрация RPC, классы данных |
CF_ModuleWorld | 4_World | Взаимодействие с сущностями, игровые системы |
CF_ModuleMission | 5_Mission | UI, HUD, хуки уровня миссии |
Пример: модуль CF
class MyLootModule : CF_ModuleWorld
{
// CF вызывает один раз при инициализации модуля
override void OnInit()
{
super.OnInit();
// Регистрация обработчиков RPC, выделение структур данных
}
// CF вызывает при старте миссии
override void OnMissionStart(Class sender, CF_EventArgs args)
{
super.OnMissionStart(sender, args);
// Загрузка конфигов, начальная расстановка лута
}
// CF вызывает каждый кадр на сервере
override void OnUpdate(Class sender, CF_EventArgs args)
{
super.OnUpdate(sender, args);
// Обновление таймеров респауна лута
}
// CF вызывает при завершении миссии
override void OnMissionFinish(Class sender, CF_EventArgs args)
{
super.OnMissionFinish(sender, args);
// Сохранение состояния, освобождение ресурсов
}
};Доступ к модулю CF
// Получить ссылку на работающий модуль по типу
MyLootModule lootMod;
CF_Modules<MyLootModule>.Get(lootMod);
if (lootMod)
{
lootMod.ForceRespawn();
}Ключевые характеристики
- Автообнаружение: модули создаются CF на основе объявлений в
config.cpp--- никаких ручных вызововnew - Аргументы событий: хуки жизненного цикла получают
CF_EventArgsс контекстными данными - Зависимость от CF: ваш мод требует Community Framework в качестве зависимости
- Широкая поддержка: если ваш мод нацелен на серверы с COT или Expansion, CF уже присутствует
VPP PluginBase / ConfigurablePlugin
VPP Admin Tools использует архитектуру плагинов, где каждый инструмент администратора --- это класс плагина, зарегистрированный в центральном менеджере.
Базовый плагин
// Паттерн VPP (упрощённо)
class PluginBase : Managed
{
void OnInit();
void OnUpdate(float dt);
void OnDestroy();
// Идентичность плагина
string GetPluginName();
bool IsServerOnly();
};ConfigurablePlugin
VPP расширяет базу вариантом с поддержкой конфигурации, который автоматически загружает/сохраняет настройки:
class ConfigurablePlugin : PluginBase
{
// VPP автоматически загружает из JSON при инициализации
ref PluginConfigBase m_Config;
override void OnInit()
{
super.OnInit();
LoadConfig();
}
void LoadConfig()
{
string path = "$profile:VPPAdminTools/" + GetPluginName() + ".json";
if (FileExist(path))
{
JsonFileLoader<PluginConfigBase>.JsonLoadFile(path, m_Config);
}
}
void SaveConfig()
{
string path = "$profile:VPPAdminTools/" + GetPluginName() + ".json";
JsonFileLoader<PluginConfigBase>.JsonSaveFile(path, m_Config);
}
};Регистрация
VPP регистрирует плагины в modded-версии MissionServer.OnInit():
// Паттерн VPP
GetPluginManager().RegisterPlugin(new VPPESPPlugin());
GetPluginManager().RegisterPlugin(new VPPTeleportPlugin());
GetPluginManager().RegisterPlugin(new VPPWeatherPlugin());Ключевые характеристики
- Ручная регистрация: каждый плагин явно создаётся через
newи регистрируется - Интеграция с конфигурацией:
ConfigurablePluginобъединяет управление конфигурацией с жизненным циклом модуля - Самодостаточность: нет зависимости от CF; менеджер плагинов VPP --- это собственная система
- Чёткое владение: менеджер плагинов хранит
refна все плагины, контролируя их время жизни
Регистрация через атрибуты (Dabs)
Dabs Framework (используется в Dabs Framework Admin Tools) применяет более современный подход: атрибуты в стиле C# для авторегистрации.
Концепция
Вместо ручной регистрации модулей вы аннотируете класс атрибутом, и фреймворк обнаруживает его при запуске с помощью рефлексии:
// Паттерн Dabs (концептуальный)
[CF_RegisterModule(DabsAdminESP)]
class DabsAdminESP : CF_ModuleWorld
{
override void OnInit()
{
super.OnInit();
// ...
}
};Атрибут CF_RegisterModule указывает менеджеру модулей CF автоматически создать экземпляр этого класса. Ручной вызов Register() не нужен.
Как работает обнаружение
При запуске CF сканирует все загруженные классы скриптов на наличие атрибута регистрации. Для каждого совпадения он создаёт экземпляр и добавляет его в менеджер модулей. Это происходит до того, как OnInit() будет вызван для любого модуля.
Ключевые характеристики
- Нулевой шаблонный код: никакого кода регистрации в классах миссий
- Декларативность: сам класс объявляет, что он является модулем
- Зависимость от CF: работает только с обработкой атрибутов Community Framework
- Обнаружимость: все модули можно найти поиском по атрибуту в кодовой базе
Собственный статический менеджер модулей
Этот подход использует явную регистрацию со статическим классом-менеджером. Экземпляра менеджера не существует --- он состоит полностью из статических методов и статического хранилища. Это полезно, когда вы хотите нулевых зависимостей от внешних фреймворков.
Базовые классы модулей
// Базовый: хуки жизненного цикла
class MyModuleBase : Managed
{
bool IsServer(); // Переопределите в подклассе
bool IsClient(); // Переопределите в подклассе
string GetModuleName();
void OnInit();
void OnMissionStart();
void OnMissionFinish();
};
// Серверный модуль: добавляет OnUpdate + события игроков
class MyServerModule : MyModuleBase
{
void OnUpdate(float dt);
void OnPlayerConnect(PlayerIdentity identity);
void OnPlayerDisconnect(PlayerIdentity identity, string uid);
};
// Клиентский модуль: добавляет OnUpdate
class MyClientModule : MyModuleBase
{
void OnUpdate(float dt);
};Регистрация
Модули регистрируются явно, обычно из modded-классов миссий:
// В modded MissionServer.OnInit():
MyModuleManager.Register(new MyMissionServerModule());
MyModuleManager.Register(new MyAIServerModule());Диспетчеризация жизненного цикла
Modded-классы миссий вызывают MyModuleManager в каждой точке жизненного цикла:
modded class MissionServer
{
override void OnMissionStart()
{
super.OnMissionStart();
MyModuleManager.OnMissionStart();
}
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
MyModuleManager.OnServerUpdate(timeslice);
}
override void OnMissionFinish()
{
MyModuleManager.OnMissionFinish();
MyModuleManager.Cleanup();
super.OnMissionFinish();
}
};Безопасность на Listen-серверах
Базовые классы модулей обеспечивают критически важный инвариант: MyServerModule возвращает true из IsServer() и false из IsClient(), тогда как MyClientModule делает наоборот. Менеджер использует эти флаги, чтобы избежать двойной диспетчеризации событий жизненного цикла на listen-серверах (где MissionServer и MissionGameplay работают в одном процессе).
Базовый класс MyModuleBase возвращает true из обоих методов --- именно поэтому кодовая база предостерегает от прямого наследования от него.
Ключевые характеристики
- Нулевые зависимости: ни CF, ни внешних фреймворков
- Статический менеджер:
GetInstance()не нужен; полностью статический API - Явная регистрация: полный контроль над тем, что регистрируется и когда
- Безопасность на listen-сервере: типизированные подклассы предотвращают двойную диспетчеризацию
- Централизованная очистка:
MyModuleManager.Cleanup()уничтожает все модули и основные таймеры
Жизненный цикл модуля: универсальный контракт
Несмотря на различия в реализации, все четыре фреймворка следуют одному и тому же контракту жизненного цикла:
┌─────────────────────────────────────────────────────┐
│ Регистрация / Обнаружение │
│ Экземпляр модуля создаётся и регистрируется │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OnInit() │
│ Однократная настройка: выделение коллекций, │
│ регистрация RPC │
│ Вызывается один раз для каждого модуля │
│ после регистрации │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OnMissionStart() │
│ Миссия активна: загрузка конфигов, запуск таймеров, │
│ подписка на события, начальная расстановка сущностей │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OnUpdate(float dt) [повторяется каждый кадр] │
│ Покадровый тик: обработка очередей, обновление │
│ таймеров, проверка условий, продвижение автоматов │
│ состояний │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OnMissionFinish() │
│ Завершение: сохранение состояния, отписка от │
│ событий, очистка коллекций, обнуление ссылок │
└─────────────────────────────────────────────────────┘Правила
- OnInit вызывается до OnMissionStart. Никогда не загружайте конфиги и не спавните сущности в
OnInit()--- мир может быть ещё не готов. - OnUpdate получает дельту времени. Всегда используйте
dtдля логики, основанной на времени, никогда не предполагайте фиксированную частоту кадров. - OnMissionFinish должен очистить всё. Каждая
ref-коллекция должна быть очищена. Каждая подписка на события должна быть удалена. Каждый синглтон должен быть уничтожен. Это единственная надёжная точка завершения. - Модули не должны зависеть от порядка инициализации друг друга. Если модулю A нужен модуль B, используйте ленивый доступ (
GetModule()), а не полагайтесь на то, что B был зарегистрирован первым.
Лучшие практики проектирования модулей
1. Один модуль --- одна ответственность
Модуль должен владеть ровно одной предметной областью. Если вы пишете VehicleAndWeatherAndLootModule, разделите его.
// ХОРОШО: Сфокусированные модули
class MyLootModule : MyServerModule { ... }
class MyVehicleModule : MyServerModule { ... }
class MyWeatherModule : MyServerModule { ... }
// ПЛОХО: Бог-модуль
class MyEverythingModule : MyServerModule { ... }2. OnUpdate должен быть дешёвым
OnUpdate выполняется каждый кадр. Если ваш модуль делает тяжёлую работу (файловый ввод-вывод, сканирование мира, поиск пути), выполняйте её по таймеру или разбивайте на кадры:
class MyCleanupModule : MyServerModule
{
protected float m_CleanupTimer;
protected const float CLEANUP_INTERVAL = 300.0; // Каждые 5 минут
override void OnUpdate(float dt)
{
m_CleanupTimer += dt;
if (m_CleanupTimer >= CLEANUP_INTERVAL)
{
m_CleanupTimer = 0;
RunCleanup();
}
}
};3. Регистрируйте RPC в OnInit, а не в OnMissionStart
Обработчики RPC должны быть на месте до того, как любой клиент сможет отправить сообщение. OnInit() выполняется при регистрации модуля, которая происходит рано в процессе настройки миссии. OnMissionStart() может быть слишком поздно, если клиенты подключаются быстро.
class MyModule : MyServerModule
{
override void OnInit()
{
super.OnInit();
MyRPC.Register("MyMod", "RPC_DoThing", this, MyRPCSide.SERVER);
}
void RPC_DoThing(PlayerIdentity sender, Object target, ParamsReadContext ctx)
{
// Обработка RPC
}
};4. Используйте менеджер модулей для межмодульного доступа
Не храните прямые ссылки на другие модули. Используйте поиск через менеджер:
// ХОРОШО: Слабое связывание через менеджер
MyModuleBase mod = MyModuleManager.GetModule("MyAIServerModule");
MyAIServerModule aiMod;
if (Class.CastTo(aiMod, mod))
{
aiMod.PauseSpawning();
}
// ПЛОХО: Прямая статическая ссылка создаёт жёсткое связывание
MyAIServerModule.s_Instance.PauseSpawning();5. Защищайтесь от отсутствующих зависимостей
Не каждый сервер запускает каждый мод. Если ваш модуль опционально интегрируется с другим модом, используйте проверки препроцессора:
override void OnMissionStart()
{
super.OnMissionStart();
#ifdef MYMOD_AI
MyEventBus.OnMissionStarted.Insert(OnAIMissionStarted);
#endif
}6. Логируйте события жизненного цикла модулей
Логирование упрощает отладку. Каждый модуль должен логировать свою инициализацию и завершение:
override void OnInit()
{
super.OnInit();
MyLog.Info("MyModule", "Initialized");
}
override void OnMissionFinish()
{
MyLog.Info("MyModule", "Shutting down");
// Очистка...
}Сравнительная таблица
| Функция | CF_ModuleCore | VPP Plugin | Атрибуты Dabs | Собственный модуль |
|---|---|---|---|---|
| Обнаружение | config.cpp + авто | Ручная Register() | Сканирование атрибутов | Ручная Register() |
| Базовые классы | Game / World / Mission | PluginBase / ConfigurablePlugin | CF_ModuleWorld + атрибут | ServerModule / ClientModule |
| Зависимости | Требует CF | Самодостаточный | Требует CF | Самодостаточный |
| Безопасность listen-сервера | CF обрабатывает | Ручная проверка | CF обрабатывает | Типизированные подклассы |
| Интеграция конфигов | Отдельно | Встроена в ConfigurablePlugin | Отдельно | Через MyConfigManager |
| Диспетчеризация обновлений | Автоматическая | Менеджер вызывает OnUpdate | Автоматическая | Менеджер вызывает OnUpdate |
| Очистка | CF обрабатывает | Ручная OnDestroy | CF обрабатывает | MyModuleManager.Cleanup() |
| Межмодульный доступ | CF_Modules<T>.Get() | GetPluginManager().Get() | CF_Modules<T>.Get() | MyModuleManager.GetModule() |
Выбирайте подход, соответствующий профилю зависимостей вашего мода. Если вы уже зависите от CF, используйте CF_ModuleCore. Если хотите нулевых внешних зависимостей, создавайте собственную систему по образцу собственного менеджера или паттерна VPP.
Совместимость и влияние
- Мульти-мод: Несколько модов могут регистрировать свои модули в одном менеджере (CF, VPP или собственном). Коллизии имён возникают только если два мода регистрируют один и тот же тип класса --- используйте уникальные имена классов с префиксом вашего мода.
- Порядок загрузки: CF автоматически обнаруживает модули из
config.cpp, поэтому порядок загрузки следуетrequiredAddons. Собственные менеджеры регистрируют модули вOnInit(), где порядок определяется цепочкойmodded class. Модули не должны зависеть от порядка регистрации --- используйте паттерны ленивого доступа. - Listen-сервер: На listen-серверах
MissionServerиMissionGameplayработают в одном процессе. Если менеджер модулей рассылаетOnUpdateиз обоих, модули получают двойные тики. Используйте типизированные подклассы (ServerModule/ClientModule), которые возвращаютIsServer()илиIsClient(), чтобы предотвратить это. - Производительность: Диспетчеризация модулей добавляет одну итерацию цикла на каждый зарегистрированный модуль за каждый вызов жизненного цикла. При 10--20 модулях это ничтожно мало. Убедитесь, что методы
OnUpdateотдельных модулей дешёвы (см. главу 7.7). - Миграция: При обновлении версий DayZ модульные системы стабильны, пока API базового класса (
CF_ModuleWorld,PluginBaseи т.д.) не изменяется. Зафиксируйте версию зависимости CF, чтобы избежать поломок.
Распространённые ошибки
| Ошибка | Последствия | Исправление |
|---|---|---|
Отсутствие очистки в OnMissionFinish модуля | Коллекции, таймеры и подписки на события переживают перезапуск миссии, вызывая устаревшие данные или краши | Переопределите OnMissionFinish, очистите все ref-коллекции, отпишитесь от всех событий |
| Двойная диспетчеризация событий жизненного цикла на listen-серверах | Серверные модули выполняют клиентскую логику и наоборот; дублирование спавнов, двойная отправка RPC | Используйте проверки IsServer() / IsClient() или типизированные подклассы модулей, обеспечивающие разделение |
Регистрация RPC в OnMissionStart вместо OnInit | Клиенты, подключающиеся во время настройки миссии, могут отправить RPC до готовности обработчиков --- сообщения молча отбрасываются | Всегда регистрируйте обработчики RPC в OnInit(), который выполняется при регистрации модуля до подключения любого клиента |
| Один «бог-модуль», обрабатывающий всё | Невозможно отладить, протестировать или расширить; конфликты слияния при работе нескольких разработчиков | Разделите на сфокусированные модули с единственной ответственностью |
Хранение прямой ref на экземпляр другого модуля | Создаёт жёсткое связывание и потенциальные утечки памяти из-за циклических ссылок | Используйте поиск через менеджер модулей (GetModule(), CF_Modules<T>.Get()) для межмодульного доступа |
Теория vs Практика
| Учебник говорит | Реальность DayZ |
|---|---|
| Обнаружение модулей должно быть автоматическим через рефлексию | Рефлексия Enforce Script ограничена; обнаружение через config.cpp (CF) или явные вызовы Register() --- единственные надёжные подходы |
| Модули должны заменяться «на горячую» во время выполнения | DayZ не поддерживает горячую перезагрузку скриптов; модули живут весь жизненный цикл миссии |
| Используйте интерфейсы для контрактов модулей | В Enforce Script нет ключевого слова interface; используйте виртуальные методы базового класса (override) |
| Внедрение зависимостей разъединяет модули | Фреймворка DI не существует; используйте поиск через менеджер и #ifdef-проверки для опциональных межмодульных зависимостей |
<< Предыдущая: Паттерн Singleton | Главная | Следующая: Паттерны RPC >>
