refactor(net): own live session composition

Extract reset, selection, entered-world, and route construction behind LiveSessionHost while preserving the sole LiveSessionController authority. Retain partial route and subscription cleanup for retry, and replace the embedded ACE-only shortcut with the exact named-retail unsigned skill formula.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 10:36:06 +02:00
parent 18d4b999de
commit 557eb7ef6b
22 changed files with 1430 additions and 236 deletions

View file

@ -13,6 +13,33 @@ namespace AcDream.App.Tests.Net;
public sealed class LiveSessionEventRouterTests
{
[Fact]
public void CreatedRouterPublishesNoHandlersUntilExplicitAttach()
{
using var session = NewSession();
int baselineGameEvents = session.GameEvents.RegisteredHandlerCount;
var router = new LiveSessionEventRouter(
session,
new LiveEntitySessionSink(
_ => { }, _ => { }, _ => { }, _ => { }, _ => { }, _ => { },
_ => { }, _ => { }, _ => { }, _ => { }, _ => { }, _ => { }),
new LiveEnvironmentSessionSink(_ => { }, _ => { }),
NewInventoryBindings(),
NewCharacterBindings(),
NewSocialBindings());
AssertSessionHandlerCounts(session, multiplier: 0);
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
router.Attach();
AssertSessionHandlerCounts(session, multiplier: 1);
Assert.True(session.GameEvents.RegisteredHandlerCount > baselineGameEvents);
router.Dispose();
AssertSessionHandlerCounts(session, multiplier: 0);
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
}
[Fact]
public void Dispose_DetachesExactHandlersAndSilencesCopiedDelegates()
{
@ -157,33 +184,46 @@ public sealed class LiveSessionEventRouterTests
Counters counters,
Action<int>? constructionCheckpoint = null,
CombatState? combat = null,
ChatLog? chat = null) => new(
session,
new LiveEntitySessionSink(
Spawned: _ => { },
Deleted: _ => { },
PickedUp: _ => { },
MotionUpdated: _ => { },
PositionUpdated: _ => { },
VectorUpdated: _ => { },
StateUpdated: _ => { },
ParentUpdated: _ => { },
TeleportStarted: _ => counters.Teleport++,
AppearanceUpdated: _ => { },
PlayPhysicsScript: _ => { },
PlayPhysicsScriptType: _ => { }),
new LiveEnvironmentSessionSink(
EnvironChanged: _ => { },
ServerTimeUpdated: _ =>
{
if (counters.ThrowOnServerTime)
throw new InvalidOperationException("injected sink failure");
counters.ServerTime++;
}),
NewInventoryBindings(),
NewCharacterBindings(combat),
NewSocialBindings(chat),
constructionCheckpoint);
ChatLog? chat = null)
{
var router = new LiveSessionEventRouter(
session,
new LiveEntitySessionSink(
Spawned: _ => { },
Deleted: _ => { },
PickedUp: _ => { },
MotionUpdated: _ => { },
PositionUpdated: _ => { },
VectorUpdated: _ => { },
StateUpdated: _ => { },
ParentUpdated: _ => { },
TeleportStarted: _ => counters.Teleport++,
AppearanceUpdated: _ => { },
PlayPhysicsScript: _ => { },
PlayPhysicsScriptType: _ => { }),
new LiveEnvironmentSessionSink(
EnvironChanged: _ => { },
ServerTimeUpdated: _ =>
{
if (counters.ThrowOnServerTime)
throw new InvalidOperationException("injected sink failure");
counters.ServerTime++;
}),
NewInventoryBindings(),
NewCharacterBindings(combat),
NewSocialBindings(chat),
constructionCheckpoint);
try
{
router.Attach();
return router;
}
catch
{
router.Dispose();
throw;
}
}
private static LiveInventorySessionBindings NewInventoryBindings() => new(
new ClientObjectTable(),