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>
After /tool/signup the client switches to SID-only headers (no UDID), so
the next request's body can't be decrypted unless the server already
knows the SID's UDID. ShadowverseSessionService now mirrors the client's
Cute/Cryptographer.MakeMd5(viewerId + udid) formula (salt
"r!I@ws8e5i="), and ToolController.Signup prestores the mapping at the
end. Verified against a live signup capture: viewerId=1 +
udid=62747917-93bc-454c-abb4-ef423b3c9317 produces the captured SID
dc4aac79d35fe15dfb6262e0071bb03c.
Note: this only fixes the fresh-signup path. Clients restarting with a
cached viewer_id (which skip /tool/signup entirely) still hit the same
issue — separate follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The client's PlayerStaticData.UpdateHaveUserGoodsNumByJsonData does direct
assignment on each reward_list entry's reward_num, so currency/item totals
must be the new viewer balance — not the gift delta. Fresh accounts were
seeing their cached crystal/rupy balances clobbered down to the gift counts
until the next /load/index. Matches the project_wire_reward_list_post_state
memory and the prod capture (which shows 120 rupy = baseline 20 + gift 100).
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>
Stacks a second [HttpPost("/tutorial/pack_info")] absolute route on
PackController.Info so the tutorial flow resolves to the same action
and returns the same pack_config_list as /pack/info (no filtering in v1).
Adds PackControllerTests.cs with TutorialPackInfo_returns_same_list_as_pack_info
verifying byte-identical responses from both URLs.
Co-Authored-By: Claude Sonnet 4.6 <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>
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.
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>
Add two spec-prescribed tests that the implementation plan missed:
- Create_proceeds_when_client_possession_snapshot_disagrees_with_server
- Protect_then_load_index_emits_is_protected_one
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five wire-level integration tests covering: flag set, round-trip unset,
401 without auth, 400 unknown_card, and empty-object response shape.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
POST /tool/signup upserts a Viewer keyed on the resolved request UDID
(via the existing SID->UDID dict). Stashes the viewer on HttpContext so
the translation middleware emits viewer_id/short_udid/udid in
data_headers. Empty data payload -- all signup outputs flow in
data_headers per spec. Idempotent: repeat signups for the same UDID
return the existing viewer.
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>
EnsureCurrentAsync now takes viewerId (was Viewer), so it works with
LoadController's AsNoTracking-loaded detached viewers.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Reads existing state from DB on each call (don't trust navigation
property — caller may pass it stale or double-tracked). Adds via DbSet
only, not via navigation property, to avoid EF double-tracking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Also wires IMissionCatalogRepository + IViewerMissionRepository +
IMissionProgressService into DI. Task 17's separate DI step is now
subsumed by these registrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Npgsql rejects DateTimeOffset writes to timestamp-with-tz unless offset
is zero. Caught by manual bootstrap against a real Postgres DB; SQLite
test provider didn't enforce this. Converting to UTC post-parse is
semantically lossless — DateTimeOffset comparisons are instant-based.
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.
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).
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>
Final cleanup of the bootstrap-seed refactor (Task 10 of 10):
- Delete the GlobalsImporter no-op stub and its two remaining call sites
(Program.cs and SVSimTestFactory.cs). All work has moved to per-domain
importers since Task 9.
- Drop the --captures CLI flag and CapturesDir / shippedCaptures plumbing
from Program.cs (BootstrapOptions, ParseArgs, PrintUsage). Bootstrap input
is now cards.json + reference CSVs + per-table seed JSON; no more
prod-captures directory.
- Shrink ImporterBase from 141 to 23 lines: LoadCapture, Serialize,
Upsert<T,TKey>, GetInt/GetString/GetBool/GetLong/GetULong all had zero
callers after the seed migration. Only ParseWireDateTime survives (still
used by PaymentItemImporter and MyPageGlobalsImporter for prod-shaped
timestamp strings).
- Remove the prod-captures Content Include glob from SVSim.Bootstrap.csproj
and both prod-captures globs (production + test-fixture overlay) from
SVSim.UnitTests.csproj. Test fixtures now overlay production seeds at
Data/seeds/ via the Task 7 layout exclusively.
Build clean; 391/391 unit tests passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>