純規の暇人趣味ブログ

広くて浅い知識の湖

[Bukkitプラグイン制作講座-其之九]チェストの中身(インベントリ)を操作する

      2016/11/29    HimaJyun

Bukkit 1.11が出た事ですし、お陰様で様々なプラグインが動かなくなりました。

きっと私の様な代替プラグインの開発、及び1.11対応に悲鳴を上げている方も居るでしょう。

と言う訳で、今回はチェストの中身(及び、インベントリ全般)の操作でも解説するとしましょうか。

前回の記事は「[Bukkitプラグイン制作講座-其之八]複数のymlファイルを扱う」です。

コードはGitHubに置いてありますので適当に眺めながら進んでみると良いでしょう。

チェストの中身を操作したい

私は普段チェストを操作する事はあまりないのですが、それでもふとチェストやインベントリ周りの操作で「どうやるんだっけ?」となる事があるので……自分用も兼ねて。

今回は説明として、「チェストをぶっ叩くと中身を保持したままシュルカーボックスに変える」なんて事をやってみようと思います。

ここで言う「中身を保持」とは入れてある順番(スロット位置)なんかも保持すると言う意味ですよ。

ぶっ叩いたら~~をする

これはイベント関連の話題ですので省略します。

イベント関連は「[Bukkitプラグイン制作講座-其之六]特定の出来事(イベント)に反応する」をご覧ください。

今回利用するのは「BlockDamageEvent」です。

インベントリの取得

getState()をChest型にキャストすれば良いのですが、その前に必ずチェストかどうか(厳密にはインベントリがあるか)を確認する必要があります。

Block block = e.getBlock(); // 殴られたブロックを取得
if (block.getType() == Material.CHEST) { // 本当はinstanceofとか使った方が良いと思う
	// Chestにキャスト
	Chest chest = (Chest) block.getState();
}

「getState()を」ですよ、稀にブロックそのものをChestにキャストしようとして例外ぶん投げられている人が居ます。

Chestにキャストした後はgetInventory()でインベントリが取得できます。

// チェストのインベントリを取得
Inventory inv = chest.getInventory();

ラージチェストの判定

「ラージシュルカーボックス」みたいなのはないので、今回は単一のチェストの場合(ラージチェストではない場合)にのみ作動するようにします。

しかし、どうやらBukkitにそれ専用の機能はないみたいです。

仕方がないので今回はゴリ押しで隣接しているチェストがないかを探し出します。

/**
 * ラージチェストかどうかをチェック(他に良い方法知ってたら教えて)
 * @param chest 対象のブロック
 * @return ラージチェストならtrue、違えばfalse
 */
private boolean isLargeChest(Block block) {
	if (block.getType() != Material.CHEST) { // そもそもチェストじゃない
		return false;
	}

	// 東西南北でテスト
	for (BlockFace face : new BlockFace[] {
			BlockFace.NORTH,
			BlockFace.SOUTH,
			BlockFace.EAST,
			BlockFace.WEST,
	}) {
		// その位置にあるブロックを取得
		Block relative = block.getRelative(face);
		if (relative.getType() == Material.CHEST) { // それが2個横並び(ラージチェスト)なら
			// true
			return true;
		}
	}

	// ラージチェストじゃない
	return false;
}

もしくは、インベントリの大きさ(getSize()で取得可能)が54(ラージチェストのインベントリサイズ)であれば……と言う手もあります、ちょっと信頼性に欠ける気がしますけど……

何か良い方法をご存知であれば教えて下さい。

インベントリ関連の操作

サンプルコード書いたんですけど使わずに終わっちゃいそう……

インベントリ周りで良くある操作をいくつかやってみましょう。

空のスロットを探す

例えばユーザ共にアイテムをぶちまけたい時とか、ユーザの手持ちとかとか、とにかくインベントリ内に空のスロットがあるかどうか、あるならどこか、それを知りたい事があるでしょう。

その様な時は「firstEmpty()」を使います。

int emptySlot = inv.firstEmpty();
if (emptySlot == -1) { // 空のスロットがない
}

文字通り、インベントリ内から最初に見つかる空のスロット位置(面倒なので以後インデックス)を探し出します。

見つからなかった(空のスロットがない)場合は-1が帰ってきます。

余談ですが、私のサーバで現在利用中のあるプラグイン(私が作ったものではない)はインベントリがいっぱいの時に更に追加しようとすると防具枠にアイテムが入り込みます。

動作に支障がないので放置していますが、稀に頭に矢がぶっ刺さったまま歩いている奴が居ます。(ユーザには軽いお遊び機能扱いされている)

(特定の)スロットを空にする

こちらは考える程の物でもないですね。

「clear()」を呼ぶと中を空に出来ます。

inv.clear();

引数にインデックスを渡すと、そのスロットだけ消去する事が出来ます。

inv.clear(1);

上の例であれば、1番のスロットのアイテムを削除します。

ちなみにですが、インベントリのインデックスは配列と同じく0始まりなので、上の例で言う1番は0番(左上)の1個右側のスロットです。

0始まり、忘れないように。

特定のスロットのアイテムを取得する

「getItem(int)」で可能です。

inv.getItem(0);

取得できなかった場合はnullが返ってきます。

ただ、これはインデックスが必要になるため、単体で使う事はあまりないかと思われます。

多くの場合は何かと組み合わせて利用する形になるかと思います。

特定のアイテムを持つか確認する

「contains()」で可能です。

Materialを渡すとそのタイプのアイテムを、ItemStackを渡すと完全に一致するアイテムを探し出します。

inv.contains(Material.STONE);
inv.contains(new ItemStack(Material.STONE,1));

ItemStackを渡す際の注意ですが、ここで道具とかを渡してしまうと、その道具が1回でも使われた時(耐久値が変わった時)に異なるItemStackになってしまうので一致しなくなります。

同じ理由で、ItemStack(Material.STONE,64)とかを渡すと、64個以外の石には一致しなくなります。

ピンポイントでアイテムを探したい時には合いますけど、それ以外は使いづらい、普段はMaterialを渡すかな。

特定のアイテムを消す

「remove()」で可能です。

使い方は「contains()」と同じ

inv.remove(Material.STONE);
inv.remove(new ItemStack(Material.STONE,1));

耐久値どうこうの問題点も「contains()」と同じ

アイテムを追加する

「addItem(ItemStack...)」で可能です。

渡すItemStackは可変長引数になっているので、渡したい物を渡したいだけ渡せます。(可変長引数はほぼイコールで配列なので配列で渡す事も可能です。)

既存のスタックと空のスロットを満たしてくれるナウいメソッドです、また、渡されたItemStackがスタックあたりの最大数を超えている場合は分割されます。

ちなみに戻り値のHashMapは格納できなかったアイテムのようです。(keyが引数のインデックス、valueがアイテム)

アイテムを渡したい時はほぼ大抵多くの場合これを使っておけば良いでしょう。

特定のスロットにアイテムを追加する

「setItem(int,ItemStack)」になります、setと言うからには前のアイテムは上書きされます。

inv.setItem(1, new ItemStack(Material.TNT,64));

要インデックスなので使い勝手はあんまりよくないかも。

特定のアイテムを探す

「all()」、もしくは「first()」で可能です。

どちらも引数はMaterialかItemStackです。

all()の場合は見つかったアイテム全てを含むHashMap(keyが位置、valueがアイテム)

first()の場合は最初に見つかった位置のインデックス(無ければ-1)です。

inv.all(Material.DIAMOND);
inv.first(Material.TNT);

ItemStackを渡す場合の注意点は「contains()」や「remove()」と同じ

アイテムを移す

どうやらそう言うメソッドはないみたいで……

とは言え、今回の目的である「チェストを殴るとシュルカーボックスに」ではアイテムを(位置も含め)移す必要があります。(単純にシュルカーボックスにしただけでは中身が溢れ出る)

と言う訳でここは自力で実装しちゃいましょう。

// チェストのインベントリを取得
Inventory oldInv = chest.getInventory();

// 中身を取り出しておく
HashMap<Integer, ItemStack> items = new HashMap<>();
// 中身を移す
for (int i = 0, size = oldInv.getSize(); i < size; ++i) {
	// アイテム取得
	ItemStack item = oldInv.getItem(i);
	if (item == null) { // アイテムがnull(ない)なら次へ
		continue;
	}

	// 追加
	items.put(i, item);
}
// 古い方の中身を消してアイテムが飛び出さない(増加しない)様にする
oldInv.clear();

// シュルカーボックスのインベントリ
Inventory newInv = shulkerBox.getInventory();

// 予め取り出しておいた中身を入れる
for (Entry<Integer, ItemStack> item : items.entrySet()) {
	// アイテムを入れる
	newInv.setItem(item.getKey(), item.getValue());
}

最後に

なんだかただの「Inventoryの使い方」記事になってしまいましたが、Inventory周りはちょっとクセがあるだけで慣れると簡単です。

0始まりの事さえ忘れなければOKです。

ちなみに、結局使う事のなかった「チェストを殴るとシュルカーボックスにする」サンプルコードはGitHubにあります。

興味があればどうぞ。

 - プログラミング , ,