Replace RewardGrantService + ICurrencySpendService with IInventoryService tx.
CommitAsync's currency-collision rule replaces the manual Crystal RemoveAll+re-append
scrub in BuyPremiumAsync. AddPointsAsync uses result.Deltas for NewlyClaimed to
preserve per-track visibility (two Rupy grants stay two entries).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace RewardGrantService with IInventoryService tx. Per-reward GrantAsync
calls inside try/catch preserve the NotSupportedException skip; CommitAsync
returns result.RewardList (post-state totals) and accumulated delta list feeds
story_reward_list. Update StoryServiceTests to inject IInventoryService.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace RewardGrantService + HttpContext.RequestServices viewer load with
IInventoryService tx. Single BeginAsync/GrantAsync/CommitAsync wraps all
mission rewards on the win path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace RewardGrantService with IInventoryService tx. EnsureCurrentAsync
still runs before BeginAsync to avoid EF concurrent-context conflicts;
tx.Viewer replaces the manually loaded viewer graph.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace RewardGrantService with IInventoryService tx. GrantAsync returns
post-state totals directly, eliminating the manual ResolvePostStateRewardNum
helper. MissionData loaded via extra include on BeginAsync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Includes Cards/Sleeves/Emblems/LeaderSkins/Degrees/MyPageBackgrounds/Items
under AsSplitQuery, plus caller-supplied extras via InventoryLoadConfig.
Opens a DB transaction and returns an InventoryTransaction shell. All
mutation methods throw NotImplementedException until subsequent tasks
land them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the all-rows-granted reward model with per-group weighted
pick. Each ArenaTwoPickReward row now belongs to a RewardGroup with a
Weight; finish/retire groups the WinCount's rows by RewardGroup and
picks exactly one row per group, weighted by Weight (excluding
Weight==0). A RewardNum==0 outcome skips both the grant and the
rewards[] emission. Empty WinCount catalogs emit empty arrays.
Existing seed entries preserve deterministic behavior by living in
single-option groups (each with weight 1). Future seasons can expand
groups to multi-option for true randomized rewards (e.g. 200-280
rupies).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bare BaseTask call fired from DeckDecisionUI.cs:140 (Arena "View Deck"
path) and the TK2 prep screen. Client task has no Parse() override —
just checks result_code, ignores body. Prod (4 captured instances
across traffic_prod_taketwo_selections + traffic_prod_tradeables_capture)
unanimously responds with data: [].
Routing smoke added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prod /arena/get_challenge_info capture (Season 26):
- reward_step_info.reward_step_list is a Dict<string,string>
({"5":"5","10":"10","15":"15"}), not the List<int> I'd assumed
- max_reward_step is stringified
The previous stub would have parsed at the client (LitJson tolerates the
shape via indexed iteration), but cleaning to match prod exactly.
Also stubs /arena/get_challenge_ranking_history (new endpoint observed
in the same capture). Prod ships {two_pick: [], sealed: []} with no
history populated — empty lists match. Routing smoke added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug 1 ("pay to enter again after restart"):
arena_info[0].is_join shipped from the static ArenaSeasonConfig seed,
so /load/index and /mypage/index always emitted false regardless of
viewer state. The client uses is_join to choose between the "Pay to
enter" and "Resume run" dialogs (Wizard/ChallengeEntry.cs:165 + the
ArenaEntryBase._isJoinFunc pivot). Without a per-viewer override every
cold start after a partial run looked like "no run" and the player got
charged again.
LoadController + MyPageController now compute is_join from
ViewerArenaTwoPickRuns presence. MyPageController grew an
IArenaTwoPickRunRepository dep (LoadController already had _db).
Bug 2: /arena/get_challenge_info 404. Stubbed via a new
ArenaController + DTO pair. Returns the season seed's begin/end_time
+ name where available; placeholder zeros for win history. All 6 keys
required by ChallangeHistoryInfoTask.Parse are present (unconditional
JsonData lookups).
Routing smoke added for /arena/get_challenge_info.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /retire and /finish responses carry two reward arrays with DIFFERENT
key schemas:
rewards[] → ReceivedReward(JsonData) parser
{reward_type, reward_detail_id, item_type, reward_count?, is_usable}
reward_list[] → PlayerStaticData.UpdateHaveUserGoodsNumByJsonData
{reward_type, reward_id, reward_num}
We were emitting both with reward_list's schema, so the client threw
KeyNotFoundException on `data["reward_detail_id"]` while parsing each
delta entry — observed live as the retire-screen failure.
- New TwoPickRewardReceivedDto mirrors the existing Achievement/
TotalReceiveCountDto shape.
- FinishResponseDto.Rewards switched from List<RewardEntryDto>
to List<TwoPickRewardReceivedDto>.
- GrantRunRewardsAndDeleteAsync pre-loads ItemEntry.Type for any
Item-typed reward so item_type ships correctly (0 for currencies).
- Existing tests renamed RewardNum→RewardCount on the deltas list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User clarified: the 0..7 win reward tiers came from Shadowverse Worlds
Beyond (sequel), not the original game we're emulating. Original SV's
Take Two caps at 5 total battles played and has no loss limit (verified
on prod: queueing continues with 2+ losses).
- arena-two-pick-rewards.json: drop 6w + 7w tiers (12 rows remain).
- ArenaTwoPickConfig: remove MaxLosses property.
- ArenaTwoPickService: termination is now battlesPlayed >= maxBattles
(5 from MAX(reward.WinCount)). RecordBattleResult no longer flips
IsSelectCompleted on the 2nd loss.
- ResolveMaxBattleCountAsync empty-catalog default 7 → 5.
- Tests updated for the new counts (16 → 12 rows, max 7 → 5).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MessagePack [Key("...")]-keyed contracts reject unknown fields, so request
DTOs that omit BaseRequest's envelope (viewer_id, steam_id,
steam_session_ticket) fail deserialization on the real msgpack wire path.
Routing smoke + JSON-direct tests didn't catch this because S.T.J. tolerates
extra keys and the routing smoke uses ValidBaseRequestJson, but anything
sent via the actual client encrypted=True path threw
MessagePackSerializationException.
Fix: every Arena*Request now inherits BaseRequest. Also updates the JSON
controller tests + e2e to include the envelope so the [ApiController]
auto-400 validation passes.
Discovered via /arena_colosseum/get_fee_info crash on the in-game arena
screen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the class-selection and card-pick turns for the Take Two arena draft:
- ChooseClassAsync validates class is offered, locks ClassId, generates first pick set via pool
- ChooseCardAsync appends the two picked cards, advances SelectTurn 1–15, completes draft at turn 15
- 6 new tests covering happy paths and all error codes (class_not_offered, invalid_state, invalid_selection)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GetRewardsByWinCountAsync and GetMaxWinCountAsync (short-circuits
to 0 on empty table). Registers as AddTransient in Program.cs alongside
other global catalog repos. 3 NUnit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror of the outer-repo data_dumps/ reorganization (commit e1e595d in
the SVSim outer repo): updates all data_dumps/extract/ → data_dumps/scripts/,
data_dumps/client_master_csv → data_dumps/client-assets, data_dumps/traffic
→ data_dumps/captures/traffic in XML doc-comments and inline comments
across importers, controllers, middlewares, DTOs, and tests. Doc-only;
no logic changes; build green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prod's /pack/get_gacha_point_rewards offers leader cards from packs
where the leader sits in a non-Legendary tier — UCL pack 16015 has
Kyoka (711531010, Runecraft) and Miyako (711331010, Dragoncraft) as
Gold-tier rows with is_leader=1 in the drawrates. The old filter
(Tier == Legendary && !IsAltArt) excluded them, so the in-game
exchange UI was empty despite the banner advertising leader-card draw
rates.
Fix: filter on (Tier == Legendary || IsLeader) && !IsAltArt. Captures
every legendary plus any leader card regardless of page tier. Verified
against the captured 16015 response in
traffic_prod_all_gacha_exchange.ndjson (28 entries: 26 legendaries +
2 Gold-tier leaders).
Across the seeded data this surfaces 6 additional cards: 3 Bronze-tier
leaders + 3 Gold-tier leaders. The 68 Legendary-tier and 81 Special-
tier leaders were already included.
Renames legendaryCardIds -> exchangeableCardIds for clarity.
Regression test seeds a Gold-tier IsLeader=true card with a Skin row
and asserts the exchange catalog returns it with the Skin reward
entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two wire fields differ for seasonal packs (verified against
traffic_prod_all_gacha_exchange.ndjson — every captured request pairs
odds_gacha_id=16xxx with parent_gacha_id=10xxx). The OLD DTO docstring
assumed they were always equal; today's controller used
ParentGachaId, which lands on the base/family pack id (often a
synthesized disabled stub with no GachaPointConfig) and returns [].
Fix:
- GetGachaPointRewards and ExchangeGachaPoint now consume OddsGachaId.
- Update both DTO docstrings to document the seasonal-pack pattern.
- Regression test seeds (16015 enabled w/ GachaPointConfig, 10015
disabled stub w/o config) and asserts the response uses 16015's
catalog.
Symptom: opening pack 16015 (parent_gacha_id=16015 in /pack/open)
accrued gacha points correctly, but /pack/get_gacha_point_rewards with
{odds_gacha_id:16015, parent_gacha_id:10015} returned an empty list.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends /pack/open beyond the v1 CRYSTAL_MULTI=2 / DAILY=3 / RUPY_MULTI=7
trio to cover every type_detail whose payment primitive already exists:
1 CRYSTAL -> ICurrencySpendService crystal debit
6 RUPY -> ICurrencySpendService rupee debit
4 TICKET / 5 TICKET_MULTI -> debit child.ItemId from OwnedItemEntry
(ticketsNeeded = cost * packNumber), 400 on
missing/short balance; reward_list gets a
RewardType=4 post-state Item entry to mirror
project_wire_reward_list_post_state
Skin-overload type_details (8/9/13) and free-pack overlays (10/11/12)
stay 501 — they need selection / banner plumbing the current code
doesn't have.
Tutorial alias unchanged: it still consumes the gating ticket post-draw
and stamps tutorial_step=100. The two ticket flows diverge by intent
(tutorial = free server-grant; normal = paid by ticket inventory).
Removed Open_rejects_ticket_type_detail (asserted the old 501 path);
covered by Open_rejects_insufficient_tickets. Updated
NonTutorial_pack_open_does_not_emit_tutorial_step to assert the new
200-on-ticket-success behavior — same invariant under test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bootstrap Program.cs now calls PackDrawTableImporter after PackImporter.
Delete DbCardPoolProvider, ICardPoolProvider, and the DbCardPoolProvider
tests — the new IPackDrawTableRepository covers what GachaPointService
needed (legendary-tier card_ids per pack) and PackOpenService takes the
draw table directly.
GachaPointService now resolves the legendary catalog from
PackDrawTable.CardWeights filtered by Tier==Legendary, instead of
ICardPoolProvider.GetPool then a rarity filter. Same end set, no DB pool
walk.
Test fallout: tests that fabricate custom card sets for gacha-point
tests now call factory.SeedPackDrawTableFromSetAsync(packId, setId)
to install a matching legendary-tier stub. Full suite: 647/647 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sampler is now driven by PackDrawTable: roll DrawTier per slot by
cumulative slot-rate weights, then pick a card within tier by per-card
weights renormalized within the tier. Rate-less Guaranteed-Leader-Card
rows draw uniform over (pool minus owned), falling back to the full
pool when all are owned. Bonus slot fires once at the end of a 10-pack
open when HasBonusSlot is set.
Slot 8 falls back to the general slot's per-card weights for the rolled
tier when slot-8 has only a rarity-level rate quoted (the common shape
on normal packs).
PackController.Open loads the draw table + viewer owned card ids and
passes them to the sampler; the category-based forced-Legendary slot-8
override is gone. ICardFoilLookup replaces ICardPoolProvider for the
foil-twin heuristic.
Drops the test-fixture pack-draw seed overlay so the production seed
flows through the importer tests; controller tests that fabricate their
own card sets now call factory.SeedPackDrawTableAsync(...) to install a
matching stub draw table.
WeightedPick helper handles the cumulative-band roll for both stages.
Five sampler tests + four WeightedPick tests + five importer/repo
tests; full suite is 653/653 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extracts the foil-twin lookup from ICardPoolProvider into a dedicated
ICardFoilLookup service. PackOpenService takes the lookup as a
parameter; the legacy DbCardPoolProvider stays registered until T12
removes it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aggregate (Config + SlotRates + CardWeights) and a single-pack getter
loaded as one unit per /pack/open. PackOpenService consumes the
aggregate; tests use the production seed (fixture overlay) to validate
shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A real /load/index dump emits my_rotation_id as a bare number (0) for
unset MyRotation slots, which 400'd against the string? DTO field
(AllowReadingFromString only covers string->number). FlexibleStringConverter
accepts either form. Also skip empty deck slots (no cards) on import — a
dump carries every slot, mostly empty placeholders the client manages
itself.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends POST /admin/import_viewer to accept owned_cards (snapshot full-replace).
Unknown card_ids are skipped, counted in skipped_card_count on the response, and
logged server-side. Count is clamped to OwnedCardEntry.MaxCopies (3). Also injects
ILogger into AdminController and adds Cards/Items/Decks to the viewer reload graph.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>