141 lines
6.2 KiB
C#
141 lines
6.2 KiB
C#
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;
|
||
}
|
||
}
|