> For the complete documentation index, see [llms.txt](https://wise-shepherd-games.gitbook.io/theia-ecs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wise-shepherd-games.gitbook.io/theia-ecs/events.md).

# Events

Subscription for entity and relation notifications

Events in Theia are attachment points for **fixed behavior, not temporary subscriptions**. Wire handlers once at world construction to declare "*this is what happens when X occurs*". **Subscribers run synchronously on the calling thread**, and events fire after the changes has already been applied to world state.

#### Two Levels of Event Hubs

Every world exposes two hubs:&#x20;

1. `World.EntitiesEvents` for entity lifecycle and component mutation events;
2. `World.RelationsEvents` for relation lifecycle events.&#x20;

Assemblages expose a third hub: `assemblage.EntityEvents`. **That fires only for entities whose composition matched that assemblage's archetype**.&#x20;

The two levels are additive: a handler on `dwarf.EntityEvents` **fires in addition** to the matching handler on `world.EntitiesEvents`, not instead of it.

#### Granularity and Predictability

Every subscription point **is as narrow or as broad as the handler needs**.&#x20;

1. At the world level, handlers observe everything;
2. At the assemblage level, handlers observe only entities that belonged to that assemblage's composition at the moment the event fired, making it trivial to wire factory-specific behavior **without any filtering logic inside the handler**.

There are no surprises: **handlers are wired once**, fire synchronously after the mutation is already committed to world state, and the only teardown is `Reset()` or `world.Dispose()`.&#x20;

#### Entity Events

`World.EntitiesEvents` exposes six subscription points:

```csharp
/* 
    Fires when an entity is created via an assemblage; 
    composition already in place
*/
world.EntitiesEvents.SubscribeOnCreated(static (entityAssembled) =>
{
    /* 
     entityAssembled.Entity,
     entityAssembled.Has<T>(),
     entityAssembled.Is(...),
     entityAssembled.Satisfies(...)
    */
});

/* Fires when any component is added to any entity */
world.EntitiesEvents.SubscribeOnAnyComponentAdded(static (entityModified) =>
{
    /* Broad: fires for every component type */
});

/* 
    Fires when a specific component type is added;
    preferred over the broad subscription 
*/
world.EntitiesEvents.SubscribeOnComponentAdded<Stunned>(static (entityModified) =>
{
    /* Fires only when Stunned is added */
});

/* 
    Fires when any component is removed from any entity;
    Does not fire when the removal leaves the entity 
    with no components... OnGhoulified fires instead!
*/
world.EntitiesEvents.SubscribeOnAnyComponentRemoved(static (entityModified) =>
{
    /* Broad: fires for every component type */
});

/*
    Fires when a specific component type is removed;
    Also does not fire when the removal leaves the entity 
    with no components!
*/
world.EntitiesEvents.SubscribeOnComponentRemoved<Stunned>(static (entityModified) =>
{
    /* Fires only when Stunned is removed */
});

/*
    Fires when an entity is destroyed and its slot is eligible for reuse;
    components and relations are already cleaned up by the time this fires!
*/
world.EntitiesEvents.SubscribeOnGhoulified(static (entityGhoulified) =>
{
    /* entityGhoulified.Had<T>(), entityGhoulified.Was(...) */
});
```

Per-component-type subscriptions via `SubscribeOnComponentAdded<T>` and `SubscribeOnComponentRemoved<T>` **are preferred over the broad variants when only one component type is relevant**.

#### Per-Assemblage Events

Each assemblage owns a `EntityEvents` hub, which mirrors `World.EntitiesEvents` exactly.&#x20;

Handlers fire for entities whose composition matched that assemblage's archetype immediately before the mutation occurred, this applies to: `OnComponentAdded`, `OnComponentRemoved`, and `OnGhoulified` .

```csharp
dwarf.EntityEvents.SubscribeOnCreated(static (entityAssembled) => { });
dwarf.EntityEvents.SubscribeOnComponentAdded<Stunned>(static (entityModified) => { });
dwarf.EntityEvents.SubscribeOnComponentRemoved<Stunned>(static (entityModified) => { });
dwarf.EntityEvents.SubscribeOnGhoulified(static (entityGhoulified) => { });
```

* `OnCreated` fires on `dwarf.EntityEvents` simply because **the entity was just created through that assemblage**;
* `OnComponentAdded` fires on `dwarf.EntityEvents` if the entity **was** a dwarf immediately before the addition transitioned it out: "*an instance of this assemblage just grew beyond its composition*";
* `OnComponentRemoved` fires on `dwarf.EntityEvents` if the entity **was** a dwarf immediately before the removal transitioned it out: "*an instance of this assemblage just lost part of its composition*";
* `OnGhoulified` fires on `dwarf.EntityEvents` if the **entity's final composition matched the dwarf** composition: "*an instance of this assemblage was just destroyed*"*.*

`SubscribeOnAnyComponentAdded` and `SubscribeOnAnyComponentRemoved` are also available on `Assemblage.EntityEvents`, following the same previous-archetype firing semantics as their per-component-type counterparts.

#### Relation Events

`World.RelationsEvents` exposes four subscription points:

```csharp
/* Fires when any relation of any type is added */
world.RelationsEvents.SubscribeOnAnyRelationAdded(static (relationModified) =>
{
    /* Broad: fires for every relation type */
});

/* Fires when a specific relation type is added */
world.RelationsEvents.SubscribeOnRelationAdded<ParentOf>(static (relationModified) =>
{
    /* Fires only when a ParentOf relation is added */
});

/* Fires when any relation of any type is removed */
world.RelationsEvents.SubscribeOnAnyRelationRemoved(static (relationModified) =>
{
    /* Broad: fires for every relation type */
});

/* Fires when a specific relation type is removed */
world.RelationsEvents.SubscribeOnRelationRemoved<ParentOf>(static (relationModified) =>
{
    /* Fires only when a ParentOf relation is removed */
});
```

#### Event Payloads

All payloads are `ref struct`s, they **alias world state directly** and **cannot be stored beyond the handler's scope**.

Using the payload's methods,  `Has<T>()`, `Had<T>()`, `Is()`, `Satisfies()`, `Was(),` is the right tool for composition analysis inside a handler. The payload already holds a direct reference to the resolved archetype state; **the contract methods are a straight lookup against it, with no dereference**.

`EntityAssembled` delivered to `OnCreated` and carries the newly created entity:

* `Has<TComponent>()`: returns `true` if the entity has the component;
* `Is<TAssemblage>(assemblage)`: returns `true` if the entity matches exactly the assemblage composition;
* `Satisfies<TAssemblage>(assemblage)`: returns `true` if the entity's composition is a superset of the assemblage's composition.

`EntityModified` delivered to `OnAnyComponentAdded` and `OnAnyComponentRemoved`, carries the entity whose composition changed along with both previous and post-change archetypes:

* `Type`: the runtime type of the component that was added or removed;
* `Has<TComponent>()`: returns `true` if the entity has the component **after** the change;
* `Had<TComponent>()`: returns `true` if the entity had the component **before** the change;
* `Is<TAssemblage>(assemblage)`: returns `true` if the entity **matches exactly** the assemblage composition after the change;
* `Satisfies<TAssemblage>(assemblage)`: returns `true` if the entity's composition **is a superset of the assemblage's composition after the change**;
* `Was<TAssemblage>(assemblage)`: returns `true` if the entity **matched exactly the assemblage composition before the change**;
* `As<TEnum>()`: classifies the component type via a user-defined `enum`.

`EntityGhoulified` delivered to `OnGhoulified`. Components and relations are already cleaned up by the time this fires:

* `Had<TComponent>()`: returns `true` if the entity **had** the component before destruction;
* `Was<TAssemblage>(assemblage)`: returns `true` if the entity **matched exactly the assemblage composition before destruction**.

`RelationModified` delivered to relation events, carries both endpoints of the relation that changed:

* `Type`: the runtime type of the relation that was added or removed;
* `Owner`: the entity owning the relation;
* `Target`: the entity the relation points at;
* `As<TEnum>()`: classifies the relation type via a user-defined `enum`.

#### StructEnum: `[Includes<T>]` and `[Matches<T>]`

`As<TEnum>()` on `EntityModified` and `RelationModified` **maps the affected component or relation type to a user-defined** `enum` **value**, letting handlers classify what changed without branching on `Type` directly.

It is strongly **recommended to reserve the default** `enum` **value**, typically `0` , as an explicit invalid sentinel. `As<TEnum>()` returns `default` for any type that has no mapping, so a named `Invalid = 0` or `None = 0` **makes unmapped cases unambiguous at a glance**.

The mapping is declared on the `enum` itself **using one of two mutually exclusive attribute** styles:

```csharp
/*
    Matches<T>: strict one-to-one... 
    each weapon type maps to exactly one enum value
*/
public enum WeaponKind
{
    Invalid = 0,
    [Matches<Axe>]        Axe,
    [Matches<Sword>]      Sword,
    [Matches<Spear>]      Spear,
    [Matches<Crossbow>]   Crossbow,
}

/*
    Includes<T>: many-to-one...
    multiple spell types share a damage school bucket
*/
public enum SpellSchool
{
    Invalid = 0,
    [Includes<Fireball>][Includes<FireRune>]         Fire,
    [Includes<IceSpike>][Includes<WallOfFrost>]      Frost,
    [Includes<LightningBolt>][Includes<Thunderbolt>] Shock,
}
```

Mixing `[Includes<T>]` and `[Matches<T>]` on the same `enum` **throws at first access**. An `enum` with neither attribute **is valid** and `As<TEnum>()` returns `default` for any input.

```csharp
world.EntitiesEvents.SubscribeOnAnyComponentAdded(static (entityModified) =>
{
    switch (entityModified.As<WeaponKind>())
    {
        case WeaponKind.Axe:
            /* Play axe equip sound... */
            break;
        case WeaponKind.Sword:
            /* Play sword equip sound... */
            break;
        case WeaponKind.Spear:
            /* Play spear equip sound... */
            break;
        case WeaponKind.Crossbow:
            /* Play crossbow equip sound... */
            break;
        default:
            /* WeaponKind.Invalid or unmapped component, ignore */
            break;
    }

    /* Pattern matching is also a good call ;)... */
});
```

#### Reset

`Reset()` is the **only teardown mechanism per hub**. It detaches every subscriber from every event on that hub, including all per-component and per-relation buckets:

```csharp
world.EntitiesEvents.Reset();
world.RelationsEvents.Reset();
dwarf.EntityEvents.Reset();
```

To tear down all event hubs at once, world-level and every per-assemblage hub, call `world.Dispose()`:

```csharp
world.Dispose();
```

Wire handlers once at world construction; call `Reset()` for selective teardown or `world.Dispose()` when shutting down the simulation entirely.

#### Static Handlers

Every payload carries a `World` reference, and since **acting on the world is by far the most common thing a handler needs to do**, this alone covers the vast majority of cases where a non-static lambda would otherwise have captured an outer variable.&#x20;

Declaring handlers as `static` **prevents accidental closure allocation**:

```csharp
/* Non-static, allocate */
world.EntitiesEvents.SubscribeOnCreated((entityAssembled) =>
{
    outsideWorld.TryAddComponent<Poison>(entityAssembled.Entity);
});

/* Static... world comes from the payload, no capture, no allocation */
world.EntitiesEvents.SubscribeOnCreated(static (entityAssembled) =>
{
    entityAssembled.World.TryAddComponent<Blessing>(entityAssembled.Entity);
});
```

The `static` modifier: **a static lambda that accidentally captures an outer variable is a compile error, not a runtime surprise**. This makes the performance characteristic a guarantee rather than a convention.

**Capturing is sometimes legitimate**. An assemblage reference needed for `.Is()` or `.Satisfies()` checks is a valid reason to use a non-static lambda, **but that should be a conscious**, deliberate choice. In practice, `As<TEnum>()` **covers most type-classification needs without capturing anything**, **and the world on the payload covers most mutation needs**. Reach for `static` by default; **capture only when you have a clear reason**.
