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)