feat(db): guilds, guild_members, guild_invites and guild_events

Four tables (MySQL migration 97, SQLite 80) and the IGuilds interface with MySQL, SQLite and TestSQL implementations,
for the guild system (docs/Guilds.md). A guild has a name (unique without regard to case), a name status like
pet_names (1 waiting for moderation, 2 approved), its founder and when it was made. A character is in at most one guild
(guild_members, with its rank: 1 leader, 2 officer, 3 veteran, 4 recruit, the client's names, and when it joined) and
has at most one pending invite (guild_invites). guild_events is each guild's history and outlives the guild. Deleting a
character removes its membership and the invites to and from it. Nothing uses the tables yet.

Check: both migrations run on a fresh and an existing database; DatabaseParityTests Guilds passes against MariaDB
(DLU_TEST_MYSQL_HOST) and SQLite gives the same results. Nothing to check in game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 05:40:38 -05:00
parent c99e2a719f
commit 88ebdf685e
14 changed files with 645 additions and 1 deletions

View File

@@ -65,6 +65,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountEmails::AccountToken, accountId, data
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountNotes::AccountNote, id, accountId, kind, text, actor, createdAt);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IAccountStrikes::Strike, id, accountId, characterId, source, subject, reason, givenById, givenBy, createdAt, revokedAt, revokedBy, revokeReason);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IPermissionGrants::Grant, id, targetType, targetId, kind, name, deny, expiresAt, note, grantedAt, grantedById, grantedBy, revokedAt, revokedBy);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IGuilds::Guild, id, name, nameStatus, founderId, createdAt);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IGuilds::Member, characterId, guildId, rank, joinedAt, name);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IGuilds::Invite, characterId, guildId, inviterId, createdAt);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IGuilds::Event, id, guildId, time, kind, characterId, characterName, actor, detail);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IGuilds::GuildSummary, guild, memberCount, leaderId, leaderName);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IGuilds::GuildPage, total, filtered, guilds);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IApiKeys::ApiKey, id, accountId, name, note, keyHash, keyPrefix, permissions, readOnly, allowedIps, allowedPaths, rateLimit, dailyQuota, createdAt, createdBy, issuedAt, expiresAt, revokedAt, revokedBy, lastUsedAt, lastIp, requestCount, quotaDay, dayCount);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ICharacterSnapshots::CharacterSnapshot, id, characterId, takenAt, reason, actor, size, hash, compressed);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IChatLog::ChatMessage, id, time, channel, senderId, senderName, accountId, recipientId, recipientName, zoneId, instanceId, cloneId, message, blocked);
@@ -1289,6 +1295,63 @@ TEST_F(ParitySeeded, PermissionGrants) {
});
}
TEST_F(ParitySeeded, Guilds) {
constexpr LWOOBJID GONE = 1152921510000900001LL; // no charinfo row
Both("InsertGuild", [](GameDatabase& db) {
return json{ db.InsertGuild({ 0, "Brick Builders", IGuilds::NAME_APPROVED, CHAR_ALICE, 1700000000 }),
db.InsertGuild({ 0, "Maelstrom Hunters", IGuilds::NAME_PENDING, CHAR_BOB, 1700000100 }),
db.InsertGuild({ 0, "Empty", IGuilds::NAME_APPROVED, CHAR_GM, 1700000200 }) };
});
Both("GetGuild", [](GameDatabase& db) { return json{ db.GetGuild(1), db.GetGuild(2), db.GetGuild(99) }; });
Both("GetGuildByName", [](GameDatabase& db) {
return json{ db.GetGuildByName("Brick Builders"), db.GetGuildByName("brick BUILDERS"), db.GetGuildByName("Brick"), db.GetGuildByName("") };
});
Both("AddGuildMember", [&](GameDatabase& db) {
db.AddGuildMember({ CHAR_ALICE, 1, 1, 1700000000 });
db.AddGuildMember({ CHAR_GM, 1, 4, 1700000300 });
db.AddGuildMember({ GONE, 1, 4, 1700000250 });
db.AddGuildMember({ CHAR_BOB, 2, 1, 1700000100 });
});
Both("GetGuildMember", [&](GameDatabase& db) { return json{ db.GetGuildMember(CHAR_ALICE), db.GetGuildMember(GONE), db.GetGuildMember(CHAR_ALICE2) }; });
Both("GetGuildMembers", [](GameDatabase& db) { return json{ db.GetGuildMembers(1), db.GetGuildMembers(2), db.GetGuildMembers(3) }; });
Both("SetGuildMemberRank", [&](GameDatabase& db) { db.SetGuildMemberRank(CHAR_GM, 2); return db.GetGuildMembers(1); });
Both("GetGuildPage", [](GameDatabase& db) {
return json{ db.GetGuildPage(0, 10, "", false), db.GetGuildPage(0, 1, "", false), db.GetGuildPage(1, 10, "", false),
db.GetGuildPage(0, 10, "", true), db.GetGuildPage(0, 10, "hunt", false), db.GetGuildPage(0, 10, "1", false),
db.GetGuildPage(0, 10, "brick", true) };
});
Both("SetGuildName", [](GameDatabase& db) {
db.SetGuildName(2, "Hunters", IGuilds::NAME_APPROVED);
return json{ db.GetGuild(2), db.GetGuildByName("Maelstrom Hunters"), db.GetGuildPage(0, 10, "", true) };
});
Both("SetGuildInvite", [&](GameDatabase& db) {
db.SetGuildInvite({ CHAR_ALICE2, 1, CHAR_ALICE, 1700000400 });
const auto first = db.GetGuildInvite(CHAR_ALICE2);
db.SetGuildInvite({ CHAR_ALICE2, 2, CHAR_BOB, 1700000500 });
return json{ first, db.GetGuildInvite(CHAR_ALICE2), db.GetGuildInvite(CHAR_BOB) };
});
Both("DeleteGuildInvite", [&](GameDatabase& db) { db.DeleteGuildInvite(CHAR_ALICE2); return db.GetGuildInvite(CHAR_ALICE2); });
Both("RemoveGuildMember", [&](GameDatabase& db) { db.RemoveGuildMember(GONE); return json{ db.GetGuildMember(GONE), db.GetGuildMembers(1) }; });
Both("InsertGuildEvent", [&](GameDatabase& db) {
json ids = json::array();
ids.push_back(db.InsertGuildEvent({ 0, 1, 1700000000, "created", CHAR_ALICE, "Alice", "Alice", "Brick Builders" }));
ids.push_back(db.InsertGuildEvent({ 0, 2, 1700000100, "created", CHAR_BOB, "Bob", "Bob", "" }));
ids.push_back(db.InsertGuildEvent({ 0, 1, 1700000300, "joined", CHAR_GM, "GameMaster", "Alice", "" }));
return ids;
});
Both("GetGuildEvents", [](GameDatabase& db) { return json{ db.GetGuildEvents(1, 10), db.GetGuildEvents(0, 2), db.GetGuildEvents(3, 10) }; });
Both("DeleteGuild", [&](GameDatabase& db) {
db.SetGuildInvite({ CHAR_ALICE2, 1, CHAR_ALICE, 1700000600 });
db.DeleteGuild(1);
return json{ db.GetGuild(1), db.GetGuildMembers(1), db.GetGuildMember(CHAR_ALICE), db.GetGuildInvite(CHAR_ALICE2), db.GetGuildEvents(1, 10), db.GetGuildPage(0, 10, "", false) };
});
Both("Guild name unique", [](GameDatabase& db) {
bool threw = false;
try { db.InsertGuild({ 0, "hunters", IGuilds::NAME_APPROVED, CHAR_ALICE, 1700000700 }); } catch (...) { threw = true; }
return threw;
});
}
TEST_F(ParitySeeded, ApiKeys) {
Both("InsertApiKey", [](GameDatabase& db) {
json ids = json::array();