fix(mypage): populate user_item_list from viewer.Items

MyPageTask.Parse (Wizard/MyPageTask.cs:155-163) does
`_userItemDict.Clear();` the moment `user_item_list` is present in
the response body — not when it's non-empty — then re-populates
from the wire. Our /mypage/index was emitting [] by default (the
field initializer on the DTO), which wiped the inventory that
/load/index had just populated.

Downstream consequence: the client's PackChildGachaInfo.CostGoodsCount
reads from _userItemDict, so a wiped dict makes every ticket-cost
pack report CostGoodsCount=0, PackConfig.EnableBuyPack returns false,
and is_hide=1 packs (including the tutorial legendary starter 99047)
disappear from the rotation pack list — even though the gift bundle
just granted the ticket and the DB row exists. The tutorial then
auto-selects whatever non-tutorial pack happens to be at index 0 of
the filtered list, the user can't afford it, and the flow is stuck.

Fix:

- MyPageController.Index now sets UserItemList from viewer.Items
  (already loaded by GetViewerByShortUdid's home-screen graph).
- DTO docstring rewritten to call out the presence-sensitive semantics
  and the load-bearing path through PackConfig.EnableBuyPack, so the
  next developer doesn't get the "empty is fine" hint the old comment
  implied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-28 18:03:50 -04:00
parent d3ef76324f
commit b5e33c15f6
2 changed files with 15 additions and 2 deletions

View File

@@ -100,6 +100,13 @@ public class MyPageController : SVSimController
},
BasicPuzzle = new Models.Dtos.Common.BadgeFlag { IsDisplayBadge = false }, // TODO(mypage-stub): viewer practice-puzzle progress
IsBattlePassPeriod = rotation.IsBattlePassPeriod,
// The client's MyPageTask.Parse (line 155-163) does `_userItemDict.Clear();` whenever
// user_item_list is present in the response — not when it's non-empty — and then
// repopulates from the wire. Emitting [] here wipes the inventory the client populated
// from /load/index, which makes PackChildGachaInfo.CostGoodsCount return 0 and filters
// out is_hide=1 tutorial packs (the legendary starter 99047) via PackConfig.EnableBuyPack.
// Populate from viewer.Items so the client's dict stays in sync with the DB.
UserItemList = viewer.Items.Select(i => new UserItem(i)).ToList(),
SpecialCrystalInfo = new(), // TODO(mypage-stub): same shape/source as /load/index
// CompetitionInfo, ShopNotification, StoryNotification, GuildNotification, GatheringInfo,
// IsHiddenBossAppeared, SubBanner/SubBannerList/HomeDialogList/UserOfflineEvent/UserItemList,

View File

@@ -267,8 +267,14 @@ public class MyPageIndexResponse
// ── Per-viewer / event state ───────────────────────────────────────────
/// <summary>
/// Updated item counts. Empty list = "no items to update" (client iterates 0 times, no UI change).
/// Per-viewer state — populate from viewer.Items when that wiring lands.
/// Full snapshot of the viewer's owned items — NOT a delta. The client's
/// <c>MyPageTask.Parse</c> (line 155-163) clears <c>_userItemDict</c> the moment it sees
/// this key, then re-populates from the wire list. Emitting <c>[]</c> wipes whatever
/// /load/index populated, breaking any client logic that reads from the dict — most
/// load-bearingly <c>PackChildGachaInfo.CostGoodsCount</c>, which gates tutorial-pack
/// visibility via <c>PackConfig.EnableBuyPack</c>. Controllers MUST populate the full
/// owned-items snapshot from <c>viewer.Items</c>; an empty list is correct only when the
/// viewer genuinely owns nothing.
/// </summary>
[JsonPropertyName("user_item_list")]
[Key("user_item_list")]