はじめに
minecraft1.14.4のMOD開発を始めたとき、開発に関する情報の少なさに苦労したので、自分用のメモ兼これからMOD開発する方の助けになる(かもしれない)ことを書いておきます。
本記事ではMOD独自のレシピ形式を作るときの解説をします。
開発環境
バージョン | |
---|---|
OS | Windows10 |
Forge | 28.2.0 |
JDK | AdoptOpenJDK 8u242-b08 |
環境構築
環境構築を説明した記事は他の方が書いたものがすでにあるので、そちらを参考にしてください。
↓ 私が環境構築する際に参考にした記事です
Minecraft 1.14.4 Forge Modの作成 その1 【開発環境の準備 IntelliJ IDEA】
TNT Modders:環境構築
独自レシピ
例えば二つのアイテムを同時に精錬して合金を得たい、といった通常のレシピでは実現できないことをやりたいときに必要になります。
手順
- IRecipeインターフェースを実装する
- JSONファイルからレシピを読み取るシリアライザを実装する
- シリアライザをForgeに登録する
おおまかな手順はこんな感じです。タイルエンティティなども動作させるには必要ですが、今回はレシピの読み取り・登録をメインに解説します。
実装
ほかに思いつかなかったので、上で書いた例のようなレシピを登録するのを目標とします。
それでは手順に沿って実装していきます。まずはIRecipeインターフェースを実装します。中身はこんな感じ。
public interface IRecipe<C extends IInventory> {
// インベントリ内のアイテムがレシピの材料とマッチするか
boolean matches(C var1, World var2);
// クラフトの結果のコピーを返す
ItemStack getCraftingResult(C var1);
// これだけよくわからない。基本trueを返す
boolean canFit(int var1, int var2);
// クラフトの結果を返す
ItemStack getRecipeOutput();
default NonNullList<ItemStack> getRemainingItems(C p_179532_1_) {
NonNullList<ItemStack> nonnulllist = NonNullList.withSize(p_179532_1_.getSizeInventory(), ItemStack.EMPTY);
for(int i = 0; i < nonnulllist.size(); ++i) {
ItemStack item = p_179532_1_.getStackInSlot(i);
if (item.hasContainerItem()) {
nonnulllist.set(i, item.getContainerItem());
}
}
return nonnulllist;
}
default NonNullList<Ingredient> getIngredients() {
return NonNullList.create();
}
default boolean isDynamic() {
return false;
}
default String getGroup() {
return "";
}
default ItemStack getIcon() {
return new ItemStack(Blocks.CRAFTING_TABLE);
}
// レシピのIDを返す
ResourceLocation getId();
// 作成するレシピ形式のJSONファイルを読み書きするシリアライザを返す
IRecipeSerializer<?> getSerializer();
// レシピのタイプを返す
IRecipeType<?> getType();
}
default以外は実装する必要があるので、
- boolean mathes(C var1, World var2);
- ItemStack getCraftingResult(C var1);
- boolean canFit(int var1, int var2);
- ItemStack getRecipeOutput();
- ResourceLocation getId();
- IRecipeSerializer<?> getSerializer();
- IRecipeType<?> getType();
の7つを実装します。実装するとだいたいこんなコードになります。
import com.google.common.collect.Lists;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.item.crafting.IRecipeType;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
import net.minecraftforge.common.util.RecipeMatcher;
import java.util.List;
public class ExampleRecipe implements IRecipe<IInventory> {
public static final IRecipeType<ExampleRecipe> RECIPE_TYPE = new IRecipeType<ExampleRecipe>() {
};
protected final ResourceLocation id;
protected NonNullList<Ingredient> ingredients;
protected ItemStack result;
protected int cookTime;
public ExampleRecipe(ResourceLocation id, NonNullList<Ingredient> ingredients, ItemStack result, int cookTime){
this.id = id;
this.ingredients = ingredients;
this.result = result;
this.cookTime = cookTime;
}
// インベントリ内のアイテムがレシピの材料とマッチするか
@Override
public boolean matches(IInventory inventory, World world) {
List<ItemStack> inputs = Lists.newArrayList();
for(int index = 0; index < inventory.getSizeInventory(); ++index){
ItemStack input = inventory.getStackInSlot(index);
if(!input.isEmpty()){
inputs.add(input);
}
}
return RecipeMatcher.findMatches(inputs, this.ingredients) != null;
}
// クラフトの結果のコピーを返す
@Override
public ItemStack getCraftingResult(IInventory inventory) {
return this.result.copy();
}
// これだけよくわからない。基本trueを返す
@Override
public boolean canFit(int i, int i1) {
return true;
}
// クラフトの結果を返す
@Override
public ItemStack getRecipeOutput() {
return this.result;
}
// レシピのIDを返す(modid:recipe_name)
@Override
public ResourceLocation getId() {
return this.id;
}
// レシピのタイプを返す
@Override
public IRecipeType<?> getType() {
return RECIPE_TYPE;
}
}
matches()やcanFit()以外はレシピの内容を返すゲッター関数なので、難しくはないと思います。
mathes()ではRecipeMatcher.findMatches()を使うことによってインベントリのアイテムがレシピと一致するか確かめています。
canFit()がどのタイミングで使われるのかわかりませんが、バニラのコードを見た限りだとtrueを返す以外のことはやっていませんでした。また、クラフトの結果を返す関数がなぜ二つあるのかもわかっていません。こちらもバニラを参考にして、結果のコピーを返すようにしています。
ちなみにgetSerializer()を実装していないのは、まだ返すシリアライザを実装していないからです。シリアライザを実装し終えたら追加します。
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.util.JSONUtils;
public static class Serializer extends ForgeRegistryEntry<IRecipeSerializer<?>> implements IRecipeSerializer<ExampleRecipe>{
public Serializer(){
this.setRegistryName(new ResourceLocation(modId, "serializer_name"));
}
private static NonNullList<Ingredient> readIngredients(JsonArray array){
NonNullList<Ingredient> ingredients = NonNullList.create();
for(JsonElement element: array){
ingredients.add(CraftingHelper.getIngredient(element));
}
if(ingredients.isEmpty()){
throw new JsonParseException("No ingredients for smelting recipe");
}
return ingredients;
}
@Override
public ExampleRecipe read(ResourceLocation recipeId, JsonObject jsonObject) {
ItemStack result = CraftingHelper.getItemStack(JSONUtils.getJsonObject(jsonObject, "result"), true);
NonNullList<Ingredient> ingredients = readIngredients(JSONUtils.getJsonArray(jsonObject, "ingredients"));
int cookTime = JSONUtils.getInt(jsonObject, "process_time", 50);
return new ExampleRecipe(recipeId, ingredients, result, cookTime);
}
// サーバーとの通信用
@Nullable
@Override
public ExampleRecipe read(ResourceLocation recipeId, PacketBuffer packetBuffer) {
final int index = packetBuffer.readVarInt();
NonNullList<Ingredient> ingredients = NonNullList.withSize(index, Ingredient.EMPTY);
for(int col = 0; col < index; ++col){
ingredients.set(col, Ingredient.read(packetBuffer));
}
ItemStack result = packetBuffer.readItemStack();
int cookTime = packetBuffer.readVarInt();
return new ExampleRecipe(recipeId, ingredients, result, cookTime);
}
// サーバーとの通信用
@Override
public void write(PacketBuffer packetBuffer, ExampleRecipe exampleRecipe) {
packetBuffer.writeVarInt(exampleRecipe.ingredients.size());
for (Ingredient ingredient : exampleRecipe.ingredients) {
ingredient.write(packetBuffer);
}
packetBuffer.writeItemStack(exampleRecipe.result);
packetBuffer.writeVarInt(exampleRecipe.cookTime);
}
}
こちらがシリアライザのコードになります。read()が二つありますが、一つ目はJSONから、二つ目はネットワークパケットから読み込むといった違いがあります。
コンストラクタでは、このシリアライザにIDを振り分けています。このIDがJSONファイルでレシピを定義する際に指定するtypeになります。modIdとserializer_nameは適宜置き換えてください。
readIngredients()はJSONUtilsに複数のIngredientを読み取る関数がないので、自作した関数です。
注意
注意点として、読み取る際の順序が挙げられます。JSONからの読み取りは順序関係がないので気にしなくても問題ないです。しかし、パケットからの読み取りはwrite()で書き込んだのと同じ順序で行わないと、サーバーにログインする際の通信でエラーが発生します。パケットからの読み取りは先頭から順に行われるためです。
オフラインで遊ぶ分には問題ない(JSONからの読み取りしかしないため)ですが、このバグが発生しているサーバーには入れなくなります。これは致命的ですので、順序には細心の注意を払いましょう。
それでは、実装したシリアライザをExampleRecipe.javaにぶち込みたいと思います。
import com.google.common.collect.Lists;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraft.item.crafting.IRecipeType;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.JSONUtils;
import net.minecraft.util.NonNullList;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
import net.minecraftforge.common.crafting.CraftingHelper;
import net.minecraftforge.common.util.RecipeMatcher;
import net.minecraftforge.registries.ForgeRegistryEntry;
import javax.annotation.Nullable;
import java.util.List;
public class ExampleRecipe implements IRecipe<IInventory> {
public static final IRecipeType<ExampleRecipe>
public static final Serializer SERIALIZER = new Serializer();
RECIPE_TYPE = new IRecipeType<ExampleRecipe>() {
};
protected final ResourceLocation id;
protected NonNullList<Ingredient> ingredients;
protected ItemStack result;
protected int cookTime;
public ExampleRecipe(ResourceLocation id, NonNullList<Ingredient> ingredients, ItemStack result, int cookTime){
this.id = id;
this.ingredients = ingredients;
this.result = result;
this.cookTime = cookTime;
}
// インベントリ内のアイテムがレシピの材料とマッチするか
@Override
public boolean matches(IInventory inventory, World world) {
List<ItemStack> inputs = Lists.newArrayList();
for(int index = 0; index < inventory.getSizeInventory(); ++index){
ItemStack input = inventory.getStackInSlot(index);
if(!input.isEmpty()){
inputs.add(input);
}
}
return RecipeMatcher.findMatches(inputs, this.ingredients) != null;
}
// クラフトの結果のコピーを返す
@Override
public ItemStack getCraftingResult(IInventory inventory) {
return this.result.copy();
}
// これだけよくわからない。基本trueを返す
@Override
public boolean canFit(int i, int i1) {
return true;
}
// クラフトの結果を返す
@Override
public ItemStack getRecipeOutput() {
return this.result;
}
// レシピのIDを返す(modid:recipe_name)
@Override
public ResourceLocation getId() {
return this.id;
}
// レシピのタイプを返す
@Override
public IRecipeType<?> getType() {
return RECIPE_TYPE;
}
// シリアライザを返す
@Override
public IRecipeSerializer<?> getSerializer() {
return SERIALIZER;
}
public static class Serializer extends ForgeRegistryEntry<IRecipeSerializer<?>> implements IRecipeSerializer<ExampleRecipe>{
public Serializer(){
this.setRegistryName(new ResourceLocation(modId, "serializer_name"));
}
private static NonNullList<Ingredient> readIngredients(JsonArray array){
NonNullList<Ingredient> ingredients = NonNullList.create();
for(JsonElement element: array){
ingredients.add(CraftingHelper.getIngredient(element));
}
if(ingredients.isEmpty()){
throw new JsonParseException("No ingredients for smelting recipe");
}
return ingredients;
}
@Override
public ExampleRecipe read(ResourceLocation recipeId, JsonObject jsonObject) {
ItemStack result = CraftingHelper.getItemStack(JSONUtils.getJsonObject(jsonObject, "result"), true);
NonNullList<Ingredient> ingredients = readIngredients(JSONUtils.getJsonArray(jsonObject, "ingredients"));
int cookTime = JSONUtils.getInt(jsonObject, "process_time", 50);
return new ExampleRecipe(recipeId, ingredients, result, cookTime);
}
@Nullable
@Override
public ExampleRecipe read(ResourceLocation recipeId, PacketBuffer packetBuffer) {
final int index = packetBuffer.readVarInt();
NonNullList<Ingredient> ingredients = NonNullList.withSize(index, Ingredient.EMPTY);
for(int col = 0; col < index; ++col){
ingredients.set(col, Ingredient.read(packetBuffer));
}
ItemStack result = packetBuffer.readItemStack();
int cookTime = packetBuffer.readVarInt();
return new ExampleRecipe(recipeId, ingredients, result, cookTime);
}
@Override
public void write(PacketBuffer packetBuffer, ExampleRecipe exampleRecipe) {
packetBuffer.writeVarInt(exampleRecipe.ingredients.size());
for (Ingredient ingredient : exampleRecipe.ingredients) {
ingredient.write(packetBuffer);
}
packetBuffer.writeItemStack(exampleRecipe.result);
packetBuffer.writeVarInt(exampleRecipe.cookTime);
}
}
}
こちらがIRecipeインターフェースを継承して独自レシピを作るコードの最終形になります。ExampleRecipe.javaにgetSerializer()と、変数を追加しました。
あとはシリアライザをForgeに登録して終了です。
import net.minecraft.item.crafting.IRecipeSerializer;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = modId, bus = Mod.EventBusSubscriber.Bus.MOD)
public class RegisterRecipeTypes {
@SubscribeEvent
public static void registerRecipeSerializer(RegistryEvent.Register<IRecipeSerializer<?>> event){
event.getRegistry().register(ExampleRecipe.SERIALIZER);
}
}
レシピを定義する
最後に今回作ったレシピ形式でレシピを定義します。
{
"type": "modId:serializer_name",
"ingredients": [
{
"item": "itemId"
},
{
"item": "itemId"
}
]
"result": "itemId"
"process_time": 100
}
おわりに
タイルエンティティの作成については解説するかわからないので、参考になりそうなサイトのリンクを貼って解説を終わりとさせていただきます。
↑海外の方が作成したMODチュートリアル用のリポジトリです。英語ですがコードの説明が書かれているので、見てみてください。