Java8においてForgeの完全動作のために書いたパッチ類

Java8環境において, 内部のコレクション/マップのとり方が変更されたためと思しきパッチ失敗が発生するようになっていた. これを修正したので, 備忘録も兼ねてパッチを公開する.

環境としてはMinecraftForge #534が対象である. forgeディレクトリの展開の後, このパッチを適用してinstall.shを実行する.

mcp関係のファイルをいちいちファイルダウンロードさせるのにも困っていたので, fml/fml.pyにもパッチを書いた. 使い方はパッチを読めばわかるが, LWJGL-2.9.3のファイル, 各種ライブラリファイル(asmもパッチ通りasm-all-5.2で良い), minecraft.jar, mincraft_server.jar, mcp7.26a.zipを用意してforgeのinstall.shからの相対パス./mcp_files, ライブラリファイルだけ./mcp_files/lib/に突っ込めばよい. install.shを叩くのが楽になった.

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で実装を書けるアイテムレンダラなんかも作ることが出来ると思う.

ネタのメモ

  • ケルトンのレアドロにSkeleton's Arrow(テクスチャは普通の矢にポーションキラキラつけただけ)、これを弓で撃ってクリーパーを倒すとレアドロでレコード - coremods?
  • スライムのドロップにSlime Bloodを追加, これでSlime-Bloody Swordを作って使うとスライムが分裂せずに一発で倒せる(ドロップは一体分) - スライムの死亡判定周りでcoremods化必要?
  • BuildCraftの木エンジンがエネルギーパイプに繋がるようにする - 書いた
  • BuildCraftのクァーリー周りの処理を書き直す - 面倒
  • Forestory for MinecraftのアイテムがLP Extractor Mk[1-3]で吸いだされた時にNullPointerExceptionで落ちるバグの修正 - デコンパイラ作ってから or coremodsでファイル差し替えする?
  • 和製版 Additional Pipes 2.0.10のテレポパイプでLPネットワークがつながらないバグの修正 - ソースコードあるしやっておきたい
  • RP2のコンフィグを読み込めるパーサー - RP2のアイテム使いたい
  • フィラーのアイテム回収モード追加Mod - 書いたけど公開面倒なので放置
  • 1.2.5のModの移植 - 面倒
  • コンパイラ作る - 面倒
  • ライブラリの更新, Java 8への対応 - 面倒そう
  • リソースパック?とか1.5.xのModを無修正で使えるようにするようなローダー or コンバーター - 確実に面倒
  • 描画方式の最適化 - 大体検討はついてる
  • Bukkitプラグインをロード出来るようにするcoremods - 面倒

サウンドシステムの扱いについて

Minecraft 1.4.7, Minecraft Forge #534でサウンドシステムを触った時のメモ。


MinecraftのサウンドシステムはPaul's Code SoundSystem (http://www.paulscode.com/forum/index.php?topic=4.0)を利用していることがクレジット等からわかるので、まずはそのソースコードを読む。また、Forge側にもいくつかサウンド関連のイベントが存在するのでそちらも読む。列挙すると、

  • SoundEvent(抽象クラス)
  • SoundSetupEvent
  • SoundLoadEvent
  • PlaySoundSourceEvent
  • PlayStreamingSourceEvent
  • PlaySoundEffectSourceEvent

  • SoundResultEvent(抽象クラス)

  • PlaySoundEvent
  • PlayBackgroundMusicEvent
  • PlaySoundEffectEvent

となる。今回やりたいのはいわゆる効果音の再生なので、用があるのはSoundSetupEventだけになる。

そのSoundSetupEventは、SoundManagerクラスのtryToSetLibraryAndCodecsメソッドでイベントバスに投げられている。実際にやってみると分かるが、このイベントが投げられるのは@Mod.PreInit@Mod.Initの間なので、イベントハンドラの設定は@Mod.PreInitでするべき。

SoundSetupEventは、SoundManagerの初期化終了後に呼び出されるイベントという意味になる。今回のような時はいちいち取得するのも面倒なので、このイベントでSoundManagerを取得し、適当なフィールドに保存しておくのが良いかと思う。

@SideOnly(Side.CLIENT)
public class SoundEventHandler {
  public static SoundManager manager;
  
  @ForgeSubscribe
  public void onSoundSetup(SoundSetupEvent event) {
    manager = event.manager;
  }
}

サウンドを利用するにはまず登録が必要となる。この登録処理はどこで行っても良いが、私はイベントハンドラ内で行っている。 注意点として、.minecraft/resources/***/***のような実際のファイルシステム上のパスではなくSomeMod.class.getResource("***/***")のような取得方法をしている場合、開発環境ではうまく行っても実際にはうまくいかない。理由としては、恐らく内部実装がjar(zip)内データの扱いが出来ないことが原因だと推測する。

登録は以下の形にする。

manager.addSound(登録名, 実体へのFileインスタンス);

登録名とは、"aaa/bbb/ccc.ogg"というような任意の文字列 + 拡張子の形の文字列、実体へのFileインスタンスは、実際のファイルへのFile(java.io.File)インスタンスである。


アイテムのonItemUse内で音を再生する時、次のように書く。

public boolean onItemUse(ItemStack par1ItemStack, EntityPlayer par2EntityPlayer, World par3World, int par4, int par5, int par6, int par7, float par8, float par9, float par10) {
    if (FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT) {
        System.out.printf("[+] Play Sound!\n");
        SoundEventHandler.manager.playSound("aaa.bbb.ccc", (float) par2EntityPlayer.posX + 0.5F, (float) par2EntityPlayer.posY + 0.5F, (float) par2EntityPlayer.posZ + 0.5F, 1.0F, 1.0F);
    }
    return true;
}

playSoundのパラメータは、登録名から拡張子を除いたもの, 再生するX座標, 再生するY座標, 再生するZ座標, ボリューム, ピッチとなっている。


Streaming等の他の要素についても同様に進めることが出来るが、それについてはソースコードを参照。

チェストのようなブロックのレンダリング処理周りについて

Minecraft 1.4.7, Minecraft Forge #534でチェストのようにアニメーションするブロックを書いた時に感じたことのメモ。

目的はいわばIronChestsのダイヤモンドチェストのような機能を提供するチェストブロックを書くこと。基本はEnderChestのような感じなので、まずはEnderChest関係のコードを読み、実装する。

別に星形等の変な形を作るのでもない限り、ここはMinecraftのデフォルトのチェストのモデルを活用すべきだろう。また、TileEntity, レンダラなどを先のEnderChestの実装から考えると次のような構成が考えられる。->のように短い矢印はextends, implements等継承関係を表し、それ以外の矢印は単なる関係を表している。

BlockMyChest <- BlockContainer
     |
     |
     +--> TileEntityMyChest <- TileEntity, IInventory
     |            |
     +------------+----------> TileEntityMyChestRenderer
                                          Λ
                                          |
                               TileEntitySpecialRenderer

これだけを実装し、適宜登録処理をすればひと通りは動く。しかし、よくよく考えるとTileEntitySpecialRendererの実装範囲はワールド上のブロックのレンダリング処理のみを提供しており、インベントリでの描画についてはノータッチとなる。EnderChestやChestはRenderBlocksクラスのハードコーディング部分でうまく処理分けされているのでちゃんと描画される。しかし、今回のようにデフォルト処理を流用して構築したクラスにはハードコーディングされているために対応できず、結果どうなるかといえばチェストが描画されてしまうのである。

これを解決するには以下のような方法がある。

  • ハードコーディング部分をcoremodsやクラス書き換えを用いて拡張可能なコードに置き換える
  • IronChestで用いられている方法を用いる(後述)
  • 地道にアイテムレンダラを書く(デフォルト処理の流用の恩恵を受けない)

2番目の手法について、実際のソースコードを示す。

ironchest/ClientProxy.java at 2735d04de27f5244458d30818e55ad38fe3ec1d6 · cpw/ironchest · GitHub

ironchest/IronChestRenderHelper.java at 2735d04de27f5244458d30818e55ad38fe3ec1d6 · cpw/ironchest · GitHub

この手法は、RenderBlocksの以下の箇所に関連している。

ChestItemRenderHelper.instance.renderChest(par1Block, par2, par3);

RenderTypeがChest/EnderChestの場合(つまり、getRenderType() == 22)の、ChestItemRenderHelper.instance.renderChestというメソッドを利用してインベントリ描画を行っている。

IronChestでは、このChestItemRenderHelper.instanceフィールドに着目した。instanceフィールドは次のように宣言されている。

public static ChestItemRenderHelper instance = new ChestItemRenderHelper();

見て分かるとおり、const指定がないのでこのフィールドには再代入することが可能だ。さらに、ChestItemRenderHelperクラスはfinal指定がされていないため、拡張クラスを作ることが出来る。これらから、ChestItemRenderHelperクラスの拡張クラスを作り、ChestItemRenderHelper.instanceフィールドを拡張したクラスのインスタンスに置き換えることで、レンダリング処理を任意の差し替えることが可能となる。

この手法にはデメリットが存在する。というのも、見て分かる通りこの手法でのキーとなっているChestItemRenderHelper.instanceフィールドは何度でも書き換えることが出来てしまう。その上、同様の手法を他のModで利用しようとすると、何も考えずに書くと競合してどちらかのModのレンダリング結果がおかしくなってしまう。これについては、個別に対応するか、自動判定処理を書いて一般的に適用出来るようにする他ないだろう。その意味では、この手法は非常にその場任せの適当なコードに見える。(しかし、本来書かないといけなかったアイテムレンダラよりは何十倍も短いコード量となる。)

ITickHandler#tickStart, tickEndの引数の調査

Minecraft 1.4.7, Minecraft Forge #534。

ゲーム内でTickごとに実行される処理を記述するのに使われるITickHandlerと呼ばれるインターフェースが存在する。その中で、実際にTickの始まり/終わりのそれぞれで呼ばれるメソッドがtickStart, tickEndだが、それぞれ定義は次のようになっている。

    /**
     * Called at the "start" phase of a tick
     * 
     * Multiple ticks may fire simultaneously- you will only be called once with all the firing ticks
     * 
     * @param type
     * @param tickData
     */
    public void tickStart(EnumSet<TickType> type, Object... tickData);
    
    /**
     * Called at the "end" phase of a tick
     * 
     * Multiple ticks may fire simultaneously- you will only be called once with all the firing ticks
     * 
     * @param type
     * @param tickData
     */
    public void tickEnd(EnumSet<TickType> type, Object... tickData);

typeはともかく、tickDataはこれだけでは何が渡されるのかも分からない。そこで、呼び出し元を探してこれをまとめてみることにした。

以下、基本的なITickHandlerの理解はできているものとし、TickTypeによって場合分けをする。

基本的に、tickStart/tickEndはcpw.mods.fml.common.FMLCommonHandlerから呼び出される。そして、 on(Pre|Post)\w+Tick()メソッドで実際に値を代入して実行しているため、これを見ることで調査が可能。

TickType.WORLD

FMLCommonHandler#on(Pre|Post)WorldTickメソッドが該当。実際の引数はWorldのインスタンス

TickType.WORLDLOAD

FMLCommonHandler#onWorldLoadTickメソッドが該当。こちらはワールドロード時に呼び出され、読み込み可能なWorld全てについて一度ずつ呼び出される。引数はTickType.WORLDと同じく該当するWorldのインスタンス

このTickTypeの時は、tickEndは呼び出されない。

TickType.SERVER

FMLCommonHandler#on(Pre|Post)ServerTickメソッドが該当。引数はなし。

TickType.CLIENT

FMLCommonHandler#on(Pre|Post)ClientTickメソッドが該当。引数はTickType.SERVERと同様になし。

TickType.Render

FMLCommonHandler#onRenderTick(Start|End)メソッドが該当。引数はPartial Render Timeと呼ばれる補正用?の値。一度レンダリング関連で使ったことがあるものの理解はできていない。

TickType.Player

FMLCommonHandler#onPlayer(Pre|Post)Tickメソッドが該当。引数はEntityPlayerインスタンスで、注意点としてはServer/Client両方で呼び出されること。

Ubuntu 14.04で開発環境を整える際のメモ

Minecraft 1.4.7 with Minecraft Forge #534の環境をUbuntu14.04で整えた際、いくつかエラーにあたったのでその対処法。

astyleについて

mcp付属のastyle.exeは少し古いバージョン(2.02.x)で、Ubuntu 14.04のapt-getで入るastyle 2.03と相違がある。このため、python install.pyを実行する前に fml/conf/astyle.cfgmax-instatement-indent=2 の箇所を削除、または max-instatement-indent=199等とする。これは、astyle 2.03から40から120の間の整数でなければならなくなったための処置である。しかし、このためにいくつかForgeのパッチを当てることができず、エラーが生じる箇所がある。RenderPlayer.javaの401行目、var21をvar22に書き換えれば(とりあえずは)動作する。エラーでpatchが失敗している場合は必ず該当ファイルと同階層に失敗したファイルの作業ファイルが残るため、問題があればこれを探して手でマージすれば良い。

JDK 1.8使用時のasmについて

Forgeの手に入れてくるライブラリの中に、Objectweb ASMと呼ばれるライブラリが存在する。しかし、このライブラリはForgeの手に入れてくるバージョンではJDK 1.8に対応しておらず、ライブラリを差し替える必要がある。これはライブラリ自体を最新のもの(私はasm-all-5.1.jarを利用した)に置き換えるだけなので大した作業にはならない。

LWJGLについて

デフォルトのLWJGLは古く、環境によっては動作しないことがあるため、これも入れ替える。asmと同様に最新版をダウンロードし、該当ファイルを差し替えるだけで動作する。私は2.9.3を利用した。 Linux上なので、natives以下にはlinux版のsoを入れないといけない。また、x86_64環境の場合はlib***64.soとなっているsoファイルをlib***.soへリネームしてやらないと、ビット数の違いでエラーを起こす。

その後、プロジェクト全体をリビルド、Client(必要ならServerも)の起動までを確認して構築終了となる。私はこの後全体に対してFormatをかけ、recompile→updatemd5を実行したが、これは任意で良いかと思う。