diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index 97c358b5f..7e08eb283 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -113,7 +113,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { return } - alliance, err := GetAllianceData(s, guild.AllianceID) + alliance, err := s.server.guildRepo.GetAllianceByID(guild.AllianceID) if err != nil { s.logger.Error("Failed to get alliance data") doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_guild_adventure.go b/server/channelserver/handlers_guild_adventure.go index 53c5d5ef9..316726516 100644 --- a/server/channelserver/handlers_guild_adventure.go +++ b/server/channelserver/handlers_guild_adventure.go @@ -22,21 +22,14 @@ type GuildAdventure struct { func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfLoadGuildAdventure) 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) + adventures, err := s.server.guildRepo.ListAdventures(guild.ID) if err != nil { s.logger.Error("Failed to get guild adventures from db", zap.Error(err)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1)) return } temp := byteframe.NewByteFrame() - count := 0 - for data.Next() { - count++ - adventureData := &GuildAdventure{} - err = data.StructScan(&adventureData) - if err != nil { - continue - } + for _, adventureData := range adventures { temp.WriteUint32(adventureData.ID) temp.WriteUint32(adventureData.Destination) temp.WriteUint32(adventureData.Charge) @@ -45,7 +38,7 @@ func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) { temp.WriteBool(stringsupport.CSVContains(adventureData.CollectedBy, int(s.charID))) } bf := byteframe.NewByteFrame() - bf.WriteUint8(uint8(count)) + bf.WriteUint8(uint8(len(adventures))) bf.WriteBytes(temp.Data()) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } @@ -53,8 +46,7 @@ func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfRegistGuildAdventure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfRegistGuildAdventure) 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 { + if err := s.server.guildRepo.CreateAdventure(guild.ID, pkt.Destination, TimeAdjusted().Unix(), TimeAdjusted().Add(6*time.Hour).Unix()); err != nil { s.logger.Error("Failed to register guild adventure", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -62,24 +54,15 @@ func handleMsgMhfRegistGuildAdventure(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAcquireGuildAdventure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireGuildAdventure) - var collectedBy string - err := s.server.db.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1", pkt.ID).Scan(&collectedBy) - if err != nil { - s.logger.Error("Error parsing adventure collected by", zap.Error(err)) - } else { - collectedBy = stringsupport.CSVAdd(collectedBy, int(s.charID)) - _, err := s.server.db.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, pkt.ID) - if err != nil { - s.logger.Error("Failed to collect adventure in db", zap.Error(err)) - } + if err := s.server.guildRepo.CollectAdventure(pkt.ID, s.charID); err != nil { + s.logger.Error("Failed to collect adventure", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfChargeGuildAdventure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfChargeGuildAdventure) - _, err := s.server.db.Exec("UPDATE guild_adventures SET charge = charge + $1 WHERE id = $2", pkt.Amount, pkt.ID) - if err != nil { + if err := s.server.guildRepo.ChargeAdventure(pkt.ID, pkt.Amount); err != nil { s.logger.Error("Failed to charge guild adventure", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -88,8 +71,7 @@ func handleMsgMhfChargeGuildAdventure(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfRegistGuildAdventureDiva(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfRegistGuildAdventureDiva) 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 { + if err := s.server.guildRepo.CreateAdventureWithCharge(guild.ID, pkt.Destination, pkt.Charge, TimeAdjusted().Unix(), TimeAdjusted().Add(1*time.Hour).Unix()); err != nil { s.logger.Error("Failed to register guild adventure", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_guild_alliance.go b/server/channelserver/handlers_guild_alliance.go index 14b21978c..a30f0330b 100644 --- a/server/channelserver/handlers_guild_alliance.go +++ b/server/channelserver/handlers_guild_alliance.go @@ -3,31 +3,12 @@ package channelserver import ( "erupe-ce/common/byteframe" ps "erupe-ce/common/pascalstring" - "fmt" "time" "erupe-ce/network/mhfpacket" - "github.com/jmoiron/sqlx" "go.uber.org/zap" ) -const allianceInfoSelectQuery = ` -SELECT -ga.id, -ga.name, -created_at, -parent_id, -CASE - WHEN sub1_id IS NULL THEN 0 - ELSE sub1_id -END, -CASE - WHEN sub2_id IS NULL THEN 0 - ELSE sub2_id -END -FROM guild_alliances ga -` - // GuildAlliance represents a multi-guild alliance. type GuildAlliance struct { ID uint32 `db:"id"` @@ -44,73 +25,9 @@ type GuildAlliance struct { SubGuild2 Guild } -// GetAllianceData loads alliance data from the database. -func GetAllianceData(s *Session, AllianceID uint32) (*GuildAlliance, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf(` - %s - WHERE ga.id = $1 - `, allianceInfoSelectQuery), AllianceID) - if err != nil { - s.logger.Error("Failed to retrieve alliance data from database", zap.Error(err)) - return nil, err - } - defer func() { _ = rows.Close() }() - hasRow := rows.Next() - if !hasRow { - return nil, nil - } - - return buildAllianceObjectFromDbResult(rows, err, s) -} - -func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*GuildAlliance, error) { - alliance := &GuildAlliance{} - - err := result.StructScan(alliance) - - if err != nil { - s.logger.Error("failed to retrieve alliance from database", zap.Error(err)) - return nil, err - } - - 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 - } else { - alliance.ParentGuild = *parentGuild - alliance.TotalMembers += parentGuild.MemberCount - } - - if alliance.SubGuild1ID > 0 { - 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 - } else { - alliance.SubGuild1 = *subGuild1 - alliance.TotalMembers += subGuild1.MemberCount - } - } - - if alliance.SubGuild2ID > 0 { - 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 - } else { - alliance.SubGuild2 = *subGuild2 - alliance.TotalMembers += subGuild2.MemberCount - } - } - - return alliance, nil -} - func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCreateJoint) - _, err := s.server.db.Exec("INSERT INTO guild_alliances (name, parent_id) VALUES ($1, $2)", pkt.Name, pkt.GuildID) - if err != nil { + if err := s.server.guildRepo.CreateAlliance(pkt.Name, pkt.GuildID); err != nil { s.logger.Error("Failed to create guild alliance in db", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x01, 0x01, 0x01, 0x01}) @@ -123,7 +40,7 @@ func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { if err != nil { s.logger.Error("Failed to get guild info", zap.Error(err)) } - alliance, err := GetAllianceData(s, pkt.AllianceID) + alliance, err := s.server.guildRepo.GetAllianceByID(pkt.AllianceID) if err != nil { s.logger.Error("Failed to get alliance info", zap.Error(err)) } @@ -131,8 +48,7 @@ func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { switch pkt.Action { case mhfpacket.OPERATE_JOINT_DISBAND: if guild.LeaderCharID == s.charID && alliance.ParentGuildID == guild.ID { - _, err = s.server.db.Exec("DELETE FROM guild_alliances WHERE id=$1", alliance.ID) - if err != nil { + if err := s.server.guildRepo.DeleteAlliance(alliance.ID); err != nil { s.logger.Error("Failed to disband alliance", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -146,18 +62,8 @@ func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { } case mhfpacket.OPERATE_JOINT_LEAVE: if guild.LeaderCharID == s.charID { - if guild.ID == alliance.SubGuild1ID && alliance.SubGuild2ID > 0 { - if _, err := s.server.db.Exec(`UPDATE guild_alliances SET sub1_id = sub2_id, sub2_id = NULL WHERE id = $1`, alliance.ID); err != nil { - s.logger.Error("Failed to update alliance on guild leave", zap.Error(err)) - } - } else if guild.ID == alliance.SubGuild1ID && alliance.SubGuild2ID == 0 { - if _, err := s.server.db.Exec(`UPDATE guild_alliances SET sub1_id = NULL WHERE id = $1`, alliance.ID); err != nil { - s.logger.Error("Failed to remove sub guild 1 from alliance", zap.Error(err)) - } - } else { - if _, err := s.server.db.Exec(`UPDATE guild_alliances SET sub2_id = NULL WHERE id = $1`, alliance.ID); err != nil { - s.logger.Error("Failed to remove sub guild 2 from alliance", zap.Error(err)) - } + if err := s.server.guildRepo.RemoveGuildFromAlliance(alliance.ID, guild.ID, alliance.SubGuild1ID, alliance.SubGuild2ID); err != nil { + s.logger.Error("Failed to remove guild from alliance", zap.Error(err)) } // NOTE: Alliance join requests are not yet implemented (no DB table exists), // so there are no pending applications to clean up on leave. @@ -172,18 +78,8 @@ func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { case mhfpacket.OPERATE_JOINT_KICK: if alliance.ParentGuild.LeaderCharID == s.charID { kickedGuildID := pkt.Data1.ReadUint32() - if kickedGuildID == alliance.SubGuild1ID && alliance.SubGuild2ID > 0 { - if _, err := s.server.db.Exec(`UPDATE guild_alliances SET sub1_id = sub2_id, sub2_id = NULL WHERE id = $1`, alliance.ID); err != nil { - s.logger.Error("Failed to update alliance on guild kick", zap.Error(err)) - } - } else if kickedGuildID == alliance.SubGuild1ID && alliance.SubGuild2ID == 0 { - if _, err := s.server.db.Exec(`UPDATE guild_alliances SET sub1_id = NULL WHERE id = $1`, alliance.ID); err != nil { - s.logger.Error("Failed to remove kicked sub guild 1 from alliance", zap.Error(err)) - } - } else { - if _, err := s.server.db.Exec(`UPDATE guild_alliances SET sub2_id = NULL WHERE id = $1`, alliance.ID); err != nil { - s.logger.Error("Failed to remove kicked sub guild 2 from alliance", zap.Error(err)) - } + if err := s.server.guildRepo.RemoveGuildFromAlliance(alliance.ID, kickedGuildID, alliance.SubGuild1ID, alliance.SubGuild2ID); err != nil { + s.logger.Error("Failed to kick guild from alliance", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } else { @@ -203,7 +99,7 @@ func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfInfoJoint(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoJoint) bf := byteframe.NewByteFrame() - alliance, err := GetAllianceData(s, pkt.AllianceID) + alliance, err := s.server.guildRepo.GetAllianceByID(pkt.AllianceID) if err != nil { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) } else { diff --git a/server/channelserver/handlers_guild_board.go b/server/channelserver/handlers_guild_board.go index 9a3b04e8e..8ca6fecc8 100644 --- a/server/channelserver/handlers_guild_board.go +++ b/server/channelserver/handlers_guild_board.go @@ -27,7 +27,7 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { if pkt.BoardType == 1 { pkt.MaxPosts = 4 } - msgs, err := s.server.db.Queryx("SELECT id, stamp_id, title, body, author_id, created_at, liked_by FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false ORDER BY created_at DESC", guild.ID, int(pkt.BoardType)) + posts, err := s.server.guildRepo.ListPosts(guild.ID, int(pkt.BoardType)) if err != nil { s.logger.Error("Failed to get guild messages from db", zap.Error(err)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -37,14 +37,7 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { s.logger.Error("Failed to update guild post checked time", zap.Error(err)) } bf := byteframe.NewByteFrame() - var postCount uint32 - for msgs.Next() { - postData := &MessageBoardPost{} - err = msgs.StructScan(&postData) - if err != nil { - continue - } - postCount++ + for _, postData := range posts { bf.WriteUint32(postData.ID) bf.WriteUint32(postData.AuthorID) bf.WriteUint32(0) @@ -56,7 +49,7 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { ps.Uint32(bf, postData.Body, true) } data := byteframe.NewByteFrame() - data.WriteUint32(postCount) + data.WriteUint32(uint32(len(posts))) data.WriteBytes(bf.Data()) doAckBufSucceed(s, pkt.AckHandle, data.Data()) } @@ -74,54 +67,43 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { } switch pkt.MessageOp { case 0: // Create message - if _, err := s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, pkt.StampID, pkt.PostType, pkt.Title, pkt.Body); err != nil { - s.logger.Error("Failed to insert guild post", zap.Error(err)) - } maxPosts := 100 if pkt.PostType == 1 { maxPosts = 4 } - if _, err := s.server.db.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN ( - SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false - ORDER BY created_at DESC OFFSET $3 - )`, guild.ID, pkt.PostType, maxPosts); err != nil { - s.logger.Error("Failed to soft-delete excess guild posts", zap.Error(err)) + if err := s.server.guildRepo.CreatePost(guild.ID, s.charID, pkt.StampID, int(pkt.PostType), pkt.Title, pkt.Body, maxPosts); err != nil { + s.logger.Error("Failed to create guild post", zap.Error(err)) } case 1: // Delete message - if _, err := s.server.db.Exec("UPDATE guild_posts SET deleted = true WHERE id = $1", pkt.PostID); err != nil { + if err := s.server.guildRepo.DeletePost(pkt.PostID); err != nil { s.logger.Error("Failed to soft-delete guild post", zap.Error(err)) } case 2: // Update message - if _, err := s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", pkt.Title, pkt.Body, pkt.PostID); err != nil { + if err := s.server.guildRepo.UpdatePost(pkt.PostID, pkt.Title, pkt.Body); err != nil { s.logger.Error("Failed to update guild post", zap.Error(err)) } case 3: // Update stamp - if _, err := s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", pkt.StampID, pkt.PostID); err != nil { + if err := s.server.guildRepo.UpdatePostStamp(pkt.PostID, pkt.StampID); err != nil { s.logger.Error("Failed to update guild post stamp", zap.Error(err)) } case 4: // Like message - var likedBy string - err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", pkt.PostID).Scan(&likedBy) + likedBy, err := s.server.guildRepo.GetPostLikedBy(pkt.PostID) if err != nil { s.logger.Error("Failed to get guild message like data from db", zap.Error(err)) } else { if pkt.LikeState { likedBy = stringsupport.CSVAdd(likedBy, int(s.charID)) - if _, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, pkt.PostID); err != nil { - s.logger.Error("Failed to update guild post likes", zap.Error(err)) - } } else { likedBy = stringsupport.CSVRemove(likedBy, int(s.charID)) - if _, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, pkt.PostID); err != nil { - s.logger.Error("Failed to update guild post likes", zap.Error(err)) - } + } + if err := s.server.guildRepo.SetPostLikedBy(pkt.PostID, likedBy); err != nil { + s.logger.Error("Failed to update guild post likes", zap.Error(err)) } } case 5: // Check for new messages - var newPosts int timeChecked, err := s.server.charRepo.ReadGuildPostChecked(s.charID) if err == nil { - _ = s.server.db.QueryRow("SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND deleted = false AND (EXTRACT(epoch FROM created_at)::int) > $2", guild.ID, timeChecked.Unix()).Scan(&newPosts) + newPosts, _ := s.server.guildRepo.CountNewPosts(guild.ID, timeChecked) if newPosts > 0 { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01}) return diff --git a/server/channelserver/handlers_guild_cooking.go b/server/channelserver/handlers_guild_cooking.go index 9cb9ef2fc..9f46e4e86 100644 --- a/server/channelserver/handlers_guild_cooking.go +++ b/server/channelserver/handlers_guild_cooking.go @@ -19,21 +19,16 @@ type GuildMeal struct { func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfLoadGuildCooking) 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) + allMeals, err := s.server.guildRepo.ListMeals(guild.ID) if err != nil { s.logger.Error("Failed to get guild meals from db", zap.Error(err)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 2)) return } - var meals []GuildMeal - var temp GuildMeal - for data.Next() { - err = data.StructScan(&temp) - if err != nil { - continue - } - if temp.CreatedAt.Add(60 * time.Minute).After(TimeAdjusted()) { - meals = append(meals, temp) + var meals []*GuildMeal + for _, meal := range allMeals { + if meal.CreatedAt.Add(60 * time.Minute).After(TimeAdjusted()) { + meals = append(meals, meal) } } bf := byteframe.NewByteFrame() @@ -52,15 +47,17 @@ func handleMsgMhfRegistGuildCooking(s *Session, p mhfpacket.MHFPacket) { 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 { + if err := s.server.guildRepo.UpdateMeal(pkt.OverwriteID, uint32(pkt.MealID), uint32(pkt.Success), startTime); err != nil { s.logger.Error("Failed to update guild meal", zap.Error(err)) } } else { - if err := s.server.db.QueryRow("INSERT INTO guild_meals (guild_id, meal_id, level, created_at) VALUES ($1, $2, $3, $4) RETURNING id", guild.ID, pkt.MealID, pkt.Success, startTime).Scan(&pkt.OverwriteID); err != nil { + id, err := s.server.guildRepo.CreateMeal(guild.ID, uint32(pkt.MealID), uint32(pkt.Success), startTime) + if err != nil { s.logger.Error("Failed to insert guild meal", zap.Error(err)) doAckBufFail(s, pkt.AckHandle, nil) return } + pkt.OverwriteID = id } bf := byteframe.NewByteFrame() bf.WriteUint16(1) @@ -91,31 +88,21 @@ func handleMsgMhfGuildHuntdata(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() switch pkt.Operation { case 0: // Acquire - if _, err := s.server.db.Exec(`UPDATE guild_characters SET box_claimed=$1 WHERE character_id=$2`, TimeAdjusted(), s.charID); err != nil { + if err := s.server.guildRepo.ClaimHuntBox(s.charID, TimeAdjusted()); err != nil { s.logger.Error("Failed to update guild hunt box claimed time", zap.Error(err)) } case 1: // Enumerate bf.WriteUint8(0) // Entries - rows, err := s.server.db.Query(`SELECT kl.id, kl.monster FROM kill_logs kl - INNER JOIN guild_characters gc ON kl.character_id = gc.character_id - WHERE gc.guild_id=$1 - AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2) - `, pkt.GuildID, s.charID) + kills, err := s.server.guildRepo.ListGuildKills(pkt.GuildID, s.charID) if err == nil { var count uint8 - var huntID, monID uint32 - for rows.Next() { - err = rows.Scan(&huntID, &monID) - if err != nil { - continue - } + for _, kill := range kills { if count == 255 { - _ = rows.Close() break } count++ - bf.WriteUint32(huntID) - bf.WriteUint32(monID) + bf.WriteUint32(kill.ID) + bf.WriteUint32(kill.Monster) } _, _ = bf.Seek(0, 0) bf.WriteUint8(count) @@ -123,12 +110,7 @@ func handleMsgMhfGuildHuntdata(s *Session, p mhfpacket.MHFPacket) { case 2: // Check 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 - INNER JOIN guild_characters gc ON kl.character_id = gc.character_id - WHERE gc.guild_id=$1 - AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2) - `, guild.ID, s.charID).Scan(&count) + count, err := s.server.guildRepo.CountGuildKills(guild.ID, s.charID) if err == nil && count > 0 { bf.WriteBool(true) } else { diff --git a/server/channelserver/handlers_guild_info.go b/server/channelserver/handlers_guild_info.go index b72917986..d98c42d80 100644 --- a/server/channelserver/handlers_guild_info.go +++ b/server/channelserver/handlers_guild_info.go @@ -134,7 +134,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(0) // Ignored if guild.AllianceID > 0 { - alliance, err := GetAllianceData(s, guild.AllianceID) + alliance, err := s.server.guildRepo.GetAllianceByID(guild.AllianceID) if err != nil { bf.WriteUint32(0) // Error, no alliance } else { @@ -361,15 +361,7 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) { if pkt.Type > 8 { var tempAlliances []*GuildAlliance - rows, queryErr := s.server.db.Queryx(allianceInfoSelectQuery) - if queryErr != nil { - err = queryErr - } else { - for rows.Next() { - alliance, _ := buildAllianceObjectFromDbResult(rows, queryErr, s) - tempAlliances = append(tempAlliances, alliance) - } - } + tempAlliances, err = s.server.guildRepo.ListAlliances() switch pkt.Type { case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ALLIANCE_NAME: searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes()) diff --git a/server/channelserver/handlers_guild_scout.go b/server/channelserver/handlers_guild_scout.go index d0f4e501d..2779cad6c 100644 --- a/server/channelserver/handlers_guild_scout.go +++ b/server/channelserver/handlers_guild_scout.go @@ -6,7 +6,6 @@ import ( "erupe-ce/network/mhfpacket" "fmt" "go.uber.org/zap" - "io" ) func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { @@ -214,65 +213,30 @@ 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 - JOIN characters c ON c.id = ga.character_id - WHERE ga.guild_id = $1 AND ga.application_type = 'invited' - `, guildInfo.ID) - + chars, err := s.server.guildRepo.ListInvitedCharacters(guildInfo.ID) if err != nil { s.logger.Error("failed to retrieve scouted characters", zap.Error(err)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - defer func() { _ = rows.Close() }() - bf := byteframe.NewByteFrame() - bf.SetBE() + bf.WriteUint32(uint32(len(chars))) - // Result count, we will overwrite this later - bf.WriteUint32(0x00) - - count := uint32(0) - - for rows.Next() { - var charName string - var charID, actorID uint32 - var HR, GR uint16 - - err = rows.Scan(&charID, &charName, &HR, &GR, &actorID) - - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) - continue - } - + for _, sc := range chars { // This seems to be used as a unique ID for the invitation sent // we can just use the charID and then filter on guild_id+charID when performing operations // this might be a problem later with mails sent referencing IDs but we'll see. - bf.WriteUint32(charID) - bf.WriteUint32(actorID) - bf.WriteUint32(charID) + bf.WriteUint32(sc.CharID) + bf.WriteUint32(sc.ActorID) + bf.WriteUint32(sc.CharID) bf.WriteUint32(uint32(TimeAdjusted().Unix())) - bf.WriteUint16(HR) // HR? - bf.WriteUint16(GR) // GR? - bf.WriteBytes(stringsupport.PaddedString(charName, 32, true)) - count++ + bf.WriteUint16(sc.HR) + bf.WriteUint16(sc.GR) + bf.WriteBytes(stringsupport.PaddedString(sc.Name, 32, true)) } - _, err = bf.Seek(0, io.SeekStart) - - if err != nil { - s.logger.Error("Failed to seek in guild scout list buffer", zap.Error(err)) - doAckBufFail(s, pkt.AckHandle, nil) - return - } - - bf.WriteUint32(count) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } diff --git a/server/channelserver/handlers_guild_tresure.go b/server/channelserver/handlers_guild_tresure.go index e9f339638..9c746ab2e 100644 --- a/server/channelserver/handlers_guild_tresure.go +++ b/server/channelserver/handlers_guild_tresure.go @@ -31,35 +31,22 @@ func handleMsgMhfEnumerateGuildTresure(s *Session, p mhfpacket.MHFPacket) { return } var hunts []TreasureHunt - var hunt TreasureHunt switch pkt.MaxHunts { case 1: - err = s.server.db.QueryRowx(`SELECT id, host_id, destination, level, start, hunt_data FROM guild_hunts WHERE host_id=$1 AND acquired=FALSE`, s.charID).StructScan(&hunt) - if err == nil { - hunts = append(hunts, hunt) + hunt, err := s.server.guildRepo.GetPendingHunt(s.charID) + if err == nil && hunt != nil { + hunts = append(hunts, *hunt) } case 30: - rows, err := s.server.db.Queryx(`SELECT gh.id, gh.host_id, gh.destination, gh.level, gh.start, gh.collected, gh.hunt_data, - (SELECT COUNT(*) FROM guild_characters gc WHERE gc.treasure_hunt = gh.id AND gc.character_id <> $1) AS hunters, - CASE - WHEN ghc.character_id IS NOT NULL THEN true - ELSE false - END AS claimed - FROM guild_hunts gh - LEFT JOIN guild_hunts_claimed ghc ON gh.id = ghc.hunt_id AND ghc.character_id = $1 - WHERE gh.guild_id=$2 AND gh.level=2 AND gh.acquired=TRUE - `, s.charID, guild.ID) + guildHunts, err := s.server.guildRepo.ListGuildHunts(guild.ID, s.charID) if err != nil { - _ = rows.Close() doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) return - } else { - for rows.Next() { - err = rows.StructScan(&hunt) - if err == nil && hunt.Start.Add(time.Second*time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntExpiry)).After(TimeAdjusted()) { - hunts = append(hunts, hunt) - } + } + for _, hunt := range guildHunts { + if hunt.Start.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntExpiry)).After(TimeAdjusted()) { + hunts = append(hunts, *hunt) } } if len(hunts) > 30 { @@ -111,8 +98,7 @@ func handleMsgMhfRegistGuildTresure(s *Session, p mhfpacket.MHFPacket) { huntData.WriteBytes(bf.ReadBytes(9)) } } - if _, err := s.server.db.Exec(`INSERT INTO guild_hunts (guild_id, host_id, destination, level, hunt_data, cats_used) VALUES ($1, $2, $3, $4, $5, $6) - `, guild.ID, s.charID, destination, level, huntData.Data(), catsUsed); err != nil { + if err := s.server.guildRepo.CreateHunt(guild.ID, s.charID, destination, level, huntData.Data(), catsUsed); err != nil { s.logger.Error("Failed to register guild treasure hunt", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -120,7 +106,7 @@ func handleMsgMhfRegistGuildTresure(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAcquireGuildTresure(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireGuildTresure) - if _, err := s.server.db.Exec(`UPDATE guild_hunts SET acquired=true WHERE id=$1`, pkt.HuntID); err != nil { + if err := s.server.guildRepo.AcquireHunt(pkt.HuntID); err != nil { s.logger.Error("Failed to acquire guild treasure hunt", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -130,18 +116,15 @@ func handleMsgMhfOperateGuildTresureReport(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfOperateGuildTresureReport) switch pkt.State { case 0: // Report registration - if _, err := s.server.db.Exec(`UPDATE guild_characters SET treasure_hunt=$1 WHERE character_id=$2`, pkt.HuntID, s.charID); err != nil { + if err := s.server.guildRepo.RegisterHuntReport(pkt.HuntID, s.charID); err != nil { s.logger.Error("Failed to register treasure hunt report", zap.Error(err)) } case 1: // Collected by hunter - if _, err := s.server.db.Exec(`UPDATE guild_hunts SET collected=true WHERE id=$1`, pkt.HuntID); err != nil { - s.logger.Error("Failed to mark treasure hunt collected", zap.Error(err)) - } - if _, err := s.server.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE treasure_hunt=$1`, pkt.HuntID); err != nil { - s.logger.Error("Failed to clear treasure hunt from guild characters", zap.Error(err)) + if err := s.server.guildRepo.CollectHunt(pkt.HuntID); err != nil { + s.logger.Error("Failed to collect treasure hunt", zap.Error(err)) } case 2: // Claim treasure - if _, err := s.server.db.Exec(`INSERT INTO guild_hunts_claimed VALUES ($1, $2)`, pkt.HuntID, s.charID); err != nil { + if err := s.server.guildRepo.ClaimHuntReward(pkt.HuntID, s.charID); err != nil { s.logger.Error("Failed to claim treasure hunt reward", zap.Error(err)) } } diff --git a/server/channelserver/repo_guild.go b/server/channelserver/repo_guild.go index 72e8eeba8..aeec4f043 100644 --- a/server/channelserver/repo_guild.go +++ b/server/channelserver/repo_guild.go @@ -6,6 +6,8 @@ import ( "fmt" "time" + "erupe-ce/common/stringsupport" + "github.com/jmoiron/sqlx" ) @@ -470,3 +472,439 @@ 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 } + +// --- Guild Posts --- + +// ListPosts returns active guild posts of the given type, ordered by newest first. +func (r *GuildRepository) ListPosts(guildID uint32, postType int) ([]*MessageBoardPost, error) { + rows, err := r.db.Queryx( + `SELECT id, stamp_id, title, body, author_id, created_at, liked_by + FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false + ORDER BY created_at DESC`, guildID, postType) + if err != nil { + return nil, err + } + defer rows.Close() + var posts []*MessageBoardPost + for rows.Next() { + post := &MessageBoardPost{} + if err := rows.StructScan(post); err != nil { + continue + } + posts = append(posts, post) + } + return posts, nil +} + +// CreatePost inserts a new guild post and soft-deletes excess posts beyond maxPosts. +func (r *GuildRepository) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error { + if _, err := r.db.Exec( + `INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)`, + guildID, authorID, stampID, postType, title, body); err != nil { + return err + } + _, err := r.db.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN ( + SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false + ORDER BY created_at DESC OFFSET $3 + )`, guildID, postType, maxPosts) + return err +} + +// DeletePost soft-deletes a guild post by ID. +func (r *GuildRepository) DeletePost(postID uint32) error { + _, err := r.db.Exec("UPDATE guild_posts SET deleted = true WHERE id = $1", postID) + return err +} + +// UpdatePost updates the title and body of a guild post. +func (r *GuildRepository) UpdatePost(postID uint32, title, body string) error { + _, err := r.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", title, body, postID) + return err +} + +// UpdatePostStamp updates the stamp of a guild post. +func (r *GuildRepository) UpdatePostStamp(postID, stampID uint32) error { + _, err := r.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", stampID, postID) + return err +} + +// GetPostLikedBy returns the liked_by CSV string for a guild post. +func (r *GuildRepository) GetPostLikedBy(postID uint32) (string, error) { + var likedBy string + err := r.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", postID).Scan(&likedBy) + return likedBy, err +} + +// SetPostLikedBy updates the liked_by CSV string for a guild post. +func (r *GuildRepository) SetPostLikedBy(postID uint32, likedBy string) error { + _, err := r.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, postID) + return err +} + +// CountNewPosts returns the count of non-deleted posts created after the given time. +func (r *GuildRepository) CountNewPosts(guildID uint32, since time.Time) (int, error) { + var count int + err := r.db.QueryRow( + `SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND deleted = false AND (EXTRACT(epoch FROM created_at)::int) > $2`, + guildID, since.Unix()).Scan(&count) + return count, err +} + +// --- Guild Alliances --- + +const allianceInfoSelectSQL = ` +SELECT +ga.id, +ga.name, +created_at, +parent_id, +CASE + WHEN sub1_id IS NULL THEN 0 + ELSE sub1_id +END, +CASE + WHEN sub2_id IS NULL THEN 0 + ELSE sub2_id +END +FROM guild_alliances ga +` + +// GetAllianceByID loads alliance data including parent and sub guilds. +func (r *GuildRepository) GetAllianceByID(allianceID uint32) (*GuildAlliance, error) { + rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE ga.id = $1`, allianceInfoSelectSQL), allianceID) + if err != nil { + return nil, err + } + defer rows.Close() + if !rows.Next() { + return nil, nil + } + return r.scanAllianceWithGuilds(rows) +} + +// ListAlliances returns all alliances with their guild data populated. +func (r *GuildRepository) ListAlliances() ([]*GuildAlliance, error) { + rows, err := r.db.Queryx(allianceInfoSelectSQL) + if err != nil { + return nil, err + } + defer rows.Close() + var alliances []*GuildAlliance + for rows.Next() { + alliance, err := r.scanAllianceWithGuilds(rows) + if err != nil { + continue + } + alliances = append(alliances, alliance) + } + return alliances, nil +} + +// CreateAlliance creates a new guild alliance with the given parent guild. +func (r *GuildRepository) CreateAlliance(name string, parentGuildID uint32) error { + _, err := r.db.Exec("INSERT INTO guild_alliances (name, parent_id) VALUES ($1, $2)", name, parentGuildID) + return err +} + +// DeleteAlliance removes an alliance by ID. +func (r *GuildRepository) DeleteAlliance(allianceID uint32) error { + _, err := r.db.Exec("DELETE FROM guild_alliances WHERE id=$1", allianceID) + return err +} + +// RemoveGuildFromAlliance removes a guild from its alliance, shifting sub2 into sub1's slot if needed. +func (r *GuildRepository) RemoveGuildFromAlliance(allianceID, guildID, subGuild1ID, subGuild2ID uint32) error { + if guildID == subGuild1ID && subGuild2ID > 0 { + _, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = sub2_id, sub2_id = NULL WHERE id = $1`, allianceID) + return err + } else if guildID == subGuild1ID { + _, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = NULL WHERE id = $1`, allianceID) + return err + } + _, err := r.db.Exec(`UPDATE guild_alliances SET sub2_id = NULL WHERE id = $1`, allianceID) + return err +} + +// scanAllianceWithGuilds scans an alliance row and populates its guild data. +func (r *GuildRepository) scanAllianceWithGuilds(rows *sqlx.Rows) (*GuildAlliance, error) { + alliance := &GuildAlliance{} + if err := rows.StructScan(alliance); err != nil { + return nil, err + } + + parentGuild, err := r.GetByID(alliance.ParentGuildID) + if err != nil { + return nil, err + } + alliance.ParentGuild = *parentGuild + alliance.TotalMembers += parentGuild.MemberCount + + if alliance.SubGuild1ID > 0 { + subGuild1, err := r.GetByID(alliance.SubGuild1ID) + if err != nil { + return nil, err + } + alliance.SubGuild1 = *subGuild1 + alliance.TotalMembers += subGuild1.MemberCount + } + + if alliance.SubGuild2ID > 0 { + subGuild2, err := r.GetByID(alliance.SubGuild2ID) + if err != nil { + return nil, err + } + alliance.SubGuild2 = *subGuild2 + alliance.TotalMembers += subGuild2.MemberCount + } + + return alliance, nil +} + +// --- Guild Adventures --- + +// ListAdventures returns all adventures for a guild. +func (r *GuildRepository) ListAdventures(guildID uint32) ([]*GuildAdventure, error) { + rows, err := r.db.Queryx( + "SELECT id, destination, charge, depart, return, collected_by FROM guild_adventures WHERE guild_id = $1", guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var adventures []*GuildAdventure + for rows.Next() { + adv := &GuildAdventure{} + if err := rows.StructScan(adv); err != nil { + continue + } + adventures = append(adventures, adv) + } + return adventures, nil +} + +// CreateAdventure inserts a new guild adventure. +func (r *GuildRepository) CreateAdventure(guildID, destination uint32, depart, returnTime int64) error { + _, err := r.db.Exec( + "INSERT INTO guild_adventures (guild_id, destination, depart, return) VALUES ($1, $2, $3, $4)", + guildID, destination, depart, returnTime) + return err +} + +// CreateAdventureWithCharge inserts a new guild adventure with an initial charge (Diva variant). +func (r *GuildRepository) CreateAdventureWithCharge(guildID, destination, charge uint32, depart, returnTime int64) error { + _, err := r.db.Exec( + "INSERT INTO guild_adventures (guild_id, destination, charge, depart, return) VALUES ($1, $2, $3, $4, $5)", + guildID, destination, charge, depart, returnTime) + return err +} + +// CollectAdventure marks an adventure as collected by the given character (CSV append). +func (r *GuildRepository) CollectAdventure(adventureID uint32, charID uint32) error { + var collectedBy string + err := r.db.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1", adventureID).Scan(&collectedBy) + if err != nil { + return err + } + collectedBy = stringsupport.CSVAdd(collectedBy, int(charID)) + _, err = r.db.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID) + return err +} + +// ChargeAdventure adds charge to a guild adventure. +func (r *GuildRepository) ChargeAdventure(adventureID uint32, amount uint32) error { + _, err := r.db.Exec("UPDATE guild_adventures SET charge = charge + $1 WHERE id = $2", amount, adventureID) + return err +} + +// --- Guild Treasure Hunts --- + +// GetPendingHunt returns the pending (unacquired) hunt for a character, or nil if none. +func (r *GuildRepository) GetPendingHunt(charID uint32) (*TreasureHunt, error) { + hunt := &TreasureHunt{} + err := r.db.QueryRowx( + `SELECT id, host_id, destination, level, start, hunt_data FROM guild_hunts WHERE host_id=$1 AND acquired=FALSE`, + charID).StructScan(hunt) + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + if err != nil { + return nil, err + } + return hunt, nil +} + +// ListGuildHunts returns acquired level-2 hunts for a guild, with hunter counts and claim status. +func (r *GuildRepository) ListGuildHunts(guildID, charID uint32) ([]*TreasureHunt, error) { + rows, err := r.db.Queryx(`SELECT gh.id, gh.host_id, gh.destination, gh.level, gh.start, gh.collected, gh.hunt_data, + (SELECT COUNT(*) FROM guild_characters gc WHERE gc.treasure_hunt = gh.id AND gc.character_id <> $1) AS hunters, + CASE + WHEN ghc.character_id IS NOT NULL THEN true + ELSE false + END AS claimed + FROM guild_hunts gh + LEFT JOIN guild_hunts_claimed ghc ON gh.id = ghc.hunt_id AND ghc.character_id = $1 + WHERE gh.guild_id=$2 AND gh.level=2 AND gh.acquired=TRUE + `, charID, guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var hunts []*TreasureHunt + for rows.Next() { + hunt := &TreasureHunt{} + if err := rows.StructScan(hunt); err != nil { + continue + } + hunts = append(hunts, hunt) + } + return hunts, nil +} + +// CreateHunt inserts a new guild treasure hunt. +func (r *GuildRepository) CreateHunt(guildID, hostID, destination, level uint32, huntData []byte, catsUsed string) error { + _, err := r.db.Exec( + `INSERT INTO guild_hunts (guild_id, host_id, destination, level, hunt_data, cats_used) VALUES ($1, $2, $3, $4, $5, $6)`, + guildID, hostID, destination, level, huntData, catsUsed) + return err +} + +// AcquireHunt marks a treasure hunt as acquired. +func (r *GuildRepository) AcquireHunt(huntID uint32) error { + _, err := r.db.Exec(`UPDATE guild_hunts SET acquired=true WHERE id=$1`, huntID) + return err +} + +// RegisterHuntReport sets a character's active treasure hunt. +func (r *GuildRepository) RegisterHuntReport(huntID, charID uint32) error { + _, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=$1 WHERE character_id=$2`, huntID, charID) + return err +} + +// CollectHunt marks a hunt as collected and clears all characters' treasure_hunt references. +func (r *GuildRepository) CollectHunt(huntID uint32) error { + if _, err := r.db.Exec(`UPDATE guild_hunts SET collected=true WHERE id=$1`, huntID); err != nil { + return err + } + _, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE treasure_hunt=$1`, huntID) + return err +} + +// ClaimHuntReward records that a character has claimed a treasure hunt reward. +func (r *GuildRepository) ClaimHuntReward(huntID, charID uint32) error { + _, err := r.db.Exec(`INSERT INTO guild_hunts_claimed VALUES ($1, $2)`, huntID, charID) + return err +} + +// --- Guild Cooking/Meals --- + +// ListMeals returns all meals for a guild. +func (r *GuildRepository) ListMeals(guildID uint32) ([]*GuildMeal, error) { + rows, err := r.db.Queryx("SELECT id, meal_id, level, created_at FROM guild_meals WHERE guild_id = $1", guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var meals []*GuildMeal + for rows.Next() { + meal := &GuildMeal{} + if err := rows.StructScan(meal); err != nil { + continue + } + meals = append(meals, meal) + } + return meals, nil +} + +// CreateMeal inserts a new guild meal and returns the new ID. +func (r *GuildRepository) CreateMeal(guildID, mealID, level uint32, createdAt time.Time) (uint32, error) { + var id uint32 + err := r.db.QueryRow( + "INSERT INTO guild_meals (guild_id, meal_id, level, created_at) VALUES ($1, $2, $3, $4) RETURNING id", + guildID, mealID, level, createdAt).Scan(&id) + return id, err +} + +// UpdateMeal updates an existing guild meal's fields. +func (r *GuildRepository) UpdateMeal(mealID, newMealID, level uint32, createdAt time.Time) error { + _, err := r.db.Exec("UPDATE guild_meals SET meal_id = $1, level = $2, created_at = $3 WHERE id = $4", + newMealID, level, createdAt, mealID) + return err +} + +// ClaimHuntBox updates the box_claimed timestamp for a guild character. +func (r *GuildRepository) ClaimHuntBox(charID uint32, claimedAt time.Time) error { + _, err := r.db.Exec(`UPDATE guild_characters SET box_claimed=$1 WHERE character_id=$2`, claimedAt, charID) + return err +} + +// GuildKill represents a kill log entry for guild hunt data. +type GuildKill struct { + ID uint32 `db:"id"` + Monster uint32 `db:"monster"` +} + +// ListGuildKills returns kill log entries for guild members since the character's last box claim. +func (r *GuildRepository) ListGuildKills(guildID, charID uint32) ([]*GuildKill, error) { + rows, err := r.db.Queryx(`SELECT kl.id, kl.monster FROM kill_logs kl + INNER JOIN guild_characters gc ON kl.character_id = gc.character_id + WHERE gc.guild_id=$1 + AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2) + `, guildID, charID) + if err != nil { + return nil, err + } + defer rows.Close() + var kills []*GuildKill + for rows.Next() { + kill := &GuildKill{} + if err := rows.StructScan(kill); err != nil { + continue + } + kills = append(kills, kill) + } + return kills, nil +} + +// CountGuildKills returns the count of kill log entries for guild members since the character's last box claim. +func (r *GuildRepository) CountGuildKills(guildID, charID uint32) (int, error) { + var count int + err := r.db.QueryRow(`SELECT COUNT(*) FROM kill_logs kl + INNER JOIN guild_characters gc ON kl.character_id = gc.character_id + WHERE gc.guild_id=$1 + AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2) + `, guildID, charID).Scan(&count) + return count, err +} + +// --- Guild Scouts --- + +// ScoutedCharacter represents an invited character in the scout list. +type ScoutedCharacter struct { + CharID uint32 `db:"id"` + Name string `db:"name"` + HR uint16 `db:"hr"` + GR uint16 `db:"gr"` + ActorID uint32 `db:"actor_id"` +} + +// ListInvitedCharacters returns all characters with pending guild invitations. +func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error) { + rows, err := r.db.Queryx(` + SELECT c.id, c.name, c.hr, c.gr, ga.actor_id + FROM guild_applications ga + JOIN characters c ON c.id = ga.character_id + WHERE ga.guild_id = $1 AND ga.application_type = 'invited' + `, guildID) + if err != nil { + return nil, err + } + defer rows.Close() + var chars []*ScoutedCharacter + for rows.Next() { + sc := &ScoutedCharacter{} + if err := rows.StructScan(sc); err != nil { + continue + } + chars = append(chars, sc) + } + return chars, nil +}