Hytale's Entity Component System: Archetypes, Chunks, and Queries

A source-grounded tour of Hytale's archetype-based ECS — Component, ComponentType, Archetype, ArchetypeChunk, ReadWriteQuery, CommandBuffer, and the system pipeline modders write against.

Player Games10 min read

Hytale's server (version 2026.03.26) is built on a data-oriented Entity Component System living under the com.hypixel.hytale.component package. An entity is not an object with methods — it is a Ref<ECS_TYPE> (an opaque, validated handle) that points at a row of plain-data Component instances stored in column arrays. There are no inheritance hierarchies of Mob extends Entity; there is only the set of components an entity currently holds, and that set is its archetype.

This design exists for one reason: locality and batch iteration. Entities with identical component sets are packed together into an ArchetypeChunk, components are stored as parallel arrays rather than per-entity objects, and systems iterate those chunks in bulk. If you understand Component, ComponentType, Archetype, ArchetypeChunk, Store, Query, and CommandBuffer, you understand how every piece of server-side gameplay logic — movement, AI, spawning, effects — is actually wired. Every symbol below is read directly from the decompiled server. Illustrative mod code is marked as a sketch; everything else is the real API surface.

#Components, Types, and Archetypes

A component is just data. The Component interface is deliberately minimal — it extends Cloneable and exists so the ECS can copy entity data when entities move between chunks:

Java
public interface Component<ECS_TYPE> extends Cloneable {
   @Nullable Component<ECS_TYPE> clone();
   default Component<ECS_TYPE> cloneSerializable() { return this.clone(); }
}

A component class (e.g. your HealthComponent implements Component<...>) is registered once to obtain a ComponentType — the stable identity the ECS uses everywhere. ComponentType is interesting: it carries an integer index (its column position) and is itself a Query. Its test(Archetype) simply asks "does this archetype contain me?":

Java
public class ComponentType<ECS_TYPE, T extends Component<ECS_TYPE>>
      implements Comparable<ComponentType<ECS_TYPE, ?>>, Query<ECS_TYPE> {
   public int getIndex() { return this.index; }

   @Override public boolean test(Archetype<ECS_TYPE> archetype) {
      return archetype.contains(this);   // archetype membership == query match
   }
}

An Archetype is an immutable set of ComponentTypes. It is stored sparsely — indexed by each type's getIndex() — so contains() is an O(1) array lookup, and add/remove return new archetypes rather than mutating:

Java
Archetype<ECS_TYPE> a = Archetype.of(positionType, velocityType);
Archetype<ECS_TYPE> b = Archetype.add(a, healthType);     // a is unchanged
boolean has = b.contains(positionType);                   // true
boolean superset = b.contains(a);                         // true: b ⊇ a

Archetype.contains(Archetype) is the workhorse of the whole query layer: a chunk's archetype matches a required archetype when it is a superset of it.

SymbolRole
Component<ECS_TYPE>Marker interface for cloneable data blobs
ComponentType<ECS_TYPE, T>Stable identity + column index; also a Query
Archetype<ECS_TYPE>Immutable, sparse set of component types
Archetype.of(...) / add / removeBuild archetypes (return new instances)
Ref<ECS_TYPE>Validated handle to one entity inside a Store

#ArchetypeChunk: the Column Store

Entities are not scattered on the heap. Every entity with a given archetype lives in an ArchetypeChunk, and that chunk holds a 2D array Component<ECS_TYPE>[componentIndex][entityIndex]structure of arrays. The i-th entity's HealthComponent sits at components[healthType.getIndex()][i], right next to every other entity's health. That is what makes a system loop cache-friendly.

Java
public class ArchetypeChunk<ECS_TYPE> {
   protected Ref<ECS_TYPE>[] refs;
   protected Component<ECS_TYPE>[][] components;   // [componentType.getIndex()][entityIndex]

   public int size() { return this.entitiesSize; }
   public Ref<ECS_TYPE> getReferenceTo(int index) { ... }

   public <T extends Component<ECS_TYPE>> T getComponent(
         int index, ComponentType<ECS_TYPE, T> componentType) { ... }

   public <T extends Component<ECS_TYPE>> void setComponent(
         int index, ComponentType<ECS_TYPE, T> componentType, T component) { ... }
}

Two consequences matter for modders. First, you read and write components by entity index within a chunk, not by Ref, inside a hot loop — chunk.getComponent(i, healthType). Second, adding or removing a component changes an entity's archetype, so the ECS must physically move the entity to a different chunk. ArchetypeChunk exposes addEntity, removeEntity, and transferTo for exactly this — and that relocation is why you almost never mutate structure mid-iteration directly (see CommandBuffer below).

Component index is column position

ComponentType.getIndex() is both the type's identity and its slot in the chunk's column array. Archetype uses the same index for O(1) contains. Never assume the index is stable across server versions or different registries — always resolve types through registration, never hard-code an integer.

#Queries: Describing What to Iterate

A system declares the entities it cares about with a Query. The interface is tiny and composable:

Java
public interface Query<ECS_TYPE> {
   boolean test(Archetype<ECS_TYPE> archetype);
   boolean requiresComponentType(ComponentType<ECS_TYPE, ?> type);

   static <E> AnyQuery<E> any();
   static <E> NotQuery<E> not(Query<E> q);
   static <E> AndQuery<E> and(Query<E>... qs);
   static <E> OrQuery<E> or(Query<E>... qs);
}

Because ComponentType and Archetype both implement Query, you compose queries directly out of them:

Java
// conceptual: "has Position AND Velocity, but NOT Frozen"
Query<ECS_TYPE> moving = Query.and(
   Archetype.of(positionType, velocityType),
   Query.not(frozenType)
);
QueryMatches when…
ComponentTypearchetype contains that one type
Archetypearchetype is a superset of it
AndQueryevery sub-query matches
OrQueryat least one sub-query matches
NotQuerythe wrapped query does not match
AnyQuery (Query.any())always (every archetype)

For systems that distinguish reads from writes, ReadWriteQuery carries two archetypes. Its default test requires the chunk to contain both the read set and the write set — this is the metadata the scheduler uses to know which systems touch which columns:

Java
public class ReadWriteQuery<ECS_TYPE> implements ReadWriteArchetypeQuery<ECS_TYPE> {
   public ReadWriteQuery(Archetype<ECS_TYPE> read, Archetype<ECS_TYPE> write) { ... }
   public Archetype<ECS_TYPE> getReadArchetype();
   public Archetype<ECS_TYPE> getWriteArchetype();
}

The Store resolves a query into the matching chunks. The public iteration entry points are forEachChunk(Query, BiConsumer<ArchetypeChunk, CommandBuffer>), the predicate-returning overloads for early exit, forEachEntityParallel(Query, ...) for fan-out, and getEntityCountFor(Query) for a cheap count.

#Systems: Where Mod Logic Lives

A system is a unit of behavior. The base contract is ISystem, which can declare a SystemGroup and a set of Dependency objects for ordering. The abstract System base class adds the registration helpers — this is where a system declares the components and resources it owns:

Java
public abstract class System<ECS_TYPE> implements ISystem<ECS_TYPE> {
   protected <T extends Component<ECS_TYPE>> ComponentType<ECS_TYPE, T>
      registerComponent(Class<? super T> tClass, Supplier<T> supplier);

   public <T extends Resource<ECS_TYPE>> ResourceType<ECS_TYPE, T>
      registerResource(Class<? super T> tClass, Supplier<T> supplier);
}

For per-tick logic over entities, you extend EntityTickingSystem, which is a QuerySystem — it supplies a getQuery() and the scheduler only feeds it chunks whose archetype passes that query (and that aren't tagged NonTicking). The per-entity callback gives you the chunk, the entity's index, the Store, and a CommandBuffer:

Java
public abstract class EntityTickingSystem<ECS_TYPE> extends ArchetypeTickingSystem<ECS_TYPE> {
   public abstract void tick(float dt, int index,
      ArchetypeChunk<ECS_TYPE> chunk, Store<ECS_TYPE> store, CommandBuffer<ECS_TYPE> buffer);
}

A sketch of a regeneration system reads and writes components by index, and queues structural change through the buffer:

Java
// conceptual mod system — symbols (EntityTickingSystem, getQuery, tick,
// chunk.getComponent, CommandBuffer.removeComponent) are real
public final class RegenSystem extends EntityTickingSystem<World> {
   private final ComponentType<World, HealthComponent> healthType;
   private final ComponentType<World, RegenComponent>  regenType;

   @Override public Query<World> getQuery() {
      return Archetype.of(healthType, regenType);   // only matching chunks are ticked
   }

   @Override public void tick(float dt, int i, ArchetypeChunk<World> chunk,
                              Store<World> store, CommandBuffer<World> buffer) {
      HealthComponent h = chunk.getComponent(i, healthType);
      RegenComponent  r = chunk.getComponent(i, regenType);
      h.current = Math.min(h.max, h.current + r.perSecond * dt);   // write into the column
      if (h.current >= h.max) {
         Ref<World> ref = chunk.getReferenceTo(i);
         buffer.removeComponent(ref, regenType);   // queued — does NOT relocate now
      }
   }
}

Beyond ticking, the family includes EntityEventSystem (handles an EcsEvent for matching entities), ArchetypeChunkSystem, SpatialSystem (rebuilds a spatial index — see below), and the data/fetch systems. Each is registered through the registry's registerSystem(ISystem).

Base classUse it for
EntityTickingSystemPer-entity logic every tick
ArchetypeTickingSystemPer-chunk logic every tick
EntityEventSystemReacting to an EcsEvent on matching entities
SpatialSystemMaintaining a spatial query structure
ArchetypeChunkSystemHooks when systems attach/detach from chunks

#CommandBuffer: Deferred, Thread-Safe Mutation

Iterating a chunk while simultaneously adding or removing components from it would relocate entities out from under the loop. The ECS solves this with CommandBuffer — a queue of Consumer<Store> operations that the engine drains after the current pass. CommandBuffer implements the full ComponentAccessor interface, so it is the single object you mutate the world through from inside a system:

Java
public class CommandBuffer<ECS_TYPE> implements ComponentAccessor<ECS_TYPE> {
   public Ref<ECS_TYPE> addEntity(Holder<ECS_TYPE> holder, AddReason reason);
   public void removeEntity(Ref<ECS_TYPE> ref, RemoveReason reason);
   public <T extends Component<ECS_TYPE>> T addComponent(Ref<ECS_TYPE> ref, ComponentType<ECS_TYPE, T> type);
   public <T extends Component<ECS_TYPE>> void removeComponent(Ref<ECS_TYPE> ref, ComponentType<ECS_TYPE, T> type);
   public <T extends Component<ECS_TYPE>> void putComponent(Ref<ECS_TYPE> ref, ComponentType<ECS_TYPE, T> type, T value);
   public <Event extends EcsEvent> void invoke(Ref<ECS_TYPE> ref, Event param);
}

Reads are immediate (getComponent, getArchetype, getResource); structural writes are deferred until consume() runs. Note the entity-lifecycle enums on the API: addEntity takes an AddReason (SPAWN or LOAD) and removals take a RemoveReason (REMOVE or UNLOAD) — so listeners can distinguish a fresh spawn from a chunk reload, and a real death from a chunk unload.

The buffer is also the unit of parallelism. fork() produces a child buffer for a worker thread, and mergeParallel(parent) folds its queued operations back into the parent so they apply deterministically on the main thread. EntityTickingSystem uses exactly this when isParallel(...) is true, forking a buffer per parallel range and merging afterward.

Never relocate during iteration

Always route addComponent / removeComponent / addEntity / removeEntity through the CommandBuffer you're handed — never call structural mutators on the Store mid-tick. The buffer guarantees those changes are applied between passes, after the chunk you're iterating is no longer live, and merges correctly across parallel workers.

#Resources, Refs, and the Spatial Layer

Not all state belongs to entities. A Resource<ECS_TYPE> is a singleton blob owned by the Store, registered with registerResource(...) to get a ResourceType, and read via store.getResource(type). World-global state (time of day, a navigation graph, a spatial index) lives here rather than being duplicated on every entity.

The spatial subpackage is a concrete, important resource. SpatialSystem ticks every frame, clears its SpatialResource, walks every matching chunk via store.forEachChunk(systemIndex, ...), reads each entity's Vector3d position through your getPosition(chunk, index) override, and rebuilds the SpatialStructure (a KDTree, with MortonCode support) so other systems can answer nearest-neighbour and radius queries cheaply. This is how proximity-driven gameplay avoids O(n²) scans.

Finally, Ref<ECS_TYPE> deserves respect. It is a validated handle, not a pointer: it carries the owning Store and a mutable internal index, and it is invalidated when its entity is removed. Calling validate() on a stale ref throws IllegalStateException — and the decompiled code even captures the invalidating stack trace as the cause, so a use-after-free in mod logic points you straight at the removal that killed it.

#Putting It Together

A mod that adds new behavior follows the same path the engine's own systems do: define plain-data components, register them to obtain ComponentTypes, build an Archetype/Query describing the entities you operate on, implement a system that reads and writes component columns by index, and route every structural change through the CommandBuffer. The ECS handles chunk packing, query resolution, ordering, and deferred mutation for you — your job is to keep the hot path doing arithmetic on arrays.

ECS modder checklist

  • Define each piece of state as a class implementing Component<ECS_TYPE> with a working clone().
  • Register it (e.g. via System.registerComponent or the registry) to obtain a stable ComponentType — never hard-code a column index.
  • Describe target entities with an Archetype.of(...), composing Query.and/or/not as needed.
  • Extend EntityTickingSystem (or EntityEventSystem), return your query from getQuery().
  • In tick, read/write via chunk.getComponent(i, type); get the handle with chunk.getReferenceTo(i).
  • Make every add/remove/spawn go through the CommandBuffer, with the right AddReason / RemoveReason.
  • Put world-global state in a Resource; use a SpatialSystem for proximity queries instead of scanning all entities.
  • Register the system with registerSystem(...) and treat stale Refs as fatal — validate before use.

Build it yourself

Turn an idea into a working Hytale mod

Describe what you want in plain English and get a real, installable build. No boilerplate, no setup.

Start building

Related guides