Создание своего типа рецепта

Версия Minecraft
1.20.2
API
Forge
22
1
0
День добрый, я пытаюсь создать кастомный тип рецепта на Forge версии 1.20.2, но все мои попытки создать его или найти какой-либо более-менее рабочий гайд приводят к тому, что ничего не работает. Что именно не работает - понятия не имею, и прошу помочь хотя бы определиться, что мне нужно фиксить.
Код рецепта крафта:
Java:
public class CoalExtruderRecipe implements Recipe<Container> {
    private final NonNullList<Ingredient> inputs;
    private final ItemStack output;

    public CoalExtruderRecipe(List<Ingredient> inputs, ItemStack output) {
        this.inputs = NonNullList.withSize(2, Ingredient.EMPTY);
        for (int index = 0; index < inputs.size(); index++) {
            this.inputs.set(index, inputs.get(index));
        }
        this.output = output;
    }

    public NonNullList<Ingredient> getInputs() {
        return this.inputs;
    }

    public ItemStack getOutput() {
        return this.output;
    }

    @Override
    public boolean matches(Container pContainer, Level pLevel) {
        ItemStack input0 = pContainer.getItem(0);
        ItemStack input1 = pContainer.getItem(1);

        return matches(input0, input1);
    }
    public boolean matches(ItemStack... inputs) {
        List<Boolean> matched = new ArrayList<>(inputs.length);

        for (int i = 0; i < inputs.length; i++) {
            matched.add(false);
        }

        for (Ingredient recipeIngredient : this.inputs) {
            for (int inputIndex = 0; inputIndex < inputs.length; inputIndex++) {
                ItemStack input = inputs[inputIndex];

                if (!matched.get(inputIndex) && recipeIngredient.test(input)) {
                    matched.set(inputIndex, true);
                    break; // Move to the next recipe ingredient
                }
            }
        }

        // Check if all ingredients were matched
        return matched.stream().allMatch(Boolean::valueOf);
    }

    @Override
    public ItemStack assemble(Container pContainer, RegistryAccess pRegistryAccess) {
        ItemStack input0 = pContainer.getItem(0);
        ItemStack input1 = pContainer.getItem(1);

        return matches(input0, input1) ? this.output.copy() : ItemStack.EMPTY;
    }

    @Override
    public boolean canCraftInDimensions(int pWidth, int pHeight) {
        return true;
    }

    @Override
    public ItemStack getResultItem(RegistryAccess pRegistryAccess) {
        return this.output;
    }

    @Override
    public RecipeSerializer<?> getSerializer() {
        return Serializer.INSTANCE;
    }

    @Override
    public RecipeType<?> getType() {
        return Type.INSTANCE;
    }

    public void assemble(CoalExtruderBlockEntity coalExtruderBlockEntity) {
        ItemStack input0 = coalExtruderBlockEntity.getInventory().getStackInSlot(0);
        ItemStack input1 = coalExtruderBlockEntity.getInventory().getStackInSlot(1);

        if (matches(input0, input1)) {
            coalExtruderBlockEntity.getInventory().insertItem(2, this.output.copy(), false);

            // Remove the items from the inputs that were used
            for (Ingredient input : this.inputs) {
                if(input.test(input0))
                    coalExtruderBlockEntity.getInventory().extractItem(0, 1, false);
                else if(input.test(input1))
                    coalExtruderBlockEntity.getInventory().extractItem(1, 1, false);
            }
        }
    }

    public static class Serializer implements RecipeSerializer<CoalExtruderRecipe> {
        public static final Serializer INSTANCE  = new Serializer();

        private static final Codec<CoalExtruderRecipe> CODEC = RecordCodecBuilder.create(instance -> instance.group(
                Ingredient.CODEC_NONEMPTY.listOf().fieldOf("inputs").forGetter(recipe -> recipe.inputs),
                ItemStack.CODEC.fieldOf("output").forGetter(recipe -> recipe.output)).apply(instance, CoalExtruderRecipe::new));

        @Override
        public Codec<CoalExtruderRecipe> codec() {
            return CODEC;
        }

        @Override
        public @Nullable CoalExtruderRecipe fromNetwork(FriendlyByteBuf pBuffer) {
            int inputCount = pBuffer.readInt();
            NonNullList<Ingredient> inputs = NonNullList.withSize(2, Ingredient.EMPTY);
            for (int index = 0; index < inputCount; index++) {
                inputs.add(Ingredient.fromNetwork(pBuffer));
            }

            ItemStack output = pBuffer.readItem();
            int processTime = pBuffer.readInt();
            return new CoalExtruderRecipe(inputs, output);
        }

        @Override
        public void toNetwork(FriendlyByteBuf pBuffer, CoalExtruderRecipe pRecipe) {
            pBuffer.writeInt(pRecipe.inputs.size());
            for (Ingredient input : pRecipe.inputs) {
                input.toNetwork(pBuffer);
            }

            pBuffer.writeItem(pRecipe.output);
        }
    }

    public static class Type implements RecipeType<CoalExtruderRecipe> {
        public static final Type INSTANCE = new Type();

        private static final String ID = new ResourceLocation(TutorialMod.MOD_ID, "coal_extruding").toString();

        @Override
        public String toString() {
            return ID;
        }
    }
}
Использование в BlockEntity:
Java:
public void tick() {
        if(this.level.isClientSide() || this.level == null) {
            return;
        }
        if(hasRecipe()) {
            increaseCraftingProgress();
            setChanged(this.level, this.worldPosition, this.getBlockState());

            if(hasProgressFinished()) {
                craftItem();
                resetProgress();
            }
        } else {
            resetProgress();
        }
    }

private boolean hasRecipe() {
        Optional<RecipeHolder<CoalExtruderRecipe>> recipe = getCurrentRecipe();
        if(recipe.isEmpty()) {
            return false;
        }
        ItemStack result = recipe.get().value().getResultItem(null);
        return canInsertAmountInSlot(result.getCount()) && canInsertItemInSlot(result.getItem());
    }

    private boolean canInsertItemInSlot(Item item) {
        return this.inventory.getStackInSlot(2).isEmpty() || this.inventory.getStackInSlot(2).is(item);
    }

    private boolean canInsertAmountInSlot(int count) {
        return this.inventory.getStackInSlot(2).getCount() + count <= this.inventory.getStackInSlot(2).getMaxStackSize();
    }

    private Optional<RecipeHolder<CoalExtruderRecipe>> getCurrentRecipe() {
        SimpleContainer Inventory = new SimpleContainer(this.inventory.getSlots());
        for (int i = 0; i < inventory.getSlots(); i++) {
            Inventory.setItem(i, this.inventory.getStackInSlot(i));
        }

        return this.level.getRecipeManager().getRecipeFor(CoalExtruderRecipe.Type.INSTANCE, Inventory, level);
    }
Ни единой ошибки я не получаю (Если бы получал, может быть, пофиксил бы), прошу помочь хоть чем-нибудь
 
Решение
Вот сейчас я на самом деле посмеялся от души. Оказывается, он (наверное) все это время парсил все json'ы, причем парсил успешно, но искал другие ключи, выблевывая нужные рецепты.

Сейчас я заменил в "output" ключи "count" на "Count" и "item" на "id", и все заработало в штатном порядке. Пойду смотреть, где определяются названия этих ключей.

Огромное спасибо за помощь и уделенное мне время
Предупреждая вопросы
Код типа рецепта взял из гитхаба одного англоязычного ютубера (он мне его скинул сам), затем немного переделал под свой пример (И, вероятно, где-то там и допустил ошибку)
Рецепт в json:
Java:
{
  "type": "tutorialmod:coal_extruding",
  "inputs":[
    {
      "item": "minecraft:iron_nugget"
    },
    {
      "item": "minecraft:coal"
    }
  ],
  "output":{
    "count": 1,
    "item": "minecraft:iron_ingot"
  }
}

Сериализатор рецептов:

Java:
package net.shawwwdy.tutorialmod.recipe;



import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import net.shawwwdy.tutorialmod.TutorialMod;
import net.shawwwdy.tutorialmod.recipe.custom.CoalExtruderRecipe;

public class ModRecipeSerializer {
    public static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS =
            DeferredRegister.create(ForgeRegistries.RECIPE_SERIALIZERS, TutorialMod.MOD_ID);

    public static final RegistryObject<CoalExtruderRecipe.Serializer> COAL_EXTRUDING_SERIALIZER =
            RECIPE_SERIALIZERS.register("coal_extruding", () -> CoalExtruderRecipe.Serializer.INSTANCE);
}

Вызван в основном классе мода:
ModRecipeSerializer.RECIPE_SERIALIZERS.register(modEventBus);
 
А RecipeType зарегать?
Что значит зарегать recipeType? разве в классе самого рецепта Type это не делает?
Проще и лучше(чаще всего), если функция не кардинально новая, подглядеть у самого майна.
Пытался посмотреть, как реализовано в печках. Выглядит, конечно, просто, но нет примера для классов сериализатора и типа (а раз их пишут, то я предположил, что они нужны), так что даже не знаю, можно ли и как их перекладывать на свой пример.
 
Что значит зарегать recipeType? разве в классе самого рецепта Type это не делает?
Type то все делает, но не зарегистрирован.

Лучше делать вот так:
Java:
public static final DeferredRegister<RecipeType<?>> RECIPE_TYPES = DeferredRegister.create(ForgeRegistries.RECIPE_TYPES, TutorialMod.MOD_ID);

public static final RegistryObject<CoalExtruderRecipe.Type> COAL_EXTRUDING_TYPE = RECIPE_TYPES.register("coal_extruding", () -> CoalExtruderRecipe.Type.INSTANCE);

UPD: Все CoalExtruderRecipe.Type.INSTANCE замени на COAL_EXTRUDING_TYPE.get() и не забудь RECIPE_TYPES.register(modEventBus);

(Код писал не в IDE так что могут быть неточности)
 
Последнее редактирование:
Я как-то перестал вообще понимать код, когда увидел, что matches() выдает true

Java:
@Override public boolean matches(SimpleContainer container, Level level) {
        return true;
    }
Это проверка на то, подходит ли открываемый контейнер для рецепта. Поскольку всё взаимодействие у меня диегитическое (т.е. без интерфейса), то у меня нет контейнеров.

Но тебя, судя по ответу @Mr.Toad интересует именно вот этот класс. Дальше, думаю, сообразишь.
 
Я добавил класс ModRecipeTypes, зарегал там тип, теперь в json не выдается ошибка, говорящая, что на месте "type":_ должны быть типы из майнкрафтовского списка.
Я не смог поменять Type.INSTANCE на getType(), ide говорит, что не может обращаться к статическому элементу из нестатического контекста.
Потом я попытался немного локализовать проблему:
В BE в методе hasRecipe() рецепт не определяется, хотя предметы лежат в слотах:
1732353081165.png1732353024384.png
Прогресс не идет, причем все, что связано с рендером стрелки и выдачей результата абсолютно точно работает нормально.
Еще я заменил пару методов из класса рецепта на:

Java:
 @Override
    public boolean matches(Container pContainer, Level pLevel) {
        ItemStack input0 = pContainer.getItem(0);
        ItemStack input1 = pContainer.getItem(1);
        return this.inputs.get(0).test(input0) && this.inputs.get(1).test(input1);
    }

    @Override
    public ItemStack assemble(Container pContainer, RegistryAccess pRegistryAccess) {
        return this.output.copy();
    }
В контейнере всего два слота, теоретически, должно работать правильно
 
Я не смог поменять Type.INSTANCE на getType(), ide говорит, что не может обращаться к статическому элементу из нестатического контекста.

UPD: Все CoalExtruderRecipe.Type.INSTANCE замени на COAL_EXTRUDING_TYPE.get() и не забудь RECIPE_TYPES.register(modEventBus);
Поменял коммент
 
Java:
private Optional<RecipeHolder<CoalExtruderRecipe>> getCurrentRecipe() {
        SimpleContainer Inventory = new SimpleContainer(this.inventory.getSlots());
        for (int i = 0; i < inventory.getSlots(); i++) {
            Inventory.setItem(i, this.inventory.getStackInSlot(i));
        }

        return this.level.getRecipeManager().getRecipeFor(ModRecipeTypes.COAL_EXTRUDING_TYPE.get(), Inventory, level);
    }
 
getCurrentRecipe() используется в методах hasRecipe() и craftItem(), оба используются в классе CoalExtruderBlockEntity в методе tick(), который вызывается 20 раз в секунду самим майнкрафтом
Java:
private void craftItem() {
        Optional<RecipeHolder<CoalExtruderRecipe>> recipe = getCurrentRecipe();
        ItemStack result = recipe.get().value().getResultItem(null);

        this.inventory.extractItem(0, 1, false);
        this.inventory.extractItem(1, 1, false);

        this.inventory.setStackInSlot(2, new ItemStack(result.getItem(), this.inventory.getStackInSlot(2).getCount() + result.getCount()));
    }
private boolean hasRecipe() {
        Optional<RecipeHolder<CoalExtruderRecipe>> recipe = getCurrentRecipe();
        if(recipe.isEmpty()) {
            return false;
        }
        ItemStack result = recipe.get().value().getResultItem(null);
        return canInsertAmountInSlot(result.getCount()) && canInsertItemInSlot(result.getItem());
    }
 
Я на версии 1.20.1 конечно, но думаю что tick майном не используется.


Метод tick теперь:

Java:
public static void serverTick(Level level, BlockPos pos, BlockState state, CoalExtruderBlockEntity entity) {
  
if(entity.hasRecipe()) {
     entity.increaseCraftingProgress();
     entity.setChanged(this.level, this.worldPosition, this.getBlockState());

      if(entity.hasProgressFinished()) {
          entity.craftItem();
          entity.resetProgress();
      }
} else {
     entity.resetProgress();
};
  
}

Дальше в классе своего блока нужно переопределить getTicker()

Класс блока:
@Override
   public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
      return leve.isClientSide() ? null : createTickerHelper(level, type, CoalExtruderBlockEntity::serverTick());
   }

Код писался на коленке, возможны не точности
 
Как я уже говорил, абсолютно все, что я написал до того, как приступить к рецепту, стопроцентно работает.
То есть если я вместо if(hasRecipe()) поставлю if(true), то прогресс запустится и будет обнуляться, когда дойдет до максимального
1732357731970.png
 
Последнее редактирование:
Назад
Сверху