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:
parent
7818494116
commit
a9a822f206
28 changed files with 2707 additions and 345 deletions
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
namespace AcDream.App.Net;
|
||||
|
||||
/// <summary>One named owner operation in the live-session reset transaction.</summary>
|
||||
internal sealed record LiveSessionResetStage(string Name, Action Reset);
|
||||
using AcDream.Runtime;
|
||||
|
||||
/// <summary>One named host operation around Runtime's generation reset.</summary>
|
||||
internal sealed record LiveSessionResetStage(
|
||||
string Name,
|
||||
Action<RuntimeGenerationToken> Reset)
|
||||
{
|
||||
public LiveSessionResetStage(string name, Action reset)
|
||||
: this(
|
||||
name,
|
||||
_ => (reset ?? throw new ArgumentNullException(nameof(reset)))())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes every session-state reset stage even when an earlier owner fails.
|
||||
|
|
@ -35,6 +47,9 @@ internal sealed class LiveSessionResetPlan
|
|||
Array.ConvertAll(_stages, static stage => stage.Name);
|
||||
|
||||
public void Execute()
|
||||
=> Execute(default);
|
||||
|
||||
public void Execute(RuntimeGenerationToken retiringGeneration)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _executing, 1) != 0)
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -47,7 +62,7 @@ internal sealed class LiveSessionResetPlan
|
|||
{
|
||||
try
|
||||
{
|
||||
stage.Reset();
|
||||
stage.Reset(retiringGeneration);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ using AcDream.Core.Social;
|
|||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
|
@ -39,6 +40,7 @@ internal sealed record LiveSessionPlayerRuntime(
|
|||
LiveWorldOriginState WorldOrigin);
|
||||
|
||||
internal sealed record LiveSessionDomainRuntime(
|
||||
GameRuntime Runtime,
|
||||
RuntimeEntityObjectLifetime EntityObjects,
|
||||
RuntimeCharacterState Character,
|
||||
RuntimeActionState Actions,
|
||||
|
|
@ -122,12 +124,18 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(connectOptions);
|
||||
var resetHost = new GraphicalRuntimeGenerationResetHost(
|
||||
_world.LiveEntities,
|
||||
() => _world.RenderSceneShadow?.DrainUpdateBoundary());
|
||||
LiveSessionResetPlan reset =
|
||||
LiveSessionResetManifest.Create(
|
||||
CreateResetBindings(resetHost));
|
||||
return new LiveSessionHost(controller, new LiveSessionHostBindings(
|
||||
Routing: new(
|
||||
CreateEventRouter,
|
||||
session => _commands.Attach(new LiveSessionCommandRouter(
|
||||
CreateCommandBindings(session)))),
|
||||
Reset: LiveSessionResetManifest.Create(CreateResetBindings()).Execute,
|
||||
Reset: reset.Execute,
|
||||
Selection: new(
|
||||
SetPlayerIdentity: id => _player.Identity.ServerGuid = id,
|
||||
SetVitalsIdentity: id => _ui.Vitals?.SetLocalPlayerGuid(id),
|
||||
|
|
@ -152,37 +160,26 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
connectOptions);
|
||||
}
|
||||
|
||||
private LiveSessionResetBindings CreateResetBindings() => new()
|
||||
private LiveSessionResetBindings CreateResetBindings(
|
||||
IRuntimeGenerationResetHost resetHost) => new()
|
||||
{
|
||||
MouseCapture = _interaction.GameplayInput.ResetSession,
|
||||
PlayerPresentation = ResetPlayerPresentation,
|
||||
TeleportTransit = _world.Teleport.ResetSession,
|
||||
TeleportPresentation =
|
||||
_world.Teleport.ResetGenerationPresentation,
|
||||
SessionDialogs = () => _ui.RetailUi?.ResetSessionTransientUi(),
|
||||
ChatCommandTargets = _domain.Communication.ResetCommandTargets,
|
||||
SettingsCharacterContext =
|
||||
_interaction.Settings.RestoreDefaultCharacterContext,
|
||||
EquippedChildren = _world.EquippedChildren.Clear,
|
||||
ExternalContainer = _domain.Inventory.ResetExternalContainer,
|
||||
InteractionAndSelection = _interaction.SelectionInteractions.ResetSession,
|
||||
InventoryTransactions = _domain.Inventory.ResetTransactions,
|
||||
InteractionPresentation =
|
||||
_interaction.SelectionInteractions.ResetGenerationPresentation,
|
||||
SelectionPresentation = _world.SelectionScene.Reset,
|
||||
ObjectTable = _domain.EntityObjects.ClearObjects,
|
||||
Spellbook = _domain.Character.ResetSpellbook,
|
||||
MagicRuntime = () => _ui.Magic?.Reset(),
|
||||
CombatAttack = _interaction.CombatAttack.ResetSession,
|
||||
CombatState = _domain.Actions.Combat.Clear,
|
||||
ItemMana = _domain.Inventory.ResetItemMana,
|
||||
LocalPlayer = _domain.Character.ResetLocalPlayer,
|
||||
Friends = _domain.Communication.ResetFriends,
|
||||
Squelch = _domain.Communication.ResetSquelch,
|
||||
TurbineChat = _domain.Communication.ResetNegotiatedChannels,
|
||||
ParticleVisibility = _world.ParticleVisibility.Reset,
|
||||
InboundEventFifo = _world.InboundEvents.Clear,
|
||||
LiveLiveness = _world.Liveness.Clear,
|
||||
LiveRuntime = ResetLiveRuntime,
|
||||
RenderSceneProjection =
|
||||
() => _world.RenderSceneShadow?.DrainUpdateBoundary(),
|
||||
SessionIdentity = ResetIdentity,
|
||||
RuntimeGeneration = generation =>
|
||||
_domain.Runtime.ResetGeneration(generation, resetHost),
|
||||
SessionIdentityPresentation = ResetIdentityPresentation,
|
||||
RemoteTeleport = _world.RemoteTeleport.Clear,
|
||||
NetworkEffects = _world.EntityEffects.ClearNetworkState,
|
||||
AnimationHookFrames = _world.AnimationHookFrames.Clear,
|
||||
|
|
@ -196,28 +193,28 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_world.SpawnClaims.Reset();
|
||||
}
|
||||
|
||||
private void ResetLiveRuntime() =>
|
||||
LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(
|
||||
_world.LiveEntities);
|
||||
|
||||
private void ResetIdentity()
|
||||
private void ResetIdentityPresentation(
|
||||
RuntimeGenerationToken retiringGeneration)
|
||||
{
|
||||
LiveSessionEntityRuntimeReset.RequireConvergence(_world.LiveEntities);
|
||||
RuntimeGenerationResetSnapshot reset =
|
||||
_domain.Runtime.GenerationReset.CaptureSnapshot();
|
||||
if (reset.IsActive
|
||||
|| reset.LastCompletedGeneration != retiringGeneration)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime generation {retiringGeneration.Value} has not "
|
||||
+ "converged before App identity projection teardown.");
|
||||
}
|
||||
|
||||
// PlayerModule::Clear @ 0x005D48A0 clears shortcuts, desired
|
||||
// components, and character option objects. CPlayerSystem::Begin
|
||||
// @ 0x0055D410 resets player identity and session fields. The option
|
||||
// default comes from PlayerModule::PlayerModule @ 0x005D51F0.
|
||||
_player.Identity.ServerGuid = 0u;
|
||||
_ui.Vitals?.SetLocalPlayerGuid(0u);
|
||||
_domain.Communication.ResetChatIdentity();
|
||||
EntityVanishProbe.PlayerGuid = 0u;
|
||||
_interaction.Settings.ResetActiveCharacterKey();
|
||||
_domain.Character.Options.ResetSession();
|
||||
_domain.Character.MovementSkills.ResetSession();
|
||||
_world.NetworkUpdates.ResetSessionState();
|
||||
_world.Hydration.ResetSessionState();
|
||||
_domain.Inventory.ResetPlayerSnapshots();
|
||||
_ui.Paperdoll?.ResetSession();
|
||||
|
||||
// X/Y are ignored until the next logical player CreateObject claims a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue