ChatGPTにMinecraftのMODを作ってもらう(後編)

ChatGPTにMODを作ってもらおうと思います。[前編](https://qiita.com/zawanume/items/506976a4ba97415b77ac)はChatGPTに任せきりにしてめちゃめちゃだったので、後編では人力で手直ししていこうと思います。結論から申し上げますと動くようにはなりましたが工業要素はゼロです。

# IronGeneratorTileEntityの実装
ChatGPTは`IronGeneratorTileEntity`クラスを`IronGenerator`クラスの内部クラスとして実装してくれました。そのままでも支障は特にありません。
Forgeを導入した開発環境ではデコンパイルし難読化解除した（確か）Minecraftのコードを読むことができます。Minecraftの内部実装では`TileEntity`の子クラスや孫クラスは内部クラスとして実装されてはいませんでした。何となく統一した方が良いような気がしたので、内部クラスとして実装するのはやめることにします。

```java:IronGeneratorTileEntity.java
public class IronGeneratorTileEntity extends TileEntityLockable implements ISidedInventory, ITickable {
    private int fuelAmount = 0;
    private int electricityAmount = 0;
    private int maxFuelAmount = 1000;
    private int maxElectricityAmount = 10000;
    private String ironGeneratorCustomName;
    private ItemStack[] ironGeneratorItemStacks = new ItemStack[3];
    private int ironGeneratorBurnTime;
    private int currentItemBurnTime;
    private int cookTime;
    private int totalCookTime;

    private final int[] SLOTS_TOP = new int[] {0};
    private final int[] SLOTS_BOTTOM = new int[] {2, 1};
    private final int[] SLOTS_SIDES = new int[] {1};

    public void update() {
        if (!worldObj.isRemote) {
            if (fuelAmount > 0) {
                fuelAmount--;
                electricityAmount += 100;
                if (electricityAmount > maxElectricityAmount) {
                    electricityAmount = maxElectricityAmount;
                }
            }
        }
    }

    public int getFuelAmount() {
        return fuelAmount;
    }

    public int getElectricityAmount() {
        return electricityAmount;
    }

    public int getMaxFuelAmount() {
        return maxFuelAmount;
    }

    public int getMaxElectricityAmount() {
        return maxElectricityAmount;
    }

    public boolean isBurning() {
        return fuelAmount > 0;
    }

    public boolean isFull() {
        return electricityAmount >= maxElectricityAmount;
    }

    public void setFuelAmount(int fuelAmount) {
        this.fuelAmount = fuelAmount;
    }

    public void setElectricityAmount(int electricityAmount) {
        this.electricityAmount = electricityAmount;
    }

    public Container createContainer(InventoryPlayer playerInventoty, EntityPlayer playerIn)
    {
        return  new ContainerIronGenerator(playerInventoty, this);
    }

    public String getGuiID() {
        return "tile:examplemod_iron_generator";
    }

    public String getName()
    {
        return this.hasCustomName() ? this.ironGeneratorCustomName : "container.iron_generator";
    }

    public boolean hasCustomName()
    {
        return this.ironGeneratorCustomName != null && !this.ironGeneratorCustomName.isEmpty();
    }

    public int getSizeInventory()
    {
        return this.ironGeneratorItemStacks.length;
    }

    @Nullable
    public ItemStack getStackInSlot(int index)
    {
        return this.ironGeneratorItemStacks[index];
    }

    @Nullable
    public ItemStack decrStackSize(int index, int count)
    {
        return ItemStackHelper.getAndSplit(this.ironGeneratorItemStacks, index, count);
    }

    @Nullable
    public ItemStack removeStackFromSlot(int index)
    {
        return ItemStackHelper.getAndRemove(this.ironGeneratorItemStacks, index);
    }

    public void setInventorySlotContents(int index, @Nullable ItemStack stack)
    {
        boolean flag = stack != null && stack.isItemEqual(this.ironGeneratorItemStacks[index]) && ItemStack.areItemStackTagsEqual(stack, this.ironGeneratorItemStacks[index]);
        this.ironGeneratorItemStacks[index] = stack;

        if (stack != null && stack.stackSize > this.getInventoryStackLimit())
        {
            stack.stackSize = this.getInventoryStackLimit();
        }
    }

    public int getInventoryStackLimit()
    {
        return 64;
    }

    public boolean isUseableByPlayer(EntityPlayer player)
    {
        return this.worldObj.getTileEntity(this.pos) != this ? false : player.getDistanceSq((double)this.pos.getX() + 0.5D, (double)this.pos.getY() + 0.5D, (double)this.pos.getZ() + 0.5D) <= 64.0D;
    }

    public void openInventory(EntityPlayer player)
    {
    }

    public void closeInventory(EntityPlayer player)
    {
    }

    public boolean isItemValidForSlot(int index, ItemStack stack)
    {
        if (index == 2)
        {
            return false;
        }
        else if (index != 1)
        {
            return true;
        }
        else
        {
            ItemStack itemstack = this.ironGeneratorItemStacks[1];
            return isItemFuel(stack) || SlotFurnaceFuel.isBucket(stack) && (itemstack == null || itemstack.getItem() != Items.BUCKET);
        }
    }

    public int getField(int id) {
        switch (id) {
            case 0:
                return this.ironGeneratorBurnTime;
            case 1:
                return this.currentItemBurnTime;
            case 2:
                return this.cookTime;
            case 3:
                return this.totalCookTime;
            default:
                return 0;
        }
    }

    public void setField(int id, int value)
    {
        switch (id)
        {
            case 0:
                this.ironGeneratorBurnTime = value;
                break;
            case 1:
                this.currentItemBurnTime = value;
                break;
            case 2:
                this.cookTime = value;
                break;
            case 3:
                this.totalCookTime = value;
        }
    }

    public int getFieldCount()
    {
        return 4;
    }

    public void clear()
    {
        for (int i = 0; i < this.ironGeneratorItemStacks.length; ++i)
        {
            this.ironGeneratorItemStacks[i] = null;
        }
    }

    public int[] getSlotsForFace(EnumFacing side)
    {
        return side == EnumFacing.DOWN ? SLOTS_BOTTOM : (side == EnumFacing.UP ? SLOTS_TOP : SLOTS_SIDES);
    }

    public boolean canInsertItem(int index, ItemStack itemStackIn, EnumFacing direction)
    {
        return this.isItemValidForSlot(index, itemStackIn);
    }

    public boolean canExtractItem(int index, ItemStack stack, EnumFacing direction)
    {
        if (direction == EnumFacing.DOWN && index == 1)
        {
            Item item = stack.getItem();

            if (item != Items.WATER_BUCKET && item != Items.BUCKET)
            {
                return false;
            }
        }

        return true;
    }
}
```

かまどについての実装が書かれている`TileEntityFurnace`を参考に実装していきました。「このinterfaceをimplementsするならこのメソッドをちゃんと定義しろ」などといろいろ怒られるので赤線が消えるまで根気良く続けます。
内部クラスを取り除いた`IronGenerator`のコードを以下に示します。

```java:IronGenerator.java
public class IronGenerator extends BlockContainer {
    public IronGenerator() {
        super(Material.IRON);
        setCreativeTab(CreativeTabs.BUILDING_BLOCKS);
        setRegistryName("iron_generator");
        setUnlocalizedName("iron_generator");
        setHardness(300);
    }

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

    @Nullable
    @Override
    public TileEntityLockable createNewTileEntity(World worldln, int meta) {
        return new IronGeneratorTileEntity();
    }
}
```

# ContainerIronGeneratorの実装
`IronGeneratorTileEntity`の`createContainer`メソッドでは、`ContainerIronGenerator`のインスタンスを返しています。かまど等の内部実装を見ていると、`Container`なるものが必要らしいので実装していきます。

```java:ContainerIronGenerator.java
public class ContainerIronGenerator extends Container {
    private final IInventory tileIronGenerator;

    public ContainerIronGenerator(InventoryPlayer playerInventory, IInventory ironGeneratorInventory)
    {
        this.tileIronGenerator = playerInventory;
    }

    public boolean canInteractWith(EntityPlayer playerIn)
    {
        return this.tileIronGenerator.isUseableByPlayer(playerIn);
    }
}
```
エラーが出ない最低限度の実装をしました。

# 起動してみる
ブロックとレシピを登録します。コードは途中省略しています。

```java:Mod.java
@Mod
public class GeneratorMod
{
    public static Block blockIronGenerator = new IronGenerator();

    @EventHandler
    public void preInit(FMLPreInitializationEvemt event)
    {
        registerBlock(blockIronGenerator, isClient);
        registerRecipe();
    }

    private void registerRecipe()
    {
        GameRegistry.addRecipe(new ItemStack(blockIronGenerator),
            "AAA",
            "ABA",
            "AAA",
            'A', new ItemStack(Items.IRON_INGOT),
            'B', new ItemStack(Items.COAL));
    {

    private void registerBlock(Block block, boolean isClient) {
        ItemBlock itemBlockInput = new ItemBlock(block);

        GameRegistry.register(block);
        GameRegistry.register(itemBlockInput, block.getRegistryName());

        if (isClient) {
            ModelResourceLocation modelName = new ModelResourceLocation(block.getRegistryName(), "inventory");
            ModelLoader.setCustomModelResourceLocation(itemBlockInput, 0, modelName);
        }
    }
}
```
起動してみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370590/61cb1be7-8c14-1a5d-807e-5852b3f44b05.png" width=75%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370590/4de08964-101d-dc32-b6ce-c5373cd1da2c.png" width=75%>

クラッシュせずにクラフトと設置ができました。ただし現段階ではテクスチャすら貼っていないただのブロックで、工業要素はどこにもありません。

# 最後に
ChatGPTはMinecraftのMODを作るのはあまり得意ではないようですが、それでも最初にいきなりそれっぽいコードを出してきたので驚きました。
所謂工業MODのようにするにはGUI（かまどとか醸造台とかで出てくるようなやつ）を実装しないといけないのですが、それを実装した経験はまだありません。春休みあたりに気分が乗れば実装してみようと思います。やるならどのバージョンがいいんでしょうか？MOD界隈で今主流のバージョンっていくつなのでしょうか？