150 lines
6.1 KiB
C#
150 lines
6.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using SVSim.Database;
|
|
using SVSim.Database.Enums;
|
|
using SVSim.Database.Models;
|
|
using SVSim.Database.Repositories.Pack;
|
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Services;
|
|
|
|
public class CardAcquisitionService : ICardAcquisitionService
|
|
{
|
|
private readonly SVSimDbContext _db;
|
|
private readonly IPackRepository _packs;
|
|
private readonly ILogger<CardAcquisitionService> _log;
|
|
|
|
public CardAcquisitionService(SVSimDbContext db, IPackRepository packs, ILogger<CardAcquisitionService> log)
|
|
{
|
|
_db = db; _packs = packs; _log = log;
|
|
}
|
|
|
|
public async Task<CardGrantResult> GrantAsync(long viewerId, IEnumerable<long>? newCardIds = null)
|
|
{
|
|
var rewardList = new List<RewardListEntry>();
|
|
|
|
var viewer = await _db.Viewers
|
|
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
|
.Include(v => v.LeaderSkins)
|
|
.Include(v => v.Sleeves)
|
|
.Include(v => v.Emblems)
|
|
.Include(v => v.Degrees)
|
|
.Include(v => v.MyPageBackgrounds)
|
|
.AsSplitQuery()
|
|
.FirstAsync(v => v.Id == viewerId);
|
|
|
|
List<long> lookupSourceCardIds;
|
|
if (newCardIds is not null)
|
|
{
|
|
var newIds = newCardIds.ToList();
|
|
await _packs.GrantCardsToViewer(viewerId, newIds);
|
|
// GrantCardsToViewer mutates OwnedCardEntry rows on the same scoped SVSimDbContext
|
|
// AND commits them via its own SaveChangesAsync. The change tracker already exposes
|
|
// the post-state via viewer.Cards. Note this means GrantAsync performs two saves total
|
|
// (one inside the repo call, one at the end for cosmetic grants) — accepted because
|
|
// any inconsistency in the failure window is self-healing via the next /load/index
|
|
// backfill.
|
|
|
|
lookupSourceCardIds = newIds.Distinct().ToList();
|
|
|
|
foreach (var distinctId in lookupSourceCardIds)
|
|
{
|
|
var owned = viewer.Cards.First(c => c.Card.Id == distinctId);
|
|
rewardList.Add(new RewardListEntry { RewardType = 5, RewardId = distinctId, RewardNum = owned.Count });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Backfill mode: scan all owned cards for missing cosmetics. No card-count mutation.
|
|
lookupSourceCardIds = viewer.Cards.Select(c => c.Card.Id).Distinct().ToList();
|
|
}
|
|
|
|
// Foil resolution: cosmetic mappings are recorded on the non-foil card row.
|
|
// Foil twins (card_id + 1) inherit via the universal +1 convention.
|
|
var lookupCardIds = lookupSourceCardIds
|
|
.Select(id =>
|
|
{
|
|
var card = viewer.Cards.FirstOrDefault(c => c.Card.Id == id)?.Card;
|
|
return (card?.IsFoil == true) ? id - 1 : id;
|
|
})
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
var rewards = await _db.CardCosmeticRewards
|
|
.Where(r => lookupCardIds.Contains(r.CardId))
|
|
.ToListAsync();
|
|
|
|
foreach (var reward in rewards)
|
|
{
|
|
if (await TryGrant(viewer, reward))
|
|
{
|
|
rewardList.Add(new RewardListEntry
|
|
{
|
|
RewardType = (int)reward.Type,
|
|
RewardId = reward.CosmeticId,
|
|
RewardNum = reward.Quantity,
|
|
});
|
|
}
|
|
}
|
|
|
|
await _db.SaveChangesAsync();
|
|
return new CardGrantResult(rewardList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the cosmetic was newly granted (caller should emit a reward_list entry).
|
|
/// Returns false if the viewer already owned it or the master row is missing (defensive log).
|
|
/// </summary>
|
|
private async Task<bool> TryGrant(Viewer viewer, CardCosmeticReward reward)
|
|
{
|
|
var id = (int)reward.CosmeticId; // master tables use int Id
|
|
|
|
switch (reward.Type)
|
|
{
|
|
case CosmeticType.Skin:
|
|
{
|
|
if (viewer.LeaderSkins.Any(s => s.Id == id)) return false;
|
|
var master = await _db.LeaderSkins.FindAsync(id);
|
|
if (master is null) { _log.LogWarning("Skin master row missing for cosmetic_id={Id}", id); return false; }
|
|
viewer.LeaderSkins.Add(master);
|
|
return true;
|
|
}
|
|
case CosmeticType.Sleeve:
|
|
{
|
|
if (viewer.Sleeves.Any(s => s.Id == id)) return false;
|
|
var master = await _db.Sleeves.FindAsync(id);
|
|
if (master is null) { _log.LogWarning("Sleeve master row missing for cosmetic_id={Id}", id); return false; }
|
|
viewer.Sleeves.Add(master);
|
|
return true;
|
|
}
|
|
case CosmeticType.Emblem:
|
|
{
|
|
if (viewer.Emblems.Any(e => e.Id == id)) return false;
|
|
var master = await _db.Emblems.FindAsync(id);
|
|
if (master is null) { _log.LogWarning("Emblem master row missing for cosmetic_id={Id}", id); return false; }
|
|
viewer.Emblems.Add(master);
|
|
return true;
|
|
}
|
|
case CosmeticType.Degree:
|
|
{
|
|
if (viewer.Degrees.Any(d => d.Id == id)) return false;
|
|
var master = await _db.Degrees.FindAsync(id);
|
|
if (master is null) { _log.LogWarning("Degree master row missing for cosmetic_id={Id}", id); return false; }
|
|
viewer.Degrees.Add(master);
|
|
return true;
|
|
}
|
|
case CosmeticType.MyPageBG:
|
|
{
|
|
if (viewer.MyPageBackgrounds.Any(b => b.Id == id)) return false;
|
|
var master = await _db.MyPageBackgrounds.FindAsync(id);
|
|
if (master is null) { _log.LogWarning("MyPageBG master row missing for cosmetic_id={Id}", id); return false; }
|
|
viewer.MyPageBackgrounds.Add(master);
|
|
return true;
|
|
}
|
|
default:
|
|
_log.LogWarning("Unknown CosmeticType {Type} for card {CardId}", reward.Type, reward.CardId);
|
|
return false;
|
|
}
|
|
}
|
|
}
|