Files
DarkflameServer/dChatServer/GuildManager.h
Aaron Kimbrell e49c22374b feat(chat): GuildManager, the guild rules of the chat server
The chat server is the guild authority (docs/Guilds.md). GuildManager keeps guilds in the database (IGuilds) and tells
clients through their worlds, with every outside dependency behind hooks so the rules are tested without a server:

- Create: name rules (3-30 of letters, digits, space ' - ., no space at either end or twice), the chat filter's deny
  list refuses a name (BAD_NAME), names not on the allow list make the guild but wait for moderation (other players see
  no guild name until then), a taken name (case ignored) is EXISTS, someone in a guild can't make one.
- Invite / answer: leaders and officers invite; the client's answers for not online, already in a guild, invite pending
  and could not invite; one invite per player, answerable for 10 minutes and gone when the player logs off; a full guild
  (guild_max_members) takes nobody. The new member is a recruit; the inviter hears the answer and gets the list again,
  the other members get GUILD_ADD_PLAYER.
- Leave, and DLU's kick, rank and disband (the client has no controls for them): the leader kicks anyone, officers kick
  veterans and recruits; a leader who leaves hands the guild to the highest-ranked, longest-serving member, and the last
  one out ends it. Rank changes send everyone the list again (the client's GuildSetPlayerRank does nothing).
- GUILD_DATA for GUILD_GET_ALL, login/logout/world-change updates to guildmates, a guild that lost its leader (character
  deleted) gets one again, and GuildChanged catches online players up with what the dashboard did.
- Every change is a guild_events row. Each character's world gets GUILD_GET_STATUS with the guild and the name others
  may see.

Not connected to the chat server's packets yet.

Check: GuildManagerTests (18 tests) run every rule against an in-memory IGuilds. Nothing to check in game yet.

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

117 lines
4.7 KiB
C++

#ifndef GUILDMANAGER_H
#define GUILDMANAGER_H
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "dCommonVars.h"
#include "eGuildRank.h"
#include "IGuilds.h"
struct LUBitStream;
/**
* The chat server's guilds (docs/Guilds.md): it is the guild authority. State is in the database (IGuilds); what it
* tells clients goes through their worlds. It only runs on the chat server's main thread.
*
* Everything outside the guild tables is reached through Hooks, so the rules can be tested without a server.
*/
class GuildManager {
public:
// What the chat server knows about an online player
struct OnlinePlayer {
LWOOBJID id{};
std::string name;
LWOZONEID zone{};
};
// The chat filter's verdict on a guild name
enum class NameCheck {
APPROVED, // on the allow list
PENDING, // not on the deny list, not (all) on the allow list: a moderator decides
DENIED, // on the deny list
};
struct Hooks {
std::function<std::optional<OnlinePlayer>(LWOOBJID)> findOnline;
std::function<std::optional<OnlinePlayer>(const std::string&)> findOnlineByName;
// Whether a character with this name exists (for "not online" versus "could not invite")
std::function<bool(const std::string&)> characterExists;
// A client packet to an online player (routed through their world)
std::function<void(LWOOBJID, const LUBitStream&)> sendToPlayer;
// A chat -> world packet to the world an online player is in
std::function<void(LWOOBJID, const LUBitStream&)> sendToWorldOf;
// A system chat line to an online player
std::function<void(LWOOBJID, const std::string&)> notify;
std::function<NameCheck(const std::string&)> checkName;
std::function<int64_t()> now;
};
struct Settings {
uint32_t maxMembers = 100;
int64_t inviteTimeout = 600; // seconds an invite can be answered
};
GuildManager(IGuilds& db, Hooks hooks, Settings settings);
// GuildNameRules::IsValid
static bool IsValidName(const std::u16string& name);
// The client's rank rules as DLU sets them (the client lets anyone try: GuildCanWeInvite/Kick are always true)
static bool CanInvite(eGuildRank rank);
static bool CanKick(eGuildRank actor, eGuildRank target);
static bool CanSetRank(eGuildRank actor, eGuildRank target, eGuildRank newRank);
// From the client (through its world)
void Create(LWOOBJID playerID, const std::u16string& name);
void Invite(LWOOBJID playerID, const std::string& targetName);
void AnswerInvite(LWOOBJID playerID, bool declined);
void Leave(LWOOBJID playerID);
void GetAll(LWOOBJID playerID);
// DLU slash commands (the client has no controls for them)
void Kick(LWOOBJID playerID, const std::string& targetName);
void SetRank(LWOOBJID playerID, const std::string& targetName, eGuildRank rank);
void Disband(LWOOBJID playerID);
// A player came online (login) or changed worlds
void PlayerOnline(LWOOBJID playerID, bool login);
// A player went offline (before the chat server forgets them)
void PlayerOffline(LWOOBJID playerID);
// The dashboard changed a guild in the database (renamed, name moderated, disbanded, member removed): tell the
// online players what changed for them
void GuildChanged(int64_t guildID);
// Online members of the player's guild, the player included (empty: not in a guild)
std::vector<LWOOBJID> OnlineGuildmates(LWOOBJID playerID);
// The name other players see: nothing while it waits for moderation
static std::string ShownName(const IGuilds::Guild& guild);
// MM/DD/YYYY, UTC
static std::string FormatDate(int64_t time);
private:
void SendStatus(LWOOBJID characterID, int64_t guildID, const std::string& shownName);
void SendData(LWOOBJID playerID);
void SendDataToOnline(int64_t guildID);
void LogEvent(int64_t guildID, const std::string& kind, LWOOBJID characterID, const std::string& characterName, const std::string& actor, const std::string& detail = "");
// The members, with a leader again if the guild lost its (the leader's character was deleted). Empty: the guild is
// gone (deleted when it had no members left).
std::vector<IGuilds::Member> Members(int64_t guildID);
std::optional<IGuilds::Member> FindMember(const std::vector<IGuilds::Member>& members, const std::string& name) const;
// Removes a member and tells everyone; hands the guild on when the leader goes, deletes it with the last member
void RemoveMember(const IGuilds::Guild& guild, const IGuilds::Member& member, eGuildLeaveReason reason, const std::string& actor);
std::string NameOf(LWOOBJID playerID, const std::string& fallback = "");
IGuilds& m_Db;
Hooks m_Hooks;
Settings m_Settings;
// Online players' guilds, to know who to tell when the dashboard changes a guild behind the chat server's back
std::unordered_map<LWOOBJID, int64_t> m_OnlineGuild;
};
#endif // GUILDMANAGER_H