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>
LastPlayTime and MissionChangeTime were typed as DateTime, which STJ
serialised as "0001-01-01T00:00:00.0000000Z" for a fresh viewer
(DateTime.MinValue). Prod's wire shape is "yyyy-MM-dd HH:mm:ss"
(no T, no Z, no fractional seconds) when present and null when
absent — verified against data_dumps/traffic_prod_tutorial.ndjson.
The .NET default format has a real chance of crashing the client's
DateTime.Parse path on any code that reads either field, and the
fields are presence-sensitive (NetworkTask-family Keys.Contains
followed by ToDateTime), so emitting the .NET default reaches the
client as a stale-but-present value.
Switching the properties to string? + FormatProdDateTime helper:
- non-default DateTime -> "yyyy-MM-dd HH:mm:ss"
- DateTime.MinValue -> null (omitted from wire via global
WhenWritingNull policy in Program.cs)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A wiped/fresh client (NukeIdentityOnStartup, new install, or any path
that clears PlayerPrefs) defaults its stored RES_VER to "00000000"
per Cute/SavedataManager.GetResourceVersion. The client builds the
Akamai manifest URL as dl/Manifest/<RES_VER>/<lang>/<Platform>/, and
Akamai 404s the "00000000" path -> Toolbox.AssetManager.InitializeManifest
fails -> the title screen shows "Connection Error / Reconnect"
before any tutorial UI loads.
Fix:
- New ResourceConfig [ConfigSection] in SVSim.Database — single
field RequiredResVer defaulting to "4670rPsPMVlRTd2" (the value
prod returned in data_dumps/traffic_prod_tutorial.ndjson and was
still returning at 2026-05-28 21:00 UTC). Lives in GameConfigs so
it can be tuned via DB / appsettings without code edits.
- ShadowverseTranslationMiddleware injects IGameConfigService and
emits required_res_ver in data_headers ONLY on /check/game_start
responses. NetworkTask.Parse opens a "new data is available" popup
whenever required_res_ver is present and the URL is anything other
than GameStartCheck (NetworkTask.cs:128-138); the suppression on
game_start is what lets us silently bump PlayerPrefs["RES_VER"]
before ResourceDownloader runs.
- DataHeaders gains a nullable RequiredResVer field. DataWrapper.DataHeaders
is now Dictionary<string, object?> instead of the typed DataHeaders POCO
directly — the construction site stays type-safe (the middleware builds
the typed POCO, then projects through the same STJ +
ConvertJsonTreeToPlainObject pipeline that DataWrapper.Data uses) so
null-valued optional fields are absent from the wire instead of being
written as "key":null. Without this, MessagePack's ContractlessStandardResolver
walked the typed properties and wrote required_res_ver=null on every
non-game_start response, tripping the popup on every boot.
- GameConfigurationJsonbTests updated to expect the 9th config section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stack [HttpPost("/tutorial/pack_open")] alias on PackController.Open. Detect
isTutorialPath via HttpContext.Request.Path; gate the type_detail rejection,
currency switch, open-count tracking, and currency reward_list entries behind
!isTutorialPath so the starter legendary pack (99047/990047, type_detail=5)
bypasses the purchasable-pack code path. After grant, set MissionData.TutorialState=100
and emit tutorial_step=100 in PackOpenResponse — this is the sole END transition,
per live-traffic capture. Add pack 99047 to test-fixture packs.json.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements POST /account/update_name — writes Viewer.DisplayName and
returns an empty array per the prod capture. Includes TDD test covering
the persist side-effect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /tutorial/update echoes tutorial_step back and saves it to
Viewer.MissionData.TutorialState. is_skip=1 is handled server-side
by honoring whatever tutorial_step value the client sends (client
already sends 100 when skipping). Adds TutorialUpdateRequest DTO,
TutorialUpdateResponse DTO, injects SVSimDbContext into
TutorialController, and adds GetViewerTutorialStateAsync helper to
SVSimTestFactory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns an empty data object (result_code=1 from middleware envelope).
Client uses SkipAllNetworkChecks so the response body is never read.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client's LitJson serializer emits the C# property name verbatim — the
SetParameter param classes in Wizard/GenerateDeckCodeTask.cs use cardID /
phantomCardID, and the matching Parse() reads jsonData["cardID"]. Snake-case
keys bound to empty in msgpack deserialize, the controller saw 0 cards, and
returned INVALID_DECK — surfaced as a blank deck code in the in-game UI.
Repro lived in data_dumps/traffic.ndjson #19-20. Existing tests pass through
the same JsonPropertyName on both serialize and deserialize, so they happily
round-tripped any consistent key — adding a wire-shape regression test that
posts the literal client JSON would be the right way to catch this class of
bug in the future (out of scope here).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the portal pair (shadowverse-portal.com deck-builder endpoints) as
anonymous routes on the app server. The translation middleware learns a new
[NoWireEncryption] attribute that skips both AES calls but keeps the rest of
the msgpack + base64 + envelope pipeline intact, matching prod's portal wire
profile observed in data_dumps/traffic_prod_deckcode.ndjson.
Storage is a 3-minute IMemoryCache — codes are anonymous-global, 4-char
lowercase alphanumeric (matches the shortest prod sample). Foil bit is
stripped on mint to match prod's normalize-on-encode behaviour.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Request mirrors LoginPostParams (device telemetry); response is empty
because all signup outputs live in data_headers (viewer_id, short_udid,
udid). MessagePackObject + Key mirrors JsonPropertyName per project
convention.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SignUpTask.Parse validates data_headers.udid against Certification.Udid;
mismatch discards the response. Sourced from the same mappedUdid the
translation middleware uses to decrypt — never controller state. Other
endpoints carry the extra key; SignUpTask is the only reader.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three endpoints + 9 integration tests. Captured-data-is-catalog: viewer's
achievement Level starts at MIN(Level) per type from the catalog (not 1),
so the assembler always has a row to render against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds BattlePassSalesPeriodInfoDto, BattlePassProductDto, BattlePassItemListResponse DTOs,
GetItemListAsync on BattlePassService (one product if not premium + CanPurchase, empty if
already premium or off-season), and the /battle_pass/item_list controller action.
2 new integration tests; all 408 pass.
- IndexResponse.BattlePassLevelInfo widened to IReadOnlyDictionary<string,BattlePassLevel>?
so any IReadOnlyDictionary impl (FrozenDictionary, wrapper, etc.) serializes correctly
instead of silently null-ing via a failed as-cast
- LoadController.Index now takes CancellationToken ct and threads it to GetLevelCurveAsync
instead of CancellationToken.None
- BattlePassRepository.ResetLevelCurveCache changed from public to internal; added
InternalsVisibleTo("SVSim.UnitTests") to SVSim.Database.csproj (was absent)