折角調べたので残す
環境
- forgeSrc-1.12.2-14.23.5.2768.jar
背景
前置き
Ingredientとは成分や材料を意味する英単語で、Minecraft Forgeでは作業台によるレシピの各材料を判定する部分のオブジェクトである。Ingredientは、アイテムスタックの判定機(Predicate<ItemStack>
)である。
とあるIngredientは、「幸運つきの耐久の削れた鉄の剣」や「あ
と命名された木の棒」など様々なアイテムスタックを与えると、それがどのような名前や付加データを持っていようと、ベースのアイテムが木の棒であるものに対してだけtrueを返すという。そのような判定機を使うと鉄の剣の柄の部分に使えるアイテムを表現することができるのだ。
Minecraftにおいて、凝ったことをしないレシピ(ShapelessOreRecipe
とか)内に含まれる個々のアイテムは、アイテムスタックが直接記述されているのではなく、Ingredientというものに包まれて記述されている。
さて、Ingredientにはいくつかの種類がある。例えば普通に単一のアイテムを指定するもの(minecraft:item
)や、指定したアイテムのNBTまで一致しているか見るもの(minecraft:item_nbt
)、鉱石辞書を指定するもの(forge:ore_dict
)などである。Ingredientの本質はアイテムスタックを判定する関数であり、鉱石辞書名というアイテムスタックとは直接関連がない文字列を使って表現することもできるのだ。
野望
では、オリジナルのIngredientを追加出来たらどのようなことになるだろうか。Ingredientの本質は単なる関数なので、無限の自由度が得られる。例えば内容が5ページ以上の本とか、日本語の名前で名前付けされた武器とか、修繕が付いている防具とか、様々な条件でレシピを作ることができるだろう(未確認)。
また、バニラのアイテム指定方式では、メタデータを数値で記述しなければならない。例えば木炭は次のようになる。
{
"item": "minecraft:coal",
"data": 1
}
木炭の場合はこれでも辛うじて読めるが、色付き羊毛の場合はもはやメタデータから色が分かる人の方が少ないであろう。こういったものもオリジナルの指定方法を作れば、もっと可読性の高い設定ファイルを作ることができる。
今回作ったもの
今回最終的に作ったものは、次のようなレシピ記述を受理するような機構である。
{
"type": "forge:ore_shapeless",
"group": "minecraft:gunpowder",
"ingredients": [
{
"type": "miragefairy2019:ore_dict_complex",
"ore": "mirageFairy2019CraftingToolFairyWandCrafting"
},
{
"item": "minecraft:coal",
"data": 1
},
{
"type": "forge:ore_dict",
"ore": "gemSulfur"
},
{
"type": "forge:ore_dict",
"ore": "gemSaltpeter"
}
],
"result": {
"item": "minecraft:gunpowder",
"count": 3
}
}
これは木炭・硫黄・硝石から火薬を3個作る不定形レシピで、miragefairy2019:ore_dict_complex
というものが自作のIngredientである。これの役目は、クラフティングごとに耐久が削れるアイテム「mirageFairy2019CraftingToolFairyWandCrafting
」を、不定形レシピ内で耐久値無視で指定した際に、耐久値が完全なものしか受理しなくなる問題に対処することである。
この問題は、Ingredientがsimpleである場合に内部的にサブアイテムのリストを呼び出して判定対象のアイテムスタックとのマッチングを行うため、副作用として耐久が削れるアイテムは満タンなもの(クリエイティブタブに表示されているもの)しか受理しなくなることに起因する。この問題はIngredientがsimpleでなければ起こらないものの、あいにくOreIngredient.isSimple
はtrueにハードコーディングされているため、自作Ingredientを用意するしか対処方法が見つからなかったのだ。
方法
Ingredientの登録
Ingredientがどこでロードされているかというと、net.minecraftforge.common.crafting.CraftingHelper.loadRecipes(boolean revertFrozen)
である。
この中から、都合がいいことにnet.minecraftforge.common.crafting.CraftingHelper.loadFactories(ModContainer mod)
が呼び出されていて、"/assets/" + ctx.getModId() + "/recipes/_factories.json"
というファイルに何かを記述すればIngredientがロードされるようだ。
- 例
/assets/miragefairy2019/recipes/_factories.json
そのJsonファイルの読み込みはnet.minecraftforge.common.crafting.CraftingHelper.loadFactories(JsonObject json, JsonContext context)
で行われている。
形式は大体次のような感じである。
{
"ingredients"【省略可】: {
"Ingredient名①": "クラス名",
...
},
"recipes"【省略可】: {
"レシピ名①": "クラス名",
...
},
"conditions"【省略可】: {
"コンディション名①": "クラス名",
...
}
}
ここで、①で示したキー名は、解析後に文脈から与えられるModIdと結合されることに気を付けたい。例えば、キーに"ore_dict_complex"
と指定し、ModIdがmiragefairy2019
だった場合、それを指定するリソース名はmiragefairy2019:ore_dict_complex
となる。
実際に作ったものはこれである。
{
"ingredients": {
"ore_dict_complex": "miragefairy2019.mod.lib.IngredientFactoryOreIngredientComplex"
}
}
登録部分は1行でよく、登録しないものはそれ自体を省略可能であるため案外短い。
登録するものの作成
前節でクラス名を"miragefairy2019.mod.lib.IngredientFactoryOreIngredientComplex"
としたが、これはIIngredientFactory
を実装していなければならない。また、java.lang.Class.newInstance()
でインスタンス化できなければならない。
IIngredientFactory
を実装したクラスの例を示す。
public class IngredientFactoryOreIngredientComplex implements IIngredientFactory
{
@Override
public Ingredient parse(JsonContext context, JsonObject json)
{
return new OreIngredientComplex(JsonUtils.getString(json, "ore"));
}
}
Forgeの内部ではこれをラムダ式で記述していたが、Modderが登録する際にはクラス名を指定しなければならないため、その戦法は使えない。
IIngredientFactory
はただのファクトリなので、Ingredientも別に用意しなければならない。Ingredientはクラス名を指定しなくてもよいため、ファクトリの中の匿名クラスにしてもよい。
public class OreIngredientComplex extends OreIngredient
{
public OreIngredientComplex(String ore)
{
super(ore);
}
@Override
public boolean isSimple()
{
return false;
}
}
結論
_factories.json
を記述するとレシピの材料の指定の仕方(Ingredient)を登録することができる。