Four targeted fixes that together let /tutorial/pack_info display
the legendary starter at index 0, let /tutorial/pack_open succeed
on it, and let the pack drop out of the shop after.
1. /pack/info now loads viewer.Items into a Dictionary<long,int>
and threads it through ToDto so child_gacha_info.item_number
reflects the viewer's actual owned count of item_id. Previously
defaulted to 0 for every pack, so the legendary pack 99047
reported item_number=0 immediately after the gift granted 1×
ticket id=90001. Verified against the prod tutorial capture.
2. PackRepository.GetActivePacks now orders parent_gacha_id DESC
to match prod's /pack/info wire order (99047, 92001, 80047,
16015...10001). The tutorial pack UI runs with controls locked
and auto-selects index 0 via GachaUI.GetCurrentLegendPackId
(FirstOrDefault on IsLegendPackId), so the legendary starter
needs to be the first legend pack in the list.
3. DbCardPoolProvider.GetPool falls back to all in-rotation cards
when a LegendCardPack's base set has no rows. Pack 99047's
base_pack_id is 90001, a synthetic "Throwback Rotation" category
that doesn't correspond to a real card_set in the prod card
master — its real pool is curated across older rotation sets
(Altersphere through Colosseum). We don't have that membership
map captured yet; the rotation fallback is broader than prod
but produces a valid 8-card draw, which is what the tutorial
needs to advance to step 100. TODO in code points at the
real fix.
4. PackController.Open's tutorial path now consumes the granted
ticket (decrement viewer.Items by packNumber for child.ItemId)
and emits the post-state count in reward_list as
{reward_type:4, reward_id:item_id, reward_num:post_count}.
Without this, the pack stayed at item_number=1 forever, the
shop kept showing it post-tutorial, and the next click hit
/pack/open (not /tutorial/pack_open) which 501s on type_detail=5.
Also: docstring on PackConfigDto.SalesPeriodInfo flags the deferred
wire-fidelity fix (prod emits {"sales_period_time": "<complete_date>"}
for limited windows, [] for evergreens; we always emit {}) and the
retype from Dictionary<string,string?> to a typed
PackSalesPeriodInfoDto. Doesn't affect tutorial flow, deferred for
the pack-system rework.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
4.5 KiB
C#
122 lines
4.5 KiB
C#
using MessagePack;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
|
|
|
[MessagePackObject]
|
|
public class PackConfigDto
|
|
{
|
|
[JsonPropertyName("parent_gacha_id")]
|
|
[Key("parent_gacha_id")]
|
|
public int ParentGachaId { get; set; }
|
|
|
|
[JsonPropertyName("base_pack_id")]
|
|
[Key("base_pack_id")]
|
|
public int BasePackId { get; set; }
|
|
|
|
[JsonPropertyName("override_draw_effect_pack_id")]
|
|
[Key("override_draw_effect_pack_id")]
|
|
public int OverrideDrawEffectPackId { get; set; }
|
|
|
|
[JsonPropertyName("override_ui_effect_pack_id")]
|
|
[Key("override_ui_effect_pack_id")]
|
|
public int OverrideUiEffectPackId { get; set; }
|
|
|
|
[JsonPropertyName("gacha_type")]
|
|
[Key("gacha_type")]
|
|
public int GachaType { get; set; }
|
|
|
|
[JsonPropertyName("sleeve_id")]
|
|
[Key("sleeve_id")]
|
|
public int SleeveId { get; set; } = 3000011;
|
|
|
|
[JsonPropertyName("special_sleeve_id")]
|
|
[Key("special_sleeve_id")]
|
|
public int SpecialSleeveId { get; set; }
|
|
|
|
[JsonPropertyName("commence_date")]
|
|
[Key("commence_date")]
|
|
public string CommenceDate { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("complete_date")]
|
|
[Key("complete_date")]
|
|
public string CompleteDate { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("cardpack_banner_list")]
|
|
[Key("cardpack_banner_list")]
|
|
public List<PackBannerDto> CardpackBannerList { get; set; } = new();
|
|
|
|
[JsonPropertyName("gacha_detail")]
|
|
[Key("gacha_detail")]
|
|
public string GachaDetail { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("child_gacha_info")]
|
|
[Key("child_gacha_info")]
|
|
public List<PackChildGachaDto> ChildGachaInfo { get; set; } = new();
|
|
|
|
[JsonPropertyName("open_count")]
|
|
[Key("open_count")]
|
|
public int OpenCount { get; set; }
|
|
|
|
[JsonPropertyName("open_count_limit")]
|
|
[Key("open_count_limit")]
|
|
public int OpenCountLimit { get; set; }
|
|
|
|
[JsonPropertyName("is_hide")]
|
|
[Key("is_hide")]
|
|
public int IsHide { get; set; }
|
|
|
|
[JsonPropertyName("pack_category")]
|
|
[Key("pack_category")]
|
|
public int PackCategory { get; set; }
|
|
|
|
/// <summary>
|
|
/// Null when the pack has no gacha-point participation. The key MUST be present on the wire
|
|
/// (explicit null) — client at PackInfoTask.cs:126 does <c>if (jsonData2["gacha_point"] != null)</c>,
|
|
/// a direct LitJson key access that throws KeyNotFoundException when the key is absent
|
|
/// (only protects against null *value*, not missing *key*). Override the global
|
|
/// WhenWritingNull per [[project_wire_null_policy]] memory.
|
|
/// </summary>
|
|
[JsonPropertyName("gacha_point")]
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
|
[Key("gacha_point")]
|
|
public PackGachaPointDto? GachaPoint { get; set; }
|
|
|
|
[JsonPropertyName("is_pre_release")]
|
|
[Key("is_pre_release")]
|
|
public bool IsPreRelease { get; set; }
|
|
|
|
[JsonPropertyName("exists_purchase_reward")]
|
|
[Key("exists_purchase_reward")]
|
|
public bool ExistsPurchaseReward { get; set; }
|
|
|
|
[JsonPropertyName("is_new")]
|
|
[Key("is_new")]
|
|
public bool IsNew { get; set; }
|
|
|
|
/// <summary>
|
|
/// Prod sends an object <c>{"sales_period_time":"..."}</c> when set and an array <c>[]</c>
|
|
/// when unset. v1 always emits an empty object when the field is null on the entity —
|
|
/// matches the active-window case and the client tolerates both shapes via
|
|
/// <c>ShopExpirtyInfo</c>'s LitJson parser. Revisit if a capture proves otherwise.
|
|
///
|
|
/// TODO(2026-05-28): the prod tutorial capture has each active pack with
|
|
/// <c>"sales_period_info": {"sales_period_time": "<complete_date>"}</c> — i.e., the
|
|
/// pack's <c>complete_date</c> echoed inside the object. Our controller emits <c>{}</c>
|
|
/// which the client tolerates (the tutorial flow doesn't filter on this field), but for
|
|
/// wire fidelity we should populate it from <c>PackConfigEntry.CompleteDate</c>. While
|
|
/// doing that, also retype this field from <c>Dictionary<string, string?></c> to a
|
|
/// typed <c>PackSalesPeriodInfoDto { string SalesPeriodTime }</c> — the current dict
|
|
/// shape is the lazy-key anti-pattern documented in
|
|
/// <c>feedback_no_lazy_response_dicts</c>. Deferred from the tutorial-bringup pass
|
|
/// because it doesn't gate any observable flow.
|
|
/// </summary>
|
|
[JsonPropertyName("sales_period_info")]
|
|
[Key("sales_period_info")]
|
|
public Dictionary<string, string?> SalesPeriodInfo { get; set; } = new();
|
|
|
|
[JsonPropertyName("poster_type")]
|
|
[Key("poster_type")]
|
|
public int PosterType { get; set; }
|
|
}
|