Skip to content

Mod reload ja

Tom_XV edited this page Sep 23, 2026 · 2 revisions

English | 日本語

実験的な機能です。 中核 1.2.0(2026-09-17 リリース)で入りました。Mod を作る人向けで、Developer tools がオンのときだけ動きます。

Mod をビルドして、ゲームを閉じて、また起動して、タイトル画面を抜けて、店を開く。これで 1 回試すのに 1 分かかり、それを 1 時間に何十回も繰り返すことになります。Mod の再読み込みを使えば、この手間がなくなります。ビルドすると、ゲームを動かしたまま、新しい DLL が動いている版と入れ替わります。開発者ツールの中の機能(ほかの Mod と一緒に動かす のルール 8)なので、遊ぶ人の目に触れることはありません。

できること・できないこと

  • Mod(プラグイン):できます。 ただし、Mod がそう宣言したときだけです(後述)。Mod がフレームワークに登録するものはすべて GUID を持っているので、古い版をきれいに外せます。
  • ライブラリ(フレームワークの Dialogue、Assets など。自作のライブラリも):できません。 ほかの Mod がライブラリの型を直接参照しているので、ライブラリだけ新しくしても、その Mod は古い型に結びついたままになります。ライブラリは再起動が要ります。
  • Mono はアセンブリを外せません。 再読み込みでは、新しいビルドを古いものの隣に読み込んで、古いほうが何もしないようにします。古いコードはメモリに残りますが、1 回あたり数百 KB なので、開発中なら問題になりません。
  • 遊ぶ人向けの「再起動なしで Mod を入れる・更新する」:できません。 「起動時に一度だけ」を前提に書かれた Mod が壊れますし、下の Direct3D 12 の問題が遊ぶ人に降りかかるからです。遊ぶ人はこれまでどおり再起動します。
  • 対象は DLL だけです。テクスチャには専用の読み直しがあり(Assets)、Drag'n Wash Localization の翻訳ファイルにもあります。

使い方

  1. Developer tools をオンにします(Options → Mods → Drag'n Wash ModFramework)。
  2. Mod が再読み込みに対応していると宣言します。登録時に ModInfo.Reloadable = true にするか、プラグインのクラスに [ReloadableMod] 属性を付けます。何も言わない Mod は再読み込みされません。
  3. ビルドしたら、DLL を <Mod>.dll.new という名前で、入っている DLL の隣に置きます(下の .csproj の例がこれをやってくれます)。Windows では動いている DLL を Mono が掴んでいて上書きできないので、フレームワークは .new から読み直し、次の起動時にプリローダーのパッチャーがそれを本物の DLL にします。上書きできる環境(Linux)では、DLL そのものが変わったときも読み直します。

ファイルが変わらなくなってから 0.5 秒後に Mod が読み直され、ログ(と Console)に Reloaded <Mod> (1st time) from <file>. と出ます。DLL の隣に .pdb があれば、それも一緒に読み込みます。

変更を拾うのは、ゲームの窓が前に出ている間です。バックグラウンドでは Unity がゲームの更新処理を止めるので、その間に届いたビルドは、ゲームに戻ったときに読み直されます。

場所 内容
[Developer] WatchMods(Mods 画面の Drag'n Wash ModFramework、Show advanced settings をオンにすると出る Reload mods when their DLL changes。既定はオン) 対応を宣言した Mod の DLL か .dll.new が変わったら、自動で読み直す
Console の mods reload <guid> 1 つの Mod を今すぐ読み直す。WatchMods がオフでも使える
mods watch on、mods watch off WatchMods を切り替える
mods どの Mod が対応していて、それぞれ何回読み直したかを出す
Mods 画面 読み直した Mod には Reloaded と、このセッションでの回数、動いているのが BepInEx が読み込んだファイルではないことが出る
<!-- .csproj に。ビルド後に DLL が .dll.new としてゲームへ行き、動いているゲームがそれを読み直します。 -->
<PropertyGroup>
  <GameDir>C:\Program Files (x86)\Steam\steamapps\common\Drag'n Wash</GameDir>
</PropertyGroup>
<Target Name="CopyToGame" AfterTargets="Build" Condition="Exists('$(GameDir)')">
  <Copy SourceFiles="$(TargetPath)" DestinationFiles="$(GameDir)\BepInEx\plugins\$(AssemblyName)\$(AssemblyName).dll.new" />
</Target>

再読み込みで起きること

  1. 先に新しいビルドを確かめます。 バイト列から読み込むのでファイルはロックされず、次のビルドの邪魔をしません。そこから同じ GUID のプラグインを探します。失敗したら理由をログに出して、古い版はそのまま動かします。入れ替えられないのに止めてしまう、ということはありません。
  2. 古い版を外します。 古い版がフレームワークとライブラリに登録したものを、GUID とアセンブリで外します。Tool window のタブとコマンド、テキストの書き換え、GameEvents の処理、ライブラリのイベントにつないだ処理、サービス、GameHooks の記録、ModInfo です。Harmony のパッチを外し、プラグインのコンポーネントを破棄するので、OnDestroy が走ります。
  3. 新しい版を始めます。 BepInEx が古い版を始めたのと同じ場所に置かれ、起動時と同じように Awake が走ります。GameEvents.OnGameStarted の処理は、遅れて登録したときと同じようにすぐ走ります。同じ id で Options の行をもう一度足すと、古い行のコールバックを引き継ぎます。

手順 2 より前に失敗したときは、古い版が動き続けます。そのあとで失敗したときはログにそう出て、Mods 画面ではその Mod の動いている版に使えない印が付きます。この場合は再起動が要ります。

古いアセンブリと、ゲームがまだ持っている古い版のオブジェクトは残ります。

Direct3D 12

新しい版の Awake はゲームの途中で走ります。そこでテクスチャを読み込んだりフォントを準備したりする Mod は、Direct3D 12 が落ちうるまさにその瞬間にアップロードすることになります(Unity UUM-140564)。ネイティブのクラッシュは捕まえられないので、フレームワークは再読み込みの前に印のファイル(BepInEx/config/<中核の GUID>.reload-in-progress)を書き、終わったら消します。次の起動で印が残っていたら、再読み込みでゲームが落ちたということです。そのときはログにそう出て、自分でオンに戻すまで WatchMods はオフになり、Mods 画面では「Automatic mod reload」が使えない機能として出ます。起動オプションに -force-d3d11 を付けて作業すれば、この問題は起きません。

Mod 側の約束

再読み込みに対応すると宣言した Mod は、次を守ってください。

  • Harmony の ID は自分の GUID(new Harmony(MyMod.Guid))。パッチはこれで見つけて外します。
  • 自前の static フィールドで抱えず、フレームワーク経由で登録する(ModFramework.Register、AddTab、AddCommand、AddRewriter、GameEvents、Services、GameOptions)。フレームワークが知らないものは外せません。
  • まっさらになるのは、新しいアセンブリ自身の static だけです。古い版がゲームに渡したもの(コルーチン、DontDestroyOnLoad のオブジェクト、ファイルの監視)は OnDestroy で片付けてください。
  • Awake に、Direct3D 12 でゲームの途中に走らせて危ないこと(テクスチャのアップロード)を置かない。置くなら GameFonts.RuntimeUploadsAreSafe で分ける。

ライブラリを作る人へ:ModReload.Unloading

ライブラリはほかの Mod からの登録を抱えているので、Mod が読み直されるときは、その古い登録を外さないといけません。ModReload.Unloading は、Mod の古い版を外す直前に、その GUID とアセンブリを渡して呼ばれます。その Mod があなたのライブラリに登録したものを、GUID で持っているものは GUID で、そうでないものはアセンブリで外してください。

ModReload.Unloading += (guid, assembly) =>
{
    MyLibrary.RemoveOwned(guid);                                              // GUID で持っているもの
    ModReload.PruneEvent(typeof(MyLibrary), nameof(MyLibrary.Changed), assembly); // static なイベントの処理
};
メンバー 内容
event Action<string, Assembly> Unloading Mod の古い版を外す前に呼ばれる。例外を投げた処理はログに出て、ほかの処理は呼ばれる
Delegate Prune(Delegate handlers, Assembly assembly) 処理のうち、メソッドか対象がそのアセンブリにあるものを除いたもの
void PruneEvent(Type type, string eventName, Assembly assembly) type の static なイベントに、名前で Prune をその場でかける
string Reload(string guid) 次のフレームでの再読み込みを予約する。予約した内容か、できない理由(開発者ツールがオフ、読み込まれていない、対応していない、ライブラリ)を返す
bool IsReloadable(string guid)、IReadOnlyList<string> ReloadableMods() Mod が対応を宣言したか。宣言した読み込み済みの Mod の一覧
int ReloadCount(string guid) このセッションで Mod を読み直した回数
bool Watching WatchMods と開発者ツールがどちらもオンなら true

どれも中核の DragNWash.ModFramework 名前空間にあります。ReloadableModAttribute もここです。

Clone this wiki locally