Modのロードの流れとロードに介入する方法

https://github.com/r3qu13m/PythonModLoader を書いた時に調べたことのメモ

Minecraft 1.4.7, Minecraft Forge #534を対象とする.

基本的な流れ

まず最初に cpw.mods.fml.common.Loader#loadMods から始まる. ファイル自体の読み込みと識別, クラス探索はここでは置いておい, 実際にModのpreinit, init等をどのようにして呼んでいるのかについて見ていく.

まずModを探索し, 依存関係によってModをソートした後にリスト自体をImmutableListへ突っ込む. 493行目の時点でModのリストアップは終わっているので, 後はLoaderStateをCONSTRUCTING, PREINITIALIZATION, INITIALIZATIONとtransitionさせるだけ. この時, EventBusへイベントを流していくことでModのinit, preinit等を呼ぶことになる.

ModContainer

coremodsを作るなら大体cpw.fml.common.DummyModContainerを実装する(はず)だが, これについてちゃんと調べた. ModContainer自体はインターフェースで, Modのメインファイルや名前, ModIdについての情報のメソッドを定義している. この中にregisterBusというメソッドがあり, 次のような定義となっている.

    /**
     * Register the event bus for the mod and the controller for error handling
     * Returns if this bus was successfully registered - disabled mods and other
     * mods that don't need real events should return false and avoid further
     * processing
     *
     * @param bus
     * @param controller
     */
    boolean registerBus(EventBus bus, LoadController controller);

このbus変数はその名の通りModのロードについてのEventBus. cpw.fml.common.FMLModContainerクラスではこの時にオブジェクトとして自分自身を登録しており, 次のようなメソッドでイベントを受け取っている.

cpw.fml.common.FMLModContainer 474〜495行目

    @Subscribe
    public void handleModStateEvent(FMLEvent event)
    {
        Class<? extends Annotation> annotation = modAnnotationTypes.get(event.getClass());
        if (annotation == null)
        {
            return;
        }
        try
        {
            for (Object o : annotations.get(annotation))
            {
                Method m = (Method) o;
                m.invoke(modInstance, event);
            }
        }
        catch (Throwable t)
        {
            controller.errorOccurred(this, t);
            Throwables.propagateIfPossible(t);
        }
    }

各フィールドの意味については実際にコードを読んでもらえば分かるが, 見たままアノテーションとイベントのクラスが一致するメソッドを順に呼び出していることが分かる. これは結局, @Mod.Init等のアノテーションの付いているメソッドを順に呼び出していることに等しい.

以上で大まかな流れは終わり.

実際の介入

ここに介入するタイミングはいくつか存在する. 例えば,

  • Modのメインクラスのコンストラクタ (LoaderState.CONSTRUCTING)
  • @Mod.PreInit (LoaderState.PREINITIALIZATION)
  • @Mod.Init (LoaderState.INITIALIZATION)

等が挙げられる. ここでは, Mod自体にinit, preinit, postinitを持たせたかったので, コンストラクタで書くことにした. コンストラクタ内ではReflectionHelper経由でLoadController, EventBus, Modの一覧のリストを取得し, このうちModのリストについてはImmutableListになっているのでこれをLinkedListへ変更する. その後, 適宜検索・JavaのクラスとしてのModを構築し, Mod一覧のリストへこれを追加する. 注意点として, LoadController#getActiveModList()で取得できるリストへModContainerを追加しておかないとActive ModとLoaded Modの数に差が発生し, 見た目だけだが少し微妙になってしまう. また, registerBusも自前で呼ぶ必要がある. 最後にReflectionHelperで操作後のModのリストをセットする. これにより, この後のpreinit, init, postinitでのイベントをModContainerの実装側で受け取れるようになる. PythonModLoaderのPythonModContainer.javaのhandleModStateEventメソッドでは簡単にこれを受け取るだけのメソッドを実装して利用している.

PythonModLoaderではJythonを利用したが, Jythonは少し遅いので実際に使うのであれば標準のRhinoインタプリタや高速ということで有名なJPHPを利用するほうが良いだろう. ファイル監視とRhinoインタプリタを組み合わせれば, JavaScriptで実装を書けるアイテムレンダラなんかも作ることが出来ると思う.