BukkitのOfflinePlayerをPlayerに変換する方法

投稿日:

この記事はhydrogen Advent Calender 2024の7日目の記事です。

OfflinePlayerは不便

Bukkit APIにおいて、ログイン中のプレイヤーの情報はBukkit.getPlayerで取得可能でありスコアボード情報を読み書きしたり、テレポートさせたりとほとんどの操作が可能である。一方、オフラインのプレイヤーの情報はこのメソッドでは取得できず、Bukkit.getOfflinePlayerを使う必要がある。そしてそれで出来ることといえば、UUIDや名前を取得するくらいである。一応統計情報を編集することも可能ではあるが、あまり使用頻度は高くないだろう。

例えば、資源ワールドの再生成に伴ってワールド内のプレイヤーをすべて安全地帯に移送する必要があるとき、オンラインのプレイヤーはそのままテレポートさせればいいが、オフラインのプレイヤーは単純にはテレポートさせることができない。この場合、Bukkit.getOfflinePlayerで取得したOfflinePlayerPlayerに変換する必要がある。

PlayerOfflinePlayerのサブクラスであるため単純にキャストしてどうにかなるものではなく、上手くその差分を補う必要がある。

本体のソースコードを追う

流石にPlayerに変換するメソッドが用意されているわけではないので、NMSというMinecraft本体の実装とCraftBukkit本体を覗いて手段を探ることとしよう。

しかし特に何もしていない場合、PaperMCの開発環境ではNMSやCraftBukkitのソースコードは見れない。そのため、次のような記述をbuild.gradleに追加して再ロードすることで、それらのソースコードを見ることができるようになる。

plugins {
    id 'java'
    id 'io.papermc.paperweight.userdev' version '1.7.2'
}
...
dependencies {
    ...
    paperweight.paperDevBundle "1.21-R0.1-SNAPSHOT"
}
...

そして、ソースコードを見るとPlayerのNMS上の実体がServerPlayerであることがわかる。そして、ServerPlayer#getBukkitEntityメゾットからPlayerの実装であるCraftPlayerを取得することができるとわかる。

そのServerPlayerのインスタンスを取得するには、プレイヤーのUUIDと名前、そしてMinecraftServerクラスのインスタンスが必要であることがわかる。また、実際にはオンラインではないプレイヤーなので、クライアント情報と接続情報にダミーの値を設定する必要がある。

それだけでは不十分であり、ServerPlayerのインスタンスの特定のprivateフィールドにCraftPlayerのインスタンスを設定してやる必要がある。ServerPlayer#getBukkitEntityはこのフィールドを返すようになっている。このように設定しなければならないのは、CraftBukkit側のほかの実装が直接的あるいは間接的にこのフィールドを参照しているためである。

なお、ここで設定するCraftPlayerのインスタンスはそのままnewしたものは使えない。ログイン日時情報などのいくつかの上書きすべきでない情報が含まれているため、それを除去したのちにセーブするようにしなければならない。

また、進捗や統計情報などが更新されないように諸々の処理を追加で行う必要がある。また、ServerPlayerのインスタンスをnewしただけではNBTデータなどは読み込まれないため、それらを読み込む処理も追加する必要がある。

注意点

私はあえて上の話をする際に具体的なコードを示さなかったが、これはMinecraft本体の実装をこういう場に記すのが極めて危険であるからである。かつて、Minecraft本体の実装を公開したことで、BukkitにDMCAが発動されたことがある。尤も、有料ソフトのソースコードをみだりに公開してはならないのは極めて当然の話ではある。

加えて本体の実装はかなり頻繁に変更される。例えば、私がソースをまじめに解析したのは1.21のもののみであり、それ以外のバージョンで同じようなものが成立するとは限らない。

特にこのゲームは頻繁に内部の実装が変更されるため、それに合わせなくてもいいようにBukkit APIが提供されているのにも関わらず、それを使わずにNMSを直接触ることは推奨されない。もし、それが必要である場合でも、その実装はバージョンごとに変わる可能性があるため、その都度対応する必要がある。マイナーバージョンの変更ですらきわめて神経質になる必要があり、かなりの労力がかかる。

実際に、NMSに直接触ることが必要な場合でも、それを隠蔽するラッパーを作成することが推奨される。どのようにそれを行うかは、実装を実際に触ってあれこれしているようなプラグインのソースコードを見ることで学ぶことができる。