Глава 8.9: Профессиональный шаблон мода
Главная | << Назад: Создание HUD-оверлея | Профессиональный шаблон мода | Далее: Создание пользовательского транспорта >>
Краткое описание: Эта глава предоставляет полный, готовый к продакшну шаблон мода со всеми файлами, необходимыми для профессионального мода DayZ. В отличие от Главы 8.5, которая представляет стартовый скелет InclementDab, это полнофункциональный шаблон с системой конфигурации, синглтон-менеджером, клиент-серверным RPC, UI-панелью, привязками клавиш, локализацией и автоматизацией сборки. Каждый файл готов к копированию и обильно комментирован для объяснения зачем существует каждая строка.
Содержание
- Обзор
- Полная структура каталогов
- mod.cpp
- config.cpp
- Файл констант (3_Game)
- Класс данных конфигурации (3_Game)
- Определения RPC (3_Game)
- Синглтон-менеджер (4_World)
- Обработчик событий игрока (4_World)
- Хук миссии: Сервер (5_Mission)
- Хук миссии: Клиент (5_Mission)
- Скрипт UI-панели (5_Mission)
- Файл макета
- stringtable.csv
- Inputs.xml
- Скрипт сборки
- Руководство по настройке
- Руководство по расширению функций
- Следующие шаги
Обзор
Мод "Hello World" доказывает, что инструменты работают. Профессиональный мод требует гораздо большего:
| Аспект | Hello World | Профессиональный шаблон |
|---|---|---|
| Конфигурация | Жёстко заданные значения | JSON-конфигурация с загрузкой/сохранением/значениями по умолчанию |
| Коммуникация | Операторы Print | Строковый RPC (клиент на сервер и обратно) |
| Архитектура | Один файл, одна функция | Синглтон-менеджер, слоистые скрипты, чистый жизненный цикл |
| Пользовательский интерфейс | Нет | UI-панель на основе макета с открытием/закрытием |
| Привязка ввода | Нет | Пользовательская привязка клавиш в Настройки > Управление |
| Локализация | Нет | stringtable.csv с 13 языками |
| Конвейер сборки | Ручной Addon Builder | Пакетный скрипт в один клик |
| Очистка | Нет | Корректное завершение при окончании миссии, без утечек |
Этот шаблон даёт вам всё это из коробки. Вы переименовываете идентификаторы, удаляете ненужные системы и начинаете строить вашу фактическую функциональность на прочном фундаменте.
Полная структура каталогов
Это полная структура исходников. Каждый файл, перечисленный ниже, предоставлен как полный шаблон в этой главе.
MyProfessionalMod/ <-- Корень исходников (на P: диске)
mod.cpp <-- Метаданные лаунчера
Scripts/
config.cpp <-- Регистрация в движке (CfgPatches + CfgMods)
Inputs.xml <-- Определения привязок клавиш
stringtable.csv <-- Локализованные строки (13 языков)
3_Game/
MyMod/
MyModConstants.c <-- Перечисления, строка версии, общие константы
MyModConfig.c <-- JSON-сериализуемая конфигурация с значениями по умолчанию
MyModRPC.c <-- Имена маршрутов RPC и регистрация
4_World/
MyMod/
MyModManager.c <-- Синглтон-менеджер (жизненный цикл, конфигурация, состояние)
MyModPlayerHandler.c <-- Хуки подключения/отключения игроков
5_Mission/
MyMod/
MyModMissionServer.c <-- modded MissionServer (инициализация/завершение сервера)
MyModMissionClient.c <-- modded MissionGameplay (инициализация/завершение клиента)
MyModUI.c <-- Скрипт UI-панели (открытие/закрытие/заполнение)
GUI/
layouts/
MyModPanel.layout <-- Определение макета UI
build.bat <-- Автоматизация упаковки PBO
После сборки распространяемая папка мода выглядит так:
@MyProfessionalMod/ <-- Что размещается на сервере / Workshop
mod.cpp
addons/
MyProfessionalMod_Scripts.pbo <-- Упаковано из Scripts/
keys/
MyMod.bikey <-- Ключ для подписанных серверов
meta.cpp <-- Метаданные Workshop (генерируются автоматически)mod.cpp
Этот файл управляет тем, что игроки видят в лаунчере DayZ. Он размещается в корне мода, не внутри Scripts/.
// ==========================================================================
// mod.cpp - Идентификация мода для лаунчера DayZ
// Этот файл читается лаунчером для отображения информации о моде в списке модов.
// Он НЕ компилируется скриптовым движком -- это чистые метаданные.
// ==========================================================================
// Отображаемое имя в списке модов лаунчера и на экране модов в игре.
name = "My Professional Mod";
// Ваше имя или название команды. Показывается в столбце "Автор".
author = "YourName";
// Строка семантической версии. Обновляйте с каждым релизом.
// Лаунчер отображает её, чтобы игроки знали, какая у них версия.
version = "1.0.0";
// Краткое описание при наведении на мод в лаунчере.
// Держите до 200 символов для читаемости.
overview = "A professional mod template with config, RPC, UI, and keybinds.";
// Подсказка при наведении. Обычно совпадает с названием мода.
tooltipOwned = "My Professional Mod";
// Необязательно: путь к изображению предпросмотра (относительно корня мода).
// Рекомендуемый размер: 256x256 или 512x512, формат PAA или EDDS.
// Оставьте пустым, если изображения ещё нет.
picture = "";
// Необязательно: логотип, отображаемый в панели деталей мода.
logo = "";
logoSmall = "";
logoOver = "";
// Необязательно: URL, открываемый при нажатии "Веб-сайт" в лаунчере.
action = "";
actionURL = "";config.cpp
Это самый критичный файл. Он регистрирует ваш мод в движке, объявляет зависимости, подключает слои скриптов и опционально устанавливает определения препроцессора и наборы изображений.
Разместите его по пути Scripts/config.cpp.
// ==========================================================================
// config.cpp - Регистрация в движке
// Движок DayZ читает этот файл, чтобы узнать, что предоставляет ваш мод.
// Два раздела важны: CfgPatches (граф зависимостей) и CfgMods (загрузка скриптов).
// ==========================================================================
// --------------------------------------------------------------------------
// CfgPatches - Объявление зависимостей
// Движок использует это для определения порядка загрузки. Если ваш мод зависит от
// другого мода, укажите класс CfgPatches этого мода в requiredAddons[].
// --------------------------------------------------------------------------
class CfgPatches
{
// Имя класса ДОЛЖНО быть глобально уникальным среди всех модов.
// Соглашение: ModName_Scripts (совпадает с именем PBO).
class MyMod_Scripts
{
// units[] и weapons[] объявляют конфигурационные классы, определённые этим аддоном.
// Для модов только со скриптами оставьте пустыми. Они используются модами,
// которые определяют новые предметы, оружие или транспорт в config.cpp.
units[] = {};
weapons[] = {};
// Минимальная версия движка. 0.1 работает для всех текущих версий DayZ.
requiredVersion = 0.1;
// Зависимости: перечислите имена классов CfgPatches из других модов.
// "DZ_Data" -- это базовая игра -- каждый мод должен зависеть от него.
// Добавьте "CF_Scripts", если используете Community Framework.
// Добавьте патчи других модов, если расширяете их.
requiredAddons[] =
{
"DZ_Data"
};
};
};
// --------------------------------------------------------------------------
// CfgMods - Регистрация модулей скриптов
// Сообщает движку, где находится каждый слой скриптов и какие определения устанавливать.
// --------------------------------------------------------------------------
class CfgMods
{
// Имя класса здесь -- это внутренний идентификатор вашего мода.
// Он НЕ обязан совпадать с CfgPatches -- но их связь
// облегчает навигацию по кодовой базе.
class MyMod
{
// dir: имя папки на P: диске (или в PBO).
// Должно точно совпадать с реальным именем корневой папки.
dir = "MyProfessionalMod";
// Отображаемое имя (показывается в Workbench и некоторых логах движка).
name = "My Professional Mod";
// Автор и описание для метаданных движка.
author = "YourName";
overview = "Professional mod template";
// Тип мода. Всегда "mod" для скриптовых модов.
type = "mod";
// credits: необязательный путь к файлу Credits.json.
// creditsJson = "MyProfessionalMod/Scripts/Credits.json";
// inputs: путь к вашему Inputs.xml для пользовательских привязок клавиш.
// Это ОБЯЗАТЕЛЬНО указать здесь, чтобы движок загрузил ваши привязки.
inputs = "MyProfessionalMod/Scripts/Inputs.xml";
// defines: символы препроцессора, устанавливаемые при загрузке вашего мода.
// Другие моды могут использовать #ifdef MYMOD для обнаружения вашего мода
// и условной компиляции интеграционного кода.
defines[] = { "MYMOD" };
// dependencies: какие ванильные модули скриптов использует ваш мод.
// "Game" = 3_Game, "World" = 4_World, "Mission" = 5_Mission.
// Большинству модов нужны все три. Добавьте "Core" только если используете 1_Core.
dependencies[] =
{
"Game", "World", "Mission"
};
// defs: сопоставляет каждый модуль скриптов с его папкой на диске.
// Движок компилирует все .c файлы, найденные рекурсивно по этим путям.
// В Enforce Script нет #include -- так загружаются файлы.
class defs
{
// imageSets: регистрация файлов .imageset для использования в макетах.
// Нужно только если у вас есть пользовательские иконки/текстуры для UI.
// Раскомментируйте и обновите пути, если добавляете imageset.
//
// class imageSets
// {
// files[] =
// {
// "MyProfessionalMod/GUI/imagesets/mymod_icons.imageset"
// };
// };
// Слой Game (3_Game): загружается первым.
// Размещайте здесь перечисления, константы, классы конфигурации, определения RPC.
// НЕ МОЖЕТ ссылаться на типы из 4_World или 5_Mission.
class gameScriptModule
{
value = "";
files[] = { "MyProfessionalMod/Scripts/3_Game" };
};
// Слой World (4_World): загружается вторым.
// Размещайте здесь менеджеры, модификации сущностей, мировые взаимодействия.
// МОЖЕТ ссылаться на типы 3_Game. НЕ МОЖЕТ ссылаться на типы 5_Mission.
class worldScriptModule
{
value = "";
files[] = { "MyProfessionalMod/Scripts/4_World" };
};
// Слой Mission (5_Mission): загружается последним.
// Размещайте здесь хуки миссий, UI-панели, логику запуска/завершения.
// МОЖЕТ ссылаться на типы из всех нижних слоёв.
class missionScriptModule
{
value = "";
files[] = { "MyProfessionalMod/Scripts/5_Mission" };
};
};
};
};Файл констант (3_Game)
Разместите по пути Scripts/3_Game/MyMod/MyModConstants.c.
Этот файл определяет все общие константы, перечисления и строку версии. Он находится в 3_Game, чтобы каждый вышестоящий слой мог получить доступ к этим значениям.
// ==========================================================================
// MyModConstants.c - Общие константы и перечисления
// Слой 3_Game: доступен всем вышестоящим слоям (4_World, 5_Mission).
//
// ЗАЧЕМ этот файл:
// Централизация констант предотвращает магические числа, разбросанные по файлам.
// Перечисления дают безопасность на этапе компиляции вместо сравнений сырых int.
// Строка версии определяется один раз и используется в логах и UI.
// ==========================================================================
// ---------------------------------------------------------------------------
// Версия - обновляйте с каждым релизом
// ---------------------------------------------------------------------------
const string MYMOD_VERSION = "1.0.0";
// ---------------------------------------------------------------------------
// Тег лога - префикс для всех сообщений Print/log этого мода
// Использование единообразного тега облегчает фильтрацию лога скриптов.
// ---------------------------------------------------------------------------
const string MYMOD_TAG = "[MyMod]";
// ---------------------------------------------------------------------------
// Пути к файлам - централизованы, чтобы опечатки обнаруживались в одном месте
// $profile: разрешается в каталог профиля сервера во время выполнения.
// ---------------------------------------------------------------------------
const string MYMOD_CONFIG_DIR = "$profile:MyMod";
const string MYMOD_CONFIG_PATH = "$profile:MyMod/config.json";
// ---------------------------------------------------------------------------
// Перечисление: Режимы функций
// Используйте перечисления вместо сырых int для читаемости и проверок компиляции.
// ---------------------------------------------------------------------------
enum MyModMode
{
DISABLED = 0, // Функция выключена
PASSIVE = 1, // Функция работает, но не вмешивается
ACTIVE = 2 // Функция полностью включена
};
// ---------------------------------------------------------------------------
// Перечисление: Типы уведомлений (используются UI для выбора иконки/цвета)
// ---------------------------------------------------------------------------
enum MyModNotifyType
{
INFO = 0,
SUCCESS = 1,
WARNING = 2,
ERROR = 3
};Класс данных конфигурации (3_Game)
Разместите по пути Scripts/3_Game/MyMod/MyModConfig.c.
Это JSON-сериализуемый класс настроек. Сервер загружает его при запуске. Если файл не существует, используются значения по умолчанию и свежая конфигурация сохраняется на диск.
// ==========================================================================
// MyModConfig.c - JSON-конфигурация со значениями по умолчанию
// Слой 3_Game, чтобы менеджеры 4_World и хуки 5_Mission могли читать её.
//
// КАК ЭТО РАБОТАЕТ:
// JsonFileLoader<MyModConfig> использует встроенный JSON-сериализатор
// Enforce Script. Каждое поле со значением по умолчанию записывается в / читается из
// JSON-файла. Добавление нового поля безопасно -- старые файлы конфигурации просто
// получают значение по умолчанию для отсутствующих полей.
//
// ОСОБЕННОСТЬ ENFORCE SCRIPT:
// JsonFileLoader<T>.JsonLoadFile(path, obj) возвращает VOID.
// Вы НЕ МОЖЕТЕ написать: if (JsonFileLoader<T>.JsonLoadFile(...)) -- это не скомпилируется.
// Всегда передавайте предварительно созданный объект по ссылке.
// ==========================================================================
class MyModConfig
{
// --- Общие настройки ---
// Главный переключатель: если false, весь мод отключён.
bool Enabled = true;
// Как часто (в секундах) менеджер выполняет тик обновления.
// Меньшие значения = более отзывчиво, но выше нагрузка на CPU.
float UpdateInterval = 5.0;
// Максимальное количество предметов/сущностей, которыми управляет этот мод одновременно.
int MaxItems = 100;
// Режим: 0 = DISABLED, 1 = PASSIVE, 2 = ACTIVE (см. перечисление MyModMode).
int Mode = 2;
// --- Сообщения ---
// Приветственное сообщение, показываемое игрокам при подключении.
// Пустая строка = без сообщения.
string WelcomeMessage = "Welcome to the server!";
// Показывать приветственное сообщение как уведомление или сообщение в чате.
bool WelcomeAsNotification = true;
// --- Логирование ---
// Включить подробное отладочное логирование. Отключите для продакшн-серверов.
bool DebugLogging = false;
// -----------------------------------------------------------------------
// Load - читает конфигурацию с диска, возвращает экземпляр с значениями по умолчанию если отсутствует
// -----------------------------------------------------------------------
static MyModConfig Load()
{
// Всегда сначала создаём новый экземпляр. Это гарантирует, что все значения по умолчанию
// установлены, даже если в JSON-файле отсутствуют поля (например, после
// обновления, добавившего новые настройки).
MyModConfig cfg = new MyModConfig();
// Проверяем, существует ли файл конфигурации, прежде чем пытаться загрузить.
// При первом запуске он не будет существовать -- используем значения по умолчанию и сохраняем.
if (FileExist(MYMOD_CONFIG_PATH))
{
// JsonLoadFile заполняет существующий объект. Он НЕ возвращает
// новый объект. Поля, присутствующие в JSON, перезаписывают значения по умолчанию;
// поля, отсутствующие в JSON, сохраняют значения по умолчанию.
JsonFileLoader<MyModConfig>.JsonLoadFile(MYMOD_CONFIG_PATH, cfg);
}
else
{
// Первый запуск: сохраняем значения по умолчанию, чтобы у администратора был файл для редактирования.
cfg.Save();
Print(MYMOD_TAG + " No config found, created default at: " + MYMOD_CONFIG_PATH);
}
return cfg;
}
// -----------------------------------------------------------------------
// Save - записывает текущие значения на диск как форматированный JSON
// -----------------------------------------------------------------------
void Save()
{
// Убеждаемся, что каталог существует. MakeDirectory безопасно вызывать,
// даже если каталог уже существует.
if (!FileExist(MYMOD_CONFIG_DIR))
{
MakeDirectory(MYMOD_CONFIG_DIR);
}
// JsonSaveFile записывает все поля как JSON-объект.
// Файл перезаписывается целиком -- слияния нет.
JsonFileLoader<MyModConfig>.JsonSaveFile(MYMOD_CONFIG_PATH, this);
}
};Результирующий config.json на диске выглядит так:
{
"Enabled": true,
"UpdateInterval": 5.0,
"MaxItems": 100,
"Mode": 2,
"WelcomeMessage": "Welcome to the server!",
"WelcomeAsNotification": true,
"DebugLogging": false
}Администраторы редактируют этот файл, перезапускают сервер, и новые значения вступают в силу.
Определения RPC (3_Game)
Разместите по пути Scripts/3_Game/MyMod/MyModRPC.c.
RPC (Remote Procedure Call) -- это способ коммуникации клиента и сервера в DayZ. Этот файл определяет имена маршрутов и предоставляет вспомогательные методы для регистрации.
// ==========================================================================
// MyModRPC.c - Определения маршрутов RPC и помощники
// Слой 3_Game: константы имён маршрутов должны быть доступны везде.
//
// КАК РАБОТАЕТ RPC В DAYZ:
// Движок предоставляет ScriptRPC и OnRPC для отправки/получения данных.
// Вы вызываете GetGame().RPCSingleParam() или создаёте ScriptRPC, записываете
// данные в него и отправляете. Получатель читает данные в том же порядке.
//
// DayZ использует целочисленные ID RPC. Для избежания коллизий между модами
// каждый мод должен выбрать уникальный диапазон ID или использовать систему
// строковой маршрутизации. Этот шаблон использует единый уникальный int ID
// со строковым префиксом для идентификации обработчика каждого сообщения.
//
// ПАТТЕРН:
// 1. Клиент хочет данные -> отправляет запросной RPC серверу
// 2. Сервер обрабатывает -> отправляет ответный RPC обратно клиенту
// 3. Клиент получает -> обновляет UI или состояние
// ==========================================================================
// ---------------------------------------------------------------------------
// ID RPC - выберите уникальное число, маловероятно совпадающее с другими модами.
// Проверьте вики сообщества DayZ для часто используемых диапазонов.
// Встроенные RPC движка используют малые числа (0-1000).
// Соглашение: используйте 5-значное число на основе хэша имени вашего мода.
// ---------------------------------------------------------------------------
const int MYMOD_RPC_ID = 74291;
// ---------------------------------------------------------------------------
// Имена маршрутов RPC - строковые идентификаторы для каждой конечной точки RPC.
// Использование констант предотвращает опечатки и позволяет поиск в IDE.
// ---------------------------------------------------------------------------
const string MYMOD_RPC_CONFIG_SYNC = "MyMod:ConfigSync";
const string MYMOD_RPC_WELCOME = "MyMod:Welcome";
const string MYMOD_RPC_PLAYER_DATA = "MyMod:PlayerData";
const string MYMOD_RPC_UI_REQUEST = "MyMod:UIRequest";
const string MYMOD_RPC_UI_RESPONSE = "MyMod:UIResponse";
// ---------------------------------------------------------------------------
// MyModRPCHelper - статический утилитарный класс для отправки RPC
// Оборачивает шаблонный код создания ScriptRPC, записи строки маршрута,
// записи полезной нагрузки и вызова Send().
// ---------------------------------------------------------------------------
class MyModRPCHelper
{
// Отправка строкового сообщения с сервера конкретному клиенту.
// identity: целевой игрок. null = рассылка всем.
// routeName: какой обработчик должен обработать это (например, MYMOD_RPC_WELCOME).
// message: строковая полезная нагрузка.
static void SendStringToClient(PlayerIdentity identity, string routeName, string message)
{
// Создаём объект RPC. Это конверт.
ScriptRPC rpc = new ScriptRPC();
// Записываем имя маршрута первым. Получатель читает его, чтобы решить,
// какой обработчик вызвать. Всегда записывайте/читайте в одном порядке.
rpc.Write(routeName);
// Записываем данные полезной нагрузки.
rpc.Write(message);
// Отправляем клиенту. Параметры:
// null = нет целевого объекта (сущность игрока не нужна для пользовательских RPC)
// MYMOD_RPC_ID = наш уникальный канал RPC
// true = гарантированная доставка (подобно TCP). Используйте false для частых обновлений.
// identity = целевой клиент. null рассылает ВСЕМ клиентам.
rpc.Send(null, MYMOD_RPC_ID, true, identity);
}
// Отправка запроса от клиента серверу (без полезной нагрузки, только маршрут).
static void SendRequestToServer(string routeName)
{
ScriptRPC rpc = new ScriptRPC();
rpc.Write(routeName);
// При отправке НА сервер identity равен null (у сервера нет PlayerIdentity).
// guaranteed = true гарантирует доставку сообщения.
rpc.Send(null, MYMOD_RPC_ID, true, null);
}
};Синглтон-менеджер (4_World)
Разместите по пути Scripts/4_World/MyMod/MyModManager.c.
Это центральный мозг вашего мода на стороне сервера. Он владеет конфигурацией, обрабатывает RPC и выполняет периодические обновления.
// ==========================================================================
// MyModManager.c - Серверный синглтон-менеджер
// Слой 4_World: может ссылаться на типы 3_Game (конфигурация, константы, RPC).
//
// ЗАЧЕМ синглтон:
// Менеджеру нужен ровно один экземпляр, который существует на протяжении всей
// миссии. Несколько экземпляров вызвали бы дублирование обработки и
// конфликтующее состояние. Паттерн синглтон гарантирует один экземпляр
// и предоставляет глобальный доступ через GetInstance().
//
// ЖИЗНЕННЫЙ ЦИКЛ:
// 1. MissionServer.OnInit() вызывает MyModManager.GetInstance().Init()
// 2. Менеджер загружает конфигурацию, регистрирует RPC, запускает таймеры
// 3. Менеджер обрабатывает события во время геймплея
// 4. MissionServer.OnMissionFinish() вызывает MyModManager.Cleanup()
// 5. Синглтон уничтожается, все ссылки освобождаются
// ==========================================================================
class MyModManager
{
// Единственный экземпляр. 'ref' означает, что этот класс ВЛАДЕЕТ объектом.
// Когда s_Instance устанавливается в null, объект уничтожается.
private static ref MyModManager s_Instance;
// Конфигурация, загруженная с диска.
// 'ref' потому что менеджер владеет временем жизни объекта конфигурации.
protected ref MyModConfig m_Config;
// Накопленное время с последнего тика обновления (секунды).
protected float m_TimeSinceUpdate;
// Отслеживает, был ли Init() успешно вызван.
protected bool m_Initialized;
// -----------------------------------------------------------------------
// Доступ к синглтону
// -----------------------------------------------------------------------
static MyModManager GetInstance()
{
if (!s_Instance)
{
s_Instance = new MyModManager();
}
return s_Instance;
}
// Вызовите при завершении миссии для уничтожения синглтона и освобождения памяти.
// Установка s_Instance в null вызывает деструктор.
static void Cleanup()
{
s_Instance = null;
}
// -----------------------------------------------------------------------
// Жизненный цикл
// -----------------------------------------------------------------------
// Вызывается один раз из MissionServer.OnInit().
void Init()
{
if (m_Initialized) return;
// Загрузка конфигурации с диска (или создание значений по умолчанию при первом запуске).
m_Config = MyModConfig.Load();
if (!m_Config.Enabled)
{
Print(MYMOD_TAG + " Mod is DISABLED in config. Skipping initialization.");
return;
}
// Сброс таймера обновления.
m_TimeSinceUpdate = 0;
m_Initialized = true;
Print(MYMOD_TAG + " Manager initialized (v" + MYMOD_VERSION + ")");
if (m_Config.DebugLogging)
{
Print(MYMOD_TAG + " Debug logging enabled");
Print(MYMOD_TAG + " Update interval: " + m_Config.UpdateInterval.ToString() + "s");
Print(MYMOD_TAG + " Max items: " + m_Config.MaxItems.ToString());
}
}
// Вызывается каждый кадр из MissionServer.OnUpdate().
// timeslice -- секунды, прошедшие с последнего кадра.
void OnUpdate(float timeslice)
{
if (!m_Initialized || !m_Config.Enabled) return;
// Накапливаем время и обрабатываем только с настроенным интервалом.
// Это предотвращает выполнение дорогостоящей логики каждый кадр.
m_TimeSinceUpdate += timeslice;
if (m_TimeSinceUpdate < m_Config.UpdateInterval) return;
m_TimeSinceUpdate = 0;
// --- Логика периодического обновления здесь ---
// Пример: итерация отслеживаемых сущностей, проверка условий и т.д.
if (m_Config.DebugLogging)
{
Print(MYMOD_TAG + " Periodic update tick");
}
}
// Вызывается при завершении миссии (выключение или перезапуск сервера).
void Shutdown()
{
if (!m_Initialized) return;
Print(MYMOD_TAG + " Manager shutting down");
// Сохранение состояния времени выполнения при необходимости.
// m_Config.Save();
m_Initialized = false;
}
// -----------------------------------------------------------------------
// Обработчики RPC
// -----------------------------------------------------------------------
// Вызывается, когда клиент запрашивает данные UI.
// sender: игрок, отправивший запрос.
// ctx: поток данных (уже после имени маршрута).
void OnUIRequest(PlayerIdentity sender, ParamsReadContext ctx)
{
if (!sender) return;
if (m_Config.DebugLogging)
{
Print(MYMOD_TAG + " UI data requested by: " + sender.GetName());
}
// Формируем данные ответа и отправляем обратно.
// В реальном моде вы бы собирали здесь фактические данные.
string responseData = "Items: " + m_Config.MaxItems.ToString();
MyModRPCHelper.SendStringToClient(sender, MYMOD_RPC_UI_RESPONSE, responseData);
}
// Вызывается при подключении игрока. Отправляет приветственное сообщение если настроено.
void OnPlayerConnected(PlayerIdentity identity)
{
if (!m_Initialized || !m_Config.Enabled) return;
if (!identity) return;
// Отправляем приветственное сообщение если настроено.
if (m_Config.WelcomeMessage != "")
{
MyModRPCHelper.SendStringToClient(identity, MYMOD_RPC_WELCOME, m_Config.WelcomeMessage);
if (m_Config.DebugLogging)
{
Print(MYMOD_TAG + " Sent welcome to: " + identity.GetName());
}
}
}
// -----------------------------------------------------------------------
// Аксессоры
// -----------------------------------------------------------------------
MyModConfig GetConfig()
{
return m_Config;
}
bool IsInitialized()
{
return m_Initialized;
}
};Обработчик событий игрока (4_World)
Разместите по пути Scripts/4_World/MyMod/MyModPlayerHandler.c.
Используется паттерн modded class для подключения к ванильной сущности PlayerBase и обнаружения событий подключения/отключения.
// ==========================================================================
// MyModPlayerHandler.c - Хуки жизненного цикла игрока
// Слой 4_World: модифицированный PlayerBase для перехвата подключения/отключения.
//
// ЗАЧЕМ modded class:
// DayZ не имеет колбэка "игрок подключён". Стандартный паттерн --
// переопределение методов MissionServer (для новых подключений)
// или подключение к PlayerBase (для событий уровня сущности, таких как смерть).
// Мы используем modded PlayerBase для демонстрации хуков уровня сущности.
//
// ВАЖНО:
// Всегда вызывайте super.MethodName() первым в переопределениях. Невыполнение
// ломает цепочку ванильного поведения и другие моды, которые также переопределяют
// тот же метод.
// ==========================================================================
modded class PlayerBase
{
// Отслеживаем, отправили ли мы событие инициализации для этого игрока.
// Это предотвращает дублирование обработки, если Init() вызывается несколько раз.
protected bool m_MyModPlayerReady;
// -----------------------------------------------------------------------
// Вызывается после полного создания и репликации сущности игрока.
// На сервере это место, где игрок "готов" принимать RPC.
// -----------------------------------------------------------------------
override void Init()
{
super.Init();
// Выполняем только на сервере. GetGame().IsServer() возвращает true на
// выделенных серверах и на хосте listen-сервера.
if (!GetGame().IsServer()) return;
// Защита от повторной инициализации.
if (m_MyModPlayerReady) return;
m_MyModPlayerReady = true;
// Получаем сетевую идентификацию игрока.
// На сервере GetIdentity() возвращает объект PlayerIdentity,
// содержащий имя игрока, Steam ID (PlainId) и UID.
PlayerIdentity identity = GetIdentity();
if (!identity) return;
// Уведомляем менеджер о подключении игрока.
MyModManager mgr = MyModManager.GetInstance();
if (mgr)
{
mgr.OnPlayerConnected(identity);
}
}
};Хук миссии: Сервер (5_Mission)
Разместите по пути Scripts/5_Mission/MyMod/MyModMissionServer.c.
Подключается к MissionServer для инициализации и завершения мода на стороне сервера.
// ==========================================================================
// MyModMissionServer.c - Серверные хуки миссии
// Слой 5_Mission: загружается последним, может ссылаться на все нижние слои.
//
// ЗАЧЕМ modded MissionServer:
// MissionServer -- точка входа для серверной логики. Его OnInit()
// выполняется один раз при старте миссии (загрузка сервера). OnMissionFinish()
// выполняется при выключении или перезапуске сервера. Это правильные
// места для настройки и очистки систем вашего мода.
//
// ПОРЯДОК ЖИЗНЕННОГО ЦИКЛА:
// 1. Движок загружает все слои скриптов (3_Game -> 4_World -> 5_Mission)
// 2. Движок создаёт экземпляр MissionServer
// 3. Вызывается OnInit() -> инициализируйте ваши системы здесь
// 4. Вызывается OnMissionStart() -> мир готов, игроки могут подключаться
// 5. OnUpdate() вызывается каждый кадр
// 6. Вызывается OnMissionFinish() -> сервер завершает работу
// ==========================================================================
modded class MissionServer
{
// -----------------------------------------------------------------------
// Инициализация
// -----------------------------------------------------------------------
override void OnInit()
{
// ВСЕГДА вызывайте super первым. Другие моды в цепочке зависят от этого.
super.OnInit();
// Инициализируем синглтон-менеджер. Это загружает конфигурацию с диска,
// регистрирует обработчики RPC и подготавливает мод к работе.
MyModManager.GetInstance().Init();
Print(MYMOD_TAG + " Server mission initialized");
}
// -----------------------------------------------------------------------
// Покадровое обновление
// -----------------------------------------------------------------------
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
// Делегируем менеджеру. Менеджер самостоятельно управляет
// ограничением частоты (UpdateInterval из конфигурации), так что это дёшево.
MyModManager mgr = MyModManager.GetInstance();
if (mgr)
{
mgr.OnUpdate(timeslice);
}
}
// -----------------------------------------------------------------------
// Подключение игрока - серверная диспетчеризация RPC
// Вызывается движком, когда клиент отправляет RPC серверу.
// -----------------------------------------------------------------------
override void OnRPC(PlayerIdentity sender, Object target, int rpc_type, ParamsReadContext ctx)
{
super.OnRPC(sender, target, rpc_type, ctx);
// Обрабатываем только наш ID RPC. Все остальные RPC проходят насквозь.
if (rpc_type != MYMOD_RPC_ID) return;
// Читаем имя маршрута (первая строка, записанная отправителем).
string routeName;
if (!ctx.Read(routeName)) return;
// Диспетчеризация к правильному обработчику на основе имени маршрута.
MyModManager mgr = MyModManager.GetInstance();
if (!mgr) return;
if (routeName == MYMOD_RPC_UI_REQUEST)
{
mgr.OnUIRequest(sender, ctx);
}
// Добавляйте больше маршрутов здесь по мере роста вашего мода:
// else if (routeName == MYMOD_RPC_SOME_OTHER)
// {
// mgr.OnSomeOther(sender, ctx);
// }
}
// -----------------------------------------------------------------------
// Завершение
// -----------------------------------------------------------------------
override void OnMissionFinish()
{
// Завершаем менеджер перед вызовом super.
// Это гарантирует выполнение нашей очистки до того, как движок разрушит
// инфраструктуру миссии.
MyModManager mgr = MyModManager.GetInstance();
if (mgr)
{
mgr.Shutdown();
}
// Уничтожаем синглтон для освобождения памяти и предотвращения устаревшего состояния,
// если миссия перезапустится (например, перезапуск сервера без завершения процесса).
MyModManager.Cleanup();
Print(MYMOD_TAG + " Server mission finished");
super.OnMissionFinish();
}
};Хук миссии: Клиент (5_Mission)
Разместите по пути Scripts/5_Mission/MyMod/MyModMissionClient.c.
Подключается к MissionGameplay для клиентской инициализации, обработки ввода и получения RPC.
// ==========================================================================
// MyModMissionClient.c - Клиентские хуки миссии
// Слой 5_Mission.
//
// ЗАЧЕМ MissionGameplay:
// На клиенте MissionGameplay -- это активный класс миссии во время
// геймплея. Он получает OnUpdate() каждый кадр (для опроса ввода)
// и OnRPC() для входящих серверных сообщений.
//
// ПРИМЕЧАНИЕ О LISTEN-СЕРВЕРАХ:
// На listen-сервере (хост + игра) активны ОБА MissionServer и
// MissionGameplay. Ваш клиентский код будет работать параллельно с
// серверным. Защищайтесь GetGame().IsClient() или GetGame().IsServer(),
// если нужна логика для конкретной стороны.
// ==========================================================================
modded class MissionGameplay
{
// Ссылка на UI-панель. null когда закрыта.
protected ref MyModUI m_MyModPanel;
// Отслеживание состояния инициализации.
protected bool m_MyModInitialized;
// -----------------------------------------------------------------------
// Инициализация
// -----------------------------------------------------------------------
override void OnInit()
{
super.OnInit();
m_MyModInitialized = true;
Print(MYMOD_TAG + " Client mission initialized");
}
// -----------------------------------------------------------------------
// Покадровое обновление: опрос ввода и управление UI
// -----------------------------------------------------------------------
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
if (!m_MyModInitialized) return;
// Опрос привязки клавиши, определённой в Inputs.xml.
// GetUApi() возвращает API пользовательских действий.
// GetInputByName() ищет действие по имени из Inputs.xml.
// LocalPress() возвращает true на кадре нажатия клавиши.
UAInput panelInput = GetUApi().GetInputByName("UAMyModPanel");
if (panelInput && panelInput.LocalPress())
{
TogglePanel();
}
}
// -----------------------------------------------------------------------
// Приёмник RPC: обрабатывает сообщения от сервера
// -----------------------------------------------------------------------
override void OnRPC(PlayerIdentity sender, Object target, int rpc_type, ParamsReadContext ctx)
{
super.OnRPC(sender, target, rpc_type, ctx);
// Обрабатываем только наш ID RPC.
if (rpc_type != MYMOD_RPC_ID) return;
// Читаем имя маршрута.
string routeName;
if (!ctx.Read(routeName)) return;
// Диспетчеризация по маршруту.
if (routeName == MYMOD_RPC_WELCOME)
{
string welcomeMsg;
if (ctx.Read(welcomeMsg))
{
// Отображаем приветственное сообщение игроку.
// GetGame().GetMission().OnEvent() может показывать уведомления,
// или вы можете использовать пользовательский UI. Для простоты используем чат.
GetGame().Chat(welcomeMsg, "");
Print(MYMOD_TAG + " Welcome message: " + welcomeMsg);
}
}
else if (routeName == MYMOD_RPC_UI_RESPONSE)
{
string responseData;
if (ctx.Read(responseData))
{
// Обновляем UI-панель полученными данными.
if (m_MyModPanel)
{
m_MyModPanel.SetData(responseData);
}
}
}
}
// -----------------------------------------------------------------------
// Переключение UI-панели
// -----------------------------------------------------------------------
protected void TogglePanel()
{
if (m_MyModPanel && m_MyModPanel.IsOpen())
{
m_MyModPanel.Close();
m_MyModPanel = null;
}
else
{
// Открываем только если игрок жив и никакое другое меню не показано.
PlayerBase player = PlayerBase.Cast(GetGame().GetPlayer());
if (!player || !player.IsAlive()) return;
UIManager uiMgr = GetGame().GetUIManager();
if (uiMgr && uiMgr.GetMenu()) return;
m_MyModPanel = new MyModUI();
m_MyModPanel.Open();
// Запрашиваем свежие данные с сервера.
MyModRPCHelper.SendRequestToServer(MYMOD_RPC_UI_REQUEST);
}
}
// -----------------------------------------------------------------------
// Завершение
// -----------------------------------------------------------------------
override void OnMissionFinish()
{
// Закрываем и уничтожаем UI-панель если открыта.
if (m_MyModPanel)
{
m_MyModPanel.Close();
m_MyModPanel = null;
}
m_MyModInitialized = false;
Print(MYMOD_TAG + " Client mission finished");
super.OnMissionFinish();
}
};Скрипт UI-панели (5_Mission)
Разместите по пути Scripts/5_Mission/MyMod/MyModUI.c.
Этот скрипт управляет UI-панелью, определённой в файле .layout. Он находит ссылки на виджеты, заполняет их данными и обрабатывает открытие/закрытие.
// ==========================================================================
// MyModUI.c - Контроллер UI-панели
// Слой 5_Mission: может ссылаться на все нижние слои.
//
// КАК РАБОТАЕТ UI В DAYZ:
// 1. Файл .layout определяет иерархию виджетов (как HTML).
// 2. Класс скрипта загружает макет, находит виджеты по имени и
// управляет ими (установка текста, показ/скрытие, реакция на клики).
// 3. Скрипт показывает/скрывает корневой виджет и управляет фокусом ввода.
//
// ЖИЗНЕННЫЙ ЦИКЛ ВИДЖЕТОВ:
// GetGame().GetWorkspace().CreateWidgets() загружает файл макета и
// возвращает корневой виджет. Затем используйте FindAnyWidget() для получения
// ссылок на именованные дочерние виджеты. По завершении вызовите widget.Unlink()
// для уничтожения всего дерева виджетов.
// ==========================================================================
class MyModUI
{
// Корневой виджет панели (загружен из .layout).
protected ref Widget m_Root;
// Именованные дочерние виджеты.
protected TextWidget m_TitleText;
protected TextWidget m_DataText;
protected TextWidget m_VersionText;
protected ButtonWidget m_CloseButton;
// Отслеживание состояния.
protected bool m_IsOpen;
// -----------------------------------------------------------------------
// Конструктор: загрузка макета и поиск ссылок на виджеты
// -----------------------------------------------------------------------
void MyModUI()
{
// CreateWidgets загружает файл .layout и создаёт все виджеты.
// Путь относительно корня мода (так же как пути config.cpp).
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyProfessionalMod/Scripts/GUI/layouts/MyModPanel.layout"
);
// Изначально скрыт до вызова Open().
if (m_Root)
{
m_Root.Show(false);
// Поиск именованных виджетов. Эти имена ДОЛЖНЫ точно совпадать с именами виджетов
// в файле .layout (с учётом регистра).
m_TitleText = TextWidget.Cast(m_Root.FindAnyWidget("TitleText"));
m_DataText = TextWidget.Cast(m_Root.FindAnyWidget("DataText"));
m_VersionText = TextWidget.Cast(m_Root.FindAnyWidget("VersionText"));
m_CloseButton = ButtonWidget.Cast(m_Root.FindAnyWidget("CloseButton"));
// Установка статического содержимого.
if (m_TitleText)
m_TitleText.SetText("My Professional Mod");
if (m_VersionText)
m_VersionText.SetText("v" + MYMOD_VERSION);
}
}
// -----------------------------------------------------------------------
// Open: показать панель и захватить ввод
// -----------------------------------------------------------------------
void Open()
{
if (!m_Root) return;
m_Root.Show(true);
m_IsOpen = true;
// Блокируем управление игроком, чтобы WASD не двигало персонаж
// пока панель открыта. Это показывает курсор.
GetGame().GetMission().PlayerControlDisable(INPUT_EXCLUDE_ALL);
GetGame().GetUIManager().ShowUICursor(true);
Print(MYMOD_TAG + " UI panel opened");
}
// -----------------------------------------------------------------------
// Close: скрыть панель и освободить ввод
// -----------------------------------------------------------------------
void Close()
{
if (!m_Root) return;
m_Root.Show(false);
m_IsOpen = false;
// Повторно включаем управление игроком.
GetGame().GetMission().PlayerControlEnable(true);
GetGame().GetUIManager().ShowUICursor(false);
Print(MYMOD_TAG + " UI panel closed");
}
// -----------------------------------------------------------------------
// Обновление данных: вызывается, когда сервер отправляет данные UI
// -----------------------------------------------------------------------
void SetData(string data)
{
if (m_DataText)
{
m_DataText.SetText(data);
}
}
// -----------------------------------------------------------------------
// Запрос состояния
// -----------------------------------------------------------------------
bool IsOpen()
{
return m_IsOpen;
}
// -----------------------------------------------------------------------
// Деструктор: очистка дерева виджетов
// -----------------------------------------------------------------------
void ~MyModUI()
{
// Unlink уничтожает корневой виджет и все его дочерние элементы.
// Это освобождает память, используемую деревом виджетов.
if (m_Root)
{
m_Root.Unlink();
}
}
};Файл макета
Разместите по пути Scripts/GUI/layouts/MyModPanel.layout.
Определяет визуальную структуру UI-панели. Макеты DayZ используют пользовательский текстовый формат (не XML).
// ==========================================================================
// MyModPanel.layout - Структура UI-панели
//
// ПРАВИЛА РАЗМЕРОВ:
// hexactsize 1 + vexactsize 1 = размер в пикселях (например, size 400 300)
// hexactsize 0 + vexactsize 0 = размер пропорциональный (от 0.0 до 1.0)
// halign/valign управляют точкой привязки:
// left_ref/top_ref = привязка к левому/верхнему краю родителя
// center_ref = центрирование в родителе
// right_ref/bottom_ref = привязка к правому/нижнему краю родителя
//
// ВАЖНО:
// - Никогда не используйте отрицательные размеры. Используйте выравнивание и позицию.
// - Имена виджетов должны точно совпадать с вызовами FindAnyWidget() в скрипте.
// - 'ignorepointer 1' означает, что виджет не получает клики мыши.
// - 'scriptclass' связывает виджет с классом скрипта для обработки событий.
// ==========================================================================
// Корневая панель: центрирована на экране, 400x300 пикселей, полупрозрачный фон.
PanelWidgetClass MyModPanelRoot {
position 0 0
size 400 300
halign center_ref
valign center_ref
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
color 0.1 0.1 0.12 0.92
priority 100
{
// Панель заголовка: полная ширина, высота 36px, вверху.
PanelWidgetClass TitleBar {
position 0 0
size 1 36
hexactpos 1
vexactpos 1
hexactsize 0
vexactsize 1
color 0.15 0.15 0.18 1
{
// Текст заголовка: выровнен влево с отступом.
TextWidgetClass TitleText {
position 12 0
size 300 36
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
valign center_ref
ignorepointer 1
text "My Mod"
font "gui/fonts/metron2"
"exact size" 16
color 1 1 1 0.9
}
// Текст версии: правая сторона панели заголовка.
TextWidgetClass VersionText {
position 0 0
size 80 36
halign right_ref
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
valign center_ref
ignorepointer 1
text "v1.0.0"
font "gui/fonts/metron2"
"exact size" 12
color 0.6 0.6 0.6 0.8
}
}
}
// Область содержимого: ниже панели заголовка, заполняет оставшееся пространство.
PanelWidgetClass ContentArea {
position 0 40
size 380 200
halign center_ref
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
color 0 0 0 0
{
// Текст данных: где отображаются серверные данные.
TextWidgetClass DataText {
position 12 12
size 356 160
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
ignorepointer 1
text "Waiting for data..."
font "gui/fonts/metron2"
"exact size" 14
color 0.85 0.85 0.85 1
}
}
}
// Кнопка закрытия: правый нижний угол.
ButtonWidgetClass CloseButton {
position 0 0
size 100 32
halign right_ref
valign bottom_ref
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
text "Close"
font "gui/fonts/metron2"
"exact size" 14
}
}
}stringtable.csv
Разместите по пути Scripts/stringtable.csv.
Предоставляет локализацию для всего текста, видимого игрокам. Движок читает столбец, соответствующий языку игры. Столбец original используется как запасной вариант.
DayZ поддерживает 13 языковых столбцов. Каждая строка должна содержать все 13 столбцов (используйте английский текст как заполнитель для языков, которые вы не переводите).
"Language","original","english","czech","german","russian","polish","hungarian","italian","spanish","french","chinese","japanese","portuguese","chinesesimp",
"STR_MYMOD_INPUT_GROUP","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod","My Mod",
"STR_MYMOD_INPUT_PANEL","Open Panel","Open Panel","Otevrit Panel","Panel offnen","Otkryt Panel","Otworz Panel","Panel megnyitasa","Apri Pannello","Abrir Panel","Ouvrir Panneau","Open Panel","Open Panel","Abrir Painel","Open Panel",
"STR_MYMOD_TITLE","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod","My Professional Mod",
"STR_MYMOD_CLOSE","Close","Close","Zavrit","Schliessen","Zakryt","Zamknij","Bezaras","Chiudi","Cerrar","Fermer","Close","Close","Fechar","Close",
"STR_MYMOD_WELCOME","Welcome!","Welcome!","Vitejte!","Willkommen!","Dobro pozhalovat!","Witaj!","Udvozoljuk!","Benvenuto!","Bienvenido!","Bienvenue!","Welcome!","Welcome!","Bem-vindo!","Welcome!",Важно: Каждая строка должна заканчиваться завершающей запятой после последнего языкового столбца. Это требование CSV-парсера DayZ.
Inputs.xml
Разместите по пути Scripts/Inputs.xml.
Определяет пользовательские привязки клавиш, которые появляются в меню игры Настройки > Управление. Поле inputs в CfgMods файла config.cpp должно указывать на этот файл.
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!--
Inputs.xml - Определения пользовательских привязок клавиш
СТРУКТУРА:
- <actions>: объявляет имена действий ввода и их строки отображения
- <sorting>: группирует действия под категорией в меню Управление
- <preset>: устанавливает привязку клавиши по умолчанию
СОГЛАШЕНИЕ ОБ ИМЕНОВАНИИ:
- Имена действий начинаются с "UA" (User Action), за которым следует префикс вашего мода.
- Атрибут "loc" ссылается на строковый ключ из stringtable.csv.
ИМЕНА КЛАВИШ:
- Клавиатура: kA до kZ, k0-k9, kInsert, kHome, kEnd, kDelete,
kNumpad0-kNumpad9, kF1-kF12, kLControl, kRControl, kLShift, kRShift,
kLAlt, kRAlt, kSpace, kReturn, kBack, kTab, kEscape
- Мышь: mouse1 (левая), mouse2 (правая), mouse3 (средняя)
- Комбинации клавиш: используйте элемент <combo> с несколькими дочерними <btn>
-->
<modded_inputs>
<inputs>
<!-- Объявление действия ввода. -->
<actions>
<input name="UAMyModPanel" loc="STR_MYMOD_INPUT_PANEL" />
</actions>
<!-- Группировка под категорией в Настройки > Управление. -->
<!-- "name" -- внутренний ID; "loc" -- отображаемое имя из stringtable. -->
<sorting name="mymod" loc="STR_MYMOD_INPUT_GROUP">
<input name="UAMyModPanel"/>
</sorting>
</inputs>
<!-- Пресет клавиш по умолчанию. Игроки могут переназначить в Настройки > Управление. -->
<preset>
<!-- Привязка к клавише Home по умолчанию. -->
<input name="UAMyModPanel">
<btn name="kHome"/>
</input>
<!--
ПРИМЕР КОМБИНАЦИИ КЛАВИШ (раскомментируйте для использования):
Это привяжет к Ctrl+H вместо одной клавиши.
<input name="UAMyModPanel">
<combo>
<btn name="kLControl"/>
<btn name="kH"/>
</combo>
</input>
-->
</preset>
</modded_inputs>Скрипт сборки
Разместите build.bat в корне мода.
Этот пакетный файл автоматизирует упаковку PBO с помощью Addon Builder из DayZ Tools.
@echo off
REM ==========================================================================
REM build.bat - Автоматизированная упаковка PBO для MyProfessionalMod
REM
REM ЧТО ДЕЛАЕТ:
REM 1. Упаковывает папку Scripts/ в PBO-файл
REM 2. Размещает PBO в распространяемую папку @mod
REM 3. Копирует mod.cpp в распространяемую папку
REM
REM ПРЕДВАРИТЕЛЬНЫЕ ТРЕБОВАНИЯ:
REM - DayZ Tools установлены через Steam
REM - Исходники мода в P:\MyProfessionalMod\
REM
REM ИСПОЛЬЗОВАНИЕ:
REM Дважды щёлкните этот файл или запустите из командной строки: build.bat
REM ==========================================================================
REM --- Конфигурация: обновите эти пути в соответствии с вашей установкой ---
REM Путь к DayZ Tools (проверьте путь библиотеки Steam).
set DAYZ_TOOLS=C:\Program Files (x86)\Steam\steamapps\common\DayZ Tools
REM Исходная папка: каталог Scripts, который упаковывается в PBO.
set SOURCE=P:\MyProfessionalMod\Scripts
REM Выходная папка: куда помещается упакованный PBO.
set OUTPUT=P:\@MyProfessionalMod\addons
REM Префикс: виртуальный путь внутри PBO. Должен совпадать с путями
REM в config.cpp (например, "MyProfessionalMod/Scripts/3_Game" должен разрешаться).
set PREFIX=MyProfessionalMod\Scripts
REM --- Этапы сборки ---
echo ============================================
echo Сборка MyProfessionalMod
echo ============================================
REM Создаём выходной каталог если не существует.
if not exist "%OUTPUT%" mkdir "%OUTPUT%"
REM Запускаем Addon Builder.
REM -clear = удаление старого PBO перед упаковкой
REM -prefix = установка префикса PBO (необходимо для разрешения путей скриптов)
echo Упаковка PBO...
"%DAYZ_TOOLS%\Bin\AddonBuilder\AddonBuilder.exe" "%SOURCE%" "%OUTPUT%" -prefix=%PREFIX% -clear
REM Проверяем, успешно ли завершился Addon Builder.
if %ERRORLEVEL% NEQ 0 (
echo.
echo ОШИБКА: Упаковка PBO не удалась! Проверьте вывод выше для деталей.
echo Частые причины:
echo - Неправильный путь к DayZ Tools
echo - Исходная папка не существует
echo - Файл .c имеет синтаксическую ошибку, предотвращающую упаковку
pause
exit /b 1
)
REM Копируем mod.cpp в распространяемую папку.
echo Копирование mod.cpp...
copy /Y "P:\MyProfessionalMod\mod.cpp" "P:\@MyProfessionalMod\mod.cpp" >nul
echo.
echo ============================================
echo Сборка завершена!
echo Результат: P:\@MyProfessionalMod\
echo ============================================
echo.
echo Для тестирования с file patching (PBO не нужен):
echo DayZDiag_x64.exe -mod=P:\MyProfessionalMod -filePatching
echo.
echo Для тестирования со собранным PBO:
echo DayZDiag_x64.exe -mod=P:\@MyProfessionalMod
echo.
pauseРуководство по настройке
При использовании этого шаблона для собственного мода необходимо переименовать все вхождения имён-заполнителей. Вот полный чеклист.
Шаг 1: Выберите ваши имена
Определите эти идентификаторы до начала редактирования:
| Идентификатор | Пример | Правила |
|---|---|---|
| Имя папки мода | MyBountySystem | Без пробелов, PascalCase или подчёркивания |
| Отображаемое имя | "My Bounty System" | Человекочитаемое, для mod.cpp и config.cpp |
| Класс CfgPatches | MyBountySystem_Scripts | Должен быть глобально уникальным среди всех модов |
| Класс CfgMods | MyBountySystem | Внутренний идентификатор движка |
| Префикс скриптов | MyBounty | Короткий префикс для классов: MyBountyManager, MyBountyConfig |
| Константа тега | MYBOUNTY_TAG | Для сообщений лога: "[MyBounty]" |
| Определение препроцессора | MYBOUNTYSYSTEM | Для #ifdef обнаружения между модами |
| ID RPC | 58432 | Уникальное 5-значное число, не используемое другими модами |
| Имя действия ввода | UAMyBountyPanel | Начинается с UA, уникальное |
Шаг 2: Переименование файлов и папок
Переименуйте каждый файл и папку, содержащие "MyMod" или "MyProfessionalMod":
MyProfessionalMod/ -> MyBountySystem/
Scripts/3_Game/MyMod/ -> Scripts/3_Game/MyBounty/
MyModConstants.c -> MyBountyConstants.c
MyModConfig.c -> MyBountyConfig.c
MyModRPC.c -> MyBountyRPC.c
Scripts/4_World/MyMod/ -> Scripts/4_World/MyBounty/
MyModManager.c -> MyBountyManager.c
MyModPlayerHandler.c -> MyBountyPlayerHandler.c
Scripts/5_Mission/MyMod/ -> Scripts/5_Mission/MyBounty/
MyModMissionServer.c -> MyBountyMissionServer.c
MyModMissionClient.c -> MyBountyMissionClient.c
MyModUI.c -> MyBountyUI.c
Scripts/GUI/layouts/
MyModPanel.layout -> MyBountyPanel.layoutШаг 3: Поиск-и-замена во всех файлах
Выполните эти замены по порядку (сначала самые длинные строки, чтобы избежать частичных совпадений):
| Найти | Заменить | Затронутые файлы |
|---|---|---|
MyProfessionalMod | MyBountySystem | config.cpp, mod.cpp, build.bat, скрипт UI |
MyModManager | MyBountyManager | Менеджер, хуки миссий, обработчик игрока |
MyModConfig | MyBountyConfig | Класс конфигурации, менеджер |
MyModConstants | MyBountyConstants | (только имя файла) |
MyModRPCHelper | MyBountyRPCHelper | Помощник RPC, хуки миссий |
MyModUI | MyBountyUI | Скрипт UI, клиентский хук миссии |
MyModPanel | MyBountyPanel | Файл макета, скрипт UI |
MyMod_Scripts | MyBountySystem_Scripts | config.cpp CfgPatches |
MYMOD_RPC_ID | MYBOUNTY_RPC_ID | Константы, RPC, хуки миссий |
MYMOD_RPC_ | MYBOUNTY_RPC_ | Все константы маршрутов RPC |
MYMOD_TAG | MYBOUNTY_TAG | Константы, все файлы с тегом лога |
MYMOD_CONFIG | MYBOUNTY_CONFIG | Константы, класс конфигурации |
MYMOD_VERSION | MYBOUNTY_VERSION | Константы, скрипт UI |
MYMOD | MYBOUNTYSYSTEM | config.cpp defines[] |
MyMod | MyBounty | config.cpp класс CfgMods, строки маршрутов RPC |
My Mod | My Bounty System | Строки в макетах, stringtable |
mymod | mybounty | Inputs.xml имя сортировки |
STR_MYMOD_ | STR_MYBOUNTY_ | stringtable.csv, Inputs.xml |
UAMyMod | UAMyBounty | Inputs.xml, клиентский хук миссии |
m_MyMod | m_MyBounty | Переменные-члены клиентского хука миссии |
74291 | 58432 | ID RPC (ваше выбранное уникальное число) |
Шаг 4: Проверка
После переименования выполните поиск по всему проекту "MyMod" и "MyProfessionalMod", чтобы обнаружить пропущенное. Затем соберите и протестируйте:
DayZDiag_x64.exe -mod=P:\MyBountySystem -filePatchingПроверьте лог скриптов на наличие вашего тега (например, [MyBounty]), чтобы подтвердить, что всё загрузилось.
Руководство по расширению функций
Когда ваш мод запущен, вот как добавлять типичные функции.
Добавление новой конечной точки RPC
1. Определите константу маршрута в MyModRPC.c (3_Game):
const string MYMOD_RPC_BOUNTY_SET = "MyMod:BountySet";2. Добавьте серверный обработчик в MyModManager.c (4_World):
void OnBountySet(PlayerIdentity sender, ParamsReadContext ctx)
{
// Чтение параметров, записанных клиентом.
string targetName;
int bountyAmount;
if (!ctx.Read(targetName)) return;
if (!ctx.Read(bountyAmount)) return;
Print(MYMOD_TAG + " Bounty set on " + targetName + ": " + bountyAmount.ToString());
// ... ваша логика здесь ...
}3. Добавьте случай диспетчеризации в MyModMissionServer.c (5_Mission), внутри OnRPC():
else if (routeName == MYMOD_RPC_BOUNTY_SET)
{
mgr.OnBountySet(sender, ctx);
}4. Отправьте с клиента (где бы ни инициировалось действие):
ScriptRPC rpc = new ScriptRPC();
rpc.Write(MYMOD_RPC_BOUNTY_SET);
rpc.Write("PlayerName");
rpc.Write(5000);
rpc.Send(null, MYMOD_RPC_ID, true, null);Добавление нового поля конфигурации
1. Добавьте поле в MyModConfig.c со значением по умолчанию:
// Минимальная сумма награды, которую могут установить игроки.
int MinBountyAmount = 100;Это всё. JSON-сериализатор автоматически подхватывает публичные поля. Существующие файлы конфигурации на диске будут использовать значение по умолчанию для нового поля, пока администратор не отредактирует и не сохранит.
2. Используйте его из менеджера:
if (bountyAmount < m_Config.MinBountyAmount)
{
// Отклонить: слишком мало.
return;
}Добавление новой UI-панели
1. Создайте макет по пути Scripts/GUI/layouts/MyModBountyList.layout:
PanelWidgetClass BountyListRoot {
position 0 0
size 500 400
halign center_ref
valign center_ref
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
color 0.1 0.1 0.12 0.92
{
TextWidgetClass BountyListTitle {
position 12 8
size 476 30
hexactpos 1
vexactpos 1
hexactsize 1
vexactsize 1
text "Active Bounties"
font "gui/fonts/metron2"
"exact size" 18
color 1 1 1 0.9
}
}
}2. Создайте скрипт по пути Scripts/5_Mission/MyMod/MyModBountyListUI.c:
class MyModBountyListUI
{
protected ref Widget m_Root;
protected bool m_IsOpen;
void MyModBountyListUI()
{
m_Root = GetGame().GetWorkspace().CreateWidgets(
"MyProfessionalMod/Scripts/GUI/layouts/MyModBountyList.layout"
);
if (m_Root)
m_Root.Show(false);
}
void Open() { if (m_Root) { m_Root.Show(true); m_IsOpen = true; } }
void Close() { if (m_Root) { m_Root.Show(false); m_IsOpen = false; } }
bool IsOpen() { return m_IsOpen; }
void ~MyModBountyListUI()
{
if (m_Root) m_Root.Unlink();
}
};Добавление новой привязки клавиш
1. Добавьте действие в Inputs.xml:
<actions>
<input name="UAMyModPanel" loc="STR_MYMOD_INPUT_PANEL" />
<input name="UAMyModBountyList" loc="STR_MYMOD_INPUT_BOUNTYLIST" />
</actions>
<sorting name="mymod" loc="STR_MYMOD_INPUT_GROUP">
<input name="UAMyModPanel"/>
<input name="UAMyModBountyList"/>
</sorting>2. Добавьте привязку по умолчанию в разделе <preset>:
<input name="UAMyModBountyList">
<btn name="kEnd"/>
</input>3. Добавьте локализацию в stringtable.csv:
"STR_MYMOD_INPUT_BOUNTYLIST","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List","Bounty List",4. Опрашивайте ввод в MyModMissionClient.c:
UAInput bountyInput = GetUApi().GetInputByName("UAMyModBountyList");
if (bountyInput && bountyInput.LocalPress())
{
ToggleBountyList();
}Добавление новой записи stringtable
1. Добавьте строку в stringtable.csv. Каждая строка должна содержать все 13 языковых столбцов плюс завершающую запятую:
"STR_MYMOD_BOUNTY_PLACED","Bounty placed!","Bounty placed!","Odměna vypsána!","Kopfgeld gesetzt!","Награда назначена!","Nagroda wyznaczona!","Fejpénz kiírva!","Taglia piazzata!","Recompensa puesta!","Prime placée!","Bounty placed!","Bounty placed!","Recompensa colocada!","Bounty placed!",2. Используйте её в коде скрипта:
// Widget.SetText() НЕ разрешает ключи stringtable автоматически.
// Вы должны использовать Widget.SetText() с разрешённой строкой:
string localizedText = Widget.TranslateString("#STR_MYMOD_BOUNTY_PLACED");
myTextWidget.SetText(localizedText);Или в файле .layout движок разрешает ключи #STR_ автоматически:
text "#STR_MYMOD_BOUNTY_PLACED"Следующие шаги
С работающим профессиональным шаблоном вы можете:
- Изучать продакшн-моды -- Читайте DayZ Expansion и исходный код
StarDZ_Coreдля паттернов реального мира в масштабе. - Добавлять пользовательские предметы -- Следуйте Главе 8.2: Создание пользовательского предмета и интегрируйте их с вашим менеджером.
- Создать админ-панель -- Следуйте Главе 8.3: Создание панели администратора, используя вашу систему конфигурации.
- Добавить HUD-оверлей -- Следуйте Главе 8.8: Создание HUD-оверлея для постоянно видимых элементов UI.
- Опубликовать в Workshop -- Следуйте Главе 8.7: Публикация в Workshop, когда ваш мод будет готов.
- Изучить отладку -- Прочитайте Главу 8.6: Отладка и тестирование для анализа логов и устранения неполадок.
Предыдущая: Глава 8.8: Создание HUD-оверлея | Главная
