From 96d07f1c046f0d8adc9ecb4bfa1bdf5bec9499c0 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Fri, 20 Feb 2026 22:06:55 +0100 Subject: [PATCH] refactor(channelserver): extract GuildRepository for guild table access Per anti-patterns.md item #9, guild-related SQL was scattered across ~15 handler files with no repository abstraction. Following the same pattern established by CharacterRepository, this centralizes all guilds, guild_characters, and guild_applications table access into a single GuildRepository (~30 methods). guild_model.go and handlers_guild_member.go are trimmed to types and pure business logic only. All handler files (guild_*, festa, mail, house, mercenary, rengoku) now call s.server.guildRepo methods instead of direct DB queries or methods on domain objects. --- docs/anti-patterns.md | 8 +- server/channelserver/guild_model.go | 491 ---------------- server/channelserver/handlers_festa.go | 14 +- server/channelserver/handlers_guild.go | 61 +- .../channelserver/handlers_guild_adventure.go | 6 +- .../channelserver/handlers_guild_alliance.go | 8 +- server/channelserver/handlers_guild_board.go | 6 +- .../channelserver/handlers_guild_cooking.go | 6 +- server/channelserver/handlers_guild_info.go | 155 +++-- server/channelserver/handlers_guild_member.go | 113 ---- server/channelserver/handlers_guild_ops.go | 68 +-- server/channelserver/handlers_guild_scout.go | 32 +- .../channelserver/handlers_guild_tresure.go | 4 +- server/channelserver/handlers_house.go | 10 +- server/channelserver/handlers_mail.go | 4 +- server/channelserver/handlers_mercenary.go | 2 +- server/channelserver/handlers_rengoku.go | 7 +- server/channelserver/repo_guild.go | 472 ++++++++++++++++ server/channelserver/repo_guild_test.go | 531 ++++++++++++++++++ server/channelserver/sys_channel_server.go | 2 + server/channelserver/testhelpers_db.go | 35 ++ 21 files changed, 1244 insertions(+), 791 deletions(-) create mode 100644 server/channelserver/repo_guild.go create mode 100644 server/channelserver/repo_guild_test.go diff --git a/docs/anti-patterns.md b/docs/anti-patterns.md index d88b027e1..707a54c2c 100644 --- a/docs/anti-patterns.md +++ b/docs/anti-patterns.md @@ -240,7 +240,9 @@ The same table is queried in different handlers with slightly different column s **Recommendation:** At minimum, define query constants. Ideally, introduce a repository layer that encapsulates all queries for a given entity. -**Status (substantial):** A `CharacterRepository` layer in `repo_character.go` now centralizes nearly all `characters` table access (27 methods). The initial PR introduced 9 core methods and rewired the 4 helpers + 6 direct queries (~70%). A second pass migrated ~56 additional inline queries across 13 handler files (cafe, misc, clients, plate, rengoku, mercenary, gacha, guild_board, guild_scout, data, items, house, session), bringing coverage to ~95% of character queries. Remaining unmigrated queries are cross-table JOINs (house+user_binary, mercenary+guild_characters, session auth), the bulk `CharacterSaveData` read/write, and a `handlers_commands.go` subquery through `users`. Next steps: guild repository (second-highest duplication) and column allowlist for SQL injection hardening. +**Status (substantial):** A `CharacterRepository` layer in `repo_character.go` now centralizes nearly all `characters` table access (27 methods). The initial PR introduced 9 core methods and rewired the 4 helpers + 6 direct queries (~70%). A second pass migrated ~56 additional inline queries across 13 handler files (cafe, misc, clients, plate, rengoku, mercenary, gacha, guild_board, guild_scout, data, items, house, session), bringing coverage to ~95% of character queries. Remaining unmigrated queries are cross-table JOINs (house+user_binary, mercenary+guild_characters, session auth), the bulk `CharacterSaveData` read/write, and a `handlers_commands.go` subquery through `users`. + +A `GuildRepository` layer in `repo_guild.go` now centralizes all `guilds`, `guild_characters`, and `guild_applications` table access (~30 methods). All guild handler files plus cross-cutting callers (`handlers_festa.go`, `handlers_mail.go`, `handlers_house.go`, `handlers_mercenary.go`, `handlers_rengoku.go`) have been migrated. Remaining unmigrated guild subsystem tables: `guild_posts`, `guild_adventures`, `guild_meals`, `guild_hunts`, `guild_hunts_claimed`, `guild_alliances`. Next steps: column allowlist for SQL injection hardening. --- @@ -304,7 +306,7 @@ Database operations use raw `database/sql` with PostgreSQL-specific syntax throu | Severity | Anti-patterns | |----------|--------------| | **High** | ~~Missing ACK responses / softlocks (#2)~~ **Fixed**, no architectural layering (#3), tight DB coupling (#13) | -| **Medium** | ~~Magic numbers (#4)~~ **Fixed**, ~~inconsistent binary I/O (#5)~~ **Resolved**, Session god object (#6), ~~copy-paste handlers (#8)~~ **Fixed**, ~~raw SQL duplication (#9)~~ **Substantially fixed** (characters table ~95% migrated) | +| **Medium** | ~~Magic numbers (#4)~~ **Fixed**, ~~inconsistent binary I/O (#5)~~ **Resolved**, Session god object (#6), ~~copy-paste handlers (#8)~~ **Fixed**, ~~raw SQL duplication (#9)~~ **Substantially fixed** (characters ~95%, guild core tables 100% migrated) | | **Low** | God files (#1), ~~`init()` registration (#10)~~ **Fixed**, ~~inconsistent logging (#12)~~ **Fixed**, mutex granularity (#7), ~~panic-based flow (#11)~~ **Fixed** | ### Root Cause @@ -316,6 +318,6 @@ Most of these anti-patterns stem from a single root cause: **the codebase grew o 1. **Add fail ACKs to silent error paths** — prevents player softlocks, ~70 existing doAckFail calls prove safety, low risk, can be done handler-by-handler 2. **Extract a character repository layer** — 152 queries across 26 files touch the `characters` table, highest SQL duplication 3. **Extract load/save helpers** — 38 handlers repeat the same ~10-15 line template, mechanical extraction -4. **Extract a guild repository layer** — 32 queries across 8-15 files, second-highest SQL duplication +4. ~~**Extract a guild repository layer**~~ — **Done.** `repo_guild.go` with ~30 methods covers `guilds`, `guild_characters`, `guild_applications` tables 5. **Define protocol constants** — 1,052 hex literals with 174 unique values, improves documentation 6. ~~**Standardize binary I/O**~~ — already standardized on `byteframe`; remaining `encoding/binary` uses are correct (see #5) diff --git a/server/channelserver/guild_model.go b/server/channelserver/guild_model.go index 073fe651d..a1bb4937a 100644 --- a/server/channelserver/guild_model.go +++ b/server/channelserver/guild_model.go @@ -1,18 +1,10 @@ package channelserver import ( - "database/sql" "database/sql/driver" "encoding/json" - "errors" - "erupe-ce/common/mhfitem" _config "erupe-ce/config" - "fmt" "time" - - "erupe-ce/common/byteframe" - "github.com/jmoiron/sqlx" - "go.uber.org/zap" ) // FestivalColor is a festival color identifier string. @@ -150,486 +142,3 @@ func (g *Guild) Rank(mode _config.Mode) uint16 { } return 17 } - -const guildInfoSelectQuery = ` -SELECT - g.id, - g.name, - rank_rp, - event_rp, - room_rp, - COALESCE(room_expiry, '1970-01-01') AS room_expiry, - main_motto, - sub_motto, - created_at, - leader_id, - c.name AS leader_name, - comment, - COALESCE(pugi_name_1, '') AS pugi_name_1, - COALESCE(pugi_name_2, '') AS pugi_name_2, - COALESCE(pugi_name_3, '') AS pugi_name_3, - pugi_outfit_1, - pugi_outfit_2, - pugi_outfit_3, - pugi_outfits, - recruiting, - COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color, - COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls, - COALESCE(( - SELECT id FROM guild_alliances ga WHERE - ga.parent_id = g.id OR - ga.sub1_id = g.id OR - ga.sub2_id = g.id - ), 0) AS alliance_id, - icon, - (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count - FROM guilds g - JOIN guild_characters gc ON gc.character_id = leader_id - JOIN characters c on leader_id = c.id -` - -func (guild *Guild) Save(s *Session) error { - _, err := s.server.db.Exec(` - UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, - pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1 - `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, - guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID) - - if err != nil { - s.logger.Error("failed to update guild data", zap.Error(err), zap.Uint32("guildID", guild.ID)) - return err - } - - return nil -} - -func (guild *Guild) CreateApplication(s *Session, charID uint32, applicationType GuildApplicationType, transaction *sql.Tx) error { - - query := ` - INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) - VALUES ($1, $2, $3, $4) - ` - - var err error - - if transaction == nil { - _, err = s.server.db.Exec(query, guild.ID, charID, s.charID, applicationType) - } else { - _, err = transaction.Exec(query, guild.ID, charID, s.charID, applicationType) - } - - if err != nil { - s.logger.Error( - "failed to add guild application", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - return err - } - - return nil -} - -func (guild *Guild) Disband(s *Session) error { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to begin transaction", zap.Error(err)) - return err - } - - _, err = transaction.Exec("DELETE FROM guild_characters WHERE guild_id = $1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild characters", zap.Error(err), zap.Uint32("guildId", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("DELETE FROM guilds WHERE id = $1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("DELETE FROM guild_alliances WHERE parent_id=$1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit transaction", zap.Error(err)) - return err - } - - s.logger.Info("Character disbanded guild", zap.Uint32("charID", s.charID), zap.Uint32("guildID", guild.ID)) - - return nil -} - -func (guild *Guild) RemoveCharacter(s *Session, charID uint32) error { - _, err := s.server.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID) - - if err != nil { - s.logger.Error( - "failed to remove character from guild", - zap.Error(err), - zap.Uint32("charID", charID), - zap.Uint32("guildID", guild.ID), - ) - - return err - } - - return nil -} - -func (guild *Guild) AcceptApplication(s *Session, charID uint32) error { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to start db transaction", zap.Error(err)) - return err - } - - _, err = transaction.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID) - - if err != nil { - s.logger.Error("failed to accept character's guild application", zap.Error(err)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec(` - INSERT INTO guild_characters (guild_id, character_id, order_index) - VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1)) - `, guild.ID, charID) - - if err != nil { - s.logger.Error( - "failed to add applicant to guild", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - rollbackTransaction(s, transaction) - return err - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit db transaction", zap.Error(err)) - rollbackTransaction(s, transaction) - return err - } - - return nil -} - -// This is relying on the fact that invitation ID is also character ID right now -// if invitation ID changes, this will break. -func (guild *Guild) CancelInvitation(s *Session, charID uint32) error { - _, err := s.server.db.Exec( - `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`, - charID, guild.ID, - ) - - if err != nil { - s.logger.Error( - "failed to cancel guild invitation", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - return err - } - - return nil -} - -func (guild *Guild) RejectApplication(s *Session, charID uint32) error { - _, err := s.server.db.Exec( - `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`, - charID, guild.ID, - ) - - if err != nil { - s.logger.Error( - "failed to reject guild application", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - return err - } - - return nil -} - -func (guild *Guild) ArrangeCharacters(s *Session, charIDs []uint32) error { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to start db transaction", zap.Error(err)) - return err - } - - for i, id := range charIDs { - _, err := transaction.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id) - - if err != nil { - err = transaction.Rollback() - - if err != nil { - s.logger.Error("failed to rollback db transaction", zap.Error(err)) - } - - return err - } - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit db transaction", zap.Error(err)) - return err - } - - return nil -} - -func (guild *Guild) GetApplicationForCharID(s *Session, charID uint32, applicationType GuildApplicationType) (*GuildApplication, error) { - row := s.server.db.QueryRowx(` - SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3 - `, charID, guild.ID, applicationType) - - application := &GuildApplication{} - - err := row.StructScan(application) - - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - - if err != nil { - s.logger.Error( - "failed to retrieve guild application for character", - zap.Error(err), - zap.Uint32("charID", charID), - zap.Uint32("guildID", guild.ID), - ) - return nil, err - } - - return application, nil -} - -func (guild *Guild) HasApplicationForCharID(s *Session, charID uint32) (bool, error) { - row := s.server.db.QueryRowx(` - SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2 - `, charID, guild.ID) - - num := 0 - - err := row.Scan(&num) - - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - - if err != nil { - s.logger.Error( - "failed to retrieve guild applications for character", - zap.Error(err), - zap.Uint32("charID", charID), - zap.Uint32("guildID", guild.ID), - ) - return false, err - } - - return true, nil -} - -// CreateGuild creates a new guild in the database and adds the session's character as its leader. -func CreateGuild(s *Session, guildName string) (int32, error) { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to start db transaction", zap.Error(err)) - return 0, err - } - - guildResult, err := transaction.Query( - "INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id", - guildName, s.charID, - ) - - if err != nil { - s.logger.Error("failed to create guild", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - var guildId int32 - - guildResult.Next() - - err = guildResult.Scan(&guildId) - - if err != nil { - s.logger.Error("failed to retrieve guild ID", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - err = guildResult.Close() - - if err != nil { - s.logger.Error("failed to finalise query", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - _, err = transaction.Exec(` - INSERT INTO guild_characters (guild_id, character_id) - VALUES ($1, $2) - `, guildId, s.charID) - - if err != nil { - s.logger.Error("failed to add character to guild", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit guild creation", zap.Error(err)) - return 0, err - } - - return guildId, nil -} - -func rollbackTransaction(s *Session, transaction *sql.Tx) { - err := transaction.Rollback() - - if err != nil { - s.logger.Error("failed to rollback transaction", zap.Error(err)) - } -} - -// GetGuildInfoByID retrieves guild info by guild ID, returning nil if not found. -func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf(` - %s - WHERE g.id = $1 - LIMIT 1 - `, guildInfoSelectQuery), guildID) - - if err != nil { - s.logger.Error("failed to retrieve guild", zap.Error(err), zap.Uint32("guildID", guildID)) - return nil, err - } - - defer func() { _ = rows.Close() }() - - hasRow := rows.Next() - - if !hasRow { - return nil, nil - } - - return buildGuildObjectFromDbResult(rows, err, s) -} - -// GetGuildInfoByCharacterId retrieves guild info for a character, including applied guilds. -func GetGuildInfoByCharacterId(s *Session, charID uint32) (*Guild, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf(` - %s - WHERE EXISTS( - SELECT 1 - FROM guild_characters gc1 - WHERE gc1.character_id = $1 - AND gc1.guild_id = g.id - ) - OR EXISTS( - SELECT 1 - FROM guild_applications ga - WHERE ga.character_id = $1 - AND ga.guild_id = g.id - AND ga.application_type = 'applied' - ) - LIMIT 1 - `, guildInfoSelectQuery), charID) - - if err != nil { - s.logger.Error("failed to retrieve guild for character", zap.Error(err), zap.Uint32("charID", charID)) - return nil, err - } - - defer func() { _ = rows.Close() }() - - hasRow := rows.Next() - - if !hasRow { - return nil, nil - } - - return buildGuildObjectFromDbResult(rows, err, s) -} - -func buildGuildObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*Guild, error) { - guild := &Guild{} - - err := result.StructScan(guild) - - if err != nil { - s.logger.Error("failed to retrieve guild data from database", zap.Error(err)) - return nil, err - } - - return guild, nil -} - -func guildGetItems(s *Session, guildID uint32) []mhfitem.MHFItemStack { - var data []byte - var items []mhfitem.MHFItemStack - if err := s.server.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data); err != nil && !errors.Is(err, sql.ErrNoRows) { - s.logger.Error("Failed to get guild item box", zap.Error(err)) - } - if len(data) > 0 { - box := byteframe.NewByteFrameFromBytes(data) - numStacks := box.ReadUint16() - box.ReadUint16() // Unused - for i := 0; i < int(numStacks); i++ { - items = append(items, mhfitem.ReadWarehouseItem(box)) - } - } - return items -} diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 1a4a61ee3..62a37ace2 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -389,10 +389,10 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { // state festa (U)ser func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStateFestaU) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) applicant := false if guild != nil { - applicant, _ = guild.HasApplicationForCharID(s, s.charID) + applicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID) } if err != nil || guild == nil || applicant { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -418,10 +418,10 @@ func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { // state festa (G)uild func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStateFestaG) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) applicant := false if guild != nil { - applicant, _ = guild.HasApplicationForCharID(s, s.charID) + applicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID) } resp := byteframe.NewByteFrame() if err != nil || guild == nil || applicant { @@ -443,12 +443,12 @@ func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaMember) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil || guild == nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } - members, err := GetGuildMembers(s, guild.ID, false) + members, err := s.server.guildRepo.GetMembers(guild.ID, false) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return @@ -487,7 +487,7 @@ func handleMsgMhfVoteFesta(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEntryFesta) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil || guild == nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index 2844e5968..97c358b5f 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -14,7 +14,7 @@ import ( func handleMsgMhfCreateGuild(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCreateGuild) - guildId, err := CreateGuild(s, pkt.Name) + guildId, err := s.server.guildRepo.Create(s.charID, pkt.Name) if err != nil { bf := byteframe.NewByteFrame() @@ -37,7 +37,7 @@ func handleMsgMhfCreateGuild(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfArrangeGuildMember(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfArrangeGuildMember) - guild, err := GetGuildInfoByID(s, pkt.GuildID) + guild, err := s.server.guildRepo.GetByID(pkt.GuildID) if err != nil { s.logger.Error( @@ -57,7 +57,7 @@ func handleMsgMhfArrangeGuildMember(s *Session, p mhfpacket.MHFPacket) { return } - err = guild.ArrangeCharacters(s, pkt.CharIDs) + err = s.server.guildRepo.ArrangeCharacters(pkt.CharIDs) if err != nil { s.logger.Error( @@ -79,13 +79,13 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { var err error if pkt.GuildID > 0 { - guild, err = GetGuildInfoByID(s, pkt.GuildID) + guild, err = s.server.guildRepo.GetByID(pkt.GuildID) } else { - guild, err = GetGuildInfoByCharacterId(s, s.charID) + guild, err = s.server.guildRepo.GetByCharID(s.charID) } if guild != nil { - isApplicant, _ := guild.HasApplicationForCharID(s, s.charID) + isApplicant, _ := s.server.guildRepo.HasApplication(guild.ID, s.charID) if isApplicant { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 2)) return @@ -93,7 +93,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { } if guild == nil && s.prevGuildID > 0 { - guild, err = GetGuildInfoByID(s, s.prevGuildID) + guild, err = s.server.guildRepo.GetByID(s.prevGuildID) } if err != nil { @@ -105,7 +105,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { return } - guildMembers, err := GetGuildMembers(s, guild.ID, false) + guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false) if err != nil { s.logger.Error("failed to retrieve guild") @@ -157,7 +157,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { if guild.AllianceID > 0 { bf.WriteUint16(alliance.TotalMembers - uint16(len(guildMembers))) if guild.ID != alliance.ParentGuildID { - mems, err := GetGuildMembers(s, alliance.ParentGuildID, false) + mems, err := s.server.guildRepo.GetMembers(alliance.ParentGuildID, false) if err != nil { s.logger.Error("Failed to get parent guild members for alliance", zap.Error(err)) doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) @@ -168,7 +168,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { } } if guild.ID != alliance.SubGuild1ID { - mems, err := GetGuildMembers(s, alliance.SubGuild1ID, false) + mems, err := s.server.guildRepo.GetMembers(alliance.SubGuild1ID, false) if err != nil { s.logger.Error("Failed to get sub guild 1 members for alliance", zap.Error(err)) doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) @@ -179,7 +179,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { } } if guild.ID != alliance.SubGuild2ID { - mems, err := GetGuildMembers(s, alliance.SubGuild2ID, false) + mems, err := s.server.guildRepo.GetMembers(alliance.SubGuild2ID, false) if err != nil { s.logger.Error("Failed to get sub guild 2 members for alliance", zap.Error(err)) doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) @@ -204,9 +204,9 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetGuildManageRight(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetGuildManageRight) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) if guild == nil || s.prevGuildID != 0 { - guild, err := GetGuildInfoByID(s, s.prevGuildID) + guild, err := s.server.guildRepo.GetByID(s.prevGuildID) s.prevGuildID = 0 if guild == nil || err != nil { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -216,7 +216,7 @@ func handleMsgMhfGetGuildManageRight(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() bf.WriteUint32(uint32(guild.MemberCount)) - members, _ := GetGuildMembers(s, guild.ID, false) + members, _ := s.server.guildRepo.GetMembers(guild.ID, false) for _, member := range members { bf.WriteUint32(member.CharID) bf.WriteBool(member.Recruiter) @@ -237,9 +237,9 @@ func handleMsgMhfGetGuildTargetMemberNum(s *Session, p mhfpacket.MHFPacket) { var err error if pkt.GuildID == 0x0 { - guild, err = GetGuildInfoByCharacterId(s, s.charID) + guild, err = s.server.guildRepo.GetByCharID(s.charID) } else { - guild, err = GetGuildInfoByID(s, pkt.GuildID) + guild, err = s.server.guildRepo.GetByID(pkt.GuildID) } if err != nil || guild == nil { @@ -266,7 +266,7 @@ func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateGuildItem) newStacks := mhfitem.DiffItemStacks(guildGetItems(s, pkt.GuildID), pkt.UpdatedItems) - if _, err := s.server.db.Exec(`UPDATE guilds SET item_box=$1 WHERE id=$2`, mhfitem.SerializeWarehouseItems(newStacks), pkt.GuildID); err != nil { + if err := s.server.guildRepo.SaveItemBox(pkt.GuildID, mhfitem.SerializeWarehouseItems(newStacks)); err != nil { s.logger.Error("Failed to update guild item box", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -275,7 +275,7 @@ func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateGuildIcon(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateGuildIcon) - guild, err := GetGuildInfoByID(s, pkt.GuildID) + guild, err := s.server.guildRepo.GetByID(pkt.GuildID) if err != nil { s.logger.Error("Failed to get guild info for icon update", zap.Error(err)) @@ -283,7 +283,7 @@ func handleMsgMhfUpdateGuildIcon(s *Session, p mhfpacket.MHFPacket) { return } - characterInfo, err := GetCharacterGuildData(s, s.charID) + characterInfo, err := s.server.guildRepo.GetCharacterMembership(s.charID) if err != nil { s.logger.Error("Failed to get character guild data for icon update", zap.Error(err)) @@ -322,7 +322,7 @@ func handleMsgMhfUpdateGuildIcon(s *Session, p mhfpacket.MHFPacket) { guild.Icon = icon - err = guild.Save(s) + err = s.server.guildRepo.Save(guild) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -364,7 +364,7 @@ func handleMsgMhfUpdateGuild(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfSetGuildManageRight(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSetGuildManageRight) - if _, err := s.server.db.Exec("UPDATE guild_characters SET recruiter=$1 WHERE character_id=$2", pkt.Allowed, pkt.CharID); err != nil { + if err := s.server.guildRepo.SetRecruiter(pkt.CharID, pkt.Allowed); err != nil { s.logger.Error("Failed to update guild manage right", zap.Error(err)) } doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -393,3 +393,22 @@ func handleMsgMhfOperationInvGuild(s *Session, p mhfpacket.MHFPacket) { } func handleMsgMhfUpdateGuildcard(s *Session, p mhfpacket.MHFPacket) {} + +// guildGetItems reads and parses the guild item box. +func guildGetItems(s *Session, guildID uint32) []mhfitem.MHFItemStack { + data, err := s.server.guildRepo.GetItemBox(guildID) + if err != nil { + s.logger.Error("Failed to get guild item box", zap.Error(err)) + return nil + } + var items []mhfitem.MHFItemStack + if len(data) > 0 { + box := byteframe.NewByteFrameFromBytes(data) + numStacks := box.ReadUint16() + box.ReadUint16() // Unused + for i := 0; i < int(numStacks); i++ { + items = append(items, mhfitem.ReadWarehouseItem(box)) + } + } + return items +} diff --git a/server/channelserver/handlers_guild_adventure.go b/server/channelserver/handlers_guild_adventure.go index 2ee1a76ef..53c5d5ef9 100644 --- a/server/channelserver/handlers_guild_adventure.go +++ b/server/channelserver/handlers_guild_adventure.go @@ -21,7 +21,7 @@ type GuildAdventure struct { func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfLoadGuildAdventure) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) data, err := s.server.db.Queryx("SELECT id, destination, charge, depart, return, collected_by FROM guild_adventures WHERE guild_id = $1", guild.ID) if err != nil { s.logger.Error("Failed to get guild adventures from db", zap.Error(err)) @@ -52,7 +52,7 @@ func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfRegistGuildAdventure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfRegistGuildAdventure) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) _, err := s.server.db.Exec("INSERT INTO guild_adventures (guild_id, destination, depart, return) VALUES ($1, $2, $3, $4)", guild.ID, pkt.Destination, TimeAdjusted().Unix(), TimeAdjusted().Add(6*time.Hour).Unix()) if err != nil { s.logger.Error("Failed to register guild adventure", zap.Error(err)) @@ -87,7 +87,7 @@ func handleMsgMhfChargeGuildAdventure(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfRegistGuildAdventureDiva(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfRegistGuildAdventureDiva) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) _, err := s.server.db.Exec("INSERT INTO guild_adventures (guild_id, destination, charge, depart, return) VALUES ($1, $2, $3, $4, $5)", guild.ID, pkt.Destination, pkt.Charge, TimeAdjusted().Unix(), TimeAdjusted().Add(1*time.Hour).Unix()) if err != nil { s.logger.Error("Failed to register guild adventure", zap.Error(err)) diff --git a/server/channelserver/handlers_guild_alliance.go b/server/channelserver/handlers_guild_alliance.go index 5c9ac4d97..14b21978c 100644 --- a/server/channelserver/handlers_guild_alliance.go +++ b/server/channelserver/handlers_guild_alliance.go @@ -73,7 +73,7 @@ func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*G return nil, err } - parentGuild, err := GetGuildInfoByID(s, alliance.ParentGuildID) + parentGuild, err := s.server.guildRepo.GetByID(alliance.ParentGuildID) if err != nil { s.logger.Error("Failed to get parent guild info", zap.Error(err)) return nil, err @@ -83,7 +83,7 @@ func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*G } if alliance.SubGuild1ID > 0 { - subGuild1, err := GetGuildInfoByID(s, alliance.SubGuild1ID) + subGuild1, err := s.server.guildRepo.GetByID(alliance.SubGuild1ID) if err != nil { s.logger.Error("Failed to get sub guild 1 info", zap.Error(err)) return nil, err @@ -94,7 +94,7 @@ func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*G } if alliance.SubGuild2ID > 0 { - subGuild2, err := GetGuildInfoByID(s, alliance.SubGuild2ID) + subGuild2, err := s.server.guildRepo.GetByID(alliance.SubGuild2ID) if err != nil { s.logger.Error("Failed to get sub guild 2 info", zap.Error(err)) return nil, err @@ -119,7 +119,7 @@ func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfOperateJoint) - guild, err := GetGuildInfoByID(s, pkt.GuildID) + guild, err := s.server.guildRepo.GetByID(pkt.GuildID) if err != nil { s.logger.Error("Failed to get guild info", zap.Error(err)) } diff --git a/server/channelserver/handlers_guild_board.go b/server/channelserver/handlers_guild_board.go index 3bdac9f7b..9a3b04e8e 100644 --- a/server/channelserver/handlers_guild_board.go +++ b/server/channelserver/handlers_guild_board.go @@ -23,7 +23,7 @@ type MessageBoardPost struct { func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateGuildMessageBoard) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) if pkt.BoardType == 1 { pkt.MaxPosts = 4 } @@ -63,10 +63,10 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateGuildMessageBoard) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) applicant := false if guild != nil { - applicant, _ = guild.HasApplicationForCharID(s, s.charID) + applicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID) } if err != nil || guild == nil || applicant { doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_guild_cooking.go b/server/channelserver/handlers_guild_cooking.go index 15180984d..9cb9ef2fc 100644 --- a/server/channelserver/handlers_guild_cooking.go +++ b/server/channelserver/handlers_guild_cooking.go @@ -18,7 +18,7 @@ type GuildMeal struct { func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfLoadGuildCooking) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) data, err := s.server.db.Queryx("SELECT id, meal_id, level, created_at FROM guild_meals WHERE guild_id = $1", guild.ID) if err != nil { s.logger.Error("Failed to get guild meals from db", zap.Error(err)) @@ -49,7 +49,7 @@ func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfRegistGuildCooking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfRegistGuildCooking) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) startTime := TimeAdjusted().Add(time.Duration(s.server.erupeConfig.GameplayOptions.ClanMealDuration-3600) * time.Second) if pkt.OverwriteID != 0 { if _, err := s.server.db.Exec("UPDATE guild_meals SET meal_id = $1, level = $2, created_at = $3 WHERE id = $4", pkt.MealID, pkt.Success, startTime, pkt.OverwriteID); err != nil { @@ -121,7 +121,7 @@ func handleMsgMhfGuildHuntdata(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint8(count) } case 2: // Check - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err == nil { var count uint8 err = s.server.db.QueryRow(`SELECT COUNT(*) FROM kill_logs kl diff --git a/server/channelserver/handlers_guild_info.go b/server/channelserver/handlers_guild_info.go index 4dafab021..b72917986 100644 --- a/server/channelserver/handlers_guild_info.go +++ b/server/channelserver/handlers_guild_info.go @@ -9,7 +9,6 @@ import ( "erupe-ce/common/stringsupport" _config "erupe-ce/config" "erupe-ce/network/mhfpacket" - "github.com/jmoiron/sqlx" ) // Guild sentinel and cost constants @@ -25,9 +24,9 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { var err error if pkt.GuildID > 0 { - guild, err = GetGuildInfoByID(s, pkt.GuildID) + guild, err = s.server.guildRepo.GetByID(pkt.GuildID) } else { - guild, err = GetGuildInfoByCharacterId(s, s.charID) + guild, err = s.server.guildRepo.GetByCharID(s.charID) } if err == nil && guild != nil { @@ -37,7 +36,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { guildComment := stringsupport.UTF8ToSJIS(guild.Comment) guildLeaderName := stringsupport.UTF8ToSJIS(guild.LeaderName) - characterGuildData, err := GetCharacterGuildData(s, s.charID) + characterGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID) characterJoinedAt := guildNotJoinedSentinel if characterGuildData != nil && characterGuildData.JoinedAt != nil { @@ -196,7 +195,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(0) // No alliance } - applicants, err := GetGuildMembers(s, guild.ID, true) + applicants, err := s.server.guildRepo.GetMembers(guild.ID, true) if err != nil || (characterGuildData != nil && !characterGuildData.CanRecruit()) { bf.WriteUint16(0) } else { @@ -280,89 +279,81 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) { var guilds []*Guild var alliances []*GuildAlliance - var rows *sqlx.Rows var err error if pkt.Type <= 8 { var tempGuilds []*Guild - rows, err = s.server.db.Queryx(guildInfoSelectQuery) + tempGuilds, err = s.server.guildRepo.ListAll() if err == nil { - for rows.Next() { - guild, err := buildGuildObjectFromDbResult(rows, err, s) - if err != nil { - continue + switch pkt.Type { + case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME: + searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes()) + for _, guild := range tempGuilds { + if strings.Contains(guild.Name, searchName) { + guilds = append(guilds, guild) + } } - tempGuilds = append(tempGuilds, guild) - } - } - switch pkt.Type { - case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME: - searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes()) - for _, guild := range tempGuilds { - if strings.Contains(guild.Name, searchName) { - guilds = append(guilds, guild) + case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME: + searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes()) + for _, guild := range tempGuilds { + if strings.Contains(guild.LeaderName, searchName) { + guilds = append(guilds, guild) + } } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME: - searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes()) - for _, guild := range tempGuilds { - if strings.Contains(guild.LeaderName, searchName) { - guilds = append(guilds, guild) + case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID: + CID := pkt.Data1.ReadUint32() + for _, guild := range tempGuilds { + if guild.LeaderCharID == CID { + guilds = append(guilds, guild) + } } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID: - CID := pkt.Data1.ReadUint32() - for _, guild := range tempGuilds { - if guild.LeaderCharID == CID { - guilds = append(guilds, guild) + case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS: + if pkt.Sorting { + sort.Slice(tempGuilds, func(i, j int) bool { + return tempGuilds[i].MemberCount > tempGuilds[j].MemberCount + }) + } else { + sort.Slice(tempGuilds, func(i, j int) bool { + return tempGuilds[i].MemberCount < tempGuilds[j].MemberCount + }) } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS: - if pkt.Sorting { - sort.Slice(tempGuilds, func(i, j int) bool { - return tempGuilds[i].MemberCount > tempGuilds[j].MemberCount - }) - } else { - sort.Slice(tempGuilds, func(i, j int) bool { - return tempGuilds[i].MemberCount < tempGuilds[j].MemberCount - }) - } - guilds = tempGuilds - case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION: - if pkt.Sorting { - sort.Slice(tempGuilds, func(i, j int) bool { - return tempGuilds[i].CreatedAt.Unix() > tempGuilds[j].CreatedAt.Unix() - }) - } else { - sort.Slice(tempGuilds, func(i, j int) bool { - return tempGuilds[i].CreatedAt.Unix() < tempGuilds[j].CreatedAt.Unix() - }) - } - guilds = tempGuilds - case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK: - if pkt.Sorting { - sort.Slice(tempGuilds, func(i, j int) bool { - return tempGuilds[i].RankRP > tempGuilds[j].RankRP - }) - } else { - sort.Slice(tempGuilds, func(i, j int) bool { - return tempGuilds[i].RankRP < tempGuilds[j].RankRP - }) - } - guilds = tempGuilds - case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO: - mainMotto := uint8(pkt.Data1.ReadUint16()) - subMotto := uint8(pkt.Data1.ReadUint16()) - for _, guild := range tempGuilds { - if guild.MainMotto == mainMotto && guild.SubMotto == subMotto { - guilds = append(guilds, guild) + guilds = tempGuilds + case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION: + if pkt.Sorting { + sort.Slice(tempGuilds, func(i, j int) bool { + return tempGuilds[i].CreatedAt.Unix() > tempGuilds[j].CreatedAt.Unix() + }) + } else { + sort.Slice(tempGuilds, func(i, j int) bool { + return tempGuilds[i].CreatedAt.Unix() < tempGuilds[j].CreatedAt.Unix() + }) } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING: - recruitingMotto := uint8(pkt.Data1.ReadUint16()) - for _, guild := range tempGuilds { - if guild.MainMotto == recruitingMotto { - guilds = append(guilds, guild) + guilds = tempGuilds + case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK: + if pkt.Sorting { + sort.Slice(tempGuilds, func(i, j int) bool { + return tempGuilds[i].RankRP > tempGuilds[j].RankRP + }) + } else { + sort.Slice(tempGuilds, func(i, j int) bool { + return tempGuilds[i].RankRP < tempGuilds[j].RankRP + }) + } + guilds = tempGuilds + case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO: + mainMotto := uint8(pkt.Data1.ReadUint16()) + subMotto := uint8(pkt.Data1.ReadUint16()) + for _, guild := range tempGuilds { + if guild.MainMotto == mainMotto && guild.SubMotto == subMotto { + guilds = append(guilds, guild) + } + } + case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING: + recruitingMotto := uint8(pkt.Data1.ReadUint16()) + for _, guild := range tempGuilds { + if guild.MainMotto == recruitingMotto { + guilds = append(guilds, guild) + } } } } @@ -370,10 +361,12 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) { if pkt.Type > 8 { var tempAlliances []*GuildAlliance - rows, err = s.server.db.Queryx(allianceInfoSelectQuery) - if err == nil { + rows, queryErr := s.server.db.Queryx(allianceInfoSelectQuery) + if queryErr != nil { + err = queryErr + } else { for rows.Next() { - alliance, _ := buildAllianceObjectFromDbResult(rows, err, s) + alliance, _ := buildAllianceObjectFromDbResult(rows, queryErr, s) tempAlliances = append(tempAlliances, alliance) } } diff --git a/server/channelserver/handlers_guild_member.go b/server/channelserver/handlers_guild_member.go index 09a63ee9a..aa964c517 100644 --- a/server/channelserver/handlers_guild_member.go +++ b/server/channelserver/handlers_guild_member.go @@ -1,11 +1,7 @@ package channelserver import ( - "fmt" "time" - - "github.com/jmoiron/sqlx" - "go.uber.org/zap" ) // GuildMember represents a guild member with role and stats. @@ -45,112 +41,3 @@ func (gm *GuildMember) CanRecruit() bool { func (gm *GuildMember) IsSubLeader() bool { return gm.OrderIndex <= 3 } - -func (gm *GuildMember) Save(s *Session) error { - _, err := s.server.db.Exec("UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3", gm.AvoidLeadership, gm.OrderIndex, gm.CharID) - - if err != nil { - s.logger.Error( - "failed to update guild member data", - zap.Error(err), - zap.Uint32("charID", gm.CharID), - zap.Uint32("guildID", gm.GuildID), - ) - return err - } - return nil -} - -const guildMembersSelectSQL = ` -SELECT - COALESCE(g.id, 0) AS guild_id, - joined_at, - COALESCE((SELECT SUM(souls) FROM festa_submissions fs WHERE fs.character_id=c.id), 0) AS souls, - COALESCE(rp_today, 0) AS rp_today, - COALESCE(rp_yesterday, 0) AS rp_yesterday, - c.name, - c.id AS character_id, - COALESCE(order_index, 0) AS order_index, - c.last_login, - COALESCE(recruiter, false) AS recruiter, - COALESCE(avoid_leadership, false) AS avoid_leadership, - c.hr, - c.gr, - c.weapon_id, - c.weapon_type, - CASE WHEN g.leader_id = c.id THEN true ELSE false END AS is_leader, - character.is_applicant - FROM ( - SELECT character_id, true as is_applicant, guild_id - FROM guild_applications ga - WHERE ga.application_type = 'applied' - UNION - SELECT character_id, false as is_applicant, guild_id - FROM guild_characters gc - ) character - JOIN characters c on character.character_id = c.id - LEFT JOIN guild_characters gc ON gc.character_id = character.character_id - LEFT JOIN guilds g ON g.id = gc.guild_id -` - -// GetGuildMembers loads all members of a guild. -func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMember, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf(` - %s - WHERE character.guild_id = $1 AND is_applicant = $2 - `, guildMembersSelectSQL), guildID, applicants) - - if err != nil { - s.logger.Error("failed to retrieve membership data for guild", zap.Error(err), zap.Uint32("guildID", guildID)) - return nil, err - } - - defer func() { _ = rows.Close() }() - - members := make([]*GuildMember, 0) - - for rows.Next() { - member, err := buildGuildMemberObjectFromDBResult(rows, err, s) - - if err != nil { - return nil, err - } - - members = append(members, member) - } - - return members, nil -} - -// GetCharacterGuildData loads a character's guild membership. -func GetCharacterGuildData(s *Session, charID uint32) (*GuildMember, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID) - - if err != nil { - s.logger.Error(fmt.Sprintf("failed to retrieve membership data for character '%d'", charID)) - return nil, err - } - - defer func() { _ = rows.Close() }() - - hasRow := rows.Next() - - if !hasRow { - return nil, nil - } - - return buildGuildMemberObjectFromDBResult(rows, err, s) -} - -func buildGuildMemberObjectFromDBResult(rows *sqlx.Rows, _ error, s *Session) (*GuildMember, error) { - memberData := &GuildMember{} - - err := rows.StructScan(&memberData) - - if err != nil { - s.logger.Error("failed to retrieve guild data from database", zap.Error(err)) - return nil, err - } - - return memberData, nil -} diff --git a/server/channelserver/handlers_guild_ops.go b/server/channelserver/handlers_guild_ops.go index bc3cc968b..395ddaee7 100644 --- a/server/channelserver/handlers_guild_ops.go +++ b/server/channelserver/handlers_guild_ops.go @@ -14,12 +14,12 @@ import ( func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfOperateGuild) - guild, err := GetGuildInfoByID(s, pkt.GuildID) + guild, err := s.server.guildRepo.GetByID(pkt.GuildID) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } - characterGuildInfo, err := GetCharacterGuildData(s, s.charID) + characterGuildInfo, err := s.server.guildRepo.GetCharacterMembership(s.charID) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return @@ -34,14 +34,14 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { s.logger.Warn(fmt.Sprintf("character '%d' is attempting to manage guild '%d' without permission", s.charID, guild.ID)) response = 0 } else { - err = guild.Disband(s) + err = s.server.guildRepo.Disband(guild.ID) if err != nil { response = 0 } } bf.WriteUint32(uint32(response)) case mhfpacket.OperateGuildResign: - guildMembers, err := GetGuildMembers(s, guild.ID, false) + guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false) if err == nil { sort.Slice(guildMembers[:], func(i, j int) bool { return guildMembers[i].OrderIndex < guildMembers[j].OrderIndex @@ -51,16 +51,16 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { guild.LeaderCharID = guildMembers[i].CharID guildMembers[0].OrderIndex = guildMembers[i].OrderIndex guildMembers[i].OrderIndex = 1 - _ = guildMembers[0].Save(s) - _ = guildMembers[i].Save(s) + _ = s.server.guildRepo.SaveMember(guildMembers[0]) + _ = s.server.guildRepo.SaveMember(guildMembers[i]) bf.WriteUint32(guildMembers[i].CharID) break } } - _ = guild.Save(s) + _ = s.server.guildRepo.Save(guild) } case mhfpacket.OperateGuildApply: - err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil) + err = s.server.guildRepo.CreateApplication(guild.ID, s.charID, s.charID, GuildApplicationTypeApplied, nil) if err == nil { bf.WriteUint32(guild.LeaderCharID) } else { @@ -68,9 +68,9 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { } case mhfpacket.OperateGuildLeave: if characterGuildInfo.IsApplicant { - err = guild.RejectApplication(s, s.charID) + err = s.server.guildRepo.RejectApplication(guild.ID, s.charID) } else { - err = guild.RemoveCharacter(s, s.charID) + err = s.server.guildRepo.RemoveCharacter(s.charID) } response := 1 if err != nil { @@ -88,11 +88,11 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { case mhfpacket.OperateGuildDonateRank: bf.WriteBytes(handleDonateRP(s, uint16(pkt.Data1.ReadUint32()), guild, 0)) case mhfpacket.OperateGuildSetApplicationDeny: - if _, err := s.server.db.Exec("UPDATE guilds SET recruiting=false WHERE id=$1", guild.ID); err != nil { + if err := s.server.guildRepo.SetRecruiting(guild.ID, false); err != nil { s.logger.Error("Failed to deny guild applications", zap.Error(err)) } case mhfpacket.OperateGuildSetApplicationAllow: - if _, err := s.server.db.Exec("UPDATE guilds SET recruiting=true WHERE id=$1", guild.ID); err != nil { + if err := s.server.guildRepo.SetRecruiting(guild.ID, true); err != nil { s.logger.Error("Failed to allow guild applications", zap.Error(err)) } case mhfpacket.OperateGuildSetAvoidLeadershipTrue: @@ -105,7 +105,7 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { return } guild.Comment, _ = stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes()) - _ = guild.Save(s) + _ = s.server.guildRepo.Save(guild) case mhfpacket.OperateGuildUpdateMotto: if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -114,7 +114,7 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { _ = pkt.Data1.ReadUint16() guild.SubMotto = pkt.Data1.ReadUint8() guild.MainMotto = pkt.Data1.ReadUint8() - _ = guild.Save(s) + _ = s.server.guildRepo.Save(guild) case mhfpacket.OperateGuildRenamePugi1: handleRenamePugi(s, pkt.Data2, guild, 1) case mhfpacket.OperateGuildRenamePugi2: @@ -128,7 +128,7 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { case mhfpacket.OperateGuildChangePugi3: handleChangePugi(s, uint8(pkt.Data1.ReadUint32()), guild, 3) case mhfpacket.OperateGuildUnlockOutfit: - if _, err := s.server.db.Exec(`UPDATE guilds SET pugi_outfits=$1 WHERE id=$2`, pkt.Data1.ReadUint32(), guild.ID); err != nil { + if err := s.server.guildRepo.SetPugiOutfits(guild.ID, pkt.Data1.ReadUint32()); err != nil { s.logger.Error("Failed to unlock guild pugi outfit", zap.Error(err)) } case mhfpacket.OperateGuildDonateRoom: @@ -138,13 +138,13 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { quantity := uint16(pkt.Data1.ReadUint32()) bf.WriteBytes(handleDonateRP(s, quantity, guild, 1)) // TODO: Move this value onto rp_yesterday and reset to 0... daily? - if _, err := s.server.db.Exec(`UPDATE guild_characters SET rp_today=rp_today+$1 WHERE character_id=$2`, quantity, s.charID); err != nil { + if err := s.server.guildRepo.AddMemberDailyRP(s.charID, quantity); err != nil { s.logger.Error("Failed to update guild character daily RP", zap.Error(err)) } case mhfpacket.OperateGuildEventExchange: rp := uint16(pkt.Data1.ReadUint32()) - var balance uint32 - if err := s.server.db.QueryRow(`UPDATE guilds SET event_rp=event_rp-$1 WHERE id=$2 RETURNING event_rp`, rp, guild.ID).Scan(&balance); err != nil { + balance, err := s.server.guildRepo.ExchangeEventRP(guild.ID, rp) + if err != nil { s.logger.Error("Failed to exchange guild event RP", zap.Error(err)) } bf.WriteUint32(balance) @@ -169,7 +169,7 @@ func handleRenamePugi(s *Session, bf *byteframe.ByteFrame, guild *Guild, num int default: guild.PugiName3 = name } - _ = guild.Save(s) + _ = s.server.guildRepo.Save(guild) } func handleChangePugi(s *Session, outfit uint8, guild *Guild, num int) { @@ -181,7 +181,7 @@ func handleChangePugi(s *Session, outfit uint8, guild *Guild, num int) { case 3: guild.PugiOutfit3 = outfit } - _ = guild.Save(s) + _ = s.server.guildRepo.Save(guild) } func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte { @@ -193,8 +193,8 @@ func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte { } var resetRoom bool if _type == 2 { - var currentRP uint16 - if err := s.server.db.QueryRow(`SELECT room_rp FROM guilds WHERE id = $1`, guild.ID).Scan(¤tRP); err != nil { + currentRP, err := s.server.guildRepo.GetRoomRP(guild.ID) + if err != nil { s.logger.Error("Failed to get guild room RP", zap.Error(err)) } if currentRP+amount >= 30 { @@ -206,23 +206,23 @@ func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte { saveData.Save(s) switch _type { case 0: - if _, err := s.server.db.Exec(`UPDATE guilds SET rank_rp = rank_rp + $1 WHERE id = $2`, amount, guild.ID); err != nil { + if err := s.server.guildRepo.AddRankRP(guild.ID, amount); err != nil { s.logger.Error("Failed to update guild rank RP", zap.Error(err)) } case 1: - if _, err := s.server.db.Exec(`UPDATE guilds SET event_rp = event_rp + $1 WHERE id = $2`, amount, guild.ID); err != nil { + if err := s.server.guildRepo.AddEventRP(guild.ID, amount); err != nil { s.logger.Error("Failed to update guild event RP", zap.Error(err)) } case 2: if resetRoom { - if _, err := s.server.db.Exec(`UPDATE guilds SET room_rp = 0 WHERE id = $1`, guild.ID); err != nil { + if err := s.server.guildRepo.SetRoomRP(guild.ID, 0); err != nil { s.logger.Error("Failed to reset guild room RP", zap.Error(err)) } - if _, err := s.server.db.Exec(`UPDATE guilds SET room_expiry = $1 WHERE id = $2`, TimeAdjusted().Add(time.Hour*24*7), guild.ID); err != nil { + if err := s.server.guildRepo.SetRoomExpiry(guild.ID, TimeAdjusted().Add(time.Hour*24*7)); err != nil { s.logger.Error("Failed to update guild room expiry", zap.Error(err)) } } else { - if _, err := s.server.db.Exec(`UPDATE guilds SET room_rp = room_rp + $1 WHERE id = $2`, amount, guild.ID); err != nil { + if err := s.server.guildRepo.AddRoomRP(guild.ID, amount); err != nil { s.logger.Error("Failed to update guild room RP", zap.Error(err)) } } @@ -233,7 +233,7 @@ func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte { } func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild, avoidLeadership bool) { - characterGuildData, err := GetCharacterGuildData(s, s.charID) + characterGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -242,7 +242,7 @@ func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild, characterGuildData.AvoidLeadership = avoidLeadership - err = characterGuildData.Save(s) + err = s.server.guildRepo.SaveMember(characterGuildData) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -255,14 +255,14 @@ func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild, func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfOperateGuildMember) - guild, err := GetGuildInfoByCharacterId(s, pkt.CharID) + guild, err := s.server.guildRepo.GetByCharID(pkt.CharID) if err != nil || guild == nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } - actorCharacter, err := GetCharacterGuildData(s, s.charID) + actorCharacter, err := s.server.guildRepo.GetCharacterMembership(s.charID) if err != nil || (!actorCharacter.IsSubLeader() && guild.LeaderCharID != s.charID) { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -272,7 +272,7 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { var mail Mail switch pkt.Action { case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT: - err = guild.AcceptApplication(s, pkt.CharID) + err = s.server.guildRepo.AcceptApplication(guild.ID, pkt.CharID) mail = Mail{ RecipientID: pkt.CharID, Subject: "Accepted!", @@ -280,7 +280,7 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { IsSystemMessage: true, } case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT: - err = guild.RejectApplication(s, pkt.CharID) + err = s.server.guildRepo.RejectApplication(guild.ID, pkt.CharID) mail = Mail{ RecipientID: pkt.CharID, Subject: "Rejected", @@ -288,7 +288,7 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { IsSystemMessage: true, } case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK: - err = guild.RemoveCharacter(s, pkt.CharID) + err = s.server.guildRepo.RemoveCharacter(pkt.CharID) mail = Mail{ RecipientID: pkt.CharID, Subject: "Kicked", diff --git a/server/channelserver/handlers_guild_scout.go b/server/channelserver/handlers_guild_scout.go index 23c037f9f..d0f4e501d 100644 --- a/server/channelserver/handlers_guild_scout.go +++ b/server/channelserver/handlers_guild_scout.go @@ -12,7 +12,7 @@ import ( func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfPostGuildScout) - actorCharGuildData, err := GetCharacterGuildData(s, s.charID) + actorCharGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID) if err != nil { s.logger.Error("Failed to get character guild data for scout", zap.Error(err)) @@ -25,7 +25,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { return } - guildInfo, err := GetGuildInfoByID(s, actorCharGuildData.GuildID) + guildInfo, err := s.server.guildRepo.GetByID(actorCharGuildData.GuildID) if err != nil { s.logger.Error("Failed to get guild info for scout", zap.Error(err)) @@ -33,7 +33,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { return } - hasApplication, err := guildInfo.HasApplicationForCharID(s, pkt.CharID) + hasApplication, err := s.server.guildRepo.HasApplication(guildInfo.ID, pkt.CharID) if err != nil { s.logger.Error("Failed to check application for scout", zap.Error(err)) @@ -54,10 +54,10 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { return } - err = guildInfo.CreateApplication(s, pkt.CharID, GuildApplicationTypeInvited, transaction) + err = s.server.guildRepo.CreateApplication(guildInfo.ID, pkt.CharID, s.charID, GuildApplicationTypeInvited, transaction) if err != nil { - rollbackTransaction(s, transaction) + _ = transaction.Rollback() s.logger.Error("Failed to create guild scout application", zap.Error(err)) doAckBufFail(s, pkt.AckHandle, nil) return @@ -77,7 +77,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { err = mail.Send(s, transaction) if err != nil { - rollbackTransaction(s, transaction) + _ = transaction.Rollback() doAckBufFail(s, pkt.AckHandle, nil) return } @@ -96,7 +96,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCancelGuildScout) - guildCharData, err := GetCharacterGuildData(s, s.charID) + guildCharData, err := s.server.guildRepo.GetCharacterMembership(s.charID) if err != nil { s.logger.Error("Failed to get character guild data for cancel scout", zap.Error(err)) @@ -109,14 +109,14 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { return } - guild, err := GetGuildInfoByID(s, guildCharData.GuildID) + guild, err := s.server.guildRepo.GetByID(guildCharData.GuildID) if err != nil { doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) return } - err = guild.CancelInvitation(s, pkt.InvitationID) + err = s.server.guildRepo.CancelInvitation(guild.ID, pkt.InvitationID) if err != nil { doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) @@ -129,7 +129,7 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAnswerGuildScout) bf := byteframe.NewByteFrame() - guild, err := GetGuildInfoByCharacterId(s, pkt.LeaderID) + guild, err := s.server.guildRepo.GetByCharID(pkt.LeaderID) if err != nil { s.logger.Error("Failed to get guild info for answer scout", zap.Error(err)) @@ -137,7 +137,7 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { return } - app, err := guild.GetApplicationForCharID(s, s.charID, GuildApplicationTypeInvited) + app, err := s.server.guildRepo.GetApplication(guild.ID, s.charID, GuildApplicationTypeInvited) if app == nil || err != nil { s.logger.Warn( @@ -154,7 +154,7 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { var mail []Mail if pkt.Answer { - err = guild.AcceptApplication(s, s.charID) + err = s.server.guildRepo.AcceptApplication(guild.ID, s.charID) mail = append(mail, Mail{ RecipientID: s.charID, Subject: s.server.i18n.guild.invite.success.title, @@ -169,7 +169,7 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { IsSystemMessage: true, }) } else { - err = guild.RejectApplication(s, s.charID) + err = s.server.guildRepo.RejectApplication(guild.ID, s.charID) mail = append(mail, Mail{ RecipientID: s.charID, Subject: s.server.i18n.guild.invite.rejected.title, @@ -201,13 +201,13 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetGuildScoutList) - guildInfo, _ := GetGuildInfoByCharacterId(s, s.charID) + guildInfo, _ := s.server.guildRepo.GetByCharID(s.charID) if guildInfo == nil && s.prevGuildID == 0 { doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } else { - guildInfo, err := GetGuildInfoByID(s, s.prevGuildID) + guildInfo, err := s.server.guildRepo.GetByID(s.prevGuildID) if guildInfo == nil || err != nil { doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return @@ -216,7 +216,7 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { rows, err := s.server.db.Queryx(` SELECT c.id, c.name, c.hr, c.gr, ga.actor_id - FROM guild_applications ga + FROM guild_applications ga JOIN characters c ON c.id = ga.character_id WHERE ga.guild_id = $1 AND ga.application_type = 'invited' `, guildInfo.ID) diff --git a/server/channelserver/handlers_guild_tresure.go b/server/channelserver/handlers_guild_tresure.go index 6a65ad369..e9f339638 100644 --- a/server/channelserver/handlers_guild_tresure.go +++ b/server/channelserver/handlers_guild_tresure.go @@ -25,7 +25,7 @@ type TreasureHunt struct { func handleMsgMhfEnumerateGuildTresure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateGuildTresure) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil || guild == nil { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) return @@ -86,7 +86,7 @@ func handleMsgMhfRegistGuildTresure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfRegistGuildTresure) bf := byteframe.NewByteFrameFromBytes(pkt.Data) huntData := byteframe.NewByteFrame() - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil || guild == nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return diff --git a/server/channelserver/handlers_house.go b/server/channelserver/handlers_house.go index 04719088e..0ef772f4d 100644 --- a/server/channelserver/handlers_house.go +++ b/server/channelserver/handlers_house.go @@ -82,11 +82,11 @@ func handleMsgMhfEnumerateHouse(s *Session, p mhfpacket.MHFPacket) { } } case 2: - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil || guild == nil { break } - guildMembers, err := GetGuildMembers(s, guild.ID, false) + guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false) if err != nil { break } @@ -190,10 +190,10 @@ func handleMsgMhfLoadHouse(s *Session, p mhfpacket.MHFPacket) { // Guild verification if state > 3 { - ownGuild, err := GetGuildInfoByCharacterId(s, s.charID) - isApplicant, _ := ownGuild.HasApplicationForCharID(s, s.charID) + ownGuild, err := s.server.guildRepo.GetByCharID(s.charID) + isApplicant, _ := s.server.guildRepo.HasApplication(ownGuild.ID, s.charID) if err == nil && ownGuild != nil { - othersGuild, err := GetGuildInfoByCharacterId(s, pkt.CharID) + othersGuild, err := s.server.guildRepo.GetByCharID(pkt.CharID) if err == nil && othersGuild != nil { if othersGuild.ID == ownGuild.ID && !isApplicant { allowed = true diff --git a/server/channelserver/handlers_mail.go b/server/channelserver/handlers_mail.go index e6093be85..2564d623c 100644 --- a/server/channelserver/handlers_mail.go +++ b/server/channelserver/handlers_mail.go @@ -339,13 +339,13 @@ func handleMsgMhfSendMail(s *Session, p mhfpacket.MHFPacket) { ` if pkt.RecipientID == 0 { // Guild mail - g, err := GetGuildInfoByCharacterId(s, s.charID) + g, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil { s.logger.Error("Failed to get guild info for mail") doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - gm, err := GetGuildMembers(s, g.ID, false) + gm, err := s.server.guildRepo.GetMembers(g.ID, false) if err != nil { s.logger.Error("Failed to get guild members for mail") doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_mercenary.go b/server/channelserver/handlers_mercenary.go index 94a295425..f03c04df3 100644 --- a/server/channelserver/handlers_mercenary.go +++ b/server/channelserver/handlers_mercenary.go @@ -388,7 +388,7 @@ type Airou struct { func getGuildAirouList(s *Session) []Airou { var guildCats []Airou bannedCats := make(map[uint32]int) - guild, err := GetGuildInfoByCharacterId(s, s.charID) + guild, err := s.server.guildRepo.GetByCharID(s.charID) if err != nil { return guildCats } diff --git a/server/channelserver/handlers_rengoku.go b/server/channelserver/handlers_rengoku.go index 41b2978f8..2d166de8b 100644 --- a/server/channelserver/handlers_rengoku.go +++ b/server/channelserver/handlers_rengoku.go @@ -213,8 +213,11 @@ type RengokuScore struct { func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateRengokuRanking) - guild, _ := GetGuildInfoByCharacterId(s, s.charID) - isApplicant, _ := guild.HasApplicationForCharID(s, s.charID) + guild, _ := s.server.guildRepo.GetByCharID(s.charID) + var isApplicant bool + if guild != nil { + isApplicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID) + } if isApplicant { guild = nil } diff --git a/server/channelserver/repo_guild.go b/server/channelserver/repo_guild.go new file mode 100644 index 000000000..72e8eeba8 --- /dev/null +++ b/server/channelserver/repo_guild.go @@ -0,0 +1,472 @@ +package channelserver + +import ( + "database/sql" + "errors" + "fmt" + "time" + + "github.com/jmoiron/sqlx" +) + +// GuildRepository centralizes all database access for guild-related tables +// (guilds, guild_characters, guild_applications). +type GuildRepository struct { + db *sqlx.DB +} + +// NewGuildRepository creates a new GuildRepository. +func NewGuildRepository(db *sqlx.DB) *GuildRepository { + return &GuildRepository{db: db} +} + +const guildInfoSelectSQL = ` +SELECT + g.id, + g.name, + rank_rp, + event_rp, + room_rp, + COALESCE(room_expiry, '1970-01-01') AS room_expiry, + main_motto, + sub_motto, + created_at, + leader_id, + c.name AS leader_name, + comment, + COALESCE(pugi_name_1, '') AS pugi_name_1, + COALESCE(pugi_name_2, '') AS pugi_name_2, + COALESCE(pugi_name_3, '') AS pugi_name_3, + pugi_outfit_1, + pugi_outfit_2, + pugi_outfit_3, + pugi_outfits, + recruiting, + COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color, + COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls, + COALESCE(( + SELECT id FROM guild_alliances ga WHERE + ga.parent_id = g.id OR + ga.sub1_id = g.id OR + ga.sub2_id = g.id + ), 0) AS alliance_id, + icon, + (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count + FROM guilds g + JOIN guild_characters gc ON gc.character_id = leader_id + JOIN characters c on leader_id = c.id +` + +const guildMembersSelectSQL = ` +SELECT + COALESCE(g.id, 0) AS guild_id, + joined_at, + COALESCE((SELECT SUM(souls) FROM festa_submissions fs WHERE fs.character_id=c.id), 0) AS souls, + COALESCE(rp_today, 0) AS rp_today, + COALESCE(rp_yesterday, 0) AS rp_yesterday, + c.name, + c.id AS character_id, + COALESCE(order_index, 0) AS order_index, + c.last_login, + COALESCE(recruiter, false) AS recruiter, + COALESCE(avoid_leadership, false) AS avoid_leadership, + c.hr, + c.gr, + c.weapon_id, + c.weapon_type, + CASE WHEN g.leader_id = c.id THEN true ELSE false END AS is_leader, + character.is_applicant + FROM ( + SELECT character_id, true as is_applicant, guild_id + FROM guild_applications ga + WHERE ga.application_type = 'applied' + UNION + SELECT character_id, false as is_applicant, guild_id + FROM guild_characters gc + ) character + JOIN characters c on character.character_id = c.id + LEFT JOIN guild_characters gc ON gc.character_id = character.character_id + LEFT JOIN guilds g ON g.id = gc.guild_id +` + +func scanGuild(rows *sqlx.Rows) (*Guild, error) { + guild := &Guild{} + if err := rows.StructScan(guild); err != nil { + return nil, err + } + return guild, nil +} + +func scanGuildMember(rows *sqlx.Rows) (*GuildMember, error) { + member := &GuildMember{} + if err := rows.StructScan(member); err != nil { + return nil, err + } + return member, nil +} + +// GetByID retrieves guild info by guild ID, returning nil if not found. +func (r *GuildRepository) GetByID(guildID uint32) (*Guild, error) { + rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE g.id = $1 LIMIT 1`, guildInfoSelectSQL), guildID) + if err != nil { + return nil, err + } + defer rows.Close() + if !rows.Next() { + return nil, nil + } + return scanGuild(rows) +} + +// GetByCharID retrieves guild info for a character, including applied guilds. +func (r *GuildRepository) GetByCharID(charID uint32) (*Guild, error) { + rows, err := r.db.Queryx(fmt.Sprintf(` + %s + WHERE EXISTS( + SELECT 1 + FROM guild_characters gc1 + WHERE gc1.character_id = $1 + AND gc1.guild_id = g.id + ) + OR EXISTS( + SELECT 1 + FROM guild_applications ga + WHERE ga.character_id = $1 + AND ga.guild_id = g.id + AND ga.application_type = 'applied' + ) + LIMIT 1 + `, guildInfoSelectSQL), charID) + if err != nil { + return nil, err + } + defer rows.Close() + if !rows.Next() { + return nil, nil + } + return scanGuild(rows) +} + +// ListAll returns all guilds. Used for guild enumeration/search. +func (r *GuildRepository) ListAll() ([]*Guild, error) { + rows, err := r.db.Queryx(guildInfoSelectSQL) + if err != nil { + return nil, err + } + defer rows.Close() + + var guilds []*Guild + for rows.Next() { + guild, err := scanGuild(rows) + if err != nil { + continue + } + guilds = append(guilds, guild) + } + return guilds, nil +} + +// Create creates a new guild and adds the leader as its first member. +func (r *GuildRepository) Create(leaderCharID uint32, guildName string) (int32, error) { + tx, err := r.db.Begin() + if err != nil { + return 0, err + } + + var guildID int32 + err = tx.QueryRow( + "INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id", + guildName, leaderCharID, + ).Scan(&guildID) + if err != nil { + _ = tx.Rollback() + return 0, err + } + + _, err = tx.Exec(`INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)`, guildID, leaderCharID) + if err != nil { + _ = tx.Rollback() + return 0, err + } + + if err := tx.Commit(); err != nil { + return 0, err + } + return guildID, nil +} + +// Save persists guild metadata changes. +func (r *GuildRepository) Save(guild *Guild) error { + _, err := r.db.Exec(` + UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, + pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1 + `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, + guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID) + return err +} + +// Disband removes a guild, its members, and cleans up alliance references. +func (r *GuildRepository) Disband(guildID uint32) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + + stmts := []string{ + "DELETE FROM guild_characters WHERE guild_id = $1", + "DELETE FROM guilds WHERE id = $1", + "DELETE FROM guild_alliances WHERE parent_id=$1", + } + for _, stmt := range stmts { + if _, err := tx.Exec(stmt, guildID); err != nil { + _ = tx.Rollback() + return err + } + } + + if _, err := tx.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guildID); err != nil { + _ = tx.Rollback() + return err + } + if _, err := tx.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guildID); err != nil { + _ = tx.Rollback() + return err + } + + return tx.Commit() +} + +// RemoveCharacter removes a character from their guild. +func (r *GuildRepository) RemoveCharacter(charID uint32) error { + _, err := r.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID) + return err +} + +// AcceptApplication deletes the application and adds the character to the guild. +func (r *GuildRepository) AcceptApplication(guildID, charID uint32) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + + if _, err := tx.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID); err != nil { + _ = tx.Rollback() + return err + } + + if _, err := tx.Exec(` + INSERT INTO guild_characters (guild_id, character_id, order_index) + VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1)) + `, guildID, charID); err != nil { + _ = tx.Rollback() + return err + } + + return tx.Commit() +} + +// CreateApplication inserts a guild application or invitation. +// If tx is non-nil, the operation participates in the given transaction. +func (r *GuildRepository) CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType, tx *sql.Tx) error { + query := `INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) VALUES ($1, $2, $3, $4)` + if tx != nil { + _, err := tx.Exec(query, guildID, charID, actorID, appType) + return err + } + _, err := r.db.Exec(query, guildID, charID, actorID, appType) + return err +} + +// CancelInvitation removes an invitation for a character. +func (r *GuildRepository) CancelInvitation(guildID, charID uint32) error { + _, err := r.db.Exec( + `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`, + charID, guildID, + ) + return err +} + +// RejectApplication removes an applied application for a character. +func (r *GuildRepository) RejectApplication(guildID, charID uint32) error { + _, err := r.db.Exec( + `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`, + charID, guildID, + ) + return err +} + +// ArrangeCharacters reorders guild members by updating their order_index values. +func (r *GuildRepository) ArrangeCharacters(charIDs []uint32) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + + for i, id := range charIDs { + if _, err := tx.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id); err != nil { + _ = tx.Rollback() + return err + } + } + + return tx.Commit() +} + +// GetApplication retrieves a specific application by character, guild, and type. +// Returns nil, nil if not found. +func (r *GuildRepository) GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error) { + app := &GuildApplication{} + err := r.db.QueryRowx(` + SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3 + `, charID, guildID, appType).StructScan(app) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + return app, nil +} + +// HasApplication checks whether any application exists for the character in the guild. +func (r *GuildRepository) HasApplication(guildID, charID uint32) (bool, error) { + var n int + err := r.db.QueryRow(`SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2`, charID, guildID).Scan(&n) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +// GetItemBox returns the raw item_box bytes for a guild. +func (r *GuildRepository) GetItemBox(guildID uint32) ([]byte, error) { + var data []byte + err := r.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return data, err +} + +// SaveItemBox writes the serialized item box data for a guild. +func (r *GuildRepository) SaveItemBox(guildID uint32, data []byte) error { + _, err := r.db.Exec(`UPDATE guilds SET item_box=$1 WHERE id=$2`, data, guildID) + return err +} + +// GetMembers loads all members (or applicants) of a guild. +func (r *GuildRepository) GetMembers(guildID uint32, applicants bool) ([]*GuildMember, error) { + rows, err := r.db.Queryx(fmt.Sprintf(` + %s + WHERE character.guild_id = $1 AND is_applicant = $2 + `, guildMembersSelectSQL), guildID, applicants) + if err != nil { + return nil, err + } + defer rows.Close() + + members := make([]*GuildMember, 0) + for rows.Next() { + member, err := scanGuildMember(rows) + if err != nil { + return nil, err + } + members = append(members, member) + } + return members, nil +} + +// GetCharacterMembership loads a character's guild membership data. +// Returns nil, nil if the character is not in any guild. +func (r *GuildRepository) GetCharacterMembership(charID uint32) (*GuildMember, error) { + rows, err := r.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID) + if err != nil { + return nil, err + } + defer rows.Close() + + if !rows.Next() { + return nil, nil + } + return scanGuildMember(rows) +} + +// SaveMember persists guild member changes (avoid_leadership and order_index). +func (r *GuildRepository) SaveMember(member *GuildMember) error { + _, err := r.db.Exec( + "UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3", + member.AvoidLeadership, member.OrderIndex, member.CharID, + ) + return err +} + +// SetRecruiting updates whether a guild is accepting applications. +func (r *GuildRepository) SetRecruiting(guildID uint32, recruiting bool) error { + _, err := r.db.Exec("UPDATE guilds SET recruiting=$1 WHERE id=$2", recruiting, guildID) + return err +} + +// SetPugiOutfits updates the unlocked pugi outfit bitmask. +func (r *GuildRepository) SetPugiOutfits(guildID uint32, outfits uint32) error { + _, err := r.db.Exec(`UPDATE guilds SET pugi_outfits=$1 WHERE id=$2`, outfits, guildID) + return err +} + +// SetRecruiter updates whether a character has recruiter rights. +func (r *GuildRepository) SetRecruiter(charID uint32, allowed bool) error { + _, err := r.db.Exec("UPDATE guild_characters SET recruiter=$1 WHERE character_id=$2", allowed, charID) + return err +} + +// AddMemberDailyRP adds RP to a member's daily total. +func (r *GuildRepository) AddMemberDailyRP(charID uint32, amount uint16) error { + _, err := r.db.Exec(`UPDATE guild_characters SET rp_today=rp_today+$1 WHERE character_id=$2`, amount, charID) + return err +} + +// ExchangeEventRP subtracts RP from a guild's event pool and returns the new balance. +func (r *GuildRepository) ExchangeEventRP(guildID uint32, amount uint16) (uint32, error) { + var balance uint32 + err := r.db.QueryRow(`UPDATE guilds SET event_rp=event_rp-$1 WHERE id=$2 RETURNING event_rp`, amount, guildID).Scan(&balance) + return balance, err +} + +// AddRankRP adds RP to a guild's rank total. +func (r *GuildRepository) AddRankRP(guildID uint32, amount uint16) error { + _, err := r.db.Exec(`UPDATE guilds SET rank_rp = rank_rp + $1 WHERE id = $2`, amount, guildID) + return err +} + +// AddEventRP adds RP to a guild's event total. +func (r *GuildRepository) AddEventRP(guildID uint32, amount uint16) error { + _, err := r.db.Exec(`UPDATE guilds SET event_rp = event_rp + $1 WHERE id = $2`, amount, guildID) + return err +} + +// GetRoomRP returns the current room RP for a guild. +func (r *GuildRepository) GetRoomRP(guildID uint32) (uint16, error) { + var rp uint16 + err := r.db.QueryRow(`SELECT room_rp FROM guilds WHERE id = $1`, guildID).Scan(&rp) + return rp, err +} + +// SetRoomRP sets the room RP for a guild. +func (r *GuildRepository) SetRoomRP(guildID uint32, rp uint16) error { + _, err := r.db.Exec(`UPDATE guilds SET room_rp = $1 WHERE id = $2`, rp, guildID) + return err +} + +// AddRoomRP atomically adds RP to a guild's room total. +func (r *GuildRepository) AddRoomRP(guildID uint32, amount uint16) error { + _, err := r.db.Exec(`UPDATE guilds SET room_rp = room_rp + $1 WHERE id = $2`, amount, guildID) + return err +} + +// SetRoomExpiry sets the room expiry time for a guild. +func (r *GuildRepository) SetRoomExpiry(guildID uint32, expiry time.Time) error { + _, err := r.db.Exec(`UPDATE guilds SET room_expiry = $1 WHERE id = $2`, expiry, guildID) + return err +} diff --git a/server/channelserver/repo_guild_test.go b/server/channelserver/repo_guild_test.go new file mode 100644 index 000000000..486afe59a --- /dev/null +++ b/server/channelserver/repo_guild_test.go @@ -0,0 +1,531 @@ +package channelserver + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" +) + +func setupGuildRepo(t *testing.T) (*GuildRepository, *sqlx.DB, uint32, uint32) { + t.Helper() + db := SetupTestDB(t) + userID := CreateTestUser(t, db, "guild_test_user") + charID := CreateTestCharacter(t, db, userID, "GuildLeader") + repo := NewGuildRepository(db) + guildID := CreateTestGuild(t, db, charID, "TestGuild") + t.Cleanup(func() { TeardownTestDB(t, db) }) + return repo, db, guildID, charID +} + +func TestGetByID(t *testing.T) { + repo, _, guildID, charID := setupGuildRepo(t) + + guild, err := repo.GetByID(guildID) + if err != nil { + t.Fatalf("GetByID failed: %v", err) + } + if guild == nil { + t.Fatal("Expected guild, got nil") + } + if guild.ID != guildID { + t.Errorf("Expected guild ID %d, got %d", guildID, guild.ID) + } + if guild.Name != "TestGuild" { + t.Errorf("Expected name 'TestGuild', got %q", guild.Name) + } + if guild.LeaderCharID != charID { + t.Errorf("Expected leader %d, got %d", charID, guild.LeaderCharID) + } +} + +func TestGetByIDNotFound(t *testing.T) { + repo, _, _, _ := setupGuildRepo(t) + + guild, err := repo.GetByID(999999) + if err != nil { + t.Fatalf("GetByID failed: %v", err) + } + if guild != nil { + t.Errorf("Expected nil for non-existent guild, got: %+v", guild) + } +} + +func TestGetByCharID(t *testing.T) { + repo, _, guildID, charID := setupGuildRepo(t) + + guild, err := repo.GetByCharID(charID) + if err != nil { + t.Fatalf("GetByCharID failed: %v", err) + } + if guild == nil { + t.Fatal("Expected guild, got nil") + } + if guild.ID != guildID { + t.Errorf("Expected guild ID %d, got %d", guildID, guild.ID) + } +} + +func TestGetByCharIDNotFound(t *testing.T) { + repo, _, _, _ := setupGuildRepo(t) + + guild, err := repo.GetByCharID(999999) + if err != nil { + t.Fatalf("GetByCharID failed: %v", err) + } + if guild != nil { + t.Errorf("Expected nil for non-member, got: %+v", guild) + } +} + +func TestCreate(t *testing.T) { + db := SetupTestDB(t) + defer TeardownTestDB(t, db) + repo := NewGuildRepository(db) + userID := CreateTestUser(t, db, "create_guild_user") + charID := CreateTestCharacter(t, db, userID, "CreateLeader") + + guildID, err := repo.Create(charID, "NewGuild") + if err != nil { + t.Fatalf("Create failed: %v", err) + } + if guildID <= 0 { + t.Errorf("Expected positive guild ID, got %d", guildID) + } + + // Verify guild exists + guild, err := repo.GetByID(uint32(guildID)) + if err != nil { + t.Fatalf("GetByID after Create failed: %v", err) + } + if guild == nil { + t.Fatal("Created guild not found") + } + if guild.Name != "NewGuild" { + t.Errorf("Expected name 'NewGuild', got %q", guild.Name) + } + + // Verify leader is a member + member, err := repo.GetCharacterMembership(charID) + if err != nil { + t.Fatalf("GetCharacterMembership failed: %v", err) + } + if member == nil { + t.Fatal("Leader not found as guild member") + } +} + +func TestSaveGuild(t *testing.T) { + repo, _, guildID, _ := setupGuildRepo(t) + + guild, err := repo.GetByID(guildID) + if err != nil { + t.Fatalf("GetByID failed: %v", err) + } + + guild.Comment = "Updated comment" + guild.MainMotto = 5 + guild.SubMotto = 3 + + if err := repo.Save(guild); err != nil { + t.Fatalf("Save failed: %v", err) + } + + updated, err := repo.GetByID(guildID) + if err != nil { + t.Fatalf("GetByID after Save failed: %v", err) + } + if updated.Comment != "Updated comment" { + t.Errorf("Expected comment 'Updated comment', got %q", updated.Comment) + } + if updated.MainMotto != 5 || updated.SubMotto != 3 { + t.Errorf("Expected mottos 5/3, got %d/%d", updated.MainMotto, updated.SubMotto) + } +} + +func TestDisband(t *testing.T) { + repo, _, guildID, charID := setupGuildRepo(t) + + if err := repo.Disband(guildID); err != nil { + t.Fatalf("Disband failed: %v", err) + } + + guild, err := repo.GetByID(guildID) + if err != nil { + t.Fatalf("GetByID after Disband failed: %v", err) + } + if guild != nil { + t.Errorf("Expected nil after disband, got: %+v", guild) + } + + member, err := repo.GetCharacterMembership(charID) + if err != nil { + t.Fatalf("GetCharacterMembership after Disband failed: %v", err) + } + if member != nil { + t.Errorf("Expected nil membership after disband, got: %+v", member) + } +} + +func TestGetMembers(t *testing.T) { + repo, db, guildID, leaderID := setupGuildRepo(t) + + // Add a second member + user2 := CreateTestUser(t, db, "member_user") + member2 := CreateTestCharacter(t, db, user2, "Member2") + if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 2)", guildID, member2); err != nil { + t.Fatalf("Failed to add member: %v", err) + } + + members, err := repo.GetMembers(guildID, false) + if err != nil { + t.Fatalf("GetMembers failed: %v", err) + } + if len(members) != 2 { + t.Fatalf("Expected 2 members, got %d", len(members)) + } + + ids := map[uint32]bool{leaderID: false, member2: false} + for _, m := range members { + ids[m.CharID] = true + } + if !ids[leaderID] || !ids[member2] { + t.Errorf("Expected members %d and %d, got: %v", leaderID, member2, members) + } +} + +func TestGetCharacterMembership(t *testing.T) { + repo, _, guildID, charID := setupGuildRepo(t) + + member, err := repo.GetCharacterMembership(charID) + if err != nil { + t.Fatalf("GetCharacterMembership failed: %v", err) + } + if member == nil { + t.Fatal("Expected membership, got nil") + } + if member.GuildID != guildID { + t.Errorf("Expected guild ID %d, got %d", guildID, member.GuildID) + } + if !member.IsLeader { + t.Error("Expected leader flag to be true") + } +} + +func TestSaveMember(t *testing.T) { + repo, _, _, charID := setupGuildRepo(t) + + member, err := repo.GetCharacterMembership(charID) + if err != nil { + t.Fatalf("GetCharacterMembership failed: %v", err) + } + + member.AvoidLeadership = true + member.OrderIndex = 5 + + if err := repo.SaveMember(member); err != nil { + t.Fatalf("SaveMember failed: %v", err) + } + + updated, err := repo.GetCharacterMembership(charID) + if err != nil { + t.Fatalf("GetCharacterMembership after Save failed: %v", err) + } + if !updated.AvoidLeadership { + t.Error("Expected avoid_leadership=true") + } + if updated.OrderIndex != 5 { + t.Errorf("Expected order_index=5, got %d", updated.OrderIndex) + } +} + +func TestRemoveCharacter(t *testing.T) { + repo, db, guildID, _ := setupGuildRepo(t) + + // Add and remove a member + user2 := CreateTestUser(t, db, "remove_user") + char2 := CreateTestCharacter(t, db, user2, "RemoveMe") + if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 2)", guildID, char2); err != nil { + t.Fatalf("Failed to add member: %v", err) + } + + if err := repo.RemoveCharacter(char2); err != nil { + t.Fatalf("RemoveCharacter failed: %v", err) + } + + member, err := repo.GetCharacterMembership(char2) + if err != nil { + t.Fatalf("GetCharacterMembership after remove failed: %v", err) + } + if member != nil { + t.Errorf("Expected nil membership after remove, got: %+v", member) + } +} + +func TestApplicationWorkflow(t *testing.T) { + repo, db, guildID, _ := setupGuildRepo(t) + + user2 := CreateTestUser(t, db, "applicant_user") + applicantID := CreateTestCharacter(t, db, user2, "Applicant") + + // Create application + err := repo.CreateApplication(guildID, applicantID, applicantID, GuildApplicationTypeApplied, nil) + if err != nil { + t.Fatalf("CreateApplication failed: %v", err) + } + + // Check HasApplication + has, err := repo.HasApplication(guildID, applicantID) + if err != nil { + t.Fatalf("HasApplication failed: %v", err) + } + if !has { + t.Error("Expected application to exist") + } + + // Get application + app, err := repo.GetApplication(guildID, applicantID, GuildApplicationTypeApplied) + if err != nil { + t.Fatalf("GetApplication failed: %v", err) + } + if app == nil { + t.Fatal("Expected application, got nil") + } + + // Accept + err = repo.AcceptApplication(guildID, applicantID) + if err != nil { + t.Fatalf("AcceptApplication failed: %v", err) + } + + // Verify membership + member, err := repo.GetCharacterMembership(applicantID) + if err != nil { + t.Fatalf("GetCharacterMembership after accept failed: %v", err) + } + if member == nil { + t.Fatal("Expected membership after accept") + } + + // Verify application removed + has, err = repo.HasApplication(guildID, applicantID) + if err != nil { + t.Fatalf("HasApplication after accept failed: %v", err) + } + if has { + t.Error("Expected no application after accept") + } +} + +func TestRejectApplication(t *testing.T) { + repo, db, guildID, _ := setupGuildRepo(t) + + user2 := CreateTestUser(t, db, "reject_user") + applicantID := CreateTestCharacter(t, db, user2, "Rejected") + + err := repo.CreateApplication(guildID, applicantID, applicantID, GuildApplicationTypeApplied, nil) + if err != nil { + t.Fatalf("CreateApplication failed: %v", err) + } + + err = repo.RejectApplication(guildID, applicantID) + if err != nil { + t.Fatalf("RejectApplication failed: %v", err) + } + + has, err := repo.HasApplication(guildID, applicantID) + if err != nil { + t.Fatalf("HasApplication after reject failed: %v", err) + } + if has { + t.Error("Expected no application after reject") + } +} + +func TestSetRecruiting(t *testing.T) { + repo, db, guildID, _ := setupGuildRepo(t) + + if err := repo.SetRecruiting(guildID, false); err != nil { + t.Fatalf("SetRecruiting failed: %v", err) + } + + var recruiting bool + if err := db.QueryRow("SELECT recruiting FROM guilds WHERE id=$1", guildID).Scan(&recruiting); err != nil { + t.Fatalf("Verification query failed: %v", err) + } + if recruiting { + t.Error("Expected recruiting=false") + } +} + +func TestRPOperations(t *testing.T) { + repo, db, guildID, _ := setupGuildRepo(t) + + // AddRankRP + if err := repo.AddRankRP(guildID, 100); err != nil { + t.Fatalf("AddRankRP failed: %v", err) + } + var rankRP uint16 + if err := db.QueryRow("SELECT rank_rp FROM guilds WHERE id=$1", guildID).Scan(&rankRP); err != nil { + t.Fatalf("Verification failed: %v", err) + } + if rankRP != 100 { + t.Errorf("Expected rank_rp=100, got %d", rankRP) + } + + // AddEventRP + if err := repo.AddEventRP(guildID, 50); err != nil { + t.Fatalf("AddEventRP failed: %v", err) + } + + // ExchangeEventRP + balance, err := repo.ExchangeEventRP(guildID, 20) + if err != nil { + t.Fatalf("ExchangeEventRP failed: %v", err) + } + if balance != 30 { + t.Errorf("Expected event_rp balance=30, got %d", balance) + } + + // Room RP operations + if err := repo.AddRoomRP(guildID, 10); err != nil { + t.Fatalf("AddRoomRP failed: %v", err) + } + roomRP, err := repo.GetRoomRP(guildID) + if err != nil { + t.Fatalf("GetRoomRP failed: %v", err) + } + if roomRP != 10 { + t.Errorf("Expected room_rp=10, got %d", roomRP) + } + + if err := repo.SetRoomRP(guildID, 0); err != nil { + t.Fatalf("SetRoomRP failed: %v", err) + } + roomRP, err = repo.GetRoomRP(guildID) + if err != nil { + t.Fatalf("GetRoomRP after reset failed: %v", err) + } + if roomRP != 0 { + t.Errorf("Expected room_rp=0, got %d", roomRP) + } + + // SetRoomExpiry + expiry := time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC) + if err := repo.SetRoomExpiry(guildID, expiry); err != nil { + t.Fatalf("SetRoomExpiry failed: %v", err) + } + var gotExpiry time.Time + if err := db.QueryRow("SELECT room_expiry FROM guilds WHERE id=$1", guildID).Scan(&gotExpiry); err != nil { + t.Fatalf("Verification failed: %v", err) + } + if !gotExpiry.Equal(expiry) { + t.Errorf("Expected expiry %v, got %v", expiry, gotExpiry) + } +} + +func TestItemBox(t *testing.T) { + repo, _, guildID, _ := setupGuildRepo(t) + + // Initially nil + data, err := repo.GetItemBox(guildID) + if err != nil { + t.Fatalf("GetItemBox failed: %v", err) + } + if data != nil { + t.Errorf("Expected nil item box initially, got %x", data) + } + + // Save and retrieve + blob := []byte{0x01, 0x02, 0x03} + if err := repo.SaveItemBox(guildID, blob); err != nil { + t.Fatalf("SaveItemBox failed: %v", err) + } + + data, err = repo.GetItemBox(guildID) + if err != nil { + t.Fatalf("GetItemBox after save failed: %v", err) + } + if len(data) != 3 || data[0] != 0x01 || data[2] != 0x03 { + t.Errorf("Expected %x, got %x", blob, data) + } +} + +func TestListAll(t *testing.T) { + repo, db, _, _ := setupGuildRepo(t) + + // Create a second guild + user2 := CreateTestUser(t, db, "list_user") + char2 := CreateTestCharacter(t, db, user2, "ListLeader") + CreateTestGuild(t, db, char2, "SecondGuild") + + guilds, err := repo.ListAll() + if err != nil { + t.Fatalf("ListAll failed: %v", err) + } + if len(guilds) < 2 { + t.Errorf("Expected at least 2 guilds, got %d", len(guilds)) + } +} + +func TestArrangeCharacters(t *testing.T) { + repo, db, guildID, leaderID := setupGuildRepo(t) + + // Add two more members + user2 := CreateTestUser(t, db, "arrange_user2") + char2 := CreateTestCharacter(t, db, user2, "Char2") + user3 := CreateTestUser(t, db, "arrange_user3") + char3 := CreateTestCharacter(t, db, user3, "Char3") + if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 2)", guildID, char2); err != nil { + t.Fatalf("Failed to add member: %v", err) + } + if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 3)", guildID, char3); err != nil { + t.Fatalf("Failed to add member: %v", err) + } + + // Rearrange (excludes leader, sets order_index starting at 2) + if err := repo.ArrangeCharacters([]uint32{char3, char2}); err != nil { + t.Fatalf("ArrangeCharacters failed: %v", err) + } + + // Verify order changed + var order2, order3 uint16 + _ = db.QueryRow("SELECT order_index FROM guild_characters WHERE character_id=$1", char2).Scan(&order2) + _ = db.QueryRow("SELECT order_index FROM guild_characters WHERE character_id=$1", char3).Scan(&order3) + if order3 != 2 || order2 != 3 { + t.Errorf("Expected char3=2, char2=3 but got char3=%d, char2=%d", order3, order2) + } + _ = leaderID +} + +func TestSetRecruiter(t *testing.T) { + repo, db, _, charID := setupGuildRepo(t) + + if err := repo.SetRecruiter(charID, true); err != nil { + t.Fatalf("SetRecruiter failed: %v", err) + } + + var recruiter bool + if err := db.QueryRow("SELECT recruiter FROM guild_characters WHERE character_id=$1", charID).Scan(&recruiter); err != nil { + t.Fatalf("Verification failed: %v", err) + } + if !recruiter { + t.Error("Expected recruiter=true") + } +} + +func TestAddMemberDailyRP(t *testing.T) { + repo, db, _, charID := setupGuildRepo(t) + + if err := repo.AddMemberDailyRP(charID, 25); err != nil { + t.Fatalf("AddMemberDailyRP failed: %v", err) + } + + var rp uint16 + if err := db.QueryRow("SELECT rp_today FROM guild_characters WHERE character_id=$1", charID).Scan(&rp); err != nil { + t.Fatalf("Verification failed: %v", err) + } + if rp != 25 { + t.Errorf("Expected rp_today=25, got %d", rp) + } +} diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 318116600..e89bb7ffb 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -46,6 +46,7 @@ type Server struct { logger *zap.Logger db *sqlx.DB charRepo *CharacterRepository + guildRepo *GuildRepository erupeConfig *_config.Config acceptConns chan net.Conn deleteConns chan net.Conn @@ -117,6 +118,7 @@ func NewServer(config *Config) *Server { } s.charRepo = NewCharacterRepository(config.DB) + s.guildRepo = NewGuildRepository(config.DB) // Mezeporta s.stages["sl1Ns200p0a0u0"] = NewStage("sl1Ns200p0a0u0") diff --git a/server/channelserver/testhelpers_db.go b/server/channelserver/testhelpers_db.go index 7dee79692..74ade5831 100644 --- a/server/channelserver/testhelpers_db.go +++ b/server/channelserver/testhelpers_db.go @@ -258,3 +258,38 @@ func CreateTestCharacter(t *testing.T, db *sqlx.DB, userID uint32, name string) return charID } + +// CreateTestGuild creates a test guild with the given leader and returns the guild ID +func CreateTestGuild(t *testing.T, db *sqlx.DB, leaderCharID uint32, name string) uint32 { + t.Helper() + + tx, err := db.Begin() + if err != nil { + t.Fatalf("Failed to begin transaction: %v", err) + } + + var guildID uint32 + err = tx.QueryRow( + "INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id", + name, leaderCharID, + ).Scan(&guildID) + if err != nil { + _ = tx.Rollback() + t.Fatalf("Failed to create test guild: %v", err) + } + + _, err = tx.Exec( + "INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)", + guildID, leaderCharID, + ) + if err != nil { + _ = tx.Rollback() + t.Fatalf("Failed to add leader to guild: %v", err) + } + + if err := tx.Commit(); err != nil { + t.Fatalf("Failed to commit guild creation: %v", err) + } + + return guildID +}