純規の暇人趣味ブログ

手を突っ込んで足を洗う

[Bukkit/Spigot]自作プラグインで/titleと同じ事をする

      2016/02/22    HimaJyun

Bukkitプラグイン開発者のみなさまこんにちは、
こんにちはこんにちはこんにちは、@HimaJyunです。

今回は、みなさまが開発しているBukkitプラグインでも簡単に/titleコマンドと同じ事が出来るAPIもどきを公開致します。

TitleSender

今回紹介するのは「TitleSender」と名付けられています。
前回紹介したプラグイン(ThisWorld)を作成する際に出来た副産物です。

[Bukkit/Spigot]別世界への移動時に/titleを表示するプラグイン作った

経緯

先程のプラグインを作成するにあたって、Minecraft 1.8から追加された/titleコマンドと同じ動作をBukkit側で行う必要がありました。

しかし、探せど探せどBukkitAPIにはそれらしき機能が見当たりません。
まぁ端折って言うと、それらの機能はBukkitAPIには存在しないようです。

そこで、動く大図書館Googleにお伺いを立てた所、以下の2つがヒットしました。

当初は前者の「TitleAPI」を利用していたのですが、ここで問題に気付く
「あれ、表示時間設定が効いてねぇ……」

色々と試行錯誤を繰り返したのですがダメ、諦めて後者の方を見たのですが
「毎度毎度newしてインスタンスを作成するのって……」

早い話、両者共に設計が僕の考え方と合わなかったのです。
そのため、両者を参考に自分で作成した、と言う訳です。

使い方

少々クソコードなのはご愛嬌……

  1. ソースをコピペ
  2. 変数を宣言->Bukkitが起動してからnew
  3. 使う->ナウい

ソースコード

勝手にコピペして使え~
(※ソースコードをダブルクリックでコピーがハッピー)

// パッケージは必要に応じて書き換えてね。
package your.program.package;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

/**
 * 1.8から追加された/titleコマンドと同等の事が出来るclassです。
 */
public class TitleSender {

	// 注意::パケットを直接送信しています、良く分からない場合は変更しないで下さい(バグりますよ?

	// class取得用の変数
	private String netMinecraftserver = "net.minecraft.server.";

	private Object enumTitle,enumSubtitle,enumTime;
	private Constructor<?> constructorTitle,constructorTime;
	private Method methodTitle,methodHandle,methodSendpacket;
	private Field fieldConnection;

	/**
	 * 初期化を行います。
	 * 必ずBukkitが起動した後(onEnableなど)で呼び出してください
	 */
	public TitleSender() {
		try {
			// 一時的に使用する変数です、Bukkitのバージョンを取得しています。
			String tmp_package[] = Bukkit.getServer().getClass().getPackage().getName().split("\\.");
			String tmp_version = tmp_package[tmp_package.length-1]+".";

			// サーババージョンの取得&結合
			netMinecraftserver += tmp_version;

			// 一時的に使用する変数です、必要なclassを取得しています。
			Class<?> tmp_packetPlayout = getNMSClass("PacketPlayOutTitle"),
					tmp_ichatBase = getNMSClass("IChatBaseComponent"),
					tmp_packetPlayoutEnumtitle = getNMSClass("PacketPlayOutTitle$EnumTitleAction");

			// 必要なclassを取得します。
			enumTitle = tmp_packetPlayout.getDeclaredClasses()[0].getField("TITLE").get(null);
			enumSubtitle = tmp_packetPlayout.getDeclaredClasses()[0].getField("SUBTITLE").get(null);
			methodTitle = tmp_ichatBase.getDeclaredClasses()[0].getMethod("a", String.class);
			constructorTitle = tmp_packetPlayout.getConstructor(tmp_packetPlayout.getDeclaredClasses()[0], tmp_ichatBase);
			constructorTime = tmp_packetPlayout.getConstructor(tmp_packetPlayoutEnumtitle,tmp_ichatBase, int.class, int.class, int.class);
			enumTime = tmp_packetPlayoutEnumtitle.getEnumConstants()[2];

			try {
				methodHandle = Class.forName("org.bukkit.craftbukkit."+tmp_version+"entity.CraftPlayer").getMethod("getHandle");
			} catch (Exception e) {
				e.printStackTrace();
			}
			fieldConnection = getNMSClass("EntityPlayer").getField("playerConnection");
			methodSendpacket = getNMSClass("PlayerConnection").getMethod("sendPacket", getNMSClass("Packet"));
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * プレイヤーに表示されているタイトルをリセットします。
	 * @param player
	 */
	public void resetTitle(Player player) {
		sendTitle(player,"","");
	}

	/**
	 * タイトルを送信します。
	 * @param player 対象のプレイヤー
	 * @param title 表示するメインタイトル、無い場合はnull
	 * @param subtitle 表示するサブタイトル、無い場合はnull
	 */
	public void sendTitle(Player player, String title, String subtitle) {
		try {
			if (title != null) {
				sendPacket(
						player,
						constructorTitle.newInstance(
								enumTitle,
								methodTitle.invoke(null,
										"{\"text\":\"" + title + "\"}"
										)
								)
						);
			}

			if (subtitle != null) {
				sendPacket(
						player,
						constructorTitle.newInstance(
								enumSubtitle,
								methodTitle.invoke(
										null,
										"{\"text\":\"" + subtitle + "\"}"
										)
								)
						);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}


	/**
	 * タイトルを表示する時間を設定します(単位::second)
	 * @param player 対象のプレイヤー
	 * @param feedIn タイトルのフェードイン
	 * @param titleShow タイトルの表示時間
	 * @param feedOut タイトルのフェードアウト
	 */
	public void setTime_second(Player player, double feedIn, double titleShow, double feedOut) {
		setTime_tick(
				player,
				(int)(feedIn * 20),
				(int)(titleShow * 20),
				(int)(feedOut * 20)
				);
	}

	/**
	 * タイトルを表示する時間を設定します(単位::second)
	 * @param player 対象のプレイヤー
	 * @param feedIn タイトルのフェードイン
	 * @param titleShow タイトルの表示時間
	 * @param feedOut タイトルのフェードアウト
	 */
	public void setTime_second(Player player, int feedIn, int titleShow, int feedOut) {
		setTime_tick(
				player,
				feedIn * 20,
				titleShow * 20,
				feedOut * 20
				);
	}

	/**
	 * タイトルを表示する時間を設定します(単位::tick)
	 * @param player 対象のプレイヤー
	 * @param feedIn タイトルのフェードイン
	 * @param titleShow タイトルの表示時間
	 * @param feedOut タイトルのフェードアウト
	 */
	public void setTime_tick(Player player, int feedIn, int titleShow, int feedOut) {
		try {
			sendPacket(
					player,
					constructorTime.newInstance(
							enumTime,
							null,
							feedIn,
							titleShow,
							feedOut
							)
					);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 作成したパケットを送出します。
	 * @param player 対象のプレイヤー
	 * @param packet 送信するパケット
	 */
	private void sendPacket(Player player, Object packet) {
		try {
			// パケットの送信
			methodSendpacket.invoke(
					fieldConnection.get(
							methodHandle.invoke(player)
							),
					packet
					);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * net.minecraft.serverのclassを取得します。
	 * @param name 取得したいclassの名前
	 * @return 取得したclassを返却します、例外が発生した場合にはprintしてnullを返却します。
	 */
	private Class<?> getNMSClass(String name) {
		try {
			return Class.forName(netMinecraftserver+ name);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

}

このコードをコピペしてみなさんのプロジェクトに追加しましょう。

準備

見ての通り、Class.forNameを極力減らすために、事前に取得して変数にぶち込んでいます。
(これが実行速度に影響あるかどうかは別として)

しかし、net.minecraft.serverのclassを取得するにはバージョン(v1_8_R3的なの)が必要です。
これはBukkit().getServer()...で取得しています、つまり、コンストラクタが実行される段階でBukkitが起動している必要があります。

ですので、このクラスをnewするのは、Bukkitが確実に起動した後(onEnableがおススメ)にして下さい。

参考までに、こうですね。

package your.program.package;

import org.bukkit.plugin.java.JavaPlugin;

public class sampleClass extends JavaPlugin {

	// TitleSender変数を宣言
	TitleSender TitleSender;

	@Override
	public void onEnable() {
		// onEnableの段階でnewする。
		TitleSender = new TitleSender();
	}
}

もし、他のclassでも使いたければ、毎度毎度newするよりコンストラクタの引数としてインスタンスを渡してあげた方が良いかと……
もしくは、変数をstaticにするとか?

あと、クラスを取得すると言う割と普通では無い手法のやり方なのでバージョンに依存する要素があるかも知れません。
ご了承下さい。

使う

JavaDocを書いてあるのでそう難しくないです、EclipseとかだとTitleSender.の時点で入力補完してくれるはず。
例えば、「TitleSender.sendTitle(Player,"メインタイトル","サブタイトル");」ですね。

まぁ、クソコードですけどご自由に使って下さい。
もし、「ここはこうするべき」みたいな先人の知恵があればコメントとかで教えて頂ければありがたいです。

では、素晴らしきかな……プログラミングの世界。

 - プログラミング ,