14 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. GetIsFeatureEnabled(0x00a32ef0) is true when the name is in either ofFeatureGating's two name sets [G]. They are filled byAddFeatureGate(0x00402670) from the client'scdclient.fdbtableFeatureGating(LWODataCache::Load) and from every event string in the login response (PacketHandler_MSG_CLIENT_LOGIN_RESPONSE, call at 0x00b33466) [G]. The shipped 1.10.64 table has noguildsrow [D], so a stock client turns guilds on only when the login response names it: set one ofevent_1…event_8in sharedconfig.ini toguilds. No client file needs changing.- 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; the login response's events turn it on).
DLU implementation
- The chat server is the guild authority (
dChatServer/GuildManager, connected inChatGuilds): create, invite, answer, leave, kick, ranks, disband, online status, guild chat. State is in the database (guilds,guild_members,guild_invites,guild_events; MySQL migration 97, SQLite 80); client packets go to members through their worlds (WorldRoutePacket). - World → chat:
GUILD_CREATE(from the client'sTMP_GUILD_CREATE),GUILD_KICK,GUILD_SET_RANKandGUILD_DISBAND(DLU ids appended toMessageType::Chat), guild chat asGENERAL_CHAT_MESSAGEchannel 10. Chat → world:GUILD_GET_STATUSwith a character's guild id and the name others may see; the world puts it in the character component. The character component reads the guild from the database when the character loads. - Ranks: leader and officers invite; the leader kicks anyone, officers kick veterans and recruits; the leader sets any
rank, officers move members between veteran and recruit. New members are recruits. A leader who leaves hands the
guild to the highest-ranked, longest-serving member; the last member out ends it. A guild whose leader's character was
deleted gets the next member as leader. Rank changes send every online member
GUILD_DATAagain. - A new member's inviter gets
GUILD_INVITE_FINAL_RESPONSEjoined and the list again; the other online members getGUILD_ADD_PLAYER(both would print "has joined" for the inviter). Disbanding sends each online member aGUILD_REMOVE_PLAYERabout themselves, which makes their client drop the guild. - One pending invite per character, answerable for
guild_invite_timeoutseconds (600) and dropped when the character logs off.guild_max_members(100) per guild. Both in chatconfig.ini. - Names:
GuildNameRules(3-30 of letters, digits, space ' - ., no space at either end or twice; surrounding spaces the player typed are dropped); unique without regard to case; the chat filter's deny list refuses a name (BAD_NAME); a name the allow list doesn't cover makes the guild but waits for moderation, and until it is approved other players see no guild name (members see it in the guild window). GUILD_DATAdates areMM/DD/YYYY(UTC): the member's join date and the guild's creation date.- Slash commands (the client has none for guilds):
/gor/guild <text>(what the guild chat tab sends; chat filter and mute as zone chat),/guildcreate(opens the create box),/gkick <name>,/grank <name> <officer|veteran|recruit>,/gleader <name>,/gdisband confirm. The Guild Master script (LOT 3001) opens the create box when used. - Dashboard: Guilds page (
guilds_manage): list, members, history, approve or reject a name (rejected: renamed "Guild "), rename, remove a member, disband; pending names also in the Review Queue. Changes are audited, added to the guild's history, and sent to the chat server (ePlayerAction::GUILD_CHANGEDthrough master), which tells online members. Guild chat is logged as channel "guild" with the guild's ID, and is private chat (chat_private); each guild's chat history is at/chat_log/guild/<id>(Guild chat on the guild's card). - Not done: the client has no packet for a member's name change (
GuildUpdatePlayerNameis local), so a renamed character shows under the new name after the members' nextGUILD_DATA; the guild reputation field is always 0.