MariesWiki

Nourished Wiki · Mod developers

Building a Nourished Addon

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.

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.

JSON datapack file, applies on /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's datapacks/ 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.

Project layout
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.

gradle.properties
# 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.

src/main/resources/META-INF/neoforge.mods.toml
[[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.

core/NourishedPackAddon.java (trimmed)
@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.

nutrient/Hydration.java
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

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:

nourished/source_classifications/minecraft/melon_slice.json
{
  "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:

SH
unzip -p farmersdelight.jar assets/farmersdelight/lang/en_us.json | grep '"item.farmersdelight.'

item.farmersdelight.tomato_sauce means the ID is farmersdelight: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>.json holds all of a mod's items in a single mappings object. It's handy when you're mapping a whole mod at once:

nourished/compat/vinery.json

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

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.

nourished/synergies/well_hydrated_harvest.json
{
  "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.

nourished/source_synergies/soup_and_bread.json
{
  "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:

1 · nourished/milestones/hydration_beginner.json
{
  "value_key": "hydration",
  "cumulative_goal": 50,
  "reward_effect_id": "minecraft:speed",
  "amplifier": 0,
  "reward_duration": 200,
  "advancement_id": "nourishedpackaddon:milestones/hydration_beginner"
}
2 · data/nourishedpackaddon/advancement/milestones/hydration_beginner.json
{
  "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" } }
}
3 · assets/nourishedpackaddon/lang/en_us.json
{
  "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.

nourished/tracking_profiles/survivalist.json
{
  "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:

  • NourishedPackAddonConfig builds a ModConfigSpec (saved to config/nourishedpackaddon-common.toml) with getters that Hydration.register reads.
  • NourishedPackAddonConfigScreen builds a Cloth Config screen. It's registered on the client only:
core/NourishedPackAddon.java (constructor)
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:…" to build.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:

  1. 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.
  2. Cancel the bucket's right-click (PlayerInteractEvent.RightClickItem) and call player.startUsingItem(hand). This only runs when the click isn't aimed at a block, so placing water still works.
  3. Return the empty bucket (LivingEntityUseItemEvent.Finish), because USE_REMAINDER doesn't exist until 1.21.2. Nourished's own HIGH-priority Finish handler has already recorded the hydration by then.
core/NourishedPackAddon.java
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

terminal
./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_key shows 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_key that doesn't match. It must equal the key in ValueDefinition.builder("…") or the values/ file name, in lowercase.
  • Expecting config changes to apply live. Anything read during NourishedRegisterEvent needs 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.

← Nourished Wiki home