diff --git a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs index 7b10a6f..01d99ac 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/LoadController.cs @@ -50,12 +50,13 @@ public class LoadController : SVSimController private readonly IBattlePassService _battlePass; private readonly IViewerMissionStateService _missionState; private readonly SVSimDbContext _db; + private readonly IViewerEntitlements _entitlements; public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository, ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository, ICardAcquisitionService acquisition, IGameConfigService config, IBattlePassService battlePass, IViewerMissionStateService missionState, - SVSimDbContext db) + SVSimDbContext db, IViewerEntitlements entitlements) { _viewerRepository = viewerRepository; _cardRepository = cardRepository; @@ -66,6 +67,7 @@ public class LoadController : SVSimController _battlePass = battlePass; _missionState = missionState; _db = db; + _entitlements = entitlements; } [HttpPost("index")] @@ -127,20 +129,11 @@ public class LoadController : SVSimController // * 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; // re-confirm the filter if we later move to Option B and start iterating card-sets. - var defaultCards = await _cardRepository.GetDefaultCards(); - var defaultCardIds = defaultCards.Select(c => c.Id).ToHashSet(); - var ownedCollectibles = viewer.Cards - .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(); + // Owned-card projection (incl. the freeplay "all cards" path) lives in the entitlements + // service so both modes share one definition. + var allCardsAsOwned = await _entitlements.EffectiveOwnedCardsAsync(viewer, ct); - List allLeaderSkins = await _collectionRepository.GetLeaderSkins(); + var cosmetics = await _entitlements.EffectiveCosmeticsAsync(viewer, ct); var classExpCurve = await _globalsRepository.GetClassExpCurve(); List classExps = new(); @@ -179,7 +172,13 @@ public class LoadController : SVSimController { UserTutorial = new UserTutorial { TutorialStep = viewer.MissionData.TutorialState }, 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(), SpotPoint = checked((int)viewer.Currency.SpotPoints), UserRotationDecks = new UserFormatDeckInfo @@ -199,13 +198,13 @@ public class LoadController : SVSimController }, UserCards = allCardsAsOwned.Select(card => new UserCard(card)).ToList(), UserClasses = viewer.Classes.Select(vc => new UserClass(vc)).ToList(), - Sleeves = viewer.Sleeves.Select(s => new SleeveIdentifier { SleeveId = s.Id }).ToList(), - UserEmblems = viewer.Emblems.Select(e => new EmblemIdentifier { EmblemId = e.Id }).ToList(), - UserDegrees = viewer.Degrees.Select(d => new DegreeIdentifier { DegreeId = d.Id }).ToList(), - LeaderSkins = allLeaderSkins - .Select(skin => new UserLeaderSkin(skin, viewer.LeaderSkins.Any(vs => vs.Id == skin.Id))) + Sleeves = cosmetics.SleeveIds.Select(id => new SleeveIdentifier { SleeveId = id }).ToList(), + UserEmblems = cosmetics.EmblemIds.Select(id => new EmblemIdentifier { EmblemId = id }).ToList(), + UserDegrees = cosmetics.DegreeIds.Select(id => new DegreeIdentifier { DegreeId = id }).ToList(), + LeaderSkins = cosmetics.AllLeaderSkins + .Select(skin => new UserLeaderSkin(skin, cosmetics.OwnedLeaderSkinIds.Contains(skin.Id))) .ToList(), - MyPageBackgrounds = viewer.MyPageBackgrounds.Select(mpbg => mpbg.Id.ToString()).ToList(), + MyPageBackgrounds = cosmetics.MyPageBackgroundIds.Select(id => id.ToString()).ToList(), LootBoxRegulations = new LootBoxRegulations(), GatheringInfo = new GatheringInfo(), IsBattlePassPeriod = rotation.IsBattlePassPeriod, diff --git a/SVSim.UnitTests/Controllers/LoadControllerFreeplayTests.cs b/SVSim.UnitTests/Controllers/LoadControllerFreeplayTests.cs new file mode 100644 index 0000000..2d7afe4 --- /dev/null +++ b/SVSim.UnitTests/Controllers/LoadControllerFreeplayTests.cs @@ -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"); + } +}