Three endpoints + 9 integration tests. Captured-data-is-catalog: viewer's achievement Level starts at MIN(Level) per type from the catalog (not 1), so the assembler always has a row to render against. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
3.6 KiB
C#
78 lines
3.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using SVSim.Database.Models;
|
|
|
|
namespace SVSim.Database.Repositories.Mission;
|
|
|
|
public sealed class MissionCatalogRepository : IMissionCatalogRepository
|
|
{
|
|
private readonly SVSimDbContext _db;
|
|
|
|
// Process-level cache for the derived MAX(Level) lookup. Cleared on host restart
|
|
// (re-bootstrap is the only legitimate way to mutate the catalog at runtime).
|
|
private static IReadOnlyDictionary<int, int>? _maxLevelCache;
|
|
private static readonly SemaphoreSlim _maxLevelLock = new(1, 1);
|
|
|
|
public MissionCatalogRepository(SVSimDbContext db) { _db = db; }
|
|
|
|
public Task<List<MissionCatalogEntry>> GetByLotTypeAsync(int lotType, CancellationToken ct) =>
|
|
_db.MissionCatalog.AsNoTracking().Where(e => e.LotType == lotType).ToListAsync(ct);
|
|
|
|
public Task<List<MissionCatalogEntry>> GetByIdsAsync(IReadOnlyCollection<int> ids, CancellationToken ct) =>
|
|
_db.MissionCatalog.AsNoTracking().Where(e => ids.Contains(e.Id)).ToListAsync(ct);
|
|
|
|
public Task<MissionCatalogEntry?> GetByIdAsync(int id, CancellationToken ct) =>
|
|
_db.MissionCatalog.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id, ct);
|
|
|
|
public Task<List<MissionCatalogEntry>> GetByEventTypesAsync(IReadOnlyCollection<string> eventTypes, CancellationToken ct) =>
|
|
_db.MissionCatalog.AsNoTracking()
|
|
.Where(e => e.EventType != null && eventTypes.Contains(e.EventType))
|
|
.ToListAsync(ct);
|
|
|
|
public Task<List<AchievementCatalogEntry>> GetAchievementsByEventTypesAsync(IReadOnlyCollection<string> eventTypes, CancellationToken ct) =>
|
|
_db.AchievementCatalog.AsNoTracking()
|
|
.Where(e => e.EventType != null && eventTypes.Contains(e.EventType))
|
|
.ToListAsync(ct);
|
|
|
|
public Task<List<int>> GetAllAchievementTypesAsync(CancellationToken ct) =>
|
|
_db.AchievementCatalog.AsNoTracking()
|
|
.Select(e => e.AchievementType).Distinct()
|
|
.ToListAsync(ct);
|
|
|
|
public async Task<IReadOnlyDictionary<int, int>> GetMaxLevelByAchievementTypeAsync(CancellationToken ct)
|
|
{
|
|
if (_maxLevelCache is not null) return _maxLevelCache;
|
|
await _maxLevelLock.WaitAsync(ct);
|
|
try
|
|
{
|
|
if (_maxLevelCache is null)
|
|
{
|
|
var pairs = await _db.AchievementCatalog.AsNoTracking()
|
|
.GroupBy(e => e.AchievementType)
|
|
.Select(g => new { Type = g.Key, Max = g.Max(e => e.Level) })
|
|
.ToListAsync(ct);
|
|
_maxLevelCache = pairs.ToDictionary(p => p.Type, p => p.Max);
|
|
}
|
|
return _maxLevelCache;
|
|
}
|
|
finally { _maxLevelLock.Release(); }
|
|
}
|
|
|
|
public async Task<IReadOnlyDictionary<int, int>> GetMinLevelByAchievementTypeAsync(CancellationToken ct)
|
|
{
|
|
var pairs = await _db.AchievementCatalog.AsNoTracking()
|
|
.GroupBy(e => e.AchievementType)
|
|
.Select(g => new { Type = g.Key, Min = g.Min(e => e.Level) })
|
|
.ToListAsync(ct);
|
|
return pairs.ToDictionary(p => p.Type, p => p.Min);
|
|
}
|
|
|
|
public Task<AchievementCatalogEntry?> GetAchievementAsync(int achievementType, int level, CancellationToken ct) =>
|
|
_db.AchievementCatalog.AsNoTracking()
|
|
.FirstOrDefaultAsync(e => e.AchievementType == achievementType && e.Level == level, ct);
|
|
|
|
public Task<List<BattlePassMonthlyMissionEntry>> GetMonthlyMissionsAsync(int year, int month, CancellationToken ct) =>
|
|
_db.BattlePassMonthlyMissions.AsNoTracking()
|
|
.Where(e => e.Year == year && e.Month == month)
|
|
.OrderBy(e => e.OrderNum).ToListAsync(ct);
|
|
}
|