Nourished · NeoForge 1.21.1 · worked example: Pack Integration
A step-by-step guide to making a mod that adds nutrients, food mappings, effects, synergies, milestones and diet profiles to Nourished, using Nourished: Pack Integration and its Hydration nutrient as the running example.
/reload
Java needs a rebuild and restart
Optional only if you need it
How an addon works
A Nourished addon is an ordinary NeoForge mod that depends on Nourished and MariesLib. Nourished owns the diet system: the nutrient bars, decay and the HUD. MariesLib's data loader reads everything under data/<your_modid>/nourished/ from your jar and registers it for you.
That means most of an addon is JSON, not code. Pack Integration ships about 290 data files and only four Java classes, and three of those exist for one reason: the nutrient icon (explained in step 3).
Datapack-only JSON
- Map items to existing nutrients (proteins, vegetables, fruits, grains, dairy)
- Threshold effects, synergies, meal combos
- Milestones and diet profiles
- A new nutrient without a custom icon
Needs Java Java
- A new nutrient with an icon
- Nutrient values players can change in a config screen
- Gameplay changes, such as making the water bucket drinkable
Shortcut: If you only want JSON, you don't even need a mod. Put the same
data/<namespace>/nourished/…folders in a normal datapack zip in your world'sdatapacks/folder. The rest of this guide builds a full mod because it's easier to share and can grow into Java later.
Project layout
Here is Pack Integration's layout. Copy this structure and replace nourishedpackaddon with your own mod ID everywhere.
NourishedPackAddon/
├── build.gradle NeoForge ModDev + compileOnly deps
├── gradle.properties mod_id, versions, dependency ranges
├── settings.gradle uses ../Nourished and ../Marieslib if they exist
└── src/main/
├── java/dev/maire/nourishedpackaddon/
│ ├── core/NourishedPackAddon.java @Mod entry point, event wiring
│ ├── nutrient/Hydration.java the nutrient definition
│ ├── config/NourishedPackAddonConfig.java
│ └── client/config/NourishedPackAddonConfigScreen.java
└── resources/
├── META-INF/neoforge.mods.toml
├── assets/nourishedpackaddon/lang/en_us.json
└── data/nourishedpackaddon/
├── advancement/milestones/ vanilla advancements for milestones
└── nourished/ ← MariesLib reads everything in here
├── source_classifications/<modid>/<item>.json
├── effects/
├── synergies/
├── source_synergies/
├── milestones/
└── tracking_profiles/1. Set up the project Gradle
Start from a copy of this repo (or the NeoForge MDK) and change the identity in gradle.properties. Keep the dependency versions in step with the Nourished and MariesLib builds you're targeting.
# Mod Info
mod_id=mynutritionaddon
mod_name=My Nutrition Addon
mod_group_id=com.example.mynutritionaddon
mod_version=0.1.0
# Dependencies
maries_lib_version=0.1.2-beta
maries_lib_version_range=[0.1.2-beta,)
nourished_version=0.2.7-beta.2
nourished_version_range=[0.2.7-beta.2,)Both libraries are compileOnly: you compile against them, but players install them separately. settings.gradle uses local checkouts at ../Nourished and ../Marieslib when they exist. Otherwise it pulls from GitHub Packages, which needs a GITHUB_TOKEN (or gpr.user / gpr.key).
Then declare the dependencies in neoforge.mods.toml. ordering = "AFTER" makes your mod load after Nourished, so the nutrients you reference already exist. Add an optional entry for each mod whose items you map.
[[dependencies.${mod_id}]]
modId = "nourished"
type = "required"
versionRange = "${nourished_version_range}"
ordering = "AFTER"
side = "BOTH"
# repeat for "marieslib" (required), then one per mod you add mappings for:
[[dependencies.${mod_id}]]
modId = "farmersdelight"
type = "optional"
versionRange = "*"
ordering = "AFTER"
side = "BOTH"2. Main mod class Java
The entry point does one job: listen for NourishedRegisterEvent on the mod event bus. Nourished fires it once during setup, and that's where you register anything that has to come from Java.
@Mod(NourishedPackAddon.MODID)
public class NourishedPackAddon {
public static final String MODID = "nourishedpackaddon";
public NourishedPackAddon(IEventBus modEventBus, ModContainer modContainer) {
modEventBus.addListener(NourishedPackAddon::onNourishedRegister);
}
private static void onNourishedRegister(NourishedRegisterEvent event) {
Hydration.register(event);
}
}The event can register more than nutrients: registerFood, registerEffect, registerNutrientSynergy, registerFoodSynergy, registerMilestone, registerProfile and registerCompat. Use JSON for those where you can. JSON reloads live and server owners can override it with their own datapacks.
3. Register a nutrient Java Optional
Skip this step if you only map foods to Nourished's built-in nutrients: proteins, vegetables, fruits, grains and dairy.
A new nutrient is a ValueDefinition. Its key ("hydration") is the value_key every JSON file uses to refer to it.
public final class Hydration {
public static final String KEY = "hydration";
public static void register(NourishedRegisterEvent event) {
NourishedPackAddonConfig config = NourishedPackAddonConfig.get();
event.registerNutrient(ValueDefinition.builder(KEY)
.displayName("Hydration")
.color(0xFF3FA7D6) // ARGB bar colour
.icon("minecraft:water_bucket") // the reason this is Java
.beneficial(true)
.criticalThreshold(config.criticalThreshold()) // 0.10
.lowThreshold(config.lowThreshold()) // 0.30
.excessThreshold(config.excessThreshold()) // 0.90
.defaultDecayRate(config.decayRate()) // 0.0015 per tick
.amountScale(config.amountScale()) // units per full bar
.build());
}
}Why Java instead of JSON?
MariesLib can load nutrients from nourished/values/<key>.json, but that format has no icon field. A JSON-only nutrient works, but it shows without an icon. If you don't need an icon, JSON is simpler:
data/mynutritionaddon/nourished/values/hydration.json
{
"display_name": "Hydration",
"color": -12605482,
"default_decay_rate": 0.0015,
"critical_threshold": 0.10,
"low_threshold": 0.30,
"excess_threshold": 0.90
}The file name is the key. color is read as a signed int, so 0xFF3FA7D6 becomes -12605482.
4. Map foods to nutrients JSON
This is the bulk of most addons. Each file links one item to one nutrient. Pack Integration uses one subfolder per mod, which keeps 266 files easy to find:
{
"item": "minecraft:melon_slice",
"value_key": "hydration",
"amount": 0.3
}amount is how much the item adds, measured against the nutrient's amountScale. With a scale of 1.0, a water bucket at 1.0 fills the bar and a melon slice at 0.3 adds about a third. Pack Integration roughly follows this scale:
| Kind of item | Example | amount |
|---|---|---|
| Pure water | minecraft:water_bucket |
1.0 |
| Milk, hearty stews | minecraft:rabbit_stew |
0.6 |
| Soups | minecraft:beetroot_soup |
0.5 |
| Sweet drinks | minecraft:honey_bottle |
0.35 |
| Juicy fruit | minecraft:melon_slice |
0.3 |
| Berries, small fruit | minecraft:sweet_berries |
0.2 |
Never guess an item ID. Display names often don't match registry IDs. Read the real ID from the mod's jar:
unzip -p farmersdelight.jar assets/farmersdelight/lang/en_us.json | grep '"item.farmersdelight.'
item.farmersdelight.tomato_saucemeans the ID isfarmersdelight:tomato_sauce.
Two other ways to map items
- Item tags. Replace
"item"with"tag": "c:foods/soup"to map every item in a tag. Tag mappings are resolved once tags have loaded. - One file per mod.
nourished/compat/<modid>.jsonholds all of a mod's items in a singlemappingsobject. It's handy when you're mapping a whole mod at once:
nourished/compat/vinery.json
{
"mod_id": "vinery",
"mappings": {
"vinery:apple_juice": "hydration", // amount defaults to 1.0
"vinery:red_grapejuice": { "value": "hydration", "amount": 0.5 }
}
}JSON doesn't allow comments. Remove the // lines before using this. Both item IDs are real Vinery items that this addon already maps.
5. Threshold effects JSON
An effect applies a potion effect while a nutrient is past a threshold. Pack Integration has three: critical, low and bonus.
nourished/effects/hydration_low.json
{
"value_key": "hydration",
"threshold": 0.30,
"threshold_type": "LOW",
"effect_id": "minecraft:weakness",
"amplifier": 0,
"duration": 200
}| threshold_type | Applies when the bar is | Pack Integration uses |
|---|---|---|
CRITICAL |
below threshold |
0.10 → Mining Fatigue |
LOW |
below threshold |
0.30 → Weakness |
BONUS |
above threshold |
0.75 → Haste |
duration is in ticks (20 ticks = 1 second). amplifier 0 is level I.
6. Synergies JSON
Nutrient synergies: two bars at once
These fire when two nutrients are both in a given state. Set is_penalty to true for a debuff.
{
"value_a_key": "hydration",
"value_a_condition": "OPTIMAL",
"value_b_key": "vegetables",
"value_b_condition": "OPTIMAL",
"bonus_effect_id": "minecraft:regeneration",
"amplifier": 0,
"effect_duration": 200,
"is_penalty": false
}Conditions used in this addon: OPTIMAL (Well Hydrated Harvest, Protein Powered) and LOW (Parched, Hungry). Your synergies can pair your new nutrient with Nourished's built-in ones.
Meal synergies: two foods in a time window
Eat source_a and source_b within time_window_ticks (1200 = 60 s) to get a bonus to a nutrient.
{
"source_a": "farm_and_charm:onion_soup",
"source_b": "bakery:sandwich",
"time_window_ticks": 1200,
"bonus_value_key": "hydration",
"bonus_amount": 0.15
}7. Milestones JSON
A milestone tracks the total amount of a nutrient a player has ever gained. Reaching the goal grants a reward effect and an advancement. Each milestone needs three files:
{
"value_key": "hydration",
"cumulative_goal": 50,
"reward_effect_id": "minecraft:speed",
"amplifier": 0,
"reward_duration": 200,
"advancement_id": "nourishedpackaddon:milestones/hydration_beginner"
}{
"display": {
"icon": { "id": "minecraft:water_bucket" },
"title": { "translate": "advancement.nourishedpackaddon.milestones.hydration_beginner.title" },
"description": { "translate": "advancement.nourishedpackaddon.milestones.hydration_beginner.description" },
"frame": "task", "show_toast": true, "announce_to_chat": false, "hidden": false
},
"parent": "nourishedpackaddon:milestones/root",
"criteria": { "impossible": { "trigger": "minecraft:impossible" } }
}{
"advancement.nourishedpackaddon.milestones.hydration_beginner.title": "First Sip",
"advancement.nourishedpackaddon.milestones.hydration_beginner.description": "Accumulate 50 Hydration."
}The advancement's minecraft:impossible trigger is intentional. Players can't earn it through vanilla means; Nourished grants it when the milestone is hit. You also need a hidden root.json advancement as the tab's parent. Copy it from advancement/milestones/root.json in this repo.
8. Diet profiles JSON
A profile changes decay rates and thresholds to suit a playstyle. Leave a nutrient out and it keeps its default. Pack Integration ships Hunter, Farmer, Traveler, Survivalist and Feast.
{
"display_name": "Survivalist",
"description": "Everything decays faster and deficiencies hit harder, but staying balanced pays off.",
"custom_decay_rates": {
"proteins": 0.0020, "vegetables": 0.0020, "grains": 0.0020,
"fruits": 0.0020, "dairy": 0.0020, "hydration": 0.0022
},
"custom_thresholds": { "hydration": 0.40 }
}9. Config screen Java Optional
JSON values can already be changed with a datapack, so a config is only useful for values that live in Java. Here that's the Hydration definition from step 3. Pack Integration uses two classes:
NourishedPackAddonConfigbuilds aModConfigSpec(saved toconfig/nourishedpackaddon-common.toml) with getters thatHydration.registerreads.NourishedPackAddonConfigScreenbuilds a Cloth Config screen. It's registered on the client only:
NourishedPackAddonConfig.register(modContainer);
modEventBus.addListener(NourishedPackAddonConfig::onModConfigLoading);
modEventBus.addListener(NourishedPackAddonConfig::onModConfigReloading);
if (FMLEnvironment.dist == Dist.CLIENT) {
modContainer.registerExtensionPoint(IConfigScreenFactory.class,
(minecraft, parent) -> NourishedPackAddonConfigScreen.create(parent));
}Mark every entry “requires restart”. The nutrient is registered once at startup, so changes only take effect after a relaunch. Add
compileOnly "me.shedaniel.cloth:cloth-config-neoforge:…"tobuild.gradle.
10. Custom behaviour Java Optional
Sometimes a mapping isn't enough because the item can't be eaten. A vanilla water bucket places water instead of being drunk, so mapping it to Hydration does nothing on its own. Pack Integration fixes this with three ordinary NeoForge listeners:
- Give it a FOOD component (
ModifyDefaultComponentsEvent, mod bus) with 0 nutrition and 0 saturation. Nourished's tooltip now shows the item's nutrients, and the item gets a 32-tick drink animation. - Cancel the bucket's right-click (
PlayerInteractEvent.RightClickItem) and callplayer.startUsingItem(hand). This only runs when the click isn't aimed at a block, so placing water still works. - Return the empty bucket (
LivingEntityUseItemEvent.Finish), becauseUSE_REMAINDERdoesn't exist until 1.21.2. Nourished's own HIGH-priority Finish handler has already recorded the hydration by then.
private static void onModifyDefaultComponents(ModifyDefaultComponentsEvent event) {
event.modify(Items.WATER_BUCKET, builder -> builder.set(DataComponents.FOOD,
new FoodProperties.Builder().nutrition(0).saturationModifier(0f).alwaysEdible().build()));
}
private static void onRightClickWaterBucket(PlayerInteractEvent.RightClickItem event) {
if (!(event.getEntity() instanceof ServerPlayer player)) return;
if (!event.getItemStack().is(Items.WATER_BUCKET)) return;
event.setCanceled(true);
InteractionHand hand = event.getHand();
MinecraftServer server = player.level().getServer();
if (server != null) server.execute(() -> player.startUsingItem(hand));
}
private static void onFinishDrinkingWaterBucket(LivingEntityUseItemEvent.Finish event) {
if (!(event.getEntity() instanceof ServerPlayer)) return;
if (!event.getItem().is(Items.WATER_BUCKET)) return;
event.setResultStack(new ItemStack(Items.BUCKET));
}Register the first listener on modEventBus and the other two on NeoForge.EVENT_BUS. The matching data file is source_classifications/minecraft/water_bucket.json with amount: 1.0.
Build and test
./gradlew compileJava # quick compile check
./gradlew runClient # launch a dev client with your addon
./gradlew build # jar in build/libs/- Open the game and check that your nutrient's bar appears in the Nourished HUD.
- Hold a mapped item and run
/nourished debug food. It should list your nutrient and amount. - Edit a JSON file in a datapack and run
/reload. Changes apply without a restart. Java and config changes still need one. - Watch the log for MariesLib data-loader errors. A typo in a field name or
value_keyshows up there.
Field reference
Every folder lives under data/<modid>/nourished/. MariesLib's dev.marie.framework.data.DatapackSchema is the authoritative source for these names.
| Folder | What it adds | Fields |
|---|---|---|
values/ |
Nutrient (no icon) | display_name, color, default_decay_rate, critical_threshold, low_threshold, excess_threshold |
source_classifications/ |
Item or tag → nutrient | item or tag, value_key, amount |
compat/ |
Many items for one mod | mod_id, mappings |
effects/ |
Threshold effect | value_key, threshold, threshold_type, effect_id, amplifier, duration |
synergies/ |
Two-nutrient combo | value_a_key, value_a_condition, value_b_key, value_b_condition, bonus_effect_id, amplifier, effect_duration, is_penalty |
source_synergies/ |
Two-food combo | source_a, source_b, time_window_ticks, bonus_value_key, bonus_amount |
milestones/ |
Lifetime goal | value_key, cumulative_goal, reward_effect_id, amplifier, reward_duration, advancement_id |
tracking_profiles/ |
Diet profile | display_name, description, custom_decay_rates, custom_thresholds |
Common mistakes
- Inventing item IDs. A wrong ID fails quietly and the food just doesn't count. Always check the mod's lang file (step 4).
- Duplicating core mappings. Nourished already maps Farmer's Delight, Croptopia, Pam's HarvestCraft 2 and Butchery for its five built-in nutrients. Only add mappings for your nutrient, which is why Pack Integration's FD and Croptopia folders contain Hydration only.
- Two systems for the same thing. Pack Integration deliberately skips Thirst Was Taken 2. Mapping a standalone thirst mod's drinks would give players two water meters at once.
- A
value_keythat doesn't match. It must equal the key inValueDefinition.builder("…")or thevalues/file name, in lowercase. - Expecting config changes to apply live. Anything read during
NourishedRegisterEventneeds a restart.
Based on the NourishedPackAddon source: NeoForge 21.1.229, Minecraft 1.21.1, Nourished 0.2.7-beta.2, MariesLib 0.1.2-beta.