Files
SVSimServer/SVSim.Database/Repositories/Viewer/ViewerRepository.cs
2026-05-24 21:13:15 -04:00

141 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
using SVSim.Database.Services;
namespace SVSim.Database.Repositories.Viewer;
public class ViewerRepository : IViewerRepository
{
protected readonly SVSimDbContext _dbContext;
private readonly IGameConfigService _config;
private const int MaxFriends = 20;
public ViewerRepository(SVSimDbContext dbContext, IGameConfigService config)
{
_dbContext = dbContext;
_config = config;
}
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)
{
// AsSplitQuery: each Include() collection runs as a separate SELECT instead of one giant
// LEFT JOIN with a cartesian product on the result set. The combined Decks+DeckCard+Cards+
// many-to-many-cosmetics shape was producing hundreds of thousands of duplicate rows after
// the import-time default-deck clone landed (24 decks × 40 DeckCards × N cosmetics each),
// pushing /load/index to ~17 s/request. Split queries take O(rows) total instead.
return await _dbContext.Set<Models.Viewer>()
.AsNoTracking()
.AsSplitQuery()
.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
};
var player = _config.Get<PlayerConfig>();
var grants = _config.Get<DefaultGrantsConfig>();
var loadout = _config.Get<DefaultLoadoutConfig>();
viewer.SocialAccountConnections.Add(new SocialAccountConnection
{
AccountId = socialAccountIdentifier,
AccountType = socialType
});
viewer.Info.MaxFriends = player.MaxFriends;
viewer.Info.CountryCode = "KOR";
viewer.Info.BirthDate = DateTime.UtcNow;
viewer.Currency.Crystals = grants.Crystals;
viewer.Currency.Rupees = grants.Rupees;
viewer.Currency.RedEther = grants.Ether;
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();
var defaultSleeveId = loadout.SleeveId;
var defaultDegreeId = loadout.DegreeId;
var defaultEmblemId = loadout.EmblemId;
var defaultBgId = loadout.MyPageBackgroundId;
var defaultSleeve = await _dbContext.Set<SleeveEntry>().FindAsync(defaultSleeveId);
var defaultDegree = await _dbContext.Set<DegreeEntry>().FindAsync(defaultDegreeId);
var defaultEmblem = await _dbContext.Set<EmblemEntry>().FindAsync(defaultEmblemId);
var defaultBg = await _dbContext.Set<MyPageBackgroundEntry>().FindAsync(defaultBgId);
if (defaultSleeve is not null) viewer.Sleeves.Add(defaultSleeve);
if (defaultDegree is not null) viewer.Degrees.Add(defaultDegree);
if (defaultEmblem is not null) viewer.Emblems.Add(defaultEmblem);
if (defaultBg is not null) viewer.MyPageBackgrounds.Add(defaultBg);
// 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;
}
}