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);