fix(session): acknowledge login after first placement

ACE intentionally creates the local player Hidden and releases that materialization state on LoginComplete. Sending LoginComplete from raw F746 receipt raced canonical placement and left the login haze visible. Route one one-shot completion callback from Runtime's local first-entry terminal edge to graphical and prepared headless hosts; retain a guarded accepted-Create edge only for content-less headless sessions. Focused Runtime login tests, all 79 Headless tests, the connected user gate, and the Release build pass.
This commit is contained in:
Erik 2026-08-03 12:10:42 +02:00
parent f24532adf3
commit 175ad6b0d0
10 changed files with 107 additions and 48 deletions

View file

@ -1,4 +1,5 @@
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
@ -16,6 +17,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
private readonly Func<RuntimeGenerationToken> _generation;
private readonly RuntimePlacementProjectionRetrySlot _retries;
private readonly RuntimeFirstEntryDriveController? _firstEntry;
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
private RuntimePlacementProjectionSubscription? _subscription;
private IDisposable? _retryLease;
private bool _attachStarted;
@ -27,7 +29,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
GameRuntime runtime,
IRuntimePlacementProjectionSink placements,
RuntimePlacementProjectionRetrySlot retries,
RuntimeFirstEntryDriveController? firstEntry = null)
RuntimeFirstEntryDriveController? firstEntry = null,
Action<RuntimeEntityRecord>? localPlayerCompleted = null)
: this(
events,
() => new RuntimePlacementProjectionSubscription(
@ -36,7 +39,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
retryPendingOnSubscribe: false),
() => runtime.Generation,
retries,
firstEntry)
firstEntry,
localPlayerCompleted)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(placements);
@ -47,7 +51,8 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
Func<RuntimePlacementProjectionSubscription> createSubscription,
Func<RuntimeGenerationToken> generation,
RuntimePlacementProjectionRetrySlot retries,
RuntimeFirstEntryDriveController? firstEntry = null)
RuntimeFirstEntryDriveController? firstEntry = null,
Action<RuntimeEntityRecord>? localPlayerCompleted = null)
{
_events = events ?? throw new ArgumentNullException(nameof(events));
_createSubscription = createSubscription
@ -56,6 +61,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
?? throw new ArgumentNullException(nameof(generation));
_retries = retries ?? throw new ArgumentNullException(nameof(retries));
_firstEntry = firstEntry;
_localPlayerCompleted = localPlayerCompleted;
}
public void Attach()
@ -68,7 +74,7 @@ internal sealed class GraphicalSessionEventRoute : ILiveSessionEventRouting
// C3c-R1 review F6: assert (not assume) that the prior route
// detached — session reset precedes a new route — before this route
// takes ownership of the shared drive controller's tracked entries.
_firstEntry?.AttachRoute(this);
_firstEntry?.AttachRoute(this, _localPlayerCompleted);
_events.Attach();
RuntimePlacementProjectionSubscription? subscription = null;

View file

@ -18,6 +18,7 @@ using AcDream.Core.Chat;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Player;
using AcDream.Core.Social;
using AcDream.Core.Spells;
@ -262,7 +263,8 @@ internal sealed class LiveSessionRuntimeFactory
_domain.Runtime,
_world.PlacementProjection,
_world.PlacementRetries,
_world.FirstEntryDrive);
_world.FirstEntryDrive,
_ => session.SendGameAction(GameActionLoginComplete.Build()));
}
private LiveInventorySessionBindings CreateInventoryBindings() => new(