mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -129,6 +129,14 @@ public:
|
||||
*/
|
||||
bool GetPvpEnabled() const;
|
||||
|
||||
/**
|
||||
* The character's guild as other players see it (docs/Guilds.md): guildName is empty while the name waits for
|
||||
* moderation, and guildID 0 is no guild. Serializes again when it changed.
|
||||
*/
|
||||
void SetGuild(LWOOBJID guildID, const std::u16string& guildName);
|
||||
LWOOBJID GetGuildID() const { return m_GuildID; }
|
||||
const std::u16string& GetGuildName() const { return m_GuildName; }
|
||||
|
||||
/**
|
||||
* Returns the characters lifetime reputation
|
||||
* @return The lifetime reputation of this character.
|
||||
@@ -449,6 +457,9 @@ private:
|
||||
*/
|
||||
bool m_DirtySocialInfo = false;
|
||||
|
||||
// Reads the character's guild from the database (on load)
|
||||
void LoadGuild();
|
||||
|
||||
/**
|
||||
* The guild this character is in
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "DEVGMCommands.h"
|
||||
#include "GMGreaterThanZeroCommands.h"
|
||||
#include "GMZeroCommands.h"
|
||||
#include "GuildCommands.h"
|
||||
#include "LiveEvents.h"
|
||||
|
||||
#include "Amf3.h"
|
||||
@@ -1328,6 +1329,61 @@ void SlashCommandHandler::Startup() {
|
||||
};
|
||||
RegisterCommand(ServerUptimeCommand);
|
||||
|
||||
// Guilds (docs/Guilds.md): the client has no guild commands; its guild chat tab sends /g
|
||||
Command guildChatCommand{
|
||||
.help = "Send a message to your guild.",
|
||||
.info = "Send a message to your guild. The guild chat tab sends this.",
|
||||
.aliases = { "g", "guild" },
|
||||
.handle = GuildCommands::Chat,
|
||||
.requiredLevel = eGameMasterLevel::CIVILIAN
|
||||
};
|
||||
RegisterCommand(guildChatCommand);
|
||||
|
||||
Command guildCreateCommand{
|
||||
.help = "Open the window to create a guild.",
|
||||
.info = "Open the window to create a guild (live opened it from the Guild Master NPC).",
|
||||
.aliases = { "guildcreate", "createguild" },
|
||||
.handle = GuildCommands::OpenCreateBox,
|
||||
.requiredLevel = eGameMasterLevel::CIVILIAN
|
||||
};
|
||||
RegisterCommand(guildCreateCommand);
|
||||
|
||||
Command guildKickCommand{
|
||||
.help = "Remove a player from your guild.",
|
||||
.info = "Remove a player from your guild: the leader can remove anyone, officers veterans and recruits.",
|
||||
.aliases = { "gkick", "guildkick" },
|
||||
.handle = GuildCommands::Kick,
|
||||
.requiredLevel = eGameMasterLevel::CIVILIAN
|
||||
};
|
||||
RegisterCommand(guildKickCommand);
|
||||
|
||||
Command guildRankCommand{
|
||||
.help = "Set a guild member's rank: /grank <name> <officer|veteran|recruit>",
|
||||
.info = "Set a guild member's rank. The leader sets any rank; officers move members between veteran and recruit.",
|
||||
.aliases = { "grank", "guildrank" },
|
||||
.handle = GuildCommands::Rank,
|
||||
.requiredLevel = eGameMasterLevel::CIVILIAN
|
||||
};
|
||||
RegisterCommand(guildRankCommand);
|
||||
|
||||
Command guildLeaderCommand{
|
||||
.help = "Hand your guild over to another member: /gleader <name>",
|
||||
.info = "Make another member the guild's leader; you become an officer.",
|
||||
.aliases = { "gleader", "guildleader" },
|
||||
.handle = GuildCommands::Leader,
|
||||
.requiredLevel = eGameMasterLevel::CIVILIAN
|
||||
};
|
||||
RegisterCommand(guildLeaderCommand);
|
||||
|
||||
Command guildDisbandCommand{
|
||||
.help = "Disband your guild: /gdisband confirm",
|
||||
.info = "The leader removes every member and the guild itself.",
|
||||
.aliases = { "gdisband", "guilddisband" },
|
||||
.handle = GuildCommands::Disband,
|
||||
.requiredLevel = eGameMasterLevel::CIVILIAN
|
||||
};
|
||||
RegisterCommand(guildDisbandCommand);
|
||||
|
||||
//Commands that are handled by the client
|
||||
|
||||
Command faqCommand{
|
||||
|
||||
@@ -2,5 +2,6 @@ set(DGAME_DUTILITIES_SLASHCOMMANDS
|
||||
"DEVGMCommands.cpp"
|
||||
"GMGreaterThanZeroCommands.cpp"
|
||||
"GMZeroCommands.cpp"
|
||||
"GuildCommands.cpp"
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
120
dGame/dUtilities/SlashCommands/GuildCommands.cpp
Normal file
120
dGame/dUtilities/SlashCommands/GuildCommands.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "GuildCommands.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
#include "Character.h"
|
||||
#include "ChatPackets.h"
|
||||
#include "ChatServerLink.h"
|
||||
#include "dChatFilter.h"
|
||||
#include "Entity.h"
|
||||
#include "eChatChannel.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "eGuildRank.h"
|
||||
#include "Game.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "PlayerMessages.h"
|
||||
#include "User.h"
|
||||
|
||||
namespace {
|
||||
std::string Lower(std::string text) {
|
||||
for (auto& c : text) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string Trimmed(const std::string& text) {
|
||||
const auto start = text.find_first_not_of(' ');
|
||||
if (start == std::string::npos) return "";
|
||||
return text.substr(start, text.find_last_not_of(' ') - start + 1);
|
||||
}
|
||||
|
||||
LUWString PlayerName(const std::string& name) {
|
||||
return LUWString(GeneralUtils::UTF8ToUTF16(name), 33);
|
||||
}
|
||||
|
||||
void SendRank(Entity* entity, const std::string& name, const eGuildRank rank) {
|
||||
ChatPackets::GuildSetRank request;
|
||||
request.playerID = entity->GetObjectID();
|
||||
request.targetPlayer = PlayerName(name);
|
||||
request.rank = static_cast<uint8_t>(rank);
|
||||
ChatServerLink::Send(request);
|
||||
}
|
||||
}
|
||||
|
||||
void GuildCommands::Chat(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
const auto message = Trimmed(args);
|
||||
if (message.empty()) return;
|
||||
auto* character = entity->GetCharacter();
|
||||
if (!character) return;
|
||||
auto* user = character->GetParentUser();
|
||||
if (user && user->GetIsMuted()) {
|
||||
character->SendMuteNotice();
|
||||
return;
|
||||
}
|
||||
// The same filter as the zone's chat
|
||||
if (!Game::chatFilter->IsSentenceOkay(message, character->GetGMLevel()).empty()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Your message was not sent to your guild.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = GeneralUtils::UTF8ToUTF16(message);
|
||||
ChatPackets::GeneralChatMessage chat;
|
||||
chat.playerID = entity->GetObjectID();
|
||||
chat.chatChannel = eChatChannel::GUILD;
|
||||
chat.messageLength = static_cast<uint32_t>(text.size());
|
||||
chat.senderName = PlayerName(character->GetName());
|
||||
chat.senderID = entity->GetObjectID();
|
||||
chat.senderGMLevel = static_cast<uint8_t>(character->GetGMLevel());
|
||||
chat.message = LUWString(text, static_cast<uint32_t>(text.size()));
|
||||
ChatServerLink::Send(chat);
|
||||
}
|
||||
|
||||
void GuildCommands::OpenCreateBox(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
GameMessages::DisplayGuildCreateBox box;
|
||||
box.target = entity->GetObjectID();
|
||||
box.Send(sysAddr);
|
||||
}
|
||||
|
||||
void GuildCommands::Kick(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
const auto name = Trimmed(args);
|
||||
if (name.empty()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Usage: /gkick <name>");
|
||||
return;
|
||||
}
|
||||
ChatPackets::GuildKick request;
|
||||
request.playerID = entity->GetObjectID();
|
||||
request.kickedPlayer = PlayerName(name);
|
||||
ChatServerLink::Send(request);
|
||||
}
|
||||
|
||||
void GuildCommands::Rank(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
const auto split = GeneralUtils::SplitString(Trimmed(args), ' ');
|
||||
const std::string rankName = split.size() == 2 ? Lower(split[1]) : "";
|
||||
eGuildRank rank = eGuildRank::NONE;
|
||||
if (rankName == "officer") rank = eGuildRank::OFFICER;
|
||||
else if (rankName == "veteran") rank = eGuildRank::VETERAN;
|
||||
else if (rankName == "recruit") rank = eGuildRank::RECRUIT;
|
||||
if (rank == eGuildRank::NONE) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Usage: /grank <name> <officer|veteran|recruit>");
|
||||
return;
|
||||
}
|
||||
SendRank(entity, split[0], rank);
|
||||
}
|
||||
|
||||
void GuildCommands::Leader(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
const auto name = Trimmed(args);
|
||||
if (name.empty()) {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"Usage: /gleader <name>");
|
||||
return;
|
||||
}
|
||||
SendRank(entity, name, eGuildRank::LEADER);
|
||||
}
|
||||
|
||||
void GuildCommands::Disband(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
||||
if (Trimmed(args) != "confirm") {
|
||||
ChatPackets::SendSystemMessage(sysAddr, u"This removes every member and the guild itself. To do it, type /gdisband confirm");
|
||||
return;
|
||||
}
|
||||
ChatPackets::GuildDisband request;
|
||||
request.playerID = entity->GetObjectID();
|
||||
ChatServerLink::Send(request);
|
||||
}
|
||||
23
dGame/dUtilities/SlashCommands/GuildCommands.h
Normal file
23
dGame/dUtilities/SlashCommands/GuildCommands.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef GUILDCOMMANDS_H
|
||||
#define GUILDCOMMANDS_H
|
||||
|
||||
#include <string>
|
||||
|
||||
class Entity;
|
||||
struct SystemAddress;
|
||||
|
||||
/**
|
||||
* Guild slash commands (docs/Guilds.md). The client has none: its guild chat tab sends "/g <text>", which reaches the
|
||||
* server like any unknown command, and it has no controls for kicking, ranks or disbanding. The chat server decides
|
||||
* everything; these only pass the request on.
|
||||
*/
|
||||
namespace GuildCommands {
|
||||
void Chat(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void OpenCreateBox(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void Kick(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void Rank(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void Leader(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
void Disband(Entity* entity, const SystemAddress& sysAddr, const std::string args);
|
||||
}
|
||||
|
||||
#endif // GUILDCOMMANDS_H
|
||||
Reference in New Issue
Block a user