Integrate with spoilage through events, modifier definitions, or the stacking helper. This guide covers source version 0.7.12; 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 |
Getting started
Package: com.fyxe.fffoodspoilageandpreservation.api. These events are posted on NeoForge.EVENT_BUS. Register a common/server listener with @SubscribeEvent; use registry access from the current level for modifier definitions.
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.
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 (including capability paths), furnace outputs, and standard NeoForge item handlers. 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);
}
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.