Back to Knowledge Base

Hytale Inventory System: Containers, Slot Filters, and Transactions

A deep dive into Hytale's inventory architecture covering ItemContainers, slot filters, transactional item operations, the six inventory sections, and the ECS-driven change event pipeline.

Player Games··hytale

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:

SectionIDDefault CapacityPurpose
Hotbar-19 slotsQuick-access items, one active slot
Storage-236 slots (4×9)Main backpack grid
Armor-3Per armor slot countHead, chest, legs, feet
Utility-54 slotsConsumables and usable items
Tools-823 slotsTool wheel with active selection
Backpack-9VariableExpandable 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 snapshots
  • ItemStackTransaction — Add or remove an ItemStack across multiple slots (e.g., adding 64 items that split across partially-filled stacks), tracking the remainder
  • MoveTransaction — Atomic move between two containers with a MoveType of either MOVE_TO_SELF or MOVE_FROM_SELF
  • MaterialTransaction / ResourceTransaction / TagTransaction — Add/remove by material type, resource type, or tag rather than specific item ID
  • ListTransaction — Batch of transactions that succeed or fail together
  • ClearTransaction — 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

  1. Inventories are ECS components, not monolithic objects. Query for InventoryComponent.Hotbar, .Storage, etc. individually.
  2. Use transactions for multi-item operations. The all-or-nothing flag prevents partial transfers that lose items.
  3. Slot filters are composable and data-driven. Use TagFilter with item tags to restrict slots without writing custom filter classes.
  4. Listen to InventoryChangeEvent rather than polling for changes. The event includes the full transaction, so you can inspect exactly what changed.
  5. Combined containers handle overflow automatically. Use the predefined search orders or define your own to control where items land across sections.