Integrate with spoilage through events, modifier definitions, or the stacking helper. This guide covers source version 0.10.0, which runs on NeoForge and Fabric for Minecraft 1.21.1, 1.21.11 and 26.x (NeoForge on 26.1.2, 26.2 and 26.3 only); check that the mod is loaded for optional integrations.
| If you want to… | Start with | |
|---|---|---|
| React when food spoils or a modifier changes | Event lifecycle | |
| Add preservation behavior with data | Modifier definitions | |
| Support a custom inventory | Stacking policy | |
| Understand storage rates | Container context | |
| 26.1.2, 26.2, 26.3 | fffoodspoilageandpreservation-26.2-neoforge (same pattern per version) | fffoodspoilageandpreservation-26.2-fabric (also 26.1, 26.1.1, 26.1.2, 26.3) |
Getting started
Package: com.fyxe.fffoodspoilageandpreservation.api. Every event extends SpoilageApiEvent and is posted on the logical server to SpoilageApiEvents listeners on every loader. On NeoForge the same events are also posted on NeoForge.EVENT_BUS, so @SubscribeEvent listeners keep working there. On Fabric, SpoilageApiEvents is the way to listen. Use registry access from the current level for modifier definitions.
Compile against the per-target Maven artifact com.fyxe.fffoodspoilageandpreservation:fffoodspoilageandpreservation-<minecraft>-<loader>:0.10.0, for example fffoodspoilageandpreservation-1.21.1-neoforge or fffoodspoilageandpreservation-1.21.11-fabric, from the Fyxe Gitea Maven registry. Each jar bundles FFUtilities, so a mod that builds against it needs nothing extra at runtime beyond Fabric API on Fabric.
| Minecraft | NeoForge | Fabric |
|---|---|---|
| 1.21.1 | fffoodspoilageandpreservation-1.21.1-neoforge | fffoodspoilageandpreservation-1.21.1-fabric |
| 1.21.11 | fffoodspoilageandpreservation-1.21.11-neoforge | fffoodspoilageandpreservation-1.21.11-fabric |
Event lifecycle
| Event | Cancellable | Use |
|---|---|---|
SpoilageStageAdvancedEvent | No | Observe a stage transition; read getStack(), getComponent(), getPreviousStage(), and getNewStage(). |
FoodSpoiledEvent | Yes | Inspect the original and proposed replacement, call setReplacement(ItemStack) to substitute another result, or cancel the replacement. |
ModifierChangedEvent | No | Observe an applied or removed modifier with getStack(), getModifier(), getAction(), wasApplied(), and wasRemoved(). |
ModifierChangedEvent.Action has APPLIED and REMOVED values. A FoodSpoiledEvent is posted before the replacement is committed; cancellation keeps the original stack. These event classes do not provide a general mutation API for spoilage state.
Modifier definitions
Use com.fyxe.fffoodspoilageandpreservation.registry.ModifierLookup with level.registryAccess() to resolve modifier IDs and definitions. The registries are ModRegistries.MODIFIER and MODIFIER_CATEGORY. Datapack definitions are the preferred extension path for new preservation behavior; inspect the mod's README and shipped data for formats.
Since 0.7.11, modifiers have no priority: ModifierDef.priority() was removed, and a legacy JSON field is ignored. Within a category, the modifier with the lowest rateMultiplier() ranks highest; use ModifierDef.outranks(other) to compare. Categories still rank by ModifierCategoryDef.priority(), and ModifierLookup.tooltipOrder(lookup) sorts IDs highest rank first.
Since 0.7.12, ModifierDef.tintItem() is replaced by ModifierDef.itemHighlight(), an ItemHighlight (NONE, TINT, OUTLINE, BOTH) read from the JSON field item_highlight. Use tint() and outline() on it to test each part. A legacy tint_item: true without item_highlight decodes as TINT.
Since 0.12.0, ModifierDef.itemVisuals() holds the layers from the JSON field item_visuals (a list of ModifierVisual records: type, color, endColor, opacity, endOpacity, blend, angle, intensity). Prefer ModifierDef.visuals(), which returns item_visuals or, when that is empty, the layers the legacy itemHighlight() stands for. See the datapack format.
Stacking policy
Since 0.7.9, the server option mergeSameStageFood defaults to true. Matching item, spoilage stage, modifiers, and all non-spoilage components can merge. The result retains maximum stored spoilage progress and the earliest initialized lastGameTime, preserving deferred age conservatively. Freeze/thaw progress and its timestamp are averaged by destination count and the count actually transferred, with integer truncation. Legacy zero lastTempGameTime falls back to lastGameTime. An untracked stack counts as fresh/unmodified with zero progress, anchored to the tracked stack's clocks. Source remainders retain their original state.
Recipe outputs made from untracked food use SpoilageComponent.uninitialized(), which stores UNINITIALIZED_TIME (-1) for both clocks. Their first world update supplies the current time without charging the world's prior age. An uninitialized stack merged into a tracked stack inherits the tracked clocks.
SpoilageComponent.equals and hashCode include all seven persisted fields, including the saved storage rate, for synchronization. Setting mergeSameStageFood=false, disabling the master toggle, or using an unloaded config requires exact-state matches. Do not use SpoilageComponent.worst to authorize merging: it does not consider timestamps or temperature state.
The mod handles menu clicks, shift-clicks, dragging, double-clicks, inventory pickups, dropped-item merging, bundles, hoppers, and furnace outputs on both loaders. On NeoForge 1.21.1 it also handles hopper capability paths and standard NeoForge item handlers. On Fabric and on NeoForge 1.21.11 and 26.x, storage that moves items through the loader's own item transfer API (item handlers were replaced by the transfer API in 1.21.11) is not hooked, so it keeps strict same-freshness stacking unless the mod calls the helper below. Custom inventories can opt in with com.fyxe.fffoodspoilageandpreservation.util.SpoilageStacking. canMerge is read-only; mergeInto changes only the destination component and must run before either count changes, after capacity, permissions, and simulation checks. It does not move items or mark inventories dirty.
// After slot permissions are checked; destination is occupied.
int amount = Math.min(source.getCount(),
Math.min(slotLimit, destination.getMaxStackSize()) - destination.getCount());
if (amount > 0 && SpoilageStacking.canMerge(destination, source)) {
if (!simulate) {
SpoilageStacking.mergeInto(destination, source, amount);
destination.grow(amount);
source.shrink(amount);
container.setChanged();
}
// Report amount to the caller; simulation must not change either stack.
}
Integration pattern
@SubscribeEvent
public static void onSpoiled(FoodSpoiledEvent event) {
// Called before the spoiled stack is replaced.
if (shouldKeepOriginal(event.getOriginal())) event.setCanceled(true);
}
That form is NeoForge only. The loader-independent form works on Fabric and NeoForge; register it once from your mod initializer:
SpoilageApiEvents.register(FoodSpoiledEvent.class, event -> {
if (shouldKeepOriginal(event.getOriginal())) event.setCanceled(true);
});
Keep gameplay changes on the logical server. Do not edit the component directly in an observer event and assume the mod will synchronize it; use the replacement event or the supported datapack path.
Container context
SpoilageHelper.identifyOpenContainer(player) resolves the backing inventory of the active menu. OpenContainer.pos() may be null for generic or compound inventories without a known world position. These use neutral ambient conditions and pause thermal advancement. Player inventory slots keep player context.
SpoilageComponent.storageRate() stores the rate for elapsed time after lastGameTime(). SpoilageHelper.advance applies that saved rate before recording a new one, so moving food between inventories does not retroactively change its prior aging. The server synchronizes menu rate, ambient conditions, label, and temperature for tooltip estimates. Networking payloads and the client cache setter are internal details.
Temperature resolution
In 0.9.3, SpoilageHelper.resolvePlayerInventoryTemperature(player) and resolveContainerTemperature(level, pos) prefer available Cold Sweat temperatures, then Tough As Nails temperature categories, then biome temperature. TAN uses ambient proxies ICY=-0.5, COLD=0, NEUTRAL=0.3, WARM=1, HOT=2; these are not physical units or Cold Sweat BODY values. Disabled or unavailable TAN temperature returns to the fallback. The ambient rate, thermal tick and tooltip helpers share this resolution.