Быстрый старт в моддинге Pathfinder: WotR — от настройки IDE до первого Hello World

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

1. Инструментарий разработчика: Конфигурация IDE и системные зависимости .NET

Скомпилированный мод скопирован в папку игры, Pathfinder: Wrath of the Righteous запущен, но ничего не происходит. В логах Unity Mod Manager (UMM) нет ни ошибок, ни предупреждений — игра просто проигнорировала файл. Причина кроется не в логике C#-кода, а в фундаментальном несовпадении сред выполнения. Бинарный файл собран с параметрами, которые игровой движок не способен прочитать.

Анатомия среды выполнения WotR

Pathfinder: Wrath of the Righteous базируется на движке Unity версии 2020.3. За выполнение скриптов и серверной логики отвечает бэкенд Mono, который жестко привязан к стандарту .NET Framework 4.7.2.

Экосистема .NET разделена на две несовместимые на бинарном уровне ветви:

  • .NET Framework (версии до 4.8) — монолитная архитектура, на которой работает большинство игр на Unity прошлых лет.
  • .NET Core / .NET 5+ — современная кроссплатформенная архитектура.
  • Если при создании проекта использовать современный .NET 8, компилятор создаст библиотеку с метаданными, непонятными среде выполнения WotR. Движок не найдет ожидаемых сигнатур старого стандарта и молча прервет загрузку мода. Задача разработчика — заставить современную IDE работать по правилам устаревшего .NET Framework 4.7.2.

    !Архитектура зависимостей мода: от движка Unity до пользовательской DLL

    Настройка рабочей среды в JetBrains Rider (Windows 11)

    Для разработки в ОС Windows 11 с использованием JetBrains Rider наличия самой IDE недостаточно. Операционной системе требуется пакет разработчика (Developer Pack) для целевой версии фреймворка.

    Среда выполнения (Runtime) позволяет только запускать готовые программы. Пакет разработчика содержит эталонные сборки (Reference Assemblies) и заголовочные файлы, необходимые Rider для проверки синтаксиса и компиляции. Если пакет для 4.7.2 отсутствует в системе, Rider не предложит эту версию в диалоге создания проекта. Скачать .NET Framework 4.7.2 Developer Pack необходимо с официального сайта Microsoft.

    Инициализация проекта

    Мод для WotR — это динамически подключаемая библиотека (DLL), встраиваемая в адресное пространство игры загрузчиком UMM. В коде отсутствует стандартная точка входа static void Main().

    Процесс создания базового репозитория в JetBrains Rider:

  • Выберите New Solution.
  • В левом меню выберите шаблон Class Library.
  • В поле Framework строго укажите .NET Framework 4.7.2. Игнорируйте любые шаблоны с пометкой .NET, .NET Core или .NET Standard.
  • Задайте имя проекта без пробелов, используя PascalCase (например, WotrHelloMod). Это имя станет названием итогового DLL-файла и корневым пространством имен.
  • Эталонная конфигурация .csproj

    Вместо ручного переключения параметров через графический интерфейс Rider (свойства сборки, целевую платформу, копирование локальных файлов), профессиональный моддинг требует прямого редактирования файла конфигурации проекта — .csproj.

    Современный подход использует SDK-style формат файла. Он лаконичен, скрывает дефолтные настройки MSBuild и автоматически управляет рутинными процессами. Чтобы открыть его в Rider, дважды кликните по названию проекта в окне Solution Explorer.

    Ниже представлен эталонный листинг .csproj для базового мода. Замените содержимое вашего файла этим кодом.

    Разбор параметров: Целевая архитектура (x64)

    Строка <PlatformTarget>x64</PlatformTarget> отвечает за архитектуру итогового бинарного файла.

    Pathfinder: WotR — ресурсоемкая игра. 32-битный процесс в Windows ограничен адресным пространством в байт (ровно 4 ГБ оперативной памяти). Для современных RPG с тысячами блюпринтов и тяжелыми ассетами этого критически мало, поэтому движок скомпилирован исключительно под 64-битную архитектуру.

    По умолчанию Rider использует значение AnyCPU. При такой настройке JIT-компилятор (Just-In-Time) среды выполнения сам решает, как развернуть код в памяти. Однако мод тесно взаимодействует с нативными библиотеками Unity, написанными на C++. Использование AnyCPU часто приводит к конфликтам при линковке памяти.

    Если компилятор создаст 32-битную DLL (), при попытке внедрения в 64-битный процесс WotR загрузчик выбросит фатальное исключение BadImageFormatException. Жесткая фиксация <PlatformTarget>x64</PlatformTarget> гарантирует побайтовое соответствие вашей DLL адресному пространству игры.

    Золотое правило зависимостей: Тег Private

    Блок <ItemGroup> содержит ссылки (References) на оригинальные библиотеки. Чтобы мод мог изменить характеристики оружия или добавить реплику, компилятору необходимо знать структуру классов WotR. Главная библиотека с логикой игры — Assembly-CSharp.dll, логика движка — UnityEngine.dll, а API загрузчика — UnityModManager.dll.

    Критический элемент конфигурации — тег <Private>False</Private>. В графическом интерфейсе IDE этот параметр называется Copy Local (Копировать локально).

    По умолчанию при добавлении ссылки на стороннюю библиотеку IDE устанавливает этот параметр в True. Логика стандартной разработки: «взять этот DLL-файл и скопировать его в папку с готовым проектом, чтобы программа гарантированно нашла свои зависимости при запуске на другом компьютере».

    В контексте моддинга копирование игровых библиотек должно быть строго отключено.

    Ваш мод загружается внутрь уже работающей игры. В изолированной области оперативной памяти (AppDomain) уже загружены все оригинальные DLL. Если мод принесет в своей папке собственные копии этих гигантских файлов (размер Assembly-CSharp.dll превышает 40 МБ), загрузчик попытается поместить их в память второй раз.

    Следствия дублирования библиотек в памяти:

  • Конфликт пространств имен. Система не понимает, из какой версии файла инстанцировать класс.
  • Ошибки приведения типов. Объект UnitEntityData, созданный игрой из оригинальной DLL, не будет равен объекту UnitEntityData, который ожидает ваш мод из скопированной DLL. Возникает InvalidCastException.
  • Мгновенный краш. Игра зависает на стартовом экране с полосой загрузки.
  • Тег <Private>False</Private> сообщает Rider: «Используй эти файлы исключительно для проверки синтаксиса и автодополнения кода во время разработки, но не прикрепляй их к итоговому артефакту сборки».

    Подготовка к публикации на GitHub

    Для создания качественного базового репозитория жестко закодированные пути в <HintPath> (например, C:\Program Files (x86)\...) являются плохой практикой. Если другой разработчик клонирует ваш репозиторий, а игра у него установлена на диске D:\Games, проект не скомпилируется.

    Для решения этой проблемы пути к зависимостям заменяются на переменные окружения или относительные пути. В WotR-моддинге стандартом де-факто стало использование системной переменной, например (WOTR_PATH)\Wrath_Data\Managed\Assembly-CSharp.dll</HintPath> <Private>False</Private> </Reference> ` Перед компиляцией достаточно добавить переменную WOTR_PATH в настройки Windows, указав путь к корневой папке игры. Это делает ваш .csproj универсальным и готовым к командной разработке.

    Конфигурации сборки: Влияние на Harmony-патчи

    В верхней панели Rider находится переключатель конфигураций сборки. В эталонном .csproj параметры <DebugSymbols>, <DebugType> и <Optimize> настроены для режима отладки (Debug).

    | Характеристика | Debug (Отладка) | Release (Релиз) | | :--- | :--- | :--- | | Оптимизация кода | Отключена (<Optimize>false</Optimize>). | Включена (<Optimize>true</Optimize>). JIT-компилятор перестраивает IL-код. | | Генерация PDB | Создается полный .pdb файл (Program Database). | Файл не создается или создается в урезанном виде. | | Влияние на Harmony | Патчи работают стабильно. | Высокий риск отказа патчей из-за Inlining-а. |

    Проблема Inlining (Встраивания методов): В режиме Release компилятор стремится ускорить выполнение кода. Если метод небольшой, JIT-компилятор может применить оптимизацию Inlining — он берет тело метода и встраивает его напрямую в место вызова, уничтожая сам факт вызова оригинального метода в памяти. Библиотека Harmony, которая является стандартом для изменения механик WotR, работает путем перехвата адресов методов в оперативной памяти. Если метод был «встроен» компилятором, его адрес исчезает. Harmony рапортует об успешном патче, но ваш код никогда не выполняется. Именно поэтому на этапе разработки и тестирования механик тег <Optimize>false</Optimize> абсолютно необходим.

    Важность PDB-файла: Когда в логике мода возникает ошибка (например, NullReferenceException), Unity Mod Manager перехватывает ее и пишет в лог. Если рядом с WotrHelloMod.dll в папке мода находится файл WotrHelloMod.pdb, менеджер сможет расшифровать стек вызовов и указать точный номер строки в вашем C#-коде. Формат <DebugType>portable</DebugType>` является кроссплатформенным стандартом, который корректно считывается парсером логов UMM.

    Настроенный таким образом проект генерирует пустую, но архитектурно безупречную библиотеку. Среда разработки синхронизирована с движком Unity, зависимости изолированы, а репозиторий готов к версионированию. База заложена.

    2. Интеграция с Unity Mod Manager: Структура Info.json и жизненный цикл загрузки мода

    Скомпилированная библиотека .dll — это мертвый груз на жестком диске. Хотя разработчики Pathfinder: Wrath of the Righteous позже добавили официальную поддержку модов (Owlcat Modification Template, сканирующий папку Modifications), исторически игра не имела встроенного API для интеграции стороннего C#-кода на этапе запуска. Метод сообщества, с которого начинался моддинг WotR и который до сих пор используется для глубокого вмешательства в механики, опирается на стороннего посредника. Этим посредником выступает Unity Mod Manager (UMM).

    UMM работает по принципу инъекции, предлагая два метода. Первый — Doorstop Proxy (предпочтительный для WotR): он подменяет системные библиотеки Unity (например, winhttp.dll), перехватывая запуск движка еще до загрузки игровых ассетов, оставляя оригинальные файлы игры нетронутыми. Когда исполняемый файл игры обращается к сетевой библиотеке, он незаметно для себя вызывает код Doorstop, который затем подгружает ядро менеджера модов. Второй метод — Assembly: он напрямую модифицирует оригинальный файл Assembly-CSharp.dll (основную библиотеку логики игры) через библиотеку Mono.Cecil, жестко встраивая вызов своего загрузчика в системные классы Unity.

    Независимо от выбранного метода инъекции, когда игра стартует, она невольно запускает ядро UMM. Менеджер берет на себя ответственность за поиск, проверку и загрузку пользовательских модов в память. Чтобы UMM распознал ваш мод, загрузил его в правильном порядке и смог найти точку входа, моду требуется строгий паспорт. Эту роль выполняет файл манифеста.

    Структура манифеста Info.json

    Файл Info.json должен лежать в корне папки вашего мода (которая, в свою очередь, помещается в директорию Mods игры). UMM использует стандартный строгий парсер JSON. Малейшая синтаксическая ошибка — забытая запятая, незакрытая кавычка или сохранение файла в кодировке UTF-8 с BOM (Byte Order Mark) вместо обычного UTF-8 — приведет к тому, что менеджер молча проигнорирует директорию, и мод даже не появится во внутриигровом списке.

    Минимально необходимый и синтаксически корректный манифест выглядит так:

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

    Id — уникальный строковый идентификатор. Это самое важное мета-поле. Оно не должно содержать пробелов и спецсимволов. Именно по Id другие моды будут ссылаться на ваш проект, если вы станете зависимостью. UMM использует Id для создания файлов настроек в пользовательской директории. Строгое правило: для корректной работы и избежания дублирования при обновлениях через архив, название папки, в которой лежит мод, должно в точности совпадать со значением поля Id. Если папка называется MyMod_v1, а Id равен MyMod, при следующем обновлении UMM создаст вторую папку, что приведет к конфликту сборок.

    Version — версия мода. Важный нюанс: UMM использует встроенный класс .NET System.Version для парсинга этого поля. Это означает, что поддерживается строгий цифровой формат ` (например, 1.2.0 или 1.2.0.4).

    | Значение в JSON | Результат парсинга UMM | Причина | | :--- | :--- | :--- | | "1.0.0" | Успех | Строгое соответствие формату System.Version. | | "2.1" | Успех | Билд и ревизия опциональны. | | "1.2.0-beta" | Ошибка загрузки | Суффиксы SemVer не поддерживаются базовым классом .NET. | | "v1.0" | Ошибка загрузки | Наличие буквенного префикса ломает парсер. |

    ManagerVersion — минимальная версия самого Unity Mod Manager, необходимая для работы мода. Если у пользователя установлена версия ниже указанной, UMM подсветит мод красным цветом в интерфейсе и откажется его загружать. Это предотвращает потенциальные краши из-за отсутствия нужных API (например, новых методов рендеринга UI) в старых версиях загрузчика.

    AssemblyName — точное имя скомпилированного файла библиотеки, включая расширение .dll. Если вы назвали проект в IDE WotrMod, компилятор выдаст WotrMod.dll. Если в JSON будет написано Wotr_Mod.dll, UMM выбросит исключение FileNotFoundException при попытке загрузки, так как он ищет файл буквально по указанной строке в директории мода.

    EntryMethod — абсолютный путь к методу, который UMM должен вызвать для запуска мода. Формат строго регламентирован: ПространствоИмен.Класс.Метод. Это контракт между вашим кодом и загрузчиком.

    !Схема связи параметров info.json со структурой и точкой входа загружаемой сборки мода.

    Управление зависимостями и порядок загрузки

    В экосистеме WotR моды редко существуют в вакууме. Многие проекты опираются на базовые библиотеки. Например, библиотека BlueprintCore предоставляет удобный API для создания блюпринтов, а ModMenu стандартизирует создание окон настроек. UMM предоставляет встроенный механизм графа зависимостей через опциональные массивы в Info.json.

    Добавление поля Requirements меняет поведение загрузчика:

    UMM читает манифесты всех модов в папке Mods до того, как начнет загружать .dll файлы в память. Если в массиве Requirements указан Id мода, которого нет у пользователя, ваш мод получит статус Missing requirements и его загрузка будет прервана.

    Более того, Requirements автоматически влияет на порядок загрузки. UMM строит топологический граф: если мод A требует мод B, менеджер гарантирует, что метод Load мода B отработает полностью до того, как будет вызван метод Load мода A. Это критически важно при использовании чужого API — вы не можете обращаться к методам библиотеки, пока она сама не инициализировалась.

    Если вам нужно загрузиться после определенного мода, но он не является жесткой зависимостью (например, для применения патчей совместимости или перезаписи чужих изменений), используется поле LoadAfter:

    В этом случае, если SomeOtherModId установлен, ваш мод загрузится после него. Если не установлен — ваш мод загрузится в штатном режиме без ошибок.

    Жизненный цикл загрузки: от парсинга до Reflection

    Понимание того, что происходит под капотом UMM в момент запуска игры, критически важно для отладки. Процесс разделен на четыре изолированные фазы.

    !Интерактивная пошаговая визуализация жизненного цикла загрузки мода в Unity Mod Manager: от обнаружения манифеста до исполнения через рефлексию.

    Фаза 1: Discovery (Обнаружение). На раннем этапе инициализации игры (до появления главного меню и загрузки профиля игрока) UMM сканирует директорию Mods. Он находит все файлы Info.json, валидирует их синтаксис и извлекает метаданные. На этом этапе создаются объекты UnityModManager.ModEntry для каждого найденного мода. Библиотеки .dll на диск еще не читаются.

    Фаза 2: Resolution (Разрешение графа). UMM анализирует массивы Requirements и LoadAfter, выстраивая линейную очередь загрузки. Обнаруживаются циклические зависимости (когда мод A требует B, а B требует A) — в таком случае оба мода блокируются, чтобы предотвратить бесконечный цикл инициализации.

    Фаза 3: Instantiation (Загрузка сборки). Согласно очереди, UMM берет строку из поля AssemblyName, строит полный путь к файлу и вызывает Assembly.LoadFrom() для загрузки вашей DLL в текущий домен приложения (AppDomain) Unity. Важный нюанс: если ваш мод тянет за собой сторонние библиотеки (например, Newtonsoft.Json.dll специфической версии), они также должны лежать в папке мода. Иначе среда выполнения не сможет разрешить зависимости, и произойдет сбой с ошибкой ReflectionTypeLoadException. Если же библиотека скомпилирована под неправильную архитектуру (например, x86 вместо x64, так как WotR — строго 64-битная игра) или отсутствуют системные зависимости, процесс падает с BadImageFormatException.

    Фаза 4: Execution (Исполнение через Reflection). Это самый хрупкий этап. UMM берет строку из поля EntryMethod (например, MyFirstWotrMod.Main.Load). Поскольку UMM на этапе своей компиляции ничего не знал о вашем классе Main, он не может вызвать метод напрямую. Он использует механизм C# Reflection (рефлексию) для динамического поиска и вызова кода.

    Загрузчик ищет в загруженной сборке класс Main в пространстве имен MyFirstWotrMod. Затем ищет внутри него метод Load. Если метод найден и его сигнатура совпадает с ожидаемой, UMM вызывает его, передавая в качестве аргумента объект ModEntry, созданный на первой фазе.

    Контракт точки входа (Entry Method)

    Поскольку вызов происходит через рефлексию, сигнатура вашего метода должна абсолютно точно совпадать с тем, что ожидает UMM. Если сигнатура отличается, рефлексия не сможет выполнить привязку (binding), и мод не загрузится, оставив в логе сообщение об отсутствии подходящего метода.

    Единственно верная и безопасная сигнатура точки входа выглядит так:

    Жесткие требования к этому контракту:

  • Модификатор public static. Метод Load обязан быть публичным и статическим. UMM не создает экземпляр вашего класса Main через new Main(). Он обращается к методу на уровне типа. Если метод будет экземплярным (не статическим), рефлексия выбросит исключение при попытке вызова без объекта. Если метод скрыть модификатором private или internal, встроенные механизмы поиска UMM могут его не обнаружить, что приведет к фатальной ошибке загрузки.
  • Возвращаемый тип bool. Метод должен вернуть true, если инициализация прошла успешно, и false, если произошла критическая ошибка (например, не найден нужный конфигурационный файл или не удалось применить патчи). Если вы вернете false, UMM окрасит статус мода в красный цвет в своем внутриигровом интерфейсе (вызываемом по умолчанию через Ctrl+F10) и пометит его как неработающий.
  • Параметр UnityModManager.ModEntry. Это единственный аргумент, который UMM передает в ваш мод. Это ваш канал связи с внешним миром. Если вы напишете метод без параметров public static bool Load(), рефлексия выдаст ошибку TargetParameterCountException.
  • Объект ModEntry содержит всю информацию из вашего Info.json, абсолютный путь к директории мода (свойство Path, критически важное для загрузки собственных ассетов, текстур или локализаций), а главное — встроенный инстанс логгера (modEntry.Logger).

    Использование этого логгера предпочтительнее стандартного UnityEngine.Debug.Log. Экземпляр modEntry.Logger автоматически добавляет префикс с Id вашего мода и дублирует вывод как во внутриигровую консоль UMM, так и в системный файл Player.log Unity. Сохранение ссылки на этот объект в публичное статическое свойство (public static UnityModManager.ModEntry.ModLogger Logger) — архитектурный стандарт. Это позволяет обращаться к логгеру из любого другого класса вашего мода простым вызовом Main.Logger.Log("Сообщение"), не прокидывая объект ModEntry через все методы.

    Управление состоянием: OnToggle

    Вызов метода Load означает лишь то, что код загрузился в память и выполнил первичную настройку. Однако UMM предоставляет пользователям возможность включать и выключать моды прямо во время игры через интерфейс менеджера.

    По умолчанию, если вы реализовали только метод Load, кнопка включения/выключения (On/Off) в интерфейсе UMM будет неактивна. Менеджер не знает, как безопасно остановить ваш код. Чтобы сделать мод "переключаемым" (что крайне полезно при отладке), необходимо подписаться на делегат OnToggle внутри объекта ModEntry.

    Контракт для метода переключения выглядит так: public static bool OnToggle(UnityModManager.ModEntry modEntry, bool value). Параметр value содержит true, если пользователь нажал "Включить", и false, если "Выключить".

    Связывание происходит внутри метода Load:

    Метод OnToggle также должен возвращать bool. Если процесс выключения по какой-то причине невозможен (например, мод внес необратимые изменения в сохраненную игру или добавил новые классы персонажей, удаление которых сломает сохранения), метод должен вернуть false. В этом случае UMM откатит ползунок в интерфейсе обратно в положение "Включено", не позволив пользователю сломать текущую сессию.

    Настройка Info.json` и понимание того, как UMM через рефлексию находит и дергает за ниточки вашего скомпилированного кода, формирует технический фундамент. Ошибка в одной букве манифеста или пропущенный модификатор доступа делают часы написания C# кода бессмысленными, так как игра просто не узнает о существовании вашей сборки.

    3. Создание точки входа: Статический метод Load и базовая логика логирования в Unity

    Написанный код компилируется без ошибок, сборка успешно копируется в папку игры, вы запускаете Pathfinder: Wrath of the Righteous, но в игре ничего не меняется. Ни вылетов, ни предупреждений, ни новых механик — просто тишина. Это стандартная ситуация при разработке модификаций. Игра представляет собой скомпилированный черный ящик, и единственный способ сделать его прозрачным — выстроить надежную систему инициализации и логирования в самую первую миллисекунду жизни вашего мода.

    В среде Unity Mod Manager (UMM) малейшая синтаксическая неточность в точке входа приводит к тому, что загрузчик просто проигнорирует вашу библиотеку. Чтобы этого избежать, необходимо понимать, как именно UMM взаимодействует с вашим кодом через механизмы рефлексии.

    Рефлексия и модификаторы доступа: Почему моды становятся «невидимками»

    Unity Mod Manager не вызывает ваш код напрямую, так как на момент компиляции самого менеджера вашего мода еще не существует. Вместо этого UMM использует механизм C# Reflection (System.Reflection). Менеджер загружает вашу DLL-библиотеку в оперативную память, сканирует её классы и ищет точку входа — метод, имя которого указано в файле Info.json (по умолчанию UMM ищет статический метод Load в классе Main).

    Критическая ошибка, на которой останавливается большинство новичков — игнорирование модификаторов доступа. Если вы напишете просто static class Main и static bool Load(...), компилятор C# по умолчанию присвоит им модификатор internal (доступно только внутри самой сборки) или private (для методов).

    Среда рефлексии UMM настроена на поиск методов со строгими флагами привязки: BindingFlags.Public | BindingFlags.Static. Если ваш класс или метод скрыт модификатором по умолчанию, UMM его не увидит. При запуске игры загрузчик выбросит скрытую ошибку MissingMethodException, и мод не запустится.

    Фундаментальное правило: Класс точки входа и методы, управляющие жизненным циклом мода (Load, OnToggle), обязаны иметь модификатор public.

    Инфраструктура логирования: Подготовка к перехвату данных

    Прежде чем писать логику инициализации, необходимо создать инструмент для отслеживания ошибок. В стандартной среде Unity используется класс UnityEngine.Debug. Вызов Debug.Log("Текст") запишет сообщение в системный лог движка. Однако в контексте моддинга использовать сырой Debug — плохая практика.

    !Схема взаимодействия: C# код передает данные в UMM Engine для вывода в консоль и файл лога.

    UMM предоставляет собственный объект логгера внутри контекста UnityModManager.ModEntry. Использование этого логгера обеспечивает двойную маршрутизацию:

  • Сообщение выводится во внутриигровой оверлей UMM (вызывается по CTRL+F10) с цветовой индикацией.
  • Сообщение транслируется в системный файл Player.log, но UMM автоматически добавляет к нему префикс с ID вашего мода.
  • В WotR файл лога находится по пути %USERPROFILE%\AppData\LocalLow\Owlcat Games\Pathfinder Wrath Of The Righteous\Player.log. Если у игрока установлено модов, а файл лога содержит строк, без префикса [MyFirstWotrMod] найти причину ошибки невозможно.

    Чтобы не передавать объект логгера по всем классам проекта, создается статический класс-обертка. Мы объявляем его до написания основной логики, чтобы сразу использовать в точке входа.

    Атрибут [Conditional("DEBUG")] позволяет писать подробнейшие логи для отслеживания переменных. При сборке мода в конфигурации Release для публикации на Nexus Mods, компилятор физически удалит все вызовы Logger.Debug из итоговой DLL, экономя ресурсы процессора.

    Глобальный контекст и метод Load

    Теперь мы можем спроектировать главный класс. Метод Load вызывается UMM строго один раз при старте игры. Его задача — сохранить метаданные мода (ModEntry), инициализировать логгер и привязать делегат для кнопки включения/выключения мода в интерфейсе.

    Метод Load обязан возвращать логическое значение типа bool. Если метод возвращает , UMM фиксирует успешную загрузку. Если вернуть , менеджер пометит мод красным цветом и остановит дальнейшую работу с ним.

    Сохранение modEntry в публичное статическое поле ModContext решает проблему доступа к данным. Если ваш патч сработает через три часа игры глубоко в логике расчета урона, вы всегда сможете обратиться к Main.ModContext для проверки настроек мода.

    Жизненный цикл и защита от утечек памяти: Метод OnToggle

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

    Здесь вступает в игру библиотека Harmony, отвечающая за инъекцию вашего кода в оригинальные методы игры.

    !Интерактивная пошаговая визуализация работы Harmony.PatchAll: сканирование сборки мода, обнаружение патч-класса и динамическое внедрение Prefix и Postfix в оригинальный метод игры.

    Критическая ошибка при работе с Harmony — отсутствие защиты от повторного применения патчей. Метод harmony.PatchAll() сканирует вашу сборку и внедряет код. Если пользователь в интерфейсе UMM быстро нажмет «Выключить», а затем снова «Включить», метод OnToggle сработает дважды.

    Если не предусмотреть логическую защиту, Harmony применит ваши патчи поверх уже существующих. В памяти образуется матрешка: оригинальный метод вызовет ваш патч, который вызовет копию вашего же патча. Это приводит к экспоненциальному росту нагрузки, утечкам оперативной памяти и, как итог, фатальному крашу движка Unity (StackOverflowException).

    Для предотвращения этой ситуации вводится статический флаг состояния.

    Безопасная реализация OnToggle

    Ниже представлено продолжение класса Main с внедрением безопасной логики переключения состояний:

    Разберем ключевые механизмы защиты в этом коде:

  • Флаг _isPatched: Гарантирует, что PatchAll выполнится строго один раз, даже если UMM по какой-то причине вызовет OnToggle(..., true) дважды подряд.
  • Изоляция через try-catch: Весь блок переключения обернут в перехват исключений. Если разработчики игры в новом патче переименовали метод, который вы пытаетесь изменить, PatchAll выбросит ошибку. Благодаря try-catch, эта ошибка будет аккуратно записана в лог, метод вернет , и игра продолжит стабильно работать. Без перехвата исключение обрушит поток выполнения самого UMM.
  • Прицельный UnpatchAll: При выключении мода передача modEntry.Info.Id строго обязательна. Если вызвать _harmonyInstance.UnpatchAll() без аргументов, библиотека удалит вообще все патчи в текущем домене приложения, сломав работу всех остальных модификаций, установленных у игрока.
  • Возврат состояний: Возврат или сообщает графическому интерфейсу UMM, удалось ли применить изменения. Если вернуть , ползунок в меню откатится назад, сигнализируя пользователю о проблеме.
  • Жизненный цикл исключений на этапе загрузки

    Особое внимание следует уделить статическим конструкторам. Среда .NET вызывает статический конструктор класса Main при первом обращении к любому его члену (то есть прямо перед вызовом Load).

    Если вы решите инициализировать сложные объекты прямо в полях класса (например, private static SomeData data = new SomeData();), и этот процесс выбросит ошибку, среда сгенерирует TypeInitializationException. UMM не сможет перехватить эту ошибку корректно, лог будет оборван, а вы не увидите конкретной строки с проблемой.

    Именно поэтому золотое правило архитектуры модов гласит: статические поля инициализируются только нулями или примитивными константами. Вся реальная работа с объектами, файлами и логикой игры должна происходить строго внутри методов Load и OnToggle, где она защищена вашими блоками try-catch и системой логирования.

    Выстроив класс Main и обертку Logger по описанному шаблону, вы получаете пуленепробиваемый фундамент. Мод корректно регистрируется в системе, не конфликтует с модификаторами доступа C#, имеет глобальный канал для вывода отладочной информации и безопасно управляет инъекциями в код игры, исключая риск утечек памяти.

    4. Автоматизация сборки и деплоя: Настройка Post-Build событий и инициализация Git-репозитория

    Ручное копирование файлов мода в директорию игры после каждой итерации компиляции занимает в среднем 15 секунд. При активной разработке, когда параметры Harmony-патчей или значения в Blueprint-классах подбираются экспериментальным путем, проект собирается 50–100 раз за одну сессию. Согласно простейшей формуле , где — количество сборок, а — время ручного переноса, разработчик теряет до 25 минут чистого времени ежедневно исключительно на механические операции.

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

    Механика MSBuild и жизненный цикл компиляции

    Процесс превращения исходного кода C# в готовую динамическую библиотеку (DLL) управляется платформой MSBuild. В современных SDK-style проектах файл с расширением .csproj представляет собой не просто набор метаданных, а полноценный XML-скрипт сборки. Жизненный цикл этого скрипта разбит на последовательные фазы — цели (Targets).

    Разработчик имеет возможность вклиниться в этот конвейер, задекларировав собственные инструкции, которые MSBuild выполнит строго после успешной генерации бинарных файлов. Этот механизм называется Post-Build событием. Важное архитектурное свойство Post-Build скриптов заключается в их транзакционности: если на этапе компиляции возникает синтаксическая ошибка, конвейер прерывается. Копирование заведомо нерабочих файлов в директорию игры не происходит, что защищает среду тестирования от поломок.

    Для маршрутизации файлов внутри скриптов используются макросы MSBuild — системные переменные, которые платформа резолвит (вычисляет) динамически в момент запуска сборки. Ключевые макросы, необходимые для разработки модов:

  • (TargetDir) — путь к выходной директории (например, bin\Debug\net472\), куда компилятор помещает готовые бинарные артефакты.
  • (TargetFileName) — полное имя скомпилированного файла с расширением (например, MyFirstWotrMod.dll).
  • !Интерактивная симуляция процесса компиляции и Post-Build деплоя мода. Показывает резолв макросов MSBuild в XML, выполнение команды xcopy в терминале и физический перенос скомпилированного файла в папку игры.

    Архитектура деплоя: от локального тестирования до релиза

    Менеджер модификаций Unity Mod Manager (UMM) накладывает строгие ограничения на файловую структуру. Каждый мод обязан располагаться в собственной изолированной поддиректории внутри папки Mods клиента игры. Имя этой поддиректории должно в точности совпадать со значением поля Id, указанным в конфигурационном файле Info.json.

    Сам файл Info.json является критической точкой отказа. UMM использует его как точку входа (Entry Point): менеджер парсит JSON, находит в нем имя DLL-библиотеки и название метода инициализации. Если при копировании файлов манифест Info.json не будет перенесен вместе с DLL, игра проигнорирует библиотеку, даже если она написана безупречно.

    Вторая архитектурная задача — подготовка релизного архива для публикации на платформах вроде Nexus Mods или GitHub Releases. Пользователи устанавливают модификации, перетаскивая .zip архив в окно UMM. Специфика парсера UMM требует, чтобы внутри архива находилась корневая папка мода, а уже внутри нее — файлы. Если запаковать файлы напрямую в корень архива, UMM не сможет корректно распаковать и смонтировать мод.

    Для решения этой проблемы применяется паттерн Staging-директории. В процессе релизной сборки MSBuild сначала создает временную папку (Staging), внутри нее создает папку с именем мода, копирует туда все необходимые файлы, и только затем архивирует саму Staging-директорию.

    Ниже представлен монолитный, кроссплатформенный блок автоматизации, использующий нативные задачи MSBuild (Tasks) вместо вызовов внешних консольных утилит. Этот код решает обе задачи: локальный деплой при каждой отладочной сборке и создание валидного ZIP-архива при переключении в режим Release.

    Блок необходимо поместить в файл .csproj непосредственно перед закрывающим тегом </Project>:

    Использование нативных тегов <Copy>, <MakeDir> и <ZipDirectory> делает скрипт полностью независимым от операционной системы. В отличие от вызовов powershell или xcopy, этот код будет одинаково надежно работать как на Windows, так и на macOS или Linux (если разработка ведется через кроссплатформенные IDE). Использование переменной окружения \rightarrow\rightarrow(WOTR_PATH) для поиска библиотек на локальной машине и жесткого .gitignore делает репозиторий юридически чистым. В нем хранится исключительно авторский исходный код мода, манифест Info.json и инструкции по сборке в .csproj. Ни один байт интеллектуальной собственности разработчиков оригинальной игры не покидает пределы компьютера моддера.

    Настроенная архитектура проекта позволяет полностью сосредоточиться на написании логики. При нажатии одной кнопки IDE компилирует C#-код, проверяет его на синтаксические ошибки, формирует требуемую структуру папок, доставляет файлы в клиент игры для тестирования, а при переключении конфигурации — автоматически собирает валидный ZIP-архив, готовый к публикации.