diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go index d4261ed03..817eac608 100644 --- a/server/channelserver/handlers_cafe.go +++ b/server/channelserver/handlers_cafe.go @@ -118,59 +118,45 @@ type CafeBonus struct { func handleMsgMhfGetCafeDurationBonusInfo(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetCafeDurationBonusInfo) - bf := byteframe.NewByteFrame() - var count uint32 - rows, err := s.server.cafeRepo.GetBonuses(s.charID) + bonuses, err := s.server.cafeRepo.GetBonuses(s.charID) if err != nil { s.logger.Error("Error getting cafebonus", zap.Error(err)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) - } else { - for rows.Next() { - count++ - cafeBonus := &CafeBonus{} - err = rows.StructScan(&cafeBonus) - if err != nil { - s.logger.Error("Error scanning cafebonus", zap.Error(err)) - } - bf.WriteUint32(cafeBonus.TimeReq) - bf.WriteUint32(cafeBonus.ItemType) - bf.WriteUint32(cafeBonus.ItemID) - bf.WriteUint32(cafeBonus.Quantity) - bf.WriteBool(cafeBonus.Claimed) - } - resp := byteframe.NewByteFrame() - resp.WriteUint32(0) - resp.WriteUint32(uint32(TimeAdjusted().Unix())) - resp.WriteUint32(count) - resp.WriteBytes(bf.Data()) - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + return } + bf := byteframe.NewByteFrame() + for _, cb := range bonuses { + bf.WriteUint32(cb.TimeReq) + bf.WriteUint32(cb.ItemType) + bf.WriteUint32(cb.ItemID) + bf.WriteUint32(cb.Quantity) + bf.WriteBool(cb.Claimed) + } + resp := byteframe.NewByteFrame() + resp.WriteUint32(0) + resp.WriteUint32(uint32(TimeAdjusted().Unix())) + resp.WriteUint32(uint32(len(bonuses))) + resp.WriteBytes(bf.Data()) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgMhfReceiveCafeDurationBonus(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfReceiveCafeDurationBonus) bf := byteframe.NewByteFrame() - var count uint32 bf.WriteUint32(0) - rows, err := s.server.cafeRepo.GetClaimable(s.charID, TimeAdjusted().Unix()-s.sessionStart) + claimable, err := s.server.cafeRepo.GetClaimable(s.charID, TimeAdjusted().Unix()-s.sessionStart) if err != nil || !mhfcourse.CourseExists(30, s.courses) { doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } else { - for rows.Next() { - cafeBonus := &CafeBonus{} - err = rows.StructScan(cafeBonus) - if err != nil { - continue - } - count++ - bf.WriteUint32(cafeBonus.ID) - bf.WriteUint32(cafeBonus.ItemType) - bf.WriteUint32(cafeBonus.ItemID) - bf.WriteUint32(cafeBonus.Quantity) + for _, cb := range claimable { + bf.WriteUint32(cb.ID) + bf.WriteUint32(cb.ItemType) + bf.WriteUint32(cb.ItemID) + bf.WriteUint32(cb.Quantity) } _, _ = bf.Seek(0, io.SeekStart) - bf.WriteUint32(count) + bf.WriteUint32(uint32(len(claimable))) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } } diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index db45d954f..7e2a71676 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -78,16 +78,13 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { const divaIDSentinel = uint32(0xCAFEBEEF) id, start := divaIDSentinel, uint32(0) - rows, err := s.server.divaRepo.GetEvents() + events, err := s.server.divaRepo.GetEvents() if err != nil { s.logger.Error("Failed to query diva schedule", zap.Error(err)) - } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - if err := rows.Scan(&id, &start); err != nil { - s.logger.Error("Failed to scan diva schedule row", zap.Error(err)) - } - } + } else if len(events) > 0 { + last := events[len(events)-1] + id = last.ID + start = last.StartTime } var timestamps []uint32 diff --git a/server/channelserver/handlers_event.go b/server/channelserver/handlers_event.go index 71114d30d..a97d301e4 100644 --- a/server/channelserver/handlers_event.go +++ b/server/channelserver/handlers_event.go @@ -136,18 +136,11 @@ func handleMsgMhfGetKeepLoginBoostStatus(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() - var loginBoosts []loginBoost - rows, err := s.server.eventRepo.GetLoginBoosts(s.charID) + loginBoosts, err := s.server.eventRepo.GetLoginBoosts(s.charID) if err != nil || s.server.erupeConfig.GameplayOptions.DisableLoginBoost { - _ = rows.Close() doAckBufSucceed(s, pkt.AckHandle, make([]byte, 35)) return } - for rows.Next() { - var temp loginBoost - _ = rows.StructScan(&temp) - loginBoosts = append(loginBoosts, temp) - } if len(loginBoosts) == 0 { temp := TimeWeekStart() loginBoosts = []loginBoost{ diff --git a/server/channelserver/handlers_mercenary.go b/server/channelserver/handlers_mercenary.go index 05af0ef8f..93419a92c 100644 --- a/server/channelserver/handlers_mercenary.go +++ b/server/channelserver/handlers_mercenary.go @@ -225,27 +225,18 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { } if pkt.Op != 2 && pkt.Op != 5 { - var loans uint8 - temp := byteframe.NewByteFrame() - rows, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID) + loans, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID) if err != nil { s.logger.Error("Failed to query mercenary loans", zap.Error(err)) - } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - if err := rows.Scan(&name, &cid, &pactID); err != nil { - continue - } - loans++ - temp.WriteUint32(uint32(pactID)) - temp.WriteUint32(cid) - temp.WriteUint32(uint32(TimeAdjusted().Unix())) - temp.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix())) - temp.WriteBytes(stringsupport.PaddedString(name, 18, true)) - } } - bf.WriteUint8(loans) - bf.WriteBytes(temp.Data()) + bf.WriteUint8(uint8(len(loans))) + for _, loan := range loans { + bf.WriteUint32(uint32(loan.PactID)) + bf.WriteUint32(loan.CharID) + bf.WriteUint32(uint32(TimeAdjusted().Unix())) + bf.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix())) + bf.WriteBytes(stringsupport.PaddedString(loan.Name, 18, true)) + } if pkt.Op != 1 && pkt.Op != 4 { data, _ := s.server.charRepo.LoadColumn(s.charID, "savemercenary") @@ -393,36 +384,28 @@ func getGuildAirouList(s *Session) []Airou { if err != nil { return guildCats } - rows, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID) + usages, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID) if err != nil { s.logger.Warn("Failed to get recently used airous", zap.Error(err)) return guildCats } - var csvTemp string - var startTemp time.Time - for rows.Next() { - err = rows.Scan(&csvTemp, &startTemp) - if err != nil { - continue - } - if startTemp.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntPartnyaCooldown)).Before(TimeAdjusted()) { - for i, j := range stringsupport.CSVElems(csvTemp) { + for _, usage := range usages { + if usage.Start.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntPartnyaCooldown)).Before(TimeAdjusted()) { + for i, j := range stringsupport.CSVElems(usage.CatsUsed) { bannedCats[uint32(j)] = i } } } - rows, err = s.server.mercenaryRepo.GetGuildAirou(guild.ID) + airouData, err := s.server.mercenaryRepo.GetGuildAirou(guild.ID) if err != nil { s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err)) return guildCats } - for rows.Next() { - var data []byte - err = rows.Scan(&data) - if err != nil || len(data) == 0 { + for _, data := range airouData { + if len(data) == 0 { continue } // first byte has cat existence in general, can skip if 0 diff --git a/server/channelserver/handlers_misc.go b/server/channelserver/handlers_misc.go index 69c94bee5..42f530b32 100644 --- a/server/channelserver/handlers_misc.go +++ b/server/channelserver/handlers_misc.go @@ -254,18 +254,13 @@ func handleMsgMhfGetTrendWeapon(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetTrendWeapon) trendWeapons := [14][3]TrendWeapon{} for i := uint8(0); i < 14; i++ { - rows, err := s.server.miscRepo.GetTrendWeapons(i) + ids, err := s.server.miscRepo.GetTrendWeapons(i) if err != nil { continue } - j := 0 - for rows.Next() { + for j, id := range ids { trendWeapons[i][j].WeaponType = i - if err := rows.Scan(&trendWeapons[i][j].WeaponID); err != nil { - s.logger.Error("Failed to scan trend weapon", zap.Error(err)) - break - } - j++ + trendWeapons[i][j].WeaponID = id } } diff --git a/server/channelserver/handlers_rengoku.go b/server/channelserver/handlers_rengoku.go index ff219a192..2571c114d 100644 --- a/server/channelserver/handlers_rengoku.go +++ b/server/channelserver/handlers_rengoku.go @@ -215,7 +215,6 @@ func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) { } } - var score RengokuScore var selfExist bool i := uint32(1) bf := byteframe.NewByteFrame() @@ -225,16 +224,14 @@ func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) { if guild != nil { guildID = guild.ID } - rows, err := s.server.rengokuRepo.GetRanking(pkt.Leaderboard, guildID) + scores, err := s.server.rengokuRepo.GetRanking(pkt.Leaderboard, guildID) if err != nil { s.logger.Error("Failed to query rengoku ranking", zap.Error(err)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 11)) return } - defer func() { _ = rows.Close() }() - for rows.Next() { - _ = rows.StructScan(&score) + for _, score := range scores { if score.Name == s.Name { bf.WriteUint32(i) bf.WriteUint32(score.Score) diff --git a/server/channelserver/handlers_scenario.go b/server/channelserver/handlers_scenario.go index 622350396..57c6e7399 100644 --- a/server/channelserver/handlers_scenario.go +++ b/server/channelserver/handlers_scenario.go @@ -20,22 +20,12 @@ type Scenario struct { func handleMsgMhfInfoScenarioCounter(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoScenarioCounter) - var scenarios []Scenario - var scenario Scenario - scenarioData, err := s.server.scenarioRepo.GetCounters() + scenarios, err := s.server.scenarioRepo.GetCounters() if err != nil { - _ = scenarioData.Close() s.logger.Error("Failed to get scenario counter info from db", zap.Error(err)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1)) return } - for scenarioData.Next() { - err = scenarioData.Scan(&scenario.MainID, &scenario.CategoryID) - if err != nil { - continue - } - scenarios = append(scenarios, scenario) - } // Trim excess scenarios if len(scenarios) > 128 { diff --git a/server/channelserver/handlers_shop.go b/server/channelserver/handlers_shop.go index 53facd174..9c33f8fa7 100644 --- a/server/channelserver/handlers_shop.go +++ b/server/channelserver/handlers_shop.go @@ -57,17 +57,9 @@ func writeShopItems(bf *byteframe.ByteFrame, items []ShopItem, mode cfg.Mode) { } func getShopItems(s *Session, shopType uint8, shopID uint32) []ShopItem { - var items []ShopItem - var temp ShopItem - rows, err := s.server.shopRepo.GetShopItems(shopType, shopID, s.charID) - if err == nil { - for rows.Next() { - err = rows.StructScan(&temp) - if err != nil { - continue - } - items = append(items, temp) - } + items, err := s.server.shopRepo.GetShopItems(shopType, shopID, s.charID) + if err != nil { + return nil } return items } @@ -270,20 +262,11 @@ func handleMsgMhfGetFpointExchangeList(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetFpointExchangeList) bf := byteframe.NewByteFrame() - var exchange FPointExchange - var exchanges []FPointExchange + exchanges, _ := s.server.shopRepo.GetFpointExchangeList() var buyables uint16 - rows, err := s.server.shopRepo.GetFpointExchangeList() - if err == nil { - for rows.Next() { - err = rows.StructScan(&exchange) - if err != nil { - continue - } - if exchange.Buyable { - buyables++ - } - exchanges = append(exchanges, exchange) + for _, e := range exchanges { + if e.Buyable { + buyables++ } } if s.server.erupeConfig.RealClientMode <= cfg.Z2 { diff --git a/server/channelserver/repo_cafe.go b/server/channelserver/repo_cafe.go index 4eb7ceb3e..064ef4ffe 100644 --- a/server/channelserver/repo_cafe.go +++ b/server/channelserver/repo_cafe.go @@ -21,8 +21,9 @@ func (r *CafeRepository) ResetAccepted(charID uint32) error { } // GetBonuses returns all cafe bonuses with their claimed status for a character. -func (r *CafeRepository) GetBonuses(charID uint32) (*sqlx.Rows, error) { - return r.db.Queryx(` +func (r *CafeRepository) GetBonuses(charID uint32) ([]CafeBonus, error) { + var result []CafeBonus + err := r.db.Select(&result, ` SELECT cb.id, time_req, item_type, item_id, quantity, ( SELECT count(*) @@ -30,11 +31,13 @@ func (r *CafeRepository) GetBonuses(charID uint32) (*sqlx.Rows, error) { WHERE cb.id = ca.cafe_id AND ca.character_id = $1 )::int::bool AS claimed FROM cafebonus cb ORDER BY id ASC;`, charID) + return result, err } // GetClaimable returns unclaimed cafe bonuses where the character has enough accumulated time. -func (r *CafeRepository) GetClaimable(charID uint32, elapsedSec int64) (*sqlx.Rows, error) { - return r.db.Queryx(` +func (r *CafeRepository) GetClaimable(charID uint32, elapsedSec int64) ([]CafeBonus, error) { + var result []CafeBonus + err := r.db.Select(&result, ` SELECT c.id, time_req, item_type, item_id, quantity FROM cafebonus c WHERE ( @@ -46,6 +49,7 @@ func (r *CafeRepository) GetClaimable(charID uint32, elapsedSec int64) (*sqlx.Ro FROM characters ch WHERE ch.id = $1 ) >= time_req`, charID, elapsedSec) + return result, err } // GetBonusItem returns the item type and quantity for a specific cafe bonus. diff --git a/server/channelserver/repo_diva.go b/server/channelserver/repo_diva.go index 923ced8dd..90b53e201 100644 --- a/server/channelserver/repo_diva.go +++ b/server/channelserver/repo_diva.go @@ -26,7 +26,15 @@ func (r *DivaRepository) InsertEvent(startEpoch uint32) error { return err } -// GetEvents returns all diva events with their ID and start_time epoch. -func (r *DivaRepository) GetEvents() (*sqlx.Rows, error) { - return r.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") +// DivaEvent represents a diva event row with ID and start_time epoch. +type DivaEvent struct { + ID uint32 `db:"id"` + StartTime uint32 `db:"start_time"` +} + +// GetEvents returns all diva events with their ID and start_time epoch. +func (r *DivaRepository) GetEvents() ([]DivaEvent, error) { + var result []DivaEvent + err := r.db.Select(&result, "SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") + return result, err } diff --git a/server/channelserver/repo_event.go b/server/channelserver/repo_event.go index 77ee96791..dd1154fd1 100644 --- a/server/channelserver/repo_event.go +++ b/server/channelserver/repo_event.go @@ -31,8 +31,10 @@ func (r *EventRepository) InsertFeatureWeapon(startTime time.Time, features uint } // GetLoginBoosts returns all login boost rows for a character, ordered by week_req. -func (r *EventRepository) GetLoginBoosts(charID uint32) (*sqlx.Rows, error) { - return r.db.Queryx("SELECT week_req, expiration, reset FROM login_boost WHERE char_id=$1 ORDER BY week_req", charID) +func (r *EventRepository) GetLoginBoosts(charID uint32) ([]loginBoost, error) { + var result []loginBoost + err := r.db.Select(&result, "SELECT week_req, expiration, reset FROM login_boost WHERE char_id=$1 ORDER BY week_req", charID) + return result, err } // InsertLoginBoost creates a new login boost entry. diff --git a/server/channelserver/repo_interfaces.go b/server/channelserver/repo_interfaces.go index a5bf64494..c7802ba38 100644 --- a/server/channelserver/repo_interfaces.go +++ b/server/channelserver/repo_interfaces.go @@ -3,8 +3,6 @@ package channelserver import ( "database/sql" "time" - - "github.com/jmoiron/sqlx" ) // Repository interfaces decouple handlers from concrete PostgreSQL implementations, @@ -224,7 +222,7 @@ type TowerRepo interface { // RengokuRepo defines the contract for rengoku score/ranking data access. type RengokuRepo interface { UpsertScore(charID uint32, maxStagesMp, maxPointsMp, maxStagesSp, maxPointsSp uint32) error - GetRanking(leaderboard uint32, guildID uint32) (*sqlx.Rows, error) + GetRanking(leaderboard uint32, guildID uint32) ([]RengokuScore, error) } // MailRepo defines the contract for in-game mail data access. @@ -270,7 +268,7 @@ type SessionRepo interface { type EventRepo interface { GetFeatureWeapon(startTime time.Time) (activeFeature, error) InsertFeatureWeapon(startTime time.Time, features uint32) error - GetLoginBoosts(charID uint32) (*sqlx.Rows, error) + GetLoginBoosts(charID uint32) ([]loginBoost, error) InsertLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error UpdateLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error GetEventQuests() (*sql.Rows, error) @@ -287,17 +285,17 @@ type AchievementRepo interface { // ShopRepo defines the contract for shop data access. type ShopRepo interface { - GetShopItems(shopType uint8, shopID uint32, charID uint32) (*sqlx.Rows, error) + GetShopItems(shopType uint8, shopID uint32, charID uint32) ([]ShopItem, error) RecordPurchase(charID, shopItemID, quantity uint32) error GetFpointItem(tradeID uint32) (quantity, fpoints int, err error) - GetFpointExchangeList() (*sqlx.Rows, error) + GetFpointExchangeList() ([]FPointExchange, error) } // CafeRepo defines the contract for cafe bonus data access. type CafeRepo interface { ResetAccepted(charID uint32) error - GetBonuses(charID uint32) (*sqlx.Rows, error) - GetClaimable(charID uint32, elapsedSec int64) (*sqlx.Rows, error) + GetBonuses(charID uint32) ([]CafeBonus, error) + GetClaimable(charID uint32, elapsedSec int64) ([]CafeBonus, error) GetBonusItem(bonusID uint32) (itemType, quantity uint32, err error) AcceptBonus(bonusID, charID uint32) error } @@ -314,25 +312,25 @@ type GoocooRepo interface { type DivaRepo interface { DeleteEvents() error InsertEvent(startEpoch uint32) error - GetEvents() (*sqlx.Rows, error) + GetEvents() ([]DivaEvent, error) } // MiscRepo defines the contract for miscellaneous data access. type MiscRepo interface { - GetTrendWeapons(weaponType uint8) (*sql.Rows, error) + GetTrendWeapons(weaponType uint8) ([]uint16, error) UpsertTrendWeapon(weaponID uint16, weaponType uint8) error } // ScenarioRepo defines the contract for scenario counter data access. type ScenarioRepo interface { - GetCounters() (*sqlx.Rows, error) + GetCounters() ([]Scenario, error) } // MercenaryRepo defines the contract for mercenary/rasta data access. type MercenaryRepo interface { NextRastaID() (uint32, error) NextAirouID() (uint32, error) - GetMercenaryLoans(charID uint32) (*sql.Rows, error) - GetGuildHuntCatsUsed(charID uint32) (*sql.Rows, error) - GetGuildAirou(guildID uint32) (*sql.Rows, error) + GetMercenaryLoans(charID uint32) ([]MercenaryLoan, error) + GetGuildHuntCatsUsed(charID uint32) ([]GuildHuntCatUsage, error) + GetGuildAirou(guildID uint32) ([][]byte, error) } diff --git a/server/channelserver/repo_mercenary.go b/server/channelserver/repo_mercenary.go index ef7e247f3..6ecff1e4a 100644 --- a/server/channelserver/repo_mercenary.go +++ b/server/channelserver/repo_mercenary.go @@ -1,7 +1,8 @@ package channelserver import ( - "database/sql" + "fmt" + "time" "github.com/jmoiron/sqlx" ) @@ -30,21 +31,73 @@ func (r *MercenaryRepository) NextAirouID() (uint32, error) { return id, err } +// MercenaryLoan represents a character that has a pact with a rasta. +type MercenaryLoan struct { + Name string + CharID uint32 + PactID int +} + // GetMercenaryLoans returns characters that have a pact with the given character's rasta_id. -func (r *MercenaryRepository) GetMercenaryLoans(charID uint32) (*sql.Rows, error) { - return r.db.Query("SELECT name, id, pact_id FROM characters WHERE pact_id=(SELECT rasta_id FROM characters WHERE id=$1)", charID) +func (r *MercenaryRepository) GetMercenaryLoans(charID uint32) ([]MercenaryLoan, error) { + rows, err := r.db.Query("SELECT name, id, pact_id FROM characters WHERE pact_id=(SELECT rasta_id FROM characters WHERE id=$1)", charID) + if err != nil { + return nil, fmt.Errorf("query mercenary loans: %w", err) + } + defer rows.Close() + var result []MercenaryLoan + for rows.Next() { + var l MercenaryLoan + if err := rows.Scan(&l.Name, &l.CharID, &l.PactID); err != nil { + return nil, fmt.Errorf("scan mercenary loan: %w", err) + } + result = append(result, l) + } + return result, rows.Err() +} + +// GuildHuntCatUsage represents cats_used and start time from a guild hunt. +type GuildHuntCatUsage struct { + CatsUsed string + Start time.Time } // GetGuildHuntCatsUsed returns cats_used and start from guild_hunts for a given character. -func (r *MercenaryRepository) GetGuildHuntCatsUsed(charID uint32) (*sql.Rows, error) { - return r.db.Query(`SELECT cats_used, start FROM guild_hunts gh +func (r *MercenaryRepository) GetGuildHuntCatsUsed(charID uint32) ([]GuildHuntCatUsage, error) { + rows, err := r.db.Query(`SELECT cats_used, start FROM guild_hunts gh INNER JOIN characters c ON gh.host_id = c.id WHERE c.id=$1`, charID) + if err != nil { + return nil, fmt.Errorf("query guild hunt cats: %w", err) + } + defer rows.Close() + var result []GuildHuntCatUsage + for rows.Next() { + var u GuildHuntCatUsage + if err := rows.Scan(&u.CatsUsed, &u.Start); err != nil { + return nil, fmt.Errorf("scan guild hunt cat: %w", err) + } + result = append(result, u) + } + return result, rows.Err() } // GetGuildAirou returns otomoairou data for all characters in a guild. -func (r *MercenaryRepository) GetGuildAirou(guildID uint32) (*sql.Rows, error) { - return r.db.Query(`SELECT c.otomoairou FROM characters c +func (r *MercenaryRepository) GetGuildAirou(guildID uint32) ([][]byte, error) { + rows, err := r.db.Query(`SELECT c.otomoairou FROM characters c INNER JOIN guild_characters gc ON gc.character_id = c.id WHERE gc.guild_id = $1 AND c.otomoairou IS NOT NULL ORDER BY c.id LIMIT 60`, guildID) + if err != nil { + return nil, fmt.Errorf("query guild airou: %w", err) + } + defer rows.Close() + var result [][]byte + for rows.Next() { + var data []byte + if err := rows.Scan(&data); err != nil { + return nil, fmt.Errorf("scan guild airou: %w", err) + } + result = append(result, data) + } + return result, rows.Err() } diff --git a/server/channelserver/repo_misc.go b/server/channelserver/repo_misc.go index d3836e6cd..f12d61010 100644 --- a/server/channelserver/repo_misc.go +++ b/server/channelserver/repo_misc.go @@ -1,7 +1,7 @@ package channelserver import ( - "database/sql" + "fmt" "github.com/jmoiron/sqlx" ) @@ -17,8 +17,21 @@ func NewMiscRepository(db *sqlx.DB) *MiscRepository { } // GetTrendWeapons returns the top 3 weapon IDs for a given weapon type, ordered by count descending. -func (r *MiscRepository) GetTrendWeapons(weaponType uint8) (*sql.Rows, error) { - return r.db.Query("SELECT weapon_id FROM trend_weapons WHERE weapon_type=$1 ORDER BY count DESC LIMIT 3", weaponType) +func (r *MiscRepository) GetTrendWeapons(weaponType uint8) ([]uint16, error) { + rows, err := r.db.Query("SELECT weapon_id FROM trend_weapons WHERE weapon_type=$1 ORDER BY count DESC LIMIT 3", weaponType) + if err != nil { + return nil, fmt.Errorf("query trend_weapons: %w", err) + } + defer rows.Close() + var result []uint16 + for rows.Next() { + var id uint16 + if err := rows.Scan(&id); err != nil { + return nil, fmt.Errorf("scan trend_weapons: %w", err) + } + result = append(result, id) + } + return result, rows.Err() } // UpsertTrendWeapon increments the count for a weapon, inserting it if it doesn't exist. diff --git a/server/channelserver/repo_rengoku.go b/server/channelserver/repo_rengoku.go index 06c454e2c..4020667b4 100644 --- a/server/channelserver/repo_rengoku.go +++ b/server/channelserver/repo_rengoku.go @@ -62,15 +62,19 @@ func rengokuIsGuildFiltered(leaderboard uint32) bool { // GetRanking returns rengoku scores for the given leaderboard. // For guild-scoped leaderboards (2,3,6,7), guildID filters the results. -func (r *RengokuRepository) GetRanking(leaderboard uint32, guildID uint32) (*sqlx.Rows, error) { +func (r *RengokuRepository) GetRanking(leaderboard uint32, guildID uint32) ([]RengokuScore, error) { col := rengokuColumnForLeaderboard(leaderboard) + var result []RengokuScore + var err error if rengokuIsGuildFiltered(leaderboard) { - return r.db.Queryx( + err = r.db.Select(&result, fmt.Sprintf("SELECT %s AS score %s WHERE guild_id=$1 ORDER BY %s DESC", col, rengokuScoreQueryRepo, col), guildID, ) + } else { + err = r.db.Select(&result, + fmt.Sprintf("SELECT %s AS score %s ORDER BY %s DESC", col, rengokuScoreQueryRepo, col), + ) } - return r.db.Queryx( - fmt.Sprintf("SELECT %s AS score %s ORDER BY %s DESC", col, rengokuScoreQueryRepo, col), - ) + return result, err } diff --git a/server/channelserver/repo_scenario.go b/server/channelserver/repo_scenario.go index 003f52371..3caa0a22b 100644 --- a/server/channelserver/repo_scenario.go +++ b/server/channelserver/repo_scenario.go @@ -1,6 +1,8 @@ package channelserver import ( + "fmt" + "github.com/jmoiron/sqlx" ) @@ -15,6 +17,19 @@ func NewScenarioRepository(db *sqlx.DB) *ScenarioRepository { } // GetCounters returns all scenario counters. -func (r *ScenarioRepository) GetCounters() (*sqlx.Rows, error) { - return r.db.Queryx("SELECT scenario_id, category_id FROM scenario_counter") +func (r *ScenarioRepository) GetCounters() ([]Scenario, error) { + rows, err := r.db.Query("SELECT scenario_id, category_id FROM scenario_counter") + if err != nil { + return nil, fmt.Errorf("query scenario_counter: %w", err) + } + defer rows.Close() + var result []Scenario + for rows.Next() { + var s Scenario + if err := rows.Scan(&s.MainID, &s.CategoryID); err != nil { + return nil, fmt.Errorf("scan scenario_counter: %w", err) + } + result = append(result, s) + } + return result, rows.Err() } diff --git a/server/channelserver/repo_shop.go b/server/channelserver/repo_shop.go index f51ac8a17..fd32c8a43 100644 --- a/server/channelserver/repo_shop.go +++ b/server/channelserver/repo_shop.go @@ -15,11 +15,13 @@ func NewShopRepository(db *sqlx.DB) *ShopRepository { } // GetShopItems returns shop items with per-character purchase counts. -func (r *ShopRepository) GetShopItems(shopType uint8, shopID uint32, charID uint32) (*sqlx.Rows, error) { - return r.db.Queryx(`SELECT id, item_id, cost, quantity, min_hr, min_sr, min_gr, store_level, max_quantity, +func (r *ShopRepository) GetShopItems(shopType uint8, shopID uint32, charID uint32) ([]ShopItem, error) { + var result []ShopItem + err := r.db.Select(&result, `SELECT id, item_id, cost, quantity, min_hr, min_sr, min_gr, store_level, max_quantity, COALESCE((SELECT bought FROM shop_items_bought WHERE shop_item_id=si.id AND character_id=$3), 0) as used_quantity, road_floors, road_fatalis FROM shop_items si WHERE shop_type=$1 AND shop_id=$2 `, shopType, shopID, charID) + return result, err } // RecordPurchase upserts a purchase record, adding to the bought count. @@ -39,6 +41,8 @@ func (r *ShopRepository) GetFpointItem(tradeID uint32) (quantity, fpoints int, e } // GetFpointExchangeList returns all frontier point exchange items ordered by buyable status. -func (r *ShopRepository) GetFpointExchangeList() (*sqlx.Rows, error) { - return r.db.Queryx(`SELECT id, item_type, item_id, quantity, fpoints, buyable FROM fpoint_items ORDER BY buyable DESC`) +func (r *ShopRepository) GetFpointExchangeList() ([]FPointExchange, error) { + var result []FPointExchange + err := r.db.Select(&result, `SELECT id, item_type, item_id, quantity, fpoints, buyable FROM fpoint_items ORDER BY buyable DESC`) + return result, err }