Capability Components
Package: net.astronomy.multilib.api.component
Ready-made energy/fluid/item buffers for controller block entities, plus a content cache that lets buffer contents survive an unform/reform cycle. These are the pieces every machine mod otherwise rewrites: a buffer with change notification, NBT persistence, and a capability registration one-liner. Mechanism only - the components never touch chat, sounds, or GUIs.
All three buffers follow the same pattern: an optional Runnable onChanged fires on every real (non-simulated) content change, so the owning block entity can pass this::setChanged and stay chunk-dirty automatically.
EnergyBufferComponent
public class EnergyBufferComponent extends EnergyStorage {
public EnergyBufferComponent(int capacity, int maxReceive, int maxExtract);
public EnergyBufferComponent(int capacity, int maxReceive, int maxExtract, @Nullable Runnable onChanged);
public void setOnChanged(@Nullable Runnable onChanged);
public void save(CompoundTag tag);
public void load(CompoundTag tag);
}
A NeoForge IEnergyStorage (FE). save/load persist only the stored amount - capacity and transfer limits are code-side configuration, so changing them in a mod update never fights stale NBT.
FluidTankComponent
public class FluidTankComponent extends FluidTank {
public FluidTankComponent(int capacity);
public FluidTankComponent(int capacity, @Nullable Predicate<FluidStack> validator);
public FluidTankComponent(int capacity, @Nullable Predicate<FluidStack> validator, @Nullable Runnable onChanged);
public void setOnChanged(@Nullable Runnable onChanged);
public void save(CompoundTag tag, HolderLookup.Provider registries);
public void load(CompoundTag tag, HolderLookup.Provider registries);
}
A NeoForge FluidTank with an optional fluid validator. Note the extra HolderLookup.Provider on save/load: FluidStack NBT encoding needs registry access on MC 1.21.1 - pass the registries argument your block entity's own save/load hooks already receive.
ItemBufferComponent
public class ItemBufferComponent extends ItemStackHandler {
public ItemBufferComponent(int slots);
public ItemBufferComponent(int slots, @Nullable BiPredicate<Integer, ItemStack> validator);
public ItemBufferComponent(int slots, @Nullable BiPredicate<Integer, ItemStack> validator, @Nullable Runnable onChanged);
public void setOnChanged(@Nullable Runnable onChanged);
public void setSlotValidator(@Nullable BiPredicate<Integer, ItemStack> validator);
public void save(CompoundTag tag, HolderLookup.Provider registries);
public void load(CompoundTag tag, HolderLookup.Provider registries);
}
A NeoForge ItemStackHandler with an optional per-slot (slot, stack) validator. Same HolderLookup.Provider requirement as the fluid tank.
MultiblockComponentHelper
public final class MultiblockComponentHelper {
public static <BE extends BlockEntity> void registerEnergy(RegisterCapabilitiesEvent event,
BlockEntityType<BE> type, Function<BE, IEnergyStorage> getter);
public static <BE extends BlockEntity> void registerFluid(RegisterCapabilitiesEvent event,
BlockEntityType<BE> type, Function<BE, IFluidHandler> getter);
public static <BE extends BlockEntity> void registerItem(RegisterCapabilitiesEvent event,
BlockEntityType<BE> type, Function<BE, IItemHandler> getter);
}
The capability wiring. MultiLib cannot auto-register - it has no visibility into your block entity types - so you call these once from your own RegisterCapabilitiesEvent listener:
public class MyControllerBE extends AbstractMultiblockControllerBE {
public final EnergyBufferComponent energy = new EnergyBufferComponent(100_000, 1_000, 1_000, this::setChanged);
@Override
protected void saveController(CompoundTag tag, HolderLookup.Provider registries) { energy.save(tag); }
@Override
protected void loadController(CompoundTag tag, HolderLookup.Provider registries) { energy.load(tag); }
}
@SubscribeEvent
public static void onRegisterCapabilities(RegisterCapabilitiesEvent event) {
MultiblockComponentHelper.registerEnergy(event, MY_CONTROLLER_BE_TYPE, be -> be.energy);
}
saveController/loadController are the protected extension points AbstractMultiblockControllerBE already provides - no saveAdditional override needed. ExampleControllerBE/BasicExampleSetup (example/basic/) in the source tree carry exactly this wiring as a working reference.
ContentCache
public final class ContentCache {
public record Slot(String key, Consumer<CompoundTag> save, Consumer<CompoundTag> load) {}
public static Slot slot(String key, Consumer<CompoundTag> save, Consumer<CompoundTag> load);
public static CompoundTag snapshot(Slot... slots);
public static void restore(CompoundTag tag, Slot... slots);
}
Mekanism-style content survival: snapshot the state of any set of components when the structure unforms, restore it when it reforms. Each Slot is a named save/load pair; snapshot nests each slot's tag under its key, restore skips keys missing from the tag instead of force-resetting - so adding a new slot in a mod update never wipes old snapshots. Keyed (not positional) on purpose: reordering slots between versions is harmless.
Where the snapshot tag lives between unform and reform is deliberately your call (a field on your controller BE, its NBT, wherever):
.onBroken(ctx -> myBE.cachedContents = ContentCache.snapshot(
ContentCache.slot("energy", myBE.energy::save, myBE.energy::load)))
.onFormed(ctx -> {
if (myBE.cachedContents != null) {
ContentCache.restore(myBE.cachedContents,
ContentCache.slot("energy", myBE.energy::save, myBE.energy::load));
}
})
For fluid/item slots that need registry access, capture it from the callback: tag -> myBE.tank.save(tag, ctx.level().registryAccess()).
See also
- Block Entity Abstractions - the
saveController/loadControllerhooks these components plug into. - Ports - expose these buffers on dedicated port blocks instead of the controller itself.
- HUD Providers -
EnergyHudProvider/FluidHudProvidershow these buffers on hover via Jade/TOP. - Callbacks & Events - the
onFormed/onBrokenhooksContentCachepairs with.