Visual Studio CodeでSpigotのnms依存なプラグイン開発を行うには

Javaのエコシステムをわからない所から頑張って諸々覚えた。半分はべつにvscodeの話ではないが、今回のじぶんの環境がvscodeだったのでそのようにタイトルに付けることにした。

tl; dr

  • もちろんBuildToolsを使用してビルドをする必要がある
  • Java Decompilerをインストールする
    • Spigot API--generate-sources して *-sources.jar を入れると良い
  • --remapped してmapsを考慮したビルドを行うようにする
  • なにか問題が起きたときはちょっと待つか、WSL2を使う??
  • nmsへの依存を複数バージョンにまたがって実現できるようにするには

nmsって何、という人向けセクション

知っていれば飛ばしてください。

Spigotで使えるプラグインを作成する際、spigot-api に依存することになる。これはSpigotのうち、開発者に対して大まかな互換性の保証をもてる範囲のAPIだけを集めたものとなる。主に org.bukkit.* (以下bukkit)が対象。では、この「範囲」というのはどういう意味なのか?についてだが、まずは下のレイヤーから順番に説明する。

まず最初に来るのが net.minecraft.server (これの頭文字をとってnms。以降もnmsと表記する)。こちらはバニラのMinecraftのサーバーを起動する時に動作するプログラムとおおよそ同じものだと捉えてよいものとなる。これはバージョンアップごとに大きく変動する可能性があり*1、サーバーに対して変更を加えたい開発者からすると直接追従するだけで大きな労力となり得る。

これをどうにかするために、まずは好きな挙動を差し込めるようにしようとしたのが org.bukkit.craftbukkit.* (以下craftbukkit)で、これはMinecraftのバージョンと1:1で毎度同じようなインターフェイスを提供してくれる。ただ、これはバージョンごとにパッケージを切るような対応方法になるので(例: org.bukkit.craftbukkit.v1_18_R1.inventory.CraftItemStack など)、そのまま依存すると例えば一つのプラグインを複数バージョンにまたがって対応したいときにReflectを使わないといけないとかでちょっと大変になってしまう。

それでようやく現れるのがbukkitで、craftbukkitに対しておおよそ1:nくらいの感覚で対応している。これによって、上に上げたような問題を解決できるようになる、ということっぽい。

それで、craftbukkitではすべての機能を拾い上げているわけではないので、従ってbukkitでも叩くことのできない機能が存在する。例えばNBTタグあたりの機能は殆ど使えない。これをどうしても使いたい場合には保証外であることを認識しながらnmsなどを叩くことを検討する必要がある。という感じ。他にも各プレイヤーに直接パケットを送信したりできるようになる。

もしかしたら違うかもしれないので、今回のソース元とした以下の動画も参考にしてみてください。

www.youtube.com

BuildToolsを使用してビルドをする

DMCAの騒動の問題もあり、配布されるようなコードにnmsを同梱することはできない。そのため、BuildToolsというビルド支援ツールを使用してSpigotをビルドする必要がある。これによって、Minecraftのjarファイルのデコンパイル結果から spigot-apispigot をビルドし、mavenのローカルリポジトリ( ~/.m2/repository/ 以下のよしななところ)に配置してくれる。その際のコマンドは以下を実行する必要がある。詳細については後で書く。

java -jar BuildTools.jar --rev [version] --remapped --generate-source --generate-docs

これによって spigot-apispigot のjarがローカルリポジトリに適切に配置される。mavenでの依存解決の際、もしローカルリポジトリに該当するものがあればそちらを先に利用するようになるため、これによって解決できるようになる。

最後に、pom.xmlartifactId としてspigot-api を指定している箇所を spigot に変更すれば良い。

<!-- pom.xml -->
<dependency>
    <groupId>org.spigotmc</groupId>
    <artifactId>spigot</artifactId>
    <version>1.18.1-R0.1-SNAPSHOT</version>
    <scope>provided</scope>
</dependency>

Java Decompilerをインストールすればだいたいどうにかなる

vscodeにてExtension Pack for Javaをインストールした後、Ctrlを押しながらbukkitのクラスやメソッドなどをクリックするとその詳細を見に行くことができる。のだが、この環境でのデフォルトの状態だと(例えばメソッドでは)メソッドの返り値の型やメソッド名、引数の情報あたりしか見ることができない。メソッド内の処理がどうなっているか?等がすべて隠れてしまう。例えば以下のように。

// Failed to get sources. Instead, stub sources have been generated by the disassembler.
// Implementation of methods is unavailable.

public abstract class JavaPlugin extends PluginBase {
   // snip
   public final File getDataFolder() {
      return null; // 本来は `this.dataFolder` が返るはず
   }
}

過去にIntelliJ IDEAを使用してSpigotのプラグインを作成していたときのことを思い出していたのだが、上に挙げたものはそれぞれ適切に確認できた覚えがある。最初はvscodeがこのあたりのソースコードの情報を誤って取得できないようになっていたのではないかと思っていたが、どうやらこれはデコンパイラーの性能差によるものであり、ソースコードとの連携によるものではなさそうということが分かった*2

ということで、vscode側のデコンパイラとしてもう少し力のあるものを使うことでこのあたりの問題が解決するようになる。それの一例がJava Decompilerで、インストールするだけでうまく動くようになった。

 // Source code is unavailable, and was generated by the Fernflower decompiler.

public abstract class JavaPlugin extends PluginBase {
   // snip
   public final File getDataFolder() {
      return this.dataFolder;
   }
}

それか、ちゃんとsources.jarを用意する

実は、ホンモノのソースコードの情報を利用することもできる。先程BuildToolsでのビルド時、 --generate-source というフラグを立てて実行したが、これを実行すると spigot-api の範囲であれば sources.jar を生成してくれる。これは名の通りソースコードをそのまま*3バンドルすることで、実際のコメント文も表示されるようになる。特に、引数に対するDoc的なコメントもあるおかげでマウスオーバー時の挙動が格段と良くなる。

しかし、フラグを立てるだけで適切に使えるようになるわけではなく、ただ sources.jar を生成するだけなので、これをローカルリポジトリに設置する必要がある。以下のように↓。

cp Spigot/Spigot-API/target/spigot-api-1.18.1-R0.1-SNAPSHOT-sources.jar ~/.m2/repository/org/spigotmc/spigot-api/1.18.1-R0.1-SNAPSHOT/
cp Spigot/Spigot-API/target/spigot-api-1.18.1-R0.1-SNAPSHOT-javadoc.jar ~/.m2/repository/org/spigotmc/spigot-api/1.18.1-R0.1-SNAPSHOT/

--remapped を使用して難読化前の名前を使用する。

nmsを利用する際、一番たいへんなのが「欲しいメソッドがどれなのか分からない」だと思う。これはMinecraftのjarを利用する兼ね合いで、難読化された状態のファイルに対してパッチをあてるため。一部のCraftBukkitより利用されるメソッド以外はほとんどアルファベット一文字の名前を持つようになる。

この件の素晴らしい対応として、1.17リリースと同時に更新されたBuildToolsから、--remapped を付けることで難読化されたnmsをMojangでの開発時の名前にマッピングできる機能が追加された。これによって、これまで a() とか b() みたいな本当に苦しい名前を利用する必要があった所を、getTag() とか setLocation() のような人間らしい名前を利用できるようになる。

方法としては1.17の時のリリース記事に書いてあるが、現時点では以下の通り↓。

<!-- pom.xml -->
<dependency>
    <groupId>org.spigotmc</groupId>
    <artifactId>spigot</artifactId>
    <version>1.8.1-R0.1-SNAPSHOT</version>
    <classifier>remapped-mojang</classifier>
    <scope>provided</scope>
</dependency>

dependencyの差として classifier が増えたことが挙げられる。実際、ローカルリポジトリに設置されるjarが増えていて、 remapped-mojangremapped-obf が増えている。きっとこれを使うように変更されるのだろう。

<!-- pom.xml -->
<plugin>
    <groupId>net.md-5</groupId>
    <artifactId>specialsource-maven-plugin</artifactId>
    <version>1.2.2</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>remap</goal>
            </goals>
            <id>remap-obf</id>
            <configuration>
                <srgIn>org.spigotmc:minecraft-server:1.18.1-R0.1-SNAPSHOT:txt:maps-mojang</srgIn>
                <reverse>true</reverse>
                <remappedDependencies>org.spigotmc:spigot:1.18.1-R0.1-SNAPSHOT:jar:remapped-mojang</remappedDependencies>
                <remappedArtifactAttached>true</remappedArtifactAttached>
                <remappedClassifierName>remapped-obf</remappedClassifierName>
            </configuration>
        </execution>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>remap</goal>
            </goals>
            <id>remap-spigot</id>
            <configuration>
                <inputFile>${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf.jar</inputFile>
                <srgIn>org.spigotmc:minecraft-server:1.17-R0.1-SNAPSHOT:csrg:maps-spigot</srgIn>
                <remappedDependencies>org.spigotmc:spigot:1.17-R0.1-SNAPSHOT:jar:remapped-obf</remappedDependencies>
            </configuration>
        </execution>
    </executions>
</plugin>

新規にpluginを追加し、新たなexecutionを追加する。remap-obf はcraftbukkitとnmsを難読化するようなもの。remap-spigotremapped-mojang で置き換えたメソッド名をすべて元のわかりづらいものに戻すような変更を加えるもので、remapped-minecraft が適用されていない(おそらくほぼ全ての)環境で正しく使えるようになる、というもの。この状態で mvn install などでビルドすると target に追加で2つのjarが生成されるようになる。

なにか問題が起きた場合、待ってみたりWSL2を使うと動くようになって何もわからん

これは何も分かっていないんですが、たとえばこれまでは spigot-api に依存していたが、急に spigot に依存するようになる時、手元の mvn は通るのにvscode上では解決できなかったことになる、という事象が発生したりする。この際、Java: Clean Language Server Workspace を実行することや、vscode自体すべて再起動とかでもあまりうまく治らなかったりする。僕の場合、既に2回くらい「待ってたら動くようになった」みたいなものが発生したのだが、これは一体どういうことだったのだろうか。

上記問題はWindows環境で起こっていたため、一度WSL2上で実行したらいいように動くようになった。そもそもWindowsを選んでいたのはfabricのmod開発環境で、デバッグ用のクライアントを立ち上げるのにWSL2だとちょっと道のりが遠そうだったから、というのがあった。Spigotのサーバーであればそのような気遣いは特に必要ないと思うので、素直にWSL2上で組んだほうが良いと思う。

nmsへの依存を複数バージョンにまたがって実現できるようにするには

残念ながらこれはReflectを使うしかない。発想としては以下のgistの getNMSClass を参考にすると良さそう。

NMSManager.java · GitHub

つまり今のMinecraftのバージョンを取得しておいて、そのバージョンがパッケージ名に入っているものを取得する、というもの。ニュアンスを見てなんとなく理解して欲しい。

ただ注意しないといけないのが、最近のバージョンだとクラスだとかの構造が完全に違っているっぽい。これは前述した保証外通りのことだと思う。最近だと1.17と1.18で大きな変更があったようだが、それについては以下を参照。

https://www.spigotmc.org/threads/not-finding-nms-class.548475/#post-4372869

こんな感じで、毎バージョンでおそらくSpigotのチームが行っている努力と同じものをを多少なりとも実行する必要がある。頑張っていただきたい。

さいごに

以上です。一昨日だとかにここに書いたようなことを一人で喋りながら録画するやつをやったので、もし声のほうが良いという人がいればそちらもどうぞ。網羅的じゃないかもしれんけど。

youtu.be

*1:恐らく難読化によるものだと思う

*2:最近のSpigotに限ればであって、過去のバージョンはどうやらそういう時期がなかったらしい

*3:詳しい人から見ると語弊がある気がする