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:
- Remove any previous instance of our modifier.
- Measure the attribute value without us.
- Add an
ADD_VALUEmodifier so the final value equals the nutrition target (same absolute result as the old base-value overwrite). - Clamp current health downward if it exceeds the new max.
Other mods can:
- Detect our contribution via
AttributeInstance#getModifier(FFNutritionAPI.getMaxHealthModifierId()). - Remove it with
AttributeInstance#removeModifier(...). - Adjust the target before application by listening to
ComputeMaxHealthEvent.
Queries (safe on both sides when the attachment exists)
| Method | Returns |
|---|---|
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:
- Post a cancellable
NutritionChangeEvent. - Apply the change (with clamping).
- Refresh the max-health modifier (fires
ComputeMaxHealthEvent+MaxHealthAppliedEvent). - Sync Metabolic Gauge state to the owning client.
- Post
NutritionChangedEvent.
| Method | Purpose |
|---|---|
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.
| Event | Cancellable | When |
|---|---|---|
NutritionChangeEvent | Yes | Before a category mutation |
NutritionChangedEvent | No | After a successful mutation |
ComputeMaxHealthEvent | No (adjust target instead) | Before the max-health modifier is written |
MaxHealthAppliedEvent | No | After the modifier is applied |
FoodNutritionAppliedEvent | No | After 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
- Category names and order come from the datapack (
nutrition_categories.json). Do not hard-code indices without also readinggetCategories(). - Item vectors are raw composition; the server scales them by vanilla food nutrition/saturation and the common-config multipliers before applying.
- Decay runs every server level tick and deliberately does not spam
NutritionChangeEventevery tick. Max-health events still fire when the applied max actually changes. setCategory/setAllCategoriesvia the API (and commands) now clamp correctly; the old command path that accidentally added instead of set has been fixed.- Prefer
FFNutritionAPIover direct attachment writes so client gauge sync and events stay correct. NutritionDatano longer implements NeoForge'sINBTSerializable(0.4.0).serializeNBTanddeserializeNBTremain as plain methods, andNutritionData.CODECis the save codec.
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