Files
SVSimServer/SVSim.EmulatedEntrypoint/Controllers/MissionController.cs
gamer147 5693ec0302 feat(missions): /load/index materializes viewer mission/achievement state
EnsureCurrentAsync now takes viewerId (was Viewer), so it works with
LoadController's AsNoTracking-loaded detached viewers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:45:31 -04:00

125 lines
4.4 KiB
C#

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;
/// <summary>
/// /mission/* — daily/weekly mission slots + achievement claim flow. Wire shapes mirror
/// MissionInfoDetail.cs + Wizard/Mission*Task.cs.
/// </summary>
[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<IActionResult> Info(BaseRequest request, CancellationToken ct)
{
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
var viewer = await LoadViewer(viewerId, ct);
await _state.EnsureCurrentAsync(viewer.Id, ct);
await _db.SaveChangesAsync(ct);
var dto = await _assembler.BuildAsync(viewer, ct);
return Ok(dto);
}
[HttpPost("retire")]
public async Task<IActionResult> 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<IActionResult> 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<Viewer> LoadViewer(long viewerId, CancellationToken ct) =>
_db.Viewers
.Include(v => v.MissionData)
.AsSplitQuery()
.FirstAsync(v => v.Id == viewerId, ct);
}