From 190b50cbaf71a4ef07525cfc44fe763a82193e6d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 28 May 2026 13:16:51 -0400 Subject: [PATCH] fix(tutorial): gift_receive reward_list carries post-state totals, not deltas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client's PlayerStaticData.UpdateHaveUserGoodsNumByJsonData does direct assignment on each reward_list entry's reward_num, so currency/item totals must be the new viewer balance — not the gift delta. Fresh accounts were seeing their cached crystal/rupy balances clobbered down to the gift counts until the next /load/index. Matches the project_wire_reward_list_post_state memory and the prod capture (which shows 120 rupy = baseline 20 + gift 100). --- .../Controllers/GiftController.cs | 30 ++++++++++++++++++- .../Controllers/GiftControllerTests.cs | 11 +++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/GiftController.cs b/SVSim.EmulatedEntrypoint/Controllers/GiftController.cs index 8828900..9d97a99 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/GiftController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/GiftController.cs @@ -154,19 +154,47 @@ public class GiftController : SVSimController .Select(p => Clone(p, nowString)) .ToList(), IsUnreceivedPresent = false, + // reward_list entries must carry POST-STATE TOTALS, not gift deltas. + // The client's PlayerStaticData.UpdateHaveUserGoodsNumByJsonData does direct + // assignment on each entry's reward_num — emitting the delta would clobber + // the client-side cached balance down to the gift amount until the next /load/index. + // See project memory: project_wire_reward_list_post_state. RewardList = TutorialGifts .Where(p => requestedIds.Contains(p.PresentId)) .Select(p => new GiftRewardListEntry { RewardType = p.RewardType, RewardId = p.RewardDetailId, - RewardNum = p.RewardCount, + RewardNum = ResolvePostStateRewardNum(p, viewer), }) .ToList(), TutorialStep = 41, }; } + /// + /// Returns the post-grant viewer balance for the given gift entry, not the gift delta. + /// reward_list on wire carries post-state totals (client does direct assignment). + /// + private static string ResolvePostStateRewardNum(PresentDto gift, SVSim.Database.Models.Viewer viewer) + { + switch (gift.RewardType) + { + case "1": // Crystal + return ((long)viewer.Currency.Crystals).ToString(System.Globalization.CultureInfo.InvariantCulture); + case "9": // Rupy + return ((long)viewer.Currency.Rupees).ToString(System.Globalization.CultureInfo.InvariantCulture); + case "4": // Item + { + int itemId = int.Parse(gift.RewardDetailId, System.Globalization.CultureInfo.InvariantCulture); + var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == itemId); + return ((long)(owned?.Count ?? 0)).ToString(System.Globalization.CultureInfo.InvariantCulture); + } + default: + return gift.RewardCount; // unknown type — fall back to gift count (better than 0) + } + } + private static UserGoodsType WireRewardTypeToUserGoodsType(int wireType) => wireType switch { 1 => UserGoodsType.Crystal, diff --git a/SVSim.UnitTests/Controllers/GiftControllerTests.cs b/SVSim.UnitTests/Controllers/GiftControllerTests.cs index 8aaa52d..1e9f543 100644 --- a/SVSim.UnitTests/Controllers/GiftControllerTests.cs +++ b/SVSim.UnitTests/Controllers/GiftControllerTests.cs @@ -81,6 +81,17 @@ public class GiftControllerTests var post = await factory.GetViewerCurrencyAsync(viewerId); Assert.That(post.Crystals - pre.Crystals, Is.EqualTo(400UL)); Assert.That(post.Rupees - pre.Rupees, Is.EqualTo(100UL)); + + // reward_list carries post-state TOTALS, not deltas, per project_wire_reward_list_post_state. + // After claiming gifts, the crystal/rupy entries in reward_list should equal viewer's post-grant totals. + var rewardList = root.GetProperty("reward_list").EnumerateArray().ToList(); + var crystalEntry = rewardList.First(e => e.GetProperty("reward_type").GetString() == "1"); + var rupyEntry = rewardList.First(e => e.GetProperty("reward_type").GetString() == "9"); + Assert.That(crystalEntry.GetProperty("reward_num").GetString(), + Is.EqualTo(post.Crystals.ToString()), + "reward_list currency entries must carry POST-STATE TOTALS, not gift deltas (client does direct assignment)."); + Assert.That(rupyEntry.GetProperty("reward_num").GetString(), + Is.EqualTo(post.Rupees.ToString())); } [Test]