diff --git a/server/channelserver/handlers_character_test.go b/server/channelserver/handlers_character_test.go index 257004ce3..f04cb7d0c 100644 --- a/server/channelserver/handlers_character_test.go +++ b/server/channelserver/handlers_character_test.go @@ -413,7 +413,7 @@ func TestGetCharacterSaveData_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) s.server.erupeConfig.RealClientMode = _config.Z2 // Get character save data @@ -457,7 +457,7 @@ func TestCharacterSaveData_Save_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) s.server.erupeConfig.RealClientMode = _config.Z2 // Load character save data diff --git a/server/channelserver/handlers_clients_test.go b/server/channelserver/handlers_clients_test.go index e358066cc..85fd876d2 100644 --- a/server/channelserver/handlers_clients_test.go +++ b/server/channelserver/handlers_clients_test.go @@ -206,7 +206,7 @@ func TestHandleMsgMhfListMember_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) pkt := &mhfpacket.MsgMhfListMember{ AckHandle: 5678, @@ -313,7 +313,7 @@ func TestHandleMsgMhfOprMember_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) pkt := &mhfpacket.MsgMhfOprMember{ AckHandle: 9999, @@ -452,7 +452,7 @@ func TestListMember_EmptyDatabase_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) pkt := &mhfpacket.MsgMhfListMember{ AckHandle: 4444, @@ -528,7 +528,7 @@ func TestOprMember_EdgeCases_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) pkt := &mhfpacket.MsgMhfOprMember{ AckHandle: 7777, diff --git a/server/channelserver/handlers_data_test.go b/server/channelserver/handlers_data_test.go index 8dca38dd0..5286d2ca3 100644 --- a/server/channelserver/handlers_data_test.go +++ b/server/channelserver/handlers_data_test.go @@ -357,7 +357,7 @@ func TestHandleMsgMhfSavedata_Integration(t *testing.T) { s := createTestSession(mock) s.charID = charID s.Name = "TestChar" - s.server.db = db + SetTestDB(s.server, db) tests := []struct { name string @@ -442,7 +442,7 @@ func TestHandleMsgMhfLoaddata_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) s.server.userBinaryParts = make(map[userBinaryPartID][]byte) pkt := &mhfpacket.MsgMhfLoaddata{ @@ -475,7 +475,7 @@ func TestHandleMsgMhfSaveScenarioData_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) scenarioData := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A} @@ -530,7 +530,7 @@ func TestHandleMsgMhfLoadScenarioData_Integration(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) pkt := &mhfpacket.MsgMhfLoadScenarioData{ AckHandle: 1111, @@ -564,7 +564,7 @@ func TestSaveDataCorruptionDetection_Integration(t *testing.T) { s := createTestSession(mock) s.charID = charID s.Name = "OriginalName" - s.server.db = db + SetTestDB(s.server, db) s.server.erupeConfig.DeleteOnSaveCorruption = false // Create save data with a DIFFERENT name (corruption) @@ -615,7 +615,7 @@ func TestConcurrentSaveData_Integration(t *testing.T) { s := createTestSession(mock) s.charID = charIDs[index] s.Name = fmt.Sprintf("Char%d", index) - s.server.db = db + SetTestDB(s.server, db) saveData := make([]byte, 150000) copy(saveData[88:], []byte(fmt.Sprintf("Char%d\x00", index))) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 62a37ace2..93d45cfeb 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -86,20 +86,8 @@ func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) { } func cleanupFesta(s *Session) { - if _, err := s.server.db.Exec("DELETE FROM events WHERE event_type='festa'"); err != nil { - s.logger.Error("Failed to delete festa events", zap.Error(err)) - } - if _, err := s.server.db.Exec("DELETE FROM festa_registrations"); err != nil { - s.logger.Error("Failed to delete festa registrations", zap.Error(err)) - } - if _, err := s.server.db.Exec("DELETE FROM festa_submissions"); err != nil { - s.logger.Error("Failed to delete festa submissions", zap.Error(err)) - } - if _, err := s.server.db.Exec("DELETE FROM festa_prizes_accepted"); err != nil { - s.logger.Error("Failed to delete festa prizes accepted", zap.Error(err)) - } - if _, err := s.server.db.Exec("UPDATE guild_characters SET trial_vote=NULL"); err != nil { - s.logger.Error("Failed to reset festa trial votes", zap.Error(err)) + if err := s.server.festaRepo.CleanupAll(); err != nil { + s.logger.Error("Failed to cleanup festa", zap.Error(err)) } } @@ -141,7 +129,7 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { cleanupFesta(s) // Generate a new festa, starting midnight tomorrow start = uint32(midnight.Add(24 * time.Hour).Unix()) - if _, err := s.server.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('festa', to_timestamp($1)::timestamp without time zone)", start); err != nil { + if err := s.server.festaRepo.InsertEvent(start); err != nil { s.logger.Error("Failed to insert festa event", zap.Error(err)) } } @@ -183,13 +171,13 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { const festaIDSentinel = uint32(0xDEADBEEF) id, start := festaIDSentinel, uint32(0) - rows, err := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='festa'") + events, err := s.server.festaRepo.GetFestaEvents() if err != nil { s.logger.Error("Failed to query festa schedule", zap.Error(err)) } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - _ = rows.Scan(&id, &start) + for _, e := range events { + id = e.ID + start = e.StartTime } } @@ -209,11 +197,12 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { return } - var blueSouls, redSouls uint32 - if err := s.server.db.QueryRow(`SELECT COALESCE(SUM(fs.souls), 0) AS souls FROM festa_registrations fr LEFT JOIN festa_submissions fs ON fr.guild_id = fs.guild_id AND fr.team = 'blue'`).Scan(&blueSouls); err != nil { + blueSouls, err := s.server.festaRepo.GetTeamSouls("blue") + if err != nil { s.logger.Error("Failed to get blue souls", zap.Error(err)) } - if err := s.server.db.QueryRow(`SELECT COALESCE(SUM(fs.souls), 0) AS souls FROM festa_registrations fr LEFT JOIN festa_submissions fs ON fr.guild_id = fs.guild_id AND fr.team = 'red'`).Scan(&redSouls); err != nil { + redSouls, err := s.server.festaRepo.GetTeamSouls("red") + if err != nil { s.logger.Error("Failed to get red souls", zap.Error(err)) } @@ -228,31 +217,9 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(blueSouls) bf.WriteUint32(redSouls) - var trials []FestaTrial - var trial FestaTrial - rows, err = s.server.db.Queryx(`SELECT ft.*, - COALESCE(CASE - WHEN COUNT(gc.id) FILTER (WHERE fr.team = 'blue' AND gc.trial_vote = ft.id) > - COUNT(gc.id) FILTER (WHERE fr.team = 'red' AND gc.trial_vote = ft.id) - THEN CAST('blue' AS public.festival_color) - WHEN COUNT(gc.id) FILTER (WHERE fr.team = 'red' AND gc.trial_vote = ft.id) > - COUNT(gc.id) FILTER (WHERE fr.team = 'blue' AND gc.trial_vote = ft.id) - THEN CAST('red' AS public.festival_color) - END, CAST('none' AS public.festival_color)) AS monopoly - FROM public.festa_trials ft - LEFT JOIN public.guild_characters gc ON ft.id = gc.trial_vote - LEFT JOIN public.festa_registrations fr ON gc.guild_id = fr.guild_id - GROUP BY ft.id`) + trials, err := s.server.festaRepo.GetTrialsWithMonopoly() if err != nil { s.logger.Error("Failed to query festa trials", zap.Error(err)) - } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - if err := rows.StructScan(&trial); err != nil { - continue - } - trials = append(trials, trial) - } } bf.WriteUint16(uint16(len(trials))) for _, trial := range trials { @@ -323,49 +290,28 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { } bf.WriteUint16(100) // Reward multiplier (%) - var temp uint32 bf.WriteUint16(4) for i := uint16(0); i < 4; i++ { - var guildID uint32 - var guildName string - var guildTeam = FestivalColorNone - if err := s.server.db.QueryRow(` - SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ - FROM festa_submissions fs - LEFT JOIN festa_registrations fr ON fs.guild_id = fr.guild_id - LEFT JOIN guilds g ON fs.guild_id = g.id - WHERE fs.trial_type = $1 - GROUP BY fs.guild_id, g.name, fr.team - ORDER BY _ DESC LIMIT 1 - `, i+1).Scan(&guildID, &guildName, &guildTeam, &temp); err != nil && !errors.Is(err, sql.ErrNoRows) { + ranking, err := s.server.festaRepo.GetTopGuildForTrial(i + 1) + if err != nil && !errors.Is(err, sql.ErrNoRows) { s.logger.Error("Failed to get festa trial ranking", zap.Error(err)) } - bf.WriteUint32(guildID) + bf.WriteUint32(ranking.GuildID) bf.WriteUint16(i + 1) - bf.WriteInt16(FestivalColorCodes[guildTeam]) - ps.Uint8(bf, guildName, true) + bf.WriteInt16(FestivalColorCodes[ranking.Team]) + ps.Uint8(bf, ranking.GuildName, true) } bf.WriteUint16(7) for i := uint16(0); i < 7; i++ { - var guildID uint32 - var guildName string - var guildTeam = FestivalColorNone offset := secsPerDay * uint32(i) - if err := s.server.db.QueryRow(` - SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ - FROM festa_submissions fs - LEFT JOIN festa_registrations fr ON fs.guild_id = fr.guild_id - LEFT JOIN guilds g ON fs.guild_id = g.id - WHERE EXTRACT(EPOCH FROM fs.timestamp)::int > $1 AND EXTRACT(EPOCH FROM fs.timestamp)::int < $2 - GROUP BY fs.guild_id, g.name, fr.team - ORDER BY _ DESC LIMIT 1 - `, timestamps[1]+offset, timestamps[1]+offset+secsPerDay).Scan(&guildID, &guildName, &guildTeam, &temp); err != nil && !errors.Is(err, sql.ErrNoRows) { + ranking, err := s.server.festaRepo.GetTopGuildInWindow(timestamps[1]+offset, timestamps[1]+offset+secsPerDay) + if err != nil && !errors.Is(err, sql.ErrNoRows) { s.logger.Error("Failed to get festa daily ranking", zap.Error(err)) } - bf.WriteUint32(guildID) + bf.WriteUint32(ranking.GuildID) bf.WriteUint16(i + 1) - bf.WriteInt16(FestivalColorCodes[guildTeam]) - ps.Uint8(bf, guildName, true) + bf.WriteInt16(FestivalColorCodes[ranking.Team]) + ps.Uint8(bf, ranking.GuildName, true) } bf.WriteUint32(0) // Clan goal @@ -398,14 +344,14 @@ func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } - var souls, exists uint32 - if err := s.server.db.QueryRow(`SELECT COALESCE((SELECT SUM(souls) FROM festa_submissions WHERE character_id=$1), 0)`, s.charID).Scan(&souls); err != nil { + souls, err := s.server.festaRepo.GetCharSouls(s.charID) + if err != nil { s.logger.Error("Failed to get festa user souls", zap.Error(err)) } - err = s.server.db.QueryRow("SELECT prize_id FROM festa_prizes_accepted WHERE prize_id=0 AND character_id=$1", s.charID).Scan(&exists) + claimed := s.server.festaRepo.HasClaimedMainPrize(s.charID) bf := byteframe.NewByteFrame() bf.WriteUint32(souls) - if err != nil { + if !claimed { bf.WriteBool(true) bf.WriteBool(false) } else { @@ -479,7 +425,7 @@ func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfVoteFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfVoteFesta) - if _, err := s.server.db.Exec(`UPDATE guild_characters SET trial_vote=$1 WHERE character_id=$2`, pkt.TrialID, s.charID); err != nil { + if err := s.server.festaRepo.VoteTrial(s.charID, pkt.TrialID); err != nil { s.logger.Error("Failed to update festa trial vote", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -493,15 +439,12 @@ func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) { return } team := uint32(token.RNG.Intn(2)) - switch team { - case 0: - if _, err := s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'blue')", guild.ID); err != nil { - s.logger.Error("Failed to register guild for festa blue team", zap.Error(err)) - } - case 1: - if _, err := s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'red')", guild.ID); err != nil { - s.logger.Error("Failed to register guild for festa red team", zap.Error(err)) - } + teamName := "blue" + if team == 1 { + teamName = "red" + } + if err := s.server.festaRepo.RegisterGuild(guild.ID, teamName); err != nil { + s.logger.Error("Failed to register guild for festa", zap.Error(err)) } bf := byteframe.NewByteFrame() bf.WriteUint32(team) @@ -510,28 +453,15 @@ func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfChargeFesta) - tx, err := s.server.db.Begin() - if err != nil { - s.logger.Error("Failed to begin festa submission transaction", zap.Error(err)) - } else { - for i := range pkt.Souls { - if pkt.Souls[i] == 0 { - continue - } - if _, err := tx.Exec(`INSERT INTO festa_submissions VALUES ($1, $2, $3, $4, now())`, s.charID, pkt.GuildID, i, pkt.Souls[i]); err != nil { - s.logger.Error("Failed to insert festa submission", zap.Error(err)) - } - } - if err := tx.Commit(); err != nil { - s.logger.Error("Failed to commit festa submissions", zap.Error(err)) - } + if err := s.server.festaRepo.SubmitSouls(s.charID, pkt.GuildID, pkt.Souls); err != nil { + s.logger.Error("Failed to submit festa souls", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFesta) - if _, err := s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES (0, $1)", s.charID); err != nil { + if err := s.server.festaRepo.ClaimPrize(0, s.charID); err != nil { s.logger.Error("Failed to accept festa prize", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -539,7 +469,7 @@ func handleMsgMhfAcquireFesta(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAcquireFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFestaPersonalPrize) - if _, err := s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", pkt.PrizeID, s.charID); err != nil { + if err := s.server.festaRepo.ClaimPrize(pkt.PrizeID, s.charID); err != nil { s.logger.Error("Failed to accept festa personal prize", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -547,7 +477,7 @@ func handleMsgMhfAcquireFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAcquireFestaIntermediatePrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFestaIntermediatePrize) - if _, err := s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", pkt.PrizeID, s.charID); err != nil { + if err := s.server.festaRepo.ClaimPrize(pkt.PrizeID, s.charID); err != nil { s.logger.Error("Failed to accept festa intermediate prize", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -563,20 +493,14 @@ type Prize struct { Claimed int `db:"claimed"` } -func handleMsgMhfEnumerateFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEnumerateFestaPersonalPrize) - rows, err := s.server.db.Queryx(`SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = $1) AS claimed FROM festa_prizes fp WHERE type='personal'`, s.charID) +func writePrizeList(s *Session, pkt mhfpacket.MHFPacket, ackHandle uint32, prizeType string) { + prizes, err := s.server.festaRepo.ListPrizes(s.charID, prizeType) var count uint32 prizeData := byteframe.NewByteFrame() if err != nil { - s.logger.Error("Failed to query festa personal prizes", zap.Error(err)) + s.logger.Error("Failed to query festa prizes", zap.Error(err), zap.String("type", prizeType)) } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - prize := &Prize{} - if err := rows.StructScan(&prize); err != nil { - continue - } + for _, prize := range prizes { count++ prizeData.WriteUint32(prize.ID) prizeData.WriteUint32(prize.Tier) @@ -590,35 +514,15 @@ func handleMsgMhfEnumerateFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) bf := byteframe.NewByteFrame() bf.WriteUint32(count) bf.WriteBytes(prizeData.Data()) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + doAckBufSucceed(s, ackHandle, bf.Data()) +} + +func handleMsgMhfEnumerateFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumerateFestaPersonalPrize) + writePrizeList(s, p, pkt.AckHandle, "personal") } func handleMsgMhfEnumerateFestaIntermediatePrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaIntermediatePrize) - rows, err := s.server.db.Queryx(`SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = $1) AS claimed FROM festa_prizes fp WHERE type='guild'`, s.charID) - var count uint32 - prizeData := byteframe.NewByteFrame() - if err != nil { - s.logger.Error("Failed to query festa intermediate prizes", zap.Error(err)) - } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - prize := &Prize{} - if err := rows.StructScan(&prize); err != nil { - continue - } - count++ - prizeData.WriteUint32(prize.ID) - prizeData.WriteUint32(prize.Tier) - prizeData.WriteUint32(prize.SoulsReq) - prizeData.WriteUint32(7) // Unk - prizeData.WriteUint32(prize.ItemID) - prizeData.WriteUint32(prize.NumItem) - prizeData.WriteBool(prize.Claimed > 0) - } - } - bf := byteframe.NewByteFrame() - bf.WriteUint32(count) - bf.WriteBytes(prizeData.Data()) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + writePrizeList(s, p, pkt.AckHandle, "guild") } diff --git a/server/channelserver/handlers_savedata_integration_test.go b/server/channelserver/handlers_savedata_integration_test.go index ce0a92e69..d9589d33c 100644 --- a/server/channelserver/handlers_savedata_integration_test.go +++ b/server/channelserver/handlers_savedata_integration_test.go @@ -71,7 +71,7 @@ func TestSaveLoad_HunterNavi(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) // Create Hunter Navi data naviData := make([]byte, 552) // G8+ size @@ -117,7 +117,7 @@ func TestSaveLoad_MonsterKillCounter(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) // Initial Koryo points initialPoints := uint32(0) @@ -255,7 +255,7 @@ func TestSaveLoad_CurrentEquipment(t *testing.T) { s := createTestSession(mock) s.charID = charID s.Name = "TestChar" - s.server.db = db + SetTestDB(s.server, db) // Create savedata with equipped gear // Equipment data is embedded in the main savedata blob @@ -369,7 +369,7 @@ func TestSaveLoad_Transmog(t *testing.T) { mock := &MockCryptConn{sentPackets: make([][]byte, 0)} s := createTestSession(mock) s.charID = charID - s.server.db = db + SetTestDB(s.server, db) // Create valid transmog/decoration set data // Format: [version byte][count byte][count * (uint16 index + setSize bytes)] @@ -466,7 +466,7 @@ func TestSaveLoad_CompleteSaveLoadCycle(t *testing.T) { s := createTestSession(mock) s.charID = charID s.Name = "SaveLoadTest" - s.server.db = db + SetTestDB(s.server, db) // 1. Set Road Points rdpPoints := uint32(5000) diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index ca0e675b6..ace1f29b5 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -2,7 +2,6 @@ package channelserver import ( _config "erupe-ce/config" - "fmt" "math" "strings" "time" @@ -66,20 +65,22 @@ func handleMsgMhfGetTowerInfo(s *Session, p mhfpacket.MHFPacket) { Level: []TowerInfoLevel{{0, 0, 0, 0}, {0, 0, 0, 0}}, } - var tempSkills string - err := s.server.db.QueryRow(`SELECT COALESCE(tr, 0), COALESCE(trp, 0), COALESCE(tsp, 0), COALESCE(block1, 0), COALESCE(block2, 0), COALESCE(skills, $1) FROM tower WHERE char_id=$2 - `, EmptyTowerCSV(64), s.charID).Scan(&towerInfo.TRP[0].TR, &towerInfo.TRP[0].TRP, &towerInfo.Skill[0].TSP, &towerInfo.Level[0].Floors, &towerInfo.Level[1].Floors, &tempSkills) + td, err := s.server.towerRepo.GetTowerData(s.charID) if err != nil { - if _, err := s.server.db.Exec(`INSERT INTO tower (char_id) VALUES ($1)`, s.charID); err != nil { - s.logger.Error("Failed to initialize tower data", zap.Error(err)) - } + s.logger.Error("Failed to initialize tower data", zap.Error(err)) + } else { + towerInfo.TRP[0].TR = td.TR + towerInfo.TRP[0].TRP = td.TRP + towerInfo.Skill[0].TSP = td.TSP + towerInfo.Level[0].Floors = td.Block1 + towerInfo.Level[1].Floors = td.Block2 } if s.server.erupeConfig.RealClientMode <= _config.G7 { towerInfo.Level = towerInfo.Level[:1] } - for i, skill := range stringsupport.CSVElems(tempSkills) { + for i, skill := range stringsupport.CSVElems(td.Skills) { if skill < math.MinInt16 || skill > math.MaxInt16 { continue } @@ -148,14 +149,14 @@ func handleMsgMhfPostTowerInfo(s *Session, p mhfpacket.MHFPacket) { switch pkt.InfoType { case 2: - var skills string - _ = s.server.db.QueryRow(`SELECT COALESCE(skills, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(64), s.charID).Scan(&skills) - if _, err := s.server.db.Exec(`UPDATE tower SET skills=$1, tsp=tsp-$2 WHERE char_id=$3`, stringsupport.CSVSetIndex(skills, int(pkt.Skill), stringsupport.CSVGetIndex(skills, int(pkt.Skill))+1), pkt.Cost, s.charID); err != nil { + skills, _ := s.server.towerRepo.GetSkills(s.charID) + newSkills := stringsupport.CSVSetIndex(skills, int(pkt.Skill), stringsupport.CSVGetIndex(skills, int(pkt.Skill))+1) + if err := s.server.towerRepo.UpdateSkills(s.charID, newSkills, pkt.Cost); err != nil { s.logger.Error("Failed to update tower skills", zap.Error(err)) } case 1, 7: // This might give too much TSP? No idea what the rate is supposed to be - if _, err := s.server.db.Exec(`UPDATE tower SET tr=$1, trp=COALESCE(trp, 0)+$2, tsp=COALESCE(tsp, 0)+$3, block1=COALESCE(block1, 0)+$4 WHERE char_id=$5`, pkt.TR, pkt.TRP, pkt.Cost, pkt.Block1, s.charID); err != nil { + if err := s.server.towerRepo.UpdateProgress(s.charID, pkt.TR, pkt.TRP, pkt.Cost, pkt.Block1); err != nil { s.logger.Error("Failed to update tower progress", zap.Error(err)) } } @@ -306,11 +307,15 @@ func handleMsgMhfGetTenrouirai(s *Session, p mhfpacket.MHFPacket) { data = append(data, bf) } case 4: - if err := s.server.db.QueryRow(`SELECT tower_mission_page FROM guilds WHERE id=$1`, pkt.GuildID).Scan(&tenrouirai.Progress[0].Page); err != nil { + progress, err := s.server.towerRepo.GetTenrouiraiProgress(pkt.GuildID) + if err != nil { s.logger.Error("Failed to read tower mission page", zap.Error(err)) + } else { + tenrouirai.Progress[0].Page = progress.Page + tenrouirai.Progress[0].Mission1 = progress.Mission1 + tenrouirai.Progress[0].Mission2 = progress.Mission2 + tenrouirai.Progress[0].Mission3 = progress.Mission3 } - _ = s.server.db.QueryRow(`SELECT SUM(tower_mission_1) AS _, SUM(tower_mission_2) AS _, SUM(tower_mission_3) AS _ FROM guild_characters WHERE guild_id=$1 - `, pkt.GuildID).Scan(&tenrouirai.Progress[0].Mission1, &tenrouirai.Progress[0].Mission2, &tenrouirai.Progress[0].Mission3) if tenrouirai.Progress[0].Page < 1 { tenrouirai.Progress[0].Page = 1 @@ -334,28 +339,19 @@ func handleMsgMhfGetTenrouirai(s *Session, p mhfpacket.MHFPacket) { data = append(data, bf) } case 5: - if pkt.MissionIndex < 1 || pkt.MissionIndex > 3 { - pkt.MissionIndex = (pkt.MissionIndex % 3) + 1 - } - rows, err := s.server.db.Query(fmt.Sprintf(`SELECT name, tower_mission_%d FROM guild_characters gc INNER JOIN characters c ON gc.character_id = c.id WHERE guild_id=$1 AND tower_mission_%d IS NOT NULL ORDER BY tower_mission_%d DESC`, pkt.MissionIndex, pkt.MissionIndex, pkt.MissionIndex), pkt.GuildID) + scores, err := s.server.towerRepo.GetTenrouiraiMissionScores(pkt.GuildID, pkt.MissionIndex) if err != nil { s.logger.Error("Failed to query tower mission scores", zap.Error(err)) - } else { - defer func() { _ = rows.Close() }() - for rows.Next() { - temp := TenrouiraiCharScore{} - _ = rows.Scan(&temp.Name, &temp.Score) - tenrouirai.CharScore = append(tenrouirai.CharScore, temp) - } } - for _, charScore := range tenrouirai.CharScore { + for _, charScore := range scores { bf := byteframe.NewByteFrame() bf.WriteInt32(charScore.Score) bf.WriteBytes(stringsupport.PaddedString(charScore.Name, 14, true)) data = append(data, bf) } case 6: - _ = s.server.db.QueryRow(`SELECT tower_rp FROM guilds WHERE id=$1`, pkt.GuildID).Scan(&tenrouirai.Ticket[0].RP) + rp, _ := s.server.towerRepo.GetGuildTowerRP(pkt.GuildID) + tenrouirai.Ticket[0].RP = rp for _, ticket := range tenrouirai.Ticket { bf := byteframe.NewByteFrame() bf.WriteUint8(ticket.Unk0) @@ -388,13 +384,14 @@ func handleMsgMhfPostTenrouirai(s *Session, p mhfpacket.MHFPacket) { } if pkt.Op == 2 { - var page, requirement, donated int - if err := s.server.db.QueryRow(`SELECT tower_mission_page, tower_rp FROM guilds WHERE id=$1`, pkt.GuildID).Scan(&page, &donated); err != nil { + page, donated, err := s.server.towerRepo.GetGuildTowerPageAndRP(pkt.GuildID) + if err != nil { s.logger.Error("Failed to read guild tower state for donation", zap.Error(err)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } + var requirement int for i := 0; i < (page*3)+1; i++ { requirement += int(tenrouiraiData[i].Cost) } @@ -406,16 +403,13 @@ func handleMsgMhfPostTenrouirai(s *Session, p mhfpacket.MHFPacket) { sd.RP -= pkt.DonatedRP sd.Save(s) if donated+int(pkt.DonatedRP) >= requirement { - if _, err := s.server.db.Exec(`UPDATE guilds SET tower_mission_page=tower_mission_page+1 WHERE id=$1`, pkt.GuildID); err != nil { + if err := s.server.towerRepo.AdvanceTenrouiraiPage(pkt.GuildID); err != nil { s.logger.Error("Failed to advance tower mission page", zap.Error(err)) } - if _, err := s.server.db.Exec(`UPDATE guild_characters SET tower_mission_1=NULL, tower_mission_2=NULL, tower_mission_3=NULL WHERE guild_id=$1`, pkt.GuildID); err != nil { - s.logger.Error("Failed to reset tower mission progress", zap.Error(err)) - } pkt.DonatedRP = uint16(requirement - donated) } bf.WriteUint32(uint32(pkt.DonatedRP)) - if _, err := s.server.db.Exec(`UPDATE guilds SET tower_rp=tower_rp+$1 WHERE id=$2`, pkt.DonatedRP, pkt.GuildID); err != nil { + if err := s.server.towerRepo.DonateGuildTowerRP(pkt.GuildID, pkt.DonatedRP); err != nil { s.logger.Error("Failed to update guild tower RP", zap.Error(err)) } } else { @@ -467,8 +461,7 @@ func handleMsgMhfGetGemInfo(s *Session, p mhfpacket.MHFPacket) { gemInfo := []GemInfo{} gemHistory := []GemHistory{} - var tempGems string - _ = s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), s.charID).Scan(&tempGems) + tempGems, _ := s.server.towerRepo.GetGems(s.charID) for i, v := range stringsupport.CSVElems(tempGems) { if v < 0 || v > math.MaxUint16 { continue @@ -513,12 +506,10 @@ func handleMsgMhfPostGemInfo(s *Session, p mhfpacket.MHFPacket) { ) } - var gems string - _ = s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), s.charID).Scan(&gems) switch pkt.Op { case 1: // Add gem i := int((pkt.Gem >> 8 * 5) + (pkt.Gem - pkt.Gem&0xFF00 - 1%5)) - if _, err := s.server.db.Exec(`UPDATE tower SET gems=$1 WHERE char_id=$2`, stringsupport.CSVSetIndex(gems, i, stringsupport.CSVGetIndex(gems, i)+int(pkt.Quantity)), s.charID); err != nil { + if err := s.server.towerRepo.AddGem(s.charID, i, int(pkt.Quantity)); err != nil { s.logger.Error("Failed to update tower gems", zap.Error(err)) } case 2: // Transfer gem diff --git a/server/channelserver/repo_festa.go b/server/channelserver/repo_festa.go new file mode 100644 index 000000000..7458bc290 --- /dev/null +++ b/server/channelserver/repo_festa.go @@ -0,0 +1,228 @@ +package channelserver + +import ( + "database/sql" + + "github.com/jmoiron/sqlx" +) + +// FestaRepository centralizes all database access for festa-related tables +// (events, festa_registrations, festa_submissions, festa_prizes, festa_prizes_accepted, festa_trials, guild_characters). +type FestaRepository struct { + db *sqlx.DB +} + +// NewFestaRepository creates a new FestaRepository. +func NewFestaRepository(db *sqlx.DB) *FestaRepository { + return &FestaRepository{db: db} +} + +// FestaEvent represents a festa event row. +type FestaEvent struct { + ID uint32 `db:"id"` + StartTime uint32 `db:"start_time"` +} + +// FestaGuildRanking holds a guild's ranking result for a trial or daily window. +type FestaGuildRanking struct { + GuildID uint32 + GuildName string + Team FestivalColor + Souls uint32 +} + +// CleanupAll removes all festa state: events, registrations, submissions, accepted prizes, and trial votes. +func (r *FestaRepository) CleanupAll() error { + for _, q := range []string{ + "DELETE FROM events WHERE event_type='festa'", + "DELETE FROM festa_registrations", + "DELETE FROM festa_submissions", + "DELETE FROM festa_prizes_accepted", + "UPDATE guild_characters SET trial_vote=NULL", + } { + if _, err := r.db.Exec(q); err != nil { + return err + } + } + return nil +} + +// InsertEvent creates a new festa event with the given start time. +func (r *FestaRepository) InsertEvent(startTime uint32) error { + _, err := r.db.Exec( + "INSERT INTO events (event_type, start_time) VALUES ('festa', to_timestamp($1)::timestamp without time zone)", + startTime, + ) + return err +} + +// GetFestaEvents returns all festa events (id and start_time as epoch). +func (r *FestaRepository) GetFestaEvents() ([]FestaEvent, error) { + var events []FestaEvent + rows, err := r.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='festa'") + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var e FestaEvent + if err := rows.StructScan(&e); err != nil { + continue + } + events = append(events, e) + } + return events, nil +} + +// GetTeamSouls returns the total souls for a given team color ("blue" or "red"). +func (r *FestaRepository) GetTeamSouls(team string) (uint32, error) { + var souls uint32 + err := r.db.QueryRow( + `SELECT COALESCE(SUM(fs.souls), 0) AS souls FROM festa_registrations fr LEFT JOIN festa_submissions fs ON fr.guild_id = fs.guild_id AND fr.team = $1`, + team, + ).Scan(&souls) + return souls, err +} + +// GetTrialsWithMonopoly returns all festa trials with their computed monopoly color. +func (r *FestaRepository) GetTrialsWithMonopoly() ([]FestaTrial, error) { + var trials []FestaTrial + rows, err := r.db.Queryx(`SELECT ft.*, + COALESCE(CASE + WHEN COUNT(gc.id) FILTER (WHERE fr.team = 'blue' AND gc.trial_vote = ft.id) > + COUNT(gc.id) FILTER (WHERE fr.team = 'red' AND gc.trial_vote = ft.id) + THEN CAST('blue' AS public.festival_color) + WHEN COUNT(gc.id) FILTER (WHERE fr.team = 'red' AND gc.trial_vote = ft.id) > + COUNT(gc.id) FILTER (WHERE fr.team = 'blue' AND gc.trial_vote = ft.id) + THEN CAST('red' AS public.festival_color) + END, CAST('none' AS public.festival_color)) AS monopoly + FROM public.festa_trials ft + LEFT JOIN public.guild_characters gc ON ft.id = gc.trial_vote + LEFT JOIN public.festa_registrations fr ON gc.guild_id = fr.guild_id + GROUP BY ft.id`) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var trial FestaTrial + if err := rows.StructScan(&trial); err != nil { + continue + } + trials = append(trials, trial) + } + return trials, nil +} + +// GetTopGuildForTrial returns the top-scoring guild for a given trial type. +// Returns sql.ErrNoRows if no submissions exist. +func (r *FestaRepository) GetTopGuildForTrial(trialType uint16) (FestaGuildRanking, error) { + var ranking FestaGuildRanking + var temp uint32 + ranking.Team = FestivalColorNone + err := r.db.QueryRow(` + SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ + FROM festa_submissions fs + LEFT JOIN festa_registrations fr ON fs.guild_id = fr.guild_id + LEFT JOIN guilds g ON fs.guild_id = g.id + WHERE fs.trial_type = $1 + GROUP BY fs.guild_id, g.name, fr.team + ORDER BY _ DESC LIMIT 1 + `, trialType).Scan(&ranking.GuildID, &ranking.GuildName, &ranking.Team, &temp) + return ranking, err +} + +// GetTopGuildInWindow returns the top-scoring guild within a time window (epoch seconds). +// Returns sql.ErrNoRows if no submissions exist. +func (r *FestaRepository) GetTopGuildInWindow(start, end uint32) (FestaGuildRanking, error) { + var ranking FestaGuildRanking + var temp uint32 + ranking.Team = FestivalColorNone + err := r.db.QueryRow(` + SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ + FROM festa_submissions fs + LEFT JOIN festa_registrations fr ON fs.guild_id = fr.guild_id + LEFT JOIN guilds g ON fs.guild_id = g.id + WHERE EXTRACT(EPOCH FROM fs.timestamp)::int > $1 AND EXTRACT(EPOCH FROM fs.timestamp)::int < $2 + GROUP BY fs.guild_id, g.name, fr.team + ORDER BY _ DESC LIMIT 1 + `, start, end).Scan(&ranking.GuildID, &ranking.GuildName, &ranking.Team, &temp) + return ranking, err +} + +// GetCharSouls returns the total souls submitted by a character. +func (r *FestaRepository) GetCharSouls(charID uint32) (uint32, error) { + var souls uint32 + err := r.db.QueryRow( + `SELECT COALESCE((SELECT SUM(souls) FROM festa_submissions WHERE character_id=$1), 0)`, + charID, + ).Scan(&souls) + return souls, err +} + +// HasClaimedMainPrize checks if a character has claimed the main festa prize (prize_id=0). +func (r *FestaRepository) HasClaimedMainPrize(charID uint32) bool { + var exists uint32 + err := r.db.QueryRow("SELECT prize_id FROM festa_prizes_accepted WHERE prize_id=0 AND character_id=$1", charID).Scan(&exists) + return err == nil +} + +// VoteTrial sets a character's trial vote. +func (r *FestaRepository) VoteTrial(charID uint32, trialID uint32) error { + _, err := r.db.Exec(`UPDATE guild_characters SET trial_vote=$1 WHERE character_id=$2`, trialID, charID) + return err +} + +// RegisterGuild registers a guild for a festa team. +func (r *FestaRepository) RegisterGuild(guildID uint32, team string) error { + _, err := r.db.Exec("INSERT INTO festa_registrations VALUES ($1, $2)", guildID, team) + return err +} + +// SubmitSouls records soul submissions for a character within a transaction. +func (r *FestaRepository) SubmitSouls(charID, guildID uint32, souls []uint16) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + for i, s := range souls { + if s == 0 { + continue + } + if _, err := tx.Exec(`INSERT INTO festa_submissions VALUES ($1, $2, $3, $4, now())`, charID, guildID, i, s); err != nil { + _ = tx.Rollback() + return err + } + } + return tx.Commit() +} + +// ClaimPrize records that a character has claimed a festa prize. +func (r *FestaRepository) ClaimPrize(prizeID uint32, charID uint32) error { + _, err := r.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", prizeID, charID) + return err +} + +// ListPrizes returns festa prizes of the given type with a claimed flag for the character. +func (r *FestaRepository) ListPrizes(charID uint32, prizeType string) ([]Prize, error) { + var prizes []Prize + rows, err := r.db.Queryx( + `SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = $1) AS claimed FROM festa_prizes fp WHERE type=$2`, + charID, prizeType, + ) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var prize Prize + if err := rows.StructScan(&prize); err != nil { + continue + } + prizes = append(prizes, prize) + } + return prizes, nil +} + +// ensure sql import is used +var _ = sql.ErrNoRows diff --git a/server/channelserver/repo_tower.go b/server/channelserver/repo_tower.go new file mode 100644 index 000000000..2f5d0d2d9 --- /dev/null +++ b/server/channelserver/repo_tower.go @@ -0,0 +1,164 @@ +package channelserver + +import ( + "fmt" + + "erupe-ce/common/stringsupport" + + "github.com/jmoiron/sqlx" +) + +// TowerRepository centralizes all database access for tower-related tables +// (tower, guilds tower columns, guild_characters tower columns). +type TowerRepository struct { + db *sqlx.DB +} + +// NewTowerRepository creates a new TowerRepository. +func NewTowerRepository(db *sqlx.DB) *TowerRepository { + return &TowerRepository{db: db} +} + +// TowerData holds the core tower stats for a character. +type TowerData struct { + TR int32 + TRP int32 + TSP int32 + Block1 int32 + Block2 int32 + Skills string +} + +// GetTowerData returns tower stats for a character, creating the row if it doesn't exist. +func (r *TowerRepository) GetTowerData(charID uint32) (TowerData, error) { + var td TowerData + err := r.db.QueryRow( + `SELECT COALESCE(tr, 0), COALESCE(trp, 0), COALESCE(tsp, 0), COALESCE(block1, 0), COALESCE(block2, 0), COALESCE(skills, $1) FROM tower WHERE char_id=$2`, + EmptyTowerCSV(64), charID, + ).Scan(&td.TR, &td.TRP, &td.TSP, &td.Block1, &td.Block2, &td.Skills) + if err != nil { + _, err = r.db.Exec(`INSERT INTO tower (char_id) VALUES ($1)`, charID) + return TowerData{Skills: EmptyTowerCSV(64)}, err + } + return td, nil +} + +// GetSkills returns the skills CSV string for a character. +func (r *TowerRepository) GetSkills(charID uint32) (string, error) { + var skills string + err := r.db.QueryRow(`SELECT COALESCE(skills, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(64), charID).Scan(&skills) + return skills, err +} + +// UpdateSkills updates a single skill and deducts TSP cost. +func (r *TowerRepository) UpdateSkills(charID uint32, skills string, cost int32) error { + _, err := r.db.Exec(`UPDATE tower SET skills=$1, tsp=tsp-$2 WHERE char_id=$3`, skills, cost, charID) + return err +} + +// UpdateProgress updates tower progress (TR, TRP, TSP, block1). +func (r *TowerRepository) UpdateProgress(charID uint32, tr, trp, cost, block1 int32) error { + _, err := r.db.Exec( + `UPDATE tower SET tr=$1, trp=COALESCE(trp, 0)+$2, tsp=COALESCE(tsp, 0)+$3, block1=COALESCE(block1, 0)+$4 WHERE char_id=$5`, + tr, trp, cost, block1, charID, + ) + return err +} + +// GetGems returns the gems CSV string for a character. +func (r *TowerRepository) GetGems(charID uint32) (string, error) { + var gems string + err := r.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), charID).Scan(&gems) + return gems, err +} + +// UpdateGems saves the gems CSV string for a character. +func (r *TowerRepository) UpdateGems(charID uint32, gems string) error { + _, err := r.db.Exec(`UPDATE tower SET gems=$1 WHERE char_id=$2`, gems, charID) + return err +} + +// AddGem adds quantity to a specific gem index. +func (r *TowerRepository) AddGem(charID uint32, gemIndex int, quantity int) error { + gems, err := r.GetGems(charID) + if err != nil { + return err + } + newGems := stringsupport.CSVSetIndex(gems, gemIndex, stringsupport.CSVGetIndex(gems, gemIndex)+quantity) + return r.UpdateGems(charID, newGems) +} + +// TenrouiraiProgressData holds the guild's tenrouirai (sky corridor) progress. +type TenrouiraiProgressData struct { + Page uint8 + Mission1 uint16 + Mission2 uint16 + Mission3 uint16 +} + +// GetTenrouiraiProgress returns the guild's tower mission page and aggregated mission scores. +func (r *TowerRepository) GetTenrouiraiProgress(guildID uint32) (TenrouiraiProgressData, error) { + var p TenrouiraiProgressData + if err := r.db.QueryRow(`SELECT tower_mission_page FROM guilds WHERE id=$1`, guildID).Scan(&p.Page); err != nil { + return p, err + } + _ = r.db.QueryRow( + `SELECT SUM(tower_mission_1) AS _, SUM(tower_mission_2) AS _, SUM(tower_mission_3) AS _ FROM guild_characters WHERE guild_id=$1`, + guildID, + ).Scan(&p.Mission1, &p.Mission2, &p.Mission3) + return p, nil +} + +// GetTenrouiraiMissionScores returns per-character scores for a specific mission index (1-3). +func (r *TowerRepository) GetTenrouiraiMissionScores(guildID uint32, missionIndex uint8) ([]TenrouiraiCharScore, error) { + if missionIndex < 1 || missionIndex > 3 { + missionIndex = (missionIndex % 3) + 1 + } + rows, err := r.db.Query( + fmt.Sprintf( + `SELECT name, tower_mission_%d FROM guild_characters gc INNER JOIN characters c ON gc.character_id = c.id WHERE guild_id=$1 AND tower_mission_%d IS NOT NULL ORDER BY tower_mission_%d DESC`, + missionIndex, missionIndex, missionIndex, + ), + guildID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var scores []TenrouiraiCharScore + for rows.Next() { + var cs TenrouiraiCharScore + if err := rows.Scan(&cs.Name, &cs.Score); err == nil { + scores = append(scores, cs) + } + } + return scores, nil +} + +// GetGuildTowerRP returns the guild's tower RP. +func (r *TowerRepository) GetGuildTowerRP(guildID uint32) (uint32, error) { + var rp uint32 + err := r.db.QueryRow(`SELECT tower_rp FROM guilds WHERE id=$1`, guildID).Scan(&rp) + return rp, err +} + +// GetGuildTowerPageAndRP returns the guild's tower mission page and donated RP. +func (r *TowerRepository) GetGuildTowerPageAndRP(guildID uint32) (page int, donated int, err error) { + err = r.db.QueryRow(`SELECT tower_mission_page, tower_rp FROM guilds WHERE id=$1`, guildID).Scan(&page, &donated) + return +} + +// AdvanceTenrouiraiPage increments the guild's tower mission page and resets member mission progress. +func (r *TowerRepository) AdvanceTenrouiraiPage(guildID uint32) error { + if _, err := r.db.Exec(`UPDATE guilds SET tower_mission_page=tower_mission_page+1 WHERE id=$1`, guildID); err != nil { + return err + } + _, err := r.db.Exec(`UPDATE guild_characters SET tower_mission_1=NULL, tower_mission_2=NULL, tower_mission_3=NULL WHERE guild_id=$1`, guildID) + return err +} + +// DonateGuildTowerRP adds RP to the guild's tower total. +func (r *TowerRepository) DonateGuildTowerRP(guildID uint32, rp uint16) error { + _, err := r.db.Exec(`UPDATE guilds SET tower_rp=tower_rp+$1 WHERE id=$2`, rp, guildID) + return err +} diff --git a/server/channelserver/session_lifecycle_integration_test.go b/server/channelserver/session_lifecycle_integration_test.go index e94d9b276..307daa160 100644 --- a/server/channelserver/session_lifecycle_integration_test.go +++ b/server/channelserver/session_lifecycle_integration_test.go @@ -590,12 +590,22 @@ func createTestServerWithDB(t *testing.T, db *sqlx.DB) *Server { RealClientMode: _config.ZZ, }, isShuttingDown: false, + done: make(chan struct{}), } // Create logger logger, _ := zap.NewDevelopment() server.logger = logger + // Initialize repositories + server.charRepo = NewCharacterRepository(db) + server.guildRepo = NewGuildRepository(db) + server.userRepo = NewUserRepository(db) + server.gachaRepo = NewGachaRepository(db) + server.houseRepo = NewHouseRepository(db) + server.festaRepo = NewFestaRepository(db) + server.towerRepo = NewTowerRepository(db) + return server } diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index ef7f85040..0b310c09b 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -50,6 +50,8 @@ type Server struct { userRepo *UserRepository gachaRepo *GachaRepository houseRepo *HouseRepository + festaRepo *FestaRepository + towerRepo *TowerRepository erupeConfig *_config.Config acceptConns chan net.Conn deleteConns chan net.Conn @@ -125,6 +127,8 @@ func NewServer(config *Config) *Server { s.userRepo = NewUserRepository(config.DB) s.gachaRepo = NewGachaRepository(config.DB) s.houseRepo = NewHouseRepository(config.DB) + s.festaRepo = NewFestaRepository(config.DB) + s.towerRepo = NewTowerRepository(config.DB) // Mezeporta s.stages["sl1Ns200p0a0u0"] = NewStage("sl1Ns200p0a0u0") diff --git a/server/channelserver/testhelpers_db.go b/server/channelserver/testhelpers_db.go index 8fcfa79ba..cc6335037 100644 --- a/server/channelserver/testhelpers_db.go +++ b/server/channelserver/testhelpers_db.go @@ -329,3 +329,16 @@ func CreateTestGuild(t *testing.T, db *sqlx.DB, leaderCharID uint32, name string return guildID } + +// SetTestDB assigns a database to a Server and initializes all repositories. +// Use this in integration tests instead of setting s.server.db directly. +func SetTestDB(s *Server, db *sqlx.DB) { + s.db = db + s.charRepo = NewCharacterRepository(db) + s.guildRepo = NewGuildRepository(db) + s.userRepo = NewUserRepository(db) + s.gachaRepo = NewGachaRepository(db) + s.houseRepo = NewHouseRepository(db) + s.festaRepo = NewFestaRepository(db) + s.towerRepo = NewTowerRepository(db) +}