DataGen 통합 가이드 — 코드로 JSON 생성하기
NeoForge DataGen 시스템을 소개합니다. GatherDataEvent, 주요 Provider 5종, build.gradle runData 태스크, DataGenHandler 예시까지 한 챕터에서 다룹니다.
DataGen이란?
모드를 만들다 보면 JSON 파일을 손으로 작성하는 일이 많습니다. 레시피, 블록 태그, 아이템 태그, 루트 테이블, blockstate, 아이템 모델. 파일 하나하나는 단순하지만, 아이템이 수십 개로 늘어나면 관리가 금방 힘들어집니다.
NeoForge DataGen은 이 문제를 Java 코드로 해결합니다. JSON을 직접 쓰는 대신, Provider 클래스를 작성하면 빌드 시 JSON이 자동 생성됩니다.
손작성 JSON 대비 DataGen의 장점:
- 재현성: 코드가 있으면 언제든 동일한 JSON을 다시 생성할 수 있습니다.
- 타입 안전: 오타나 잘못된 키 이름을 컴파일 타임에 잡습니다.
- 리팩터링 용이: 아이템 ID를 바꾸면 관련 JSON이 모두 자동 갱신됩니다.
- 중복 제거: 비슷한 패턴의 레시피를 루프로 처리할 수 있습니다.
GatherDataEvent
DataGen의 진입점은 GatherDataEvent입니다. 이 이벤트는 Mod EventBus에서 발생하며, ./gradlew runData 태스크 실행 시 트리거됩니다.
@EventBusSubscriber(modid = "examplemod")
public class DataGenHandler {
@SubscribeEvent
public static void onGatherData(GatherDataEvent event) {
DataGenerator gen = event.getGenerator();
PackOutput output = gen.getPackOutput();
// 여기에 Provider를 등록합니다
}
}event.includeServer()와 event.includeClient()로 서버 데이터(레시피, 태그, 루트 테이블)와 클라이언트 데이터(모델, blockstate)를 구분해서 등록합니다.
주요 Provider 5종
1. RecipeProvider — 레시피 JSON
조합 레시피, 제련 레시피, 돌 절단기 레시피 등을 생성합니다.
gen.addProvider(event.includeServer(), new RecipeProvider(output, event.getLookupProvider()) {
@Override
protected void buildRecipes(RecipeOutput out, HolderLookup.Provider lookup) {
// 3x3 조합 레시피
ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ExampleMod.EXAMPLE_ITEM.get())
.pattern("###")
.pattern("###")
.pattern("###")
.define('#', Items.IRON_INGOT)
.unlockedBy("has_iron", has(Items.IRON_INGOT))
.save(out);
// 무형 조합 레시피
ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, Items.STICK, 4)
.requires(Items.OAK_PLANKS)
.unlockedBy("has_planks", has(Items.OAK_PLANKS))
.save(out, Identifier.fromNamespaceAndPath("examplemod", "stick_from_planks"));
}
});2. BlockTagsProvider — 블록 태그
minecraft:mineable/pickaxe 같은 바닐라 태그나 커스텀 태그에 블록을 추가합니다.
gen.addProvider(event.includeServer(), new BlockTagsProvider(output, event.getLookupProvider(), "examplemod", event.getExistingFileHelper()) {
@Override
protected void addTags(HolderLookup.Provider lookup) {
// 곡괭이로 채굴 가능
tag(BlockTags.MINEABLE_WITH_PICKAXE)
.add(ExampleMod.EXAMPLE_BLOCK.get());
// 커스텀 태그
tag(ExampleMod.MY_SPECIAL_BLOCKS)
.add(ExampleMod.EXAMPLE_BLOCK.get());
}
});3. ItemTagsProvider — 아이템 태그
블록 태그와 같은 방식으로 아이템 태그를 관리합니다. BlockTagsProvider를 의존성으로 받아 블록 태그를 아이템 태그로 복사할 수 있습니다.
gen.addProvider(event.includeServer(), new ItemTagsProvider(output, event.getLookupProvider(), blockTagsProvider.contentsGetter(), "examplemod", event.getExistingFileHelper()) {
@Override
protected void addTags(HolderLookup.Provider lookup) {
tag(ItemTags.SWORDS)
.add(ExampleMod.RUBY_SWORD.get().asItem());
}
});4. LootTableProvider — 루트 테이블
블록 파괴 시 드롭, 엔티티 처치 시 드롭 등을 정의합니다.
gen.addProvider(event.includeServer(), new LootTableProvider(output, Set.of(), List.of(
new LootTableProvider.SubProviderEntry(
ExampleBlockLootTables::new,
LootContextParamSets.BLOCK
)
), event.getLookupProvider()));ExampleBlockLootTables는 BlockLootSubProvider를 상속해서 각 블록의 드롭을 정의합니다.
public class ExampleBlockLootTables extends BlockLootSubProvider {
protected ExampleBlockLootTables(HolderLookup.Provider lookup) {
super(Set.of(), FeatureFlags.REGISTRY.allFlags(), lookup);
}
@Override
protected void generate() {
// 블록 자체를 드롭
dropSelf(ExampleMod.EXAMPLE_BLOCK.get());
}
@Override
protected Iterable<Block> getKnownBlocks() {
return ExampleMod.BLOCKS.getEntries().stream()
.map(DeferredHolder::get)
.toList();
}
}5. BlockStateProvider + ItemModelProvider — 모델
BlockStateProvider는 blockstate JSON과 블록 모델을 함께 생성합니다. ItemModelProvider는 아이템 모델을 생성합니다.
// BlockState + 블록 모델
gen.addProvider(event.includeClient(), new BlockStateProvider(output, "examplemod", event.getExistingFileHelper()) {
@Override
protected void registerStatesAndModels() {
// 단순 큐브 블록
simpleBlock(ExampleMod.EXAMPLE_BLOCK.get());
}
});
// 아이템 모델
gen.addProvider(event.includeClient(), new ItemModelProvider(output, "examplemod", event.getExistingFileHelper()) {
@Override
protected void registerModels() {
// 블록 아이템 (블록 모델 참조)
withExistingParent(
ExampleMod.EXAMPLE_BLOCK_ITEM.getId().getPath(),
modLoc("block/example_block")
);
}
});build.gradle runData 태스크
examplemod-template-26.1.2/build.gradle에 이미 data 런 설정이 포함되어 있습니다.
// examplemod-template-26.1.2/build.gradle (71번 줄)
data {
clientData()
// DataGen 출력 경로와 기존 리소스 경로를 지정합니다
programArguments.addAll '--mod', project.mod_id, '--all',
'--output', file('src/generated/resources/').getAbsolutePath(),
'--existing', file('src/main/resources/').getAbsolutePath()
}./gradlew runData를 실행하면 src/generated/resources/ 아래에 JSON 파일이 생성됩니다. sourceSets.main.resources에 이 경로가 이미 포함되어 있으므로 빌드 시 자동으로 포함됩니다.
DataGenHandler 전체 예시
한 챕터에서 손으로 작성했던 JSON을 DataGen으로 재작성하는 예시입니다.
// src/main/java/com/example/examplemod/datagen/DataGenHandler.java
@EventBusSubscriber(modid = "examplemod")
public class DataGenHandler {
@SubscribeEvent
public static void onGatherData(GatherDataEvent event) {
DataGenerator gen = event.getGenerator();
PackOutput output = gen.getPackOutput();
CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();
// 서버 데이터
BlockTagsProvider blockTags = new BlockTagsProvider(
output, lookupProvider, "examplemod", event.getExistingFileHelper()
) {
@Override
protected void addTags(HolderLookup.Provider lookup) {
tag(BlockTags.MINEABLE_WITH_PICKAXE)
.add(ExampleMod.EXAMPLE_BLOCK.get());
}
};
gen.addProvider(event.includeServer(), blockTags);
gen.addProvider(event.includeServer(), new ItemTagsProvider(
output, lookupProvider, blockTags.contentsGetter(),
"examplemod", event.getExistingFileHelper()
) {
@Override
protected void addTags(HolderLookup.Provider lookup) {
tag(ItemTags.SWORDS)
.add(ExampleMod.RUBY_SWORD.get().asItem());
}
});
gen.addProvider(event.includeServer(), new RecipeProvider(output, lookupProvider) {
@Override
protected void buildRecipes(RecipeOutput out, HolderLookup.Provider lookup) {
ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ExampleMod.EXAMPLE_ITEM.get())
.pattern("###")
.pattern("###")
.pattern("###")
.define('#', Items.IRON_INGOT)
.unlockedBy("has_iron", has(Items.IRON_INGOT))
.save(out);
}
});
gen.addProvider(event.includeServer(), new LootTableProvider(
output, Set.of(),
List.of(new LootTableProvider.SubProviderEntry(
ExampleBlockLootTables::new, LootContextParamSets.BLOCK
)),
lookupProvider
));
// 클라이언트 데이터
gen.addProvider(event.includeClient(), new BlockStateProvider(
output, "examplemod", event.getExistingFileHelper()
) {
@Override
protected void registerStatesAndModels() {
simpleBlock(ExampleMod.EXAMPLE_BLOCK.get());
}
});
}
}안티패턴: runData 출력을 손으로 수정하지 말 것
⚠️ runData 출력을 손으로 수정하면 안 됩니다
src/generated/resources/data/examplemod/recipe/<file>.json을 직접 편집한 뒤 다시./gradlew runData를 실행하면, DataGen이 파일을 덮어씁니다. 손수정 내용은 사라집니다.DataGen을 사용하는 경우 JSON은 항상 코드에서만 수정하세요. 데이터팩 작성자처럼 JSON을 직접 편집하고 싶다면 DataGen을 쓰지 않고
src/main/resources/에 직접 파일을 두는 방식을 선택하세요.
다음 단계
DataGen은 각 챕터에서 다루는 기능(레시피, 태그, 루트 테이블, 모델)과 1:1로 대응합니다. 이 가이드를 참고해서 각 챕터에서 손으로 작성한 JSON을 DataGen으로 전환해 보세요.
- 레시피:
RecipeProvider+ShapedRecipeBuilder/ShapelessRecipeBuilder - 태그:
BlockTagsProvider+ItemTagsProvider - 루트 테이블:
LootTableProvider+BlockLootSubProvider - 모델:
BlockStateProvider+ItemModelProvider