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
|
|
@ -13,6 +13,7 @@ using AcDream.Core.Selection;
|
|||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
|
|
@ -199,190 +200,107 @@ public sealed class LiveSessionResetPlanTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Manifest_MutatedA_FailedDrain_RetryConvergesWithoutClearingRetainedWorld()
|
||||
public void Manifest_PreservesHostOrderAndForwardsExactRetiringGeneration()
|
||||
{
|
||||
const uint playerA = 0x50000001u;
|
||||
const uint objectA = 0x70000001u;
|
||||
const uint landblock = 0x0101FFFFu;
|
||||
var staticEntity = Entity(42u, 0u);
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
landblock,
|
||||
new LandBlock(),
|
||||
new List<WorldEntity> { staticEntity }));
|
||||
var live = LiveEntityRuntimeFixture.Create(spatial, new FailingOnceResources());
|
||||
WorldSession.EntitySpawn spawn = Spawn(playerA, 1, 1, 0x01010001u);
|
||||
live.RegisterLiveEntity(spawn);
|
||||
live.MaterializeLiveEntity(
|
||||
playerA,
|
||||
0x01010001u,
|
||||
id => Entity(id, playerA));
|
||||
spatial.MarkPersistent(playerA);
|
||||
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = objectA, Name = "A" });
|
||||
var spells = new Spellbook();
|
||||
spells.OnSpellLearned(123u, 1f);
|
||||
var combat = new CombatState();
|
||||
combat.OnUpdateHealth(objectA, 0.5f);
|
||||
combat.SetCombatMode(CombatMode.Melee);
|
||||
var mana = new ItemManaState();
|
||||
mana.OnQueryItemManaResponse(objectA, 0.5f, valid: true);
|
||||
var localPlayer = new LocalPlayerState(spells);
|
||||
localPlayer.OnVitalUpdate(7u, 1u, 100u, 5u, 80u);
|
||||
var friends = new FriendsState();
|
||||
friends.Apply(new FriendsUpdate(
|
||||
FriendsUpdateType.Full,
|
||||
[new FriendEntry(objectA, "A", true, false, [], [])]));
|
||||
var squelch = new SquelchState();
|
||||
squelch.Replace(new SquelchDatabase(
|
||||
new Dictionary<string, uint> { ["A"] = 1u },
|
||||
new Dictionary<uint, SquelchInfo>(),
|
||||
new SquelchInfo("A", false, new HashSet<uint>())));
|
||||
var turbine = new TurbineChatState();
|
||||
turbine.OnChannelsReceived(1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u);
|
||||
var selection = new SelectionState();
|
||||
selection.Select(objectA, SelectionChangeSource.World);
|
||||
var external = new ExternalContainerState();
|
||||
external.RequestOpen(objectA);
|
||||
external.ApplyViewContents(objectA);
|
||||
var chat = new ChatLog();
|
||||
chat.SetLocalPlayerGuid(playerA);
|
||||
chat.OnSystemMessage("retained transcript", 1u);
|
||||
|
||||
uint sessionIdentity = playerA;
|
||||
int savedDefaultSetting = 73;
|
||||
int activeSetting = savedDefaultSetting;
|
||||
var calls = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var dirty = ExpectedManifestNames().ToDictionary(
|
||||
static name => name,
|
||||
static _ => true,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
Action Stage(string name, Action? reset = null) => () =>
|
||||
{
|
||||
calls[name] = calls.GetValueOrDefault(name) + 1;
|
||||
reset?.Invoke();
|
||||
dirty[name] = false;
|
||||
};
|
||||
|
||||
LiveSessionResetPlan plan = LiveSessionResetManifest.Create(new()
|
||||
var calls = new List<string>();
|
||||
RuntimeGenerationToken observed = default;
|
||||
Action Stage(string name) => () => calls.Add(name);
|
||||
var plan = LiveSessionResetManifest.Create(new()
|
||||
{
|
||||
MouseCapture = Stage("mouse capture"),
|
||||
PlayerPresentation = Stage("player presentation"),
|
||||
TeleportTransit = Stage("teleport transit"),
|
||||
TeleportPresentation = Stage("teleport presentation"),
|
||||
SessionDialogs = Stage("session dialogs"),
|
||||
ChatCommandTargets = Stage("chat command targets"),
|
||||
SettingsCharacterContext = Stage(
|
||||
"settings character context",
|
||||
() => activeSetting = savedDefaultSetting),
|
||||
SettingsCharacterContext = Stage("settings character context"),
|
||||
EquippedChildren = Stage("equipped children"),
|
||||
ExternalContainer = Stage("external container", () => external.Reset()),
|
||||
InteractionAndSelection = Stage(
|
||||
"interaction and selection",
|
||||
() => selection.Reset()),
|
||||
InventoryTransactions = Stage("inventory transactions"),
|
||||
InteractionPresentation = Stage("interaction presentation"),
|
||||
SelectionPresentation = Stage("selection presentation"),
|
||||
ObjectTable = Stage("object table", objects.Clear),
|
||||
Spellbook = Stage("spellbook", spells.Clear),
|
||||
MagicRuntime = Stage("magic runtime"),
|
||||
CombatAttack = Stage("combat attack"),
|
||||
CombatState = Stage("combat state", combat.Clear),
|
||||
ItemMana = Stage("item mana", mana.Clear),
|
||||
LocalPlayer = Stage("local player", localPlayer.Clear),
|
||||
Friends = Stage("friends", friends.Clear),
|
||||
Squelch = Stage("squelch", squelch.Clear),
|
||||
TurbineChat = Stage("turbine chat", turbine.Reset),
|
||||
ParticleVisibility = Stage("particle visibility"),
|
||||
InboundEventFifo = Stage("inbound event fifo"),
|
||||
LiveLiveness = Stage("live liveness"),
|
||||
LiveRuntime = Stage(
|
||||
"live runtime",
|
||||
() => LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(live)),
|
||||
RenderSceneProjection = Stage("render scene projection"),
|
||||
SessionIdentity = Stage("session identity", () =>
|
||||
RuntimeGeneration = generation =>
|
||||
{
|
||||
LiveSessionEntityRuntimeReset.RequireConvergence(live);
|
||||
sessionIdentity = 0u;
|
||||
chat.ResetSessionIdentity();
|
||||
}),
|
||||
observed = generation;
|
||||
calls.Add("runtime generation");
|
||||
},
|
||||
SessionIdentityPresentation = _ =>
|
||||
calls.Add("session identity presentation"),
|
||||
RemoteTeleport = Stage("remote teleport"),
|
||||
NetworkEffects = Stage("network effects"),
|
||||
AnimationHookFrames = Stage("animation hook frames"),
|
||||
LivePresentation = Stage("live presentation"),
|
||||
RemoteMovementDiagnostics = Stage("remote movement diagnostics"),
|
||||
RemoteMovementDiagnostics =
|
||||
Stage("remote movement diagnostics"),
|
||||
});
|
||||
|
||||
var retiring = new RuntimeGenerationToken(73);
|
||||
plan.Execute(retiring);
|
||||
|
||||
Assert.Equal(ExpectedManifestNames(), plan.StageNames);
|
||||
AggregateException first = Assert.Throws<AggregateException>(plan.Execute);
|
||||
Assert.Equal(ExpectedManifestNames(), calls);
|
||||
Assert.Equal(retiring, observed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalResetHost_RetriesExactProjectionBeforeIdentityClears()
|
||||
{
|
||||
const uint player = 0x50000001u;
|
||||
const uint landblock = 0x0101FFFFu;
|
||||
using GameRuntime runtime = GameRuntimeTestFactory.Create();
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
landblock,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
var live = new LiveEntityRuntime(
|
||||
spatial,
|
||||
new FailingOnceResources(),
|
||||
runtime.EntityObjects);
|
||||
live.RegisterLiveEntity(Spawn(player, 1, 1, 0x01010001u));
|
||||
live.MaterializeLiveEntity(
|
||||
player,
|
||||
0x01010001u,
|
||||
id => Entity(id, player));
|
||||
var host = new GraphicalRuntimeGenerationResetHost(
|
||||
live,
|
||||
static () => { });
|
||||
var retiring = new RuntimeGenerationToken(3);
|
||||
|
||||
RuntimeGenerationResetStageException first =
|
||||
Assert.Throws<RuntimeGenerationResetStageException>(
|
||||
() => runtime.ResetGeneration(retiring, host));
|
||||
|
||||
Assert.Equal(
|
||||
["live runtime", "session identity"],
|
||||
first.InnerExceptions
|
||||
.Cast<LiveSessionResetStageException>()
|
||||
.Select(static error => error.StageName));
|
||||
Assert.Equal(playerA, sessionIdentity);
|
||||
RuntimeGenerationResetStage.RetireEntities,
|
||||
first.Stage);
|
||||
Assert.Equal(player, runtime.PlayerIdentity.ServerGuid);
|
||||
Assert.Equal(1, live.PendingTeardownCount);
|
||||
Assert.Contains(staticEntity, spatial.Entities);
|
||||
Assert.Equal(1, spatial.PersistentGuidCount);
|
||||
Assert.Equal(1, live.MaterializedCount);
|
||||
|
||||
plan.Execute();
|
||||
runtime.ResetGeneration(retiring, host);
|
||||
|
||||
Assert.All(ExpectedManifestNames(), name => Assert.Equal(2, calls[name]));
|
||||
Assert.DoesNotContain(dirty, static pair => pair.Value);
|
||||
Assert.Equal(0u, sessionIdentity);
|
||||
Assert.Equal(savedDefaultSetting, activeSetting);
|
||||
Assert.Equal(0u, runtime.PlayerIdentity.ServerGuid);
|
||||
Assert.Equal(0, live.Count);
|
||||
Assert.Equal(0, live.PendingTeardownCount);
|
||||
Assert.Equal(0, live.MaterializedCount);
|
||||
Assert.Equal(0, spatial.PersistentGuidCount);
|
||||
Assert.Contains(staticEntity, spatial.Entities);
|
||||
Assert.True(spatial.IsLoaded(landblock));
|
||||
Assert.Null(objects.Get(objectA));
|
||||
Assert.Equal(0, spells.LearnedCount);
|
||||
Assert.Equal(CombatMode.NonCombat, combat.CurrentMode);
|
||||
Assert.False(combat.HasHealth(objectA));
|
||||
Assert.False(mana.HasMana(objectA));
|
||||
Assert.Null(localPlayer.Get(LocalPlayerState.VitalKind.Health));
|
||||
Assert.Empty(friends.Snapshot());
|
||||
Assert.Same(SquelchDatabase.Empty, squelch.Snapshot());
|
||||
Assert.False(turbine.Enabled);
|
||||
Assert.Null(selection.SelectedObjectId);
|
||||
Assert.Equal(0u, external.CurrentContainerId);
|
||||
Assert.Equal(1, chat.Count);
|
||||
chat.OnLocalSpeech("A", "after", playerA, isRanged: false);
|
||||
Assert.Equal("A", chat.Snapshot()[1].Sender);
|
||||
}
|
||||
|
||||
private static string[] ExpectedManifestNames() =>
|
||||
[
|
||||
"mouse capture",
|
||||
"player presentation",
|
||||
"teleport transit",
|
||||
"teleport presentation",
|
||||
"session dialogs",
|
||||
"chat command targets",
|
||||
"settings character context",
|
||||
"equipped children",
|
||||
"external container",
|
||||
"interaction and selection",
|
||||
"inventory transactions",
|
||||
"interaction presentation",
|
||||
"selection presentation",
|
||||
"object table",
|
||||
"spellbook",
|
||||
"magic runtime",
|
||||
"combat attack",
|
||||
"combat state",
|
||||
"item mana",
|
||||
"local player",
|
||||
"friends",
|
||||
"squelch",
|
||||
"turbine chat",
|
||||
"particle visibility",
|
||||
"inbound event fifo",
|
||||
"live liveness",
|
||||
"live runtime",
|
||||
"render scene projection",
|
||||
"session identity",
|
||||
"runtime generation",
|
||||
"session identity presentation",
|
||||
"remote teleport",
|
||||
"network effects",
|
||||
"animation hook frames",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Net;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
|
|
@ -92,7 +93,8 @@ public sealed class LiveSessionShutdownIntegrationTests
|
|||
deactivateCommands: static () => { },
|
||||
detachEvents: static () => { });
|
||||
|
||||
public void ResetSessionState() { }
|
||||
public void ResetSessionState(
|
||||
RuntimeGenerationToken retiringGeneration) { }
|
||||
public void ReportConnecting(string host, int port, string user) { }
|
||||
public void ReportConnected() { }
|
||||
public void ApplySelectedCharacter(LiveSessionCharacterSelection selection) { }
|
||||
|
|
|
|||
|
|
@ -945,31 +945,17 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
{
|
||||
MouseCapture = noop,
|
||||
PlayerPresentation = noop,
|
||||
TeleportTransit = noop,
|
||||
TeleportPresentation = noop,
|
||||
SessionDialogs = noop,
|
||||
ChatCommandTargets = noop,
|
||||
SettingsCharacterContext = noop,
|
||||
EquippedChildren = noop,
|
||||
ExternalContainer = noop,
|
||||
InteractionAndSelection = noop,
|
||||
InventoryTransactions = noop,
|
||||
InteractionPresentation = noop,
|
||||
SelectionPresentation = noop,
|
||||
ObjectTable = noop,
|
||||
Spellbook = noop,
|
||||
MagicRuntime = noop,
|
||||
CombatAttack = noop,
|
||||
CombatState = noop,
|
||||
ItemMana = noop,
|
||||
LocalPlayer = noop,
|
||||
Friends = noop,
|
||||
Squelch = noop,
|
||||
TurbineChat = noop,
|
||||
ParticleVisibility = noop,
|
||||
InboundEventFifo = noop,
|
||||
LiveLiveness = noop,
|
||||
LiveRuntime = noop,
|
||||
RenderSceneProjection = noop,
|
||||
SessionIdentity = noop,
|
||||
RuntimeGeneration = _ => { },
|
||||
SessionIdentityPresentation = _ => { },
|
||||
RemoteTeleport = noop,
|
||||
NetworkEffects = noop,
|
||||
AnimationHookFrames = noop,
|
||||
|
|
|
|||
|
|
@ -850,6 +850,9 @@ public sealed class LocalPlayerTeleportControllerTests
|
|||
public void ResetSession()
|
||||
{
|
||||
}
|
||||
public void ResetGenerationPresentation()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakePresentation : ILocalPlayerTeleportPresentation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue