Reactorを使わない Among UsのMod開発
https://scrapbox.io/files/6197a65eebd589001e308132.jpg
Among Us のMod開発では有名なMod実行・改造環境であるReactorがあり、 ただ、Among Us本体にアプデが入ると、有志によるReactorの対応を待つ必要があり、
開発したいときに開発できないという問題もある。
その問題を解決するには、Reactorがやってくれることを自分でやる必要がある。
ということで、Reactorを使わないMod開発について調べ、試してみたい。
何はともあれBepInEx
BepInEx はUnity製のゲームにModを突っ込むためのフレームワーク。 元からModを導入しやすいつくりならいいけど、実際はそうはなってないから、
BepInExのようにModを突っ込むプログラムが必要になる。
そして、BepInExにはGetting Startedが用意されているので、やってみたいと思う。
BepInExのGetting Started
Installing BepInEx
まず最初はBepInExのインストールですね
BepInExのEdge版をダウンロードしてきます
UnityIL2CPP_x86をダウンロードします
僕の環境は Windows 10 64bitですが、Among Usは32bitなので、x86をダウンロードします
このメモを作成時点では BepInEx_UnityIL2CPP_x86_e66c15b_6.0.0-be.520.zip です
Among Usのディレクトリに解凍します
Steamからゲームを起動し、初期ディレクトリを生成します
https://scrapbox.io/files/61939966c70d07001e90e064.jpg
対応していないバージョンのBepInExを入れた場合、初期ディレクトリが生成されません
ディレクトリ作成時にはいつもより起動に時間がかかります
Configuration
設定ファイルの変更っぽい。
Among Us/BepInEx/config/BepInEx.cfg ファイルが設定ファイル。
開発前では何を設定すればいいか分からないので、後の手順を進めてから確認し、追記していく。
[Logging.Console]
Enabled = true
コンソールログを確認できるようにする設定
Writing a basic plugin
簡単なプラグイン作成の手順ですね。
Setting up the development environment
Creating a new plugin project
Using loggers to simplify debugging
Reading and writing configuration files
環境構築、開発プロジェクト作成、デバッグ、設定ファイルの読み書きというステップがあるみたいです
Setting up the development environment
開発環境の構築です。
Reactorの環境構築をやっていればほとんど終わってると思いますが、一応頭から確認していきます。
.NET SDKのインストール
Reactorのセットアップをしたときは5系やったのに、今見たら6系がある
と思ったけど、6系はIDEが対応してなかったりするから5系がいいかも、環境見て見極めて
インストールの確認
$ dotnet --version
6.0.100
$ dotnet --list-sdks
ちゃんと入ってる
IDEのインストール
普段からJetBrains製のIDEを使っているので、Riderを使用します
インストールは割愛
BepInEx プラグインのテンプレートを取得
次のパッケージがインストールされます:
BepInEx.Templates
成功: BepInEx.Templates::1.3.0により次のテンプレートがインストールされました。
テンプレートのダウンロードに成功した
ここまでで開発環境の下準備が完了!
Creating a new plugin project
ここから実際にMODを作るステップです。
セットアップ
bepinex6_IL2CPPを利用するので、テンプレートもIL2CPP用の物を使います。
初期化済みプロジェクトの作成
プロジェクトを設置したいディレクトリに移動する
僕の場合は D:\project\tsuchinaga\among-us-mod\MyFirstPlugin ディレクトリを作成し、そこで下記コマンドを実行しました。
$ dotnet new bep6plugin_il2cpp -n MyFirstPlugin
テンプレート "BepInEx 6 Il2Cpp Plugin Template" が正常に作成されました。
以下 bepinex5での手順です。間違って作業した履歴として残していますが、必要のない作業です
TFMの特定
Among Us_Data/Managed/netstandard.dll が存在すれば netstandard2.0
No
mscorlib.dll が存在し、バージョンが 4.0.0.0 より新しければ net46
No
ここまでヒットしなければ net35
Unityバージョンの特定
Among Us_Data/globalgamemanagers をテキストエディタで開き、文字化けしてるなか最初に登場する /\d+\.\d+\.\d+/ を探す
僕の場合は 2020.3.7 というのが見つかりました
初期化済みプロジェクトの作成
プロジェクトを設置したいディレクトリに移動する
僕の場合は D:\project\tsuchinaga\among-us-mod\MyFirstPlugin ディレクトリを作成し、そこで下記コマンドを実行しました。
$ dotnet new bepinex5plugin -n MyFirstPlugin -T net35 -U 2020.3.7
テンプレート "BepInEx 5 Plugin Template" が正常に作成されました。
中身を書いてみる
ここまでの手順で作られた、MyFirstPlugin.csproj をIDEで開いてみましょう!
って開いて気付いたけど、Riderが.NET SDK 6系に対応してなかった。インストールするなら5系がいいね
IDEで Plugin.cs を開く。
起動時にログを吐こうとするプログラムが表示されるはず。
これを動くものに変えていく。
Plugin class の前にあるメタデータに、GUID、NAME、VERSIONがある。
これを設定する。
GUID: 必ず一意に特定できる文字列にする。Javaっぽくドメインの逆順で記載するのを進められてるけど、一意であればあとは自由
NAME: 人間が読めるプラグインの名前
VERSION: プラグインのバージョン。セマンティックバージョニングに従う必要がある
とりあえず [BepInPlugin("com.tsuchinaga.among-us-mod.MyFirstPlugin", "MyFirstPlugin", "0.0.1")] みたいになるかな。
が、デフォルトで入ってる PluginInfo.PLUGIN_GUID を見ると、Debugディレクトリに入ってるファイルで初期値が入ってる。
そのまま変えずに使っても問題は起きない
依存関係の解決は最初はないから、
お行儀程度にプラグインの実行可能なプロセスの指定も別にしなくていい。
とにもかくにも動いているのをみたい!
ので、さっさとBuildしちゃいましょう。
IDEならbuild機能がついてると思うし、dotnetコマンドでbuildしてもいい。
buildされたdllファイルは、 bin/Debug/netstandard2.1/ に出力される MyFirstPlugin.dll ファイル。
これを、Among Usゲームフォルダにある、 BepInEx/plugins/ に設置する。
後は起動するだけ!!!が、読み込めない
code:log
Info :Unhollower Registered mono type UnhollowerRuntimeLib.DelegateSupport+Il2CppToMonoDelegateReference in il2cpp domain 自作プラグインをロードし、ログを出すことに成功!
Debugging plugins
プラグインをデバッグする方法はいくつかあるみたいやけど、とりあえずのオススメはdnSpyというツールを使うものみたい。
ただし、amongusはIL2CPP形式なので、まずはdumpしてあげる必要がありそう。
そのあと、dnSpyデバッグするって感じなのかな?
Il2CppDumper
Unityのil2cppデータから逆コンパイルしたdllを吐き出してくれるツール。
このツールで吐き出したdllを解析しながらModを作るんだろう、という感じ。
適当なディレクトリに解答
必要なパスを3つ確認する
Among Usのゲームディレクトリ直下にある GameAssembly.dll
ゲームディレクトリから Among Us_Data/il2cpp_data/Metadata と進んだところにある global-metadata.dat
出力結果を入れるためのディレクトリ。これはどこでもいいですが、複数のファイルがでるからまっさらなディレクトリがいいです
Il2CppDumper.exe のあるディレクトリで、上記で確認したパスをくっつけて実行
$ ./Il2CppDumper.exe "D:\game\steamapps\common\Among Us\GameAssembly.dll" "D:\game\steamapps\common\Among Us\Among Us_Data\il2cpp_data\Metadata\global-metadata.dat" work
この例では、GameAssembly.dllが "D:\game\steamapps\common\Among Us\GameAssembly.dll"
global-metadata.datが "D:\game\steamapps\common\Among Us\Among Us_Data\il2cpp_data\Metadata\global-metadata.dat"
出力先を work にしています (workディレクトリは暫定でIl2CppDumperのディレクトリの中において、後から動かす感じにしてる)
dnSpy
とにもかくにも、dnSpyを手に入れる。
ゲームのbitに合わせておくと、Debugでも使えるかも。
among usは32bitのゲームなので、32bitのをダウンロードしときましょ。
ダウンロードし起動後、Assembly ExploerにIl2CppDumperで出した、Assembly-CSharp.dll を、
Assembly Exploerにドラッグアンドドロップする。
すると、定義が見れる!!!
https://scrapbox.io/files/6196b39401382b001d80cd9c.png
ただExploerから目的の値を探すのは至難の業なので、検索窓を開いておく。
Edit > Search Assemblies か、Shift + Ctrl + Kで開ける。
例えばReactorのGetting Startedで登場する nameText を検索したら、 PlayerController もあるはず。
Il2CppAssemblyUnhollower
後の手順で気付きましたが、不要な手順です
ついでに、このままだと開発時に不便なので少し手を加えます。
Il2CppDumperが出力したdllのメソッドは、ほとんどがprivateなメソッドになっていて、
Harmonyで指定するときに見つからなかったり、privateだから読めなかったりということが起きる。
この問題を解決するために、privateなメソッドをpublicなメソッドにしたdllを出してくれるツール。
最新のUnhollowerを取得
適当なディレクトリに展開
コマンドを実行
$ ./AssemblyUnhollower.exe --input="D:\game\steamapps\common\Among Us\DummyDll.2021.11.9s\DummyDll" --output=work --mscorlib="D:\game\steamapps\common\Among Us\mono\Managed\mscorlib.dll"
inputにはIl2CppDumperが出したDummyDllのディレクトリを指定
outputには出力先を指定
mscorlibにはBepInExで入れたmono配下にあるmscorlib.dllを指定
出力されたファイルを分かりやすい場所に置いておく
Unhollowerで作ったファイルはコーディングのときに使います
Getting Startedで使えそうなのはこんなところかな?
名前を書き換えるModを意識しながら組んでみる
ReactorのGetting Startedに登場する名前を強制的に変更するMod。
これをBepInExと、ここまでに登場した情報を使って、意識しながら作ってみる。
まずはテンプレートを使ってプロジェクトを生成。
$ dotnet new bep6plugin_il2cpp -n ChangeName
テンプレート "BepInEx 6 Il2Cpp Plugin Template" が正常に作成されました。
IDEで開いて、Plugin.cs を変更していく。
今回は名前を変更するパッチなので、ChangeNamePatchとでもして、本来のPlayerControlの処理の動きが終わった後に名前を書き換えるので、Postfixを使う。
code:Plugin.cs
public static class ChangeNamePatch
{
public static void Postfix(PlayerControl __instance)
{
__instance.nameText.text = "つちなが";
}
}
ここで重要なのが __instance という名前。この名前を変えると動かなくなる。
__instance はPostfixなどでは意味のある変数名で、この変数名にClassを渡すというお約束になっている。
なので、別の名前にすると、そんな名前はねぇ!!!と怒られることになる。
ただこのままだと、PlayerControl で Can not resolve symbol 'PlayerControl' ってWarnが出る。
依存関係のなかに PlayerControl というclassがないからかな。
なら、依存関係に追加しちゃいましょう。
BepInExのNuGetで探せば見つかります。
ただ僕の環境ではこれを実行しても名前解決などはできませんでした。
思ってた使い方と違う?もうちょっと調べないと分からない。
もしくは、Il2CppDumperで出したdllからさらにUnhollowerで出したdllを利用するか。
Riderを利用してる場合、Dependenciesを右クリックで Add Reference... を選択し、 Add Form で Unhollowerで出したすべてのファイルを指定する。
https://scrapbox.io/files/6196e7fa357e21001df53b1b.png
https://scrapbox.io/files/6196e80905b42f001d5c325b.png
https://scrapbox.io/files/619761b70d0b2b001ff56661.jpg
これで名前解決できないエラーは消えるはず。
ここまでやって気付いた。
bepinexにunhollowedが出てる!!!
わざわざ手動でunhollowerを使う必要がない!!!くそー!!!
あとは、ChangeNamePatch classにHarmonyPatchの定義と、
LoadメソッドにPatchAllを追記すれば動くものができているはず!
code: ChangeName
namespace ChangeName
{
public class ChangeName : BasePlugin
{
public override void Load()
{
// Plugin startup logic
Log.LogInfo($"Plugin {PluginInfo.PLUGIN_GUID} is loaded!");
Harmony.CreateAndPatchAll(typeof(ChangeName));
Harmony.CreateAndPatchAll(typeof(ChangeNamePatch));
}
public static class ChangeNamePatch
{
public static void Postfix(PlayerControl __instance)
{
__instance.nameText.text = "wocun";
}
}
}
}
これをbuildし、吐き出されたChangeName.dllをBepInExのpluginsに放り込めばOK!
名前がwocunにかわった!!!
https://scrapbox.io/files/6197a65eebd589001e308132.jpg
参考
更新履歴