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