refactor(runtime): unify generation reset for direct hosts

Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-27 00:43:26 +02:00
parent 7818494116
commit a9a822f206
28 changed files with 2707 additions and 345 deletions

View file

@ -1,6 +1,8 @@
namespace AcDream.App.Net;
using AcDream.App.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
/// <summary>
/// Focused owner operations composed by the App host for one character-session
@ -11,31 +13,18 @@ internal sealed class LiveSessionResetBindings
{
public required Action MouseCapture { get; init; }
public required Action PlayerPresentation { get; init; }
public required Action TeleportTransit { get; init; }
public required Action TeleportPresentation { get; init; }
public required Action SessionDialogs { get; init; }
public required Action ChatCommandTargets { get; init; }
public required Action SettingsCharacterContext { get; init; }
public required Action EquippedChildren { get; init; }
public required Action ExternalContainer { get; init; }
public required Action InteractionAndSelection { get; init; }
public required Action InventoryTransactions { get; init; }
public required Action InteractionPresentation { get; init; }
public required Action SelectionPresentation { get; init; }
public required Action ObjectTable { get; init; }
public required Action Spellbook { get; init; }
public required Action MagicRuntime { get; init; }
public required Action CombatAttack { get; init; }
public required Action CombatState { get; init; }
public required Action ItemMana { get; init; }
public required Action LocalPlayer { get; init; }
public required Action Friends { get; init; }
public required Action Squelch { get; init; }
public required Action TurbineChat { get; init; }
public required Action ParticleVisibility { get; init; }
public required Action InboundEventFifo { get; init; }
public required Action LiveLiveness { get; init; }
public required Action LiveRuntime { get; init; }
public required Action RenderSceneProjection { get; init; }
public required Action SessionIdentity { get; init; }
public required Action<RuntimeGenerationToken> RuntimeGeneration { get; init; }
public required Action<RuntimeGenerationToken> SessionIdentityPresentation
{ get; init; }
public required Action RemoteTeleport { get; init; }
public required Action NetworkEffects { get; init; }
public required Action AnimationHookFrames { get; init; }
@ -59,38 +48,23 @@ internal static class LiveSessionResetManifest
[
new("mouse capture", bindings.MouseCapture),
new("player presentation", bindings.PlayerPresentation),
new("teleport transit", bindings.TeleportTransit),
new("teleport presentation", bindings.TeleportPresentation),
new("session dialogs", bindings.SessionDialogs),
new("chat command targets", bindings.ChatCommandTargets),
new("settings character context", bindings.SettingsCharacterContext),
// Attachment projections own GL-backed registrations and must leave
// before canonical live GUID records are released.
new("equipped children", bindings.EquippedChildren),
new("external container", bindings.ExternalContainer),
new("interaction and selection", bindings.InteractionAndSelection),
new("inventory transactions", bindings.InventoryTransactions),
new("interaction presentation", bindings.InteractionPresentation),
new("selection presentation", bindings.SelectionPresentation),
new("object table", bindings.ObjectTable),
new("spellbook", bindings.Spellbook),
new("magic runtime", bindings.MagicRuntime),
new("combat attack", bindings.CombatAttack),
new("combat state", bindings.CombatState),
new("item mana", bindings.ItemMana),
new("local player", bindings.LocalPlayer),
new("friends", bindings.Friends),
new("squelch", bindings.Squelch),
new("turbine chat", bindings.TurbineChat),
new("particle visibility", bindings.ParticleVisibility),
new("inbound event fifo", bindings.InboundEventFifo),
new("live liveness", bindings.LiveLiveness),
new("live runtime", bindings.LiveRuntime),
// Canonical teardown appends exact projection removals. Drain them
// before identity changes so no prior-session scene record or
// journal entry can carry into the next character generation.
new("render scene projection", bindings.RenderSceneProjection),
new("runtime generation", bindings.RuntimeGeneration),
// Identity must remain A until live teardown has converged so
// player-specific teardown can still classify the old record.
new("session identity", bindings.SessionIdentity),
new(
"session identity presentation",
bindings.SessionIdentityPresentation),
new("remote teleport", bindings.RemoteTeleport),
// F754/F755 can precede CreateObject, so pending network effects
// must clear even when no LiveEntityRecord was constructed.
@ -102,31 +76,33 @@ internal static class LiveSessionResetManifest
}
}
/// <summary>Shared convergence gate for canonical live runtime teardown.</summary>
internal static class LiveSessionEntityRuntimeReset
/// <summary>
/// App's narrow borrowed projection acknowledgement for Runtime's canonical
/// generation-reset transaction. It owns no reset cursor or entity identity.
/// </summary>
internal sealed class GraphicalRuntimeGenerationResetHost
: IRuntimeGenerationResetHost
{
public static void ClearAndRequireConvergence(LiveEntityRuntime? runtime)
private readonly LiveEntityRuntime _entities;
private readonly Action _drainRenderProjection;
public GraphicalRuntimeGenerationResetHost(
LiveEntityRuntime entities,
Action drainRenderProjection)
{
if (runtime is null)
return;
runtime.Clear();
RequireConvergence(runtime);
_entities = entities
?? throw new ArgumentNullException(nameof(entities));
_drainRenderProjection = drainRenderProjection
?? throw new ArgumentNullException(nameof(drainRenderProjection));
}
public static void RequireConvergence(LiveEntityRuntime? runtime)
{
if (runtime is null)
return;
if (runtime.Count == 0
&& runtime.PendingTeardownCount == 0
&& runtime.MaterializedCount == 0)
{
return;
}
public void RetireEntityProjection(
RuntimeEntityRecord entity) =>
_entities.RetireGenerationProjection(entity);
throw new InvalidOperationException(
"The live-entity runtime has not converged after session clear " +
$"(records={runtime.Count}, pending={runtime.PendingTeardownCount}, " +
$"materialized={runtime.MaterializedCount}).");
}
public void DrainEntityProjectionBoundary() =>
_drainRenderProjection();
public void CompleteEntityProjectionRetirement() =>
_entities.CompleteGenerationProjectionRetirement();
}