この記事は"Minecraft Forge 10.13.4.1448+"を前提MODとしています。 |
IInventoryとは
マインクラフトには、"内部にアイテム(ItemStack)を持っているオブジェクト"が色々なところで存在します。これらは「インベントリ」と呼ばれ、チェストのようなTileEntityに限らず、Entity(プレイヤーやロバなど)、アイテム(一部MODで追加されるような、右クリックでインベントリGUIを開けるアイテムなど)にも実装できます。 IInventoryは、これらのインベントリを実装・操作するためのインターフェイスです。
取得したBlockやItemが具体的にどのブロックか調べることなく、IInventoryを実装しているかどうかを判定することで、そのオブジェクトからアイテムを取り出したり、または挿入することが出来ます。
IInventoryは、これを継承して機能を増やしたインターフェイスもあります。
- ISidedInventory
- バニラではカマドなどに実装されています。主にTileEntityなどワールドへの設置物に実装されるもので、操作されている"Side"(面や方角)によって操作できるスロットが異なるようにしたり、アクセス出来るスロットを制限できる機能があります。
例:バニラのカマドは、完成品スロットにアイテムを挿入できないよう制限されています。 - IHopper
- バニラではホッパーに実装されています。機能追加と言うよりは、"ホッパーであるかどうか"の判定に利用されているようです。(ホッパーブロックやホッパーカート)
IInventoryの実装例については別のチュートリアルで解説しますので、ここでは、他のクラスで実装されているIInventoryを操作する手段の説明を記述します。
操作の流れ
- TileEntity、Entity、Itemなどのインスタンスを取得する
- インベントリを実装しているかを確かめるため、IInventoryを実装しているかを判定する
- そのインベントリが利用可能な状態かどうかを判定する
- アイテムを取り出したり、挿入する操作をする
実例
IInventoryの判定
必要な部分でTileEntityやEntityのインスタンスを取得し、instanceofで判定するだけです。
- TileEntity
よく利用されるのは、WorldとXYZ座標が得られるところで、.getTileEntityメソッドを利用してTileEntityを調べる方法ですね。
引数にWorldとXYZ座標を持つメソッド内であれば色々な場所で利用できます。
別のTileEntityからの参照、Block.onBlockActivatedメソッド、Item.onItemUseメソッド等々。
操作する場合、サーバー側で処理を行うよう注意して下さい。
TileEntity tile = world.getTileEntity(x, y, z); if (tile != null && tile instanceof IInventory) { /* インベントリ操作をここで行う */ }
- Entityの場合
EntityでIInventoryを実装しているのは、バニラであればチェスト付きカートなど。
但し、馬(EntityHorse)は持っているインベントリがアクセス出来ないよう隠蔽されているので、馬のチェストは操作できません。
引数にEntityを持つメソッド内で行うのであれば、そのEntityについて調べるだけでOK。例えば、Block.onEntityCollidedWithBlockメソッド内で接触中のEntityを操作したい場合など。
それ以外の場所では、AABB等を使って調べたい場所のEntityを取得する方法もあります。
※この方法の場合、サーバー側とクライアント側で得られるEntityの結果が異なるので、サーバー側のみで行うよう注意して下さい。
if (!world.isRemote){ // 範囲内のEntityを調べるメソッド List<Entity> entities = world.getEntitiesWithinAABB(Entity.class, AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ)); // 範囲内に含まれていた全てのEntityを順に調査する if (entities != null && !entities.isEmpty()) { for (Entity entity : entities) { if (entity != null && entity instanceof IInventory) { /* インベントリ操作部分 */ } } }
- Item
ItemStackを取得できる場所は多く、これが得られればItemStack.getItemメソッドでItemのインスタンスを得られます。
但し、インベントリを操作する場合はTileEntityと同様、サーバー側でのみ操作する方が望ましいです。
(インベントリに限らず多くの処理は基本的にサーバー側で行い、クライアントへは後から処理結果を伝えるのみという方式が多いです。)
if (itemStack != null && itemStack.getItem() != null && itemStack.getItem() instanceof IInventory) { /* インベントリ操作をここで行う */ }
インベントリの操作
IInventoryの種類の判定、利用可能判定、そしてItemStack操作を行います。
以下は、拙作FluidHopperでの一例です。
FluidHopperのTileEntityのアップデート処理での、「真下にあるTileEntityのインベントリにアイテムを挿入する」という部分の実装例。
※関係するメソッド部分のみの抜粋のため、以下のソースのみでは動作しない点に注意して下さい。
private void extractItemFromHopper() { /* 調べているのは、FluidHopperの真下の位置にあるTileEntity */ TileEntity tile = worldObj.getTileEntity(xCoord, yCoord - 1, zCoord); if (tile != null && tile instanceof IInventory) { IInventory inv = (IInventory) tile; /* このcurrentとは、これから真下に挿入される予定の、FluidHopper自身のインベントリのアイテムです。 */ ItemStack current = this.getStackInSlot(1); if (current == null) return; /* 真下がSidedInventoryの場合。この場合はアクセス出来るスロットかどうかを判定し、可能なスロットだけを挿入先スロットの候補とします。 */ if (tile instanceof ISidedInventory) { ISidedInventory sided = (ISidedInventory) tile; /* アクセス可能なスロットの番号のリストを得ます */ int[] accessible = sided.getAccessibleSlotsFromSide(1); ItemStack ret = null; int slot = 0; for (int i : accessible) { ItemStack item = sided.getStackInSlot(i); /* 挿入しようとしているアイテムが、その番号のスロットに挿入可能かの判定を行います。 * falseの場合は挿入処理を止めます。 */ if (!sided.isItemValidForSlot(i, current)) continue; /* スロットが空であるか、挿入しようとしているアイテムとスタックが重ねられる場合。 * ここではアイテム挿入処理をせずに、挿入予定のItemStackとスロット番号だけ記憶させています。 * 単なる作者の趣味なので、必ずしもこのような実装にする必要はないと思われます。 */ if (item == null || this.isItemStackable(new ItemStack(current.getItem(), 1, current.getItemDamage()), item)) { ret = new ItemStack(current.getItem(), 1, current.getItemDamage()); slot = i; break; } } /* 挿入可能なアイテムが無事に取得できたならば、挿入処理をします。 */ if (ret != null) { /* スロットが空であれば、挿入予定のItemStackがそのままSlotにはいります。 */ if (sided.getStackInSlot(slot) == null) { sided.setInventorySlotContents(slot, ret); } else { /* 空で無かった場合は、挿入予定の個数分だけ、移送先Slotのスタック数を増加させます。 */ sided.getStackInSlot(slot).stackSize++; } /* 最後に、挿入完了したあと、FluidHopper側のアイテムは減少させます。 */ this.decrStackSize(1, 1); /* インベントリ内容に変更があったため、変更を更新させる処理を呼びます。 */ inv.markDirty(); this.markDirty(); } } else { /* 以下はSidedInventoryで無かった場合の処理。基本は上記と同様の流れです。 */ ItemStack ret = null; int slot = 0; /* Sidedと異なり、スロットごとにアクセス不可の判定がないので、全スロットを走査します。 */ for (int i = 0; i < inv.getSizeInventory(); i++) { ItemStack item = inv.getStackInSlot(i); if (!inv.isItemValidForSlot(i, current)) continue; if (item == null || this.isItemStackable(new ItemStack(current.getItem(), 1, current.getItemDamage()), item)) { ret = new ItemStack(current.getItem(), 1, current.getItemDamage()); slot = i; break; } } if (ret != null) { if (inv.getStackInSlot(slot) == null) { inv.setInventorySlotContents(slot, ret); } else { inv.getStackInSlot(slot).stackSize++; } this.decrStackSize(1, 1); inv.markDirty(); this.markDirty(); } } } } /* 上記extractItemFromHopper()内で使用しているメソッド。スタック可能なアイテムかどうかの判定です。 */ private static boolean isItemStackable(ItemStack target, ItemStack current) { if (target == null || current == null) return false; if (target.getItem() == current.getItem() && target.getItemDamage() == current.getItemDamage()) { return (current.stackSize + target.stackSize) <= current.getMaxStackSize(); } return false; }