From 8c14e0207cc150ee4995b8bea4ee7a1b6f5edf3c Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 23:48:37 +0200 Subject: [PATCH] =?UTF-8?q?feat(net):=20Phase=204.10=20=E2=80=94=20DddInte?= =?UTF-8?q?rrogationResponse=20+=20correct=20LoginComplete=20trigger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reported that even with the Phase 4.9 ack pump, acdream's character still rendered to other clients as the purple loading haze. Spent another round in holtburger's references and found two more gaps in the post-EnterWorld handshake: 1. Server sends DddInterrogation (game opcode 0xF7E5) and waits for the client to acknowledge dat-list versions. We never replied. Build the canonical empty response (12 bytes: opcode + language=1 + count=0 lists) and ship it as soon as DddInterrogation arrives. 2. LoginComplete was being sent immediately after CharacterEnterWorld in Phase 4.8, which is too early — the server hasn't finished creating the player object yet so it ignores LoginComplete and the player stays in transition state. The correct trigger is the server's PlayerCreate (0xF746) game message for our character; that's when holtburger fires send_login_complete (see references/ holtburger/.../client/messages.rs::PlayerCreate handler). Wired both into ProcessDatagram. Removed the unconditional LoginComplete from the EnterWorld flow. Added a _loginCompleteSent latch so re-PlayerCreate (e.g., across portal teleports) doesn't re-fire LoginComplete during the same session. Reference repo cited per the new CLAUDE.md guidance — holtburger is the authoritative client-behavior reference. Should have looked there sooner; this would have saved the Phase 4.8 false fix. 220 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Messages/DddInterrogationResponse.cs | 41 +++++++++++++++++ src/AcDream.Core.Net/WorldSession.cs | 44 +++++++++++++++---- 2 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 src/AcDream.Core.Net/Messages/DddInterrogationResponse.cs 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);