203 lines
9.9 KiB
Markdown
203 lines
9.9 KiB
Markdown
# Retained Session Contract
|
|
|
|
## Objective
|
|
|
|
One named Ymir session owns one retained Box3D world. A product may restore the
|
|
world once, issue explicit mutations, step it, query it, and consume typed
|
|
Box3D contact facts. A body snapshot is output and checkpoint material; it is
|
|
not an implicit command language.
|
|
|
|
## Current Mechanism
|
|
|
|
`YmirSimulator` is an isolated snapshot compatibility API. It creates a native
|
|
session, synchronizes a complete body list, steps once, projects the result,
|
|
and disposes the session. Its private compatibility helper uses that complete
|
|
list, so omission can delete a body and stale input can teleport or overwrite
|
|
solver velocity inside the isolated call. Its legacy contact output merges
|
|
current manifolds, begin events, and hit events into one pair-deduplicated list
|
|
and omits End.
|
|
|
|
The public `YmirSession` does not use that mechanism for ordinary retained
|
|
steps. The compatibility path is not the retained session contract.
|
|
|
|
## Owner
|
|
|
|
- One public Ymir session serializes commands for one named world.
|
|
- Its native Box3D `b3World` exclusively owns integration, collision,
|
|
broadphase, queries, solver state, and contact lifecycle.
|
|
- Ymir owns stable GameCult ids, body generations, session revision, step
|
|
sequence, deterministic projection, fact identity, and command receipts.
|
|
- The product owns session lifetime and gameplay policy. Aetheria maps one
|
|
`(RunId, ZoneIndex)` lifetime to one Ymir session.
|
|
|
|
## Target Inputs
|
|
|
|
The retained port accepts narrow operations with explicit meaning:
|
|
|
|
- spawn a body, rejecting an existing id;
|
|
- remove a body, rejecting an unknown id;
|
|
- teleport a body;
|
|
- set linear and angular velocity;
|
|
- configure body shape, mass, static/kinematic/dynamic type, and restitution;
|
|
- apply one-shot force or torque;
|
|
- step with a fixed delta, substep count, and typed fields;
|
|
- dispose the session.
|
|
|
|
The current public slice implements those mutation and step operations plus
|
|
revision-checked circle overlap and cast queries. A query observes the current
|
|
retained body projection under the session lock and accepts an explicit set of
|
|
stable candidate body ids. The candidate set is product-owned eligibility;
|
|
Box3D still owns the overlap/cast geometry and numeric result.
|
|
Bullet, collision-filter, and field-participation profiles are currently
|
|
spawn/restore definitions. Retained `Configure` changes radius, mass,
|
|
static/kinematic/dynamic type, and restitution only. Changing the profile
|
|
requires explicit remove-and-spawn until a typed profile command exists.
|
|
|
|
Restore/import is the only path allowed to accept a complete world snapshot.
|
|
There is no `Upsert`, generic patch bag, or omission-based removal.
|
|
|
|
## Outputs
|
|
|
|
Each accepted mutation produces a receipt that names the session generation,
|
|
command id, before revision, and after revision. A step additionally returns:
|
|
|
|
- authoritative Box3D body state;
|
|
- the new step index;
|
|
- typed `Begin`, `Hit`, and `End` facts;
|
|
- stable fact ids and contact-episode ids;
|
|
- the accepted step configuration through its typed command.
|
|
|
|
The current library validates the native Box3D version and ABI before session
|
|
creation but does not repeat those facts in every Step result.
|
|
|
|
Facts canonicalize bodies by stable id and flip normals when canonicalization
|
|
swaps endpoints. Their public order is body pair, contact episode, kind, then
|
|
within-kind ordinal. This is stable publication order, not invented substep
|
|
chronology.
|
|
|
|
## Derived State
|
|
|
|
- Box3D handles are process-local and never cross the contract.
|
|
- DTO and SoA body arrays are projections.
|
|
- `gamecult.ymir.world_state.v2` is an inspection/migration snapshot, not a
|
|
retained-session checkpoint.
|
|
- Receipt and contact-episode ledgers are session metadata, not physics law.
|
|
- Aetheria cargo, damage, loot, feedback, and scoring remain gameplay state
|
|
derived from Ymir facts.
|
|
|
|
The replay checkpoint preserves the logical session generation, complete typed
|
|
command journal, receipt ledger, body-generation counter, world projection, and
|
|
active contact episodes. Restore creates a fresh native Box3D world and replays
|
|
that journal through the checkpoint boundary before facts may leave the
|
|
session. The world and episode records verify replay; they do not replace it or
|
|
infer contact continuity from body proximity.
|
|
|
|
`gamecult.ymir.session_checkpoint.replay.v1` is encoded by the bounded binary
|
|
`YmirSessionCheckpointCodec`. The checkpoint carries the pinned Box3D/ABI/
|
|
precision fingerprint and fails closed on incompatible provenance, corrupt or
|
|
trailing data, journal divergence, world divergence, or contact-lineage
|
|
divergence. A SHA-256 checksum binds session identity, provenance, journal,
|
|
world verifier, and active episodes. ABI v5 independently validates the exact
|
|
pinned native build id before opening a session. It contains no native Box3D
|
|
handle or contact key.
|
|
|
|
For incremental host persistence,
|
|
`gamecult.ymir.session_journal_chunk.v1` carries a contiguous command-journal
|
|
suffix and `gamecult.ymir.session_resume.v1` carries the bounded reconstruction
|
|
boundary. `CapturePersistence(persistedJournalEntryCount)` never repeats an
|
|
already acknowledged prefix. Restore orders chunks by their first entry,
|
|
requires exact coverage from zero through the descriptor count, and then uses
|
|
the same full replay and verifier path as a complete checkpoint. Both binary
|
|
forms are independently checksummed.
|
|
|
|
The cursor is an index within one session generation. Bounded persistence uses
|
|
an explicit generation rollover, never an inferred acknowledgement side effect.
|
|
`TryCreateCompactedPersistenceBaseline` may create a replacement session only
|
|
when the current journal is empty or ends in an accepted Step and Box3D reports
|
|
no active contact episodes. The replacement starts from the current body
|
|
projection and time with a fresh session generation, revision zero, an empty
|
|
receipt ledger, and an empty journal. The host atomically swaps ownership and
|
|
then disposes the prior session.
|
|
|
|
Active contacts, pending forces, or other unstepped mutations refuse rollover.
|
|
Those sessions retain replay reconstruction until a later quiescent boundary.
|
|
Ymir does not synthesize contact closure, serialize private Box3D handles, or
|
|
pretend a body snapshot can preserve solver contact state. Old generation
|
|
journals become audit history after the host commits the replacement boundary;
|
|
they are not inputs to the new generation's restore.
|
|
|
|
## Forbidden Writers
|
|
|
|
- recurring complete-body synchronization after restore;
|
|
- snapshot omission as body removal;
|
|
- transform or velocity setters hidden inside ordinary Step;
|
|
- managed contact lifecycle inference that replaces Box3D events;
|
|
- random fact ids or ids derived from manifold floats;
|
|
- Box3D callback order exposed as a public ordering guarantee;
|
|
- Unity callbacks, renderer state, or gameplay proximity checks deciding
|
|
contact truth.
|
|
|
|
## Shared Paths
|
|
|
|
Direct commands, agent commands, spawns, and checkpoint replay use the same
|
|
explicit session operations. Restore establishes a fresh native world through
|
|
those operations; it is not a parallel state writer.
|
|
Retained queries will observe a named revision without mutating it. Snapshot
|
|
reads cannot be fed back without an explicit restore, teleport, velocity, or
|
|
configuration operation.
|
|
|
|
## Contact Identity
|
|
|
|
A contact episode is identified by the canonical pair of body id plus body
|
|
generation and a monotonically assigned episode number. Begin and End share
|
|
that identity. A later re-contact creates a new episode. Fact identity derives
|
|
from session generation, step index, contact episode, kind, and deterministic
|
|
ordinal. Box3D contact and shape handles are never durable identity.
|
|
|
|
Body removal must close any active episode exactly once. Because Box3D End
|
|
events may reference destroyed shapes, the native boundary retains stable ids
|
|
until the event buffer has been drained.
|
|
|
|
Box3D publishes Begin, Hit, and End in separate arrays rather than one causal
|
|
sequence. The private ABI carries Box3D's stored transient contact key so Ymir
|
|
can correlate those arrays before assigning public episode identity. That key
|
|
is derived process state: it never appears in a public fact or checkpoint.
|
|
|
|
## Cut Line
|
|
|
|
1. Typed native mutation operations and step-owned lifecycle buffers: done.
|
|
2. Public managed retained session with no ordinary full synchronization:
|
|
done.
|
|
3. Revision-checked retained-session overlap and cast queries: done.
|
|
4. One embedded retained world and physical-payload session per Aetheria run
|
|
and zone: done.
|
|
5. Incremental journal chunks and bounded resume descriptors: done. Aetheria
|
|
stores them in a daemon-private CultCache; complete history does not ride in
|
|
public product frames. Quiescent generation rollover provides the compaction
|
|
primitive; host adoption and old-generation retention policy are separate.
|
|
6. For optional standalone hosting, add a session registry, generation-bearing
|
|
routed commands, idempotent Create, durable receipts, and CultMesh lowering.
|
|
|
|
The isolated `YmirSimulator` remains a compatibility API. It is never the
|
|
retained-world authority.
|
|
|
|
Mutation-generated End evidence is currently retained in the native pending
|
|
buffer and published by the next accepted Step. The host transaction cut
|
|
must make mutation plus Step one publication boundary, or return mutation facts
|
|
directly, before Ymir promises that removal followed by disposal publishes an
|
|
End fact.
|
|
|
|
## Verification
|
|
|
|
- A spawned body advances across steps without being resubmitted.
|
|
- Omission from unrelated commands cannot remove or reset a body.
|
|
- Force and torque affect exactly one accepted step.
|
|
- stale revisions and conflicting command ids cannot mutate the world.
|
|
- persistent touching emits one Begin, separation emits one End, and re-contact
|
|
creates a new episode.
|
|
- duplicate fact delivery cannot commit Aetheria cargo twice.
|
|
- different insertion orders produce the same semantic fact order.
|
|
- queries observe the requested retained revision and cannot inject bodies.
|
|
- disposed or stale session generations fail closed.
|
|
- restored continuation reproduces command receipts, bodies, active contact
|
|
identity, and subsequent Begin/End facts before authoritative publication.
|