From f584c5a688498269c9c0273cde233d66efe50cec Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Sat, 21 Feb 2026 00:50:55 +0100 Subject: [PATCH] feat(channelserver): add daily noon resets for gacha stepup and guild RP Gacha stepup progress now resets when queried after the most recent noon boundary, using a new created_at column on gacha_stepup. Guild member rp_today rolls into rp_yesterday lazily when members are enumerated after noon, using a new rp_reset_at column on guilds. Both follow the established lazy-reset pattern from the cafe handler. --- schemas/patch-schema/30-daily-resets.sql | 6 ++++++ server/channelserver/guild_model.go | 1 + server/channelserver/handlers_gacha.go | 27 +++++++++++++++++++++--- server/channelserver/handlers_guild.go | 15 ++++++++++++- server/channelserver/repo_gacha.go | 19 +++++++++++++++++ server/channelserver/repo_guild.go | 25 ++++++++++++++++++++++ 6 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 schemas/patch-schema/30-daily-resets.sql diff --git a/schemas/patch-schema/30-daily-resets.sql b/schemas/patch-schema/30-daily-resets.sql new file mode 100644 index 000000000..18b24e5ca --- /dev/null +++ b/schemas/patch-schema/30-daily-resets.sql @@ -0,0 +1,6 @@ +BEGIN; +ALTER TABLE IF EXISTS public.gacha_stepup + ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE DEFAULT now(); +ALTER TABLE IF EXISTS public.guilds + ADD COLUMN IF NOT EXISTS rp_reset_at TIMESTAMP WITH TIME ZONE; +END; diff --git a/server/channelserver/guild_model.go b/server/channelserver/guild_model.go index a1bb4937a..baa825cd5 100644 --- a/server/channelserver/guild_model.go +++ b/server/channelserver/guild_model.go @@ -56,6 +56,7 @@ type Guild struct { Souls uint32 `db:"souls"` AllianceID uint32 `db:"alliance_id"` Icon *GuildIcon `db:"icon"` + RPResetAt time.Time `db:"rp_reset_at"` GuildLeader } diff --git a/server/channelserver/handlers_gacha.go b/server/channelserver/handlers_gacha.go index bf912f53b..4245be3f2 100644 --- a/server/channelserver/handlers_gacha.go +++ b/server/channelserver/handlers_gacha.go @@ -1,9 +1,13 @@ package channelserver import ( + "database/sql" + "errors" + "math/rand" + "time" + "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" - "math/rand" "go.uber.org/zap" ) @@ -306,8 +310,25 @@ func handleMsgMhfPlayStepupGacha(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetStepupStatus(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetStepupStatus) - // TODO: Reset daily (noon) - step, _ := s.server.gachaRepo.GetStepupStep(pkt.GachaID, s.charID) + + // Compute the most recent noon boundary + midday := TimeMidnight().Add(12 * time.Hour) + if TimeAdjusted().Before(midday) { + midday = midday.Add(-24 * time.Hour) + } + + step, createdAt, err := s.server.gachaRepo.GetStepupWithTime(pkt.GachaID, s.charID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + s.logger.Error("Failed to get gacha stepup state", zap.Error(err)) + } + // Reset stale stepup progress (created before the most recent noon) + if err == nil && createdAt.Before(midday) { + if err := s.server.gachaRepo.DeleteStepup(pkt.GachaID, s.charID); err != nil { + s.logger.Error("Failed to reset stale gacha stepup", zap.Error(err)) + } + step = 0 + } + hasEntry, _ := s.server.gachaRepo.HasEntryType(pkt.GachaID, step) if !hasEntry { if err := s.server.gachaRepo.DeleteStepup(pkt.GachaID, s.charID); err != nil { diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index 7e08eb283..488554010 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -1,10 +1,12 @@ package channelserver import ( + "sort" + "time" + "erupe-ce/common/byteframe" "erupe-ce/common/mhfitem" _config "erupe-ce/config" - "sort" ps "erupe-ce/common/pascalstring" "erupe-ce/network/mhfpacket" @@ -105,6 +107,17 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { return } + // Lazy daily RP rollover: move rp_today → rp_yesterday at noon + midday := TimeMidnight().Add(12 * time.Hour) + if TimeAdjusted().Before(midday) { + midday = midday.Add(-24 * time.Hour) + } + if guild.RPResetAt.Before(midday) { + if err := s.server.guildRepo.RolloverDailyRP(guild.ID, midday); err != nil { + s.logger.Error("Failed to rollover guild daily RP", zap.Error(err)) + } + } + guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false) if err != nil { diff --git a/server/channelserver/repo_gacha.go b/server/channelserver/repo_gacha.go index 9bdbef129..f361b8706 100644 --- a/server/channelserver/repo_gacha.go +++ b/server/channelserver/repo_gacha.go @@ -1,6 +1,10 @@ package channelserver import ( + "database/sql" + "errors" + "time" + "github.com/jmoiron/sqlx" ) @@ -96,6 +100,21 @@ func (r *GachaRepository) GetStepupStep(gachaID uint32, charID uint32) (uint8, e return step, err } +// GetStepupWithTime returns the current step and creation time for a stepup entry. +// Returns sql.ErrNoRows if no entry exists. +func (r *GachaRepository) GetStepupWithTime(gachaID uint32, charID uint32) (uint8, time.Time, error) { + var step uint8 + var createdAt time.Time + err := r.db.QueryRow( + `SELECT step, COALESCE(created_at, '2000-01-01'::timestamptz) FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`, + gachaID, charID, + ).Scan(&step, &createdAt) + if errors.Is(err, sql.ErrNoRows) { + return 0, time.Time{}, err + } + return step, createdAt, err +} + // HasEntryType returns whether a gacha has any entries of the given type. func (r *GachaRepository) HasEntryType(gachaID uint32, entryType uint8) (bool, error) { var count int diff --git a/server/channelserver/repo_guild.go b/server/channelserver/repo_guild.go index 18eb7601c..734de2969 100644 --- a/server/channelserver/repo_guild.go +++ b/server/channelserver/repo_guild.go @@ -54,6 +54,7 @@ SELECT ga.sub2_id = g.id ), 0) AS alliance_id, icon, + COALESCE(rp_reset_at, '2000-01-01'::timestamptz) AS rp_reset_at, (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count FROM guilds g JOIN guild_characters gc ON gc.character_id = leader_id @@ -939,6 +940,30 @@ func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedChara return chars, nil } +// RolloverDailyRP moves rp_today into rp_yesterday for all members of a guild, +// then updates the guild's rp_reset_at timestamp. +func (r *GuildRepository) RolloverDailyRP(guildID uint32, noon time.Time) error { + tx, err := r.db.Begin() + if err != nil { + return err + } + if _, err := tx.Exec( + `UPDATE guild_characters SET rp_yesterday = rp_today, rp_today = 0 WHERE guild_id = $1`, + guildID, + ); err != nil { + _ = tx.Rollback() + return err + } + if _, err := tx.Exec( + `UPDATE guilds SET rp_reset_at = $1 WHERE id = $2`, + noon, guildID, + ); err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + // AddWeeklyBonusUsers atomically adds numUsers to the guild's weekly bonus exceptional user count. func (r *GuildRepository) AddWeeklyBonusUsers(guildID uint32, numUsers uint8) error { _, err := r.db.Exec(