From a6b2c47b988d58398e5ca46eda1d4cba28172c89 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 4 Jul 2026 11:49:17 -0400 Subject: [PATCH] refactor(pack): migrate PackController to IGameCalendarService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swaps all four DateTime.UtcNow.Date same-day checks in PackController for _calendar.ResetReady() — daily-single open, /pack/info daily-single gate, FreePacks /pack/info visibility, FreePacks /pack/open quota. Removes the TODO(daily-reset) comments; ToDto promoted from static to instance so it can see _calendar. Also fills in GameCalendarConfig.ShippedDefaults() (required by the [ConfigSection] discovery in SVSimDbContext.EnsureSeedDataAsync — bootstrap throws without it). Co-Authored-By: Claude Opus 4.7 --- .../Models/Config/GameCalendarConfig.cs | 2 ++ .../Controllers/PackController.cs | 27 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/SVSim.Database/Models/Config/GameCalendarConfig.cs b/SVSim.Database/Models/Config/GameCalendarConfig.cs index e2af1b86..f040404b 100644 --- a/SVSim.Database/Models/Config/GameCalendarConfig.cs +++ b/SVSim.Database/Models/Config/GameCalendarConfig.cs @@ -15,4 +15,6 @@ public class GameCalendarConfig /// WeekKey / MonthKey — treat as a startup constant in production. /// public int DailyResetUtcHour { get; set; } = 0; + + public static GameCalendarConfig ShippedDefaults() => new(); } diff --git a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs index d48a543a..de40ab05 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/PackController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/PackController.cs @@ -34,6 +34,7 @@ public class PackController : SVSimController private readonly SVSimDbContext _db; private readonly IInventoryService _inv; private readonly IGachaPointService _gachaPoint; + private readonly IGameCalendarService _calendar; public PackController( IPackRepository packs, @@ -43,7 +44,8 @@ public class PackController : SVSimController IRandom rng, SVSimDbContext db, IInventoryService inv, - IGachaPointService gachaPoint) + IGachaPointService gachaPoint, + IGameCalendarService calendar) { _packs = packs; _opener = opener; @@ -53,6 +55,7 @@ public class PackController : SVSimController _db = db; _inv = inv; _gachaPoint = gachaPoint; + _calendar = calendar; } [HttpPost("info")] @@ -120,7 +123,7 @@ public class PackController : SVSimController }; } - private static PackConfigDto ToDto( + private PackConfigDto ToDto( PackConfigEntry p, IReadOnlyDictionary openCounts, IReadOnlyDictionary ownedItemsByItemId, @@ -137,21 +140,18 @@ public class PackController : SVSimController // stays visible after the first successful open and a second click 400s with // `daily_free_already_claimed`. Keep the child entry itself so the CRYSTAL_MULTI // full-price button still activates. - // TODO(daily-reset): using UTC midnight; migrate to the JST-boundary DailyReset service - // when it lands (see docs/plans/daily-reset-service.md). - var today = DateTime.UtcNow.Date; - bool dailyClaimedToday = oc?.LastDailyFreeAt is DateTime lastDaily && lastDaily.Date == today; + bool dailyClaimedToday = !_calendar.ResetReady(oc?.LastDailyFreeAt); // Drop type_detail=10 (FREE_PACKS) children whose daily quota for THIS viewer is spent. // Mirrors prod behavior: post-claim /pack/info simply omits the free child from // child_gacha_info (verified in traffic_event_crate_free_pack.ndjson lines 28→32). - // Today's claim count >= DailyFreeGachaCount and same UTC date => hide. + // Reset happens at the daily boundary — new day resets ClaimCount effectively. bool ChildAvailable(PackChildGachaEntry c) { if (c.TypeDetail != CardPackType.FreePacks) return true; if (c.FreeGachaCampaignId is not int campaignId) return true; if (!freeClaimsByCampaignId.TryGetValue(campaignId, out var claim)) return true; - if (claim.LastClaimedAt.Date != today) return true; + if (_calendar.ResetReady(claim.LastClaimedAt)) return true; int dailyCap = c.DailyFreeGachaCount > 0 ? c.DailyFreeGachaCount : 1; return claim.ClaimCount < dailyCap; } @@ -461,11 +461,8 @@ public class PackController : SVSimController { // DAILY is the once-per-day half-off crystal single-pack (GachaUI.cs:1046 → // CheckBuyPackWithCrystal, decompile-confirmed). Currency is Crystal, NOT Rupee. - // TODO(daily-reset): using UTC midnight; migrate to the JST-boundary - // DailyReset service when it lands (see docs/plans/daily-reset-service.md). - var now = DateTime.UtcNow; var existing = viewer.PackOpenCounts.FirstOrDefault(p => p.PackId == pack.Id); - if (existing?.LastDailyFreeAt is DateTime last && last.Date == now.Date) + if (!_calendar.ResetReady(existing?.LastDailyFreeAt)) return BadRequest(new { error = "daily_free_already_claimed" }); long cost = (long)child.Cost * packNumber; @@ -490,9 +487,9 @@ public class PackController : SVSimController return StatusCode(StatusCodes.Status501NotImplemented, new { error = "free_pack_missing_campaign_id" }); int dailyCap = child.DailyFreeGachaCount > 0 ? child.DailyFreeGachaCount : 1; - var today = DateTime.UtcNow.Date; var existing = viewer.FreePackClaims.FirstOrDefault(c => c.FreeGachaCampaignId == campaignId); - if (existing is not null && existing.LastClaimedAt.Date == today && existing.ClaimCount >= dailyCap) + bool resetSinceLastClaim = existing is null || _calendar.ResetReady(existing.LastClaimedAt); + if (existing is not null && !resetSinceLastClaim && existing.ClaimCount >= dailyCap) return BadRequest(new { error = "free_pack_already_claimed_today" }); // pack_number is forced to 1 — free-pack metadata never authorizes multi-opens. @@ -508,7 +505,7 @@ public class PackController : SVSimController LastClaimedAt = DateTime.UtcNow, }); } - else if (existing.LastClaimedAt.Date != today) + else if (resetSinceLastClaim) { existing.ClaimCount = 1; existing.LastClaimedAt = DateTime.UtcNow;