using Microsoft.EntityFrameworkCore; using SVSim.Database.Enums; using SVSim.Database.Models; namespace SVSim.Database.Services; /// /// Wire-shape returned by . Field names match the /// reward_list entries used by /pack/open and /basic_puzzle/finish. /// reward_num is a POST-STATE TOTAL for currencies and a count for collection grants — see /// ... see SVSim.EmulatedEntrypoint.Models.Dtos.RewardListEntry /// for the on-the-wire DTO and the rationale. /// public sealed record GrantedReward(int RewardType, long RewardId, int RewardNum); /// /// General reward-grant primitive. Switches on , mutates the /// appropriate viewer collection or field, and returns the /// wire-shape entry the caller should embed in its response's reward_list. /// /// Caller is responsible for SaveChangesAsync — this service only mutates the in-memory /// graph so a controller can stack several grants in a single transaction. /// public sealed class RewardGrantService { private readonly SVSimDbContext _db; public RewardGrantService(SVSimDbContext db) => _db = db; public GrantedReward Apply(Viewer viewer, UserGoodsType type, long detailId, int num) { switch (type) { case UserGoodsType.Sleeve: AddCosmeticIfMissing(viewer.Sleeves, detailId, _db.Sleeves); return new GrantedReward((int)type, detailId, 1); case UserGoodsType.Emblem: AddCosmeticIfMissing(viewer.Emblems, detailId, _db.Emblems); return new GrantedReward((int)type, detailId, 1); case UserGoodsType.Skin: // LeaderSkin in our schema AddCosmeticIfMissing(viewer.LeaderSkins, detailId, _db.LeaderSkins); return new GrantedReward((int)type, detailId, 1); case UserGoodsType.Degree: AddCosmeticIfMissing(viewer.Degrees, detailId, _db.Degrees); return new GrantedReward((int)type, detailId, 1); case UserGoodsType.MyPageBG: AddCosmeticIfMissing(viewer.MyPageBackgrounds, detailId, _db.MyPageBackgrounds); return new GrantedReward((int)type, detailId, 1); case UserGoodsType.Rupy: viewer.Currency.Rupees += (ulong)num; return new GrantedReward((int)type, detailId, checked((int)viewer.Currency.Rupees)); case UserGoodsType.Crystal: viewer.Currency.Crystals += (ulong)num; return new GrantedReward((int)type, detailId, checked((int)viewer.Currency.Crystals)); case UserGoodsType.RedEther: viewer.Currency.RedEther += (ulong)num; return new GrantedReward((int)type, detailId, checked((int)viewer.Currency.RedEther)); case UserGoodsType.Item: { var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId); if (owned is null) { var item = _db.Items.Find((int)detailId) ?? throw new InvalidOperationException($"Item {detailId} not in catalog"); viewer.Items.Add(new OwnedItemEntry { Item = item, Count = num, Viewer = viewer }); return new GrantedReward((int)type, detailId, num); } owned.Count += num; return new GrantedReward((int)type, detailId, owned.Count); } case UserGoodsType.Card: case UserGoodsType.SpotCard: case UserGoodsType.SpotCardOnlyLatestCardPack: throw new NotSupportedException( $"{type} rewards are out of Phase 1 scope — extend RewardGrantService when /pack/open or similar needs them."); default: throw new NotSupportedException($"UserGoodsType {type} not yet handled by RewardGrantService"); } } private static void AddCosmeticIfMissing(List collection, long detailId, DbSet catalog) where T : class { // Cosmetic ownership is binary — if the viewer already owns it, the grant is a no-op // (matches client UpdateHaveUserGoodsNum behaviour which just calls .Acquired() each time). bool alreadyOwned = collection.Any(e => GetId(e) == detailId); if (alreadyOwned) return; // Wire reward_detail_id is long, but every cosmetic catalog in this codebase uses // BaseEntity; downcast for Find. The checked() throws OverflowException if a // future capture ships a real long id rather than silently truncating it. var entity = catalog.Find(checked((int)detailId)) ?? throw new InvalidOperationException( $"Cosmetic id {detailId} not in catalog for type {typeof(T).Name}"); collection.Add(entity); } /// /// Reflectively reads an entity's Id property — works for both BaseEntity<int> /// (cosmetics) and BaseEntity<long> (e.g. Viewer/Card) without forcing two /// non-generic overloads of . /// private static long GetId(T e) { var prop = typeof(T).GetProperty("Id") ?? throw new InvalidOperationException($"Type {typeof(T).Name} missing Id property"); var val = prop.GetValue(e); return val switch { long l => l, int i => i, _ => 0 }; } }