Skip to content

Глава 7.2: Модульные / плагинные системы

Главная | << Предыдущая: Паттерн Singleton | Модульные / плагинные системы | Следующая: Паттерны RPC >>


Введение

Каждый серьёзный фреймворк для моддинга DayZ использует модульную или плагинную систему для организации кода в самостоятельные блоки с определёнными хуками жизненного цикла. Вместо того чтобы разбрасывать логику инициализации по modded-классам миссий, модули регистрируются в центральном менеджере, который рассылает события жизненного цикла --- OnInit, OnMissionStart, OnUpdate, OnMissionFinish --- каждому модулю в предсказуемом порядке.

В этой главе рассматриваются четыре реальных подхода: CF_ModuleCore из Community Framework, PluginBase / ConfigurablePlugin из VPP, регистрация на основе атрибутов из Dabs Framework и собственный статический менеджер модулей. Каждый решает одну и ту же задачу по-своему; понимание всех четырёх поможет вам выбрать подходящий паттерн для своего мода или корректно интегрироваться с существующим фреймворком.


Содержание


Зачем нужны модули?

Без модульной системы DayZ-мод обычно превращается в монолитный modded-класс MissionServer или MissionGameplay, который разрастается до неуправляемых размеров:

c
// ПЛОХО: всё запихнуто в один 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 тиков
    }
};

Модульная система заменяет это единой стабильной точкой подключения:

c
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 построены на ней.

Как это работает

  1. Вы объявляете класс модуля, наследующий один из базовых классов CF
  2. Регистрируете его в config.cpp в секции CfgPatches / CfgMods
  3. CF_ModuleCoreManager из CF автоматически обнаруживает и создаёт экземпляры всех зарегистрированных классов модулей при запуске
  4. События жизненного цикла рассылаются автоматически

Базовые классы модулей

CF предоставляет три базовых класса, соответствующих слоям скриптов DayZ:

Базовый классСлойТипичное использование
CF_ModuleGame3_GameРанняя инициализация, регистрация RPC, классы данных
CF_ModuleWorld4_WorldВзаимодействие с сущностями, игровые системы
CF_ModuleMission5_MissionUI, HUD, хуки уровня миссии

Пример: модуль CF

c
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

c
// Получить ссылку на работающий модуль по типу
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 использует архитектуру плагинов, где каждый инструмент администратора --- это класс плагина, зарегистрированный в центральном менеджере.

Базовый плагин

c
// Паттерн VPP (упрощённо)
class PluginBase : Managed
{
    void OnInit();
    void OnUpdate(float dt);
    void OnDestroy();

    // Идентичность плагина
    string GetPluginName();
    bool IsServerOnly();
};

ConfigurablePlugin

VPP расширяет базу вариантом с поддержкой конфигурации, который автоматически загружает/сохраняет настройки:

c
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():

c
// Паттерн 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# для авторегистрации.

Концепция

Вместо ручной регистрации модулей вы аннотируете класс атрибутом, и фреймворк обнаруживает его при запуске с помощью рефлексии:

c
// Паттерн Dabs (концептуальный)
[CF_RegisterModule(DabsAdminESP)]
class DabsAdminESP : CF_ModuleWorld
{
    override void OnInit()
    {
        super.OnInit();
        // ...
    }
};

Атрибут CF_RegisterModule указывает менеджеру модулей CF автоматически создать экземпляр этого класса. Ручной вызов Register() не нужен.

Как работает обнаружение

При запуске CF сканирует все загруженные классы скриптов на наличие атрибута регистрации. Для каждого совпадения он создаёт экземпляр и добавляет его в менеджер модулей. Это происходит до того, как OnInit() будет вызван для любого модуля.

Ключевые характеристики

  • Нулевой шаблонный код: никакого кода регистрации в классах миссий
  • Декларативность: сам класс объявляет, что он является модулем
  • Зависимость от CF: работает только с обработкой атрибутов Community Framework
  • Обнаружимость: все модули можно найти поиском по атрибуту в кодовой базе

Собственный статический менеджер модулей

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

Базовые классы модулей

c
// Базовый: хуки жизненного цикла
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-классов миссий:

c
// В modded MissionServer.OnInit():
MyModuleManager.Register(new MyMissionServerModule());
MyModuleManager.Register(new MyAIServerModule());

Диспетчеризация жизненного цикла

Modded-классы миссий вызывают MyModuleManager в каждой точке жизненного цикла:

c
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()                                   │
│  Завершение: сохранение состояния, отписка от         │
│  событий, очистка коллекций, обнуление ссылок        │
└─────────────────────────────────────────────────────┘

Правила

  1. OnInit вызывается до OnMissionStart. Никогда не загружайте конфиги и не спавните сущности в OnInit() --- мир может быть ещё не готов.
  2. OnUpdate получает дельту времени. Всегда используйте dt для логики, основанной на времени, никогда не предполагайте фиксированную частоту кадров.
  3. OnMissionFinish должен очистить всё. Каждая ref-коллекция должна быть очищена. Каждая подписка на события должна быть удалена. Каждый синглтон должен быть уничтожен. Это единственная надёжная точка завершения.
  4. Модули не должны зависеть от порядка инициализации друг друга. Если модулю A нужен модуль B, используйте ленивый доступ (GetModule()), а не полагайтесь на то, что B был зарегистрирован первым.

Лучшие практики проектирования модулей

1. Один модуль --- одна ответственность

Модуль должен владеть ровно одной предметной областью. Если вы пишете VehicleAndWeatherAndLootModule, разделите его.

c
// ХОРОШО: Сфокусированные модули
class MyLootModule : MyServerModule { ... }
class MyVehicleModule : MyServerModule { ... }
class MyWeatherModule : MyServerModule { ... }

// ПЛОХО: Бог-модуль
class MyEverythingModule : MyServerModule { ... }

2. OnUpdate должен быть дешёвым

OnUpdate выполняется каждый кадр. Если ваш модуль делает тяжёлую работу (файловый ввод-вывод, сканирование мира, поиск пути), выполняйте её по таймеру или разбивайте на кадры:

c
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() может быть слишком поздно, если клиенты подключаются быстро.

c
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. Используйте менеджер модулей для межмодульного доступа

Не храните прямые ссылки на другие модули. Используйте поиск через менеджер:

c
// ХОРОШО: Слабое связывание через менеджер
MyModuleBase mod = MyModuleManager.GetModule("MyAIServerModule");
MyAIServerModule aiMod;
if (Class.CastTo(aiMod, mod))
{
    aiMod.PauseSpawning();
}

// ПЛОХО: Прямая статическая ссылка создаёт жёсткое связывание
MyAIServerModule.s_Instance.PauseSpawning();

5. Защищайтесь от отсутствующих зависимостей

Не каждый сервер запускает каждый мод. Если ваш модуль опционально интегрируется с другим модом, используйте проверки препроцессора:

c
override void OnMissionStart()
{
    super.OnMissionStart();

    #ifdef MYMOD_AI
    MyEventBus.OnMissionStarted.Insert(OnAIMissionStarted);
    #endif
}

6. Логируйте события жизненного цикла модулей

Логирование упрощает отладку. Каждый модуль должен логировать свою инициализацию и завершение:

c
override void OnInit()
{
    super.OnInit();
    MyLog.Info("MyModule", "Initialized");
}

override void OnMissionFinish()
{
    MyLog.Info("MyModule", "Shutting down");
    // Очистка...
}

Сравнительная таблица

ФункцияCF_ModuleCoreVPP PluginАтрибуты DabsСобственный модуль
Обнаружениеconfig.cpp + автоРучная Register()Сканирование атрибутовРучная Register()
Базовые классыGame / World / MissionPluginBase / ConfigurablePluginCF_ModuleWorld + атрибутServerModule / ClientModule
ЗависимостиТребует CFСамодостаточныйТребует CFСамодостаточный
Безопасность listen-сервераCF обрабатываетРучная проверкаCF обрабатываетТипизированные подклассы
Интеграция конфиговОтдельноВстроена в ConfigurablePluginОтдельноЧерез MyConfigManager
Диспетчеризация обновленийАвтоматическаяМенеджер вызывает OnUpdateАвтоматическаяМенеджер вызывает OnUpdate
ОчисткаCF обрабатываетРучная OnDestroyCF обрабатывает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 >>

Released under CC BY-SA 4.0 | Code examples under MIT License