minecraft1.7.10左右を入れ替えたときに別のアイテムを出すクラフトレシピを登録したい

# 環境

minecraft 1.7.10

多分他のバージョンでも同じ。~~それこそ1.2.5とかでも~~

# 背景

例えばしたの2つの例を見てほしい。このように左右逆に並べた時に別々のアイテムを作るレシピを登録したいとする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/b9f8e37a-8d67-9374-064e-d863b59abc2c.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/add56b2e-291c-a432-dd6c-21d3a08b3d4e.png)

何も考えずに
[1.7のレシピ追加 - Minecraft Modding Wiki](https://mcmodding.jp/modding/index.php/1.7%E3%81%AE%E3%83%AC%E3%82%B7%E3%83%94%E8%BF%BD%E5%8A%A0)
を真似して

```java
package com.example.examplemod;

import net.minecraft.init.Blocks;
import net.minecraft.item.ItemStack;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.registry.GameRegistry;
@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION)
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        GameRegistry.addShapedRecipe(new ItemStack(Blocks.stone),
                "WA",
                'A',Blocks.dirt,
                'W',Blocks.grass);
        GameRegistry.addShapedRecipe(new ItemStack(Blocks.cobblestone),
                "AW",
                'A',Blocks.dirt,
                'W',Blocks.grass);
    }
}
```

のようにすると思う。このとき、予想に反して両方とも焼石が生成されてしまう。

# 原因

左右反転レシピが自動的に登録される仕様がMinecraftにはある。このよけいなおせっかい機能が原因である。

そもそも`GameRegistry.addShapedRecipe`がどう実装されているかというと
https://gitlab.zveronline.ru/Mirrors/Minecraft/MinecraftForge/-/blob/1.7.10/fml/src/main/java/cpw/mods/fml/common/registry/GameRegistry.java#L248-251

```java
    public static IRecipe addShapedRecipe(ItemStack output, Object... params)
    {
        return CraftingManager.getInstance().addRecipe(output, params);
    }
```

`net.minecraft.item.crafting.CraftingManager`に丸投げされている。

これをデコンパイルして追跡した先人たち曰く、内部でいろいろ処理された後、`net.minecraft.item.crafting.ShapedRecipes`のインスタンスが作られるのだそうだ。この中に`matches`というメソッドがある。でコンパイルした結果を読むと、`matches`が呼び出している`checkMatch`というprivate methodの第4引数にtrueが渡るときとfalseが渡るときがあり、先にtrueのときが処理されている。反転が先に判定される状態だ。これを書き換えることでなんとかできそうだ。

そこで`ShapedRecipes`をoverrideして`matches`を書き換えることにする。ただ、`checkMatch`がprivate methodで呼び出せないので仕方ないからデコンパイルしたものを持ってきた。

# 解決方法

デコンパイルしたコードを結構引っ張ってきたようなコードとなる

```java:NonReversedShapedRecipe.java
package com.example.examplemod;

import java.util.HashMap;

import net.minecraft.block.Block;
import net.minecraft.inventory.InventoryCrafting;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.ShapedRecipes;
import net.minecraft.world.World;
import cpw.mods.fml.common.registry.GameRegistry;

public class NonReversedShapedRecipe {
	public static ShapedRecipes addShapedRecipe(ItemStack output, Object... params) {
		String s = "";
		int i = 0;
		int j = 0;
		int k = 0;

		if (params[i] instanceof String[]) {
			String[] astring = (String[]) ((String[]) params[i++]);

			for (int l = 0; l < astring.length; ++l) {
				String s1 = astring[l];
				++k;
				j = s1.length();
				s = s + s1;
			}
		} else {
			while (params[i] instanceof String) {
				String s2 = (String) params[i++];
				++k;
				j = s2.length();
				s = s + s2;
			}
		}

		HashMap<Character, ItemStack> hashmap;

		for (hashmap = new HashMap<>(); i < params.length; i += 2) {
			Character character = (Character) params[i];
			ItemStack itemstack1 = null;

			if (params[i + 1] instanceof Item) {
				itemstack1 = new ItemStack((Item) params[i + 1]);
			} else if (params[i + 1] instanceof Block) {
				itemstack1 = new ItemStack((Block) params[i + 1], 1, 32767);
			} else if (params[i + 1] instanceof ItemStack) {
				itemstack1 = (ItemStack) params[i + 1];
			}

			hashmap.put(character, itemstack1);
		}

		ItemStack[] aitemstack = new ItemStack[j * k];

		for (int i1 = 0; i1 < j * k; ++i1) {
			char c0 = s.charAt(i1);

			if (hashmap.containsKey(Character.valueOf(c0))) {
				aitemstack[i1] = ((ItemStack) hashmap.get(Character.valueOf(c0))).copy();
			} else {
				aitemstack[i1] = null;
			}
		}

		ShapedRecipes shapedrecipes = new ShapedRecipes(j, k, aitemstack, output) {
			@Override
			public boolean matches(InventoryCrafting inv, World worldIn) {
				for (int i = 0; i <= 3 - this.recipeWidth; ++i) {
					for (int j = 0; j <= 3 - this.recipeHeight; ++j) {
						// When you uncomment this, you can get vanilla's behavior.
						// if (this.checkMatch(inv, i, j, true)) {
						// 	return true;
						// }

						if (this.checkMatch(inv, i, j, false)) {
							return true;
						}

					}
				}
				return false;
			}

			private boolean checkMatch(InventoryCrafting inv, int i, int j, boolean p_77573_4_) {
				for (int k = 0; k < 3; ++k) {
					for (int l = 0; l < 3; ++l) {
						int i1 = k - i;
						int j1 = l - j;
						ItemStack itemstack = null;

						if (i1 >= 0 && j1 >= 0 && i1 < this.recipeWidth && j1 < this.recipeHeight) {
							if (p_77573_4_) {
								itemstack = this.recipeItems[this.recipeWidth - i1 - 1 + j1 * this.recipeWidth];
							} else {
								itemstack = this.recipeItems[i1 + j1 * this.recipeWidth];
							}
						}

						ItemStack itemstack1 = inv.getStackInRowAndColumn(k, l);

						if (itemstack1 != null || itemstack != null) {
							if (itemstack1 == null && itemstack != null || itemstack1 != null && itemstack == null) {
								return false;
							}

							if (itemstack.getItem() != itemstack1.getItem()) {
								return false;
							}

							if (itemstack.getItemDamage() != 32767
									&& itemstack.getItemDamage() != itemstack1.getItemDamage()) {
								return false;
							}
						}
					}
				}

				return true;
			}
		};
		GameRegistry.addRecipe(shapedrecipes);
		return shapedrecipes;
	}
}
```

なんとこの記事を書いている今日、はじめてまともにJavaに触ったのだが、`ShapedRecipes shapedrecipes = new ShapedRecipes(j, k, aitemstack, output) {`以下の部分は匿名(無名)クラスという機能らしい。ここでは無名のShapedRecipesを継承したクラス作ってることになる。

あとは`GameRegistry.addShapedRecipe`を`NonReversedShapedRecipe.addShapedRecipe`に置き換えるだけでよい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/49d04b31-a516-e23d-4d00-f6800aa6a5c2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94177/3f586777-9418-91ab-326d-98c4518c6f5c.png)

ちゃんと左右を入れ替えたときに別のアイテムを出すことができた

# 謝辞

この記事は解析を担当した某Discord鯖のE氏を始めとする方々の成果を私が記事としてまとめたものです。サンプルコードが動くことは自力で再現していますがそれ以上のことは私は行っていません。解析に感謝します。