Hytale's inventory system is built around a transactional container abstraction that enforces slot-level filtering, supports atomic multi-slot operations, and fires ECS events on every change. Rather than treating inventories as flat arrays of items, Hytale splits player inventories into six typed sections—each backed by an ItemContainer with its own capacity, filters, and active-slot tracking. This reference is based on Hytale server version 2026.03.26-89796e57b.
The Six Inventory Sections
Every player entity has six InventoryComponent subtypes, each registered as a separate ECS component:
| Section | ID | Default Capacity | Purpose |
|---|---|---|---|
| Hotbar | -1 | 9 slots | Quick-access items, one active slot |
| Storage | -2 | 36 slots (4×9) | Main backpack grid |
| Armor | -3 | Per armor slot count | Head, chest, legs, feet |
| Utility | -5 | 4 slots | Consumables and usable items |
| Tools | -8 | 23 slots | Tool wheel with active selection |
| Backpack | -9 | Variable | Expandable overflow storage |
Each section is an independent ECS component with its own ItemContainer backing store:
public abstract class InventoryComponent implements Component<EntityStore> {
protected ItemContainer inventory = EmptyItemContainer.INSTANCE;
protected final AtomicBoolean isDirty = new AtomicBoolean();
protected final AtomicBoolean needsSaving = new AtomicBoolean();
}
Sections with an active slot—Hotbar, Tools, and Utility—track which slot the player currently has selected. The getActiveItem() method returns the ItemStack in the active slot, or null if nothing is selected:
// From InventoryComponent.Hotbar
public ItemStack getActiveItem() {
return this.activeSlot != -1 && this.activeSlot < this.inventory.getCapacity()
? this.inventory.getItemStack(this.activeSlot)
: null;
}
The server determines what the player is “holding” by checking the Tools section first, falling back to the Hotbar:
public static ItemStack getItemInHand(ComponentAccessor<EntityStore> accessor, Ref<EntityStore> ref) {
InventoryComponent.Tool toolComponent = accessor.getComponent(ref, Tool.getComponentType());
if (toolComponent != null && toolComponent.isUsingToolsItem()) {
return toolComponent.getActiveItem();
}
InventoryComponent.Hotbar hotbarComponent = accessor.getComponent(ref, Hotbar.getComponentType());
return hotbarComponent != null ? hotbarComponent.getActiveItem() : null;
}
ItemStack: Immutable Item Representation
Items in containers are represented by ItemStack—a value object with an item ID, quantity, durability, and optional BSON metadata. Stacks are effectively immutable; mutation methods return new instances:
public class ItemStack {
protected String itemId;
protected int quantity = 1;
protected double durability;
protected double maxDurability;
protected BsonDocument metadata;
// Mutation returns a new ItemStack
public ItemStack withQuantity(int quantity) {
if (quantity == 0) return null; // Zero quantity = removal
return quantity == this.quantity ? this
: new ItemStack(this.itemId, quantity, this.durability, this.maxDurability, this.metadata);
}
public ItemStack withDurability(double durability) {
return new ItemStack(this.itemId, this.quantity,
MathUtil.clamp(durability, 0.0, this.maxDurability),
this.maxDurability, this.metadata);
}
}
Stacking is controlled by isStackableWith(), which requires matching item ID, durability values, and metadata. Items with different durability levels cannot stack even if they share the same item type—this prevents exploits where damaged tools merge with fresh ones:
public boolean isStackableWith(ItemStack other) {
if (Double.compare(other.durability, this.durability) != 0) return false;
if (Double.compare(other.maxDurability, this.maxDurability) != 0) return false;
if (!this.itemId.equals(other.itemId)) return false;
return Objects.equals(this.metadata, other.metadata);
}
Items also support states via withState(String state), which resolves to a different item ID through the item's asset definition. This enables items that change form (e.g., a torch that becomes a placed block variant) without losing their stack properties.
Slot Filters: Per-Slot Validation
The filter system controls what items can enter, leave, or be dropped from specific slots. Every filter implements SlotFilter:
public interface SlotFilter {
SlotFilter ALLOW = (actionType, container, slot, itemStack) -> true;
SlotFilter DENY = (actionType, container, slot, itemStack) -> false;
boolean test(FilterActionType actionType, ItemContainer container, short slot, ItemStack itemStack);
}
FilterActionType is an enum with three values: ADD, REMOVE, and DROP. This means a slot can accept items but prevent them from being removed, or vice versa—useful for quest items or locked equipment.
Hytale ships several built-in filter implementations:
ArmorSlotAddFilter — Restricts each armor slot to its correct type (helmet, chestplate, leggings, boots):
public class ArmorSlotAddFilter implements ItemSlotFilter {
private final ItemArmorSlot itemArmorSlot;
public boolean test(Item item) {
return item == null || (item.getArmor() != null
&& item.getArmor().getArmorSlot() == this.itemArmorSlot);
}
}
TagFilter — Allows only items with a specific tag, enabling data-driven slot restrictions without code changes:
public class TagFilter implements ItemSlotFilter {
private final int tagIndex;
public boolean test(Item item) {
return item == null || item.getData().getExpandedTagIndexes().contains(this.tagIndex);
}
}
NoDuplicateFilter — Prevents the same item type from appearing twice in a container, useful for the utility bar where you want one of each consumable:
public class NoDuplicateFilter implements ItemSlotFilter {
private final SimpleItemContainer container;
public boolean test(Item item) {
for (short i = 0; i < container.getCapacity(); i++) {
ItemStack existing = container.getItemStack(i);
if (existing != null && existing.getItemId().equals(item.getId())) {
return false; // Already have this item type
}
}
return true;
}
}
Filters can be applied globally to a container with setGlobalFilter() or per-slot with setSlotFilter(). The Utility section automatically applies a filter ensuring only items with getUtility().isUsable() can be placed there.
Transactional Operations
All container mutations go through a transaction system that records what changed, enabling atomic rollbacks and event propagation. The base interface is minimal:
public interface Transaction {
boolean succeeded();
boolean wasSlotModified(short slot);
}
The key transaction types form a hierarchy:
SlotTransaction— Single-slot set/remove with before/after snapshotsItemStackTransaction— Add or remove an ItemStack across multiple slots (e.g., adding 64 items that split across partially-filled stacks), tracking the remainderMoveTransaction— Atomic move between two containers with aMoveTypeof eitherMOVE_TO_SELForMOVE_FROM_SELFMaterialTransaction/ResourceTransaction/TagTransaction— Add/remove by material type, resource type, or tag rather than specific item IDListTransaction— Batch of transactions that succeed or fail togetherClearTransaction— Wipes an entire container
ItemStackTransaction is particularly interesting because it supports an all-or-nothing mode. When allOrNothing is true, adding 10 items to a container with only 3 free slots will fail entirely rather than partially filling. This prevents inventory desync bugs where items are silently lost:
public class ItemStackTransaction implements Transaction {
private final boolean succeeded;
private final ActionType action; // ADD or REMOVE
private final ItemStack query; // What was requested
private final ItemStack remainder; // What couldn't be placed (null = fully placed)
private final boolean allOrNothing;
private final List<ItemStackSlotTransaction> slotTransactions;
}
Combined Containers and Search Order
When the server needs to add an item across multiple sections (e.g., a pickup goes to hotbar first, then storage, then backpack), it uses CombinedItemContainer. The InventoryComponent class defines several predefined search orders:
// Pickup order: hotbar first, then storage, then backpack
HOTBAR_STORAGE_BACKPACK = { hotbar, storage, backpack };
// Crafting withdrawal: storage first, then hotbar
STORAGE_FIRST = { storage, hotbar };
// Overflow from backpack: backpack → storage → hotbar
BACKPACK_STORAGE_HOTBAR = { backpack, storage, hotbar };
// Equipment check: armor → hotbar → utility → storage
ARMOR_HOTBAR_UTILITY_STORAGE = { armor, hotbar, utility, storage };
CombinedItemContainer delegates add/remove calls through each child container in order, passing remainders forward. This means a 64-stack pickup that half-fills a hotbar slot will automatically overflow into storage without any special-case code.
Change Events and Stat Recalculation
Every container mutation fires an ItemContainerChangeEvent through a synchronous event bus. The InventoryComponent collects these into a ConcurrentLinkedQueue, which ECS ticking systems drain each frame:
public class InventoryChangeEvent extends EcsEvent {
private final ComponentType<EntityStore, ? extends InventoryComponent> componentType;
private final InventoryComponent inventory;
private final ItemContainer itemContainer;
private final Transaction transaction;
}
Dedicated systems watch each section type: HotbarChangeEventSystem, ArmorChangeEventSystem, StorageChangeEventSystem, and so on. When armor changes, the server invalidates the entity's equipment network state and triggers a full stat modifier recalculation. When the active hotbar item changes, weapon stats are recalculated and any stat-clearing effects from the previous weapon are applied.
This event-driven approach means mods that listen for InventoryChangeEvent can react to any inventory mutation—whether triggered by the player, a command, or another mod—without needing to hook into every possible code path.
Key Takeaways for Modders
- Inventories are ECS components, not monolithic objects. Query for
InventoryComponent.Hotbar,.Storage, etc. individually. - Use transactions for multi-item operations. The all-or-nothing flag prevents partial transfers that lose items.
- Slot filters are composable and data-driven. Use
TagFilterwith item tags to restrict slots without writing custom filter classes. - Listen to
InventoryChangeEventrather than polling for changes. The event includes the full transaction, so you can inspect exactly what changed. - Combined containers handle overflow automatically. Use the predefined search orders or define your own to control where items land across sections.