> 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/entities-and-composition/relations.md).

# Relations

Directed bilateral connections between entities

A relation is a directed connection from **an owner entity to one or more target entities**. The owner holds the relation, one per relation type, containing its list of targets. Relations are not components; they live in their own graph, **maintained bilaterally** by the framework.

Relation types **must also be strictly blittable**. The framework enforces this at registration time and throws if the type fails the check.

#### The `[Relationship]` Attribute

Every relation type **must be decorated** with `[Relationship]`:

```csharp
[Relationship]
public struct ParentOf { }
```

The attribute serves two purposes.&#x20;

1. It is an explicit declaration of intent, a struct decorated with `[Relationship]` **communicates its role at a glance**, the same way an assemblage declaration communicates the meaning of a composition. The framework throws if the attribute is missing because **an undeclared relation is considered an incomplete declaration of intent**.&#x20;
2. It acts as a detection signal used internally by the framework to identify relation types and automatically trigger their registration in certain situations.

#### Tag vs Evaluated

Whether a relation is a tag or an evaluated relation is determined automatically by whether the struct has fields.

A **tag relation** is **fieldless**. The type itself carries the meaning: `ParentOf` doesn't need a payload, since the existence of the relation is the information. An **evaluated relation** carries a **per-target data payload**, accessible and mutable per owner and target pair.

A relation that carries data **can also be used as a component**. The two registries are independent, a struct can participate in both without conflict.

```c#
/* Tag: no fields, the relation itself is the information */
[Relationship]
public struct ParentOf { }

/* 
 Evaluated: carries a per-target payload; e.g: a dwarf 
 entity carries multiple item entities, each with a weight
*/
[Relationship]
public struct Carries { public float Weight; }
```

The framework **stores them differently** at the type level. Tag relations use a targets-only array, **no payload storage is allocated at all**. Evaluated relations **carry a second array aligned to the targets**, so each target and its payload share the same dense index. Both layouts are **cache-coherent**; the choice between them is purely about whether your relation needs to carry data.

The API enforces the split strictly. Calling `TryAddTagRelation` with an evaluated relation throws, and vice versa.

#### Bilateral O(1)

**Every relation is recorded on both sides atomically**. "*Who do I point at?*" and "*Who points at me*?" both run in **constant time**. There is no degenerate query direction.

#### Thread Safety

Concurrent reads and writes from worker threads **are safe** **for all relation operations**. When iterating and mutating relation values from multiple threads, prefer `QueryRelation`, it holds the relation's update lock for the duration of the iteration.

#### Ghoulification

When an entity is ghoulified, the framework **automatically tears down every relation it participates in on either side**. If the entity is an owner, all its outgoing relations are removed. If it is a target, all external links pointing at it are unwound. **The bilateral graph is left consistent with no orphaned relations or dangling external links**.

#### Events

Relations have their own event bus on the world: `World.RelationsEvents`. It supports broad subscriptions for any relation added or removed, as well as per-relation-type filtering. See [Events](/theia-ecs/events.md) for the full relation event API.

#### Serialization

The full relation graph is included in world serialization; both tag and evaluated relations, with their payloads. See [Serialization](#serialization) for details.

#### Relations API

Adding a tag or evaluated relation is easy as:

```csharp
world.TryAddTagRelation<ParentOf>(gloin, gimli);

world.TryAddEvaluatedRelation<Carries>(
    gimli, 
    ironIngot, 
    new Carries() { Weight = 2.5f }
);
```

Both return `false` if either entity is a ghoul, owner equals target, or the relation already exists.

And, for removal:

<pre class="language-csharp"><code class="lang-csharp">/* 
 Remove all Carries relations from Gimli; e.g: clears 
<strong> the entire inventory
</strong>*/
world.TryRemoveRelation&#x3C;Carries>(gimli);

/* Remove the specific relation from Gimli to iron ingot */
world.TryRemoveRelation&#x3C;Carries>(gimli, ironIngot);
</code></pre>

Unlike adding, removal **makes no distinction between tag and evaluated relations**. The same `TryRemoveRelation` overloads handle both.

There are a couple of methods for query-related questions:

```csharp

world.HasRelation<Carries>(gimli); /* At least one relation of this type? */                         
world.CountRelations<Carries>(gimli); /* How many items Gimli is carrying? */
world.IsRelatedTo<Carries>(gimli, ironIngot); /* Does Gimli carries the ironIngot? */
world.HasExternalLinks<Carries>(treeLog); /* Is the tree log being carried by any dwarf? */
world.CountExternalLinks<Carries>(treeLog); /* How many dwarves are carrying the treeLog? */

/* Span over all targets; aliases underlying storage, not thread-safe */
ReadOnlySpan<Entity> inventory = world.GetRelations<Carries>(gimli);

/* Span over all owners pointing at this entity */
ReadOnlySpan<ExternalLink> carriers = world.GetExternalLinks<Carries>(treeLog);
```

`GetRelations` and `GetExternalLinks` return spans that alias the underlying storage directly. **Concurrent structural changes while the span is in use produce undefined behavior**; check via `HasRelation` or `HasExternalLinks` first and ensure no other thread is modifying the relation for the span's lifetime.

`ExternalLink` implicitly converts to `Entity`, so iteration is ergonomic:

```csharp
foreach (ExternalLink link in world.GetExternalLinks<Carries>(treeLog))
{
    Entity carrier = link; /* Implicit conversion */
}
```

Evaluated relations have two additional get methods:

```csharp
/* 
 Reference to the payload on a specific relation, 
 mutations persist in storage 
*/
ref Carries carries = ref world.GetEvaluatedRelation<Carries>(gimli, ironIngot);
carries.Weight += 1.0f;

/* Index-aligned spans of targets and payloads */
EntityEvaluatedRelations<Carries> inventory = world.GetEvaluatedRelations<Carries>(gimli);
```

Both `GetEvaluatedRelation` and `GetEvaluatedRelations` alias underlying storage and carry the same thread-safety responsibility as `GetRelations`.

`QueryRelation` and `QueryEvaluatedRelation` **follow the same struct callback pattern** as [queries](/theia-ecs/execution/queries.md). Implement `IQueryRelation` or `IQueryEvaluatedRelation<TRelation>` on a struct or `ref struct` and pass it by `ref`:

```csharp
/* 
    Tag or evaluated relation iteration;
    receive each target entity
*/
private ref struct InventoryForEach : IQueryRelation
{
    public readonly World World;

    public void Execute(Entity item)
    {
        /* Item is each entity Gimli carries */
    }
}

InventoryForEach inventoryForEach = new InventoryForEach() { World = world };

/* Callback iteration; holds the relation's update lock for the duration... thread-safe! */
world.QueryRelation<Carries, InventoryForEach>(gimli, ref inventoryForEach);
```

For evaluated relations, `IQueryEvaluatedRelation<TRelation>` also supplies the per-link payload by `ref`, **mutations persist in storage**:

```csharp
/* 
    Evaluated relation iteration;
    receives each target entity and its payload by ref
*/
private ref struct WeightForEach : IQueryEvaluatedRelation<Carries>
{
    public float TotalWeight;

    public void Execute(Entity item, ref Carries carries)
    {
        TotalWeight += carries.Weight;
    }
}

WeightForEach weightForEach = new WeightForEach();
/* 
 Callback iteration with payload passed by reference for in-place mutation;
 holds the relation's update lock for the duration... thread-safe!
*/
world.QueryEvaluatedRelation<Carries, WeightForEach>(gimli, ref weightForEach);

/* weightForEach.TotalWeight now holds the sum of all carried weights */
```

The **relation API is thread-safe**, with the exception of methods that return spans aliasing underlying storage. When multiple jobs are querying relations concurrently via `QueryRelation` or `QueryEvaluatedRelation`, those methods hold the relation's update lock for the duration of the iteration; **any attempt to add or remove relations from another thread during that window throws since it would cause a structural change during iteration**. In those cases, use the deferred variants to stage the changes for the next flush instead. See [Deferred Commands](/theia-ecs/entities-and-composition/deferred-commands.md) for the full deferred relation API.
