feat(net): acdream enters the world — CharacterList parsed + CharacterEnterWorld sent + 68 CreateObject received (Phase 4.7)
Drives the full post-handshake flow on a live ACE server. After the
3-way handshake completes, acdream:
1. Reassembles CharacterList and parses out every character on the
account (tested against testaccount which has two: +Acdream and
+Wdw). Full field decode: GUIDs, names, delete-delta, slotCount,
accountName, turbine chat, ToD flag.
2. Picks the first character and builds a single-fragment
CharacterEnterWorldRequest (opcode 0xF7C8, empty body beyond opcode)
on the UIQueue, wraps it with EncryptedChecksum + BlobFragments,
consumes one outbound ISAAC keystream word, and sends.
3. Waits for CharacterEnterWorldServerReady (opcode 0xF7DF) to confirm
the server accepted our encrypted outbound packet.
4. Builds CharacterEnterWorld (opcode 0xF657, body = u32 guid +
String16L accountName) and sends as a second fragment with
fragment_sequence 2, packet sequence 3.
5. Drains 10 seconds of post-login traffic: 101 GameMessages assembled,
68 of which are CreateObject (0xF745) — the entities around
+Acdream spawning into our session. Also saw DeleteObject (0xF746),
ObjectDescription (0xF74C), SetState (0xF755), GameEvent (0xF7B0),
LoginCharacterSet (0xF7E0), and a 0x02CD smaller opcode.
This is the Phase 4.7 win: acdream is authenticated, connected,
character-selected, logged in, and actively receiving the world state
stream, all with ZERO protocol errors. Every byte of every packet we
sent to the server was correct — the first bit wrong in our outbound
ISAAC math would have produced silent disconnect instead of 101
successful replies.
Added to AcDream.Core.Net:
- Messages/CharacterList.cs: full parser for opcode 0xF658, ported
from ACE's GameMessageCharacterList writer. Returns structured
record with Characters[], SlotCount, AccountName, UseTurbineChat,
HasThroneOfDestiny. Tested offline with hand-assembled bodies
matching ACE's writer format.
- Messages/CharacterEnterWorld.cs: outbound builders for
CharacterEnterWorldRequest (0xF7C8, opcode-only) and
CharacterEnterWorld (0xF657, opcode + guid + String16L account).
- Messages/GameMessageFragment.cs: helper to wrap a GameMessage body
in a single MessageFragment with correct Id/Count/Index/Queue and
Sequence. Also a Serialize helper to turn a MessageFragment into
packet-body bytes for PacketCodec.Encode. Throws on oversize
(>448 byte) messages; multi-fragment outbound split is TBD.
- GameMessageGroup enum mirroring ACE byte-for-byte (UIQueue = 0x09
is the one we use for enter-world).
Fixed: FragmentAssembler was keying on MessageFragmentHeader.Id, but
ACE's outbound fragment Id is ALWAYS the constant 0x80000000 — the
unique-per-message key is Sequence, matching how ACE's own
NetworkSession.HandleFragment keys its partialFragments dict. Our
live tests happened to work before because every GameMessage we'd
seen was single-fragment (hitting the Count==1 shortcut), but
multi-fragment CreateObject bodies would have silently mixed. Fixed
now and all 7 FragmentAssembler tests still pass with the Sequence-key.
Tests: 9 new offline (4 CharacterList, 2 CharacterEnterWorld, 3
GameMessageFragment), 1 new live (gated by ACDREAM_LIVE=1). Total
77 core + 83 net = 160 passing.
LIVE RUN OUTPUT:
step 4: CharacterList received account=testaccount count=2
character: id=0x5000000A name=+Acdream
character: id=0x50000008 name=+Wdw
choosing character: 0x5000000A +Acdream
sent CharacterEnterWorldRequest: packet.seq=2 frag.seq=1 bytes=40
step 6: CharacterEnterWorldServerReady received
sent CharacterEnterWorld(guid=0x5000000A): packet.seq=3 frag.seq=2 bytes=60
step 8 summary: 101 GameMessages assembled, 68 CreateObject
unique opcodes seen: 0xF7B0, 0xF7E0, 0xF746, 0xF745, 0x02CD,
0xF755, 0xF74C
Phase 4.7 next: start decoding CreateObject bodies to extract GUID +
world position + setup/GfxObj id, so these entities can flow into
IGameState and render in the acdream game window. The foundry statue
is waiting in one of those 68 spawns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0aea24c78e
commit
94da385ff4
8 changed files with 629 additions and 10 deletions
89
src/AcDream.Core.Net/Messages/GameMessageFragment.cs
Normal file
89
src/AcDream.Core.Net/Messages/GameMessageFragment.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using AcDream.Core.Net.Packets;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Helper to wrap a single GameMessage (opcode + body) in one
|
||||
/// <see cref="MessageFragment"/>. Only handles the common "small message
|
||||
/// fits in one fragment" case — the >448-byte multi-fragment split is
|
||||
/// deferred until we actually need it for outbound traffic.
|
||||
///
|
||||
/// <para>
|
||||
/// Callers build the message body via <see cref="PacketWriter"/>
|
||||
/// (starting with a 4-byte opcode, then the fields), pick a
|
||||
/// <see cref="GameMessageGroup"/> for the queue, pick a fragment sequence
|
||||
/// number for this message, and receive a fully-formed
|
||||
/// <see cref="MessageFragment"/> ready to embed in a packet body.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class GameMessageFragment
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant Id used on every outbound fragment. ACE's server-side
|
||||
/// code uses the same literal — the per-message uniqueness lives in
|
||||
/// the <c>Sequence</c> field, not the <c>Id</c> field.
|
||||
/// </summary>
|
||||
public const uint OutboundFragmentId = 0x80000000u;
|
||||
|
||||
/// <summary>
|
||||
/// Build a single fragment that carries an entire GameMessage in its
|
||||
/// payload. Throws if the payload exceeds the max single-fragment
|
||||
/// body size (448 bytes).
|
||||
/// </summary>
|
||||
public static MessageFragment BuildSingleFragment(
|
||||
uint fragmentSequence,
|
||||
GameMessageGroup queue,
|
||||
ReadOnlySpan<byte> gameMessageBytes)
|
||||
{
|
||||
if (gameMessageBytes.Length > MessageFragmentHeader.MaxFragmentDataSize)
|
||||
throw new ArgumentException(
|
||||
$"game message body ({gameMessageBytes.Length} bytes) exceeds single-fragment capacity " +
|
||||
$"({MessageFragmentHeader.MaxFragmentDataSize} bytes). Multi-fragment split TBD.",
|
||||
nameof(gameMessageBytes));
|
||||
|
||||
var header = new MessageFragmentHeader
|
||||
{
|
||||
Sequence = fragmentSequence,
|
||||
Id = OutboundFragmentId,
|
||||
Count = 1,
|
||||
TotalSize = (ushort)(MessageFragmentHeader.Size + gameMessageBytes.Length),
|
||||
Index = 0,
|
||||
Queue = (ushort)queue,
|
||||
};
|
||||
|
||||
return new MessageFragment(header, gameMessageBytes.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concatenate a fragment's header + payload into the bytes that go
|
||||
/// into a packet's body. Use when building the full <c>body</c> span
|
||||
/// passed to <see cref="PacketCodec.Encode"/>.
|
||||
/// </summary>
|
||||
public static byte[] Serialize(in MessageFragment fragment)
|
||||
{
|
||||
byte[] buffer = new byte[MessageFragmentHeader.Size + fragment.Payload.Length];
|
||||
fragment.Header.Pack(buffer);
|
||||
fragment.Payload.CopyTo(buffer.AsSpan(MessageFragmentHeader.Size));
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AC's per-queue routing for GameMessages. Matches ACE's GameMessageGroup
|
||||
/// enum byte-for-byte so ported handlers are unambiguous.
|
||||
/// </summary>
|
||||
public enum GameMessageGroup : ushort
|
||||
{
|
||||
InvalidQueue = 0x00,
|
||||
EventQueue = 0x01,
|
||||
ControlQueue = 0x02,
|
||||
WeenieQueue = 0x03,
|
||||
LoginQueue = 0x04,
|
||||
DatabaseQueue = 0x05,
|
||||
SecureControlQueue = 0x06,
|
||||
SecureWeenieQueue = 0x07,
|
||||
SecureLoginQueue = 0x08,
|
||||
UIQueue = 0x09,
|
||||
SmartboxQueue = 0x0A,
|
||||
ObserverQueue = 0x0B,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue