docs/Guilds.md: what the client's guild system needs, from Ghidra and the client's Scaleform UI. The feature gate (FeatureGating "guilds", missing from the shipped cdclient), the UI calls and messages, what the character component serializes, every guild packet's layout and what the client does with it (including the ones it has no handler for), GUILD_DATA's member records, DisplayGuildCreateBox, the guild chat channel (the chat box sends /g), rank names and the limits the client enforces. No live capture has guild traffic. Check in game: nothing (documentation only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
Guilds
The 1.10.64 client's guild system, and what DLU does with it.
Evidence tags: [G] verified in Ghidra (legouniverse.exe 1.10.64), [F] the client's Scaleform UI
(res/ui/ingame/*.gfx, decompiled with JPEXS), [D] client data (cdclient, locale, scripts, maps), [I] inferred
(no client code decides it; DLU's choice). No live capture contains guild traffic (the available captures were scanned
for chat ids 0x16-0x1c, client ids 0x25-0x30 and game message 626: none).
Enabling the client side
- Everything guild related in the client is gated by
FeatureGating::GetIsFeatureEnabled("guilds")[G]: the guild component registers no handlers, the create/invite/leave UI does nothing andPlayerReadysends nothing. - The feature is read from the client's
cdclient.fdbtableFeatureGating(LWODataCache::Load) [G]. The shipped 1.10.64 table has noguildsrow [D], so guilds are off in a stock client. A rowfeatureName=guilds, major=1, current=0, minor=0turns them on (any version at or below the client's). DLU cannot send this; it is client data. - The Guild Master NPC (LOT 3001, script
scripts\ai\FV\L_GUILD_CREATE.lua:onUse→DisplayGuildCreateBox) exists in the cdclient [D] but is placed in no 1.10.64.lvlfile [D].
UI
| UI file | Calls to C++ | Messages it listens for |
|---|---|---|
guildcreate.gfx [F] |
RequestGuildCreate {guildName} |
ToggleGuildCreate |
guild.gfx [F] |
RequestGuildInvite {playerName}, LeaveGuild {}, UI GuildUIClosed |
ToggleGuildUI, LoadGuildList {guildName, guildMates[] {name, online, zone, rank}} |
statusbar.gfx [F] |
RequestGuildUI {} (guild button) |
EnableGuild {bDisplay} (shows the guild button) |
chatbox.gfx [F] |
ChatString_FlashToCPP {chatField} |
PlayerInGuild, PlayerLeftGuild, SendChat channel 10 → guild tab |
- Guild create input:
maxlength 32[F];LWOGuildComponent::SendTMPGuildCreatecopies at most 30 characters [G]. - The guild window has no kick, promote or disband control [F]. No slash command for guilds is registered in
SlashCommandHandler::InitializeCommands[G]. - Guild chat: typing in the guild tab sends
/g <text>[F]./gis not a client command [G], so it reaches the server as aParseChatMessagelike any unknown command. - The guild list shows rank as text: 1
Leader, 2Officer, 3Veteran, 4Recruit, anything else nothing [G] (LWOGuildComponent::SendGuildListToUI). Online members'zoneisZoneTable.DisplayDescriptionof the map id [G]. Online members are listed first, each group sorted by name case-insensitively [G] (RefreshGuildUI).
Client component state
LWOCharacterComponentholdsguildId(u64) andguildName[G]. They come only from its serialization (below).GetCharIsInGuild/GetCharGuildInfoanswer from them (guildId != 0) [G].SetCharGuildInfoandGuildSetStatushave no handler anywhere [G].LWOGuildComponentholds the member map (GuildData: name, two dates,map<LWOOBJID, {name, zone, rank, online}>) [G], filled byGUILD_DATAand the add/remove/online packets.
Character component serialization (social info) [G]
LWOCharacterComponent::Deserialize, after the GM info and activity blocks:
| Field | Type |
|---|---|
| dirty flag | bit |
| guild id | u64 |
| guild name length | u8 |
| guild name | length × UTF-16 |
| is LEGO club member | bit |
| country code | u32 |
On a non-construction update the client re-renders the name billboard (GuildRenderName, ReRenderNameBillboard).
Triggers (client) [G]
| Event | Client does |
|---|---|
PlayerReady |
if in guild (component guild id ≠ 0): send GUILD_GET_ALL |
ChatConnectionUpdate (connected) |
same |
RequestGuildData (sent by the client itself after GUILD_CREATE_RESPONSE created or GUILD_INVITE_CONFIRM success) |
send GUILD_GET_ALL |
UI RequestGuildCreate |
if not in guild: world packet TMP_GUILD_CREATE; else local "MSG_GUILD_ALREADY_IN_GUILD" |
UI RequestGuildInvite |
CheckInviteSpamming (type 4, local rate limit), then GUILD_INVITE |
UI LeaveGuild |
if in guild: GUILD_LEAVE |
Message box MSG_GUILD_NAME_INVITED_YOU_TO_THE_GUILDNAME_GUILD! |
GUILD_INVITE_RESPONSE, button 1 → declined 0, otherwise declined 1 |
GuildInvite from an ignored player |
GUILD_INVITE_RESPONSE declined 1, no box |
DisplayGuildCreateBox (network GM 626) |
UI ToggleGuildCreate {bShow: true} (bShow from the message is not used) |
Packets
Offsets are into the packet after the 1-byte 0x53: u16 connection type, u32 id, u8 padding, then data at 7 (wire
offset 8). All strings are fixed-size UTF-16 buffers read to the first NUL.
Client → server
The client sends chat-service packets (connection type 2) through its world as ROUTE_PACKET; DLU's world forwards
them to chat with the player's object ID (WorldPackets::RoutePacket::ToChat, which drops the first 4 data bytes).
| Packet | Id | Bytes | Layout (from offset 7) | Evidence |
|---|---|---|---|---|
World TMP_GUILD_CREATE |
4 / 20 | 69 | wchar[31] name (≤ 30 characters) | [G] SendTMPGuildCreate |
Chat GUILD_INVITE |
2 / 23 | 81 | u64 0, wchar[33] player name | [G] SendGuildInvitePacket |
Chat GUILD_INVITE_RESPONSE |
2 / 24 | 16 | u64 0, u8 declined | [G] LWOGuildComponent::SendMessage |
Chat GUILD_LEAVE |
2 / 25 | 81 | u64 0, then 66 bytes of stack (the client's length is wrong) | [G] LeaveGuild |
Chat GUILD_GET_ALL |
2 / 28 | 15 | u64 0 | [G] SendGuildGetAll |
GUILD_CREATE (22), GUILD_KICK (26) and GUILD_GET_STATUS (27) are never sent by the client [G].
Server → client (MessageType::Client, connection type 5)
| Packet | Id | Layout (from offset 7) | Client does | Evidence |
|---|---|---|---|---|
GUILD_CREATE_RESPONSE |
37 | u8 result, u64 at 8 (unread; DLU: guild id [I]), wchar[31] name at 16 | 0 → "MSG_GUILD_GUILD_NAME_CREATED" (name) + RequestGuildData; 1 → "…NAME_CANT_BE_USED"; 2 → "…NAME_ALREADY_IN_USE"; else "…COULD_NOT_BE_CREATED" (chat channel 0) |
[G] |
GUILD_GET_STATUS_RESPONSE |
38 | — | no handler | [G] |
GUILD_INVITE |
39 | wchar[33] inviter name, wchar[31] guild name at 73 | "MSG_GUILD_NAME_WANTS_YOU_TO_BE_IN_NAME_GUILD!" unless the inviter is ignored, then game message GuildInvite → message box |
[G] |
GUILD_INVITE_INITIAL_RESPONSE |
40 | u8 code, wchar[33] name | 0 "…INVITE_SENT_TO_NAME", 1 "MSG_GENERIC_NAME_IS_NOT_ONLINE", 2 "…NAME_IS_ALREADY_IN_A_GUILD", 3 "…ALREADY_HAS_A_GUILD_INVITE_PENDING", else "…COULD_NOT_INVITE_NAME" | [G] |
GUILD_INVITE_FINAL_RESPONSE |
41 | u8 code, wchar[33] name | 0 "…NAME_HAS_JOINED_THE_GUILD", 1 "…DECLINED_YOUR_INVITATION", 2 "…IS_NOT_ONLINE", else "CLIENTMSG_COULD_NOT_INVITE_NAME" | [G] |
GUILD_INVITE_CONFIRM |
42 | u8 failed, wchar[33] guild name | 0 → "MSG_GUILD_YOU_JOINED_THE_GUILD_NAME" + RequestGuildData; else "…YOU_COULD_NOT_BE_ADDED…" |
[G] |
GUILD_ADD_PLAYER |
43 | wchar[33] name, u64 id at 73, u8 rank at 81, LWOZoneID at 82, u8 online at 90 | "…NAME_HAS_JOINED_THE_GUILD" (channel 10); adds the member if the guild data is loaded | [G] |
GUILD_REMOVE_PLAYER |
44 | u8 reason (0 left, 1 kicked), wchar[33] name, u64 id at 74, u64 new leader at 82 | message (channel 10); id = self → guild UI cleared (PlayerLeftGuild, EnableGuild false); else member removed, new leader's rank set to 1 |
[G] |
GUILD_LOGIN_LOGOUT |
45 | wchar[33] name, u64 id at 73, u8 online at 81, LWOZoneID at 82, u8 world-update-only at 90 | unless world-update-only: "…GUILDMATE_NAME_LOGGED_IN/OFF" (channel 10); sets online + zone | [G] |
GUILD_RANK_CHANGE |
46 | — | no handler | [G] |
GUILD_DATA |
47 | below | PopulateGuildData |
[G] |
GUILD_STATUS |
48 | — | no handler | [G] |
GuildSetPlayerRank (game message 590) is a no-op in LWOGuildComponent [G]: a rank change reaches the client only
through a new GUILD_DATA.
GUILD_DATA [G]
| Offset | Type | Field |
|---|---|---|
| 7 | u8 | status; only 0 is used (anything else: ignored) |
| 8 | wchar[31] | guild name |
| 70 | wchar[11] | date (ends up in GuildData.wsJoinDate) |
| 92 | wchar[11] | date (ends up in GuildData.wsFoundDate) |
| 114 | i32 | reputation (not used) |
| 118 | i32, i32, u16 | not read |
| 128 | u16 | member count (0: ignored) |
| 130 | member × count |
Member (84 bytes): u8 rank, u8 online, LWOZoneID zone (8 bytes, read only when online), u64 object id, wchar[33] name.
The dates are not shown by the UI (setGuildInfo(name, "") [F]); their format is unknown ([I] DLU sends MM/DD/YYYY).
On success the client shows the guild button and guild chat tab (PlayerInGuild, EnableGuild true).
Game message
| Message | Id | Direction | Fields | Evidence |
|---|---|---|---|---|
DisplayGuildCreateBox |
626 | server → client | bit bShow | [G] Serialize 0x00dbb260, Deserialize 0x00dbb2a0 |
Game messages 578-596 (GuildGetSize … GuildRenderName) are client-local [G]: their Serialize/Deserialize slots are
the shared no-op stubs (e.g. GuildInvite vtable 0x015932ac: 0x00411820, 0x004175d0); the packet handlers above create
them.
Chat channels
eChatChannel 10 GUILD, 11 GUILDNOTIFY [G][F]. Guild chat arrives at the client as MSG_CHAT_PRIVATE_CHAT_MESSAGE
(or general chat) with channel 10; PacketHandler_MSG_CHAT_PRIVATE_CHAT_MESSAGE passes the channel to the UI, which
routes 10 to the guild tab [G][F]. Channel 11 has no UI box [F].
Limits
| Rule | Value | Evidence |
|---|---|---|
| Name length | ≤ 30 characters (client), ≥ 3 [I] | [G] / [I] |
| Name characters | letters, digits, space, ' - .; no leading/trailing or double spaces [I] |
[I] |
| Name uniqueness | case-insensitive [I] | "…NAME_ALREADY_IN_USE" exists [G] |
| Members | guild_max_members (chat config, default 100) [I] |
none in client |
| Ranks | 1 Leader, 2 Officer, 3 Veteran, 4 Recruit | [G] |
| One guild per character, one pending invite per character | "…ALREADY_IN_A_GUILD", "…INVITE_PENDING" [G] |
Locale strings [D]
MSG_GUILD_* (41 ids) and UI_GUILD_*, UI_GUILDCREATE_*, UI_CHAT_CHGUILD, UI_CHAT_GUILD_CHAT in locale.xml.
No cdclient table is about guilds (besides FeatureGating, which lacks the row).