mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
201 lines
14 KiB
Markdown
201 lines
14 KiB
Markdown
# 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 <id>"), 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" 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 (`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.
|