Aetheria/docs/cultmesh-feature-implementation-guide.md
2026-06-30 19:14:49 +01:00

16 KiB

Implementing Aetheria Features With CultMesh

Date: 2026-06-28

This guide describes the target developer experience for adding Aetheria state with CultMesh. If the workflow grows into a chain of facades, sessions, projectors, adapters, and surface builders, stop and simplify the state API. That chain is heretek now: it means the feature is compensating for a missing typed document, query, operation, pointer, native view, or generated handle.

The rule is blunt:

small UI-only value: derive it inline from already accessible typed state
named UI-only derived view: define the typed derived document, then read it
simulation feature: define one canonical typed document, let daemon authority mutate it, consume the same document everywhere
hot rendering/physics: use SoA, not object-graph UI documents

CultMesh owns document identity, sync, prediction, reconciliation, and reactive access. Aetheria code should read like it is using game state, not walking protocol layers.

Canonical Document Rule

The default shape is one shared typed document contract.

AetheriaRuntimeFooDocument
  -> daemon authority mutates/publishes it
  -> Unity/Electron/tools read the same document type
  -> authorized clients write intent or predictions through the managed handle

Do not split a feature into "daemon truth" and a separate "CultMesh typed state" unless the second shape is intentionally different. The assembly is shared, so the document the daemon owns is also the document clients receive. Across runtimes, callers should feel like they are grabbing a handle on typed state and either reading it for display or writing to it for client input.

A second document is justified only when it earns a distinct job:

  • hidden-information filtering;
  • expensive or shared derived state;
  • viewport/windowed selection;
  • SoA/native render or physics layout;
  • lossy presentation summaries;
  • UI surface documents, including nested CultUI embeddedDocuments slots when a parent surface reserves layout for a separately synced child document;
  • compatibility mirrors with a named removal stage.

Projection is not the normal way to make state client-visible. Projection is an exception for a different shape.

Nested UI does not loosen this rule. If a daemon-owned panel needs an embedded dropdown, item inspector, scene insert, or drag/drop target surface, the parent surface carries a surface.slot with embeddedDocuments, and each child is resolved as its own managed CultMesh document. The renderer lowers the retained tree and wires commands; it does not invent a Unity-only state model for the child surface.

Discovery and parity live with the shared Eve contract, not in an Aetheria-only helper. Any CultUI feature that affects nested surfaces must update the canonical fixture at E:\Projects\Eve\web\fixtures\cultui-embedded-surface.json or an equivalent versioned fixture, and it must be discoverable from E:\Projects\Eve\docs\parity-testing-harness.md. The active runtime checks are the browser DSL test, Eve parity harness, Flutter parity smoke, Unity build, and Aetheria verifier scripts. Android/Kotlin and Fensalir must remain listed in the matrix with explicit pending/unsupported status until they become active GUI CultUI renderers. Do not let an untested target disappear into politeness.

The Ergonomic Bar

UI-only state should take one or two steps.

Use one step when the value is small and already derivable from state the caller has:

var currentDocking = client.State.Reactive<AetheriaRuntimeCurrentDockingDocument>();
var bayName = currentDocking.Current?.DockingBayName ?? "";

Use two steps when the derived view is shared, non-trivial, or should have a stable schema:

1. Define the typed derived document.
2. Read it from Unity as a managed reactive typed document.

Simulation state should take two or three steps:

1. Define the canonical typed document in the shared runtime package.
2. Let the daemon mutate/publish that document as the authority for the Verse.
3. Read or modify the same managed document from clients according to authority policy.

Anything beyond that needs a clear reason. Performance-sensitive entity rendering uses SoA because it is a different data shape, not because ordinary UI state should become ceremonial.

What CultMesh Should Hide

Feature authors should not manually handle:

  • document routing keys;
  • protocol envelopes;
  • schema lookup;
  • cache hydration;
  • sync subscriptions;
  • prediction dispatch;
  • reconciliation smoothing;
  • session wrapper lifetime around a single document;
  • joins against raw daemon frame rows in UI code.

Those are CultMesh/runtime concerns. Unity callers should hold either a CultMeshReactiveDocument<TDocument> or a named typed handle that returns one.

What Not To Add

Do not add new single-document wrappers such as:

AetheriaRuntimeSomeStateSession

when the wrapper only stores a document, exposes Current, and calls Dispose(). That is a managed reactive document with a worse name.

Do not add feature-specific access paths like this:

daemon document -> session -> facade -> projector -> adapter -> surface builder -> caller

That shape preserves reconstruction-era scaffolding. It does not express the game. At most, one boundary adapter may exist at an actual boundary: Unity GameObject presentation, Eve/CultUI lowering, legacy import, persistence, or native view ownership. If two or more layers appear in a row just to obtain one domain value, stop and move the missing primitive into CultMesh/CultLib or the generated Aetheria handle surface.

Choosing The State Shape

Use inline derivation when:

  • the value is UI-only;
  • the derivation is local and obvious;
  • the source typed document is already available to the caller;
  • no other system needs the result as a named state surface.

Use a derived typed document when:

  • multiple callers need the same derived state;
  • the state is useful to Unity, RTS, tests, tooling, or the website simulator;
  • the derivation joins multiple source documents;
  • the result needs a stable schema or cross-runtime binding;
  • the derived view should update reactively as daemon state changes.

Use a canonical simulation document when:

  • the value affects game rules;
  • input can mutate it;
  • it needs authority, replay, persistence, or reconciliation;
  • the daemon must validate operations before publishing the next state.

Use a derived typed document when the caller needs a different state shape, not merely because a client needs to see daemon-owned state.

Use SoA/native views when:

  • Unity jobs, Burst, Ymir, or rendering need large columnar data;
  • row count and per-frame access make object graphs the wrong format;
  • the data is a hot view over canonical simulation state rather than a UI document.

One-Step UI Derivation

For a small UI-only value, read existing typed state directly.

using GameCult.Aetheria.State.Verse;
using GameCult.Mesh;

public sealed class DockingBadge : IDisposable
{
    private readonly AetheriaClient _client;
    private CultMeshReactiveDocument<AetheriaRuntimeCurrentDockingDocument> _docking;

    public DockingBadge(AetheriaClient client)
    {
        _client = client;
    }

    public string Text
    {
        get
        {
            _docking ??= _client.State
                .Reactive<AetheriaRuntimeCurrentDockingDocument>();
            var current = _docking.Current;
            return current?.IsDocked == true ? current.DockingBayName : "";
        }
    }

    public void Dispose()
    {
        _docking?.Dispose();
    }
}

The caller does not need a custom facade, session, projector, or adapter. If the inline derivation becomes shared or hard to read, promote it to a derived document.

Two-Step Derived UI Document

Use this for shared presentation state such as player HUD, docking summary, commander wave status, station service summary, or Starbridge support alerts.

Before adding this document, ask whether the UI can read the canonical simulation document directly. If it can, do that. The derived document exists only when the UI shape is genuinely derived, filtered, windowed, or shared enough to deserve its own schema.

Step 1: Define The Derived Document

Put the document type in the shared runtime package so every runtime sees the same schema:

Packages/org.gamecult.aetheria.state/Runtime

Example:

[MessagePackObject]
[CultDocument(AetheriaRuntimeDaemonSchemas.ZoneDefenseStatus)]
public sealed class AetheriaRuntimeZoneDefenseStatusDocument
{
    [Key(0)]
    public int ZoneIndex { get; set; } = -1;

    [Key(1)]
    public double BaseShieldRatio { get; set; }

    [Key(2)]
    public int IncomingHostileCount { get; set; }

    [Key(3)]
    public int ActiveTurretCount { get; set; }

    [Key(4)]
    public string AlertLevel { get; set; } = "";
}

Then make it available through generic managed state access:

using var status =
    client.State.Reactive<AetheriaRuntimeZoneDefenseStatusDocument>();

The exact registration/publishing code belongs in the runtime plumbing. It should be generated or centralized. The feature author should not have to write five separate access classes, wrapper methods, sessions, or facades for one derived state shape.

Step 2: Read It From Unity

Unity holds the typed reactive document and reads Current.

using GameCult.Aetheria.State.Verse;
using GameCult.Mesh;

public sealed class ZoneDefenseHud : IDisposable
{
    private readonly AetheriaClient _client;
    private CultMeshReactiveDocument<AetheriaRuntimeZoneDefenseStatusDocument> _status;

    public ZoneDefenseHud(AetheriaClient client)
    {
        _client = client;
    }

    public void Refresh()
    {
        _status ??= _client.State
            .Reactive<AetheriaRuntimeZoneDefenseStatusDocument>();
        var status = _status.Current;
        if (status == null)
            return;

        DrawShield(status.BaseShieldRatio);
        DrawIncoming(status.IncomingHostileCount);
        DrawAlert(status.AlertLevel);
    }

    public void Dispose()
    {
        _status?.Dispose();
    }
}

That is the whole derived UI document workflow. If more steps are required, they belong in CultMesh/runtime infrastructure, not in every feature.

Two Or Three-Step Simulation Feature

Use this when the feature changes gameplay: heat support, repair drones, station construction, scenario progression, commander infrastructure, or wave director state.

Step 1: Define The Canonical Document

Simulation truth is the shared typed document. The daemon owns authority over that document, but the document contract is not daemon-private.

[MessagePackObject]
[CultDocument(AetheriaRuntimeDaemonSchemas.StationSupport)]
public sealed class AetheriaRuntimeStationSupportDocument
{
    [Key(0)]
    public double CoolingReserve { get; set; }

    [Key(1)]
    public double RepairNaniteReserve { get; set; }

    [Key(2)]
    public int ActiveSupportDrones { get; set; }
}

Step 2: Let The Daemon Own Authority

The daemon validates operations, applies ticks, and publishes the canonical document into the Verse. Unity does not own gameplay truth, but Unity does not need a separate client-facing copy either. CultMesh handles sync. If the client has prediction authority, local changes to the managed document are predictions; reconciliation corrects them without requiring feature code to manually shuttle deltas.

Step 3: Read Or Mutate Through The Managed Handle

Unity reads the same document through the managed typed handle:

var support = client.State.Reactive<AetheriaRuntimeStationSupportDocument>();
var cooling = support.Current?.CoolingReserve ?? 0;

When the client does not have direct simulation authority, it submits a typed operation:

client.Operations.Submit(new AetheriaRuntimeDeploySupportDroneOperation
{
    ZoneIndex = currentZone,
    TargetEntityIndex = targetEntity,
    DroneLoadoutId = selectedLoadout
});

The daemon validates and applies the operation. The next published document is the authoritative result.

When the client has prediction or simulation authority, the ergonomic target is even simpler: modifying the managed reactive document records a prediction, debounced by the update frame, and CultMesh routes/reconciles it according to the Verse authority policy.

SoA For Hot Paths

Use SoA when Unity needs fast columnar access, not when a UI needs a label.

Good SoA candidates:

  • visible entity transforms;
  • render splats;
  • physics bodies;
  • sensor/contact rows for thousands of entities;
  • Ymir/Burst job inputs.

Bad SoA candidates:

  • current docking bay;
  • selected object details;
  • HUD status;
  • player settings;
  • station service summary.

SoA should be a named high-performance view over canonical simulation state. It should not force ordinary presentation code to understand frame slabs, column handles, or native buffer ownership.

Reconnection

State needed to reconstruct a player after a client crash must be in daemon or scenario/session CultMesh documents, not Unity locals.

Persist or republish:

  • player identity and seat;
  • current entity or cockpit module;
  • docked station and bay;
  • inventory/loadout references;
  • scenario progress;
  • commander infrastructure state;
  • active operations that survive reconnect;
  • escape pod/proxy ship state when ejected.

On reconnect, Unity should reacquire typed documents and rebuild presentation from Current. Reconnection should not require replaying UI actions.

Tests

For inline UI derivation, test the source document and the small consuming component when behavior is non-trivial.

For derived UI documents, test:

  • derivation from representative daemon state;
  • empty/default state;
  • reactive update after source state changes;
  • Unity caller reads the typed document directly.

For nested CultUI surfaces, test every target runtime listed in the Eve parity matrix:

  • browser DSL lowering discovers and renders embeddedDocuments;
  • Eve parity harness preserves slots, child document identity, and command wiring;
  • Flutter parity smoke renders the same nested surface contract;
  • Unity build/lowering binds the parent surface and embedded child documents without local projector state;
  • Aetheria verifier scripts prove the game daemon publishes the discoverable CultMesh surface document;
  • pending runtimes such as Android/Kotlin and Fensalir remain explicitly marked pending/unsupported until they have a real renderer test.

For simulation features, test:

  • daemon tick/application mutates the canonical typed document;
  • typed operation validation accepts and rejects correctly;
  • clients read the same shared document type the daemon publishes;
  • prediction/reconciliation behavior when the runtime supports local authority;
  • reconnect can reconstruct the required player state.

For SoA, test:

  • row identity and generation;
  • bounds/ownership;
  • empty views;
  • large views;
  • native buffer lifetime.

Verifier Rules

Verifier rules should guard the desired ergonomic shape. They should forbid reconstruction-era detours.

Good verifier expectations:

Unity caller owns CultMeshReactiveDocument<TDocument>
Unity caller reads .Current
Unity caller uses client.State.Reactive<TDocument>()
Unity caller submits typed operations for mutations
authorized caller mutates managed typed state as prediction/input
hot renderer uses named SoA view

Bad verifier expectations:

Unity caller owns AetheriaRuntime*Session for one document
Unity caller calls ObserveFeatureName() wrapper only to read Current
UI caller reads raw daemon frames and joins rows
feature adds facade/projector/adapter/surface-builder chain
feature introduces multiple translation layers to get one typed value

The verifier should make the codebase harder to regress into ceremony, not freeze the ceremony in place.

Final Check

Before calling a CultMesh feature ergonomic, answer these questions:

  • Can a UI-only inline value be implemented in one local derivation?
  • Can a shared UI view be implemented by defining the document and reading it from Unity?
  • Can a simulation feature be explained as one canonical typed document plus authority policy and typed operations?
  • Does Unity code read domain state instead of protocol plumbing?
  • Is SoA reserved for hot paths?
  • Did the verifier protect the clean path instead of the old wrappers?

If the answer is no, fix the access shape before adding more feature code.