using SVSim.Database.Models; using SVSim.Database.Repositories.Mission; using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission; namespace SVSim.EmulatedEntrypoint.Services; public sealed class MissionAssembler : IMissionAssembler { private readonly IMissionCatalogRepository _catalog; private readonly IViewerMissionRepository _viewerRepo; private readonly TimeProvider _time; public MissionAssembler( IMissionCatalogRepository catalog, IViewerMissionRepository viewerRepo, TimeProvider time) { _catalog = catalog; _viewerRepo = viewerRepo; _time = time; } public async Task BuildAsync(Viewer viewer, CancellationToken ct = default) { var now = _time.GetUtcNow(); var dto = new MissionInfoDataDto(); // Read fresh state — don't trust navigation properties. var viewerMissions = await _viewerRepo.GetMissionsAsync(viewer.Id, ct); var viewerAchievements = await _viewerRepo.GetAchievementsAsync(viewer.Id, ct); var missionCatalogIds = viewerMissions.Select(m => m.MissionCatalogId).Distinct().ToList(); var missionCatalog = missionCatalogIds.Count == 0 ? new List() : await _catalog.GetByIdsAsync(missionCatalogIds, ct); var missionById = missionCatalog.ToDictionary(c => c.Id); var maxLevelByType = await _catalog.GetMaxLevelByAchievementTypeAsync(ct); // Gather all event keys we'll need to read counters for, in one batch. var counterEventKeys = new HashSet(); foreach (var m in viewerMissions) { if (missionById.TryGetValue(m.MissionCatalogId, out var cat) && cat.EventType is not null) counterEventKeys.Add(cat.EventType); } // Achievements need catalog rows at viewer's current Level to find their EventType. var achievementCatalogByKey = new Dictionary<(int, int), AchievementCatalogEntry>(); foreach (var a in viewerAchievements) { var c = await _catalog.GetAchievementAsync(a.AchievementType, a.Level, ct); if (c is null) continue; achievementCatalogByKey[(a.AchievementType, a.Level)] = c; if (c.EventType is not null) counterEventKeys.Add(c.EventType); } // BP monthly missions for current JST month. var jstNow = now.ToOffset(TimeSpan.FromHours(9)).AddHours(-2); var monthlyMissions = await _catalog.GetMonthlyMissionsAsync(jstNow.Year, jstNow.Month, ct); foreach (var mm in monthlyMissions) { if (mm.EventType is not null) counterEventKeys.Add(mm.EventType); } var periods = new[] { JstPeriod.DayKey(now), JstPeriod.WeekKey(now), JstPeriod.MonthKey(now), JstPeriod.AllTime }; var counters = counterEventKeys.Count == 0 ? new List() : await _viewerRepo.GetCountersAsync(viewer.Id, counterEventKeys.ToList(), periods, ct); var counterLookup = counters.ToDictionary(c => (c.EventKey, c.Period), c => c.Count); int GetCounter(string eventKey, string period) => counterLookup.TryGetValue((eventKey, period), out var v) ? v : 0; // user_mission_list foreach (var m in viewerMissions.OrderBy(m => m.Slot)) { if (!missionById.TryGetValue(m.MissionCatalogId, out var cat)) continue; int total = 0; if (cat.EventType is not null) { string period = cat.LotType == 6 ? JstPeriod.DayKey(now) : JstPeriod.WeekKey(now); total = GetCounter(cat.EventType, period); } dto.UserMissionList.Add(new UserMissionDto { Id = m.Id, MissionId = cat.Id, TotalCount = total, MissionStatus = m.MissionStatus, DisplayOrder = 0, MissionName = cat.Name, LotType = cat.LotType.ToString(), BattlePassPoint = cat.BattlePassPoint.ToString(), RequireNumber = cat.RequireNumber, RewardType = (int)cat.RewardType, RewardDetailId = cat.RewardDetailId, RewardNumber = cat.RewardNumber, DefaultFlag = cat.DefaultFlag, StartTime = cat.StartTime, EndTime = cat.EndTime, }); } // user_achievement_list — one row per ViewerAchievement, looking up catalog at viewer's current Level. foreach (var a in viewerAchievements.OrderBy(a => a.AchievementType)) { if (!achievementCatalogByKey.TryGetValue((a.AchievementType, a.Level), out var catalog)) continue; int total = catalog.EventType is null ? 0 : GetCounter(catalog.EventType, JstPeriod.AllTime); int maxLevel = maxLevelByType.TryGetValue(a.AchievementType, out var ml) ? ml : a.Level; dto.UserAchievementList.Add(new UserAchievementDto { AchievementType = a.AchievementType, AchievementStatus = a.AchievementStatus, Level = a.Level, NowAchievedLevel = a.NowAchievedLevel, ResultAnnounceSawLevel = a.ResultAnnounceSawLevel, TotalCount = total, AchievementName = catalog.Name, RequireNumber = catalog.RequireNumber, RewardType = (int)catalog.RewardType, RewardDetailId = catalog.RewardDetailId, RewardNumber = catalog.RewardNumber, MaxLevel = maxLevel, OrderNum = catalog.OrderNum, Ios = "", Android = "", }); } // Mission-change gating: viewer.MissionData.MissionChangeTime is when retire becomes available again. bool canChange = viewer.MissionData.MissionChangeTime <= now.UtcDateTime; dto.IsChangeMission = canChange; dto.CanChangeMissionTime = canChange ? null : new DateTimeOffset(viewer.MissionData.MissionChangeTime, TimeSpan.Zero).ToUnixTimeSeconds(); // Receive-type cooldown: v1 has none, always true/null. dto.IsChangeReceiveType = true; dto.CanChangeReceiveTypeTime = null; dto.MissionReceiveType = viewer.MissionData.MissionReceiveType.ToString(); // BP monthly missions block — omit when no rows for current month. if (monthlyMissions.Count > 0) { var startUtc = new DateTimeOffset(jstNow.Year, jstNow.Month, 1, 2, 0, 0, TimeSpan.FromHours(9)); var endUtc = startUtc.AddMonths(1).AddSeconds(-1); dto.BattlePassMonthlyMission = new BPMonthlyMissionsDto { StartDate = startUtc.ToString("yyyy-MM-dd HH:mm:ss"), EndDate = endUtc.ToString("yyyy-MM-dd HH:mm:ss"), MissionList = monthlyMissions.Select(mm => { int done = mm.EventType is null ? 0 : GetCounter(mm.EventType, JstPeriod.MonthKey(now)); var entry = new BPMonthlyMissionDto { Name = mm.Name, IsCleared = done >= mm.RequireNumber, RequireNumber = mm.RequireNumber, DoneNumber = done, BattlePassPoint = mm.BattlePassPoint, }; if (mm.RewardType is not null) { entry.RewardInfo = new BPMonthlyMissionRewardInfoDto { RewardType = ((int)mm.RewardType.Value).ToString(), RewardDetailId = (mm.RewardDetailId ?? 0).ToString(), RewardNumber = (mm.RewardNumber ?? 0).ToString(), }; } return entry; }).ToList(), }; } return dto; } }