NeoForge 26.1 Docs
  • 문서
  • 노트
NeoForge 26.1
NeoForge 26.1
v26.1
학습 진도
0 / 77 챕터 완료방문 0
마스터 트랙 #3: 기계 모드 프로젝트 셋업분쇄기 블록 등록 — CrusherBlock + facing BlockStateBlockEntity + 인벤토리 — ItemStackHandlerMenu + Screen — GUI 구현화면 위젯 — 진행 바와 슬롯 시각화커스텀 레시피 타입 — Crusher Recipe서버↔클라이언트 동기화 패킷기계 모드 마무리 + JAR 빌드
마스터기계

커스텀 레시피 타입 — Crusher Recipe

MapCodec + StreamCodec으로 CrusherRecipe 커스텀 레시피 타입을 설계하고, RecipeSerializer·RecipeType을 DeferredRegister로 등록하여 데이터팩 JSON에서 레시피를 정의합니다.

커스텀 레시피 타입 — Crusher Recipe

이 챕터에서는 분쇄기 전용 커스텀 레시피 타입을 만듭니다. NeoForge 26.1의 Recipe<RecipeInput> 인터페이스와 MapCodec + StreamCodec 이중 직렬화 구조를 사용하여, 데이터팩 JSON으로 레시피를 정의하고 RecipeManager가 자동으로 로드·동기화합니다.


1. CrusherRecipe 레코드

Java record는 불변 데이터 클래스에 최적입니다. 레시피는 한 번 생성되면 변경되지 않으므로 record로 선언합니다.

// examplemod-master-projects/machine/src/main/java/com/example/master/machine/recipe/CrusherRecipe.java
package com.example.master.machine.recipe;
 
import com.example.master.machine.MasterMachineMod;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemStackTemplate;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.item.crafting.PlacementInfo;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeBookCategory;
import net.minecraft.world.item.crafting.RecipeSerializer;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.item.crafting.SingleRecipeInput;
import net.minecraft.world.level.Level;
 
public record CrusherRecipe(Ingredient input, ItemStackTemplate output, float chance)
        implements Recipe<SingleRecipeInput> {
 
    @Override
    public boolean matches(SingleRecipeInput input, Level level) {
        return this.input.test(input.item());
    }
 
    @Override
    public ItemStack assemble(SingleRecipeInput input) {
        return output.create();
    }
 
    @Override
    public boolean showNotification() {
        return false;
    }
 
    @Override
    public String group() {
        return "";
    }
 
    @Override
    public PlacementInfo placementInfo() {
        return PlacementInfo.create(input);
    }
 
    @Override
    public RecipeBookCategory recipeBookCategory() {
        return new RecipeBookCategory();
    }
 
    @Override
    public RecipeSerializer<? extends Recipe<SingleRecipeInput>> getSerializer() {
        return MasterMachineMod.CRUSHER_RECIPE_SERIALIZER.get();
    }
 
    @Override
    public RecipeType<? extends Recipe<SingleRecipeInput>> getType() {
        return MasterMachineMod.CRUSHER_RECIPE_TYPE.get();
    }
}

핵심 포인트

메서드역할
matches()Ingredient.test()로 슬롯 아이템이 재료와 일치하는지 확인
assemble()ItemStackTemplate.create()로 결과물 ItemStack을 새로 생성 — 원본 변형 방지
placementInfo()PlacementInfo.create(input)로 재료 배치 정보 제공 (레시피북용)
getSerializer() / getType()등록된 Supplier를 .get()으로 반환

2. CrusherRecipeSerializer

레시피 직렬화기는 두 개의 코덱을 제공합니다. NeoForge 26.1.2에서 RecipeSerializer는 구현해야 할 인터페이스가 아니라 두 코덱을 받는 record이므로, 이 클래스는 코덱 상수만 보관하고 등록 시점(섹션 3)에 new RecipeSerializer<>(CODEC, STREAM_CODEC)로 조립합니다.

  • MapCodec — 데이터팩 JSON 파일 파싱
  • StreamCodec — 서버→클라이언트 네트워크 동기화
// examplemod-master-projects/machine/src/main/java/com/example/master/machine/recipe/CrusherRecipeSerializer.java
package com.example.master.machine.recipe;
 
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.world.item.ItemStackTemplate;
import net.minecraft.world.item.crafting.Ingredient;
 
public final class CrusherRecipeSerializer {
 
    public static final MapCodec<CrusherRecipe> CODEC = RecordCodecBuilder.mapCodec(builder ->
            builder.group(
                    Ingredient.CODEC.fieldOf("input").forGetter(CrusherRecipe::input),
                    ItemStackTemplate.CODEC.fieldOf("output").forGetter(CrusherRecipe::output),
                    Codec.FLOAT.optionalFieldOf("chance", 1.0f).forGetter(CrusherRecipe::chance)
            ).apply(builder, CrusherRecipe::new));
 
    public static final StreamCodec<RegistryFriendlyByteBuf, CrusherRecipe> STREAM_CODEC =
            StreamCodec.composite(
                    Ingredient.CONTENTS_STREAM_CODEC, CrusherRecipe::input,
                    ItemStackTemplate.STREAM_CODEC, CrusherRecipe::output,
                    ByteBufCodecs.FLOAT, CrusherRecipe::chance,
                    CrusherRecipe::new);
 
    private CrusherRecipeSerializer() {}
}

MapCodec vs StreamCodec

데이터팩 로드 흐름:
  data/master_machine/recipe/iron_to_dust.json
    └─ Gson 파싱 → MapCodec.decode() → CrusherRecipe 인스턴스

네트워크 동기화 흐름:
  서버 RecipeManager
    └─ StreamCodec.encode() → 바이트 버퍼 → 클라이언트
                              StreamCodec.decode() → 클라이언트 RecipeManager

optionalFieldOf("chance", 1.0f)는 JSON에서 "chance" 키가 없으면 기본값 1.0을 사용합니다.


3. RecipeType + RecipeSerializer 등록

MasterMachineMod.java에 두 개의 DeferredRegister를 추가합니다.

// MasterMachineMod.java (발췌)
public static final DeferredRegister<RecipeType<?>> RECIPE_TYPES =
        DeferredRegister.create(Registries.RECIPE_TYPE, MOD_ID);
 
public static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS =
        DeferredRegister.create(Registries.RECIPE_SERIALIZER, MOD_ID);
 
public static final Supplier<RecipeType<CrusherRecipe>> CRUSHER_RECIPE_TYPE =
        RECIPE_TYPES.register("crusher", () -> new RecipeType<>() {
            @Override
            public String toString() { return "master_machine:crusher"; }
        });
 
public static final Supplier<RecipeSerializer<CrusherRecipe>> CRUSHER_RECIPE_SERIALIZER =
        RECIPE_SERIALIZERS.register("crusher",
                () -> new RecipeSerializer<>(CrusherRecipeSerializer.CODEC, CrusherRecipeSerializer.STREAM_CODEC));
 
// 생성자에서 등록
public MasterMachineMod(IEventBus modEventBus, ModContainer container) {
    // ... 기존 등록 ...
    RECIPE_TYPES.register(modEventBus);
    RECIPE_SERIALIZERS.register(modEventBus);
}

RecipeType의 toString()은 디버그 출력에 사용되며 "modid:name" 형식을 따릅니다.


4. 레시피 JSON 작성

데이터팩 레시피 파일은 src/main/resources/data/<modid>/recipe/ 경로에 둡니다.

// examplemod-master-projects/machine/src/main/resources/data/master_machine/recipe/iron_to_dust.json
{
  "type": "master_machine:crusher",
  "input": "minecraft:iron_ingot",
  "output": {
    "id": "minecraft:redstone",
    "count": 4
  },
  "chance": 1
}

"type" 키가 어떤 RecipeSerializer를 사용할지 결정합니다. MapCodec의 필드명("input", "output", "chance")과 JSON 키가 정확히 일치해야 합니다. Ingredient.CODEC은 단일 아이템을 "minecraft:iron_ingot"처럼 문자열로 받고, 태그는 "#minecraft:..." 형식을 씁니다. 결과물은 ItemStackTemplate.CODEC이 파싱하므로 "id" + "count" 형식으로 작성합니다.


5. BlockEntity tick에서 레시피 조회

// CrusherBlockEntity.java — tick 메서드 (발췌)
public static void tick(Level level, BlockPos pos, BlockState state, CrusherBlockEntity be) {
    if (level.isClientSide()) return;
 
    ItemStack input = be.inventory.getStackInSlot(0);
    if (input.isEmpty()) { be.progress = 0; return; }
 
    Optional<RecipeHolder<CrusherRecipe>> recipe = ((ServerLevel) level).recipeAccess()
            .getRecipeFor(
                    MasterMachineMod.CRUSHER_RECIPE_TYPE.get(),
                    new SingleRecipeInput(input),
                    level);
 
    if (recipe.isPresent()) {
        be.progress++;
        if (be.progress >= 200) {
            be.progress = 0;
            be.inventory.extractItem(0, 1, false);
            ItemStack result = recipe.get().value().assemble(
                    new SingleRecipeInput(input));
            be.inventory.insertItem(1, result, false);
        }
    }
}

getRecipeFor()는 해당 RecipeType의 레시피 목록을 순회하며 matches()가 true인 첫 번째 레시피를 반환합니다. 200틱(10초)마다 가공이 완료됩니다.


⚠️ 성능 안티패턴 — 매 tick 레시피 조회

매 tick getRecipeFor 호출은 피하세요

// ❌ 매 tick 호출 — HashMap 조회 + Ingredient 매칭이 반복됨
public static void tick(...) {
    var recipe = ((ServerLevel) level).recipeAccess().getRecipeFor(...);  // 매 tick 실행
}
 
// ✅ 캐시 — input이 변경될 때만 다시 조회
private RecipeHolder<CrusherRecipe> cachedRecipe;
private ItemStack lastInput = ItemStack.EMPTY;
 
// tick 내부:
ItemStack currentInput = be.inventory.getStackInSlot(0);
if (!ItemStack.isSameItemSameComponents(be.lastInput, currentInput)) {
    be.cachedRecipe = ((ServerLevel) level).recipeAccess()
            .getRecipeFor(MasterMachineMod.CRUSHER_RECIPE_TYPE.get(),
                          new SingleRecipeInput(currentInput), level)
            .orElse(null);
    be.lastInput = currentInput.copy();
}
if (be.cachedRecipe != null) { /* 가공 진행 */ }

Ingredient.test()는 태그 조회를 포함하므로 비용이 높습니다. input 슬롯이 변경될 때만 재조회하고, 이외엔 캐시를 사용하세요.


정리

클래스역할
CrusherRecipe레시피 데이터 홀더 (record) + Recipe 인터페이스 구현
CrusherRecipeSerializerJSON → MapCodec, 네트워크 → StreamCodec
MasterMachineMod.CRUSHER_RECIPE_TYPERecipeManager 조회 키
MasterMachineMod.CRUSHER_RECIPE_SERIALIZER직렬화기 레지스트리 항목
data/.../recipe/iron_to_dust.json실제 레시피 정의 (데이터팩)

화면 위젯 — 진행 바와 슬롯 시각화

CrusherScreen의 extractContents에 진행 바를 그리고, 슬롯 호버 강조와 텍스트 렌더링으로 GUI를 완성합니다.

서버↔클라이언트 동기화 패킷

CustomPacketPayload + StreamCodec으로 CrusherProgressPayload를 설계하고, RegisterPayloadHandlersEvent로 핸들러를 등록하여 분쇄 진행도를 서버에서 클라이언트로 효율적으로 동기화합니다.

On this page

커스텀 레시피 타입 — Crusher Recipe1. CrusherRecipe 레코드핵심 포인트2. CrusherRecipeSerializerMapCodec vs StreamCodec3. RecipeType + RecipeSerializer 등록4. 레시피 JSON 작성5. BlockEntity tick에서 레시피 조회⚠️ 성능 안티패턴 — 매 tick 레시피 조회정리
NeoForge 26.1 Docs

NeoForge 26.1 모딩 개발 문서 사이트

GitHubDiscord

문서

  • 문서
  • 노트

GitHub

  • GitHub
  • Discord

© 2026 NeoForge 26.1 Docs. 콘텐츠는 MIT 라이선스로 제공됩니다.

Built with Next.js · Tailwind CSS · shadcn/ui