Verified against Wizard.Title/UserNameInput.cs:30 in the 2026-05-23
decompile:
IsFinished = !string.IsNullOrEmpty(PlayerStaticData.UserName);
Any non-empty seeded value — including the prior " - " placeholder
this method was passing — sets IsFinished=true on the first frame and
silently skips both the input dialog and the /tutorial/update_action #1
+ /account/update_name calls that travel with it. The in-source comment
described the opposite behavior; empty string is what actually triggers
the dialog.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BuildDefaultViewer hardcoded TutorialState=1 — correct for fresh anonymous
signups (RegisterAnonymousViewer) but wrong for AdminController.ImportViewer
and Steam-social signups, which both go through RegisterViewer and expect a
prod-replica viewer that boots to the home screen. Add an initialTutorialState
parameter (default 1 preserves RegisterAnonymousViewer behavior); RegisterViewer
passes 100.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
Three corrections to BuildDefaultViewer + RegisterAnonymousViewer
verified against data_dumps/traffic_prod_tutorial.ndjson:
- TutorialState now 1 (TUTORIAL_STEP0), not 0 (PRE_TUTORIAL_STEP).
Wizard.Title.NextSceneSwitcher routes step==1 to the Prologue scene;
any other non-{31,41,100} step routes to AreaSelect at section 0,
which has no chapter data and crashes the client with a LINQ
Single() "Sequence contains no matching element" from
AreaSelectUI.SelectChapter. Prod's first /check/game_start returns
now_tutorial_step="1"; step 0 is a pre-existence state we never
want to expose on the wire.
- DisplayName " - " (literal space-dash-space), not "Player".
Wizard.Title.UserNameInput.Start short-circuits with
IsFinished=true on !string.IsNullOrEmpty(PlayerStaticData.UserName),
silently skipping the name dialog AND the tutorial sub-step it
drives (/tutorial/update_action #1 + /account/update_name). Prod
uses " - " as the unset placeholder.
- viewer.Info.SelectedEmblem/SelectedDegree assigned from the default
emblem/degree the loadout grants. Without this, /load/index emits
selected_emblem_id=0 and selected_degree_id=0 for a fresh viewer
that owns those cosmetics — prod sends the real granted IDs.
Also surfaces the actual Postgres constraint name in the unique-
violation re-raise (ExtractConstraintName), instead of always
saying "UDID" — the original message was misleading whenever the
real constraint was on owned-collection rows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously RegisterAnonymousViewer auto-completed the tutorial, which
prevented the client from ever entering the tutorial flow. SeedViewerAsync
gains a tutorialState parameter (default 100) so existing tests keep
their pre-completed-tutorial assumption.
Implements ICardInventoryRepository.SetProtected — loads only the
owned-cards collection (no decks/currency), guards against the EF
owned-nav Card.Id==0 default-init quirk, and accepts Count=0 rows
(destruct→re-protect round-trip). Covered by 4 new NUnit tests
(flip, unset, zero-count-row, unknown-card error). Full suite: 533/533.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ItemEntry gains Type (client item_type enum, 1=challenge, 2=card-pack
ticket, 3=premium orb, 4=colosseum, 5=orb piece, 6=skin/event ticket,
7=other) and ThumbnailPath. ItemImporter mirrors PaymentItemImporter
shape: find-or-create per item_id, save once, idempotent. Wired into
Bootstrap.Program and SVSimTestFactory.SeedGlobalsAsync. Unblocks
/item_purchase/info (filters card-pack tickets by Type==2) and any
reward grant of UserGoodsType.Item, which previously threw because
the catalog was empty.
466 tests pass (was 461).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
(1) RegisterAnonymousViewer now catches the unique-violation
race (SQLSTATE 23505 on Postgres / code 19 on SQLite) and
re-reads by UDID, returning the existing row instead of
surfacing 500 to the second concurrent /tool/signup caller.
New repo test exercises the back-to-back register path.
(2) Add unique index on SocialAccountConnection (AccountType,
AccountId). The auth handler's find-or-link path claimed
this index existed as the dedup backstop; the claim was
accurate as design intent but the schema was missing. Now
matched. Comment in handler updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review polish on the prior commit (30874c6):
- BuildDefaultViewer extract dropped the "filter out Id=0 placeholders
and dedupe" comment from the leader-skin grant block — restored.
- LinkSteamToViewer test now calls link twice and asserts count stays
at 1, exercising the alreadyLinked short-circuit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extracts the default-loadout body into a private BuildDefaultViewer
helper shared by the existing Steam-import path and a new
RegisterAnonymousViewer for /tool/signup. LinkSteamToViewer is the
seam SteamSessionAuthenticationHandler will call on first-Steam-touch
of a UDID-keyed viewer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Backstop for /tool/signup idempotency: signup-created viewers carry
the client's UDID (the AES key for that client's wire traffic);
admin-imported viewers stay null. Partial unique index allows the
column to coexist with pre-existing null rows.
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>
GetOrCreateProgressAsync now persists the new row itself and catches
DbUpdateException on unique-constraint violations — concurrent /info
calls no longer throw 500s. BattlePassService no longer calls
SaveChangesAsync after the get-or-create. FormatWireDate uses a named
JstOffset constant instead of an inline TimeSpan.FromHours(9).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).
- 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)
Wire IBattlePassService.GetLevelCurveAsync into LoadController so /load/index
emits the 100-entry battle_pass_level_info dict when levels are seeded.
Also adds BattlePassRepository.ResetLevelCurveCache() to bust the process-level
static cache in tests that seed levels after earlier HTTP calls have primed it
with an empty list, and updates SVSimTestFactory.SeedGlobalsAsync + the stale
Index_surfaces_seeded_globals_after_bootstrap assertion accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ValueGeneratedOnAdd to ViewerBattlePassProgress.Id and
ViewerBattlePassClaims.Id so Postgres generates IDENTITY values at
runtime. Regenerate AddBattlePass migration in-place to include the
IdentityByDefaultColumn annotations. Add IBattlePassRepository /
BattlePassRepository (season lookup + level-curve cache) and
IViewerBattlePassRepository / ViewerBattlePassRepository
(get-or-create progress, claim reads/writes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds DbSets and OnModelCreating config for BattlePassSeasonEntry,
BattlePassRewardEntry, ViewerBattlePassProgressEntry, and
ViewerBattlePassClaimEntry; generates migration 20260527021011_AddBattlePass
with DDL-only CreateTable + CreateIndex calls and no InsertData.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stage 9C of the bootstrap-seed-refactor:
- Add 6 seed DTOs for the card-id-keyed load-index tables (SpotCard,
ReprintedCard, UnlimitedRestriction, LoadingExclusionCard, MaintenanceCard,
FeatureMaintenance).
- Add CardListsImporter: idempotent upsert of the 6 tables, sharing one
Cards FK set for orphan-warning. FeatureMaintenances clear-and-rewrites
(synthetic ordinal Id; no natural key).
- Add RotationFlagUpdater: reads RotationConfig.RotationCardSetIds from the
GameConfigs section (populated by RotationConfigImporter) and flips
CardSet.IsInRotation to match.
- Add RotationConfig.RotationCardSetIds list property + wire it through
RotationConfigImporter. No migration needed (sections are JSON blobs).
- RotationConfigImporter: use legacy local-kind DateTime parse for schedule
windows so the JSON round-trip stays byte-equivalent to GlobalsImporter.
- Strip GlobalsImporter down to a no-op stub (Task 10 will delete it).
- Wire all 9 new importers into Program.cs and SVSimTestFactory.SeedGlobalsAsync,
in the order RotationConfigImporter -> ... -> CardListsImporter -> RotationFlagUpdater.
- Delete prod-captures/load-index-2026-05-23.json.
- Add CardListsImporterTests covering each sub-table, idempotency,
empty-seed handling, orphan-warning, and the clear-and-rewrite path.
Tests: 391 passing (382 baseline + 9 new).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>