using System.Buffers.Binary; namespace AcDream.Core.Net.Messages; /// /// Inbound ServerName GameMessage (opcode 0xF7E1). ACE sends /// this in the SAME batch as , right after /// AuthConnectResponse completes — it is the world (server) name the /// retail character-select screen's "World" box shows. /// /// /// Retail wire path: CM_Login::DispatchUI_WorldInfo@0x006ad860 checks /// the leading opcode against 0xf7e1, unpacks the trailing /// PStringBase<char>, and calls /// ClientUISystem::Handle_Login__WorldInfo@0x005641a0(currentConnections, /// maxConnections, worldName), which forwards only the name to /// ECM_Login::SendNotice_WorldName@0x00692b10 (notice id /// 0x186a2). gmCharacterManagementUI registers for that notice /// in its ctor (0x004ec8f0) and both /// RecvNotice_WorldName@0x004ec360 and its own /// UpdateWorldName@0x004ec120 resolve child element 0x1000039B /// (UIElement::GetChildRecursive(m_rootField, 0x1000039b), dynamic-cast /// to UIElement_Text) and call /// UIElement_Text::SetText(Client::GetInstance()->GetWorldName()) — /// Client::GetWorldName@0x00401ca0/SetWorldName@0x00402090 just /// hold the string the notice delivered. The two leading dwords /// (currentConnections/maxConnections) are read off the wire by the /// dispatcher but never consumed by the character-management screen itself. /// /// /// /// ACE: GameMessageOpcode.ServerName = 0xF7E1 /// (ACE.Server/Network/GameMessages/GameMessageOpcode.cs); /// GameMessageServerName /// (ACE.Server/Network/GameMessages/Messages/GameMessageServerName.cs) /// writes i32 currentConnections, i32 maxConnections, String16L /// serverName; sent from /// AuthenticationHandler.SendConnectResponse /// (ACE.Server/Network/Handlers/AuthenticationHandler.cs:258) /// alongside GameMessageCharacterList and /// GameMessageDDDInterrogation. holtburger's /// ServerNameData /// (holtburger-protocol/src/messages/character/types.rs) parses the /// same three fields and cross-checks the field order/types. /// /// /// /// u32 opcode (0xF7E1) /// i32 currentConnections /// i32 maxConnections /// String16L worldName /// /// public static class ServerName { public const uint Opcode = 0xF7E1u; public readonly record struct Parsed( int CurrentConnections, int MaxConnections, string WorldName); /// /// Parse a ServerName body. must start with the /// 4-byte opcode (0xF7E1) — i.e. pass the full reassembled GameMessage /// output from . /// public static Parsed Parse(ReadOnlySpan body) { int pos = 0; uint opcode = ReadU32(body, ref pos); if (opcode != Opcode) throw new FormatException($"expected ServerName opcode 0x{Opcode:X4}, got 0x{opcode:X8}"); int currentConnections = unchecked((int)ReadU32(body, ref pos)); int maxConnections = unchecked((int)ReadU32(body, ref pos)); string worldName = StringReader.ReadString16L(body, ref pos); return new Parsed(currentConnections, maxConnections, worldName); } private static uint ReadU32(ReadOnlySpan source, ref int pos) { if (source.Length - pos < 4) throw new FormatException("truncated u32"); uint value = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(pos)); pos += 4; return value; } }