feat(load): project currency/cards/cosmetics through entitlements (freeplay)
Route /load/index currency, owned-card list, and cosmetic id-lists through IViewerEntitlements so freeplay mode inflates all three without touching the viewer's DB state. Adds LoadControllerFreeplayTests (2 tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,12 +50,13 @@ public class LoadController : SVSimController
|
|||||||
private readonly IBattlePassService _battlePass;
|
private readonly IBattlePassService _battlePass;
|
||||||
private readonly IViewerMissionStateService _missionState;
|
private readonly IViewerMissionStateService _missionState;
|
||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly IViewerEntitlements _entitlements;
|
||||||
|
|
||||||
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
||||||
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository,
|
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository,
|
||||||
ICardAcquisitionService acquisition, IGameConfigService config,
|
ICardAcquisitionService acquisition, IGameConfigService config,
|
||||||
IBattlePassService battlePass, IViewerMissionStateService missionState,
|
IBattlePassService battlePass, IViewerMissionStateService missionState,
|
||||||
SVSimDbContext db)
|
SVSimDbContext db, IViewerEntitlements entitlements)
|
||||||
{
|
{
|
||||||
_viewerRepository = viewerRepository;
|
_viewerRepository = viewerRepository;
|
||||||
_cardRepository = cardRepository;
|
_cardRepository = cardRepository;
|
||||||
@@ -66,6 +67,7 @@ public class LoadController : SVSimController
|
|||||||
_battlePass = battlePass;
|
_battlePass = battlePass;
|
||||||
_missionState = missionState;
|
_missionState = missionState;
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_entitlements = entitlements;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("index")]
|
[HttpPost("index")]
|
||||||
@@ -127,20 +129,11 @@ public class LoadController : SVSimController
|
|||||||
// * card_set_id=90000 (engine tokens, char_type=4): never collectible
|
// * card_set_id=90000 (engine tokens, char_type=4): never collectible
|
||||||
// Both naturally fall out of "ownership-only" since the viewer can't own them;
|
// Both naturally fall out of "ownership-only" since the viewer can't own them;
|
||||||
// re-confirm the filter if we later move to Option B and start iterating card-sets.
|
// re-confirm the filter if we later move to Option B and start iterating card-sets.
|
||||||
var defaultCards = await _cardRepository.GetDefaultCards();
|
// Owned-card projection (incl. the freeplay "all cards" path) lives in the entitlements
|
||||||
var defaultCardIds = defaultCards.Select(c => c.Id).ToHashSet();
|
// service so both modes share one definition.
|
||||||
var ownedCollectibles = viewer.Cards
|
var allCardsAsOwned = await _entitlements.EffectiveOwnedCardsAsync(viewer, ct);
|
||||||
.Where(c => c.Count > 0 && !defaultCardIds.Contains(c.Card.Id));
|
|
||||||
var allCardsAsOwned = ownedCollectibles
|
|
||||||
.Concat(defaultCards.Select(bc => new OwnedCardEntry
|
|
||||||
{
|
|
||||||
Card = bc,
|
|
||||||
Count = 3,
|
|
||||||
IsProtected = true
|
|
||||||
}))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
List<LeaderSkinEntry> allLeaderSkins = await _collectionRepository.GetLeaderSkins();
|
var cosmetics = await _entitlements.EffectiveCosmeticsAsync(viewer, ct);
|
||||||
var classExpCurve = await _globalsRepository.GetClassExpCurve();
|
var classExpCurve = await _globalsRepository.GetClassExpCurve();
|
||||||
|
|
||||||
List<ClassExp> classExps = new();
|
List<ClassExp> classExps = new();
|
||||||
@@ -179,7 +172,13 @@ public class LoadController : SVSimController
|
|||||||
{
|
{
|
||||||
UserTutorial = new UserTutorial { TutorialStep = viewer.MissionData.TutorialState },
|
UserTutorial = new UserTutorial { TutorialStep = viewer.MissionData.TutorialState },
|
||||||
UserInfo = new UserInfo(deviceType, viewer),
|
UserInfo = new UserInfo(deviceType, viewer),
|
||||||
UserCurrency = new UserCurrency(viewer),
|
UserCurrency = new UserCurrency(viewer)
|
||||||
|
{
|
||||||
|
Crystals = (ulong)_entitlements.EffectiveBalance(viewer, SpendCurrency.Crystal),
|
||||||
|
TotalCrystals = (ulong)_entitlements.EffectiveBalance(viewer, SpendCurrency.Crystal),
|
||||||
|
Rupees = (ulong)_entitlements.EffectiveBalance(viewer, SpendCurrency.Rupee),
|
||||||
|
RedEther = (ulong)_entitlements.EffectiveBalance(viewer, SpendCurrency.RedEther),
|
||||||
|
},
|
||||||
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
||||||
SpotPoint = checked((int)viewer.Currency.SpotPoints),
|
SpotPoint = checked((int)viewer.Currency.SpotPoints),
|
||||||
UserRotationDecks = new UserFormatDeckInfo
|
UserRotationDecks = new UserFormatDeckInfo
|
||||||
@@ -199,13 +198,13 @@ public class LoadController : SVSimController
|
|||||||
},
|
},
|
||||||
UserCards = allCardsAsOwned.Select(card => new UserCard(card)).ToList(),
|
UserCards = allCardsAsOwned.Select(card => new UserCard(card)).ToList(),
|
||||||
UserClasses = viewer.Classes.Select(vc => new UserClass(vc)).ToList(),
|
UserClasses = viewer.Classes.Select(vc => new UserClass(vc)).ToList(),
|
||||||
Sleeves = viewer.Sleeves.Select(s => new SleeveIdentifier { SleeveId = s.Id }).ToList(),
|
Sleeves = cosmetics.SleeveIds.Select(id => new SleeveIdentifier { SleeveId = id }).ToList(),
|
||||||
UserEmblems = viewer.Emblems.Select(e => new EmblemIdentifier { EmblemId = e.Id }).ToList(),
|
UserEmblems = cosmetics.EmblemIds.Select(id => new EmblemIdentifier { EmblemId = id }).ToList(),
|
||||||
UserDegrees = viewer.Degrees.Select(d => new DegreeIdentifier { DegreeId = d.Id }).ToList(),
|
UserDegrees = cosmetics.DegreeIds.Select(id => new DegreeIdentifier { DegreeId = id }).ToList(),
|
||||||
LeaderSkins = allLeaderSkins
|
LeaderSkins = cosmetics.AllLeaderSkins
|
||||||
.Select(skin => new UserLeaderSkin(skin, viewer.LeaderSkins.Any(vs => vs.Id == skin.Id)))
|
.Select(skin => new UserLeaderSkin(skin, cosmetics.OwnedLeaderSkinIds.Contains(skin.Id)))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
MyPageBackgrounds = viewer.MyPageBackgrounds.Select(mpbg => mpbg.Id.ToString()).ToList(),
|
MyPageBackgrounds = cosmetics.MyPageBackgroundIds.Select(id => id.ToString()).ToList(),
|
||||||
LootBoxRegulations = new LootBoxRegulations(),
|
LootBoxRegulations = new LootBoxRegulations(),
|
||||||
GatheringInfo = new GatheringInfo(),
|
GatheringInfo = new GatheringInfo(),
|
||||||
IsBattlePassPeriod = rotation.IsBattlePassPeriod,
|
IsBattlePassPeriod = rotation.IsBattlePassPeriod,
|
||||||
|
|||||||
63
SVSim.UnitTests/Controllers/LoadControllerFreeplayTests.cs
Normal file
63
SVSim.UnitTests/Controllers/LoadControllerFreeplayTests.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.UnitTests.Infrastructure;
|
||||||
|
|
||||||
|
namespace SVSim.UnitTests.Controllers;
|
||||||
|
|
||||||
|
public class LoadControllerFreeplayTests
|
||||||
|
{
|
||||||
|
private static StringContent Body() => new(
|
||||||
|
"""{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"steam","card_master_hash":""}""",
|
||||||
|
Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoadIndex_freeplay_on_inflates_currency_and_grants_all_cards()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
await factory.SeedGlobalsAsync();
|
||||||
|
long viewerId = await factory.SeedViewerAsync();
|
||||||
|
// Seed one collectible card so EffectiveOwnedCardsAsync has at least one entry
|
||||||
|
// (the minimal test set has no CollectionInfo rows — those cards are non-collectible).
|
||||||
|
await factory.SeedOwnedCardAsync(viewerId, 50001001L, count: 1);
|
||||||
|
await factory.EnableFreeplayAsync();
|
||||||
|
|
||||||
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
var resp = await client.PostAsync("/load/index", Body());
|
||||||
|
var json = await resp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK), json);
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// Wire key for UserCurrency is "user_crystal_count" (IndexResponse.[JsonPropertyName("user_crystal_count")])
|
||||||
|
Assert.That(root.GetProperty("user_crystal_count").GetProperty("crystal").GetUInt64(), Is.EqualTo(99999UL));
|
||||||
|
Assert.That(root.GetProperty("user_crystal_count").GetProperty("rupy").GetUInt64(), Is.EqualTo(99999UL));
|
||||||
|
Assert.That(root.GetProperty("user_crystal_count").GetProperty("red_ether").GetUInt64(), Is.EqualTo(99999UL));
|
||||||
|
|
||||||
|
var cards = root.GetProperty("user_card_list");
|
||||||
|
Assert.That(cards.GetArrayLength(), Is.GreaterThan(0));
|
||||||
|
// Wire key for card count is "number" (UserCard.[JsonPropertyName("number")])
|
||||||
|
for (int i = 0; i < cards.GetArrayLength(); i++)
|
||||||
|
Assert.That(cards[i].GetProperty("number").GetInt32(), Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task LoadIndex_freeplay_off_unchanged_baseline()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
await factory.SeedGlobalsAsync();
|
||||||
|
long viewerId = await factory.SeedViewerAsync();
|
||||||
|
|
||||||
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
var resp = await client.PostAsync("/load/index", Body());
|
||||||
|
var json = await resp.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK), json);
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
Assert.That(doc.RootElement.GetProperty("user_crystal_count").GetProperty("crystal").GetUInt64(),
|
||||||
|
Is.Not.EqualTo(99999UL), "freeplay off must not inflate currency");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user