Files
SVSimServer/SVSim.Database/Repositories/Viewer/ViewerRepository.cs
2026-05-23 14:18:01 -04:00

123 lines
5.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Card;
using SVSim.Database.Repositories.Globals;
namespace SVSim.Database.Repositories.Viewer;
public class ViewerRepository : IViewerRepository
{
protected readonly SVSimDbContext _dbContext;
private const int MaxFriends = 20;
public ViewerRepository(SVSimDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId)
{
// SocialAccountConnection is [Owned]-by-Viewer — can't be queried as a top-level Set<T>.
// Look up the Viewer that has a matching owned connection instead.
return await _dbContext.Set<Models.Viewer>()
.AsNoTracking()
.FirstOrDefaultAsync(v => v.SocialAccountConnections.Any(sac =>
sac.AccountType == accountType && sac.AccountId == socialId));
}
public async Task<Models.Viewer?> GetViewerWithSocials(long id)
{
return await _dbContext.Set<Models.Viewer>().AsNoTracking().Include(viewer => viewer.SocialAccountConnections)
.FirstOrDefaultAsync(viewer => viewer.Id == id);
}
/// <summary>
/// Loads a viewer with every navigation property needed to render the home-screen
/// (/load/index). Heavy query — only call from LoadController.Index.
/// </summary>
public async Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid)
{
return await _dbContext.Set<Models.Viewer>()
.AsNoTracking()
.Include(v => v.MissionData)
.Include(v => v.Info).ThenInclude(i => i.SelectedEmblem)
.Include(v => v.Info).ThenInclude(i => i.SelectedDegree)
.Include(v => v.Currency)
.Include(v => v.Classes).ThenInclude(c => c.Class).ThenInclude(c => c.LeaderSkins)
.Include(v => v.Classes).ThenInclude(c => c.LeaderSkin)
.Include(v => v.Decks).ThenInclude(d => d.Class)
.Include(v => v.Decks).ThenInclude(d => d.Sleeve)
.Include(v => v.Decks).ThenInclude(d => d.LeaderSkin)
.Include(v => v.Cards).ThenInclude(c => c.Card)
.Include(v => v.Items).ThenInclude(i => i.Item)
.Include(v => v.Sleeves)
.Include(v => v.Emblems)
.Include(v => v.Degrees)
.Include(v => v.LeaderSkins).ThenInclude(ls => ls.Class)
.Include(v => v.MyPageBackgrounds)
.FirstOrDefaultAsync(viewer => viewer.ShortUdid == shortUdid);
}
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
ulong socialAccountIdentifier, ulong? shortUdid = null)
{
Models.Viewer viewer = new Models.Viewer
{
DisplayName = displayName
};
GameConfiguration gameConfig = await new GlobalsRepository(_dbContext).GetGameConfiguration("default");
viewer.SocialAccountConnections.Add(new SocialAccountConnection
{
AccountId = socialAccountIdentifier,
AccountType = socialType
});
viewer.Info.MaxFriends = gameConfig.MaxFriends;
viewer.Info.CountryCode = "KOR";
viewer.Info.BirthDate = DateTime.UtcNow;
viewer.Currency.Crystals = gameConfig.DefaultCrystals;
viewer.Currency.Rupees = gameConfig.DefaultRupees;
viewer.Currency.RedEther = gameConfig.DefaultEther;
viewer.MissionData.TutorialState = 100; // finishes tutorial for now
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
// and would otherwise be null (audit §6 #3 latent NRE — this is the one).
List<ClassEntry> classes = await _dbContext.Set<ClassEntry>()
.Include(c => c.LeaderSkins)
.ToListAsync();
viewer.Classes = classes.Select(ce =>
{
var skin = ce.DefaultLeaderSkin ?? ce.LeaderSkins.FirstOrDefault();
return new ViewerClassData
{
Class = ce,
Exp = 0,
Level = 0,
LeaderSkin = skin ?? new LeaderSkinEntry { Id = 0, Name = "<missing>", ClassId = ce.Id }
};
}).ToList();
if (gameConfig.DefaultSleeve is not null) viewer.Sleeves.Add(gameConfig.DefaultSleeve);
if (gameConfig.DefaultDegree is not null) viewer.Degrees.Add(gameConfig.DefaultDegree);
if (gameConfig.DefaultEmblem is not null) viewer.Emblems.Add(gameConfig.DefaultEmblem);
if (gameConfig.DefaultMyPageBackground is not null) viewer.MyPageBackgrounds.Add(gameConfig.DefaultMyPageBackground);
// Grant one of each class's default leader skin. Filter out the synthetic placeholders
// (Id=0) and dedupe — skins are many-to-many via SleeveEntryViewer-style join.
var grantedSkins = viewer.Classes
.Select(vcd => vcd.LeaderSkin)
.Where(s => s.Id != 0)
.DistinctBy(s => s.Id)
.ToList();
viewer.LeaderSkins.AddRange(grantedSkins);
_dbContext.Set<Models.Viewer>().Add(viewer);
await _dbContext.SaveChangesAsync();
return viewer;
}
}