初心者modderの備忘録

マイクラのmodを作りたくて、初めて見たのですが難しくて忘れそうなので自分用の備忘録も兼ねてブログにしようと思います

12日目 木の追加

今回は、木を追加していこうと思います。

木を作るためには、最低限、原木、葉、苗木ブロックを追加する必要があります。
その後、必要に応じて木材やそれらに対応したドア、階段等も追加することになるかもしれません。
とりあえず今回は、原木、葉、苗木のみを追加して木の生成をしたいと思います。

11日目 バイオームの追加

今回はバイオームを追加しようと思います。

まず、Biomeを継承したクラスを作成します。

public class TestBiome extends Biome {

    protected TestBiome() {
        super(new Biome.Builder()
                .surfaceBuilder(SurfaceBuilder.DEFAULT, new SurfaceBuilderConfig(Blocks.END_STONE.getDefaultState(), Blocks.NETHERRACK.getDefaultState(), Blocks.LAVA.getDefaultState()))
                .precipitation(RainType.SNOW)
                .depth(0.0f)
                .scale(0.0f)
                .category(Category.DESERT)
                .downfall(1.0f)
                .temperature(1.2f))
                .waterColor(0xffffff)
                .waterFogColor(0x000000);


        DefaultBiomeFeatures.addLakes(this);

        this.addSpawn(EntityClassification.MONSTER, new SpawnListEntry(EntityType.field_226289_e_, 5, 1, 2));

        setRegistryName(Location.create("test_biome"));
    }
}


コンストラクターで、super()にBiome.Builderを渡します。
newでBuilderのインスタンスを生成してから、各メソッドで設定を行ないます。
わからない部分も結構あるのですが、すべて設定しておかないとエラーがでます。
surfaceBuilderはバイオームを構成するブロックを設定します。第一引数はSurfaceBuilderのタイプ、第二引数にSurfaceBuilderConfigに、表面のブロック (平原バイオームの草ブロック)、表面下のブロック (平原バイオームの土ブロック)、第三引数が水の下のブロックを設定して渡します。

precipitationでは、雨のタイプを設定します。

depthはバイオームの起伏の大きさだと思います。

scaleも大きくすると、バイオームの高さが高くなります。depthとの違いがよくわかりません。
バイオームのサイズではないようです。

categoryも設定する必要がありますが、どのように反映されるのかいまいち分かりません。

downfallは降水量らしいですが、0にしても雨が降っていたりしてよくわかりません。
また草ブロックの色等にも関係しているみたいです。

temperatureはバニラでは0から2くらいの間に設定されていて、この値が0.15未満なら雪が降り、0.15から0.95であれば雨が降り、それ以上高ければ雪も雨も降らないみたいです。

waterColorとwaterFogColorは水の色と霧の色で、16進数のカラーコードで設定します。


またDefaultBiomeFeatures.addXXX(this)で、このバイオームにデフォルトの構造物を追加できます。
addSpawnで、このバイオームに出現するMobを設定します。


registryNameも設定しておきます。



このあたりを設定したら、続いてバイオームの登録を行ないます。
これも、ブロック等と同様に@SubscribeEventで登録するので、どこのクラスでも登録出来ます。
今回は、ModBiomesというクラスを作成しました。

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModBiomes {

    public static final Biome TEST_BIOME = new TestBiome();

    @SubscribeEvent
    public static void registerBiome(final RegistryEvent.Register<Biome> event) {
        event.getRegistry().register(TEST_BIOME);

        BiomeDictionary.addTypes(TEST_BIOME, BiomeDictionary.Type.DRY, BiomeDictionary.Type.HOT);
        BiomeManager.addBiome(BiomeType.WARM, new BiomeEntry(TEST_BIOME, 100));
        BiomeManager.addSpawnBiome(TEST_BIOME);
    }
}


宣言と RegistryEventでの登録はアイテムやブロック等と同様です。
バイオームの場合は、それだけだと自然にスポーンしないのでBiomeDictionaryとBiomeManagerに登録します。

BiomeDictionaryでは、バイオームのタイプを登録します。
おそらく、タイプが近いと、近いタイプのバイオーム同士が隣接して生成されるんだと思います。


BiomeManagerのaddBiomeでBiomeTypeとBiomeEntryを登録します。
ここでのBiomeTypeは、BiomeDictionaryのTypeとは別でもっと大きく分類されたタイプです。
BiomeEntryの第二引数はweightで大きいほど生成されやすくなります。
何回かテストしてみた感覚では、10で普通のバイオームくらい、100にするとそのBiomeTypeではほとんど登録したバイオームしか見かけないような印象でした。


BiomeManagerのaddSpawnBiomeは初期スポーン地点になる可能性のあるバイオームとして登録します。


これらを登録して、新たな世界を作成すれば登録したバイオームが生成されるはずです。

10日目 Tile EntityへのGUIの追加

先日作成したインベントリ付きタイルエンティティにGUIを実装します。

GUIを持たせるためには、Containerを継承したクラスとScreenを継承したクラスが必要になります。

まず、Containerクラスから作成します。

public class TestTileContainer extends Container
{
	public TestTileContainer(int windowId, PlayerInventory playerInv, PacketBuffer extraData)
	{
		this(windowId, playerInv, playerInv.player.world, extraData.readBlockPos());
	}

	protected TestTileContainer(int id, PlayerInventory playerInv, World world, BlockPos pos)
	{
		super(ModTileEntities.TEST_TILE_CONTAINER, id);
		TileEntity te = world.getTileEntity(pos);

		te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
		.ifPresent(h ->
		{
			for(int i = 0; i < 3; ++i)
			{
				for(int j = 0; j < 3; ++j)
				{
					addSlot(new SlotItemHandler(h, j + i * 3, 62 + j * 18, 17 + i * 18));
				}
			}
		});

		for(int k = 0; k < 3; ++k) {
			for(int i1 = 0; i1 < 9; ++i1) {
				this.addSlot(new Slot(playerInv, i1 + k * 9 + 9, 8 + i1 * 18, 84 + k * 18));
			}
		}

		for(int l = 0; l < 9; ++l) {
			this.addSlot(new Slot(playerInv, l, 8 + l * 18, 142));
		}
	}

	@Override
	public boolean canInteractWith(PlayerEntity playerIn)
	{
		return true;
	}

	@Override
	public ItemStack transferStackInSlot(PlayerEntity playerIn, int index)
	{
		ItemStack itemstack = ItemStack.EMPTY;
		Slot slot = this.inventorySlots.get(index);
		if (slot != null && slot.getHasStack()) {
			ItemStack itemstack1 = slot.getStack();
			itemstack = itemstack1.copy();
			if (index == 0) {
				if (!this.mergeItemStack(itemstack1, 1, 37, true)) {
					return ItemStack.EMPTY;
				}

				slot.onSlotChange(itemstack1, itemstack);
			} else if (this.mergeItemStack(itemstack1, 0, 1, false)) { //Forge Fix Shift Clicking in beacons with stacks larger then 1.
				return ItemStack.EMPTY;
			} else if (index >= 1 && index < 28) {
				if (!this.mergeItemStack(itemstack1, 28, 37, false)) {
					return ItemStack.EMPTY;
				}
			} else if (index >= 28 && index < 37) {
				if (!this.mergeItemStack(itemstack1, 1, 28, false)) {
					return ItemStack.EMPTY;
				}
			} else if (!this.mergeItemStack(itemstack1, 1, 37, false)) {
				return ItemStack.EMPTY;
			}

			if (itemstack1.isEmpty()) {
				slot.putStack(ItemStack.EMPTY);
			} else {
				slot.onSlotChanged();
			}

			if (itemstack1.getCount() == itemstack.getCount()) {
				return ItemStack.EMPTY;
			}

			slot.onTake(playerIn, itemstack1);
		}

		return itemstack;
	}

}

まずコンストラクタですが、引数が(int, PlayerInventory, PacketBuffer)のコンストラクタ (3~6行目) を作成しておくと、登録時にコンストラクタ参照で渡せるため楽です。このコンストラクタから実際に処理を行なうコンストラクタ (8~34行目) へ、引数を渡せば良いと思います。
PacketBufferのread~~メソッドを使うことで、呼び出し側の対応するwrite~~の情報を受け取ることが出来ます。
今回はTileEntity側でBlockPosを書き込み、Container側で読み込んでいます。

実際に処理を行なうコンストラクタの中身では、まずタイルエンティティを取得し、続いてタイルエンティティの持つItemStackHandlerをこのコンテナーのスロットとして登録します。
SlotItemHandlerの引数は(ItemHandler, Handlerの番号, コンテナ画像上でのx座標, コンテナ画像上でのy座標) です。
今回はインベントリを9つに増やそうと思うので、それらを3×3で表示しています (13~23行目)。

同様にプレイヤーのインベントリも設定します。ここの処理はバニラのチェスト、かまど、ホッパー等と同様です (24~34行目)。


次にcanInteractWithをtrueにして、transferStackInSlotを実装します。
transferStackInSlotはシフトキーを押しながら、アイテムを転送したりする処理に関する内容で、設定しておかないとシフトキーを押してアイテムを選択するとクラッシュしてしまいます。
バニラと同じ動作で良いならバニラのコンテナーを実装している機能が近いクラスからコピペでいいと思います。



次に、Screenを作成します。

public class TestTileScreen extends ContainerScreen<TestTileContainer>
{
	private static final ResourceLocation BACKGROUND_TEXTURE = new ResourceLocation(TestMod.MODID, "textures/gui/dispenser.png");

	public TestTileScreen(TestTileContainer screenContainer, PlayerInventory inv, ITextComponent titleIn) {
		super(screenContainer, inv, titleIn);
		this.xSize = 176;
		this.ySize = 166;
	}

	@Override
	public void render(final int mouseX, final int mouseY, final float partialTicks) {
		this.renderBackground();
		super.render(mouseX, mouseY, partialTicks);
		this.renderHoveredToolTip(mouseX, mouseY);
	}

	@Override
	protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY)
	{
		super.drawGuiContainerForegroundLayer(mouseX, mouseY);
		this.font.drawString(this.title.getFormattedText(), 8.0f, 5.0f, 0x000000);
		this.font.drawString(this.playerInventory.getDisplayName().getFormattedText(), 8.0f, 72.0f, 0xffffff);
	}

	@Override
	protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY)
	{
		RenderSystem.color4f(1.0f, 1.0f, 1.0f, 1.0f);
		this.minecraft.getTextureManager().bindTexture(BACKGROUND_TEXTURE);
		int x = (this.width - this.xSize) / 2;
		int y = (this.height - this.ySize) / 2;
		this.blit(x, y, 0, 0, this.xSize, this.ySize);
	}
}

まず、3行目でGUIに使用するテクスチャのロケーションを宣言しておきます。
今回はディスペンサーのテクスチャをそのまま使います。

コンストラクタで、pngファイル上の使用する部分のサイズを設定します。

f:id:json_fileman:20200514222656p:plain


他の3つのメソッドはバニラのホッパーからコピペしてきました。
drawStringを無くせば、GUIの文字を消せるし、第一引数にStringを渡して別の文字を表示させることもできます。


drawGuiContainerBackgroundLayerでは設定した画像を表示させます。
bindTextureで画像をセットして、blitで表示させます。
blitの引数は(画面上のx座標, 画面上のy座標, 画像上の開始x座標, 画像上の開始y座標, Δx: 画像上の開始点からx方向にどれだけ幅をとるか, Δy)
になっています。


すこし話が逸れますが、Minecraft.ingameGUIに画像をblitすることでゲーム画面上に画像を貼ることもできます。


f:id:json_fileman:20200514223931p:plain



ContainerとScreenが完成したので、これらを登録し、TileEntityに紐付けます。

まず登録を行ないます。

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModTileEntities
{
	public static final TestTileBlock TEST_TILE_BLOCK = new TestTileBlock(Block.Properties.create(Material.PLANTS), "test_tile");
	public static final TileEntityType<TestTileEntity> TEST_TILE = TileEntityType.Builder.create(TestTileEntity::new, TEST_TILE_BLOCK).build(null);
	public static final ContainerType<TestTileContainer> TEST_TILE_CONTAINER = IForgeContainerType.create(TestTileContainer::new);

	@SubscribeEvent
	public static void registerBlocks(final RegistryEvent.Register<Block> event)
	{
		event.getRegistry().register(TEST_TILE_BLOCK);
	}

	@SubscribeEvent
	public static void registerBlockItems(final RegistryEvent.Register<Item> event)
	{
		event.getRegistry().register(new BlockItem(TEST_TILE_BLOCK, new Item.Properties().group(TestItemGroups.TEST_GROUP)).setRegistryName(TEST_TILE_BLOCK.getRegistryName()));
	}

	@SubscribeEvent
	public static void registerTileEntity(final RegistryEvent.Register<TileEntityType<?>> event)
	{
		TEST_TILE.setRegistryName(TestMod.MODID, "test_tile");
		event.getRegistry().register(TEST_TILE);
	}


	@SubscribeEvent
	public static void registerContainer(final RegistryEvent.Register<ContainerType<?>> event)
	{
		TEST_TILE_CONTAINER.setRegistryName(TestMod.MODID, "test_tile_container");
		event.getRegistry().register(TEST_TILE_CONTAINER);
	}

	@OnlyIn(Dist.CLIENT)
	@SubscribeEvent
	public static void registerScreen(final FMLClientSetupEvent event)
	{
		ScreenManager.registerFactory(TEST_TILE_CONTAINER, TestTileScreen::new);
	}
}

6行目でコンテナをContainerTypeとして宣言します。
IForgeContainerType.createにコンテナのコンストラクタ参照を渡します。
コンテナのコンストラクタに一致するものがない場合は、ラムダ式で記入します。

宣言したコンテナはブロック等と同様に、registryNameを設定し、登録します。

スクリーンは35~40行目で登録しています。スクリーンはクライアント側のみに登録するので、FMLClientSetupEventを呼び出し、
そこでScreenManager.registerFactoryにコンテナータイプとスクリーンのコンストラクタ参照を渡します。


これで登録は完了です。

最後にこれらをTileEntityに紐付け、GUIを開くための機能をBlockに実装します。

public class TestTileEntity extends TileEntity implements INamedContainerProvider
{
	private int count = 0;
	private LazyOptional<ItemStackHandler> itemHandler = LazyOptional.of(() -> new ItemStackHandler(9));

	public TestTileEntity()
	{
		super(ModTileEntities.TEST_TILE);
	}

	@Override
	public CompoundNBT write(CompoundNBT compound)
	{
		compound.putInt("T_count", count);
		itemHandler.ifPresent(h ->
		{
			compound.put("Items", h.serializeNBT() );
		});
		System.out.println("write");
		return super.write(compound);
	}

	@Override
	public void read(CompoundNBT compound)
	{
		System.out.println("read");
		super.read(compound);
		count = compound.getInt("T_count");
		itemHandler.ifPresent(h ->
		{
			if(compound.contains("Items"))
			{
				h.deserializeNBT(compound.getCompound("Items"));
			}
		});
	}

	public void increment()
	{
		count++;
	}

	public int getCount()
	{
		return count;
	}

	@Override
	public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side)
	{
		if(cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
		{
			return itemHandler.cast();
		}
		return super.getCapability(cap, side);
	}

	@Override
	public Container createMenu(int windowId, PlayerInventory inv, PlayerEntity player)
	{
		return new TestTileContainer(windowId, inv, world, pos);
	}

	@Override
	public ITextComponent getDisplayName()
	{
		return new TranslationTextComponent("Test tile entity with GUI!");
	}
}

コンテナと紐付けるためには INamedContainerProviderをimplements します。
また、4行目でItemStackHandlerの数を9に変更しています。

INamedContainerProviderの抽象メソッド、createMenuとgetDisplayNameをそれぞれ58~62行目、64~68行目で定義しています。

createMenuでは実際に処理をコンストラクタの引数を渡してnewします。
getDisplayNameではTranslationTextComponentに任意の文字列を渡すと、それが表示される名前になります。


最後にブロックを右クリックの挙動にGUIを開く機能を追加します。

@Override
	public ActionResultType func_225533_a_(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult result)
	{
		TileEntity te = world.getTileEntity(pos);
		if(te instanceof TestTileEntity && !world.isRemote)
		{
			((TestTileEntity) te).increment();

			te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).<ItemStackHandler>cast()
			.ifPresent(h ->
			{
				h.setStackInSlot(0, new ItemStack(Items.DIAMOND, ((TestTileEntity) te).getCount()));
			}
					);

			NetworkHooks.openGui((ServerPlayerEntity) player, (TestTileEntity) te, b -> b.writeBlockPos(pos));
			System.out.println(((TestTileEntity) te).getCount() +", "+pos);
		}
		return ActionResultType.SUCCESS;
	}


NetworkHooks.openGuiでGUIが開きます。
引数はそれぞれ、(サーバープレイヤー、GUIと紐付けたこのブロックのタイルエンティティ、PacketBuffer型のConsumer)です。
ここのConsumerの処理でwriteしたものを、コンテナーのコンストラクタのPacketBufferから読み込みます。
今回は、BlockPosをwriteしています。
以前に、MobEntityと紐付いたGUIコンテナを作成したときはentityIdをint型としてwriteして渡しました。


ついでに、ItemStackHandlerの数を9個にしたので、ブロックが壊されたときに9個分のインベントリをドロップするように修正します。

	if(world.getTileEntity(pos) instanceof TestTileEntity && !world.isRemote)
		{
			TestTileEntity te = (TestTileEntity) world.getTileEntity(pos);

			te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).ifPresent(h ->
			{
				for(int i=0; i < h.getSlots(); i++)
				{
					world.addEntity(new ItemEntity(world, pos.getX(), pos.getY(), pos.getZ(), h.getStackInSlot(i)));
					System.out.println(h.getStackInSlot(i).getItem()+", "+h.getStackInSlot(i).getCount());
				}

			});
		}
		super.onReplaced(state, world, pos, newState, isMoving);
	}


これで、GUIが開けるようになったので、実際に起動します。


f:id:json_fileman:20200514231027p:plain

GUIが開けます。

また破壊されたときにも、ちゃんとすべてのアイテムをドロップしています。

f:id:json_fileman:20200514231125p:plain


今回は以上です。

9日目 Tile Entityへのインベントリの追加

先日作成したTileEntityにインベントリを追加しようと思います。
インベントリ自体はTileEntityクラスのみ編集することで追加できます。
今回はそれに加えてインベントリにアクセスするための機能をブロッククラスに追加していきます。


インベントリを実装するためのLazyOptionalをTileEntityクラスに追加し、それに合わせてコードを追加していきます。

public class TestTileEntity extends TileEntity
{
	private int count = 0;
	private LazyOptional<ItemStackHandler> itemHandler = LazyOptional.of(() -> new ItemStackHandler(1));

	public TestTileEntity()
	{
		super(ModTileEntities.TEST_TILE);
	}

	@Override
	public CompoundNBT write(CompoundNBT compound)
	{
		compound.putInt("T_count", count);
		itemHandler.ifPresent(h ->
		{
			compound.put("Items", h.serializeNBT() );
		});
		System.out.println("write");
		return super.write(compound);
	}

	@Override
	public void read(CompoundNBT compound)
	{
		System.out.println("read");
		super.read(compound);
		count = compound.getInt("T_count");
		itemHandler.ifPresent(h ->
		{
			if(compound.contains("Items"))
			{
				h.deserializeNBT(compound.getCompound("Items"));
			}
		});
	}

	public void increment()
	{
		count++;
	}

	public int getCount()
	{
		return count;
	}

	@Override
	public <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side)
	{
		if(cap == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY)
		{
			return itemHandler.cast();
		}
		return super.getCapability(cap, side);
	}
}

4行目でインベントリを持たせるためのItemStackHandlerを作成しています。今回は1スロットのみ追加しています。ItemStackHandlerのコンストラクターの引数でスロット数を指定できます。
ItemHandlerを他のクラスに渡すためのgetCapabilityメソッドがLazyOptionalなので、宣言はItemStackHandler型のLazyOptionalで行ないます。

続いて、ItemStackHandlerを保存するために、NBTcompoundのwrtie (15行目~) およびread (29行目~) に書き加えます。
ItemStackHandlerを丸ごとシリアライズしてNBTに書き込み、読み込むときはNBTからItemStackHandlerにデシリアライズします。
ただし読み込みに関して、初回読み込み時にはNBTがnullになってしまうのでnullチェックを入れています。

最後に、このクラスのItemStackHandlerを他のクラスに渡すためのgetCapabilityメソッドをオーバーライドします (49行目~)。
引数のcapabilityがITEM_HANDLER_CAPABILITYである場合には、このクラスのLazyOptionalをIItemHandlerにキャストして渡し、それ以外の場合ではスーパークラスの同メソッドを呼び出します。

これでタイルエンティティへインベントリが追加されました。



続いて、Blockクラスの編集を行ないます。

public class TestTileBlock extends Block
{
	public TestTileBlock(Properties properties, String name)
	{
		super(properties);
		setRegistryName(TestMod.MODID, name);
	}

	@Override
	public boolean hasTileEntity(BlockState state)
	{
		return true;
	}

	@Override
	public TileEntity createTileEntity(BlockState state, IBlockReader world)
	{
		return ModTileEntities.TEST_TILE.create();
	}

	@Override
	public ActionResultType func_225533_a_(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult result)
	{
		TileEntity te = world.getTileEntity(pos);
		if(te instanceof TestTileEntity && !world.isRemote)
		{
			((TestTileEntity) te).increment();

			te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).<ItemStackHandler>cast()
			.ifPresent(h ->
			{
				h.setStackInSlot(0, new ItemStack(Items.DIAMOND, ((TestTileEntity) te).getCount()));
			}
					);

			System.out.println(((TestTileEntity) te).getCount() +", "+pos);
		}
		return ActionResultType.SUCCESS;
	}

	@SuppressWarnings("deprecation")
	@Override
	public void onReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean isMoving)
	{
		if(world.getTileEntity(pos) instanceof TestTileEntity && !world.isRemote)
		{
			TestTileEntity te = (TestTileEntity) world.getTileEntity(pos);

			te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY).ifPresent(h ->
			{
				world.addEntity(new ItemEntity(world, pos.getX(), pos.getY(), pos.getZ(), h.getStackInSlot(0)));
			});
		}
		super.onReplaced(state, world, pos, newState, isMoving);
	}
}

まず、ブロックを右クリックしたときの挙動として、29~34行目の処理を追加しました。
getCapabilityでIItemHandler型としてTestTileEntityのItemStackHandlerを受け取り、それをItemStackHandlerにキャストします (IItemHandlerにはsetStackInSlotメソッドが存在しないため)。
そして、setStackInSlotを使ってcountの数だけダイアモンドをスロットにセットします。



次に、このブロックが壊されたときの挙動として、49~52行目の処理を追加しました。
先程と同様に、getCapabilityでIItemHandlerを受け取ります。
getStackInSlotはIItenHandlerにも実装されているのでキャストなしで使います。
インベントリ内のアイテムスタックをアイテムエンティティとして生成し、コンソールにアイテムと個数を表示するようにしました。

これで実際に動かしてみます。
12回クリックした後、一度セーブします。

f:id:json_fileman:20200513233208p:plain


その後、再びログインしブロックを破壊します。

f:id:json_fileman:20200513233543p:plain


すると、12個のダイヤモンドがドロップし、コンソールにもそのように表示されています。
ちゃんとインベントリの内容が保存されています。


次回は、このインベントリをもつタイルエンティティにGUIを実装したいと思います。

8日目 Tile Entityの追加 (1.15)

タイルエンティティの追加の1.15バージョンを書いていきます。
タイルエンティティは情報を持ったブロックで、チェストやかまど等のように多くの場合にインベントリとGUIを持ちます。

今回も最終的にはインベントリとGUIを持ったタイルエンティティを追加しますが、一度に書くと長くなるので何回かに分けて書こうと思います。
初回は、GUIもインベントリも持たない基本的なタイルエンティティの追加を行ないます。
機能は右クリックされた回数を記録し、破壊されたときにその数だけカメのタマゴをドロップさせます。

タイルエンティティを作成するためには、タイルエンティティ自体のクラスとタイルエンティティと紐付けるブロックのクラスが必要になります。
今回は、TestTileEntityとTestTileBlockを作成します。

まず、TestTileEntityから。

public class TestTileEntity extends TileEntity
{
	private int count = 0;

	public TestTileEntity()
	{
		super(ModTileEntities.TEST_TILE);
	}

	@Override
	public CompoundNBT write(CompoundNBT compound)
	{
		compound.putInt("T_count", count);
		System.out.println("write");
		return super.write(compound);
	}

	@Override
	public void read(CompoundNBT compound)
	{
		System.out.println("read");
		super.read(compound);
		count = compound.getInt("T_count");
	}

	public void increment()
	{
		count++;
	}

	public int getCount()
	{
		return count;
	}
}


回数を記録するためのcountと、カウントアップのためのincrementメソッド、回数を得るためのgetterを持ちます。
writeメソッドとreadメソッドでは、それぞれnbtに回数を記録、またはnbtから回数を読み込んでいます。
これによって各ブロックに情報を持たせることができ、セーブした際に情報もセーブされるようになります。



続いて、TestTileBlockです。

public class TestTileBlock extends Block
{
	public TestTileBlock(Properties properties, String name)
	{
		super(properties);
		setRegistryName(TestMod.MODID, name);
	}

	@Override
	public boolean hasTileEntity(BlockState state)
	{
		return true;
	}

	@Override
	public TileEntity createTileEntity(BlockState state, IBlockReader world)
	{
		return ModTileEntities.TEST_TILE.create();
	}

	@Override
	public ActionResultType func_225533_a_(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult result)
	{
		TileEntity te = world.getTileEntity(pos);
		if(te instanceof TestTileEntity && !world.isRemote)
		{
			((TestTileEntity) te).increment();
			System.out.println(((TestTileEntity) te).getCount() +", "+pos);
		}
		return ActionResultType.SUCCESS;
	}

	@SuppressWarnings("deprecation")
	@Override
	public void onReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean isMoving)
	{
		if(world.getTileEntity(pos) instanceof TestTileEntity && !world.isRemote)
		{
			TestTileEntity te = (TestTileEntity) world.getTileEntity(pos);

			world.addEntity(new ItemEntity(world, pos.getX(), pos.getY(), pos.getZ(), new ItemStack(Items.TURTLE_EGG, te.getCount())));
		}

		super.onReplaced(state, world, pos, newState, isMoving);
	}
}


hasTileEntity、createTileEntityでこのブロックをタイルエンティティを紐付けるます。
登録用のクラス (後述) からTileEntityTypeを指定して、create()でタイルエンティティが生成されます。

func_225533_a_が右クリックされたときの挙動です。
onReplacedでタイルエンティティが壊されたり、置き換えられたときの挙動を設定します。
スーパークラスで、壊されたときのタイルエンティティの除去を行なっているので、スーパークラスのメソッドも呼んでいます。


最後にブロックとタイルエンティティを登録します。
EventBusSubscriberをつけたどこのクラスでもいいのですが、今回はModTileEntitiesというクラスで登録を行ないました。

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModTileEntities
{
	public static final TestTileBlock TEST_TILE_BLOCK = new TestTileBlock(Block.Properties.create(Material.PLANTS), "test_tile");
	public static final TileEntityType<TestTileEntity> TEST_TILE = TileEntityType.Builder.create(TestTileEntity::new, TEST_TILE_BLOCK).build(null);

	@SubscribeEvent
	public static void registerBlocks(final RegistryEvent.Register<Block> event)
	{
		event.getRegistry().register(TEST_TILE_BLOCK);
	}

	@SubscribeEvent
	public static void registerBlockItems(final RegistryEvent.Register<Item> event)
	{
		event.getRegistry().register(new BlockItem(TEST_TILE_BLOCK, new Item.Properties().group(TestItemGroups.TEST_GROUP)).setRegistryName(TEST_TILE_BLOCK.getRegistryName()));
	}

	@SubscribeEvent
	public static void registerTileEntity(final RegistryEvent.Register<TileEntityType<?>> event)
	{
		TEST_TILE.setRegistryName(TestMod.MODID, "test_tile");
		event.getRegistry().register(TEST_TILE);
	}
}

ブロックの登録は通常通りです。
タイルエンティティは、TileEntityTypeとして登録します。
Builder.createの引数に(TileEntityのクラスのコンストラクタ参照, ブロックのインスタンス) を渡します。
第一引数がSupplierなので、目的のTileEntityクラスに引数なしのコンストラクタがない場合には、第一引数をラムダ式で記述する必要があります。



無事に登録出来ていれば、画像のように動作します。

f:id:json_fileman:20200512215611p:plain

各座標に置いたブロックが、それぞれ固有の情報を持っています。



f:id:json_fileman:20200512215709p:plain

また破壊するとカウントの数分のタマゴをドロップします。


テクスチャ等は今回指定していませんが、ブロックのテクスチャと同様に設定できます。


次回は、このタイルエンティティにインベントリを追加します。

7日目 nbtデータを使った構造物の追加

今回は、nbtデータを使って構造物を追加します。
基本的なコードは通常の構造物の追加とあまり変わりません。

以前の記事参照
2日目 構造物の生成 - 初心者modderの備忘録

二つに分けて書いていきます。

①構造物の保存
Javaの記述

①構造物の保存
まず、構造物を保存します。
IDEのrunClienetの実行などからマイクラを開いてクリエイティブモードで世界を生成します (バージョンが合っていれば通常のラウンチャーからの起動でも大丈夫です)。
余計なものが入るのを避けたい場合はスーパーフラットワールドが良いでしょう。

生成したら、ストラクチャーブロックをコマンドで入手します。
/give @p structure_block

f:id:json_fileman:20200423233425p:plain




次に、保存したい構造物を建築します。
今回は、レンガブロックの上に適当に木を生やしたものを作成しました。

f:id:json_fileman:20200423233542p:plain

ストラクチャーブロックの範囲に収まる構造物にする必要があります。
もし、より大きな構造物を追加したい場合は、少し複雑ですが後述の方法で追加できます。

建築できたら、構造物を保存します。
2つのストラクチャーブロックを構造物が収まる直方体の空間の対角となる頂点にそれぞれ設置します。

f:id:json_fileman:20200423234113p:plain

設置したストラクチャーブロックを右クリックで開き、左下のDataと表示されているボタンを3回クリックしてCornerモードにします。
上部の空欄に任意を名前を入力してDoneをクリックします。

f:id:json_fileman:20200423234508p:plain


同様に、対角においたもう一つのストラクチャーブロックに対しても、Cornerモードで名前をつけます。
このとき反対側においたストラクチャーブロックと同じ名前をつけます。



続いて、新たにストラクチャーブロックを設置し、開いてDataを1回クリックしてSaveモードにします。
上部の空欄に先程つけた名前を正確に入力し、DETECTをクリックします。

f:id:json_fileman:20200423234814p:plain


ちゃんと検知されると下図のようにストラクチャーブロックで囲んだ範囲が枠線で表示されます。

f:id:json_fileman:20200423234953p:plain

この状態で、もう一度3個目においたストラクチャーブロックをクリックし、大文字のSAVEをクリックします。


f:id:json_fileman:20200423235456p:plain


うまくセーブされていれば、
\forge-1.15.2-31.1.0-mdk\run\saves\New World\generated\minecraft\structures
に名前をつけた構造物のnbtファイルが保存されています (Forgeで起動した場合。World名はそのとき使ったWorld名)。


f:id:json_fileman:20200424000659p:plain




Javaの記述
構造物が保存出来たら続いて、Javaでコードを書いていきます。

public class TestStructure extends Feature<NoFeatureConfig>
{
	private final PlacementSettings SETTING = new PlacementSettings().setMirror(Mirror.NONE).setRotation(Rotation.NONE).setIgnoreEntities(true);

	public TestStructure(Function<Dynamic<?>, ? extends NoFeatureConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public boolean place(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generator, Random rand, BlockPos pos, NoFeatureConfig config)
	{
		if(rand.nextInt(1000)!=0)
		{
			return false;
		}else
		{
			TemplateManager manager = worldIn.getWorld().getServer().getWorld(DimensionType.OVERWORLD).getStructureTemplateManager();
			Template template = manager.getTemplate(new ResourceLocation(TestMod.MODID, "test"));

			template.addBlocksToWorld(worldIn, pos, SETTING);

			return true;
		}
	}
}

Featureクラスを継承した、TestStructureクラスを作成しました。
placeメソッド内で引数のIWorldから辿って、TemplateManagerを取得します。
取得したTemplateManagerを使ってTemplateのファイル場所を設定して渡します。
先程保存したnbtファイルをコピーして、
resources/data/MODID/structures
に保存します。

その後、TemplateのaddBlocksToWorldメソッドを使って生成させます。
第三引数は設置の設定で、3行目で宣言しています。
設置するときに回転させるか、反転させるかなどを設定したものを渡します。


続いて、構造物の登録を行ないます。

@Mod.EventBusSubscriber(bus=Mod.EventBusSubscriber.Bus.MOD)
public class ModStructures
{
	public static final Feature<NoFeatureConfig> TEST_STRUCTURE = new TestStructure(NoFeatureConfig::deserialize);

	@SubscribeEvent
	public static void registerStructures(final RegistryEvent.Register<Feature<?>> event)
	{
		TEST_STRUCTURE.setRegistryName("structure_test");
		event.getRegistry().registerAll(TEST_STRUCTURE);

		addStructure();
	}

	private static void addStructure()
	{
		ForgeRegistries.BIOMES.forEach(biome ->
		{
			biome.addFeature(Decoration.SURFACE_STRUCTURES,
					TEST_STRUCTURE
					.func_225566_b_(IFeatureConfig.NO_FEATURE_CONFIG)
					.func_227228_a_(TestFrequency.TEST_FREQ
					.func_227446_a_(new NoPlacementConfig())));
		});
	}
}


この辺りは、アイテムやブロックの登録と同様に、@SubscribeEventをつけたメソッド内で登録を行ないます。
今回は、登録用のクラスとして ModStructuresというクラスを作成しました。

また構造物は、登録しただけでは自然に生成されないので各バイオームにaddFeatureメソッドを用いて自然生成を登録します。
今回は登録している構造物が1つですが、構造物が多くなるとコードが長くなるので、addStructureメソッド内で処理を記述し、それを@SubscribeEventn内で呼び出すようにしています。

addFeatureの中の.func_227228_a_にはPlacementを渡します。PlacementではFeatureをどのように生成させるかが記述されています。バニラのConfiguredPlacementを使いたい時は、Placementクラスで宣言されているものを呼び出して使うことも出来ます (上記の過去記事参照)。

今回はテスト用で、シンプルな設置がしたかったので自作のPlacementを作成しました。

public class TestFrequency extends Placement<NoPlacementConfig>
{
	public static final Placement<NoPlacementConfig> TEST_FREQ = new TestFrequency(NoPlacementConfig::deserialize);

	public TestFrequency(Function<Dynamic<?>, ? extends NoPlacementConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public Stream<BlockPos> getPositions(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generatorIn,
			Random random, NoPlacementConfig configIn, BlockPos pos)
	{
		int x = pos.getX();
		int z = pos.getZ();
		int y = worldIn.getHeight(Heightmap.Type.WORLD_SURFACE, x, z);

		return Stream.of(new BlockPos(x, y, z));
	}
}

Placementは登録しないまま使うので、自分のクラスの中で宣言を行ないました。Placementsクラス等を作成して、そこでまとめて宣言してもいいと思います。



ここまで行なって起動すると、


f:id:json_fileman:20200423233720p:plain


図のように、登録した構造物が地表に生成されています。


②大きな構造物の登録

バニラの方法とは違うので、あまり良い方法ではないかもしれませんが一応一つの方法として書いておきます。

例として、6チャンク×6チャンクの大きさの構造物を生成するとします。


f:id:json_fileman:20200424005116p:plain



昔のゲームのマップチップのような考え方で登録します。
なので、まず1つのチップの大きさを何チャンク×何チャンクにするかを決めます。
例では2×2チャンクを1チップとしています。


f:id:json_fileman:20200424005504p:plain


さらに1つの構造物が入る範囲をregionとして、
1regionが何チップ×何チップかを決めます。
例では3×3チップを1regionとしました。



f:id:json_fileman:20200424005859p:plain




そして、worldのseed値、regionのX座標、Z座標を使うことで、1region毎に、固有の乱数を与えて、生成判定やマップチップの配置を行ないます。
こうすることで、1reagionがチャンクの読み込み範囲超えるような大きな構造物であってもクラッシュせず、次にチャンクが読み込まれるタイミングで生成されます。


かなり雑ですが、実際の実施例とコードを載せておきます。
実例では2チャンク×5チップの10チャンク四方からなる構造物を作成しています。
チップ毎につながりを持たせれば、ネザー要塞や森の洋館みたいな構造物も作れるのではないかと考えています。


f:id:json_fileman:20200424010755p:plain


今回はこれで終わります。




ExampleStructure.class

public class ExampleStructure extends Feature<NoFeatureConfig>
{

	private static final int sizeX = 5;
	private static final int sizeZ = 5;

	public static final int chipX = 2;
	public static final int chipZ = 2;

	private static final int region_sizeX = sizeX * chipX;
	private static final int region_sizeZ = sizeZ * chipZ;

	private boolean isLoaded = false;
	private TemplateManager manager;


	private final PlacementSettings SETTING = new PlacementSettings().setMirror(Mirror.NONE).setRotation(Rotation.NONE).setIgnoreEntities(true);

	public ExampleStructure(Function<Dynamic<?>, ? extends NoFeatureConfig> configFactoryIn)
	{
		super(configFactoryIn);
	}

	@Override
	public boolean place(IWorld worldIn, ChunkGenerator<? extends GenerationSettings> generator, Random rand, BlockPos pos, NoFeatureConfig config)
	{
		System.out.println("regionX ="+ pos.getX() / (sizeX*16) + "posX is" + pos.getX());
		System.out.println("regionZ ="+ pos.getZ() / (sizeZ*16) + "posZ is" + pos.getZ());
		System.out.println("isPlace ="+ isPlace(worldIn.getSeed(), pos.getX(), pos.getZ(), rand));

		if(!isLoaded)
		{
			manager = worldIn.getWorld().getServer().getWorld(DimensionType.OVERWORLD).getStructureTemplateManager();

			Arrays.asList(mapChip.values())
			.forEach(map -> map.setTemplate(manager));

			isLoaded = true;
		}


		if(!isPlace(worldIn.getSeed(), pos.getX(), pos.getZ(), rand))
			return false;
		else
		{
			generate(worldIn, rand, pos, manager);
			return true;
		}
	}

	private boolean isPlace(long seed, int posX, int posZ, Random rand)
	{
		int regionX = posX / (region_sizeX*16);
		int regionZ = posZ / (region_sizeZ*16);

		if(regionX == 0 && regionZ == 0)
		{
			return false;
		}else if(regionX == 0 && regionZ == 1)
		{
			rand.setSeed(seed+regionZ);
		}else if(regionX == 0)
		{
			rand.setSeed(seed*regionZ);
		}else if(regionZ == 0)
		{
			rand.setSeed(seed*regionX);
		}else
		{
			rand.setSeed(seed*regionX);
			rand.setSeed(rand.nextLong()*regionZ);
		}

		return rand.nextInt(5) == 0;
	}

	private void generate(IWorld world, Random rand, BlockPos pos, TemplateManager manager)
	{
		int x = pos.getX() >= 0? (pos.getX() / 16) % region_sizeX : (pos.getX() / 16) % region_sizeX + region_sizeX - 1;
		int z = pos.getZ() >= 0? (pos.getZ() / 16) % region_sizeZ : (pos.getZ() / 16) % region_sizeZ + region_sizeZ - 1;

		long seed = world.getSeed();

		int regionX = pos.getX() / (region_sizeX*16);
		int regionZ = pos.getZ() / (region_sizeZ*16);

		if(regionX == 0 && regionZ == 0)
		{

		}else if(regionX == 0 && regionZ == 1)
		{
			rand.setSeed(seed+regionZ);
		}else if(regionX == 0)
		{
			rand.setSeed(seed*regionZ);
		}else if(regionZ == 0)
		{
			rand.setSeed(seed*regionX);
		}else
		{
			rand.setSeed(seed*regionX);
			rand.setSeed(rand.nextLong()*regionZ);
		}

		mapChip map[][] = new mapChip[sizeX][sizeZ];

		for(int i=0; i < sizeX; i++)
		{
			for(int j=0; j < sizeZ; j++)
			{
				map[i][j] = setMap(i, j, rand, map);
			}
		}

		map[x/chipX][z/chipZ].template[x%chipX][z%chipZ].addBlocksToWorld(world, pos, SETTING);

		System.out.println(map[x/chipX][z/chipZ].toString());


		System.out.println(x/chipX);

	}

	private mapChip setMap(int i, int j, Random rand, mapChip[][] map)
	{
		mapChip chip;
		int Xmax = sizeX -1;
		int Zmax = sizeZ -1;

		if((i == 0 && j == 0) || (i == Xmax && j == 0) || (i == 0 && j == Zmax) || (i == Xmax && j == Zmax))
		{
			chip = mapChip.MAP_A;
		}else if(j == 0 || j == Zmax)
		{
			chip = rand.nextBoolean()? mapChip.MAP_B : mapChip.MAP_C;
		}else if(i == 0 || i == Xmax)
		{
			chip = rand.nextBoolean()? mapChip.MAP_D : mapChip.MAP_E;
		}else
		{
			if(map[i][j-1].canJointVir && map[i-1][j].canJointHor)
			{
				chip = mapChip.MAP_F;
			}else
			{
				chip = rand.nextBoolean()? mapChip.MAP_G : mapChip.MAP_H;
			}
		}

		return chip;

	}

	private enum mapChip
	{
		MAP_A(true, true, "map_a"),
		MAP_B(false,true, "map_b"),
		MAP_C(true, true, "map_c"),
		MAP_D(true, false, "map_d"),
		MAP_E(true, true, "map_e"),
		MAP_F(true, true, "map_f"),
		MAP_G(true, true, "map_g"),
		MAP_H(true, false, "map_h");

		private final boolean canJointVir;
		private  final boolean canJointHor;
		private final String name;
		private Template[][] template = new Template[chipX][chipZ];

		private mapChip(boolean Virtical, boolean Horazontal, String name)
		{
			this.canJointVir = Virtical;
			this.canJointHor = Horazontal;
			this.name = name;
		}

		private void setTemplate(TemplateManager manager)
		{

			for(int i=0; i < chipX; i++)
			{
				for(int j=0; j < chipZ; j++)
				{
					this.template[i][j] = manager.getTemplate(new ResourceLocation(TestMod.MODID, name+code(i,j)));
				}
			}
		}

		private String code(int i, int j)
		{
			if(i == 0 && j == 0)
			{
				return "a";
			}else if(i == 1 && j == 0)
			{
				return "b";
			}else if(i == 0 && j == 1)
			{
				return "c";
			}else
			{
				return "d";
			}
		}
	}

}

よく使うサイト

MC Assets - Browser for Minecraft Asset Files

バニラのアセットやテクスチャーを見るのに使用。


MCPBot - Exports

1.14と1.15の難読化マップをcsv形式でアップロードしてくれている。
func_xxxxx_xとかfield_xxxxx_xとかの元の名前等を調べることが出来る。


TurtyWurty - YouTube

1.15も1.12も動画の本数や内容もかなり豊富。非常に丁寧に進めてくれるのでわかりやすい。


Mr. Pineapple - YouTube

1.15のmodding動画を多数あげている。1つ1つの動画を比較的簡潔にまとめてくれているので見やすい。


McJty Modding

海外の人のサイト。項目がたくさんあって、youtubeで動画もあげているので英語が分からなくても、理解しやすい。


Harry Talks - YouTube

こちらも海外の人。youtubeいくつか動画をあげている。1.12の動画はかなり豊富で結構マニアックなmoddingも。