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 GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId) { // SocialAccountConnection is [Owned]-by-Viewer — can't be queried as a top-level Set. // Look up the Viewer that has a matching owned connection instead. return await _dbContext.Set() .AsNoTracking() .FirstOrDefaultAsync(v => v.SocialAccountConnections.Any(sac => sac.AccountType == accountType && sac.AccountId == socialId)); } public async Task GetViewerWithSocials(long id) { return await _dbContext.Set().AsNoTracking().Include(viewer => viewer.SocialAccountConnections) .FirstOrDefaultAsync(viewer => viewer.Id == id); } /// /// Loads a viewer with every navigation property needed to render the home-screen /// (/load/index). Heavy query — only call from LoadController.Index. /// public async Task GetViewerByShortUdid(long shortUdid) { return await _dbContext.Set() .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 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 classes = await _dbContext.Set() .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 = "", 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().Add(viewer); await _dbContext.SaveChangesAsync(); return viewer; } }