Анатомия игры: Декомпиляция и поиск данных

Глубокое погружение в реверс-инжиниринг Pathfinder: WotR. Вы научитесь извлекать исходный код механик из Assembly-CSharp.dll и находить идентификаторы игровых объектов (Blueprints) для их последующей модификации.

1. Инструменты реверс-инжиниринга: Сравнение и настройка dnSpy и ILSpy

Инструменты реверс-инжиниринга: Сравнение и настройка dnSpy и ILSpy

Пятьдесят гигабайт ассетов, текстур и звуков формируют визуальную оболочку Pathfinder: Wrath of the Righteous, но истинные правила игры — формулы урона, логика искусственного интеллекта, условия квестов — заперты в единственном файле размером около 80 мегабайт. Этот файл называется Assembly-CSharp.dll. Вы не можете открыть его в обычном текстовом редакторе, а без понимания того, как работают оригинальные механики, создание модификаций сводится к слепому копированию чужого кода. Чтобы получить доступ к исходной логике (Ground Truth), необходимо перевести машиночитаемый формат обратно в человекочитаемый код.

Иллюзия компиляции: Почему код Unity открыт как книга

Разработка игр на движке Unity (особенно старых версий, использующих Mono-бэкенд) имеет фундаментальную особенность. Когда программисты Owlcat Games писали код на C# и нажимали кнопку сборки, компилятор не переводил текст напрямую в нули и единицы, понятные процессору. Вместо этого код транслировался в промежуточный язык — CIL (Common Intermediate Language, часто называемый просто IL).

IL — это низкоуровневый набор инструкций, который сохраняет колоссальное количество метаданных об исходном коде. Внутри Assembly-CSharp.dll хранятся точные названия всех классов, методов, полей, а также структура их взаимосвязей. При запуске игры среда выполнения (в нашем случае Mono) считывает эти IL-инструкции и на лету компилирует их в машинный код с помощью JIT-компилятора (Just-In-Time).

Декомпилятор выполняет обратный процесс. Он читает IL-инструкции и метаданные, применяет паттерны распознавания и восстанавливает исходный C#-код.

!Процесс трансляции и потери данных при декомпиляции

Результат декомпиляции не является побайтовой копией того, что писали разработчики. Комментарии удаляются на этапе компиляции, а имена локальных переменных внутри методов часто теряются (если к сборке не приложен файл отладочных символов .pdb). Однако архитектура, логика ветвлений и математические операции восстанавливаются с точностью до 99%. Для моддера этого более чем достаточно, чтобы понять, как работает любая механика, и написать для неё Harmony-патч.

Выбор инструмента: dnSpy против ILSpy

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

ILSpy: Аналитик и реставратор

ILSpy — это активно развивающийся open-source проект. Его главное преимущество заключается в современном движке декомпиляции. Язык C# постоянно эволюционирует: появляются локальные функции, switch-выражения, сложные паттерны сопоставления. ILSpy умеет распознавать эти современные конструкции в IL-коде и восстанавливать их в лаконичный, легко читаемый C#.

Особенно ярко это проявляется при работе с асинхронным кодом и корутинами (coroutines), которые в Unity используются повсеместно. На уровне IL корутина превращается в сложную конечную автоматную машину (state machine) с десятками переходов. ILSpy распознает этот паттерн и сворачивает его обратно в аккуратный метод с операторами yield return.

Вердикт: ILSpy используется для чтения кода, статического анализа и поиска связей. Это ваша основная библиотека.

dnSpy (и dnSpyEx): Хирург и отладчик

Оригинальный dnSpy был заброшен автором, но сообщество поддерживает форк под названием dnSpyEx. Движок декомпиляции dnSpy безнадежно устарел. Он часто не справляется с современным синтаксисом C#, выдавая полотна нечитаемого спагетти-кода при попытке разобрать генераторы или асинхронные методы.

Однако у dnSpy есть две киллер-фичи, которых нет у ILSpy:

  • Редактирование IL в реальном времени. Вы можете изменить логику метода прямо в декомпиляторе и пересохранить DLL. Хотя для создания модов мы используем Harmony (чтобы не ломать совместимость), прямое редактирование DLL бесценно для быстрых тестов и проверки гипотез.
  • Встроенный отладчик Unity. dnSpy умеет подключаться к запущенному процессу игры, ставить точки останова (breakpoints) и просматривать значения переменных в оперативной памяти.
  • Вердикт: dnSpy используется исключительно для динамического анализа (отладки) и точечных инъекций при прототипировании.

    Подготовка среды: Загрузка Assembly-CSharp.dll

    Первый шаг к изучению анатомии игры — правильная загрузка библиотек в декомпилятор.

  • Разрядность имеет значение. Assembly-CSharp.dll в WotR весит около 80 МБ. В развернутом виде, с построенными индексами и абстрактными синтаксическими деревьями, декомпилятору потребуется более 2 ГБ оперативной памяти. Если вы запустите 32-битную версию инструмента, процесс упрется в архитектурный лимит выделения памяти (около байт) и упадет с ошибкой OutOfMemoryException. Всегда используйте 64-битные версии (ILSpy.exe или dnSpy.exe, а не их x86 аналоги).
  • Точка входа. Откройте декомпилятор и перетащите в него файл Pathfinder Wrath Of The Righteous\Wrath_Data\Managed\Assembly-CSharp.dll.
  • Разрешение зависимостей. Игра опирается на движок Unity. Декомпилятор автоматически подтянет связанные библиотеки (например, UnityEngine.CoreModule.dll), если они лежат в той же папке Managed. Если вы скопируете Assembly-CSharp.dll на рабочий стол и откроете оттуда, декомпилятор не найдет зависимости. Многие типы будут подсвечены красным, а статический анализ сломается. Всегда открывайте DLL прямо из папки игры.
  • Статический анализ: Искусство навигации по коду

    Читать 80 мегабайт кода сверху вниз бессмысленно. Реверс-инжиниринг — это движение от известного к неизвестному.

    Поиск по строкам и типам

    Самый простой способ найти механику — искать текстовые константы. Если в игре есть способность "Cleave", логично нажать Ctrl+F в ILSpy и искать тип или член класса с таким именем. Декомпиляторы позволяют фильтровать поиск: искать только классы (Types), только методы (Members) или только константы (Constants).

    Инструмент Analyzer (Анализатор)

    Поиск по имени дает вам конкретный класс, например RuleDealDamage, который отвечает за применение урона. Но как узнать, кто вызывает этот урон? Когда он применяется? Для этого используется Анализатор (вызывается правой кнопкой мыши по классу или методу -> Analyze).

    !Дерево зависимостей в окне Analyzer

    Анализатор строит граф зависимостей на основе IL-инструкций. Ключевые узлы графа:

  • Used By (Используется в): Показывает все методы во всей игре, которые вызывают выбранный метод. Это позволяет раскрутить цепочку вызовов в обратную сторону — от низкоуровневой механики до пользовательского интерфейса.
  • Uses (Использует): Показывает, к каким другим методам обращается исследуемый код.
  • Assigned By (Назначается в): Критически важно при исследовании полей. Если вы нашли поле HP и хотите узнать, где игра отнимает здоровье, вы смотрите узел "Assigned By", который покажет все места в коде, где в это поле записывается новое значение.
  • Архитектура данных: Введение в BlueprintCatalog

    Исследуя код WotR, вы быстро заметите паттерн: логика отделена от данных. Классы вроде RuleDealDamage содержат только математику (как вычесть броню из атаки). Но где хранятся сами параметры? Сколько базового урона наносит огненный шар? Какой радиус у ауры паладина?

    Вся база данных игры построена на системе Блюпринтов (Blueprints). Блюпринт — это сериализованный объект данных, наследник SimpleBlueprint. При запуске игры тысячи блюпринтов загружаются в оперативную память и помещаются в глобальный реестр — BlueprintCatalog.

    В ILSpy вы можете найти статический класс Kingmaker.Blueprints.ResourcesLibrary. Это сердце системы данных. Метод TryGetBlueprint<T>(string assetId) — это то, как игра извлекает данные. Каждый блюпринт имеет уникальный идентификатор (Asset ID, он же GUID), представляющий собой 32-значную строку.

    Если вы хотите изменить параметры заклинания, вам не нужно патчить код через Harmony. Вам нужно найти блюпринт этого заклинания в BlueprintCatalog и изменить его поля. Но как узнать GUID конкретного заклинания среди десятков тысяч других? Здесь статический анализ бессилен, и в дело вступает динамический поиск.

    Мост между игрой и кодом: ToyBox Search 'n Pick

    Для поиска конкретных данных (Ground Truth) моддеры используют инструмент, созданный самим сообществом — мод ToyBox. Это швейцарский нож, который имеет доступ к рантайму игры.

    Вкладка Search 'n Pick в ToyBox — это интерфейс прямого доступа к BlueprintCatalog.

  • Запустите игру с установленным ToyBox.
  • Откройте Search 'n Pick и введите название интересующего объекта (например, "Magic Missile").
  • ToyBox отфильтрует загруженные блюпринты и покажет вам класс объекта (например, BlueprintAbility) и его GUID (например, 4ac47ddb9fa1eaf43a1b6809980cfbd2).
  • Получив эту информацию, алгоритм действий замыкается:

  • Вы берете класс BlueprintAbility, открываете ILSpy, находите этот класс и изучаете, какие поля в нем есть (урон, радиус, тип магии).
  • Вы берете GUID, используете библиотеку BlueprintCore (которую мы настроим в следующих главах) и пишете код, который при загрузке мода извлекает этот блюпринт из каталога и меняет значения его полей.
  • Динамический анализ: Подключение отладчика dnSpy

    Иногда статический анализ в ILSpy заходит в тупик. Код WotR активно использует интерфейсы, абстрактные классы и систему событий (Event Bus). Если вы анализируете метод OnEventAboutToTrigger, Анализатор может показать, что этот метод вызывается через абстрактный интерфейс IUnitSubscriber. Найти конкретное место вызова статически становится невозможно — связь формируется только во время выполнения игры.

    В этот момент мы закрываем ILSpy и открываем dnSpy для динамического анализа.

    !Подключение отладчика dnSpy к процессу Unity

    Процесс перехвата управления:

  • Запустите WotR.
  • В dnSpy в верхнем меню выберите Debug -> Attach to Process.
  • В списке процессов найдите Wrath.exe. Убедитесь, что тип отладчика установлен на Unity (Mono). Нажмите Attach.
  • Найдите в dnSpy интересующий вас метод (например, RuleDealDamage.OnTrigger).
  • Кликните на левое поле рядом с номером строки кода. Появится красный круг — точка останова (Breakpoint).
  • Теперь вернитесь в игру и совершите действие, которое должно вызвать этот код (ударьте противника). В ту же миллисекунду игра намертво зависнет. Окно dnSpy выйдет на передний план, а строка с точкой останова подсветится желтым.

    Вы поймали игру за руку. В нижней панели Locals (Локальные переменные) вы увидите точное состояние оперативной памяти в этот момент: кто атакует, кто защищается, какие баффы висят на персонажах, чему равен бросок кубика. Вы можете выполнять код пошагово (клавиша F10), наблюдая, как игра проходит по ветвлениям if/else. Это абсолютный Ground Truth — вы видите не то, как код должен работать, а то, как он работает прямо сейчас.

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

    10. Документирование находок: Создание локальной базы знаний для разработки мода

    Документирование находок: Создание локальной базы знаний для разработки мода

    Вы провели три часа в dnSpy, отслеживая цепочку вызовов от клика в интерфейсе до финального расчета урона. Вы нашли идеальный Choke Point, выписали на листок 32-значный GUID нужного баффа и закрыли декомпилятор. Через неделю, открыв IDE для написания кода, вы смотрите на строку 5d7b6a... и не можете вспомнить: это GUID самого заклинания, его визуального эффекта или скрытого технического компонента, который накладывается при критическом попадании?

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

    !Страница лабораторного журнала Александра Грэма Белла

    Разработка сложной модификации требует подхода, близкого к ведению строгого лабораторного журнала. Локальная база знаний (Local Knowledge Base, LKB) становится мостом между сырыми данными из декомпилятора и готовым C# кодом.

    Инструментарий: Почему Markdown и графы

    Для документирования находок в WotR не подходят линейные текстовые редакторы (Word, Блокнот). Структура данных игры — это направленный граф, где один BlueprintCharacterClass ссылается на десятки BlueprintProgression, которые, в свою очередь, активируют сотни BlueprintFeature.

    Оптимальным выбором становятся системы ведения заметок с поддержкой двунаправленных ссылок (Zettelkasten) и графового отображения, такие как Obsidian, Logseq или Roam Research.

    Их архитектура идеально ложится на архитектуру движка Owlcat:

  • Файл заметки = Блюпринт. Каждая сущность в игре получает отдельный файл.
  • Двунаправленная ссылка = BlueprintReference<T>. Если в заметке о заклинании вы ставите ссылку [[Бафф_ОгненныйЩит]], система автоматически показывает, что бафф вызывается этим заклинанием (Backlinks), имитируя инструмент Analyzer из декомпилятора.
  • Теги = Типы компонентов. Пометка #ConditionsChecker позволяет мгновенно найти все разобранные вами блюпринты, использующие сложную логику ветвления.
  • !Граф связей блюпринтов в Obsidian

    C# Ledger: Паспортизация GUID

    Первое правило внедрения базы знаний: строковые идентификаторы (GUID) не должны существовать в вакууме. Хранение их в Markdown-заметках полезно для теории, но для практики они должны быть немедленно перенесены в код в виде строго типизированного реестра.

    Оставлять «магические строки» непосредственно в методах мода — грубая архитектурная ошибка. Опечатка в одном символе GUID приведет к тому, что ResourcesLibrary.TryGetBlueprint вернет null, что вызовет NullReferenceException на этапе выполнения (рантайме), обрушив логику игры.

    Для решения этой проблемы создается статический класс-реестр (Ledger). Он выполняет роль единого источника истины (Single Source of Truth) для всех идентификаторов, которые вы нашли через ToyBox или чтение .jbp файлов.

    Такая структура дает три преимущества:

  • Автодополнение (IntelliSense): При написании логики вы обращаетесь к Guids.Spells.HellfireRay, не вспоминая сам хэш.
  • Защита от опечаток: Компилятор не позволит собрать мод, если вы ошиблись в имени константы.
  • Документируемость: XML-комментарии над константой могут содержать прямую ссылку на локальную заметку с разбором этого блюпринта.
  • Досье блюпринта: Стандартизация заметок

    Когда вы исследуете сложную механику (например, цепочку квестов или многоступенчатое заклинание), каждый ключевой узел должен получить свое «досье» в вашей LKB. Стандартизация полей позволяет быстро считывать информацию спустя месяцы.

    Структура эффективного досье блюпринта содержит следующие блоки:

    1. Метаданные (Frontmatter) Обязательный блок в начале файла. Содержит GUID (для быстрого поиска), оригинальное внутреннее имя (Internal Name) и тип C#-класса.

    2. Иерархия (Родители и Дети) Где используется этот блюпринт и что вызывает он сам. В WotR логика часто делегируется. Заклинание не наносит урон само — оно спавнит снаряд, который накладывает бафф, внутри которого лежит ActionList, вызывающий ContextActionDealDamage.

    3. Ключевые компоненты (ComponentsArray) Перечисление только тех компонентов, которые критичны для вашей задачи. Нет смысла переписывать весь JSON. Если вы изучаете, как работает бонус к характеристике, зафиксируйте только AddStatBonus и его параметры (тип бонуса, дескриптор, значение).

    Пример заполненной заметки в Markdown:

    Такое досье экономит часы повторного реверс-инжиниринга. Вы сразу видите, что модификация урона этого заклинания требует работы с данными (BlueprintCore), а не вмешательства в исполняемый код через Harmony.

    Паспортизация точек перехвата (Harmony Passports)

    Если данные блюпринтов статичны и их связи можно отследить через текстовый поиск, то динамическое выполнение кода требует иного подхода к документированию. Когда вы находите Choke Point для внедрения Harmony-патча, вы вторгаетесь в конвейер вычислений реального времени.

    Оставить в коде пустой атрибут [HarmonyPatch(typeof(RuleCalculateDamage), nameof(RuleCalculateDamage.OnTrigger))] — значит заложить мину замедленного действия. Через полгода выйдет официальный патч игры, сигнатура метода изменится, мод сломается, а вы не сможете вспомнить, зачем вообще перехватывали именно эту фазу RulebookEvent.

    Каждый сложный патч должен сопровождаться «Паспортом перехвата» в вашей базе знаний.

    Структура паспорта перехвата

  • Target (Цель): Полное имя класса и метода, включая типы аргументов (особенно важно для перегруженных методов).
  • Phase (Тип патча): Prefix, Postfix, Transpiler или Finalizer.
  • Justification (Обоснование Choke Point): Почему выбран именно этот метод? Почему не метод выше или ниже по стеку вызовов?
  • State Assumptions (Ожидаемое состояние): В каком состоянии находятся данные на момент входа в метод? (Например: «Ожидается, что DamageBundle уже инициализирован, но модификаторы критического урона еще не применены»).
  • Side Effects (Побочные эффекты): Что именно делает ваш патч? (Например: «Принудительно устанавливает флаг IgnoreDamageReduction = true, если у атакующего есть бафф X»).
  • Пример паспорта в локальной вики:

    В самом C# коде мода над классом патча оставляется краткая выжимка и ссылка (даже если это просто текстовое название заметки) на этот паспорт:

    Синхронизация: Отрыв базы знаний от реальности

    Главная угроза любой документации — рассинхронизация с кодом. В процессе отладки вы можете обнаружить, что выбранный Choke Point вызывает баг при атаке по площади (AoE), и перенесете патч в другой метод. Если при этом не обновить паспорт в базе знаний, документация превратится из помощника в источник дезинформации.

    Для предотвращения этого применяется правило «Единого коммита». В рамках разработки мода изменение бизнес-логики (перенос патча, смена GUID, изменение архитектуры) не фиксируется в системе контроля версий (Git) до тех пор, пока не обновлена соответствующая заметка в LKB.

    Если вы используете Obsidian или систему на базе обычных папок, саму базу знаний можно и нужно хранить в том же репозитории, что и исходный код мода (например, в папке docs/architecture). Это гарантирует, что при откате к старой версии кода (checkout) вы одновременно получите доступ к той версии документации, которая была актуальна на момент написания этого кода.

    Документирование находок — это инвестиция времени, которая окупается на этапе отладки и поддержки. Игры масштаба Pathfinder обновляются годами, ломая старые механики. Когда очередной патч от разработчиков изменит иерархию классов, именно подробные паспорта перехватов и досье блюпринтов позволят вам восстановить мод за вечер, а не начинать реверс-инжиниринг с нуля.

    2. Навигация по Assembly-CSharp.dll: Поиск логики в пространстве имен Kingmaker

    Навигация по Assembly-CSharp.dll: Поиск логики в пространстве имен Kingmaker

    Библиотека Assembly-CSharp.dll в Pathfinder: Wrath of the Righteous весит около 40 мегабайт в скомпилированном виде и содержит более 30 000 классов. Открытие этого файла в декомпиляторе впервые вызывает закономерную дезориентацию: список пространств имен уходит за пределы экрана, а попытка найти механику атаки или расчета характеристик через обычный текстовый поиск выдает тысячи результатов. Разработка модов невозможна без понимания внутренней географии кода Owlcat Games. Весь ключевой код игры, определяющий правила, сущности, интерфейс и логику, инкапсулирован в глобальном пространстве имен Kingmaker.

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

    Топография пространства имен Kingmaker

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

    !Архитектура пространства имен Kingmaker

    Kingmaker.Blueprints (Статические данные)

    Здесь хранятся классы, описывающие структуру всех статических данных в игре. В предыдущих материалах упоминалось, что блюпринты — это сериализованные объекты. В этом пространстве имен лежат C#-классы, которые выступают шаблонами для этих объектов. Например, класс BlueprintSpellbook определяет, какие поля есть у книги заклинаний (максимальный круг, атрибут для каста, список доступных заклинаний).

    Важно понимать: классы в Kingmaker.Blueprints не содержат игровой логики. Они содержат только поля данных и геттеры. Пытаться найти здесь метод, который наносит урон, бессмысленно. Это чертежи, по которым игра собирает объекты при загрузке.

    Kingmaker.EntitySystem (Сущности и компоненты)

    Это ядро архитектуры игры, построенное по принципу, близкому к ECS (Entity-Component-System). Главный класс здесь — Entity. Любой динамический объект в игре (персонаж, бочка, невидимый триггер ловушки) является наследником EntityDataBase.

    Ключевая особенность WotR в том, что сущности обрастают логикой через EntityFact и EntityComponent. Когда персонаж получает бафф (например, «Ускорение»), в его список фактов добавляется новый EntityFact, который содержит компоненты, описывающие влияние этого баффа на характеристики. Поиск логики работы пассивных способностей, аур и временных эффектов всегда ведет в это пространство имен.

    Kingmaker.Controllers (Управляющие системы)

    Контроллеры — это менеджеры, которые каждый кадр или каждый ход обходят сущности и заставляют игру работать. CombatController управляет очередностью ходов, RestController отвечает за механику привала, SummonController следит за временем жизни призванных существ. Если вам нужно изменить глобальные правила течения времени, инициативы или смены состояний игры, точки входа для Harmony-патчей следует искать именно в контроллерах.

    Kingmaker.View (Связь с движком Unity)

    Owlcat Games используют паттерн, строго изолирующий игровую логику от движка Unity. Классы в Kingmaker.View (например, UnitEntityView) являются наследниками UnityEngine.MonoBehaviour. Они висят на 3D-моделях в сцене, проигрывают анимации, двигают объекты в пространстве и воспроизводят звуки. Они ничего не знают о правилах Pathfinder.

    Разделение Data и View: Главная ловушка моддера

    Новички, приходящие в моддинг WotR с базовым опытом работы в Unity, часто совершают концептуальную ошибку. Они находят в памяти объект персонажа, получают его компонент NavMeshAgent (отвечающий за поиск пути в Unity) и пытаются изменить его скорость, чтобы персонаж бегал быстрее. В следующем же кадре скорость сбрасывается к исходному значению.

    Это происходит из-за жесткого разделения на Data (данные) и View (представление).

    В игре существует UnitEntityData — чистый C#-класс, живущий в оперативной памяти, не привязанный к сцене Unity. Он содержит характеристики (Сила, Ловкость, Скорость), инвентарь, список заклинаний. Параллельно существует UnitEntityView — объект на сцене Unity.

    Вектор зависимости всегда направлен в одну сторону: Data управляет View. Каждый кадр игра берет значение скорости из UnitEntityData.Stats.Speed и принудительно записывает его в NavMeshAgent внутри UnitEntityView.

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

  • Вы никогда не патчите классы *View, если хотите изменить механику игры.
  • Вы патчите классы *View только если хотите изменить визуал (заменить эффект частиц, отключить анимацию, изменить размер модели).
  • Все изменения характеристик, урона, шансов попадания производятся исключительно в классах *Data или в системах, которые их обрабатывают.
  • RuleSystem: Сердце механик Pathfinder

    Пространство имен Kingmaker.RuleSystem — это место, где настольные правила Pathfinder переведены в программный код. Любое значимое событие в игре (бросок кубика, атака, получение урона, проверка навыка, провокация атаки по возможности) оборачивается в объект, наследующий базовый класс RulebookEvent.

    Понимание работы RulebookEvent критически важно, так как 90% модов, меняющих баланс или добавляющих новые классы, взаимодействуют с этой системой.

    Жизненный цикл RulebookEvent

    Когда игра хочет нанести урон, она не просто отнимает здоровье у цели. Она создает объект события, например RuleDealDamage, заполняет его начальными данными (кто бьет, кого бьет, базовый урон) и отправляет в глобальный метод Rulebook.Trigger().

    !Жизненный цикл события RulebookEvent

    Внутри Trigger() событие проходит строгую последовательность фаз:

  • Инициализация (Initialization): Событие настраивает внутренние переменные.
  • Сбор модификаторов (OnEventAboutToTrigger): Игра опрашивает всех участников события. У атакующего проверяются баффы на увеличение урона. У защищающегося проверяются сопротивления. Все модификаторы записываются в само событие.
  • Выполнение (OnTrigger): Происходит фактическое применение правил. Бросаются кубики, высчитывается финальная математика. На этом этапе формируется итоговый результат.
  • Разрешение (OnEventDidTrigger): Результат применяется к игровому миру. Именно здесь у UnitEntityData отнимаются хитпоинты, а в лог боя выводится сообщение.
  • Для математических расчетов внутри правил часто используются формулы с модификаторами. Например, расчет финального урона в упрощенном виде выглядит так:

    Где — результат броска кубиков, — плоские прибавки (например, от Силы), — множители (критический удар), а — поглощение урона броней. Все эти компоненты хранятся как отдельные свойства внутри экземпляра RuleDealDamage.

    Вложенность правил

    События RulebookEvent редко существуют изолированно. Они порождают друг друга, образуя дерево вызовов.

    Рассмотрим цепочку событий при обычной атаке мечом:

  • Контроллер боя инициирует RuleAttackWithWeapon.
  • Внутри фазы выполнения RuleAttackWithWeapon игра понимает, что нужно узнать, попал ли персонаж. Она создает и триггерит дочернее событие RuleCalculateAttackBonus.
  • RuleCalculateAttackBonus в свою очередь триггерит RuleRollDice, чтобы бросить виртуальный двадцатигранник (d20).
  • Если бросок успешен, RuleAttackWithWeapon создает событие RuleDealDamage.
  • RuleDealDamage создает RuleCalculateDamage, чтобы учесть все сопротивления.
  • Такая гранулярность — подарок для реверс-инжиниринга. Если вы хотите сделать мод, который заставляет все атаки с фланга игнорировать броню, вам не нужно переписывать всю логику атаки. Достаточно найти класс RuleCalculateDamage, изучить его поля и написать Harmony-патч типа Postfix, который обнулит броню цели, если выполняется условие фланкирования.

    Стратегии поиска логики в декомпиляторе

    Зная топографию Kingmaker, можно выработать четкие алгоритмы поиска нужного кода в dnSpy или ILSpy. Поиск «в лоб» по названию механики часто не работает, так как внутренние имена классов могут отличаться от терминов в игре.

    Стратегия 1: Отталкивание от локализации (Поиск по строкам)

    Самый надежный способ найти механику — начать с того, что вы видите на экране. Допустим, вы хотите изменить механику состояния «Ошеломление» (Staggered).

  • Вы находите это слово в файлах локализации игры (обычно это JSON-файлы в папке игры или извлеченные через ToyBox).
  • Вы находите уникальный строковый ключ (Localization Key) этого термина, например "1234abcd-56ef-78gh-90ij-klmnopqrstuv".
  • В декомпиляторе вы используете глобальный поиск (Search) в режиме Number/String. Декомпилятор найдет место, где этот ключ используется.
  • Скорее всего, поиск приведет вас к BlueprintBuff, описывающему Ошеломление.
  • Изучив компоненты этого блюпринта (массив Components), вы увидите класс, например, AddCondition.
  • Открыв класс AddCondition, вы найдете логику применения состояния к UnitEntityData.
  • Стратегия 2: Отталкивание от RuleSystem

    Если мод касается боевой механики, начинать поиск нужно с пространства имен Kingmaker.RuleSystem.Rules. Вам нужно изменить механику провоцирования атак по возможности (Attacks of Opportunity)?

  • Откройте пространство имен Kingmaker.RuleSystem.Rules.
  • Пролистайте список классов. Вы быстро заметите класс RuleProvokeAttackOfOpportunity.
  • Откройте его и используйте инструмент Analyzer (Анализатор) декомпилятора.
  • Выберите метод OnTrigger или конструктор класса и нажмите Analyze -> Used By (Используется в).
  • Декомпилятор построит граф и покажет все места в коде игры, где персонаж может спровоцировать атаку (при стрельбе в ближнем бою, при перемещении, при вставании). Вы нашли все точки входа для модификации этой механики.
  • Стратегия 3: Отталкивание от интерфейсов и базовых классов

    Разработчики Owlcat активно используют полиморфизм. Многие механики реализованы как наследники специфических интерфейсов. Если вы хотите добавить новый тип действия в бою (например, уникальный рывок), вам нужно понять, как игра обрабатывает действия.

  • Найдите известное действие, например, движение — класс UnitMoveTo.
  • Посмотрите на его объявление: public class UnitMoveTo : UnitCommand.
  • Перейдите к базовому классу UnitCommand.
  • В декомпиляторе нажмите на UnitCommand и выберите Analyze -> Derived Types (Производные классы).
  • Вы получите полный список всех возможных команд в игре (атака, каст заклинания, использование предмета). Изучение их структуры даст вам готовый шаблон для создания собственной команды.
  • Поиск данных в Assembly-CSharp.dll — это процесс последовательного сужения контекста. Вместо того чтобы читать код как книгу, вы используете декомпилятор как поисковую систему по графу зависимостей. Вы находите статические данные (Blueprint), смотрите, какие компоненты (Component) к ним прикреплены, переходите в логику этих компонентов и анализируете, какие правила (RulebookEvent) они вызывают. Этот маршрут является фундаментальным навыком для создания сложных сюжетных и механических модификаций.

    3. Архитектура данных: Понятие Blueprint и структура библиотеки ресурсов игры

    Архитектура данных: Понятие Blueprint и структура библиотеки ресурсов игры

    Ролевая система Pathfinder: Wrath of the Righteous содержит более 200 базовых классов и архетипов, тысячи заклинаний, черт и предметов. Если бы разработчики попытались описать логику каждого заклинания или классовой способности отдельным C#-классом с жестко заданными параметрами, размер скомпилированной библиотеки Assembly-CSharp.dll превысил бы все разумные пределы, а добавление нового контента требовало бы постоянного переписывания исходного кода. Чтобы избежать коллапса архитектуры, студия Owlcat Games вынесла 95% игровой механики из монолитного кода в сериализованные данные — блюпринты (Blueprints).

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

    Анатомия базового объекта данных

    В основе всей базы данных игры лежит абстрактный класс SimpleBlueprint. Он наследуется от базового класса Unity ScriptableObject, что позволяет движку сериализовать эти объекты и сохранять их на диск в виде ассетов. Ранее мы уже упоминали, что доступ к блюпринтам осуществляется через глобальный реестр по уникальному GUID. Однако сам по себе GUID — это лишь ключ. Настоящая ценность кроется в полях класса.

    Если открыть SimpleBlueprint в декомпиляторе dnSpy, можно увидеть минимальный набор метаданных:

  • AssetGuid — строковое представление уникального идентификатора.
  • name — техническое имя объекта (наследуется от Object в Unity).
  • От SimpleBlueprint наследуется BlueprintScriptableObject, который добавляет критически важное поле — массив ComponentsArray. Именно этот массив определяет поведение подавляющего большинства игровых сущностей. От BlueprintScriptableObject выстраивается огромное дерево наследников: BlueprintFact, BlueprintItem, BlueprintCharacterClass, BlueprintDialog и сотни других.

    Каждый специфичный класс блюпринта содержит поля, характерные только для него. Например, BlueprintWeapon имеет поля DamageType, AttackRange и CriticalModifier, а BlueprintSpellbookCastingAttribute и SpellsPerDay. Это статические, неизменяемые данные. Блюпринт никогда не меняет свое состояние в процессе игры. Если меч наносит урона, эта информация зашита в блюпринте навсегда. Когда персонаж берет меч в руки, игра создает экземпляр предмета (ItemEntity), который ссылается на блюпринт меча как на эталонную инструкцию.

    Композиция вместо наследования: BlueprintComponent

    Самая частая ошибка при изучении архитектуры WotR — попытка найти специфичный класс для каждого сложного эффекта. Например, можно предположить, что для заклинания «Огненный шар» существует класс FireballSpell, наследуемый от AreaOfEffectSpell. В реальности архитектура Owlcat строится на жестком паттерне «композиция поверх наследования» (Composition over Inheritance).

    !Структура BlueprintScriptableObject и массива компонентов

    Блюпринт сам по себе ничего не делает. Это просто контейнер. Логика поведения задается через массив BlueprintComponent.

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

  • Компонент, отслеживающий критические попадания (AddInitiatorAttackWithWeaponTrigger).
  • Внутрь этого компонента вложить инструкции на нанесение урона и исцеление.
  • Каждый компонент имеет строгий жизненный цикл. Когда игра применяет блюпринт к персонажу (например, персонаж получает способность), система перебирает все компоненты в массиве блюпринта и вызывает у них методы инициализации (обычно OnTurnOn или OnActivate). Компоненты подписываются на события RulebookEvent, о которых мы говорили ранее, и начинают влиять на расчеты.

    Рассмотрим классический компонент AddStatBonus. В декомпиляторе его структура выглядит так:

  • Stat — перечисление (enum), указывающее, какую характеристику модифицировать (например, StatType.Strength).
  • Value — числовое значение бонуса.
  • Descriptor — тип бонуса (например, ModifierDescriptor.Enhancement, чтобы бонусы одного типа не складывались).
  • Когда блюпринт с этим компонентом добавляется сущности (в UnitEntityData), компонент регистрирует модификатор в системе характеристик. Формула итогового значения характеристики в памяти выглядит примерно так: . Блюпринт лишь предоставляет данные для слагаемых этой суммы.

    Инкапсуляция логики: ActionList и ConditionsChecker

    Не все действия в игре можно описать пассивными бонусами. Заклинания, диалоговые реплики, скриптовые сцены требуют выполнения последовательности команд: «проверить, жив ли NPC», «если да, переместить камеру», «нанести урон». Для сериализации алгоритмической логики внутри данных используются две мощные структуры: ActionList и ConditionsChecker.

    ActionList — это обертка над массивом объектов GameAction. GameAction представляет собой атомарное действие: DealDamage, SpawnFx, GiveGold, StartCombat. ConditionsChecker — это массив объектов Condition, которые возвращают логическое значение (true/false): PcHasItem, EtudeStatus, AlignmentCheck.

    Вместо написания C#-методов с ветвлениями if-else, разработчики собирают деревья из GameAction и Condition прямо внутри блюпринтов. Например, компонент AbilityEffectRunAction, который висит на большинстве активных заклинаний, содержит ровно одно ключевое поле — ActionList. Когда заклинание применяется, движок берет этот список и последовательно вызывает метод RunAction() у каждого элемента.

    Если требуется ветвление, используется специальный экшен Conditional. Внутри него лежат три поля:

  • ConditionsChecker (условие).
  • IfTrue (вложенный ActionList).
  • IfFalse (вложенный ActionList).
  • Изучая исходники через dnSpy, вы постоянно будете сталкиваться с этими структурами. Найти логику квеста означает найти блюпринт квеста, спуститься в его компоненты, найти там ActionList и прочитать цепочку сериализованных GameAction.

    Проблема связанности и BlueprintReference

    Игровая база данных представляет собой колоссальный направленный граф. Класс ссылается на прогрессию. Прогрессия ссылается на уровни. Уровни ссылаются на способности (Features). Способности ссылаются на заклинания. Заклинания ссылаются на баффы.

    Если бы в классе BlueprintCharacterClass поле прогрессии было объявлено как прямая ссылка public BlueprintProgression Progression;, сериализатор Unity при загрузке класса был бы вынужден загрузить в оперативную память прогрессию. Загрузка прогрессии потянула бы за собой все способности. Способности потянули бы баффы. В итоге попытка прочитать название одного класса привела бы к загрузке 90% базы данных игры в оперативную память, вызывая зависание и исчерпание лимитов RAM.

    Для решения проблемы каскадной загрузки Owlcat внедрили структуру BlueprintReference<T>.

    !Пошаговое разрешение BlueprintReference в памяти

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

  • public BlueprintCharacterClassReference m_BaseClass;
  • public BlueprintFeatureReference[] m_Features;
  • BlueprintReference<T> хранит внутри себя только строку — тот самый GUID целевого объекта. Сам целевой объект в памяти не существует до тех пор, пока он не потребуется логике игры.

    Когда коду нужно получить доступ к данным по ссылке, вызывается метод .Get(). В этот момент происходит следующее:

  • Обертка берет свой внутренний GUID.
  • Обращается к глобальному BlueprintCatalog.
  • Каталог проверяет внутренний словарь (поиск за ). Если блюпринт с таким GUID уже загружен в кэш, каталог возвращает прямую ссылку на него.
  • Если блюпринта в кэше нет, каталог обращается к файловой системе (ассет-бандлам), десериализует JSON/JBP файл в C#-объект, помещает его в кэш и только затем возвращает ссылку.
  • Для моддера это означает строгое правило: при создании новых блюпринтов или модификации существующих через код, связывание объектов всегда должно происходить через создание AnyBlueprintReference.Create(guid). Попытка вставить прямую ссылку в обход системы референсов сломает механизм сериализации и приведет к крашу игры при сохранении или загрузке.

    Деконструкция сложного блюпринта: BlueprintCharacterClass

    Чтобы закрепить понимание архитектуры, разберем структуру одного из самых массивных объектов — BlueprintCharacterClass. Если вы захотите добавить в игру новый класс (например, Антипаладина), вам придется вручную заполнять аналог этой структуры.

    В dnSpy класс BlueprintCharacterClass выглядит как огромный набор полей, определяющих каркас механики:

  • m_BaseAttackBonus — ссылка (BlueprintStatProgressionReference) на таблицу роста базового бонуса атаки в зависимости от уровня.
  • m_SavesPrestige, m_SavesFortitude, m_SavesReflex, m_SavesWill — ссылки на таблицы спасбросков.
  • m_Progression — ссылка на BlueprintProgression, которая определяет, какие способности выдаются на каждом уровне.
  • m_Archetypes — массив ссылок на BlueprintArchetype, которые могут модифицировать базовую прогрессию.
  • HitDice — перечисление (например, DiceType.D10), определяющее прирост здоровья.
  • ClassSkills — массив перечислений StatType, задающий классовые навыки.
  • IsDivineCaster, IsArcaneCaster — булевы флаги для интеграции с правилами магии.
  • Обратите внимание: сам BlueprintCharacterClass не содержит логики выдачи способностей. Он не знает, что на втором уровне паладин получает «Благодать». Он лишь хранит ссылку m_Progression. Если мы перейдем по этой ссылке в BlueprintProgression, то увидим поле LevelEntries — массив объектов LevelEntry. Каждый LevelEntry содержит номер уровня и массив ссылок на BlueprintFeature. И уже внутри конкретного BlueprintFeature будут лежать компоненты BlueprintComponent, которые непосредственно влияют на UnitEntityData персонажа.

    Это многослойная матрешка. Моддинг данных в WotR — это искусство навигации по этим слоям. Вы находите корень (класс), спускаетесь в прогрессию, из нее в уровень, из уровня в фичу (feature), из фичи в массив компонентов, из компонента в ActionList, и там меняете урон с на .

    Разделение на контейнеры (Блюпринты), строительные блоки (Компоненты) и скриптовые инструкции (Экшены и Условия) делает архитектуру WotR невероятно гибкой. Понимая, как движок читает эти данные через систему отложенных ссылок, можно конструировать механики любой сложности, не написав ни одной строчки исполняемого C#-кода, вмешивающегося в базовые системы. Достаточно правильно собрать структуру данных, и игра сама встроит ее в свой конвейер обработки правил.

    4. Работа с BlueprintCatalog: Программный доступ к базе данных игровых объектов

    Работа с BlueprintCatalog: Программный доступ к базе данных игровых объектов

    Любая попытка изменить характеристики оружия, добавить новый класс или переписать диалог неизбежно разбивается о суровую реальность движка Unity: на момент запуска игры в оперативной памяти нет ни мечей, ни классов, ни диалогов. Если ваш мод попытается найти заклинание Fireball сразу после старта процесса, он получит NullReferenceException. Игровые данные существуют в виде сжатых бинарных архивов на жестком диске, и игра извлекает их оттуда строго по мере необходимости. Понимание того, как именно происходит этот процесс извлечения и кэширования, является границей между модом, который работает стабильно, и модом, который вызывает утечки памяти и крашит сохранения.

    Архитектура ResourcesLibrary: Иллюзия глобального словаря

    В коде Pathfinder: Wrath of the Righteous за доступ к данным отвечает статический класс ResourcesLibrary. При поверхностном изучении через декомпилятор может показаться, что это гигантский словарь, хранящий все объекты игры. На практике это сложный фасад, скрывающий за собой систему управления памятью и ассетами Unity.

    Истинная база данных игры — это набор файлов AssetBundle, сгенерированных при сборке проекта. Каждый блюпринт упакован в один из таких бандлов. Когда вы пишете код мода, вы не можете напрямую обратиться к бандлу. Вы обращаетесь к ResourcesLibrary, который выполняет роль посредника (брокера) между вашим кодом и файловой системой.

    Ключевым элементом этого посредника является внутренний кэш — статический словарь s_LoadedBlueprints (название поля может слегка отличаться в разных версиях игры из-за обфускации, но логика неизменна). Этот словарь связывает строковый идентификатор (GUID) с уже загруженным в оперативную память экземпляром SimpleBlueprint (базовый класс для всех блюпринтов).

    !Схема связи GUID, AssetBundle и кэша в оперативной памяти

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

    Механика запроса: Жизненный цикл TryGetBlueprint

    Основной метод, с которым вы будете работать — ResourcesLibrary.TryGetBlueprint<T>(string assetId). Это дженерик-метод, который принимает GUID блюпринта в виде строки и возвращает строго типизированный объект, унаследованный от BlueprintScriptableObject.

    Рассмотрим пошагово, что происходит под капотом при вызове ResourcesLibrary.TryGetBlueprint<BlueprintItemWeapon>("8a404... "):

  • Проверка кэша (Cache Hit/Miss). Метод обращается к внутреннему словарю загруженных объектов. Доступ осуществляется за время . Если объект с таким GUID уже есть в словаре, метод немедленно возвращает ссылку на него.
  • Разрешение адреса (Address Resolution). Если произошел промах по кэшу (Cache Miss), система обращается к манифесту ресурсов (обычно это предварительно загруженный легковесный индекс). Индекс указывает, в каком именно AssetBundle на жестком диске лежит запрошенный GUID.
  • Десериализация (Deserialization). Движок Unity открывает нужный AssetBundle, находит бинарные данные объекта и десериализует их, создавая экземпляр C#-класса (в нашем случае BlueprintItemWeapon).
  • Инициализация (OnEnable). Вызываются внутренние методы Unity для инициализации ScriptableObject.
  • Кэширование. Созданный объект помещается в статический словарь s_LoadedBlueprints.
  • Приведение типов (Type Casting). Объект приводится к типу T (в данном случае BlueprintItemWeapon). Если GUID указывает на броню (BlueprintItemArmor), а вы запросили оружие, приведение типов вернет null, защищая код от ошибок несовпадения типов.
  • !Процесс ленивой загрузки блюпринта

    Эта архитектура называется ленивой загрузкой (lazy loading). Она критически важна для игр масштаба WotR, где объем сырых данных блюпринтов превышает доступную оперативную память средних ПК.

    Следствие для моддинга: первый вызов TryGetBlueprint для конкретного GUID всегда медленный (включает дисковые операции I/O), все последующие вызовы для этого же GUID — практически мгновенные, так как возвращают ссылку на один и тот же участок памяти.

    Модификация данных в оперативной памяти

    Самое фундаментальное правило моддинга WotR: кэш ResourcesLibrary является глобальным состоянием игры.

    Когда TryGetBlueprint возвращает вам объект, он не возвращает его копию. Он возвращает ссылку (reference) на область в куче (heap), где лежит этот блюпринт. Если вы измените значение поля в этом объекте, оно изменится для всей игры до тех пор, пока процесс не будет завершен.

    В этом примере мы навсегда (в рамках текущей сессии) изменили стоимость и вес длинного меча. Любой торговец, который попытается сгенерировать инвентарь, обратится к ResourcesLibrary, получит ссылку на тот же самый измененный объект и прочитает новую стоимость.

    Проблема ссылочных типов внутри блюпринтов

    Изменение примитивных типов (числа, строки) работает тривиально. Но блюпринты состоят из массивов компонентов (BlueprintComponent), списков действий (ActionList) и ссылок на другие блюпринты (BlueprintReference<T>). Здесь кроется частая ошибка.

    Допустим, вы хотите добавить новый эффект к заклинанию. Заклинание содержит массив ComponentsArray. В C# массивы имеют фиксированную длину. Вы не можете просто вызвать метод Add() у массива.

    При перезаписи ComponentsArray старый массив отправляется на съедение сборщику мусора (Garbage Collector), а блюпринт начинает ссылаться на новый массив, содержащий ваш компонент. Так как сборщик мусора в Unity может вызывать микрофризы, массовые перестроения массивов внутри блюпринтов следует делать один раз при загрузке мода, а не в процессе геймплея.

    Итерация по каталогу: Опасность GetBlueprints<T>

    Иногда возникает задача: «Я хочу увеличить урон всего рубящего оружия в игре на 1». Логичным шагом кажется перебрать все оружие в цикле. Для этого в ResourcesLibrary существует метод GetBlueprints<T>(), который возвращает IEnumerable<T>.

    Использование этого метода в рантайме (во время игрового процесса) — это архитектурное преступление.

    Вспомните механику ленивой загрузки. Если вы вызываете GetBlueprints<BlueprintItemWeapon>(), вы заставляете ResourcesLibrary просканировать весь индекс, найти сотни файлов AssetBundle, распаковать их все в оперативную память, десериализовать тысячи объектов оружия и поместить их в кэш.

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

    Безопасное использование итерации

    Если вам необходимо обработать массив блюпринтов, это нужно делать исключительно на этапе инициализации мода (во время показа экрана загрузки, до входа в главное меню).

    Для этого в Unity Mod Manager и библиотеке Harmony используются специальные точки входа, которые мы рассмотрим позже. Главное правило: если ваш код содержит ResourcesLibrary.GetBlueprints<T>(), он должен выполняться один раз за сессию.

    Даже при загрузке следует быть осторожным. Owlcat Games реализовали систему отложенной инициализации, и некоторые бандлы могут быть недоступны на самых ранних этапах запуска. Поэтому глобальные модификации обычно вешают на события пост-инициализации каталога (например, патчинг метода BlueprintsCache.Init).

    Взаимодействие с BlueprintReference<T>

    В предыдущей части мы разбирали структуру BlueprintReference<T> — обертку, которая позволяет одному блюпринту ссылаться на другой, не загружая его в память мгновенно. Теперь, понимая устройство ResourcesLibrary, мы можем точно описать, как работает эта связь.

    Внутри BlueprintReference<T> хранится строковое поле с GUID целевого объекта. Когда игровая логика обращается к свойству .Get() этой ссылки, под капотом происходит следующее:

  • Проверяется внутреннее закэшированное поле самой ссылки. Если оно не пустое, возвращается объект.
  • Если пустое, вызывается ResourcesLibrary.TryGetBlueprint<T>(this.AssetId).
  • Отрабатывает весь конвейер запроса (поиск в кэше s_LoadedBlueprints, чтение бандла, десериализация).
  • Полученный объект сохраняется во внутреннем поле ссылки и возвращается вызывающему коду.
  • Это означает, что модифицируя объект через ResourcesLibrary.TryGetBlueprint, вы автоматически модифицируете его для всех компонентов, которые на него ссылаются.

    Например, если вы изменили BlueprintCharacterClass (Класс Воина), вам не нужно искать все BlueprintProgression, которые на него ссылаются, и обновлять их. Когда прогрессия вызовет m_CharacterClass.Get(), она обратится к глобальному кэшу ResourcesLibrary и получит ваш измененный класс.

    Проблема порядка загрузки модов

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

    Если Мод А загружается первым и меняет урон меча с 1d8 на 2d6, а затем загружается Мод Б и меняет урон того же меча на 1d10, итоговым значением в игре будет 1d10. Мод Б просто перезаписал данные в оперативной памяти поверх изменений Мода А.

    Именно поэтому в файле Info.json менеджера UMM существует массив LoadAfter. Он позволяет выстроить топологическую сортировку загрузки. Однако при работе с ResourcesLibrary важно понимать: кто последним вызвал модификацию объекта в кэше, того и правда.

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

    Такой подход требует глубокого понимания того, в какой момент времени ваш код обращается к каталогу. Работа с ResourcesLibrary — это не статический SQL-запрос к базе данных, это динамическое вмешательство в живой граф объектов, постоянно меняющийся в оперативной памяти по мере того, как движок подгружает новые бандлы.

    5. Инструментарий Ground Truth: Использование ToyBox для поиска GUID в рантайме

    Инструментарий Ground Truth: Использование ToyBox для поиска GUID в рантайме

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

    В контексте реверс-инжиниринга WotR концепция Ground Truth (истинных данных) означает получение состояния объекта именно в том виде, в котором им оперирует движок в данный момент времени. Идеальным мостом между визуальным представлением игры и ее внутренними базами данных выступает модификация ToyBox, а конкретно — ее подсистема Search 'n Pick.

    Механика работы Search 'n Pick

    Интерфейс Search 'n Pick не является просто текстовым поисковиком по файлам локализации. Под капотом этот инструмент напрямую взаимодействует с ResourcesLibrary, о которой мы говорили ранее.

    Когда вы впервые открываете вкладку поиска и выбираете категорию (например, All или Blueprints), игра может кратковременно зависнуть. В этот момент ToyBox принудительно обходит ограничения ленивой загрузки. Он запрашивает у загрузчика ассетов метаданные всех доступных в игре блюпринтов, чтобы построить собственный индексированный кэш для быстрого поиска.

    !Схема потока данных: от ResourcesLibrary к интерфейсу ToyBox

    Поиск осуществляется сразу по нескольким векторам:

  • Localized Name (Локализованное имя): Текст, который выводится в UI игры (названия предметов, описания способностей). Поиск здесь зависит от текущего языка игры.
  • Internal Name (Внутреннее имя): Название самого объекта ScriptableObject в проекте Unity (например, LongswordPlus1).
  • Asset ID (GUID): Уникальный 32-значный идентификатор.
  • Type (Тип блюпринта): Класс C#, к которому принадлежит объект (например, BlueprintItemWeapon).
  • Понимание того, что ToyBox ищет сразу по всем этим полям, критически важно для фильтрации мусорной выдачи. Если вы введете слово "Fire", вы получите и заклинания огня, и мечи с уроном от огня, и персонажей, в чьем внутреннем имени затесалось это сочетание букв.

    Стратегии эффективного поиска

    Поскольку база данных WotR огромна, прямой поиск по названию часто выдает десятки дубликатов. Разработчики из Owlcat Games создают множество скрытых версий одного и того же объекта для катсцен, специфических NPC или разных этапов квестов.

    Поиск через префиксы Owlcat

    Внутренние имена блюпринтов (Internal Names) подчиняются негласным правилам именования, которые разработчики использовали для организации проекта. Знание этих префиксов позволяет мгновенно отсекать ненужные результаты, даже если вы ищете объект по обрывочному описанию.

  • WPN_ — оружие (Weapons). Пример: WPN_FlamingLongsword.
  • EQ_ — экипировка (Equipment), включая броню, кольца и амулеты.
  • SP_ — заклинания (Spells).
  • PR_ — прогрессии классов (Progressions).
  • FEAT_ — черты и таланты (Feats).
  • BUFF_ — временные эффекты (Buffs).
  • Если вам нужен GUID заклинания "Fireball", надежнее искать не по слову "Fireball", а комбинировать фильтры: выбрать категорию BlueprintAbility и ввести SP_Fireball. Это исключит из выдачи свитки, зелья и эффекты попадания, оставив только саму способность.

    Разделение Способностей и Баффов

    Одна из самых частых ошибок при поиске данных — путаница между источником эффекта и самим эффектом. В архитектуре Kingmaker заклинание (то, что находится на панели действий) и эффект, который оно накладывает (иконка возле портрета), — это два совершенно разных блюпринта.

    Допустим, вы хотите изменить длительность заклинания "Haste" (Ускорение).

  • Вы ищете "Haste" в Search 'n Pick.
  • Находите BlueprintAbility с внутренним именем HasteSpell.
  • Копируете его GUID, идете в код, но не находите там поля Duration.
  • Причина в том, что BlueprintAbility описывает только дальность применения, время каста и анимацию. Сама логика наложения эффекта скрыта внутри массива ComponentsArray в компоненте AbilityEffectRunAction. Этот компонент содержит действие ContextActionApplyBuff, которое уже ссылается на BlueprintBuff с именем HasteBuff. Именно в BlueprintBuff хранится логика изменения характеристик персонажа.

    Поэтому при поиске эффектов всегда ищите связанную пару: Ability (источник) и Buff (состояние).

    Инспекция данных в реальном времени

    Главная ценность ToyBox для разработчика модов заключается не в кнопках добавления предметов, а в маленькой кнопке "Show Components" (или "Inspect"), доступной для каждого найденного блюпринта.

    Нажатие этой кнопки разворачивает дерево свойств объекта. ToyBox использует механизм Reflection (рефлексию) для обхода всех публичных и приватных полей класса и вывода их значений прямо в интерфейс игры.

    !Интерактивное дерево инспекции компонентов блюпринта

    Эта функция решает сразу три фундаментальные задачи:

  • Проверка структуры без декомпилятора. Вы можете увидеть, из каких BlueprintComponent состоит объект, не открывая dnSpy. Если вы видите у меча компонент WeaponEnchantment, вы сразу понимаете, где искать логику дополнительного урона.
  • Чтение значений. Вы видите точные числа: радиус взрыва, кубики урона ( или ), модификаторы.
  • Анализ мутаций от других модов. Это самое важное. Если вы откроете файлы игры или посмотрите в dnSpy, вы увидите «чистые» данные (ванильную версию). Но если у игрока установлен другой мод, который уже изменил этот блюпринт, ToyBox покажет текущее состояние в оперативной памяти. Это позволяет отлаживать конфликты совместимости: вы смотрите на объект через ToyBox и видите, применились ли изменения из вашего кода или их перезаписал мод, загруженный позже.
  • Практический маршрут: От визуального эффекта к коду

    Разберем алгоритм поиска Ground Truth на конкретной задаче. Цель: найти GUID логики мифической способности "Destructive Shockwave" (Разрушительная ударная волна), чтобы в будущем изменить формулу ее урона.

    Шаг 1: Локализация цели в игре. Мы знаем, что это мифическая черта (Mythic Feat), которая наносит урон при промахе в ближнем бою. В игре она называется "Destructive Shockwave".

    Шаг 2: Первичный поиск. Открываем ToyBox (Ctrl+F10), переходим в Search 'n Pick. Вводим "Destructive Shockwave". Играем на английском языке — это критически важно, так как русская локализация часто не совпадает с внутренними ключами, а поиск по русскому тексту может не выдать связанных служебных объектов, не имеющих перевода.

    Шаг 3: Фильтрация выдачи. В результатах мы видим несколько объектов. Нас интересуют типы BlueprintFeature (сама черта в окне прокачки) и, возможно, BlueprintAbility (если бы это была активируемая способность). Находим BlueprintFeature с внутренним именем DestructiveShockwaveFeature. Рядом с ним указан GUID: ...a4b7....

    Шаг 4: Инспекция логики. Нажимаем "Show Components" рядом с найденной чертой. Разворачивается список ComponentsArray. Внутри мы видим компонент типа AddInitiatorAttackWithWeaponTrigger. Раскрываем его свойства и видим условие: WaitForAttackResolve = true, OnlyHit = false. Это подтверждает нашу гипотезу — триггер срабатывает при любой атаке, включая промах. Смотрим глубже в поле Action этого триггера. Там находится ContextActionDealDamage.

    Шаг 5: Переход в статическую среду. Теперь у нас есть полная картина. Мы знаем точный GUID черты. Мы знаем, что логика урона лежит в компоненте AddInitiatorAttackWithWeaponTrigger. С этими данными мы можем открыть среду разработки (Rider/Visual Studio), использовать библиотеку BlueprintCore для получения этого блюпринта по GUID и программно изменить параметры внутри найденного компонента.

    Проблема скрытых связей и ссылок

    ToyBox отлично справляется с объектами верхнего уровня, но имеет слепые зоны при работе со сложными графами зависимостей.

    Некоторые механики реализованы через BlueprintUnlockableFlag. Это глобальные переменные состояния (флаги), которые отслеживают прогресс квестов, романов или глобальных решений. Найти нужный флаг через Search 'n Pick крайне сложно, так как у них часто нет локализованных имен, а внутренние имена выглядят как Romance_Camellia_Counter.

    Если вы ищете условия запуска определенного диалога, прямой поиск по тексту реплики приведет вас к BlueprintCue (конкретной фразе). Однако сама логика проверки (например, жив ли определенный NPC) находится в массиве ConditionsChecker внутри этого диалога. ToyBox покажет вам наличие этих условий, но для глубокого анализа логических цепочек (какой флаг активирует этот диалог, а какой квест меняет этот флаг) интерфейса игры уже недостаточно. В таких случаях найденный через ToyBox стартовый GUID используется как точка входа для статического анализа связей через Analyzer в dnSpy.

    Подмена объектов и клоны

    В процессе поиска вы неизбежно столкнетесь с ситуацией, когда по одному запросу ToyBox выдает несколько абсолютно идентичных по типу и названию блюпринтов, но с разными GUID.

    Например, при поиске компаньона "Seelah" вы найдете несколько BlueprintUnit. Один из них — это реальный компаньон, которого вы берете в группу. Другие — это невидимые клоны, используемые движком исключительно для рендеринга персонажа в окне инвентаря или в специфических катсценах пролога.

    Как определить «настоящий» Ground Truth:

  • Анализ компонентов: У настоящего компаньона всегда будет обширный массив компонентов, включая AddClassLevels, Experience, PregenUnitComponent. У клонов для катсцен этот список обычно пуст или содержит только визуальные настройки.
  • Проверка через Party View: В ToyBox есть вкладка "Party". Если вы найдете нужного персонажа там и посмотрите его свойства, вы увидите GUID именно того инстанса, который сейчас находится в вашей группе.
  • Имена с суффиксами: Клоны часто имеют внутренние имена с приписками _Cutscene, _Inventory, _Level1.
  • Использование ToyBox в связке с пониманием архитектуры Kingmaker превращает процесс поиска из слепого перебора в точную хирургическую операцию. Вы находите визуальное проявление в игре, извлекаете его GUID через Search 'n Pick, инспектируете текущее состояние компонентов и только после этого переносите полученные данные в код своего мода для внесения изменений.

    6. Декомпиляция сериализованных данных: Чтение .jbp файлов и JSON-представлений

    Декомпиляция сериализованных данных: Чтение .jbp файлов и JSON-представлений

    Если вам нужно найти все заклинания в игре, которые накладывают эффект ошеломления, использование ToyBox превратится в многочасовое ручное прокликивание сотен записей. Декомпилятор ILSpy здесь тоже не поможет: он покажет логику работы компонента AddCondition, но не скажет, к каким именно блюпринтам этот компонент прикреплен. Исходный код описывает правила мира, оперативная память хранит его текущее состояние, но фундамент — изначальные характеристики каждого меча, квеста и персонажа — хранится на жестком диске в виде сериализованных данных. В проектах Owlcat Games эти данные извлекаются датамайнерами из закрытых AssetBundles движка Unity и сохраняются в формате .jbp (JSON Blueprint).

    Умение читать сырой JSON-дамп игры — это навык получения доступа к Ground Truth в обход запущенного клиента игры. Это позволяет проводить массовый текстовый анализ, выявлять скрытые механики и точно понимать, как именно движок конструирует объекты при загрузке.

    Анатомия формата .jbp

    Файл .jbp — это обычный текстовый файл, содержащий данные в формате JSON (JavaScript Object Notation). Движок игры использует библиотеку Newtonsoft.Json для трансформации C#-объектов из памяти в текст и обратно.

    Когда вы открываете любой дамп блюпринта, вы видите корневой объект, который всегда содержит два фундаментальных ключа: AssetId и Data.

    Значение поля type, открываете dnSpy, нажимаете Ctrl+Shift+K (поиск типа) и вставляете это имя. Вы мгновенно получаете доступ к исходному коду компонента и можете изучить, как именно переменные m_BaseValueType и m_Progression обрабатываются в методах движка.

    Граф ссылок: Разрешение ref

    !Схема разрешения ref внутри одного файла

    Игровая логика полна дублирований и циклических связей. Например, одно и то же условие (Condition) может проверяться в нескольких ветках диалога внутри одного файла, или эффект может ссылаться на своего родителя. Если бы сериализатор просто вкладывал объекты друг в друга, файл разросся бы до гигантских размеров, а при десериализации циклической ссылки программа ушла бы в бесконечный цикл (StackOverflowException).

    Чтобы избежать этого, используется механизм PreserveReferencesHandling. При первой встрече объекта сериализатор записывает его полные данные и присваивает ему мета-поле id": "1", "Operation": "And", "Conditions": [ { "type": "ea981728db8a5f84888ecba390671a05, EtudeStatus", "name": "b3b6", "m_Etude": "!bp_df17" } ] } json "IfTrue": { "id и ref": "42"}. Чтобы понять, какая логика там скрыта, вам нужно воспользоваться текстовым поиском по файлу, найти строку "ref, связи между разными независимыми блюпринтами (например, заклинание ссылается на бафф, который оно накладывает) реализуются через структуру BlueprintReference<T>.

    В JSON-дампах Owlcat эта обертка сериализуется в специфический строковый формат с префиксом !bp_.

    Префикс !bp_ (blueprint) сообщает кастомному конвертеру Newtonsoft, написанному разработчиками игры, что следующая за ним строка — это GUID. При загрузке игры в память этот конвертер перехватывает строку, извлекает GUID 03464790f40c3c24aa684b57155f3280 и создает структуру BlueprintReference<BlueprintBuff>, которая в дальнейшем будет обработана через ResourcesLibrary.

    Для моддера это означает, что поиск зависимостей становится тривиальным. Если вы хотите узнать, какие заклинания накладывают определенный бафф, вам достаточно взять GUID этого баффа, добавить префикс !bp_ и выполнить массовый поиск по папке со всеми .jbp файлами игры.

    Практический разбор: Реконструкция заклинания Snowball

    Рассмотрим, как глубокое чтение JSON позволяет понять механику без запуска игры и отладчика. Возьмем заклинание первого уровня «Снежок» (Snowball). Его логика: нанести урон холодом, который зависит от уровня заклинателя (до 5d6), и при провале спасброска ошеломить цель.

    В дампе блюпринта Snowball мы находим массив Components и ищем компонент, отвечающий за действия — AbilityEffectRunAction.

    Обратите внимание на объект Value внутри ContextActionDealDamage. Урон задан кубиком D6. Но сколько кубиков бросать? Поле DiceCountValue (количество кубиков) имеет тип ValueType: "Rank", а само значение Value равно 0.

    Если бы мы пытались угадать механику только по C# коду ContextActionDealDamage, мы бы увидели, что при ValueType.Rank движок обращается к некоему контексту. JSON дает нам точный ответ: в массиве Components этого же заклинания должен быть компонент ContextRankConfig, который настраивает этот «Ранг».

    Ищем дальше по файлу и находим:

    ``json { "DiceCount = \min(CasterLevel, 5)type этого компонента, идете в dnSpy, изучаете его поля и понимаете, как именно сконструировать свой собственный блюпринт для нового класса.

    Анализ сериализованных данных замыкает цикл реверс-инжиниринга. Декомпилятор (dnSpy) показывает как работает механика. Инструменты рантайма (ToyBox) показывают что происходит в данный момент. А сырые JSON-дампы показывают из чего эта механика была собрана разработчиками. Умение свободно перемещаться между этими тремя слоями абстракции отличает начинающего мододела от инженера, способного глубоко модифицировать архитектуру игры.

    7. Связи объектов: Исследование графа зависимостей между компонентами и блюпринтами

    Вы успешно изменили урон меча, отредактировав одно поле в JSON, и игра послушно применила новые правила. Окрыленные успехом, вы решаете добавить новую реплику в диалог с Голфри, которая должна выдать этот меч и запустить скрытый квест. Вы находите нужный файл, вписываете текст, загружаете игру — и при попытке заговорить с королевой диалоговое окно зависает, а лог Unity заполняется красными ошибками NullReferenceException. Проблема в том, что меч — это изолированный узел данных. Диалог, квест или сложное заклинание — это массивная паутина зависимостей. Изменение одного элемента без понимания того, кто на него ссылается и на кого ссылается он сам, неминуемо рушит логику движка.

    Архитектура направленного графа

    База данных Pathfinder: Wrath of the Righteous не является плоским списком или строгим деревом. Это колоссальный направленный граф, где узлами выступают блюпринты, а ребрами — ссылки BlueprintReference<T>.

    Когда вы открываете декомпилированный код или сериализованный .jbp файл, вы видите лишь локальный фрагмент этого графа. Особенность архитектуры Owlcat заключается в агрессивной декомпозиции: ни один сложный объект не хранит всю свою логику внутри себя. Вместо создания монолитного класса «Огненный шар», разработчики собирают его из десятка независимых узлов.

    Для моддера это означает необходимость освоить два фундаментальных метода навигации по данным: нисходящий (Top-Down) и восходящий (Bottom-Up) анализ. Без этого навыка невозможно найти Ground Truth — истинный источник механики, подлежащий изменению.

    Нисходящий анализ: От корня к листьям

    Нисходящий анализ применяется, когда вы знаете глобальную сущность (например, название квеста или имя NPC), но вам нужно найти конкретный триггер внутри нее. Вы двигаетесь по ссылкам !bp_ от родительского объекта к дочерним.

    Ярче всего этот подход раскрывается при разборе нарративных механик, к которым мы вплотную приближаемся в рамках создания сюжетных модов.

    Декомпозиция диалоговой системы

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

    Корневым узлом выступает BlueprintDialog. Он содержит базовые настройки (кто говорит, какой фон используется) и ссылку на стартовую реплику.

    Сама реплика NPC — это BlueprintCue. Внутри нее находится локализованный текст (через SharedStringAsset) и массив Answers — список ссылок на варианты ответов игрока.

    Вариант ответа игрока — это BlueprintAnswer. Он содержит текст ответа, условия его появления (массив ConditionsChecker) и, что самое важное, действия, которые произойдут при его выборе (массив OnSelect, являющийся ActionList).

    !Структура графа диалога в WotR

    Представим, что мы исследуем диалог, где игрок может запугать стражника. Нисходящий путь по JSON-файлам будет выглядеть так:

  • Открываем BlueprintDialog стражника. Видим стартовый BlueprintCue.
  • В стартовом BlueprintCue находим массив Answers, копируем GUID нужного BlueprintAnswer.
  • Открываем файл этого BlueprintAnswer. Видим текст «[Запугать] Пропусти меня, или пожалеешь».
  • Изучаем его OnSelect. В массиве действий Actions находим компонент StartCombat (начать бой) или UnlockFlag (записать в базу данных, что стражник напуган).
  • Если вы попытаетесь просто добавить текст ответа в BlueprintCue, игра сломается, потому что движок ожидает ссылку на объект типа BlueprintAnswer, который имеет свой собственный жизненный цикл и логику валидации.

    Ветвление через ConditionsChecker

    Граф диалога редко бывает линейным. Доступность узлов BlueprintAnswer или переход к следующему BlueprintCue часто регулируется массивом ConditionsChecker.

    Например, ответ доступен только если у игрока есть определенный предмет. Внутри BlueprintAnswer вы найдете условие HasFact или ItemsEnough, ссылающееся на GUID нужного меча. Движок при построении UI диалога асинхронно проверяет все условия в ConditionsChecker. Если хотя бы одно условие не выполняется (и не имеет флага Not), ребро графа обрывается, и ответ не рендерится на экране.

    Восходящий анализ: Поиск первоисточника

    Нисходящий поиск идеален, когда вы идете от общего к частному. Но в моддинге 80% времени занимает обратная задача. Вы видите следствие (на персонаже висит бафф, квест обновился, переменная романа увеличилась) и хотите найти причину — какой именно диалог или скрипт это вызвал.

    Проблема в том, что ссылки BlueprintReference<T> однонаправленные. Блюпринт баффа не знает, кто его наложил. Флаг романа не знает, какой диалог его переключает. В оперативной памяти (через ToyBox или dnSpy) найти обратные связи практически невозможно без полного сканирования всех загруженных объектов, что убьет производительность.

    Здесь на помощь приходит статический текстовый поиск по сырым .jbp файлам (Ground Truth).

    Отслеживание сюжетных флагов

    Ключевым элементом управления состоянием игры является BlueprintUnlockableFlag. Это простейший блюпринт, который по сути представляет собой глобальную целочисленную переменную (по умолчанию равную 0). Именно через них отслеживаются романы, решения мифических путей и скрытые счетчики концовок.

    Допустим, вы хотите сделать мод, расширяющий роман с Камелией. Вы знаете GUID флага, отвечающего за уровень симпатии: CamelliaRomance_Counter. Как найти все диалоги, которые его меняют?

    Вы открываете папку со всеми распакованными .jbp файлами игры в продвинутом текстовом редакторе (VS Code, Notepad++) и запускаете глобальный поиск по строке: !bp_[GUID флага].

    В результатах поиска вы увидите два типа связей:

  • Чтение (Conditions): Блюпринты, содержащие компонент FlagUnlocked или FlagInRange. Это диалоги, которые проверяют уровень романа, чтобы показать уникальную реплику.
  • Запись (Actions): Блюпринты, содержащие компонент UnlockFlag или IncrementFlagValue. Это и есть искомые триггеры — ответы игрока (BlueprintAnswer) или скриптовые события катсцен, которые изменяют отношение компаньона.
  • Восходящий анализ позволяет вам построить полную карту влияния переменной до того, как вы начнете писать код мода. Вы точно будете знать, в какие узлы графа нужно внедрить новые BlueprintAnswer, чтобы они корректно увеличивали этот флаг.

    Цепочки механик: От заклинания до урона

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

    Вы хотите изменить урон заклинания «Кислотный туман» (Acid Fog). Вы находите BlueprintAbility этого заклинания. Но внутри нет компонента ContextActionDealDamage. Урона там просто не существует.

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

    !Пошаговое делегирование логики в площадном заклинании

    Цепочка зависимостей выглядит так:

  • Способность (BlueprintAbility): Игрок нажимает кнопку. В массиве компонентов способности есть AbilityEffectRunAction. Внутри него — действие ContextActionSpawnAreaEffect, которое ссылается на GUID зоны.
  • Зона (BlueprintAbilityAreaEffect): Это невидимый объект на сцене. У него есть компонент AbilityAreaEffectRunAction, который срабатывает каждый раунд для всех, кто стоит в зоне. Внутри него — действие ContextActionApplyBuff, ссылающееся на GUID дебаффа.
  • Состояние (BlueprintBuff): Этот блюпринт вешается на конкретного персонажа в зоне. У него есть компонент AddFactContextActions, который реагирует на события жизненного цикла баффа (например, OnNewRound).
  • Урон: И только внутри события OnNewRound баффа лежит долгожданный ContextActionDealDamage, где прописаны кубики урона (например, кислоты).
  • Зачем разработчикам такая сложность? Ради переиспользования узлов графа. Тот же самый BlueprintBuff с уроном от кислоты может применяться не только заклинанием, но и ловушкой в подземелье, или аурой монстра. Если бы урон был жестко зашит в BlueprintAbility, его пришлось бы дублировать.

    Для моддера это означает, что изменение урона в BlueprintBuff повлияет на все источники в игре, которые на него ссылаются. Если вы хотите усилить «Кислотный туман» только для вашего нового класса, восходящий анализ покажет, что оригинальный бафф трогать нельзя. Вам придется создать клоны всех узлов цепочки: новый Buff новую AreaEffect новую Ability, и связать их новыми !bp_ ссылками.

    Скрытые связи: Глобальная машина состояний (Etudes)

    Самый коварный тип связей в WotR — это неявные зависимости. Вы можете изменить диалог, выдать предмет, поставить нужный BlueprintUnlockableFlag, но квест все равно не обновится. Причина кроется в подсистеме BlueprintEtude.

    Этюды — это глобальная иерархическая машина состояний игры. Они контролируют всё: от того, какие NPC стоят в таверне, до того, какая глава сюжета сейчас активна.

    Этюд обладает жизненным циклом (Started, Playing, Completed). Главная особенность этюдов — их иерархия. У этюда есть родитель (Parent) и дети (StartsWith). Правило движка гласит: дочерний этюд не может перейти в состояние Playing, если его родитель не активен.

    Представьте граф: Etude_Chapter2 Etude_DrezenSiege Etude_TavernDefenders. Внутри Etude_TavernDefenders есть компонент, который спавнит торговца.

    Если вы через мод или ToyBox попытаетесь принудительно запустить Etude_TavernDefenders во время первой главы, ничего не произойдет. Движок проверит граф вверх, увидит, что Etude_Chapter2 не активен, и заблокирует выполнение.

    Связь между диалогами и этюдами часто реализуется через триггеры. В диалоге вы не запускаете квест напрямую. Вы ставите флаг. А где-то в базе данных лежит BlueprintEtude, у которого в массиве компонентов висит EtudePlayTrigger с условием на этот флаг. Как только флаг меняется, этюд просыпается, переходит в состояние Playing, и уже его внутренний массив Actions выдает квест, спавнит врагов и меняет музыку.

    Чтобы распутать этот клубок при создании сюжетного мода, вам придется комбинировать методы. Сначала восходящим текстовым поиском найти, какой этюд реагирует на ваш флаг. Затем проверить родительскую иерархию этого этюда, чтобы убедиться, что он вообще может быть запущен в данный момент игры. И только потом нисходящим анализом изучить действия, которые этот этюд выполняет при старте.

    Понимание графа зависимостей — это водораздел между слепым редактированием JSON и осознанной инженерией модов. Умение читать связи позволяет предсказывать последствия изменений. Когда вы видите BlueprintCue, вы должны автоматически искать глазами Answers. Когда вы видите ContextActionSpawnAreaEffect, вы должны понимать, что настоящая механика спрятана на два уровня глубже. Этот навык статического анализа данных станет вашим главным инструментом, когда мы перейдем к написанию C# кода для автоматизированной генерации и связывания этих блюпринтов через BlueprintCore.

    8. Поиск точек входа для Harmony: Анализ методов на пригодность к патчингу

    Моддер решает удвоить стоимость всего продаваемого в игре оружия. Он открывает декомпилятор, находит класс ItemEntityWeapon, видит свойство Cost и пишет Harmony-патч, умножающий возвращаемое значение на два. Мод компилируется, игра запускается. Но при открытии окна торговли FPS падает до нуля, а через десять секунд игра аварийно завершается с переполнением стека (StackOverflowException). Проблема не в синтаксисе C# и не в библиотеке Harmony. Проблема в том, что свойство Cost вызывается не только при продаже, но и при сортировке инвентаря, расчете общей ценности лута на локации и даже при рендеринге всплывающих подсказок — тысячи раз за кадр. Более того, внутри оригинального геттера Cost происходил вызов другого метода, который моддер случайно зациклил своим патчем.

    Умение написать код патча — это лишь 10% работы. Остальные 90% — это поиск правильного места для внедрения. В архитектуре Pathfinder: Wrath of the Righteous, где миллионы строк кода тесно переплетены через систему компонентов и событий, выбор неверной точки входа гарантированно приведет к поломке смежных механик.

    Анатомия идеальной точки входа

    Библиотека Harmony работает на уровне среды выполнения (CLR). Она находит метод в памяти, копирует его оригинальный IL-код в новое место, а по старому адресу ставит безусловный переход (detour) на ваш код — Prefix или Postfix. Технически Harmony может перехватить почти любой метод. Но педагогически и архитектурно методы делятся на «безопасные» и «токсичные».

    Идеальная точка входа должна отвечать трем критериям:

  • Узкая специализация (High Cohesion). Метод должен делать ровно то, что вы хотите изменить, и ничего больше.
  • Изолированный контекст (Low Coupling). Изменение возвращаемого значения или состояния внутри метода не должно ломать логику вызывающих его функций (Callers).
  • Техническая доступность. Метод не должен быть встроен компилятором (inlined), не должен быть скрытым конечным автоматом (как корутины) и должен иметь стабильную сигнатуру.
  • Поиск «Узкого места» (Choke Point)

    В крупных базах кода логика часто распределена по слоям. Интерфейс вызывает контроллер, контроллер вызывает сервис, сервис обращается к данным. Ваша задача — найти в этой цепочке «бутылочное горлышко» (Choke Point), через которое гарантированно проходят все пути исполнения нужной механики, но не проходят посторонние.

    !Граф вызовов и узкое место

    Рассмотрим механику атаки исподтишка (Sneak Attack). Вы хотите изменить условие, при котором цель считается застигнутой врасплох. Если вы начнете искать логику через Analyzer (Used By) от компонента AddSneakAttack, вы увидите разветвленный граф. Атака исподтишка проверяется при выстреле из лука, при ударе мечом, при использовании некоторых заклинаний.

    Ошибкой будет пытаться патчить методы вроде UnitCombatState.IsFlanked или UnitPartConcealment.IsConcealed. Эти методы — слишком низкоуровневые. IsFlanked используется не только для урона, но и для ИИ противников (чтобы они понимали, стоит ли отступать), для анимаций и для срабатывания талантов вроде Outflank. Изменив логику здесь, вы сломаете поведение врагов.

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

    Технические ограничения Harmony в реалиях Unity

    Даже если метод идеально подходит логически, он может быть непригоден технически. Декомпиляторы (ILSpy, dnSpy) показывают C#-код, который был восстановлен из IL-инструкций. Но этот код не всегда буквально отражает то, что находится в оперативной памяти.

    Проблема Inlining (Встраивание методов)

    Компилятор JIT (Just-In-Time) в .NET агрессивно оптимизирует код. Если метод короткий (обычно меньше 32 байт IL-кода) и не содержит сложных ветвлений, JIT-компилятор может применить Inlining. Это значит, что тело метода копируется прямо в место его вызова, а сам метод как отдельная сущность в памяти перестает вызываться.

    Чаще всего жертвами Inlining становятся свойства (Properties):

    Если вы напишете Postfix для метода get_MaxHP, он может никогда не сработать. Движок игры при обращении к unit.MaxHP будет читать поле m_MaxHP напрямую, минуя геттер, потому что JIT оптимизировал вызов.

    Как распознать риск:

  • Метод состоит из 1-2 строк.
  • Метод помечен атрибутом [MethodImpl(MethodImplOptions.AggressiveInlining)].
  • Метод является простой оберткой над математической операцией, например .
  • Если вы столкнулись с таким методом, вам придется искать точку входа на уровень выше — патчить тот метод, который вызывает это свойство.

    Корутины и конечные автоматы (IEnumerator)

    В Unity часто используются корутины для логики, растянутой во времени (катсцены, анимации, задержки). В C# они возвращают тип IEnumerator.

    Если вы попытаетесь повесить Prefix на PlayDeathAnimation, вы перехватите только момент создания корутины, а не ее выполнение. Ключевое слово yield заставляет компилятор C# сгенерировать скрытый вложенный класс (конечный автомат), который реализует интерфейс IEnumerator. Вся реальная логика (анимация, ожидание, уничтожение) переносится в метод MoveNext() этого скрытого класса.

    В ILSpy такие классы выглядят как <PlayDeathAnimation>d__14. Чтобы изменить логику внутри корутины, вам придется патчить метод MoveNext этого сгенерированного класса, что крайне нестабильно: при любом патче игры от разработчиков номер d__14 может измениться, и ваш мод сломается. Вывод: методов, возвращающих IEnumerator, лучше избегать.

    Generic-методы (Шаблоны)

    Методы с обобщенными типами (Generics) патчить можно, но это требует явного указания типов при конфигурации Harmony. Если в игре есть метод public T GetComponent<T>(), вы не можете запатчить его «вообще для всех типов». JIT-компилятор создает отдельные версии этого метода в памяти для ссылочных типов (Reference Types) и значимых типов (Value Types).

    Если вам нужно перехватить логику компонента, ищите методы внутри самого компонента, а не патчьте механизмы их извлечения.

    Архитектура RulebookEvent как золотой стандарт

    В предыдущих материалах мы разбирали конвейер событий RulebookEvent. Для моддера, использующего Harmony, классы-наследники RulebookEvent (например, RuleDealDamage, RuleAttackWithWeapon, RuleCastSpell) — это самые безопасные и мощные точки входа во всей игре.

    Почему система правил Owlcat идеальна для патчинга:

  • Строгая фазность. Логика разделена на OnEventAboutToTrigger (подготовка) и OnTrigger (исполнение).
  • Инкапсуляция контекста. Объект правила содержит в себе всё необходимое: кто инициатор (Initiator), кто цель (Target), какое оружие используется, какие модификаторы применены. Вам не нужно собирать эти данные по всей игре.
  • Отсутствие побочных эффектов. Правила создаются, выполняются и уничтожаются. Они не висят в памяти постоянно, как Update() в MonoBehaviour.
  • Выбор между Prefix и Postfix в контексте правил

    !Жизненный цикл перехвата метода

    Harmony предоставляет два основных способа внедрения: Prefix (выполняется до оригинального метода) и Postfix (выполняется после). Выбор между ними при работе с правилами строго регламентирован логикой игры.

    Когда использовать Prefix: Prefix нужен, когда вы хотите изменить вводные данные до того, как игра начнет их обрабатывать, или когда вы хотите полностью отменить оригинальный метод (вернув false из Prefix). В системе правил идеальная точка для Prefix — метод OnEventAboutToTrigger. Именно здесь собираются бонусы, бросаются предварительные кубики скрытности и рассчитываются шансы. Например, если вы хотите, чтобы определенный класс всегда наносил максимальный урон по демонам, вы пишете Prefix для RuleCalculateDamage.OnEventAboutToTrigger. Вы проверяете расу цели, и если это демон, принудительно устанавливаете флаг MaximizeDamage = true. Оригинальный метод подхватит этот флаг и сделает всю остальную работу за вас.

    Когда использовать Postfix: Postfix получает управление, когда оригинальный метод уже отработал. Он может прочитать результат (через специальный параметр __result) и изменить его. В системе правил Postfix отлично ложится на метод OnTrigger. Например, вы хотите, чтобы при критическом попадании из лука цель отбрасывало назад. Вы пишете Postfix на RuleAttackWithWeapon.OnTrigger. Проверяете: если атака была успешной и это критический удар (данные уже рассчитаны оригинальным методом), вы спавните новое правило RuleForceMove и запускаете его.

    Пытаться изменить итоговый урон через Prefix в OnTrigger бессмысленно — оригинальный метод перезапишет ваши изменения. Пытаться читать финальный урон в Postfix для OnEventAboutToTrigger тоже нельзя — урон еще не рассчитан. Совпадение фаз Harmony и фаз Rulebook критически важно.

    Анализ состояния и опасности методов Update

    В архитектуре Unity есть методы, которые вызываются каждый кадр — Update(), LateUpdate(), FixedUpdate(). В WotR, благодаря паттерну Entity-Component-System (ECS) и разделению Data/View, чистая игровая логика редко висит в Update(). Однако визуальные контроллеры и UI работают именно так.

    Допустим, вы хотите изменить цвет полоски здоровья (HP bar) над персонажем, если он отравлен. Вы находите скрипт UnitHPBar и его метод Update(). Если вы повесите на него Postfix, ваш код будет выполняться раз в секунду для каждого персонажа на экране. Если на локации врагов, ваш патч сработает раз в секунду.

    Если внутри этого Postfix вы используете тяжелые операции:

  • Поиск объектов через GameObject.Find()
  • Чтение данных через рефлексию (Reflection)
  • Выделение памяти под новые строки (создание мусора для Garbage Collector)
  • Игра неминуемо начнет тормозить. Микрофризы будут накапливаться, пока сборщик мусора не остановит главный поток (Main Thread) для очистки памяти.

    Правило работы с часто вызываемыми методами: Если вам абсолютно необходимо запатчить Update или метод рендеринга, ваш код должен быть алгоритмически элементарным . Никаких циклов, никаких аллокаций памяти. Все необходимые данные (например, флаг IsPoisoned) должны быть заранее закэшированы в полях класса, а в Update должно происходить только чтение этого флага.

    Работа со ссылочными типами (ref и out)

    При анализе методов в декомпиляторе вы можете встретить параметры, передаваемые по ссылке.

    Методы с out или ref параметрами — это отличные точки входа, если вам нужно подменить промежуточные вычисления физики или поиска пути. Harmony позволяет перехватывать такие параметры. В вашем Prefix или Postfix вы просто объявляете параметр с тем же именем и модификатором ref (даже если в оригинале был out, в Harmony всегда используется ref).

    Это дает мощный контроль: вы можете написать Prefix, который сам рассчитает collisionPoint, присвоит ему значение и вернет false, запретив оригинальному методу тратить ресурсы процессора на сложные вычисления Raycast. Это паттерн «Короткое замыкание» (Short-circuiting), часто применяемый для оптимизации или создания модов на игнорирование препятствий (NoClip).

    Чек-лист пригодности метода

    Подводя черту под процессом анализа, перед написанием атрибута [HarmonyPatch] пропустите найденный метод через этот фильтр:

  • Где он находится? Это слой Data (надежно) или View (только визуал)?
  • Как часто он вызывается? Это событие (вызывается по триггеру) или цикл кадра (вызывается постоянно)?
  • Есть ли у него состояние? Это обычный метод или скрытый конечный автомат (IEnumerator)?
  • Не слишком ли он мал? Есть ли риск, что компилятор применил к нему Inlining?
  • Является ли он узким местом? Затрагивает ли он только нужную механику, или его используют смежные системы?
  • Умение читать граф вызовов в декомпиляторе и отвечать на эти вопросы до написания кода отличает профессионального инженера от человека, который собирает моды методом проб и ошибок.

    9. Практикум: Реконструкция алгоритма расчета урона по исходному коду

    Практикум: Реконструкция алгоритма расчета урона по исходному коду

    Вы атакуете Бабау длинным мечом +1 из холодного железа. Сила вашего персонажа равна 18. В логе боя появляется строка: «Нанесено 9 урона (1d8 + 5) — снижено на 10». Математика базового удара прозрачна, но когда в дело вступают мифические черты, множители критического удара, урон от скрытой атаки и множественные сопротивления цели, лог боя перестает давать ответы. Чтобы создать мод, изменяющий баланс оружия, добавляющий новый тип стихии или пробивающий иммунитеты, невозможно опираться на догадки. Необходимо точно реконструировать математическую модель игры путем статического анализа исходного кода.

    Разделение ответственности: Вычисление против Исполнения

    Открыв сборку Assembly-CSharp.dll в dnSpy, логично начать поиск с пространства имен Kingmaker.RuleSystem.Rules.Damage. Внутри мы обнаруживаем два фундаментальных класса, которые часто путают новички: RuleCalculateDamage и RuleDealDamage.

    Архитектура Owlcat строго разделяет процесс на две независимые фазы. RuleCalculateDamage — это чистая математика. Этот класс собирает все кубики, бонусы от характеристик, баффы и множители, чтобы сформировать итоговую абстрактную цифру. Он не изменяет состояние мира и не отнимает очки здоровья (HP).

    За исполнение отвечает RuleDealDamage. Он принимает результат вычислений, проверяет цель на неуязвимости (Immunities), применяет снижение урона (Damage Reduction, DR), рассчитывает энергетические сопротивления (Energy Resistance) и только после этого вызывает метод изменения HP у компонента UnitEntityData.

    !Конвейер обработки урона от броска кубика до изменения HP

    Такое разделение диктует стратегию моддинга. Если цель мода — добавить к атакам бонусный урон от интеллекта, точкой входа (Choke Point) станет RuleCalculateDamage. Если же задача — создать способность, игнорирующую любую броню и сопротивления, перехватывать нужно логику внутри RuleDealDamage.

    Анатомия DamageBundle и базовой математики

    Взглянув на исходный код RuleCalculateDamage.OnTrigger, мы не увидим простой переменной типа int. Урон в Pathfinder: Wrath of the Righteous представлен сложной структурой DamageBundle.

    Оружие редко наносит урон одного типа. Пылающий длинный меч наносит рубящий урон (физический) и урон огнем (энергетический). DamageBundle — это коллекция объектов базового класса BaseDamage. Каждый элемент коллекции рассчитывается изолированно, так как к огню и к стали применяются разные модификаторы и разные сопротивления цели.

    !Структура DamageBundle и наслоение модификаторов

    Рассмотрим декомпилированный процесс формирования физического урона внутри RuleCalculateDamage:

  • Инициализация кубиков (DiceFormula). Код обращается к WeaponStats, чтобы получить базовый урон оружия. Формула имеет вид , где — количество кубиков, — тип кубика.
  • Модификатор характеристики. Код проверяет тип хвата оружия (одноручный, двуручный, левая рука). Для двуручного хвата применяется формула . Функция Math.Floor жестко зашита в логику, поэтому дробные значения всегда округляются в меньшую сторону.
  • Наслоение модификаторов. Вызывается метод AddModifier, который добавляет бонусы усиления оружия (Enhancement Bonus) и баффы.
  • Особый интерес представляет реализация множителей при критическом ударе. В настольной системе Pathfinder множители не перемножаются между собой напрямую. Если у вас есть два эффекта, удваивающих урон (множитель ), итоговый урон не будет умножен на 4. Игра использует аддитивную систему множителей.

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

    Декомпилятор показывает, что точный урон (Precision Damage), такой как скрытая атака (Sneak Attack), инкапсулируется в отдельный объект DamageDescription с флагом IgnoreCritical. При итерации по DamageBundle алгоритм пропускает эти объекты, гарантируя, что скрытая атака никогда не умножается при критическом попадании.

    Механика снижения урона (Damage Reduction)

    Переместимся в класс RuleDealDamage, чтобы понять, почему атака на 9 урона была полностью поглощена демоном. Метод ApplyDamageReduction демонстрирует сложную логику фильтрации.

    Когда RuleDealDamage получает DamageBundle, он перебирает все компоненты DamageResistance на целевом юните. Физический урон (класс PhysicalDamage) содержит битовую маску Material (ColdIron, Adamantine, Silver) и Form (Slashing, Piercing, Bludgeoning).

    Алгоритм проверки в коде выглядит следующим образом:

  • Берется текущее значение физического урона .
  • Запрашивается значение DR цели (например, 10).
  • Проверяется условие пробития. Если в маске оружия есть флаг ColdIron, а DR цели имеет тип DR/Cold Iron, сопротивление игнорируется.
  • Если пробития нет, применяется формула: .
  • Важный нюанс, который раскрывает исходный код: игра отслеживает остаток DR. Если юнит получил удар двумя разными физическими эффектами в рамках одной атаки, DR применяется к первому, а его неиспользованный остаток переносится на второй. Это предотвращает двойное поглощение урона от одной сложной атаки.

    Практическая реализация: Патч абсолютного пробития

    Понимая архитектуру, мы можем написать Harmony-патч, который модифицирует эту логику. Допустим, мы создаем мифическую способность «Абсолютный удар», которая должна игнорировать любое снижение физического урона (DR), но оставлять энергетические сопротивления (Energy Resistance) нетронутыми.

    Анализ показал, что математика DR инкапсулирована в приватном методе ApplyDamageReduction внутри RuleDealDamage. Это идеальное «узкое место» (Choke Point). Мы применим паттерн Short-circuiting (короткое замыкание) с помощью Prefix-патча.

    В этом коде мы перехватываем конкретный этап конвейера. Мы не трогаем RuleCalculateDamage, сохраняя всю базовую математику кубиков и множителей. Мы не отменяем весь RuleDealDamage, позволяя игре корректно отнять HP и запустить триггеры получения урона. Мы точечно отключаем только фазу ApplyDamageReduction, вмешиваясь в состояние объекта BaseDamage по ссылке.

    Реверс-инжиниринг формул с плавающей точкой

    При реконструкции алгоритмов важно обращать внимание на типы данных. В RuleCalculateDamage часто встречаются конвертации между float и int. Движок Unity работает с позиционированием и временем в формате с плавающей точкой, но ролевая система Pathfinder оперирует строгими целыми числами.

    В исходном коде можно заметить частое использование структуры Kingmaker.Utility.CustomMath. Например, при расчете урона от способностей, масштабирующихся от дистанции, игра вычисляет расстояние во float, а затем применяет специфичные правила округления. Если вы пишете Postfix-патч для изменения итогового урона, вам необходимо вручную приводить ваши вычисления к int, иначе JIT-компилятор выдаст ошибку несоответствия типов при попытке записать результат в __result.

    Реконструкция RuleCalculateDamage дает универсальный ключ к пониманию всей ролевой системы игры. Тот же паттерн разделения на вычисление (RuleCalculate...) и исполнение (Rule...) применяется к броскам атаки (RuleCalculateAttackBonus и RuleAttackWithWeapon), спасброскам и лечению. Умение читать эти цепочки в декомпиляторе позволяет не просто менять цифры в JSON-файлах, а переписывать сами законы физики игрового мира.