У большинства игр нет официальной поддержки модов, но при этом постоянно появляются новости о том, как моддеры добавляют в игры новый контент. Часто речь идет о замене моделей, текстур или музыки, но бывают и полноценные новые игровые механики, т.е. моддеры внедряются в код игры и изменяют ее внутреннюю логику.
В этой статье мы рассмотрим, какие методы используется для того, чтобы игра, написанная на Unity, могла запускать сторонние моды: модификации библиотек игры и самого движка, подмена адресов в таблице импорта dll, вмешательство в загрузку Mono Runtime и другие.
О Beat Saber
В качестве примера я буду использовать Beat Saber — одну из самых популярных игр для VR-шлемов. Если у вас есть такой шлем, то, скорее всего, вы уже знаете, что такое Beat Saber. Если нет, то, возможно, вы видели хотя бы одно видео из игры в рекомендациях Youtube.
Единственное, что Beat Saber поддерживает из модов — это пользовательские уровни. В игре есть официальный редактор, но нет никакого сервиса, чтобы как-либо распространять свои творения, и, по моему мнению, вряд ли появится. В основе практически любого уровня лежит какая-то песня. Разрешить выкладывать такие файлы в своем официальном сервисе — значит привлечь к себе внимание правообладателей с их постоянными запросами что-то удалить или вообще с требованиями закрыть весь сервис.
Поэтому не удивительно, что вокруг Beat Saber сформировалось огромное сообщество: Beat Saber Modding Group (BSMG). Именно сообщество ответственно за большую часть того, что есть в игре.
- beatsaver.com — коллекция пользовательских уровней, созданных с помощью официального или неофициальных редакторов. Сюда добавляются десятки уровней каждый день.
- bsaber.com — сайт, главная задача которого — помочь найти что-то хорошее среди того, что появляется на beatsaver.com. Здесь есть рекомендации кураторов, топы за неделю, поиск по музыкальным жанрам и многое другое.
- scoresaber.com — таблицы лидеров для пользовательских уровней.
- modelsaber.com — модели мечей, платформ и аватаров. Тут хранится визуальный 3D-контент, не содержащий дополнительного кода.
- beatmods.com — коллекция модов, модифицирующих код игры.
- github.com/Assistant/ModAssistant — ModAssistant, программа для установки модов. Игрок просто выбирает моды, которые ему нужны, и жмет Install. Не нужно даже вручную копировать файлы.
Все это поддерживается сообществом. Большинство модов и даже сами сайты — это проекты с открытым исходным кодом, их можно найти на GitHub, чем я и воспользовался при подготовке к этой статье.
Общая информация о Unity
Beat Saber сделан на игровом движке Unity. В качестве языка разработки логики для игр в Unity используется C# — в таком контексте его часто называют скриптовым языком. C# — это язык, программы на котором собираются в сборки с управляемым кодом .NET (managed .NET assemblies) — dll-библиотеки, содержащие команды промежуточного языка (Common Intermediate Language, CIL или иногда просто IL). При этом ядро движка написано на C++, а часть его библиотек — те, которые связаны с игровой логикой и пользовательским интерфейсом — на C#. Это значит, что Unity нужен дополнительный скриптовый движок, который должен выполнять CIL-код и иметь возможность обмениваться данными с ядром. Для этого в Unity используется Mono.
Mono — это одна из реализаций спецификации общеязыковой инфраструктуры (Common Language Infrastructure, CLI). Mono работает на Windows, Linux, macOS, мобильных устройствах и игровых консолях. Разрабатывается с 2001 года компанией Ximian, которую в 2003 купила компания Novell, которую в 2011 купила Attachmate. Attachmate сократила почти всех, но разработчики Mono хотели и дальше разрабатывать Mono, поэтому организовали в том же году отдельную компанию Xamarin, которую в 2016 купили Microsoft. Код Mono открыт и хранится на GitHub.
Это стандартная практика в разработке игр, когда высокопроизводительная часть движка написана на C или C++, а игровая логика пишется на скриптовом языке, например Lua. Преимущество C# в качестве скриптового языка в том, что он достаточно “высокоуровневый”, чтобы упростить и ускорить разработку игры, но при этом позволяет использовать JIT-компиляцию в нативный код.
Подробнее об этом можно прочитать в документации Mono.
Программные моды для игр и программ (также известные как плагины) — это dll-файлы (в нашем случае .NET-сборки), которые добавляются к игре и загружаются в память вместе с ней. Проблема в том, что Beat Saber не поддерживает сторонние плагины и сам по себе ничего загружать не будет. За внедрение поддержки плагинов в игру отвечает BSIPA.
BSIPA
BSIPA (Beat Saber Illusion Plugin Architecture) — это набор библиотек, которые модифицируют файлы Beat Saber так, чтобы игра могла загружать сторонние моды. Иногда такие библиотеки называют менеджерами плагинов.
BSIPA написана на C# (исходный код) и является форком IPA. Т.е. моддеры взяли уже существующий менеджер плагинов для Unity и улучшили его специально под нужды Beat Saber. В исходном коде BSIPA можно выделить три основных модуля: IPA, IPA.Loader и IPA.Injector.
IPA.exe
Обычно игроки используют ModAssistant и его аналоги, чтобы устанавливать моды, но мы рассмотрим, как это делается вручную. Для этого нужно скачать последний релиз BSIPA, распаковать все файлы в папку с игрой и запустить IPA.exe. Это исполняемый файл, который копирует файлы из только что распакованного архива в те папки, где они должны находиться. И это, в общем-то, все, что он делает: просто копирует файлы, и если какой-то из файлов уже существует в игре, то он делает его резервную копию. Вот список копируемых файлов:
Beat Saber_Data\Managed\I18N.dll
Beat Saber_Data\Managed\I18N.West.dll
Beat Saber_Data\Managed\IPA.Injector.dll
Beat Saber_Data\Managed\IPA.Injector.pdb
Beat Saber_Data\Managed\IPA.Loader.dll
Beat Saber_Data\Managed\IPA.Loader.pdb
Beat Saber_Data\Managed\IPA.Loader.xml
Beat Saber_Data\Managed\Microsoft.CSharp.dll
Beat Saber_Data\Managed\System.Runtime.Serialization.dll
Libs\0Harmony.1.2.0.1.dll
Libs\Ionic.Zip.1.9.1.8.dll
Libs\Mono.Cecil.0.10.4.0.dll
Libs\Mono.Cecil.Mdb.0.10.4.0.dll
Libs\Mono.Cecil.Pdb.0.10.4.0.dll
Libs\Mono.Cecil.Rocks.0.10.4.0.dll
Libs\Newtonsoft.Json.12.0.0.0.dll
Libs\SemVer.1.2.0.0.dll
winhttp.dll
Мы видим здесь еще два модуля BSIPA, которые я упомянул ранее: IPA.Loader.dll и IPA.Injector.dll. Остальные библиотеки нужны, чтобы работали эти две. Некоторые из них мы еще рассмотрим подробнее в этой статье и во второй части.
IPA.Loader.dll
Как и следует из названия этого модуля, он отвечает за загрузку и управление плагинами. Здесь определены классы PluginLoader, PluginManager и PluginComponent. PluginLoader — это класс, в котором определена логика загрузки плагинов из папки Plugins. В оригинальной IPA плагины загружаются в алфавитном порядке, что иногда приводит к проблемам. В BSIPA логика загрузки была переписана: Loader сначала загружает метаданные плагинов, анализирует зависимости, находит конфликты и строит порядок загрузки на основе этой информации.
Класс PluginManager отвечает за управление плагинами: поиск, активацию и деактивацию с учетом их зависимостей.
Класс PluginComponent — это компонент Unity, отвечающий за старт загрузки модов, их хранение и передачу им событий из игры. Например, если в основной игре сменилась активная сцена (мы запустили какой-то уровень или, наоборот, вернулись в главное меню), то PluginComponent итерирует по всем модам и сообщает им об этом. Подробнее об этом будет во второй части.
IPA.Injector.dll
Название Injector намекает, что этот модуль отвечает за внедрение в оригинальную игру. Начнем с забавного факта: BSIPA добавляет в игру антипиратскую защиту.
if (AntiPiracy.IsInvalid(Environment.CurrentDirectory))
{
loader.Error("Invalid installation; please buy the game to run BSIPA.");
return;
}
Защита примитивная, она просто проверяет, есть ли в папке с игрой файлы, которые явно указывают на взлом: SmartSteamEmu.ini, BSteam crack.dll, HUHUVR_steam_api64.dll и другие.
public static bool IsInvalid(string path)
{
var dataPlugins = Path.Combine(GameVersionEarly.ResolveDataPath(path), "Plugins");
return
File.Exists(Path.Combine(path, "IGG-GAMES.COM.url")) ||
File.Exists(Path.Combine(path, "SmartSteamEmu.ini")) ||
File.Exists(Path.Combine(path, "GAMESTORRENT.CO.url")) ||
File.Exists(Path.Combine(dataPlugins, "BSteam crack.dll")) ||
File.Exists(Path.Combine(dataPlugins, "HUHUVR_steam_api64.dll")) ||
Directory.GetFiles(dataPlugins, "*.ini", SearchOption.TopDirectoryOnly).Length > 0;
}
Скорее всего, BSIPA это нужно из практических интересов. Если игра пиратская, то это значит, что она была взломана и часть ее файлов изменены. BSIPA тоже изменяет файлы игры, а значит, совместимость с пиратскими библиотеками не гарантируется.
IPA.Injector использует стороннюю библиотеку: Mono.Cecil (исходный код). Ее автор: Jb Evain, работал в Novell над разработкой Mono, сейчас работает в Microsoft, руководит разработкой Visual Studio Tools for Unity. Mono.Cecil существует с 2004 года, т.е. примерно со времен релиза первой версии Mono. Она позволяет читать и модернизировать .NET-сборки: с ее помощью можно менять различные параметры библиотек или даже редактировать IL-код, который в них записан.
IPA.Injector использует Mono.Cecil, чтобы модифицировать библиотеку UnityEngine.CoreModule.dll. Она написана на C# (иначе бы мы не смогли ее редактировать) и содержит базовые сущности Unity. Например, там определены классы GameObject (все объекты в игре, которые располагаются на игровых сценах, являются объектами этого класса) и класс MonoBehaviour (базовый класс для компонентов с игровой логикой, в том числе PluginComponent). IPA.Injector находит в библиотеке UnityEngine.CoreModule.dll класс UnityEngine.Application и модифицирует его статический конструктор (или создает, если его нет), добавляя туда создание бутстраппера:
var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters { ... });
var unityModDef = unityAsmDef.MainModule;
var application = unityModDef.GetType("UnityEngine", "Application");
MethodDefinition cctor = null;
foreach (var m in application.Methods)
if (m.IsRuntimeSpecialName && m.Name == ".cctor")
cctor = m;
var createBootstrapper = unityModDef.ImportReference(((Action)CreateBootstrapper).Method);
if (cctor == null)
{
cctor = new MethodDefinition(".cctor", ...);
application.Methods.Add(cctor);
var ilp = cctor.Body.GetILProcessor();
ilp.Emit(OpCodes.Call, cbs);
ilp.Emit(OpCodes.Ret);
}
else
{
var ilp = cctor.Body.GetILProcessor();
ilp.Replace(cctor.Body.Instructions[0], ilp.Create(OpCodes.Call, cbs));
ilp.Replace(cctor.Body.Instructions[1], ilp.Create(OpCodes.Ret));
}
По крайней мере, в Unity 2019.3.0f3 у Application нет статического конструктора, а значит, эта модификация не удаляет существующий важный код и относительно безопасна. Если статический конструктор уже существует и Injector заменяет его, то это только в том случае, если мы повторно модифицируем библиотеку.
После модификации в классе Application появляется новый статический конструктор (код получен с помощью декомпиляции модифицированной UnityEngine.CoreModule.dll):
static Application()
{
IPA.Injector.Injector.CreateBootstrapper();
}
Метод CreateBootstrapper создает новый объект и добавляет в него компонент Bootstrapper:
private static void CreateBootstrapper()
{
...
var bootstrapper = new GameObject("NonDestructiveBootstrapper")
.AddComponent<Bootstrapper>();
bootstrapper.Destroyed += Bootstrapper_Destroyed;
}
Бутстраппер представляет собой интересное техническое решение:
internal class Bootstrapper : MonoBehaviour
{
public event Action Destroyed = delegate {};
public void Start()
{
Destroy(gameObject);
}
public void OnDestroy()
{
Destroyed();
}
}
Метод Start похож на метод Awake, но Awake вызывается в первый кадр, когда создается компонент, а Start вызывается в первый кадр после активации компонента (компоненты могут создаваться неактивными). Каждый из методов вызывается ровно один раз за жизненный цикл компонента и может использоваться для разных этапов инициализации компонента. Компонент Bootstrapper в своем методе Start вызывает уничтожение объекта, к которому он прикреплен. В методе OnDestroy вызывается колбэк, установленный в CreateBootstrapper:
private static void Bootstrapper_Destroyed()
{
// wait for plugins to finish loading
pluginAsyncLoadTask.Wait();
permissionFixTask.Wait();
PluginComponent.Create();
}
Колбэк дожидается асинхронной загрузки плагинов и создает компонент PluginComponent. Если честно, я не знаю, зачем нужен этот механизм с самоуничтожением бутстраппера и созданием PluginComponent в OnDestroy. Мое предположение: это нужно для того, чтобы сделать загрузку отложенной — метод Start вызывается, когда компонент становится активным, а он не может стать активным, пока не загрузится игровая сцена. Буду рад, если более опытные Unity-разработчики поправят меня в комментариях.
Теперь, когда загружается класс Application (т.е. на ранних этапах загрузки .NET-сборок Unity), у нас создается PluginComponent и загружаются плагины из папки Plugins. Получается, мы внедрились не в код Beat Saber, а в код самого Unity.
Помимо этого IPA.Injector редактирует еще один файл — MainAssembly.dll. Эта библиотека содержит код Beat Saber. С помощью все того же Mono.Cecil IPA.Injector убирает у всех классов sealed и делает все методы публичными и виртуальными. Теперь любой класс в любом плагине может наследоваться от любого класса в оригинальной игре и переопределять поведение его методов. Эта модификация уже не относится к загрузке плагинов, но она очень упрощает разработку модов.
Если бы у нас использовалась не BSIPA, а оригинальная IPA, то на этом можно было бы остановиться. В оригинальной версии IPA.exe запускает Injector, модифицирует UnityEngine.CoreModule.dll, и в игре появляются моды. У такого подхода есть один минус — каждый раз, когда оригинальная игра обновляется, нужно заново запускать IPA.exe и патчить игру. BSIPA решает эту проблему, но, как я уже писал выше, IPA.exe в BSIPA просто копирует файлы и не делает больше ничего. За запуск Injector в этом случае отвечает другая библиотека.
Unity Doorstop
За внедрение в код Unity отвечает библиотека UnityDoorstop-BSIPA. Она лежит среди файлов BSIPA и написана на чистом C. UnityDoorstop-BSIPA (исходный код) — это тоже форк, оригинальный проект можно найти здесь. Далее для простоты буду вместо UnityDoorstop-BSIPA писать Doorstop. Лозунгом Doorstop является фраза “Run managed code before Unity does!”, что в примерном переводе звучит как “Запускай управляемый код до того, как Unity сможет это сделать”. Напомню, что “управляемый код” — это в нашем случае код C#. Выше мы уже выяснили, что ядро движка Unity написано на C++, а пользовательские скрипты для игровой логики и некоторые части самого Unity — на C#. Значит, Doorstop каким-то образом позволяет нам вмешаться в логику, когда ядро Unity уже загрузилось, а C#-скрипты — еще нет.
Когда мы запускаем игру на Unity (например, Beat Saber.exe), то в память одной из первых загружается библиотека UnityPlayer.dll. Она прилагается ко всем Unity-играм и отвечает за запуск и выполнение самой игры. У этой библиотеки есть таблица импорта, в которой говорится, что UnityPlayer использует функцию GetProcAddress из библиотеки kernel32.dll. GetProcAddress — это функция WinAPI, которая возвращает адрес функции из определенной библиотеки по ее названию. Я не видел исходного кода Unity, но судя по тому, что я видел в BSIPA и Doorstop, в UnityPlayer должно быть что-то вроде такого:
mono_dll = LoadLibrary(“Mono.dll”);
init_mono = GetProcAddress(mono_dll, ”mono_jit_init_version”);
mono = init_mono(...);
// дальше используем mono, чтобы загрузить и запустить код игры
mono_jit_init_version — это функция, которая отвечает за инициализацию и запуск Mono. Подробнее можно прочитать здесь. Doorstop вмешивается в этот процесс. Делается это в два шага.
Шаг 1. Проксируем GetProcAddress
Когда библиотека Doorstop.dll загружается в память компьютера, вызывается ее DllMain и, если reasonForDllLoad == DLL_PROCESS_ATTACH
, то выполняется код, который в упрощенном виде выглядит так:
HMODULE targetModule = GetModuleHandleA("UnityPlayer");
iat_hook(targetModule, "kernel32.dll", &GetProcAddress, &hookGetProcAddress);
Полный код DllMain, полный код iat_hook
Этот код находит в памяти уже загруженную библиотеку UnityPlayer.dll, берет ее таблицу импорта (Import Address Table, IAT), находит в ней GetProcAddress из kernel32.dll и заменяет ее на нашу функцию hookGetProcAddress из Doorstop.dll. hookGetProcAddress в упрощенном виде выглядит так:
void * WINAPI hookGetProcAddress(HMODULE module, char const *name)
{
if (lstrcmpA(name, "mono_jit_init_version") == 0)
{
init(module);
return (void*)&ownMonoJitInitVersion;
}
return (void*)GetProcAddress(module, name);
}
Подробнее про IAT Hooking можно прочитать здесь. Получается, что hookGetProcAddress проксирует все вызовы GetProcAddress. Прокси-функция смотрит название функции, которую у нее запрашивают. Если это НЕ mono_jit_init_version, то hookGetProcAddress просто вызывает настоящую GetProcAddress и возвращает то, что у нее попросили, тем самым не мешая нормальной работе. Если у нее запрашивают mono_jit_init_version, то тогда она возвращает переопределенную функцию ownMonoJitInitVersion. Пользуясь случаем, прокси-функция получает указатель на библиотеку, в котором ищется mono_jit_init_version, и берет из нее настоящие функции Mono с помощью GetProcAddress (внутри init(module);
):
/**
* \brief Loads Mono C API function pointers so that the above definitions can be called.
* \param monoLib Mono.dll module.
*/
inline void loadMonoFunctions(HMODULE monoLib)
{
// Enjoy the fact that C allows such sloppy casting
// In C++ you would have to cast to the precise function pointer type
#define GET_MONO_PROC(name) name = (void*)GetProcAddress(monoLib, #name)
// Find and assign all our functions that we are going to use
GET_MONO_PROC(mono_debug_domain_create);
GET_MONO_PROC(mono_domain_assembly_open);
GET_MONO_PROC(mono_assembly_get_image);
GET_MONO_PROC(mono_runtime_invoke);
GET_MONO_PROC(mono_debug_init);
GET_MONO_PROC(mono_jit_init_version);
GET_MONO_PROC(mono_jit_parse_options);
GET_MONO_PROC(mono_method_desc_new);
GET_MONO_PROC(mono_method_desc_search_in_image);
GET_MONO_PROC(mono_method_signature);
GET_MONO_PROC(mono_signature_get_param_count);
GET_MONO_PROC(mono_array_new);
GET_MONO_PROC(mono_get_string_class);
GET_MONO_PROC(mono_string_new_utf16);
GET_MONO_PROC(mono_gc_wbarrier_set_arrayref);
GET_MONO_PROC(mono_array_addr_with_size);
GET_MONO_PROC(mono_object_to_string);
GET_MONO_PROC(mono_string_to_utf8);
GET_MONO_PROC(mono_dllmap_insert);
GET_MONO_PROC(mono_free);
}
Шаг 2. Переопределеяем mono_jit_init_version
ownMonoJitInitVersion сначала вызывает настоящую mono_jit_init_version, чтобы создать Mono. Затем она с помощью Mono загружает сборку IPA.Injector.dll и запускает из нее статический метод Main. В (очень) упрощенном виде код ownMonoJitInitVersion выглядит так:
void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_version)
{
void *domain = mono_jit_init_version(root_domain_name, runtime_version);
// Load our custom assembly into the domain
void *assembly = mono_domain_assembly_open(domain, dll_path); // dll_path - это путь к IPA.Inhector.dll
// Get assembly's image that contains CIL code
void *image = mono_assembly_get_image(assembly);
// Create a descriptor for a random Main method
void *desc = mono_method_desc_new("*:Main", FALSE);
// Find the first possible Main method in the assembly
void *method = mono_method_desc_search_in_image(desc, image);
mono_runtime_invoke(method, NULL, args, &exception);
return domain;
}
Полный код ownMonoJitInitVersion
Мы уже рассмотрели выше, что IPA.Injector содержит код, который внедряет плагины в Beat Saber. После того, как IPA.Injector завершает свою работу, ownMonoJitInitVersion отдает Mono в Unity. Unity даже не в состоянии понять, что что-то было не так. Если б он вызвал настоящую mono_jit_init_version, то он бы получил Mono и начал бы дальше с ним работать. Если Unity запускает переопределенную ownMonoJitInitVersion, то он тоже получает Mono — он просто не в курсе, что этим Mono успели воспользоваться для чего-то еще.
winhttp.dll
Остался один нерешенный вопрос. Во-первых, в описании Doorstop я писал про библиотеку Doorstop.dll. Давайте опять взглянем на файлы, которые IPA.exe устанавливает в игру:
Beat Saber_Data\Managed\I18N.dll
Beat Saber_Data\Managed\I18N.West.dll
Beat Saber_Data\Managed\IPA.Injector.dll
Beat Saber_Data\Managed\IPA.Injector.pdb
Beat Saber_Data\Managed\IPA.Loader.dll
Beat Saber_Data\Managed\IPA.Loader.pdb
Beat Saber_Data\Managed\IPA.Loader.xml
Beat Saber_Data\Managed\Microsoft.CSharp.dll
Beat Saber_Data\Managed\System.Runtime.Serialization.dll
Libs\0Harmony.1.2.0.1.dll
Libs\Ionic.Zip.1.9.1.8.dll
Libs\Mono.Cecil.0.10.4.0.dll
Libs\Mono.Cecil.Mdb.0.10.4.0.dll
Libs\Mono.Cecil.Pdb.0.10.4.0.dll
Libs\Mono.Cecil.Rocks.0.10.4.0.dll
Libs\Newtonsoft.Json.12.0.0.0.dll
Libs\SemVer.1.2.0.0.dll
winhttp.dll
Как вы можете видеть, Doorstop.dll здесь нет. Во-вторых, даже если бы Doorstop.dll здесь бы, то почему Beat Saber или Unity должны его загружать в память, если его нет в таблице импорта? Решение: выдать Doorstop.dll за другую библиотеку, которую загружает Unity, и сделать так, чтобы она загрузилась раньше настоящей. Такой библиотекой в нашем случае является winhttp.dll — это системная библиотека Windows для http-запросов (хранится в C:/Windows/System32). У Unity в одной из библиотек в таблице импорта указано, что ей нужна библиотека winhttp.dll, поэтому во время загрузки Unity Windows сначала загружает winhttp.dll в память, и только после этого Unity начинает работать.
Doorstop собирается в библиотеку под тем же названием: winhttp.dll. Там содержится код Doorstop, отвечающий за все манипуляции с GetProcAddress и mono_jit_init_version, а также туда добавляется таблица экспорта (Export Address Table) со всеми теми же функциями, что в оригинальной winhttp.dll. Загрузка библиотек в Windows устроена так, что Windows сначала проверяет, есть ли нужные библиотеки в папке с программой, и только потом, если ничего не нашел, идет в System32. Поэтому при запуске игры Windows в первую очередь находит наш файл. Поддельная библиотека загружает настоящую winhttp динамически (с помощью LoadLibrary) и просто перенаправляет все вызовы из своей таблицы экспорта на адреса настоящих функций (с помощью GetProcAddress). Можно сравнить размеры: поддельная библиотека весит 16кб, а настоящая — 960кб.
В репозиториях IPA и BSIPA есть скрипты для генерации файла proxy.c, который содержит таблицу экспорта, совпадающую с оригинальной библиотекой, и загружает все методы через GetProcAddress. В IPA такой скрипт написан на PowerShell, а в BSIPA — на Python. Сгенерированный файл выглядит так: proxy.c.
Подробнее про DLL Search Order Hijacking можно прочитать тут.
Перечисление действий в хронологическом порядке
Повторяем все шаги в хронологическом порядке.
- Запускаем IPA.exe. Все библиотеки копируются в нужные папки игры. Никаких изменений в коде игры пока нет.
- Запускаем игру (Beat Saber.exe).
- Windows ищет системные библиотеки, нужные для Unity. Одна из таких библиотек — это winhttp.dll. Windows находит в папке с игрой нашу поддельную библиотеку winhttp.dll и загружает ее.
- В поддельной winhttp.dll вызывается код Doorstop. Он заменяет функцию GetProcAddress из системной библиотеки kernel32.dll на прокси hookGetProcAddress.
- Загружается библиотека UnityPlayer.dll, написанная на C++.
- Unity вызывает GetProcAddress, чтобы найти функцию mono_jit_init_version. Так как мы заменили GetProcAddress, она находит переопределенную ownMonoJitInitVersion.
- ownMonoJitInitVersion загружает Mono.
- ownMonoJitInitVersion использует Mono, чтобы загрузить IPA.Injector.dll и вызвать метод Main из нее.
- IPA.Injector с помощью библиотеки Mono.Cecil модифицирует класс Application из библиотеки UnityEngine.CoreModule.dll и добавляет туда статический конструктор, в котором загружаются моды для Beat Saber.
- ownMonoJitInitVersion возвращает Mono в Unity.
- Unity использует Mono, чтобы запустить части движка, написанные на C#.
- Загружается модифицированный класс Application и вызывается его статический конструктор. Он загружает моды для Beat Saber.
- Unity использует Mono, чтобы запустить код Beat Saber.
- Оригинальный код игры и код из модов теперь существуют вместе.
Про часть 2
На этом первая часть завершена. Мы разобрались, как с помощью манипуляций с библиотеками Windows и Unity можно добавить моды в Beat Saber. Несмотря на то, что BSIPA — это форк, сделанный специально для этой игры, мы в этой статье нигде не делали предположений о внутренней структуре игры и не использовали ее код, а значит можно сказать, что эти методы применимы ко всем Unity-играм.
В следующей части мы напишем свой собственный мод для Beat Saber: посмотрим, как моды обмениваются информацией с игрой, как модифицировать поведение оригинальной игры, а также воспользуемся Harmony — библиотекой для модификации C#-кода, которая используется моддерами в RimWorld, BATTLETECH, Cities: Skylines, Kerbal Space Program, Oxygen Not Included, Stardew Valley, Subnautica и многих других.
Благодарности
Хочется поблагодарить сообщество BSMG и в особенности DaNike за то, что ответили на мои вопросы в Discord и помогли разобраться с тем, как работает Doorstop и зачем ему нужна winhttp.dll. Вряд ли они это прочитают, но без них у меня бы не сложилось полное понимание, как все работает, а значит и не было бы этой статьи.