mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
refactor(channelserver): extract FestaRepository and TowerRepository
Move all direct DB calls from handlers_festa.go (23 calls across 8 tables) and handlers_tower.go (16 calls across 4 tables) into dedicated repository structs following the established pattern. FestaRepository (14 methods): lifecycle cleanup, event management, team souls, trial stats/rankings, user state, voting, registration, soul submission, prize claiming/enumeration. TowerRepository (12 methods): personal tower data (skills, progress, gems), guild tenrouirai progress/scores/page advancement, tower RP. Also fix pre-existing nil pointer panics in integration tests by adding SetTestDB helper that initializes both the DB connection and all repositories, and wire the done channel in createTestServerWithDB to prevent Shutdown panics.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
228
server/channelserver/repo_festa.go
Normal file
228
server/channelserver/repo_festa.go
Normal file
@@ -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
|
||||
164
server/channelserver/repo_tower.go
Normal file
164
server/channelserver/repo_tower.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user