Как разрабатываются моды для игр, которые не поддерживают моды (на примере Beat Saber) — часть 1: внедряемся в код игры

Простым языком о dll-файлах, Unity, Mono, паре хакерских методов и о крутости open-source-сообщества.

Я довольно много играю в Beat Saber с модами, и в какой-то момент появилась необходимость написать свои. Beat Saber сделан на Unity, так что я покопался в его коде, в коде существующих модов, разобрался, как там всё устроено, и написал об этом лонгрид.

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

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

Источники изображений: <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fwww.oculus.com%2Fexperiences%2Fquest%2F2448060205267927%2F&postId=113167" rel="nofollow noreferrer noopener" target="_blank">1</a>, <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fyoutu.be%2FOH4txDD23_c&postId=113167" rel="nofollow noreferrer noopener" target="_blank">2</a>
Источники изображений: 1, 2

О Beat Saber

Если у вас есть VR-шлем, то вы почти наверняка знаете, что такое Beat Saber. Если нет, то, вероятно, вы видели хотя бы одно видео из игры.

Единственное, что Beat Saber поддерживает из модов — это пользовательские уровни. В игре есть официальный редактор, но нет никакого сервиса, чтобы как-либо распространять свои творения (даже мастерской Steam), и вряд ли появится. В основе практически любого уровня лежит какая-то песня. Разрешить выкладывать такие файлы в своем официальном сервисе — значит привлечь к себе внимание правообладателей с их постоянными запросами что-то удалить, а то и вообще с требованиями всё закрыть.

Поэтому не удивительно, что вокруг Beat Saber сформировалось огромное сообщество: Beat Saber Modding Group (BSMG). Именно сообщество ответственно за большую часть того, что есть в игре.

  • https://beatsaver.com — коллекция пользовательских уровней, созданных с помощью официального или неофициальных редакторов. Сюда добавляются десятки уровней каждый день.
  • https://bsaber.com — сайт, главная задача которого — помочь найти что-то хорошее среди того, что появляется на https://beatsaver.com. Здесь есть рекомендации кураторов, топы за неделю, поиск по музыкальным жанрам и многое другое.
  • https://scoresaber.com — таблицы лидеров для пользовательских уровней.
  • https://modelsaber.com — модели мечей, платформ и аватаров. Тут хранится визуальный 3D-контент, не содержащий дополнительного кода.
  • https://beatmods.com — коллекция модов, модифицирующих код игры.
  • https://github.com/Assistant/ModAssistant — ModAssistant, программа для установки модов. Игрок просто выбирает моды, которые ему нужны, и жмет Install. Не нужно даже вручную копировать файлы.

Всё это поддерживается сообществом. Большинство модов и даже сами сайты — это проекты с открытым исходным кодом, их можно найти на GitHub — сайте, где программисты выкладывают свой код. Этим мы и воспользуемся дальше.

Немного информатики

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

Исполняемый файл (executable) — это файл, который содержит код в понятном для компьютера виде — в виде набора инструкций процессора. В большинстве случаев в Windows это файлы с расширением .exe. Исполняемые файлы, внезапно, могут исполняться — у них есть точка входа (entry point) и набор подпрограмм (они же процедуры, функции, методы).

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

Файлы .dll — это файлы динамических библиотек (dynamic link library). По своей сути и структуре они очень похожи на исполняемые файлы, но у них нет точки входа. Это просто наборы подпрограмм, которые загружаются в память вместе с exe-файлом и могут использоваться процессором.

У таких библиотек есть много преимуществ. Одну такую библиотеку могут использовать несколько программ одновременно: достаточно один раз загрузить её в память, и она станет доступна сразу для всех. Или при определённых условиях программе может вообще не понадобиться какая-то библиотека, и тогда её можно не загружать в память (потому они и называются «динамические»).

Также такие библиотеки могут поставляться другими разработчиками. Если вы когда-нибудь при запуске игры видели сообщение, что вам нужно дополнительно установить Microsoft Visual C++ Redistributable 2015, то знайте — это как раз оно и есть.

Как разрабатываются моды для игр, которые не поддерживают моды (на примере Beat Saber) — часть 1: внедряемся в код игры

Помимо простых .dll существуют ещё так называемые сборки с управляемым кодом .NET (managed .NET assemblies). Это тоже dll-файлы, но они содержат не команды процессора, а команды промежуточного языка (Common Intermediate Language, CIL или иногда просто IL).

Процессоры не знают, что такое CIL, они не могут выполнять такие инструкции сами по себе. Для этого Windows запускает отдельную программу, которая на ходу переводит CIL-команды в инструкции, понятные процессору. Такой процесс называется компиляцией на ходу (just-in-time compilation, JIT), а программа, которая это делает — «общеязыковая исполняющая среда» (Common Language Runtime, CLR). Есть много языков, программы на которых собираются в такие сборки, но самым известным и популярным является C# (именно он используется в Unity).

Есть разные реализации Common Language Runtime. По сути это просто стандарт, и кто угодно может написать свою версию. Популярных версий всего три:

  • .NET Framework — основная платформа, с неё и пошли все эти C#, CIL и CLR. Разрабатывается Microsoft c 1999 года, работает под Windows. Считается, что .NET Framework появился как ответ Java, набирающей популярность в то время.
  • .NET Core — платформа с открытым исходным кодом, тоже разрабатывается Microsoft, но уже под разные платформы: Windows, Linux и macOS. Разрабатывается с 2014 года. Судя по всему, является попыткой Microsoft охватить серверы, работающие на Linux.
  • Mono — ещё одна платформа с открытым исходным кодом, работает на Windows, Linux, macOS, мобилках и консолях. Используется в Unity. Разрабатывается с 2001 года компанией Ximian, которую в 2003 купила компания Novell, которую в 2011 купила Attachmate. Attachmate поуволняла всех, но разработчики Mono хотели и дальше разрабатывать Mono, поэтому организовали в том же году отдельную компанию Xamarin, которую в 2016 купили Microsoft.
Как разрабатываются моды для игр, которые не поддерживают моды (на примере Beat Saber) — часть 1: внедряемся в код игры

Таким образом, все основные реализации CLR сейчас находятся под крылом Microsoft. Однако некоторое время назад титул «корпорации добра» перешел от Google к Microsoft, так что не факт, что это плохо.

На этом с ликбезом пока достаточно, пора переходить к основной теме. Программные моды (также известные как плагины) — это dll-файлы, которые добавляются к игре и загружаются в память вместе с ней. Проблема в том, что Beat Saber не поддерживает сторонние плагины и сам по себе ничего загружать и использовать не собирается. Значит, нам нужно что-то, что внедрится в код игры и научит её это делать. Этим чем-то является BSIPA.

BSIPA

BSIPA (Beat Saber Illusion Plugin Architecture) — это набор библиотек, которые модифицируют файлы Beat Saber так, чтобы игра могла загружать сторонние моды. Иногда такие библиотеки называют менеджерами плагинов.

Небольшое лирическое отступление. Для того, чтобы мне было проще объяснять некоторые вещи, мне нужно объяснить значение слова «форк».

Разработка программ с открытым исходным кодом (open source) — это явление, которое оказывает колоссальный эффект на развитие технологий. Разработчики пишут программы, выкладывают их исходный код со свободной лицензией, например, на GitHub, а другие разработчики могут свободно использовать эти программы, предлагать определённые улучшения для существующих программ (пул реквесты, pull requests) или вообще копировать другие проекты и делать что-то свое на их основе. Последнее как раз и называется «форком» (fork, переводится буквально как «вилка», «развилка», «ответвление»).

Форк
Форк

Так вот, BSIPA (написана на C#, исходный код: GitHub) является форком IPA (GitHub). Т.е. моддеры взяли уже существующий менеджер плагинов для Unity и улучшили его специально под нужды Beat Saber. Давайте залезем в исходный код, чтобы понять, как оно всё работает. Я выделяю там три основных модуля: 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

Как следует из названия этого модуля, он отвечает за загрузку плагинов и управление ими. Здесь определен класс 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; }

Защита очень примитивная, никакого Denuvo, никакой виртуализации, она просто проверяет, есть ли в папке с игрой файлы, которые явно указывают на взлом: SmartSteamEmu.ini, BSteam crack.dll, huhuvr_steam_api64.dll и другие. Скорее всего, BSIPA это нужно из практических интересов. Внедрение в код игры и добавление туда модов — это по своей сути взлом. Если игра ещё и пиратская, то получается, что она взломана дважды, а совместная работа двух взломов не гарантируется.

В IPA.Injector используется сторонняя библиотека Mono.Cecil (исходный код: GitHub). Её автор: Jb Evain, когда-то работал в Novell над разработкой Mono, сейчас работает в Microsoft, руководит разработкой Visual Studio Tools for Unity. Mono.Cecil существует с 2004 года — примерно со времен релиза первой версии Mono.

Она позволяет читать и модернизировать .NET-сборки. То есть с её помощью можно менять различные параметры C#-библиотек или даже редактировать код, который в них записан. Здесь нет ничего особо сложного: Mono.Cecil редактирует файлы dll примерно как Microsoft Office редактирует файлы docx — нужно просто знать структуру файла, что как прочитать и что куда записать. Я не умаляю заслуг Jb Evain, если что. Библиотека всё ещё очень сложная и полезная, просто со стороны её изучать не так интересно.

И раз я уже не первый раз упомянул Mono в этом лонге, то надо написать про него чуть подробнее. Я уже писал в начале, что Mono — это одна из реализаций Common Language Runtime — программы, которая нужна, чтобы запускать код из управляемых сборок .NET. Если говорить проще и про наш конкретный случай, Mono нужен для того, чтобы в Unity можно было писать код на C#. Ядро движка Unity написано на C++, но некоторые части движка и практически все скрипты разрабатываемых на нем игр пишутся на C#. Когда мы запускаем игру, то сначала загружается C++ код. Этот код запускает Mono, передает в него C#-код движка и код игры, и после этого сама игра начинает работать — можно играть.

С помощью Mono.Cecil IPA.Injector редактирует библиотеку UnityEngine.CoreModule.dll. Как видно из названия, это одна из библиотек Unity. Она написана на C# (иначе бы мы не смогли её редактировать) и содержит базовые сущности движка. Например, там есть класс GameObject — все объекты в игре, которые располагаются на игровых сценах, являются объектами этого класса.

Ещё там есть класс MonoBehaviour — это класс для компонентов с игровой логикой (тут Mono даже в название класса затесался). IPA.Injector находит в библиотеке UnityEngine.CoreModule.dll класс UnityEngine.Application и модифицирует его статический конструктор (или создает, если его нет), добавляя туда код из IPA.Loader. Не буду углубляться в то, что такое статический конструктор, но теперь, когда загружается класс Application, у нас создаётся PluginComponent и загружаются плагины из папки Plugins. Получается, что мы внедрились не в код Beat Saber, чтобы он загружал моды, а в код самого Unity.

Если бы у нас использовалась не BSIPA, а оригинальная IPA, то на этом можно было бы и остановиться. В оригинальной версии IPA.exe запускает Injector, модифицирует UnityEngine.CoreModule.dll, и у нас появляются моды в игре. У такого подхода есть один минус — каждый раз, когда оригинальная игра обновляется, нужно заново запускать IPA.exe и патчить игру.

Это может показаться не такой серьёзной проблемой, но видели бы вы, сколько жалоб поступает от игроков к разработчикам игры за то, что они очередным своим обновлением сломали их моды. BSIPA решает эту проблему, но как я писал выше, IPA.exe в BSIPA просто копирует файлы и не делает больше ничего. Тогда непонятно, кто же запускает Injector в этом случае? И тут начинается самое интересное.

Unity Doorstop

Как разрабатываются моды для игр, которые не поддерживают моды (на примере Beat Saber) — часть 1: внедряемся в код игры

Как оказалось, за внедрение в код Unity отвечает библиотека UnityDoorstop-BSIPA. Она лежит среди файлов BSIPA и написана на чистом C. UnityDoorstop-BSIPA (исходный код: GitHub) — это тоже форк, оригинальный проект можно найти здесь: GitHub. Далее для простоты буду вместо UnityDoorstop-BSIPA просто писать Doorstop. Лозунгом Doorstop является фраза «Run managed code before Unity does», что в примерном переводе звучит как «Запускай управляемый код до того, как Unity сможет это сделать».

Напомню, что «управляемый код» — это в нашем случае код C#. Выше мы уже выяснили, что ядро движка Unity написано на C++, а пользовательские скрипты для игровой логики и некоторые части самого Unity — на C#. Значит, Doorstop каким-то образом позволяет нам вмешаться в логику, когда ядро Unity уже загрузилось, а C#-скрипты — ещё нет.

Теперь уже лезем в исходники UnityDoorstop-BSIPA и смотрим, как это делается. Дальше идет немного более продвинутый уровень. Я постарался описать всё простыми словами, но при этом достаточно подробно, чтобы можно было оценить всю техническую глубину происходящей там магии.

Чтобы объяснить, как работает Doorstop, давайте вернёмся к нашим «основам информатики», которые уже перестают быть основами. Я уже писал выше, что исполняемые файлы могут использовать функции из других библиотек, переходя по адресам этих функций. Давайте уточним, откуда берутся эти адреса. В файлах .exe и .dll есть такие параметры как таблица импорта (Import Address Table, IAT) и таблица экспорта (Export Address Table, EAT). Импорт — это то, что требуется файлу из других библиотек, а экспорт — это то, что файл предоставляет для использования другим файлам. Очевидно, у .exe есть только импорт, потому что его никто использовать не может, а у .dll может быть и то, и другое, потому что они и сами что-то используют, и другим дают пользоваться.

Теперь вернемся к Unity и рассмотрим конкретный пример, который и подведет нас к тому, как работает Doorstop. Когда мы запускаем игру на Unity (например, Beat Saber.exe), то в память одной из первых загружается библиотека UnityPlayer.dll. Она прилагается ко всем Unity-играм и отвечает за запуск и выполнение самой игры. Всё указывает на то, что там как раз и есть код движка, написанный на C++, но я не проверял.

У этой библиотеки есть таблица импорта, в которой говорится, что UnityPlayer использует функцию GetProcAddress из библиотеки kernel32.dll. kernel32.dll — это системная библиотека ядра Windows, которая отвечает за базовые вещи, такие как управление памятью и ввод-вывод. Системные библиотеки — это библиотеки, которые нам дает Windows, они обычно хранятся в папке C:/Windows/System32.

GetProcAddress — это очень важная для нас функция, она даёт нам адрес какой-то функции из определенной библиотеки по её названию. Помните, в начале я писал, что dll-библиотеки называются динамическими, потому что их можно загружать по мере необходимости? Представим, что у нас есть библиотека HelloWorld.dll и в ней есть функция hello_world(). Тогда динамическая загрузка и вызов функции будут выглядеть примерно так:

dll = LoadLibrary(“HelloWorld.dll”); // загружаем библиотеку hello_world = GetProcAddress(dll, ”hello_world”); // находим функцию по её названию hello_world(); // вызываем найденную функцию FreeLibrary(dll); // выгружаем библиотеку из памяти, если она больше не нужна

Примерно такой же код есть и в UnityPlayer.dll. Напомню, что чтобы Unity мог запускать код на C#, ему нужно сначала запустить Common Language Runtime (чем в нашем случае является Mono). 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 вмешивается в запуск игры до того, как Unity запускает свой C#-код, то это должно быть как-то связано с этим Mono. Да, всё так. Делается это в два шага.

Шаг 1. Подделываем GetProcAddress

Когда библиотека Doorstop.dll загружается в память компьютера, она запускает свой код. Этот код находит в памяти уже загруженную библиотеку UnityPlayer.dll, берет её таблицу импорта, находит там GetProcAddress из kernel32.dll и заменяет её на нашу поддельную функцию GetProcAddress из Doorstop.dll. Поддельная функция GetProcAddress смотрит название функции, которую у неё запрашивают. Если это НЕ mono_jit_init_version, то поддельная GetProcAddress просто вызывает настоящую GetProcAddress и дает то, что у неё попросили, тем самым не мешая нормальной работе. Но если у неё запрашивают mono_jit_init_version, то тогда она возвращает поддельную mono_jit_init_version.

Шаг 2. Подделываем mono_jit_init_version

Поддельная mono_jit_init_version сначала вызывает настоящую mono_jit_init_version, чтобы загрузить Mono. Затем она с помощью этого Mono загружает и запускает библиотеку IPA.Injector.dll. Мы уже рассмотрели выше, что IPA.Injector содержит код, который внедряет плагины в Beat Saber. После того, как IPA.Injector завершает свою работу, поддельная mono_jit_init_version отдаёт Mono в Unity.

Unity даже не в состоянии понять, что что-то было не так. Если бы он вызвал настоящую mono_jit_init_version, то он бы получил Mono и начал бы дальше с ним работать. Если Unity запускает поддельную mono_jit_init_version, то он тоже получает Mono — он просто не в курсе, что этим Mono успели воспользоваться для чего-то ещё.

Всё работает именно так, как и написано в лозунге Doorstop: «Run managed code before Unity does». Мы запустили наш управляемый C#-код из IPA.Injector до того, как Unity смог запустить свой C#-код.

Небольшое лирическое отступление. Подмена адресов в таблице импорта — это один из методов хакерских взломов (подробнее тут (на английском): https://pentest.blog/offensive-iat-hooking/). Ещё один интересный факт: в коде UnityDoorstop-BSIPA я нашел благодарность ez (профиль на GitHub) за код, который модифицирует таблицу импорта. Это ещё один пример того, как сообщество совместно работает над решением технических задач и использует код друг друга.

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 должны его загружать в память? Beat Saber.exe знает про UnityPlayer.dll, поэтому UnityPlayer.dll загружается. UnityPlayer.dll знает про kernel32.dll, поэтому kernel32.dll тоже загружается. Но про Doorstop.dll они ничего не знают и загружать не собираются, как быть?

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

Такой библиотекой в нашем случае является winhttp.dll — это системная библиотека Windows для http-запросов (хранится в C:/Windows/System32). У Unity в какой-то из библиотек в таблице импорта указано, что ей нужна библиотека winhttp.dll, поэтому Windows во время загрузки Unity загружает ещё и её, и только после этого Unity начинает работать.

Doorstop собирается в библиотеку под тем же названием: winhttp.dll. Там содержится код Doorstop, отвечающий за все манипуляции с GetProcAddress и mono_jit_init_version, а ещё там есть таблица экспорта (Export Address Table) со всеми теми же функциями, что в оригинальной winhttp.dll. Загрузка библиотек в Windows устроена так, что Windows сначала проверяет, есть ли нужные библиотеки в папке с программой, а только потом, если ничего не найдено, идет в System32. Поэтому при запуске игры Windows в первую очередь находит наш файл.

Так как у поддельной библиотеки такое же название и такая же таблица экспорта, Windows считает, что это та самая библиотека, которая нужна для игры, поэтому загружает её в память. Поддельная библиотека, в свою очередь, загружает настоящую winhttp динамически (с помощью LoadLibrary) и просто перенаправляет все вызовы из своей таблицы экспорта на адреса настоящих функций (с помощью GetProcAddress). Можно даже размеры сравнить: поддельная библиотека весит 16кб, а настоящая — 960кб.

Вместо winhttp.dll может использоваться любая другая библиотека, используемая Unity, просто разработчики Doorstop выбрали именно её.

В этом месте хочется поблагодарить сообщество BSMG и в особенности DaNike за то, что ответили на мои вопросы в дискорде и помогли разобраться с тем, как работает Doorstop и зачем ему нужна winhttp.dll. Вряд ли они это прочитают, но тем не менее.

Все действия в хронологическом порядке

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

  • запускаем IPA.exe. Все библиотеки копируются в нужные папки игры. Никаких изменений в коде игры пока нет;
  • запускаем игру (Beat Saber.exe);
  • загружается библиотека UnityPlayer.dll, написанная на C++;
  • Windows ищет системные библиотеки, нужные для Unity. Одна из таких библиотек — это winhttp.dll. Windows находит в папке с игрой нашу поддельную библиотеку winhttp.dll и загружает её;
  • в поддельной winhttp.dll вызывается код Doorstop. Он подделывает функцию GetProcAddress из системной библиотеки kernel32.dll;
  • Unity вызывает GetProcAddress, чтобы найти функцию mono_jit_init_version. Так как мы подделали GetProcAddress, она находит поддельную mono_jit_init_version;
  • поддельная mono_jit_init_version загружает Mono;
  • поддельная mono_jit_init_version использует Mono, чтобы загрузить IPA.Injector.dll;
  • IPA.Injector с помощью библиотеки Mono.Cecil модифицирует класс Application из библиотеки UnityEngine.CoreModule.dll так, чтобы он использовал код из IPA.Loader.dll;
  • поддельная mono_jit_init_version передает Mono в Unity;
  • Unity использует Mono, чтобы запустить части движка, написанные на C#;
  • загружается модифицированный класс Application. Вызывается код из IPA.Loader.dll;
  • IPA.Loader загружает моды;
  • Unity использует Mono, чтобы запустить код Beat Saber;
  • оригинальный код игры и код из модов теперь существуют вместе;
  • ?
  • profit.

Конец первой части

На этом часть первая завершена. Мы разобрались, как с помощью манипуляций с библиотеками Windows и Unity, можно добавить моды в любую Unity-игру.

В следующей части мы напишем свой собственный мод для Beat Saber: посмотрим, как моды обмениваются информацией с игрой, как модифицировать поведение оригинальной игры, а также взглянем поближе на Harmony — библиотеку для модификации C#-кода, которая используется моддерами в RimWorld, BATTLETECH, Cities: Skylines, Kerbal Space Program, Oxygen Not Included, Stardew Valley, Subnautica и многих других (по крайней мере так пишет автор Harmony, GitHub).

Подписывайтесь на мой блог

Я начинаю вести свой блог здесь на DTF. Он будет об историях из разработки, о геймдеве и об информационных технологиях в целом. Моя цель — рассказывать людям простыми словами, что технологии — это интересно. Ширяев вон уже этим занимается со своими нейронками, вроде даже успешно. Так что подписывайтесь! Лонги тоже иногда будут.

127127
24 комментария

Я себя переоценил

18
Ответить

А ты все равно подпишись на блог!

3
Ответить

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

3
Ответить

Поддержка модов означает софт для упрощения их создания.

1
Ответить

Очень познавательно, спасибо.

3
Ответить

Чуть не по теме, какие вы знаете офигенские карты для Beat Saber? Мне вот например нравится The Wall, очень красивая работа.

1
Ответить

А офигенскость какая имеется в виду? Если аудио-визуальная, то мне нравится Everything - Said the Sky (https://youtu.be/NtHSmqJoBG8). Если геймплейно, то могу составить топ своих любимых уровней, это займет некоторое время.

3
Ответить