feat(net): Phase 4.8 — send GameAction.LoginComplete after EnterWorld
User reported that when they observed acdream's character through a second AC client running on a different account, the character rendered as a stationary purple haze (AC's "loading screen / portal space" indicator) instead of a normal avatar. The character was "in-world enough" to receive the CreateObject stream but never "in-world enough" for the server to flip its first-enter-world flag, push initial property updates / equipment overrides, or show the character to other clients in the area. Root cause: WorldSession.EnterWorld stopped after sending CharacterEnterWorld (0xF657). The handshake is supposed to continue with one more message — a GameAction(LoginComplete) — that ACE's GameActionLoginComplete handler interprets as "client has exited portal space, mark FirstEnterWorldDone, push property updates, make the character visible to others." Wire layout (confirmed via references/ACE/Source/ACE.Server/Network/GameAction/GameActionPacket.cs and .../Actions/GameActionLoginComplete.cs): u32 game-message opcode = 0xF7B1 (GameAction) u32 sequence = 0 (ACE ignores; TODO comment in source) u32 GameActionType opc = 0x000000A1 (LoginComplete) Send happens immediately after CharacterEnterWorld and just before flipping the WorldSession state to InWorld. acdream has no portal- space transition animation, so we can claim "loading complete" the moment we've sent the EnterWorld message — the dat-side world is already loaded by then. 1 new test (97 Core.Net total). 220 tests green overall. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bb9ff774dc
commit
8744bd6179
3 changed files with 87 additions and 0 deletions
53
src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs
Normal file
53
src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using AcDream.Core.Net.Packets;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Outbound <c>GameAction</c> message announcing that the client has
|
||||
/// finished loading into the world. Without this, the server keeps the
|
||||
/// character in a transitional "exiting portal space" state forever —
|
||||
/// other players see the character rendered as a stationary purple haze
|
||||
/// (AC's loading-screen indicator) instead of a real avatar, and the
|
||||
/// server doesn't push initial property updates / equipment overrides
|
||||
/// to the client either.
|
||||
///
|
||||
/// <para>
|
||||
/// Wire layout (see
|
||||
/// <c>references/ACE/Source/ACE.Server/Network/GameAction/GameActionPacket.cs</c>
|
||||
/// and <c>references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionLoginComplete.cs</c>):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>u32 game-message opcode = 0xF7B1 (<c>GameAction</c>)</item>
|
||||
/// <item>u32 sequence — ACE's <c>GameActionPacket.HandleGameAction</c>
|
||||
/// reads this and currently ignores its value (<c>// TODO: verify
|
||||
/// sequence</c> in the source). 0 is fine.</item>
|
||||
/// <item>u32 GameActionType opcode = 0x000000A1 (<c>LoginComplete</c>)</item>
|
||||
/// <item>(no payload)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// Should be sent immediately after <c>CharacterEnterWorld</c> (0xF657)
|
||||
/// completes the in-world transition. Retail clients send it once the
|
||||
/// portal-space transition animation finishes; we send it as soon as we
|
||||
/// flag the session <c>InWorld</c> because acdream doesn't have a portal-
|
||||
/// space animation yet.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class GameActionLoginComplete
|
||||
{
|
||||
public const uint GameActionOpcode = 0xF7B1u;
|
||||
public const uint LoginCompleteActionType = 0x000000A1u;
|
||||
|
||||
/// <summary>
|
||||
/// Build the body bytes for an outbound <c>GameAction(LoginComplete)</c>.
|
||||
/// Layout: opcode(4) + sequence(4) + actionType(4) = 12 bytes total.
|
||||
/// </summary>
|
||||
public static byte[] Build()
|
||||
{
|
||||
var w = new PacketWriter(16);
|
||||
w.WriteUInt32(GameActionOpcode);
|
||||
w.WriteUInt32(0u); // sequence — server ignores per ACE source
|
||||
w.WriteUInt32(LoginCompleteActionType);
|
||||
return w.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -201,6 +201,15 @@ public sealed class WorldSession : IDisposable
|
|||
if (!serverReady) { Transition(State.Failed); throw new TimeoutException("ServerReady not received"); }
|
||||
|
||||
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());
|
||||
|
||||
Transition(State.InWorld);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public class GameActionLoginCompleteTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_ProducesExactly12Bytes_WithCorrectOpcodes()
|
||||
{
|
||||
var body = GameActionLoginComplete.Build();
|
||||
|
||||
// 4 bytes GameAction opcode + 4 bytes sequence + 4 bytes action type.
|
||||
Assert.Equal(12, body.Length);
|
||||
|
||||
// Little-endian decode.
|
||||
uint gameActionOpcode = (uint)(body[0] | (body[1] << 8) | (body[2] << 16) | (body[3] << 24));
|
||||
uint sequence = (uint)(body[4] | (body[5] << 8) | (body[6] << 16) | (body[7] << 24));
|
||||
uint actionType = (uint)(body[8] | (body[9] << 8) | (body[10] << 16) | (body[11] << 24));
|
||||
|
||||
Assert.Equal(0xF7B1u, gameActionOpcode);
|
||||
Assert.Equal(0u, sequence);
|
||||
Assert.Equal(0x000000A1u, actionType);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue