Pular para o conteúdo principal

ECS architecture

The pieces

ConceptIn practice
ComponentPlain data on an entity — SimNPCComponent, RoutineAIComponent
ComponentTypeThe key used to read and write a component
Store<EntityStore>Where entities and their components live
Ref<EntityStore>A handle to one entity
CommandBufferQueues structural changes to apply after the tick
ArchetypeChunkA batch of entities with the same component layout

The rule that bites everyone

Structural writes throw during a tick.

addComponent, putComponent, removeComponent and addEntity all fail with:

IllegalStateException: Store is currently processing!

if called while the store is ticking. This silently broke three separate features before it was understood: auto-spawn never worked, growth transitions never ran, and visual scaling never applied.

Two ways out:

// Inside a system, with a CommandBuffer at hand
commandBuffer.addComponent(ref, TYPE, value);

// Outside, or when there is no buffer
world.execute(() -> store.addComponent(ref, TYPE, value));

Reading and mutating fields of an existing component is fine — store.getComponent returns the live instance.

Query is optimisation; the guard is the rule

A system declares what it ticks over:

public Query<EntityStore> getQuery() {
return Query.or(
SimTale.SIM_NPC_COMPONENT_TYPE,
Player.getComponentType(),
PersistentModel.getComponentType());
}

That is a coarse filter for performance — the ECS does not even call tick for entities that do not match. PlumbobSystem previously queried UUIDComponent, which is every entity in the world: dropped items, projectiles, everything, every tick.

But Query.or lets through more than you want, so correctness needs an explicit check inside the tick:

boolean isPlayer = chunk.getComponent(index, Player.getComponentType()) != null;
boolean isNpc = chunk.getComponent(index, SimTale.SIM_NPC_COMPONENT_TYPE) != null;
if (!isPlayer && !isNpc) return;
Event handlers have no query

SimTaleEventHandler and SimTaleUseNPCInteraction are event handlers, not ticking systems. There is no query to lean on — the event fires for whatever entity the player interacted with. They depend entirely on the guard, and for a long time they had none: interacting with any entity attached SIM_NPC_COMPONENT_TYPE to it. Since RoutineAISystem queries exactly that component, cows and players alike started running the villager routine.

Component cloning

RoutineAIComponent.clone() is called by the engine when replacing a component. Every field has to be copied by hand.

A field forgotten there does not produce an error; it silently resets. The leash-throttling fields were once missing, so every component replacement reset navigation and NPCs froze mid-walk.

Registries in memory

SimTale.ACTIVE_NPCS is a fast lookup list, kept so tick loops never scan the world. NPCs register on spawn and unregister on despawn.

BedRegistry and ChestRegistry are static sets holding furniture anchors. They are global, with no world scope — two worlds in the same session share them. Rebuilt by the join scan.

Persistence

Two layers, and confusing them causes bugs:

LayerWhat it stores
Native ECS codec on SimNPCComponentOnly EntityId and Name
Caskara, shell simtaleEverything else: needs, personality, relationships, house

Anything not in the codec and not in the Caskara record is lost on reload. Needs goes through Caskara — which is why the starvation counter lives there and survives a relog.

Use the right shell

Caskara.load() resolves to the default shell. NPC data lives in simtale. A re-attach path once always returned null for exactly this reason and never ran.