diff --git a/src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs b/src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs
new file mode 100644
index 0000000..aa6d7fb
--- /dev/null
+++ b/src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs
@@ -0,0 +1,41 @@
+using AcDream.Core.Net.Packets;
+
+namespace AcDream.Core.Net.Messages;
+
+///
+/// Outbound reply to the server's DddInterrogation game message
+/// (0xF7E5). Sent during the post-EnterWorld handshake; without it the
+/// server keeps the client in a transitional state and other clients
+/// see the character as a stationary purple loading haze.
+///
+///
+/// Wire layout (see
+/// references/holtburger/crates/holtburger-protocol/src/messages/misc/types.rs::DddInterrogationResponseData):
+///
+///
+/// - u32 game-message opcode = 0xF7E6 (DddInterrogationResponse)
+/// - u32 language = 1 (English)
+/// - u32 count = 0 (no tagged iteration lists; we acknowledge the
+/// interrogation without claiming any dat-list versions). The
+/// loop body is then empty.
+///
+///
+///
+/// Total payload: 12 bytes. Should be sent automatically by
+/// WorldSession in response to receiving 0xF7E5.
+///
+///
+public static class DddInterrogationResponse
+{
+ public const uint Opcode = 0xF7E6u;
+ public const uint EnglishLanguage = 1u;
+
+ public static byte[] Build()
+ {
+ var w = new PacketWriter(16);
+ w.WriteUInt32(Opcode);
+ w.WriteUInt32(EnglishLanguage);
+ w.WriteUInt32(0u); // empty TaggedIterationList vec
+ return w.ToArray();
+ }
+}
diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs
index e29fde9..0e4ee1c 100644
--- a/src/AcDream.Core.Net/WorldSession.cs
+++ b/src/AcDream.Core.Net/WorldSession.cs
@@ -108,6 +108,14 @@ public sealed class WorldSession : IDisposable
private uint _clientPacketSequence;
private uint _fragmentSequence = 1;
+ ///
+ /// 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).
+ ///
+ private bool _loginCompleteSent;
+
public WorldSession(IPEndPoint serverLogin)
{
_loginEndpoint = serverLogin;
@@ -202,14 +210,13 @@ public sealed class WorldSession : IDisposable
SendGameMessage(CharacterEnterWorld.BuildEnterWorldBody(chosen.Id, account));
- // Tell the server "I'm done loading, you can show me to other players
- // and push my initial property updates." Without this, the character
- // stays in a transitional state and renders as the purple loading
- // haze to other clients in the same area. ACE's GameActionLoginComplete
- // handler is what flips Player.FirstEnterWorldDone and triggers
- // SendPropertyUpdatesAndOverrides.
- SendGameMessage(GameActionLoginComplete.Build());
-
+ // 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.
Transition(State.InWorld);
}
@@ -280,6 +287,27 @@ public sealed class WorldSession : IDisposable
try { Characters = CharacterList.Parse(body); }
catch { /* malformed — ignore and keep draining */ }
}
+ else if (op == 0xF7E5u) // DddInterrogation — server asks "what dat list versions do you have?"
+ {
+ // Phase 4.10: reply with an empty DddInterrogationResponse
+ // (language=1 English, count=0 lists). The server is happy
+ // with an empty acknowledgement; without ANY reply it keeps
+ // the client in a transitional state and renders us as the
+ // purple loading haze to other clients. Pattern from
+ // 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);