This guide describes the public API in the current source version 0.8.3. Use the dependency guidance below and check that the mod is loaded when integrating conditionally.

FF Nutrition — Modding API

This document describes the public API other mods (and agents) can use to interact with FF Nutrition on Minecraft 1.20.1, 1.20.6, 1.21.1, 1.21.11 and 26.1 to 26.3 (Fabric, Forge and NeoForge; NeoForge on 26.1.2, 26.2 and 26.3).

Primary entry point: com.fyxeinc.ffnutrition.api.FFNutritionAPI

Events live under com.fyxeinc.ffnutrition.api.event.


Dependency

Optional (recommended). NeoForge (neoforge.mods.toml):

[[dependencies.yourmod]]
    modId="ffnutrition"
    type="optional"
    versionRange="[0.2,)"
    ordering="NONE"
    side="BOTH"

Forge 1.20.1 (mods.toml) uses mandatory=false in place of type="optional".

Fabric (fabric.mod.json): "suggests": { "ffnutrition": ">=0.4.0" }. The API is loader-free, so the same calls work on both loaders.

Compile against the per-target Maven artifact com.fyxeinc.ffnutrition:ffnutrition-<minecraft>-<loader>:0.8.3, for example ffnutrition-1.21.1-neoforge, ffnutrition-1.21.11-fabric or ffnutrition-1.20.1-forge. NeoForge 1.21.1 builds before 0.4.0 used com.fyxeinc.ffnutrition:ffnutrition.

Always prefer the API over writing the nutrition_data attachment or attribute base value directly. The API keeps events, the max-health modifier, and the Metabolic Gauge client sync consistent.


Max-health compatibility

FF Nutrition no longer calls AttributeInstance#setBaseValue on Attributes.MAX_HEALTH.

It applies a permanent AttributeModifier with a stable id:

ffnutrition:nutrition_max_health

Exposed as FFNutritionMod.NUTRITION_MAX_HEALTH_MODIFIER_ID and FFNutritionAPI.getMaxHealthModifierId().

Behaviour:

  1. Remove any previous instance of our modifier.
  2. Measure the attribute value without us.
  3. Add an ADD_VALUE modifier so the final value equals the nutrition target (same absolute result as the old base-value overwrite).
  4. Clamp current health downward if it exceeds the new max.

Other mods can:


Queries (safe on both sides when the attachment exists)

MethodReturns
getNutritionData(LivingEntity)Attachment instance (creates defaults if needed)
getNutritionScore(Player)Score in [0.0, 1.0]
getCategoryValue(Player, String)Category fill in [0.0, 100.0]
getCategoryValues(Player)Unmodifiable category map
getCategories()Ordered datapack category names
hasCategory(String)Whether the category is currently loaded
getItemNutritionValues(Item)Raw datapack vector (unscaled)
hasItemNutrition(Item)Whether a datapack entry exists, direct or derived from category name patterns
getDefaultTargetMaxHealth(Player)Built-in formula target (before events)
getMaxHealthModifierId()ResourceLocation (Identifier on 1.21.11) of our permanent modifier; on 1.20.x it is stored under a UUID derived from this id
getMaxHealthModifierAmount(Player)Optional<Double> of current modifier amount
foodValueScale(double nutrition, double saturation)nutrition * nutritionModifier + saturation * saturationModifier from the common config (defaults 4.0 and 2.0); saturation is the absolute amount (0.8.0)

Mutations (server only; no-ops on client)

All mutation helpers:

  1. Post a cancellable NutritionChangeEvent.
  2. Apply the change (with clamping).
  3. Refresh the max-health modifier (fires ComputeMaxHealthEvent + MaxHealthAppliedEvent).
  4. Sync Metabolic Gauge state to the owning client.
  5. Post NutritionChangedEvent.
MethodPurpose
addToCategory(Player, String, double)Add delta to one category
addToCategory(..., Cause)Same with explicit cause
setCategory(Player, String, double)Absolute set (clamped)
setCategory(..., Cause)Same with explicit cause
setAllCategories(Player, double)Set every category to the same value (clamped)
setAllCategories(..., Cause)Same with explicit cause
addNutritionVector(Player, double[], Cause)Ordered vector add (food-style)
applyFoodNutrition(ServerPlayer, Item, double[], usedFallback, fromPlacedBlock)Food path + FoodNutritionAppliedEvent; the change event carries an empty stack
applyFoodNutrition(ServerPlayer, ItemStack, double[], usedFallback)Eaten-item food path (0.8.0): same as above, but the NutritionChangeEvent carries the stack
applyMaxHealth(Player)Force modifier refresh only

Cause values: FOOD, DECAY, COMMAND, API, DEATH_CLONE, OTHER.


Events (server-side)

All events extend NutritionEvent. On every loader, listen with NutritionEvents.register(EventClass.class, listener); listeners run on the server thread in registration order. On NeoForge 1.20.6 and newer the events also extend NeoForge's Event and are posted on NeoForge.EVENT_BUS, so @SubscribeEvent listeners work there. NutritionChangeEvent implements Cancellable (NeoForge's ICancellableEvent on NeoForge). Forge 1.20.1 uses the listener registry only.

EventCancellableWhen
NutritionChangeEventYesBefore a category mutation
NutritionChangedEventNoAfter a successful mutation
ComputeMaxHealthEventNo (adjust target instead)Before the max-health modifier is written
MaxHealthAppliedEventNoAfter the modifier is applied
FoodNutritionAppliedEventNoAfter food (item or placed block) contributed values

Example — override max health from another mod

// Any loader, once during mod setup:
NutritionEvents.register(ComputeMaxHealthEvent.class, YourMod::onComputeMaxHealth);

// NeoForge alone can use @SubscribeEvent on the same method.
public static void onComputeMaxHealth(ComputeMaxHealthEvent event) {
    // e.g. give a bonus heart while a custom effect is active
    if (event.getPlayer().hasEffect(YOUR_EFFECT)) {
        event.setTargetMaxHealth(event.getTargetMaxHealth() + 2.0);
    }
}

Example — block nutrition gain from a specific food

public static void onNutritionChange(NutritionChangeEvent event) {
    if (event.getCause() == NutritionChangeEvent.Cause.FOOD
            && event.getOperation() == NutritionChangeEvent.Operation.ADD_VECTOR) {
        // cancel or mutate event.getVector()
        event.setCanceled(true);
    }
}

Example — scale eaten food by its stack

Since 0.8.0, NutritionChangeEvent.getStack() returns the food stack as it was eaten for Cause.FOOD changes from an eaten item. It is ItemStack.EMPTY for placed food blocks and for every other cause, and is read only. The event has a new constructor NutritionChangeEvent(ServerPlayer, Cause, Operation, ItemStack); the three-argument constructor passes an empty stack. Item compositions are multiplied by FFNutritionAPI.foodValueScale(nutrition, saturation) for the item's own food values, so a listener can compare the values of two versions of a food with the same weighting.

public static void onNutritionChange(NutritionChangeEvent event) {
    ItemStack stack = event.getStack();
    if (event.getCause() == NutritionChangeEvent.Cause.FOOD
            && event.getOperation() == NutritionChangeEvent.Operation.ADD_VECTOR
            && !stack.isEmpty()) {
        double factor = yourFreshness(stack); // 0.0 to 1.0
        double[] scaled = event.getVector().clone();
        for (int i = 0; i < scaled.length; i++) {
            scaled[i] *= factor;
        }
        event.setVector(scaled);
    }
}

Example — react after score changes

public static void onNutritionChanged(NutritionChangedEvent event) {
    if (event.getNewScore() >= 0.9) {
        // grant achievement, particle, etc.
    }
}

Notes for agents / integrators


Package layout

com.fyxeinc.ffnutrition.api
├── FFNutritionAPI.java
└── event
    ├── NutritionEvent.java            (base class)
    ├── NutritionEvents.java           (listener registry, every loader)
    ├── Cancellable.java
    ├── NutritionChangeEvent.java
    ├── NutritionChangedEvent.java
    ├── ComputeMaxHealthEvent.java
    ├── MaxHealthAppliedEvent.java
    └── FoodNutritionAppliedEvent.java