Files
DarkflameServer/docs/Guilds.md
Aaron Kimbrell 4789711f82 docs(guilds): the login response's events turn guilds on
The client adds every event string of the login response as an enabled feature (AddFeatureGate from the login
response handler), so event_1..event_8 = guilds turns guilds on in a stock client; no client file needs changing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:56:05 -05:00

14 KiB
Raw Blame History

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 and PlayerReady sends nothing.
  • GetIsFeatureEnabled (0x00a32ef0) is true when the name is in either of FeatureGating's two name sets [G]. They are filled by AddFeatureGate (0x00402670) from the client's cdclient.fdb table FeatureGating (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 no guilds row [D], so a stock client turns guilds on only when the login response names it: set one of event_1 … event_8 in sharedconfig.ini to guilds. 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 .lvl file [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::SendTMPGuildCreate copies 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]. /g is not a client command [G], so it reaches the server as a ParseChatMessage like any unknown command.
  • The guild list shows rank as text: 1 Leader, 2 Officer, 3 Veteran, 4 Recruit, anything else nothing [G] (LWOGuildComponent::SendGuildListToUI). Online members' zone is ZoneTable.DisplayDescription of the map id [G]. Online members are listed first, each group sorted by name case-insensitively [G] (RefreshGuildUI).

Client component state

  • LWOCharacterComponent holds guildId (u64) and guildName [G]. They come only from its serialization (below). GetCharIsInGuild / GetCharGuildInfo answer from them (guildId != 0) [G]. SetCharGuildInfo and GuildSetStatus have no handler anywhere [G].
  • LWOGuildComponent holds the member map (GuildData: name, two dates, map<LWOOBJID, {name, zone, rank, online}>) [G], filled by GUILD_DATA and 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 in ChatGuilds): 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's TMP_GUILD_CREATE), GUILD_KICK, GUILD_SET_RANK and GUILD_DISBAND (DLU ids appended to MessageType::Chat), guild chat as GENERAL_CHAT_MESSAGE channel 10. Chat → world: GUILD_GET_STATUS with 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_DATA again.
  • A new member's inviter gets GUILD_INVITE_FINAL_RESPONSE joined and the list again; the other online members get GUILD_ADD_PLAYER (both would print "has joined" for the inviter). Disbanding sends each online member a GUILD_REMOVE_PLAYER about themselves, which makes their client drop the guild.
  • One pending invite per character, answerable for guild_invite_timeout seconds (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_DATA dates are MM/DD/YYYY (UTC): the member's join date and the guild's creation date.
  • Slash commands (the client has none for guilds): /g or /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_CHANGED through master), which tells online members. Guild chat is logged as channel "guild" and is private chat (chat_private).
  • Not done: the client has no packet for a member's name change (GuildUpdatePlayerName is local), so a renamed character shows under the new name after the members' next GUILD_DATA; the guild reputation field is always 0.