mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
refactor(channelserver): eliminate *sqlx.Rows/*sql.Rows from repository interfaces
Move scan loops from handlers into repository methods so that interfaces return typed slices instead of leaking database cursors. This fixes resource leaks (7 of 12 call sites never closed rows) and makes all 12 methods mockable for unit tests. Affected repos: CafeRepo, ShopRepo, EventRepo, RengokuRepo, DivaRepo, ScenarioRepo, MiscRepo, MercenaryRepo. New structs: DivaEvent, MercenaryLoan, GuildHuntCatUsage. EventRepo.GetEventQuests left as-is (requires broader Server refactor).
This commit is contained in:
@@ -118,59 +118,45 @@ type CafeBonus struct {
|
|||||||
|
|
||||||
func handleMsgMhfGetCafeDurationBonusInfo(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfGetCafeDurationBonusInfo(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfGetCafeDurationBonusInfo)
|
pkt := p.(*mhfpacket.MsgMhfGetCafeDurationBonusInfo)
|
||||||
bf := byteframe.NewByteFrame()
|
|
||||||
|
|
||||||
var count uint32
|
bonuses, err := s.server.cafeRepo.GetBonuses(s.charID)
|
||||||
rows, err := s.server.cafeRepo.GetBonuses(s.charID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Error getting cafebonus", zap.Error(err))
|
s.logger.Error("Error getting cafebonus", zap.Error(err))
|
||||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
} else {
|
return
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
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) {
|
func handleMsgMhfReceiveCafeDurationBonus(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfReceiveCafeDurationBonus)
|
pkt := p.(*mhfpacket.MsgMhfReceiveCafeDurationBonus)
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
var count uint32
|
|
||||||
bf.WriteUint32(0)
|
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) {
|
if err != nil || !mhfcourse.CourseExists(30, s.courses) {
|
||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||||
} else {
|
} else {
|
||||||
for rows.Next() {
|
for _, cb := range claimable {
|
||||||
cafeBonus := &CafeBonus{}
|
bf.WriteUint32(cb.ID)
|
||||||
err = rows.StructScan(cafeBonus)
|
bf.WriteUint32(cb.ItemType)
|
||||||
if err != nil {
|
bf.WriteUint32(cb.ItemID)
|
||||||
continue
|
bf.WriteUint32(cb.Quantity)
|
||||||
}
|
|
||||||
count++
|
|
||||||
bf.WriteUint32(cafeBonus.ID)
|
|
||||||
bf.WriteUint32(cafeBonus.ItemType)
|
|
||||||
bf.WriteUint32(cafeBonus.ItemID)
|
|
||||||
bf.WriteUint32(cafeBonus.Quantity)
|
|
||||||
}
|
}
|
||||||
_, _ = bf.Seek(0, io.SeekStart)
|
_, _ = bf.Seek(0, io.SeekStart)
|
||||||
bf.WriteUint32(count)
|
bf.WriteUint32(uint32(len(claimable)))
|
||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,16 +78,13 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
|
|
||||||
const divaIDSentinel = uint32(0xCAFEBEEF)
|
const divaIDSentinel = uint32(0xCAFEBEEF)
|
||||||
id, start := divaIDSentinel, uint32(0)
|
id, start := divaIDSentinel, uint32(0)
|
||||||
rows, err := s.server.divaRepo.GetEvents()
|
events, err := s.server.divaRepo.GetEvents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to query diva schedule", zap.Error(err))
|
s.logger.Error("Failed to query diva schedule", zap.Error(err))
|
||||||
} else {
|
} else if len(events) > 0 {
|
||||||
defer func() { _ = rows.Close() }()
|
last := events[len(events)-1]
|
||||||
for rows.Next() {
|
id = last.ID
|
||||||
if err := rows.Scan(&id, &start); err != nil {
|
start = last.StartTime
|
||||||
s.logger.Error("Failed to scan diva schedule row", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var timestamps []uint32
|
var timestamps []uint32
|
||||||
|
|||||||
@@ -136,18 +136,11 @@ func handleMsgMhfGetKeepLoginBoostStatus(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
|
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
|
|
||||||
var loginBoosts []loginBoost
|
loginBoosts, err := s.server.eventRepo.GetLoginBoosts(s.charID)
|
||||||
rows, err := s.server.eventRepo.GetLoginBoosts(s.charID)
|
|
||||||
if err != nil || s.server.erupeConfig.GameplayOptions.DisableLoginBoost {
|
if err != nil || s.server.erupeConfig.GameplayOptions.DisableLoginBoost {
|
||||||
_ = rows.Close()
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 35))
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 35))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for rows.Next() {
|
|
||||||
var temp loginBoost
|
|
||||||
_ = rows.StructScan(&temp)
|
|
||||||
loginBoosts = append(loginBoosts, temp)
|
|
||||||
}
|
|
||||||
if len(loginBoosts) == 0 {
|
if len(loginBoosts) == 0 {
|
||||||
temp := TimeWeekStart()
|
temp := TimeWeekStart()
|
||||||
loginBoosts = []loginBoost{
|
loginBoosts = []loginBoost{
|
||||||
|
|||||||
@@ -225,27 +225,18 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pkt.Op != 2 && pkt.Op != 5 {
|
if pkt.Op != 2 && pkt.Op != 5 {
|
||||||
var loans uint8
|
loans, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID)
|
||||||
temp := byteframe.NewByteFrame()
|
|
||||||
rows, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to query mercenary loans", zap.Error(err))
|
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.WriteUint8(uint8(len(loans)))
|
||||||
bf.WriteBytes(temp.Data())
|
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 {
|
if pkt.Op != 1 && pkt.Op != 4 {
|
||||||
data, _ := s.server.charRepo.LoadColumn(s.charID, "savemercenary")
|
data, _ := s.server.charRepo.LoadColumn(s.charID, "savemercenary")
|
||||||
@@ -393,36 +384,28 @@ func getGuildAirouList(s *Session) []Airou {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return guildCats
|
return guildCats
|
||||||
}
|
}
|
||||||
rows, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID)
|
usages, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Failed to get recently used airous", zap.Error(err))
|
s.logger.Warn("Failed to get recently used airous", zap.Error(err))
|
||||||
return guildCats
|
return guildCats
|
||||||
}
|
}
|
||||||
|
|
||||||
var csvTemp string
|
for _, usage := range usages {
|
||||||
var startTemp time.Time
|
if usage.Start.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntPartnyaCooldown)).Before(TimeAdjusted()) {
|
||||||
for rows.Next() {
|
for i, j := range stringsupport.CSVElems(usage.CatsUsed) {
|
||||||
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) {
|
|
||||||
bannedCats[uint32(j)] = i
|
bannedCats[uint32(j)] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err = s.server.mercenaryRepo.GetGuildAirou(guild.ID)
|
airouData, err := s.server.mercenaryRepo.GetGuildAirou(guild.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err))
|
s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err))
|
||||||
return guildCats
|
return guildCats
|
||||||
}
|
}
|
||||||
|
|
||||||
for rows.Next() {
|
for _, data := range airouData {
|
||||||
var data []byte
|
if len(data) == 0 {
|
||||||
err = rows.Scan(&data)
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// first byte has cat existence in general, can skip if 0
|
// first byte has cat existence in general, can skip if 0
|
||||||
|
|||||||
@@ -254,18 +254,13 @@ func handleMsgMhfGetTrendWeapon(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
pkt := p.(*mhfpacket.MsgMhfGetTrendWeapon)
|
pkt := p.(*mhfpacket.MsgMhfGetTrendWeapon)
|
||||||
trendWeapons := [14][3]TrendWeapon{}
|
trendWeapons := [14][3]TrendWeapon{}
|
||||||
for i := uint8(0); i < 14; i++ {
|
for i := uint8(0); i < 14; i++ {
|
||||||
rows, err := s.server.miscRepo.GetTrendWeapons(i)
|
ids, err := s.server.miscRepo.GetTrendWeapons(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
j := 0
|
for j, id := range ids {
|
||||||
for rows.Next() {
|
|
||||||
trendWeapons[i][j].WeaponType = i
|
trendWeapons[i][j].WeaponType = i
|
||||||
if err := rows.Scan(&trendWeapons[i][j].WeaponID); err != nil {
|
trendWeapons[i][j].WeaponID = id
|
||||||
s.logger.Error("Failed to scan trend weapon", zap.Error(err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
j++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var score RengokuScore
|
|
||||||
var selfExist bool
|
var selfExist bool
|
||||||
i := uint32(1)
|
i := uint32(1)
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
@@ -225,16 +224,14 @@ func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
if guild != nil {
|
if guild != nil {
|
||||||
guildID = guild.ID
|
guildID = guild.ID
|
||||||
}
|
}
|
||||||
rows, err := s.server.rengokuRepo.GetRanking(pkt.Leaderboard, guildID)
|
scores, err := s.server.rengokuRepo.GetRanking(pkt.Leaderboard, guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to query rengoku ranking", zap.Error(err))
|
s.logger.Error("Failed to query rengoku ranking", zap.Error(err))
|
||||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 11))
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 11))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
|
|
||||||
for rows.Next() {
|
for _, score := range scores {
|
||||||
_ = rows.StructScan(&score)
|
|
||||||
if score.Name == s.Name {
|
if score.Name == s.Name {
|
||||||
bf.WriteUint32(i)
|
bf.WriteUint32(i)
|
||||||
bf.WriteUint32(score.Score)
|
bf.WriteUint32(score.Score)
|
||||||
|
|||||||
@@ -20,22 +20,12 @@ type Scenario struct {
|
|||||||
|
|
||||||
func handleMsgMhfInfoScenarioCounter(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfInfoScenarioCounter(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfInfoScenarioCounter)
|
pkt := p.(*mhfpacket.MsgMhfInfoScenarioCounter)
|
||||||
var scenarios []Scenario
|
scenarios, err := s.server.scenarioRepo.GetCounters()
|
||||||
var scenario Scenario
|
|
||||||
scenarioData, err := s.server.scenarioRepo.GetCounters()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = scenarioData.Close()
|
|
||||||
s.logger.Error("Failed to get scenario counter info from db", zap.Error(err))
|
s.logger.Error("Failed to get scenario counter info from db", zap.Error(err))
|
||||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for scenarioData.Next() {
|
|
||||||
err = scenarioData.Scan(&scenario.MainID, &scenario.CategoryID)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
scenarios = append(scenarios, scenario)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim excess scenarios
|
// Trim excess scenarios
|
||||||
if len(scenarios) > 128 {
|
if len(scenarios) > 128 {
|
||||||
|
|||||||
@@ -57,17 +57,9 @@ func writeShopItems(bf *byteframe.ByteFrame, items []ShopItem, mode cfg.Mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getShopItems(s *Session, shopType uint8, shopID uint32) []ShopItem {
|
func getShopItems(s *Session, shopType uint8, shopID uint32) []ShopItem {
|
||||||
var items []ShopItem
|
items, err := s.server.shopRepo.GetShopItems(shopType, shopID, s.charID)
|
||||||
var temp ShopItem
|
if err != nil {
|
||||||
rows, err := s.server.shopRepo.GetShopItems(shopType, shopID, s.charID)
|
return nil
|
||||||
if err == nil {
|
|
||||||
for rows.Next() {
|
|
||||||
err = rows.StructScan(&temp)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = append(items, temp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
@@ -270,20 +262,11 @@ func handleMsgMhfGetFpointExchangeList(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
pkt := p.(*mhfpacket.MsgMhfGetFpointExchangeList)
|
pkt := p.(*mhfpacket.MsgMhfGetFpointExchangeList)
|
||||||
|
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
var exchange FPointExchange
|
exchanges, _ := s.server.shopRepo.GetFpointExchangeList()
|
||||||
var exchanges []FPointExchange
|
|
||||||
var buyables uint16
|
var buyables uint16
|
||||||
rows, err := s.server.shopRepo.GetFpointExchangeList()
|
for _, e := range exchanges {
|
||||||
if err == nil {
|
if e.Buyable {
|
||||||
for rows.Next() {
|
buyables++
|
||||||
err = rows.StructScan(&exchange)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if exchange.Buyable {
|
|
||||||
buyables++
|
|
||||||
}
|
|
||||||
exchanges = append(exchanges, exchange)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.server.erupeConfig.RealClientMode <= cfg.Z2 {
|
if s.server.erupeConfig.RealClientMode <= cfg.Z2 {
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ func (r *CafeRepository) ResetAccepted(charID uint32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBonuses returns all cafe bonuses with their claimed status for a character.
|
// GetBonuses returns all cafe bonuses with their claimed status for a character.
|
||||||
func (r *CafeRepository) GetBonuses(charID uint32) (*sqlx.Rows, error) {
|
func (r *CafeRepository) GetBonuses(charID uint32) ([]CafeBonus, error) {
|
||||||
return r.db.Queryx(`
|
var result []CafeBonus
|
||||||
|
err := r.db.Select(&result, `
|
||||||
SELECT cb.id, time_req, item_type, item_id, quantity,
|
SELECT cb.id, time_req, item_type, item_id, quantity,
|
||||||
(
|
(
|
||||||
SELECT count(*)
|
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
|
WHERE cb.id = ca.cafe_id AND ca.character_id = $1
|
||||||
)::int::bool AS claimed
|
)::int::bool AS claimed
|
||||||
FROM cafebonus cb ORDER BY id ASC;`, charID)
|
FROM cafebonus cb ORDER BY id ASC;`, charID)
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClaimable returns unclaimed cafe bonuses where the character has enough accumulated time.
|
// GetClaimable returns unclaimed cafe bonuses where the character has enough accumulated time.
|
||||||
func (r *CafeRepository) GetClaimable(charID uint32, elapsedSec int64) (*sqlx.Rows, error) {
|
func (r *CafeRepository) GetClaimable(charID uint32, elapsedSec int64) ([]CafeBonus, error) {
|
||||||
return r.db.Queryx(`
|
var result []CafeBonus
|
||||||
|
err := r.db.Select(&result, `
|
||||||
SELECT c.id, time_req, item_type, item_id, quantity
|
SELECT c.id, time_req, item_type, item_id, quantity
|
||||||
FROM cafebonus c
|
FROM cafebonus c
|
||||||
WHERE (
|
WHERE (
|
||||||
@@ -46,6 +49,7 @@ func (r *CafeRepository) GetClaimable(charID uint32, elapsedSec int64) (*sqlx.Ro
|
|||||||
FROM characters ch
|
FROM characters ch
|
||||||
WHERE ch.id = $1
|
WHERE ch.id = $1
|
||||||
) >= time_req`, charID, elapsedSec)
|
) >= time_req`, charID, elapsedSec)
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBonusItem returns the item type and quantity for a specific cafe bonus.
|
// GetBonusItem returns the item type and quantity for a specific cafe bonus.
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ func (r *DivaRepository) InsertEvent(startEpoch uint32) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEvents returns all diva events with their ID and start_time epoch.
|
// DivaEvent represents a diva event row with ID and start_time epoch.
|
||||||
func (r *DivaRepository) GetEvents() (*sqlx.Rows, error) {
|
type DivaEvent struct {
|
||||||
return r.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'")
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
// GetLoginBoosts returns all login boost rows for a character, ordered by week_req.
|
||||||
func (r *EventRepository) GetLoginBoosts(charID uint32) (*sqlx.Rows, error) {
|
func (r *EventRepository) GetLoginBoosts(charID uint32) ([]loginBoost, error) {
|
||||||
return r.db.Queryx("SELECT week_req, expiration, reset FROM login_boost WHERE char_id=$1 ORDER BY week_req", charID)
|
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.
|
// InsertLoginBoost creates a new login boost entry.
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package channelserver
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository interfaces decouple handlers from concrete PostgreSQL implementations,
|
// 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.
|
// RengokuRepo defines the contract for rengoku score/ranking data access.
|
||||||
type RengokuRepo interface {
|
type RengokuRepo interface {
|
||||||
UpsertScore(charID uint32, maxStagesMp, maxPointsMp, maxStagesSp, maxPointsSp uint32) error
|
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.
|
// MailRepo defines the contract for in-game mail data access.
|
||||||
@@ -270,7 +268,7 @@ type SessionRepo interface {
|
|||||||
type EventRepo interface {
|
type EventRepo interface {
|
||||||
GetFeatureWeapon(startTime time.Time) (activeFeature, error)
|
GetFeatureWeapon(startTime time.Time) (activeFeature, error)
|
||||||
InsertFeatureWeapon(startTime time.Time, features uint32) 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
|
InsertLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
|
||||||
UpdateLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
|
UpdateLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
|
||||||
GetEventQuests() (*sql.Rows, error)
|
GetEventQuests() (*sql.Rows, error)
|
||||||
@@ -287,17 +285,17 @@ type AchievementRepo interface {
|
|||||||
|
|
||||||
// ShopRepo defines the contract for shop data access.
|
// ShopRepo defines the contract for shop data access.
|
||||||
type ShopRepo interface {
|
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
|
RecordPurchase(charID, shopItemID, quantity uint32) error
|
||||||
GetFpointItem(tradeID uint32) (quantity, fpoints int, err 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.
|
// CafeRepo defines the contract for cafe bonus data access.
|
||||||
type CafeRepo interface {
|
type CafeRepo interface {
|
||||||
ResetAccepted(charID uint32) error
|
ResetAccepted(charID uint32) error
|
||||||
GetBonuses(charID uint32) (*sqlx.Rows, error)
|
GetBonuses(charID uint32) ([]CafeBonus, error)
|
||||||
GetClaimable(charID uint32, elapsedSec int64) (*sqlx.Rows, error)
|
GetClaimable(charID uint32, elapsedSec int64) ([]CafeBonus, error)
|
||||||
GetBonusItem(bonusID uint32) (itemType, quantity uint32, err error)
|
GetBonusItem(bonusID uint32) (itemType, quantity uint32, err error)
|
||||||
AcceptBonus(bonusID, charID uint32) error
|
AcceptBonus(bonusID, charID uint32) error
|
||||||
}
|
}
|
||||||
@@ -314,25 +312,25 @@ type GoocooRepo interface {
|
|||||||
type DivaRepo interface {
|
type DivaRepo interface {
|
||||||
DeleteEvents() error
|
DeleteEvents() error
|
||||||
InsertEvent(startEpoch uint32) error
|
InsertEvent(startEpoch uint32) error
|
||||||
GetEvents() (*sqlx.Rows, error)
|
GetEvents() ([]DivaEvent, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MiscRepo defines the contract for miscellaneous data access.
|
// MiscRepo defines the contract for miscellaneous data access.
|
||||||
type MiscRepo interface {
|
type MiscRepo interface {
|
||||||
GetTrendWeapons(weaponType uint8) (*sql.Rows, error)
|
GetTrendWeapons(weaponType uint8) ([]uint16, error)
|
||||||
UpsertTrendWeapon(weaponID uint16, weaponType uint8) error
|
UpsertTrendWeapon(weaponID uint16, weaponType uint8) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScenarioRepo defines the contract for scenario counter data access.
|
// ScenarioRepo defines the contract for scenario counter data access.
|
||||||
type ScenarioRepo interface {
|
type ScenarioRepo interface {
|
||||||
GetCounters() (*sqlx.Rows, error)
|
GetCounters() ([]Scenario, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MercenaryRepo defines the contract for mercenary/rasta data access.
|
// MercenaryRepo defines the contract for mercenary/rasta data access.
|
||||||
type MercenaryRepo interface {
|
type MercenaryRepo interface {
|
||||||
NextRastaID() (uint32, error)
|
NextRastaID() (uint32, error)
|
||||||
NextAirouID() (uint32, error)
|
NextAirouID() (uint32, error)
|
||||||
GetMercenaryLoans(charID uint32) (*sql.Rows, error)
|
GetMercenaryLoans(charID uint32) ([]MercenaryLoan, error)
|
||||||
GetGuildHuntCatsUsed(charID uint32) (*sql.Rows, error)
|
GetGuildHuntCatsUsed(charID uint32) ([]GuildHuntCatUsage, error)
|
||||||
GetGuildAirou(guildID uint32) (*sql.Rows, error)
|
GetGuildAirou(guildID uint32) ([][]byte, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
@@ -30,21 +31,73 @@ func (r *MercenaryRepository) NextAirouID() (uint32, error) {
|
|||||||
return id, err
|
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.
|
// GetMercenaryLoans returns characters that have a pact with the given character's rasta_id.
|
||||||
func (r *MercenaryRepository) GetMercenaryLoans(charID uint32) (*sql.Rows, error) {
|
func (r *MercenaryRepository) GetMercenaryLoans(charID uint32) ([]MercenaryLoan, error) {
|
||||||
return r.db.Query("SELECT name, id, pact_id FROM characters WHERE pact_id=(SELECT rasta_id FROM characters WHERE id=$1)", charID)
|
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.
|
// GetGuildHuntCatsUsed returns cats_used and start from guild_hunts for a given character.
|
||||||
func (r *MercenaryRepository) GetGuildHuntCatsUsed(charID uint32) (*sql.Rows, error) {
|
func (r *MercenaryRepository) GetGuildHuntCatsUsed(charID uint32) ([]GuildHuntCatUsage, error) {
|
||||||
return r.db.Query(`SELECT cats_used, start FROM guild_hunts gh
|
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)
|
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.
|
// GetGuildAirou returns otomoairou data for all characters in a guild.
|
||||||
func (r *MercenaryRepository) GetGuildAirou(guildID uint32) (*sql.Rows, error) {
|
func (r *MercenaryRepository) GetGuildAirou(guildID uint32) ([][]byte, error) {
|
||||||
return r.db.Query(`SELECT c.otomoairou FROM characters c
|
rows, err := r.db.Query(`SELECT c.otomoairou FROM characters c
|
||||||
INNER JOIN guild_characters gc ON gc.character_id = c.id
|
INNER JOIN guild_characters gc ON gc.character_id = c.id
|
||||||
WHERE gc.guild_id = $1 AND c.otomoairou IS NOT NULL
|
WHERE gc.guild_id = $1 AND c.otomoairou IS NOT NULL
|
||||||
ORDER BY c.id LIMIT 60`, guildID)
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"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.
|
// 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) {
|
func (r *MiscRepository) GetTrendWeapons(weaponType uint8) ([]uint16, error) {
|
||||||
return r.db.Query("SELECT weapon_id FROM trend_weapons WHERE weapon_type=$1 ORDER BY count DESC LIMIT 3", weaponType)
|
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.
|
// UpsertTrendWeapon increments the count for a weapon, inserting it if it doesn't exist.
|
||||||
|
|||||||
@@ -62,15 +62,19 @@ func rengokuIsGuildFiltered(leaderboard uint32) bool {
|
|||||||
|
|
||||||
// GetRanking returns rengoku scores for the given leaderboard.
|
// GetRanking returns rengoku scores for the given leaderboard.
|
||||||
// For guild-scoped leaderboards (2,3,6,7), guildID filters the results.
|
// 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)
|
col := rengokuColumnForLeaderboard(leaderboard)
|
||||||
|
var result []RengokuScore
|
||||||
|
var err error
|
||||||
if rengokuIsGuildFiltered(leaderboard) {
|
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),
|
fmt.Sprintf("SELECT %s AS score %s WHERE guild_id=$1 ORDER BY %s DESC", col, rengokuScoreQueryRepo, col),
|
||||||
guildID,
|
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(
|
return result, err
|
||||||
fmt.Sprintf("SELECT %s AS score %s ORDER BY %s DESC", col, rengokuScoreQueryRepo, col),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,6 +17,19 @@ func NewScenarioRepository(db *sqlx.DB) *ScenarioRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCounters returns all scenario counters.
|
// GetCounters returns all scenario counters.
|
||||||
func (r *ScenarioRepository) GetCounters() (*sqlx.Rows, error) {
|
func (r *ScenarioRepository) GetCounters() ([]Scenario, error) {
|
||||||
return r.db.Queryx("SELECT scenario_id, category_id FROM scenario_counter")
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ func NewShopRepository(db *sqlx.DB) *ShopRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetShopItems returns shop items with per-character purchase counts.
|
// GetShopItems returns shop items with per-character purchase counts.
|
||||||
func (r *ShopRepository) GetShopItems(shopType uint8, shopID uint32, charID uint32) (*sqlx.Rows, error) {
|
func (r *ShopRepository) GetShopItems(shopType uint8, shopID uint32, charID uint32) ([]ShopItem, error) {
|
||||||
return r.db.Queryx(`SELECT id, item_id, cost, quantity, min_hr, min_sr, min_gr, store_level, max_quantity,
|
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,
|
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
|
road_floors, road_fatalis FROM shop_items si WHERE shop_type=$1 AND shop_id=$2
|
||||||
`, shopType, shopID, charID)
|
`, shopType, shopID, charID)
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordPurchase upserts a purchase record, adding to the bought count.
|
// 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.
|
// GetFpointExchangeList returns all frontier point exchange items ordered by buyable status.
|
||||||
func (r *ShopRepository) GetFpointExchangeList() (*sqlx.Rows, error) {
|
func (r *ShopRepository) GetFpointExchangeList() ([]FPointExchange, error) {
|
||||||
return r.db.Queryx(`SELECT id, item_type, item_id, quantity, fpoints, buyable FROM fpoint_items ORDER BY buyable DESC`)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user