-
Notifications
You must be signed in to change notification settings - Fork 0
Data Generation
While writing JSON files by hand is perfectly valid, it can quickly become tedious and prone to syntax errors—especially when dealing with deeply nested objects like the Spell Field API. Thankfully, EnchantmentCore is built natively on Mojang's serialization framework DataFixerUpper utilizing Codecs. This means every component, effect, and parameter fully supports Minecraft's Data Generation (Datagen) ecosystem.
When maintaining a mod across Fabric, Forge, and NeoForge, duplicating data generation logic is an immense waste of time. Because the output of datagen (JSON files) is platform-agnostic, you can establish a Single Source of Truth for your entire workspace.
This guide covers how to set up a unified datagen pipeline, construct enchantments scalably, and seamlessly integrate dynamic configuration wrappers.
The strategy is to write all Data Providers using vanilla APIs inside the common project, execute the Data Generator using your preferred loader platform as the "driver," and route the output back into a shared generated folder within common.
First, instruct Gradle to treat a newly created generated directory inside common as a valid resource folder, and expose the directories to the platform projects.
In your common/build.gradle:
sourceSets {
main {
resources {
// Include standard resources
srcDirs += ['src/main/resources']
// Include our routed datagen output
srcDirs += ['src/main/generated']
}
}
}
artifacts {
// Expose directory elements to platform projects
commonJava sourceSets.main.java.sourceDirectories.elements
commonResources sourceSets.main.resources.sourceDirectories.elements
}Due to strict type enforcement in Gradle 8+, the multiloader script needs explicit file collections for the common dependencies.
In your buildSrc/src/main/groovy/multiloader-loader.gradle, update the dependencies block:
dependencies {
compileOnly(project(':common')) {
capabilities {
requireCapability "$group:$mod_id"
}
}
// Extract source directories into explicit file collections
commonJava files(project(":common").sourceSets.main.java.srcDirs)
commonResources files(project(":common").sourceSets.main.resources.srcDirs)
}You only need to run the datagen task from one platform, but you can configure all of them to support it.
Fabric Setup
fabric/build.gradle:
loom {
runs {
datagen {
inherit client
name "Data Generation"
vmArg "-Dfabric-api.datagen"
vmArg "-Dfabric-api.datagen.output-dir=${project(":common").file("src/main/generated")}"
vmArg "-Dfabric-api.datagen.modid=${mod_id}"
runDir "build/datagen"
}
}
}fabric/src/main/resources/fabric.mod.json:
{
"entrypoints": {
"fabric-datagen": [
"com.yourname.yourmod.datagen.FabricDatagenEntrypoint"
]
}
}FabricDatagenEntrypoint.java:
public class FabricDatagenEntrypoint implements DataGeneratorEntrypoint {
@Override
public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) {
FabricDataGenerator.Pack pack = fabricDataGenerator.createPack();
pack.addProvider((output, registries) -> new FabricDynamicRegistryProvider(output, registries) {
@Override
protected void configure(HolderLookup.Provider registries, Entries entries) {
entries.addAll(registries.lookupOrThrow(Registries.ENCHANTMENT));
}
@Override
public String getName() { return "My Mod Datapack"; }
});
}
@Override
public void buildRegistry(RegistrySetBuilder registryBuilder) {
registryBuilder.add(Registries.ENCHANTMENT, EnchantmentRegistryBootstrap::bootstrap);
}
}NeoForge Setup
neoforge/build.gradle:
neoforge {
runs {
data {
data()
programArguments.addAll '--mod', mod_id,
'--all',
'--output', rootProject.file("common/src/main/generated").absolutePath,
'--existing', rootProject.file("common/src/main/resources").absolutePath
}
}
}NeoForgeDatagenEntrypoint.java:
@EventBusSubscriber(modid = Constants.MOD_ID, bus = EventBusSubscriber.Bus.MOD)
public class NeoForgeDatagenEntrypoint {
@SubscribeEvent
public static void onGatherData(GatherDataEvent event) {
DataGenerator generator = event.getGenerator();
PackOutput output = generator.getPackOutput();
RegistrySetBuilder builder = new RegistrySetBuilder()
.add(Registries.ENCHANTMENT, EnchantmentRegistryBootstrap::bootstrap);
generator.addProvider(
event.includeServer(),
new net.neoforged.neoforge.common.data.DatapackBuiltinEntriesProvider(
output, event.getLookupProvider(), builder, Set.of(Constants.MOD_ID)
)
);
}
}Forge Setup
forge/build.gradle:
minecraft {
runs {
data {
workingDirectory project.file('run-data')
args '--mod', mod_id,
'--all',
'--output', rootProject.file("common/src/main/generated").absolutePath,
'--existing', rootProject.file("common/src/main/resources").absolutePath
mods { "${mod_id}" { source sourceSets.main } }
}
}
}ForgeDatagenEntrypoint.java:
@Mod.EventBusSubscriber(modid = Constants.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class ForgeDatagenEntrypoint {
@SubscribeEvent
public static void onGatherData(GatherDataEvent event) {
DataGenerator generator = event.getGenerator();
PackOutput output = generator.getPackOutput();
RegistrySetBuilder builder = new RegistrySetBuilder()
.add(Registries.ENCHANTMENT, EnchantmentRegistryBootstrap::bootstrap);
generator.addProvider(
event.includeServer(),
new net.minecraftforge.common.data.DatapackBuiltinEntriesProvider(
output, event.getLookupProvider(), builder, Set.of(Constants.MOD_ID)
)
);
}
}If you put all your enchantment logic in a single provider class, it will quickly become an unmanageable monolith. Instead, encapsulate each enchantment in its own class using static properties and Java Functions.
This guarantees registry tags and items are evaluated only when the BootstrapContext is actually ready and provided by the datagenerator.
ExplosiveStrike.java (Inside common project):
package com.example.mod.datagen.enchantment;
import johnsmith.enchantmentcore.enchantment.effect.ExplosionDefinition;
import johnsmith.enchantmentcore.enchantment.effect.ExplosionEffect;
import johnsmith.enchantmentcore.enchantment.value.PolynomialValue;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.BootstrapContext;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.entity.EquipmentSlotGroup;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.EnchantmentEffectComponents;
import net.minecraft.world.item.enchantment.EnchantmentTarget;
import net.minecraft.world.item.enchantment.LevelBasedValue;
import java.util.Optional;
import java.util.function.Function;
public class ExplosiveStrike {
// 1. Define the ResourceKey
public static final ResourceKey<Enchantment> KEY = ResourceKey.create(
Registries.ENCHANTMENT, ResourceLocation.fromNamespaceAndPath("example_mod", "explosive_strike")
);
// 2. Build the baseline definition using a Function
public static final Function<BootstrapContext<Enchantment>, Enchantment.EnchantmentDefinition> DEFINITION = (context) ->
Enchantment.definition(
context.lookup(Registries.ITEM).getOrThrow(ItemTags.SWORD_ENCHANTABLE),
2, // Weight
5, // Max Level (This will be our fallback value later)
Enchantment.dynamicCost(10, 5), // Min cost
Enchantment.dynamicCost(30, 5), // Max cost
4, // Anvil cost
EquipmentSlotGroup.MAINHAND
);
// 3. Construct the EnchantmentCore specific payload
public static final ExplosionEffect EFFECT = new ExplosionEffect(
LevelBasedValue.constant(1.0f),
// 3.1 Construct the custom effect payload
new ExplosionDefinition(
// 3.1.1 Construct a LevelBasedValue
new PolynomialValue(1.5f, 1.2f, 0.0f, 0.0f),
false, // createFire
true, // damageEntities
ExplosionDefinition.ExplosionInteraction.NONE,
Optional.empty(), // Immune filter
Optional.empty(), // Default small particles
Optional.empty(), // Default large particles
Optional.empty() // Default sound
)
);
// 4. Assemble the final builder
public static final Function<BootstrapContext<Enchantment>, Enchantment> ENCHANTMENT = (context) ->
Enchantment.enchantment(ExplosiveStrike.DEFINITION.apply(context))
.withEffect(EnchantmentEffectComponents.POST_ATTACK,
EnchantmentTarget.ATTACKER,
EnchantmentTarget.VICTIM,
ExplosiveStrike.EFFECT
).build(ExplosiveStrike.KEY.location());
}Now, your central bootstrap class remains incredibly clean. You simply mass-register your modular enchantments:
EnchantmentRegistryBootstrap.java:
package com.example.mod.datagen;
import johnsmith.enchantmentcore.datagen.enchantment.ExplosiveStrike;
import net.minecraft.data.worldgen.BootstrapContext;
import net.minecraft.world.item.enchantment.Enchantment;
public class EnchantmentRegistryBootstrap {
public static void bootstrap(BootstrapContext<Enchantment> context) {
context.register(ExplosiveStrike.KEY, ExplosiveStrike.ENCHANTMENT.apply(context));
// context.register(OtherEnchant.KEY, OtherEnchant.ENCHANTMENT.apply(context));
}
}Run the Gradle task for your chosen driver platform to serialize the vanilla-compliant JSON files into your common/src/main/generated/ folder.
-
Fabric:
./gradlew :fabric:runDatagen -
NeoForge:
./gradlew :neoforge:runData -
Forge:
./gradlew :forge:runData
Because Config Overhauled dynamic wrappers require live property evaluation to ensure fallback integrity, they cannot be injected natively during the headless Datagen phase. The generated files from Step 4 are standard, static vanilla JSONs.
To make your enchantments configurable, you will use Enchantment Core's in-game bulk processor.
-
Prepare the Data: Move your generated static enchantments from the
generatedfolder into your mod's primarydatafolder (e.g.,src/main/resources/data/). This ensures the game actively loads them when you boot up the client. - Launch the Game: Run the standard Minecraft client environment with your mod installed.
-
Execute the Import Command: Enter a local single-player world and run the import command to map your enchantments to your configuration registry:
/enchantment_core import <source_namespace> <target_namespace>Note: Replace<source_namespace>with your mod's namespace. The<target_namespace>specifies where the properties will be bound and MUST possess a registeredConfigManagervia Config Overhauled. -
Update Your Workspace: The command automatically maps your enchantments through the
DataTransformerRegistry, injects the{ "config": {...}, "fallback": ... }JSON structures, and places the modified files inenchantment_core/import/<source_namespace>/enchantment/inside your run directory. -
Overwrite: Move those modified JSON files directly into your
common/src/main/generated/data/<source_namespace>/enchantment/directory, overwriting the static files.
Your datapack is now perfectly formatted and fully bound to runtime configurations! Refer to the Config Overhauled wiki to generate translation keys for the configurable enchantments.
Want to learn more about the configuration engine powering Enchantment Core?
Check out Config Understood, the official wiki for Config Overhauled, for a deep dive into dynamic property generation and state synchronization!