feat(inventory): GrantAsync handles cosmetic branches

Sleeve/Emblem/Skin/Degree/MyPageBG grants are idempotent on the viewer's
owned-collection but always emit a wire entry at the top level (preserves
"+1 sleeve" purchase popup). Unknown ids throw InventoryCatalogException.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-31 15:51:46 -04:00
parent 3bc38b407b
commit a1cf1d7519
2 changed files with 112 additions and 0 deletions

View File

@@ -74,6 +74,31 @@ internal sealed class InventoryTransaction : IInventoryTransaction
_ops.Add(new GrantOp(type, detailId, num, spot, false));
return Single(type, detailId, spot);
case UserGoodsType.Sleeve:
AddCosmeticIfMissing(Viewer.Sleeves, detailId, _db.Sleeves);
_ops.Add(new GrantOp(type, detailId, num, 1, false));
return Single(type, detailId, 1);
case UserGoodsType.Emblem:
AddCosmeticIfMissing(Viewer.Emblems, detailId, _db.Emblems);
_ops.Add(new GrantOp(type, detailId, num, 1, false));
return Single(type, detailId, 1);
case UserGoodsType.Skin:
AddCosmeticIfMissing(Viewer.LeaderSkins, detailId, _db.LeaderSkins);
_ops.Add(new GrantOp(type, detailId, num, 1, false));
return Single(type, detailId, 1);
case UserGoodsType.Degree:
AddCosmeticIfMissing(Viewer.Degrees, detailId, _db.Degrees);
_ops.Add(new GrantOp(type, detailId, num, 1, false));
return Single(type, detailId, 1);
case UserGoodsType.MyPageBG:
AddCosmeticIfMissing(Viewer.MyPageBackgrounds, detailId, _db.MyPageBackgrounds);
_ops.Add(new GrantOp(type, detailId, num, 1, false));
return Single(type, detailId, 1);
default:
throw new NotImplementedException(
$"UserGoodsType {type} grant lands in a subsequent task");
@@ -99,6 +124,24 @@ internal sealed class InventoryTransaction : IInventoryTransaction
throw new InvalidOperationException("Inventory transaction already committed");
}
private static bool AddCosmeticIfMissing<T>(List<T> collection, long detailId, Microsoft.EntityFrameworkCore.DbSet<T> catalog) where T : class
{
if (collection.Any(e => GetId(e) == detailId)) return false;
var entity = catalog.Find(checked((int)detailId))
?? throw new InventoryCatalogException(
$"Cosmetic id {detailId} not in catalog for type {typeof(T).Name}");
collection.Add(entity);
return true;
}
private static long GetId<T>(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 };
}
public async ValueTask DisposeAsync()
{
if (!_committed)