acdream/docs/plans/2026-07-25-modern-runtime-slice-j3-object-table.md

270 lines
12 KiB
Markdown

# Modern runtime J3.4 — canonical object-table ownership
**Status:** COMPLETE
**Parent:** `2026-07-25-modern-runtime-slice-j3.md`
**Required production base:** `e937cc36df39cf6ea1eaba2f9c0243d1929da702`
**Prior closeout:** `docs/research/2026-07-25-slice-j3-3-exact-projection-store.md`
**Production commit:** `5ef8b5371d8990f0380acd939e71cd711289d429`
**Closeout:** `docs/research/2026-07-26-slice-j3-4-canonical-object-lifetime.md`
## Objective
Make one presentation-independent Runtime lifetime-group root own both:
- the canonical `RuntimeEntityDirectory`; and
- the existing canonical `ClientObjectTable`.
App, retained UI, interaction controllers, and the graphical projection host
must borrow those exact same instances. There must be no copied inventory
model, second object table, asynchronous delivery seam, extra input frame, or
change to retail event order.
This slice moves ownership and transaction boundaries, not the
`ClientObjectTable` implementation out of `AcDream.Core`. The type is already
presentation-free and has extensive Core conformance coverage.
## Current ownership inventory
At the required base:
- `LiveEntityRuntime` constructs its own `RuntimeEntityDirectory`.
- `GameWindow` constructs a public readonly `ClientObjectTable Objects`.
- App composition records pass that table to retained UI, selection,
interaction, magic, combat, paperdoll, appraisal, cooldown, and diagnostics.
- Runtime `LiveSessionEventRouter` already applies property, stack, inventory,
player-description, container, and appraisal traffic directly to the
borrowed table through `ObjectTableWiring`.
- App `LiveEntityHydrationController` applies CreateObject to the table only
after canonical Runtime create acceptance and revalidates
`CreateIntegrationVersion`.
- App `LiveEntityDeletionController` applies logical DeleteObject before the
exact App projection teardown suffix.
- App `LiveSessionResetManifest` clears the object table before clearing live
graphical projections.
- Optimistic moves, confirmations, rollback, stack updates, container
replacement, and every table event are synchronous on the current thread.
Retail behavior and citations already live in:
- `src/AcDream.Core/Items/ClientObjectTable.cs`;
- `src/AcDream.Core.Net/ObjectTableWiring.cs`;
- `docs/research/2026-07-13-retail-give-item-pseudocode.md`;
- the named-retail symbols cited beside `ACCObjectMaint::CreateObject`,
`ACCObjectMaint::DeleteObject`, and
`ACCWeenieObject::ServerSaysMoveItem`.
J3.4 does not reinterpret those algorithms.
## Fixed target
Add a presentation-free Runtime owner equivalent to:
```csharp
public sealed class RuntimeEntityObjectLifetime
{
public RuntimeEntityDirectory Entities { get; }
public ClientObjectTable Objects { get; }
}
```
The concrete name may change, but these invariants do not:
1. The owner constructs each canonical instance exactly once.
2. `LiveEntityRuntime` receives `Entities`; it may not construct a directory.
3. Runtime session routing receives `Objects` from the same owner.
4. App projection, UI, and interaction composition receive borrowed references
only.
5. Accepted CreateObject/DeleteObject object-table mutation is implemented by
the Runtime group and revalidates the exact canonical record/version.
6. Reset preserves the current order: object-table clear before graphical live
projection teardown, with completed stages never replayed.
7. Studio/sample-data object tables remain independent fixture owners and are
not part of the live session.
## Execution
### 1. Pin parity traces and construction identity
- Capture normalized traces for:
- initial player description and equipment manifest;
- item CreateObject add/update;
- same-incarnation refresh;
- new-generation replacement;
- property Int/Int64/String/Float/DataID updates;
- stack-size update;
- optimistic move, authoritative confirm, and rejection rollback;
- wield/unwield;
- ViewContents replacement and close;
- logical delete, GUID reuse, and session clear.
- Add a construction-identity test proving the router, App projection host,
retained UI source, and interaction source all receive one table instance.
- Preserve current synchronous event order in the golden trace.
### 2. Introduce the Runtime lifetime-group root
- Add the owner under `src/AcDream.Runtime/Entities/`.
- Construct `RuntimeEntityDirectory` and `ClientObjectTable` there.
- Expose borrowed properties only; no collection mirror or event repeater.
- Add Runtime-only tests that instantiate it without loading App, UI, Silk,
OpenAL, Arch, or ImGui assemblies.
- Keep mutable state instance-scoped so two roots can hold conflicting GUIDs
and inventory contents without interference.
### 3. Inject canonical identity into the App projection host
- Change `LiveEntityRuntime` to require the exact
`RuntimeEntityDirectory` instance from the group.
- Remove its internal `new RuntimeEntityDirectory(...)`.
- Preserve the existing local-ID start override for focused wraparound tests
through an explicit fixture/group construction seam, not a second production
allocator.
- Assert that App cannot replace the directory after projection construction.
- Preserve J3.3 `RuntimeEntityKey` identity and teardown behavior unchanged.
### 4. Replace the `GameWindow` object-table owner
- Construct the Runtime group before retained UI and live presentation
composition.
- Delete `GameWindow`'s public readonly `ClientObjectTable Objects` field.
- Pass `runtimeGroup.Objects` through the existing typed composition records.
- Keep App controllers as direct synchronous borrowers; do not wrap every read
in a copied DTO or event bus.
- Add source guards rejecting a production App `new ClientObjectTable()` and a
second Runtime entity directory. Exempt Studio/sample-data and tests.
### 5. Move accepted object mutation to the Runtime group
- Move the generation/version-sensitive
`ObjectTableWiring.ApplyEntitySpawn/Delete` transaction boundary out of App.
- Runtime must apply a CreateObject only after the canonical directory accepts
that exact incarnation and while its create-integration version remains
current.
- New-generation replacement must remove the old object generation and publish
the same add/update/removal sequence as today.
- Logical delete must mutate the table before App graphical teardown, exactly
as the current `beforeTeardown` callback does.
- Pickup/parent leave-world must not delete the object-table entry.
- Dormant liveness pruning must retain the existing object-table semantics;
only an authoritative logical delete removes the retained object generation.
- Keep all callbacks synchronous and revalidate after every callback that can
replace, delete, or clear the incarnation.
### 6. Route session and reset ownership
- Bind J2 `LiveSessionEventRouter` directly from the Runtime group rather than
an App-owned table field.
- Keep the current pre-Connect subscription order.
- Keep local-player property projection to `LocalPlayerState` and the same
table instance until J4 moves the coherent player/gameplay state group.
- Replace the App reset field with a Runtime-group object-table reset
acknowledgement at the same manifest position.
- Failure injection must retain the unfinished reset suffix and never clear a
replacement session's table.
### 7. Delete superseded ownership seams
- Remove public ownership comments and aliases that describe the table as a
GameWindow state owner.
- Remove any App construction helper whose only purpose was to allocate the
live table.
- Keep direct table parameters on UI/controllers for now: they are explicit
borrowed dependencies and J4 will move higher-level inventory transactions.
- Update architecture, plans, durable memory, and exact rollback in the same
closeout commit.
## Adversarial cases
- Reentrant `ObjectAdded` deletes or replaces the same GUID.
- Reentrant `ObjectMoved` starts another optimistic move.
- Delete arrives during CreateObject callbacks.
- Same GUID with a new `INSTANCE_TS` while old projection teardown is pending.
- Unknown-object authoritative move publishes the same nullable item event.
- Equal, stale, and wrapped object/physics generations.
- Multiple in-flight optimistic moves followed by confirm/reject in either
order.
- Container replacement while item events mutate the same container.
- Reset during object-table event dispatch.
- Failed graphical teardown after canonical object removal.
- Two Runtime roots use identical GUIDs with different contents.
- No-window root executes the full entity/object lifecycle without loading App
or backend assemblies.
## Mandatory gates
Run in this order:
1. Runtime entity/object lifetime tests.
2. Core `ClientObjectTable*` and `ObjectTableWiring*` tests.
3. Focused App hydration, deletion, reset, inventory, interaction, paperdoll,
magic, cooldown, and runtime-adapter tests.
4. All Runtime tests.
5. All App tests.
6. `dotnet build AcDream.slnx -c Release`.
7. `dotnet test AcDream.slnx -c Release --no-build`.
8. Exact-binary connected lifecycle/reconnect gate.
9. At every connected checkpoint: zero render-shadow mismatches, zero pending
deltas, graceful process exit, and object/projection teardown convergence.
A user visual pause is not required unless an automated or connected gate
exposes an inventory, paperdoll, selection, or interaction symptom.
## Acceptance
- One Runtime lifetime-group root owns the exact entity directory and object
table.
- `GameWindow` owns neither instance directly.
- App and Runtime routing borrow the same object table.
- Runtime remains the only GUID/incarnation/local-ID authority.
- Create/delete/property/container ordering matches the pinned trace.
- Optimistic move/rollback and press-time UI remain synchronous.
- No App/UI/backend dependency enters Runtime.
- No mirror, queue, frame delay, or steady-state traversal allocation is added.
- Runtime-only entity/object lifecycle and multiple-instance isolation pass.
- Complete Release and exact connected gates pass on one committed binary.
## Rollback
The exact J3.4 rollback is:
```text
git revert 5ef8b5371d8990f0380acd939e71cd711289d429
```
J3.3 remains independently reversible; do not use its rollback for a J3.4
failure.
## Result
`RuntimeEntityObjectLifetime` now constructs and owns the one
`RuntimeEntityDirectory` and one live `ClientObjectTable`. `GameWindow`,
session routing, retained UI, interaction, and the graphical projection host
borrow those exact instances.
Accepted CreateObject mutation revalidates the exact canonical record and
`CreateIntegrationVersion` before, during, and after synchronous object-table
callbacks. Authoritative DeleteObject acceptance uses an exact, single-use
Runtime token: the active identity is retired first and retained-object
removal then commits before App resource teardown. Dormant/offscreen pruning
does not remove retained object information.
Final gates:
- Runtime: 118 passed;
- App: 3,762 passed / 3 skipped;
- complete Release solution: 8,453 passed / 5 skipped;
- exact-binary connected lifecycle/reconnect:
`logs/connected-world-gate-20260726-055713/report.json`;
- two sessions, seven checkpoints, graceful zero-code exits, zero failures,
zero render-shadow mismatches, zero pending deltas, and zero pending live
teardown/publication/retirement work.
## Final J3 goal
J3.4 is not the end of J3. J3.5 must publish one ordered per-session
entity/object delta stream and make graphical/direct hosts consume the same
committed facts without a second queue. J3.6 then failure-injects and proves
the complete no-window entity/object lifecycle, zero leaked records,
tombstones, projections, container entries, queued deltas, or subscriptions,
plus Release and connected parity. That J3.6 proof is the clean boundary before
J4 moves inventory transactions, vitals, enchantments, magic, and other
gameplay-state services.