This commit is contained in:
gamer147
2026-05-25 12:03:47 -04:00
parent d067f8a64a
commit 558e8288eb
44 changed files with 6512 additions and 3 deletions

View File

@@ -0,0 +1,15 @@
namespace SVSim.Database.Repositories.Viewer;
public interface IPuzzleClearRepository
{
/// <summary>Returns the set of puzzle_ids this viewer has cleared.</summary>
Task<HashSet<int>> GetClearedPuzzleIds(long viewerId);
/// <summary>Returns cleared puzzle_ids grouped by their PuzzleEntry.GroupId. Only groups
/// with at least one clear appear in the dictionary.</summary>
Task<Dictionary<int, HashSet<int>>> GetClearedPuzzleIdsByGroup(long viewerId);
/// <summary>Inserts or updates the (viewer, puzzle) clear row. BestRetryCount keeps the
/// minimum retry_count across all wins.</summary>
Task UpsertClearAsync(long viewerId, int puzzleId, int retryCount);
}

View File

@@ -0,0 +1,59 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Models;
namespace SVSim.Database.Repositories.Viewer;
public class PuzzleClearRepository : IPuzzleClearRepository
{
private readonly SVSimDbContext _db;
public PuzzleClearRepository(SVSimDbContext db) => _db = db;
public async Task<HashSet<int>> GetClearedPuzzleIds(long viewerId)
{
var ids = await _db.ViewerPuzzleClears
.Where(c => c.ViewerId == viewerId)
.Select(c => c.PuzzleId)
.ToListAsync();
return ids.ToHashSet();
}
public async Task<Dictionary<int, HashSet<int>>> GetClearedPuzzleIdsByGroup(long viewerId)
{
// Join via Puzzles to resolve each cleared PuzzleId to its GroupId.
var rows = await (
from c in _db.ViewerPuzzleClears
where c.ViewerId == viewerId
join p in _db.Puzzles on c.PuzzleId equals p.Id
select new { p.GroupId, c.PuzzleId }
).ToListAsync();
return rows
.GroupBy(r => r.GroupId)
.ToDictionary(g => g.Key, g => g.Select(r => r.PuzzleId).ToHashSet());
}
public async Task UpsertClearAsync(long viewerId, int puzzleId, int retryCount)
{
// CONCURRENCY: this read-then-write is not isolated. Two simultaneous /finish calls
// for the same (viewer, puzzle) could both insert and one will lose to the PK. The
// wider mission-completion concurrency note lives on PuzzleController.Finish.
var existing = await _db.ViewerPuzzleClears
.FirstOrDefaultAsync(c => c.ViewerId == viewerId && c.PuzzleId == puzzleId);
if (existing is null)
{
_db.ViewerPuzzleClears.Add(new ViewerPuzzleClear
{
ViewerId = viewerId,
PuzzleId = puzzleId,
ClearedAt = DateTime.UtcNow,
BestRetryCount = retryCount,
});
}
else
{
existing.BestRetryCount = Math.Min(existing.BestRetryCount, retryCount);
}
await _db.SaveChangesAsync();
}
}