feat(world): guilds in the world: create box, character component, guild commands

The world side of guilds (docs/Guilds.md):

- TMP_GUILD_CREATE, which the client's guild create box sends to its world, goes to the chat server as GUILD_CREATE.
- The character component reads the character's guild from the database when it loads (instead of the old unused "gn"
  and "gid" charxml attributes) and takes GUILD_GET_STATUS from the chat server; a change is serialized once (the
  client redraws the name billboard every time it reads a guild name). A name waiting for moderation isn't shown.
- Slash commands (the client has none for guilds): /g and /guild (guild chat; the client's guild tab sends /g, which
  goes through the chat filter and mute like zone chat, then to the chat server as channel 10), /guildcreate (opens the
  create box with DisplayGuildCreateBox), /gkick, /grank <name> <officer|veteran|recruit>, /gleader and
  /gdisband confirm.
- The Guild Master script (LOT 3001, L_GUILD_CREATE.lua: using it opens the create box). Live never placed it; GMs
  can spawn it.

Check in game (two accounts, a client with a FeatureGating row "guilds", 1, 0, 0 in its cdclient.fdb): /guildcreate,
make a guild (the name shows under yours, the guild button appears on the status bar); invite the second character
from the guild window, accept, both lists show both; guild chat tab; /grank, /gkick, /gleader; leave from the window;
log the second character out and in (guildmate logged off/in lines).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 06:07:19 -05:00
parent d1eec7dacb
commit f6f937955e
12 changed files with 319 additions and 15 deletions

View File

@@ -91,8 +91,8 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo
cmptType.PushDebug<AMFBoolValue>("Has PVP flag turned on") = m_PvpEnabled;
cmptType.PushDebug<AMFIntValue>("GM Level") = GeneralUtils::ToUnderlying(m_GMLevel);
cmptType.PushDebug<AMFIntValue>("Editor level") = GeneralUtils::ToUnderlying(m_EditorLevel);
cmptType.PushDebug<AMFStringValue>("Guild ID") = "0";
cmptType.PushDebug<AMFStringValue>("Guild Name") = "";
cmptType.PushDebug<AMFStringValue>("Guild ID") = std::to_string(m_GuildID);
cmptType.PushDebug<AMFStringValue>("Guild Name") = GeneralUtils::UTF16ToWTF8(m_GuildName);
cmptType.PushDebug<AMFDoubleValue>("Reputation") = m_Reputation;
cmptType.PushDebug<AMFIntValue>("Current Activity Type") = GeneralUtils::ToUnderlying(m_CurrentActivity);
cmptType.PushDebug<AMFDoubleValue>("Property Clone ID") = m_Character->GetPropertyCloneID();
@@ -253,9 +253,28 @@ void CharacterComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInit
outBitStream.Write(m_IsLEGOClubMember);
outBitStream.Write(m_CountryCode);
// Written once per change: the client redraws the name billboard each time it reads a guild name
if (!bIsInitialUpdate) m_DirtySocialInfo = false;
}
}
void CharacterComponent::LoadGuild() {
// The chat server owns guilds (docs/Guilds.md); the character's is read when it loads and GUILD_GET_STATUS brings
// changes. A name waiting for moderation is not shown to other players.
const auto characterID = m_Character ? m_Character->GetObjectID() : m_Parent->GetObjectID();
const auto member = Database::Get()->GetGuildMember(characterID);
const auto guild = member ? Database::Get()->GetGuild(member->guildId) : std::nullopt;
if (!guild) return;
SetGuild(guild->id, guild->nameStatus == IGuilds::NAME_APPROVED ? GeneralUtils::UTF8ToUTF16(guild->name) : u"");
}
void CharacterComponent::SetGuild(const LWOOBJID guildID, const std::u16string& guildName) {
if (m_GuildID == guildID && m_GuildName == guildName) return;
m_GuildID = guildID;
m_GuildName = guildName;
m_DirtySocialInfo = true;
}
bool CharacterComponent::GetPvpEnabled() const {
return m_PvpEnabled;
}
@@ -359,19 +378,7 @@ void CharacterComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
m_EditorEnabled = false; //We're not currently in HF if we're loading in
}
//Annoying guild bs:
const tinyxml2::XMLAttribute* guildName = character->FindAttribute("gn");
if (guildName) {
const char* gn = guildName->Value();
int64_t gid = 0;
character->QueryInt64Attribute("gid", &gid);
if (gid != 0) {
std::string guildname(gn);
m_GuildName = GeneralUtils::UTF8ToUTF16(guildname);
m_GuildID = gid;
m_DirtySocialInfo = true;
}
}
LoadGuild();
if (character->FindAttribute("time")) {
character->QueryUnsigned64Attribute("time", &m_TotalTimePlayed);