using SVSim.Database.Models; using SVSim.Database.Repositories.Mission; namespace SVSim.EmulatedEntrypoint.Services; public sealed class ViewerMissionStateService : IViewerMissionStateService { private const int DailySlot = 0; private const int WeeklySlotStart = 1; private const int WeeklySlotCount = 3; private const int LotTypeDaily = 6; private const int LotTypeWeekly = 2; private readonly IMissionCatalogRepository _catalog; private readonly IViewerMissionRepository _viewerRepo; private readonly TimeProvider _time; public ViewerMissionStateService( IMissionCatalogRepository catalog, IViewerMissionRepository viewerRepo, TimeProvider time) { _catalog = catalog; _viewerRepo = viewerRepo; _time = time; } public async Task EnsureCurrentAsync(Viewer viewer, CancellationToken ct = default) { // Always read fresh from DB. Navigation properties on the passed viewer may be stale or // double-tracked depending on caller's load path — don't trust them. var existingAchievements = await _viewerRepo.GetAchievementsAsync(viewer.Id, ct); var existingMissions = await _viewerRepo.GetMissionsAsync(viewer.Id, ct); await MaterializeAchievementsAsync(viewer.Id, existingAchievements, ct); await EnsureMissionSlotsAsync(viewer.Id, existingMissions, ct); } private async Task MaterializeAchievementsAsync(long viewerId, List existing, CancellationToken ct) { var catalogTypes = await _catalog.GetAllAchievementTypesAsync(ct); if (catalogTypes.Count == 0) return; var existingTypes = existing.Select(a => a.AchievementType).ToHashSet(); foreach (var type in catalogTypes) { if (existingTypes.Contains(type)) continue; _viewerRepo.AddAchievement(new ViewerAchievement { ViewerId = viewerId, AchievementType = type, Level = 1, AchievementStatus = 0, NowAchievedLevel = 0, ResultAnnounceSawLevel = 0, }); } } private async Task EnsureMissionSlotsAsync(long viewerId, List existing, CancellationToken ct) { var bySlot = existing.ToDictionary(m => m.Slot); var now = _time.GetUtcNow().ToUnixTimeSeconds(); // Daily slot (slot 0) if (!bySlot.ContainsKey(DailySlot)) { var pool = await _catalog.GetByLotTypeAsync(LotTypeDaily, ct); if (pool.Count > 0) { var pick = pool[Random.Shared.Next(pool.Count)]; _viewerRepo.AddMission(new ViewerMission { ViewerId = viewerId, MissionCatalogId = pick.Id, Slot = DailySlot, AssignedAt = now, MissionStatus = 1, }); } } // Weekly slots (1..3) — assign all-or-nothing for simplicity in v1. bool weeklyNeedsAssignment = Enumerable.Range(WeeklySlotStart, WeeklySlotCount) .Any(s => !bySlot.ContainsKey(s)); if (weeklyNeedsAssignment) { var pool = await _catalog.GetByLotTypeAsync(LotTypeWeekly, ct); if (pool.Count >= WeeklySlotCount) { var alreadyAssigned = existing .Where(m => m.Slot >= WeeklySlotStart && m.Slot < WeeklySlotStart + WeeklySlotCount) .Select(m => m.MissionCatalogId).ToHashSet(); var available = pool.Where(p => !alreadyAssigned.Contains(p.Id)).ToList(); var shuffled = available.OrderBy(_ => Random.Shared.Next()).ToList(); int pickIdx = 0; for (int slot = WeeklySlotStart; slot < WeeklySlotStart + WeeklySlotCount; slot++) { if (bySlot.ContainsKey(slot)) continue; if (pickIdx >= shuffled.Count) break; _viewerRepo.AddMission(new ViewerMission { ViewerId = viewerId, MissionCatalogId = shuffled[pickIdx++].Id, Slot = slot, AssignedAt = now, MissionStatus = 1, }); } } } } }