Простейший C# моддинг Spaceflight Simulator
ВАЖНО! Эта статья не является универсальным пособием по созданию модов любой сложности, но она поможет вам сделать первые шаги в увлекательный и сложный мир моддинга. Каждый мод требует индивидуального подхода и занимает время и усилия.
Что нас ждёт?
Практически все Unity-игры написаны на языке C#, что позволяет относительно просто изменять код или добавлять функционал к коду игры. Основной код компилируется в библиотеки классов - DLL-файлы. Большая часть логики игры обычно находится в Assembly-CSharp.dll.
Здесь вы познакомитесь с основами реверс-инжиниринга и основными инструментами работы с кодом на C#.
Это будет нелегко
Почему не Android?
Все действия из этой статьи можно проделать только на ПК-версии Spaceflight Simulator (и аналогично для других игр, которые есть на нескольких платформах). На Android и других мобильных устройствах код игры компилируется в такой формат, который нельзя просто открыть и изменить, поэтому модификация DLL невозможна.
Reverse-Engineering
Чтобы приступить к моддингу игры, необходимо «заглянуть под её капот» и понять, как она работает. Это и есть реверс-инжиниринг.
Для этого можно использовать программы ILSpy и dnSpy — инструменты для просмотра и редактирования C# DLL. Я буду использовать dnSpy v6.1.8.
Нас встречает такой интерфейс.
Слева находится окно «Обозреватель сборок» — наше главное окошко для изучения игры. С его помощью можно заглянуть в сборки и узнать, какие классы, методы и переменные в них находятся.
Сборка - скомпилированный файл программы на C#
Теперь нужно загрузить все необходимые сборки в dnSpy. Для этого:
- В левом верхнем углу выберите «Файл» → «Открыть».
- Откроется проводник. Найдите папку с установленной игрой. В случае лицензионной версии обычно она находится по пути:
C:\Program Files (x86)\Steam\steamapps\common\Spaceflight Simulator - В этой папке откройте
Spaceflight Simulator_Data\Managedи выделите все файлы внутри. - Нажмите «Открыть», чтобы загрузить сборки в dnSpy.
Открыв все сборки, в Обозревателе сборок мы можем выбрать Assembly-CSharp.dll. Именно в этой сборке содержится основная логика игры. Остальные сборки — это системные и Unity-библиотеки. Однако это не значит, что они не важны!
Теоретически можно работать только с Assembly-CSharp.dll, но остальные сборки помогают получить более полное понимание кода и дают возможность компилировать изменения прямо в dnSpy.
Следует понимать, что dnSpy показывает нам не исходный код, а лишь его представление - псевдокод.
Псевдокод — это приближённая версия исходного C# кода, которую dnSpy восстанавливает из скомпилированной сборки. Он очень похож на оригинальный код, но некоторые детали могут отличаться, а имена переменных иногда упрощаются.
Это создаёт некоторые трудности с чтением кода, а также с компиляцией отдельных методов и классов.
Идея, анализ и изменение кода
Прежде чем углубляться в код сборки, важно задать себе вопрос: что именно мы хотим изменить или добавить в игре? Только после того, как цель ясна, имеет смысл приступать к анализу кода.
Я поставил себе цель: изменить удельный импульс двигателей в игре. Значит, надо найти класс или метод, отвечающий за двигатели. Наибольшее внимание привлекают пространства SFS.Parts и SFS.Parts.Modules. Жмём на них и смотрим содержимое
Пространство SFS.Parts не содержало ничего подходящего, а вот SFS.Parts.Modules содержит EngineModule. Скорее всего, то что нужно мне - там.
Открыв этот класс, я начинаю искать нужную часть кода — способ задания удельного импульса двигателя.
Метод Draw уже содержит почти то, что мне нужно. В нём можно увидеть, как создаются и отображаются характеристики двигателя, которые появляются при нажатии на него в игре.
drawer.DrawStat(40, (this.ISP.Value * (float)Base.worldBase.settings.difficulty.IspMultiplier).ToEfficiencyString(), null);
Как видно, удельный импульс (ISP) вычисляется как произведение базового удельного импульса двигателя на множитель сложности difficulty.IspMultiplier.
Нажимаю на ISP, и программа переносит меня в то место, где эта переменная объявлена.
Однако там видно только объявление поля, а само значение явно не задаётся — значит продолжаю копать дальше. Скорее всего, игра получает значение удельного импульса для каждого двигателя из ассетов.
Нажимаю на IspMultiplier в методе Draw и попадаю на свойство IspMultiplier.
Это свойство берёт текущий уровень сложности и находит подходящий множитель ISP в таблице значений ispMultipliers
Жму на последний difficulty и меня перекидывает к переменной, которая определяет текущий уровень сложности мира.
Дальше идут массивы значений, которые содержат множители для трёх уровней сложностей. Значит, существует и массив ispMultipliers.
Уже на данном этапе мы можем внести изменения в код и скомпилировать его.
Для этого жмём ПКМ по пустому пространству около нужных значений и выбираем «Изменить метод (C#)»
Открывается окно изменения кода, где мы вправе поменять значения. Увеличиваю множитель в два раза и нажимаю компилировать. Готово!
Теперь осталось в верхнем левом углу, во вкладке «Файл», выбрать «Сохранить модуль» — после этого все внесённые изменения будут применены в игре (значение удельных импульсов всех двигателей увеличится в два раза).
Однако у такого способа есть ряд недостатков:
- Изменения становятся неотделимы от самой игры — они будут действовать постоянно, пока вы вручную не вернёте исходные значения.
- Некоторые методы или классы могут не скомпилироваться корректно из-за ошибок при декомпиляции псевдокода.
Создание мода
В отличие от предыдущего способа, создание полноценной модификации лишено перечисленных недостатков, однако занимает больше времени и требует знаний C#.
Для создания мода нам понадобится среда разработки.
Я буду использовать Microsoft Visual Studio Community 2022 — это бесплатная и удобная IDE для работы с C#.
При установке Visual Studio важно выбрать нужную рабочую нагрузку -отметьте пункт «Разработка классических приложений .NET». Она включает всё необходимое для написания и сборки C#-проектов.
Если Visual Studio уже установлена, но в ней нет поддержки C#, откройте Visual Studio Installer и добавьте эту рабочую нагрузку вручную.
Открываем Visual Studio и создаём новый проект. Далее выбираем «Библиотека классов (.NET Framework)»
Для удобства можно переименовать файл с кодом — кликните по Class1.cs левой кнопкой мыши и выберите «Переименовать».
Аналогично можно переименовать и сам проект, и решение.
Теперь нам нужно добавить ссылку на игру — файл Assembly-CSharp.dll.
Это позволит обращаться к её классам и методам напрямую.
- В Обозревателе решений найдите раздел «Ссылки» и выберите «Добавить ссылку…».
- В открывшемся окне перейдите в знакомую папку:
...\Spaceflight Simulator_Data\Managed\ - Выберите файл Assembly-CSharp.dll и нажмите «Добавить».
- После этого убедитесь, что в менеджере ссылок стоит галочка напротив этого файла.
Теперь нужно определить сам мод в игре.
- Сначала подключим пространство имён Modloader, чтобы иметь доступ к необходимым базовым классам:
- Затем создаём свой класс мода, который наследует базовый класс
Mod:
- Перезапишем публичные свойства, которые характеризуют наш мод:
- уникальный идентификатор (
ModNameID), - название (
ModName), - описание (
ModDescription) и т.д.
Это позволяет модлоадеру распознать мод, отобразить его в списке и применить его функциональность.
Вернёмся к массиву с множителями. Мы не можем просто изменить его напрямую или использовать что-то вроде SetValue, потому что он закрытый (private) и только для чтения (readonly). Обычные способы присвоения нового значения здесь не сработают, потому придётся использовать Harmony для безопасного перехвата и изменения поведения методов игры на лету.
Теперь нам нужно добавить ссылку на Harmony.
Делаем это точно так же, как мы делали для Assembly-CSharp.dll:
- В Обозревателе решений выбираем «Ссылки» → «Добавить ссылку…».
- Находим библиотеку 0Harmony.dll и добавляем её в проект.
- Убедитесь, что в списке ссылок стоит галочка напротив файла.
В начале кода мы подключаем необходимые пространства имён:
using Harmonylib; // для работы с Harmony
using SFS.WorldBase; // так как массив с множителями хранится именно здесь
Harmonylibпозволяет использовать все возможности Harmony: создавать патчи, перехватывать методы и свойства.SFS.WorldBaseнужен, чтобы обращаться к массивуispMultipliersи другим классам игры, которые находятся в этом пространстве имён.
С помощью Harmony мы создаём патч для свойства IspMultiplier класса Difficulty
Здесь мы перехватываем вызов геттера IspMultiplier и с помощью метода Postfix изменяем результат, возвращаемый этим свойством (__result).
Внутри метода мы смотрим на текущую сложность игры (__instance.difficulty) и подставляем новые значения множителя ISP:
- Normal → 2.0
- Hard → 2.5
- Realistic → 3.0
public override void Early_Load() — это метод, который переопределяет базовый метод Early_Load из класса Mod.
Он вызывается очень рано при загрузке игры, ещё до того, как начнёт работать основной игровой код.
var harmony = new Harmony(ModNameID); — создаём объект Harmony для нашего мода.
ModNameID используется как уникальный идентификатор патчей, чтобы они не конфликтовали с другими модами.
harmony.PatchAll(); — Harmony автоматически находит все классы с атрибутом [HarmonyPatch] в проекте и применяет их.
Сборка
Выбираем добавленные ссылки (например, Assembly-CSharp.dll и 0Harmony.dll) и внизу, в свойствах ссылки, находим параметр «Копировать локально».
Ставим его в False.
Это важно, чтобы при сборке нашей библиотеки (.dll) не появлялось множество лишних файлов, которые не нужны для работы мода
Теперь откроем свойства нашего проекта, чтобы настроить сборку.
- В свойствах можно убедиться в названии сборки.
- Слева выбираем вкладку «Сборка».
- В поле «Выходной путь» указываем папку, куда будет собираться наш мод (обязательно в папке Mods в корневой папке игры).
ВАЖНО! Название сборки и название папки мода должны совпадать, иначе игра не сможет задетектить мод.
Сверху выбираем вкладку «Сборка» и нажимаем «Собрать решение»
Снизу увидим окошко с тем, что наша сборка успешно собралась и где собралась.
Готовый мод
Заходим в игру в меню Modloader. Мод корректно отображается.
Удельный импульс успешно увеличился в два раза.
Пример более сложного мода
Также я создал мод, который каждую секунду случайным образом меняет текстуру (ColorTex) бака
Я не буду объяснять, как он работает, так как это потребует целого ряда дополнительных статей и хороших знаний C# от вас. Однако это может послужить вам примером и помочь лучше разобраться в моддинге.
Итог
В этой статье на моём примере вы познакомились с основами реверс-инжиниринга, разобрались с некоторыми важными определениями и на практическом примере увидели, как осуществляется моддинг игры на Unity.
Вы узнали, какие инструменты используются — dnSpy для изучения и анализа кода, Visual Studio для создания собственного мода, и Harmony для безопасного изменения поведения игры.
По этой статье вы шаг за шагом можете создать тот же мод, что создал я, а затем попробовать другие идеи, например: увеличить максимальное физическое ускорение времени. Это поможет вам набить руку и начать понимать написанное.
Однако многое зависит от вас: насколько усердно вы будете изучать C# и разбираться в коде, насколько глубоко захотите погрузиться в мир моддинга. Эта статья — лишь первый шаг на долгом и увлекательном пути, который потребует практики, времени, терпения и желания учиться.
