refactor(net): converge live session state reset
This commit is contained in:
parent
78a9223b65
commit
4f31a5085f
35 changed files with 1460 additions and 83 deletions
127
src/AcDream.App/Net/LiveSessionResetManifest.cs
Normal file
127
src/AcDream.App/Net/LiveSessionResetManifest.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
namespace AcDream.App.Net;
|
||||
|
||||
using AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
/// Focused owner operations composed by the App host for one character-session
|
||||
/// reset. Mixed world/static registries are deliberately absent: their
|
||||
/// session-owned entries leave through canonical live-entity teardown.
|
||||
/// </summary>
|
||||
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 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 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 SessionIdentity { get; init; }
|
||||
public required Action RemoteTeleport { get; init; }
|
||||
public required Action NetworkEffects { get; init; }
|
||||
public required Action AnimationHookFrames { get; init; }
|
||||
public required Action LivePresentation { get; init; }
|
||||
public required Action RemoteMovementDiagnostics { get; init; }
|
||||
public required Action PhysicsHostIndex { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The concrete, ordered character-session reset manifest. Retail
|
||||
/// <c>CPlayerSystem::OnEndCharacterSession @ 0x00562870</c> calls End then
|
||||
/// Begin; <c>ClientUISystem::OnEndCharacterSession @ 0x00564AD0</c>
|
||||
/// independently resets gameplay UI. App resource dependencies impose the
|
||||
/// exact projection-before-canonical-runtime order pinned here.
|
||||
/// </summary>
|
||||
internal static class LiveSessionResetManifest
|
||||
{
|
||||
public static LiveSessionResetPlan Create(LiveSessionResetBindings bindings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bindings);
|
||||
return new LiveSessionResetPlan(
|
||||
[
|
||||
new("mouse capture", bindings.MouseCapture),
|
||||
new("player presentation", bindings.PlayerPresentation),
|
||||
new("teleport transit", bindings.TeleportTransit),
|
||||
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("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),
|
||||
// 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("remote teleport", bindings.RemoteTeleport),
|
||||
// F754/F755 can precede CreateObject, so pending network effects
|
||||
// must clear even when no LiveEntityRecord was constructed.
|
||||
new("network effects", bindings.NetworkEffects),
|
||||
new("animation hook frames", bindings.AnimationHookFrames),
|
||||
new("live presentation", bindings.LivePresentation),
|
||||
new("remote movement diagnostics", bindings.RemoteMovementDiagnostics),
|
||||
new("physics host index", bindings.PhysicsHostIndex),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shared convergence gate for canonical live runtime teardown.</summary>
|
||||
internal static class LiveSessionEntityRuntimeReset
|
||||
{
|
||||
public static void ClearAndRequireConvergence(LiveEntityRuntime? runtime)
|
||||
{
|
||||
if (runtime is null)
|
||||
return;
|
||||
runtime.Clear();
|
||||
RequireConvergence(runtime);
|
||||
}
|
||||
|
||||
public static void RequireConvergence(LiveEntityRuntime? runtime)
|
||||
{
|
||||
if (runtime is null)
|
||||
return;
|
||||
if (runtime.Count == 0
|
||||
&& runtime.PendingTeardownCount == 0
|
||||
&& runtime.MaterializedCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"The live-entity runtime has not converged after session clear " +
|
||||
$"(records={runtime.Count}, pending={runtime.PendingTeardownCount}, " +
|
||||
$"materialized={runtime.MaterializedCount}).");
|
||||
}
|
||||
}
|
||||
79
src/AcDream.App/Net/LiveSessionResetPlan.cs
Normal file
79
src/AcDream.App/Net/LiveSessionResetPlan.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
namespace AcDream.App.Net;
|
||||
|
||||
/// <summary>One named owner operation in the live-session reset transaction.</summary>
|
||||
internal sealed record LiveSessionResetStage(string Name, Action Reset);
|
||||
|
||||
/// <summary>
|
||||
/// Executes every session-state reset stage even when an earlier owner fails.
|
||||
/// A failed attempt throws after the complete manifest has run; lifecycle code
|
||||
/// therefore cannot construct the next session until a later attempt converges.
|
||||
/// </summary>
|
||||
internal sealed class LiveSessionResetPlan
|
||||
{
|
||||
private readonly LiveSessionResetStage[] _stages;
|
||||
private int _executing;
|
||||
|
||||
public LiveSessionResetPlan(IEnumerable<LiveSessionResetStage> stages)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stages);
|
||||
_stages = stages.ToArray();
|
||||
var names = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (LiveSessionResetStage stage in _stages)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stage);
|
||||
if (string.IsNullOrWhiteSpace(stage.Name))
|
||||
throw new ArgumentException("Reset stage names must be non-empty.", nameof(stages));
|
||||
ArgumentNullException.ThrowIfNull(stage.Reset);
|
||||
if (!names.Add(stage.Name))
|
||||
throw new ArgumentException(
|
||||
$"Duplicate live-session reset stage '{stage.Name}'.",
|
||||
nameof(stages));
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> StageNames =>
|
||||
Array.ConvertAll(_stages, static stage => stage.Name);
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _executing, 1) != 0)
|
||||
throw new InvalidOperationException(
|
||||
"Live-session reset cannot run concurrently or reentrantly.");
|
||||
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
foreach (LiveSessionResetStage stage in _stages)
|
||||
{
|
||||
try
|
||||
{
|
||||
stage.Reset();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(new LiveSessionResetStageException(
|
||||
stage.Name,
|
||||
error));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Volatile.Write(ref _executing, 0);
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"Live-session state did not converge; a new session must not start.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveSessionResetStageException(
|
||||
string stageName,
|
||||
Exception innerException) : Exception(
|
||||
$"Live-session reset stage '{stageName}' failed.",
|
||||
innerException)
|
||||
{
|
||||
public string StageName { get; } = stageName;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue