using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SVSim.Database; using SVSim.Database.Models; using SVSim.Database.Repositories.Mission; using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission; using SVSim.EmulatedEntrypoint.Models.Dtos.Mission; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint.Controllers; /// /// /mission/* — daily/weekly mission slots + achievement claim flow. Wire shapes mirror /// MissionInfoDetail.cs + Wizard/Mission*Task.cs. /// [Route("mission")] public class MissionController : SVSimController { private const int RetireCooldownSeconds = 75600; // 21h per capture private const int FailureResultCode = 2; private readonly SVSimDbContext _db; private readonly IViewerMissionStateService _state; private readonly IMissionAssembler _assembler; private readonly IMissionCatalogRepository _catalog; private readonly IViewerMissionRepository _viewerRepo; private readonly TimeProvider _time; public MissionController( SVSimDbContext db, IViewerMissionStateService state, IMissionAssembler assembler, IMissionCatalogRepository catalog, IViewerMissionRepository viewerRepo, TimeProvider time) { _db = db; _state = state; _assembler = assembler; _catalog = catalog; _viewerRepo = viewerRepo; _time = time; } [HttpPost("info")] public async Task Info(BaseRequest request, CancellationToken ct) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); var viewer = await LoadViewer(viewerId, ct); await _state.EnsureCurrentAsync(viewer, ct); await _db.SaveChangesAsync(ct); var dto = await _assembler.BuildAsync(viewer, ct); return Ok(dto); } [HttpPost("retire")] public async Task Retire(MissionRetireRequest request, CancellationToken ct) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); var viewer = await LoadViewer(viewerId, ct); var missions = await _viewerRepo.GetMissionsAsync(viewerId, ct); var target = missions.FirstOrDefault(m => m.Id == request.Id); if (target is null) { return Ok(new { result_code = FailureResultCode }); } var catalogRow = await _catalog.GetByIdAsync(target.MissionCatalogId, ct); if (catalogRow is null || catalogRow.LotType != 2) { return Ok(new { result_code = FailureResultCode }); } var pool = await _catalog.GetByLotTypeAsync(2, ct); var assignedIds = missions .Where(m => m.Slot != target.Slot) .Select(m => m.MissionCatalogId).ToHashSet(); var candidates = pool.Where(p => p.Id != target.MissionCatalogId && !assignedIds.Contains(p.Id)).ToList(); if (candidates.Count == 0) { return Ok(new { result_code = FailureResultCode }); } var pick = candidates[Random.Shared.Next(candidates.Count)]; var now = _time.GetUtcNow(); _viewerRepo.RemoveMission(target); _viewerRepo.AddMission(new ViewerMission { ViewerId = viewerId, MissionCatalogId = pick.Id, Slot = target.Slot, AssignedAt = now.ToUnixTimeSeconds(), MissionStatus = 1, }); viewer.MissionData.MissionChangeTime = now.AddSeconds(RetireCooldownSeconds).UtcDateTime; await _db.SaveChangesAsync(ct); var dto = await _assembler.BuildAsync(viewer, ct); return Ok(dto); } [HttpPost("change_receive_setting")] public async Task ChangeReceiveSetting(MissionChangeReceiveSettingRequest request, CancellationToken ct) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); var viewer = await LoadViewer(viewerId, ct); viewer.MissionData.MissionReceiveType = request.MissionReceiveType; await _db.SaveChangesAsync(ct); var dto = await _assembler.BuildAsync(viewer, ct); return Ok(dto); } private Task LoadViewer(long viewerId, CancellationToken ct) => _db.Viewers .Include(v => v.MissionData) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId, ct); }