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

@ -29,8 +29,9 @@ namespace AcDream.Core.Net.Messages;
/// Retail clients send it once the portal-space transition animation finishes.
/// acdream's F751 teleport path now does the same through
/// <c>TeleportAnimSequencer.FireLoginComplete</c>. Initial session bootstrap
/// still sends from the PlayerCreate handler; that remaining ordering gap is
/// tracked as TS-28 in the divergence register.
/// sends from the canonical local-player first-placement completion edge, not
/// from raw packet receipt, so ACE's intentional Hidden/pink-bubble state is
/// released only after the client can actually present the player.
/// </para>
/// </summary>
public static class GameActionLoginComplete

View file

@ -555,16 +555,6 @@ public sealed class WorldSession : IDisposable
/// </summary>
public double LastServerTimeTicks { get; private set; }
/// <summary>
/// Allow re-sending LoginComplete after a portal teleport. The normal
/// _loginCompleteSent latch prevents duplicate sends on the initial spawn
/// path; this method resets it so the teleport completion path can send
/// another LoginComplete to tell the server the client has finished loading
/// the destination cell. Pattern from holtburger's PlayerTeleport handler
/// (client/messages.rs line 434-440: call send_login_complete on teleport).
/// </summary>
public void ResetLoginComplete() => _loginCompleteSent = false;
/// <summary>Raised every time the state machine transitions.</summary>
public event Action<State>? StateChanged;
@ -771,14 +761,6 @@ public sealed class WorldSession : IDisposable
Buffer.AsMemory(0, Length);
}
/// <summary>
/// Phase 4.10 latch — true after we've sent the LoginComplete game
/// action in response to PlayerCreate. Prevents re-sending if the
/// server emits multiple PlayerCreate messages (rare but possible
/// across recall / portal teleports).
/// </summary>
private bool _loginCompleteSent;
/// <summary>L.2g slice 1: one-shot guard so the [setstate-hex] probe
/// emits the first SetState's body bytes only, not 510/sec.</summary>
private bool _setStateHexDumped;
@ -1061,13 +1043,10 @@ public sealed class WorldSession : IDisposable
// login form. ACE validates this canonical account value.
SendGameMessage(selection.EnterWorldBody);
// NOTE: LoginComplete used to be sent here unconditionally. That was
// wrong — per holtburger's flow (see references/holtburger/.../client/
// messages.rs lines 391-422), LoginComplete is sent in response to the
// server's PlayerCreate (0xF746) game message, NOT immediately after
// EnterWorld. Sending it too early means the player object isn't
// ready and the server ignores it. The actual trigger lives in
// ProcessDatagram.
// LoginComplete is emitted by the host only after the accepted local
// Create has completed its canonical first placement. Sending it at
// EnterWorld or merely on PlayerCreate races the server's intentional
// Hidden/pink-bubble login state.
Transition(State.InWorld);
// Phase A.3: start the background receive thread now that the
@ -1705,17 +1684,6 @@ public sealed class WorldSession : IDisposable
// references/holtburger/.../client/messages.rs::DddInterrogation
SendGameMessage(DddInterrogationResponse.Build());
}
else if (op == 0xF746u && !_loginCompleteSent) // PlayerCreate — server creates our player object
{
// Phase 4.10: PlayerCreate for our character is the cue to
// send LoginComplete. Sending it earlier (right after the
// outbound CharacterEnterWorld) was wrong because the server
// hadn't finished spawning the player yet. Holtburger's
// client/messages.rs (PlayerCreate handler) confirms this is
// the correct trigger. Send once per session.
_loginCompleteSent = true;
SendGameMessage(GameActionLoginComplete.Build());
}
else if (op == CreateObject.Opcode)
{
var parsed = CreateObject.TryParse(body);