Commit Graph

289 Commits

Author SHA1 Message Date
gamer147
0a2eddd920 feat(battle-node): port AES-256-CBC encryptForNode/decryptForNode codec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:26:05 -04:00
gamer147
50790a706c feat(battle-node): scaffold SVSim.BattleNode class library 2026-05-31 21:21:14 -04:00
gamer147
dd231b081d Merge branch 'inventory-service'
InventoryService consolidation: replaces RewardGrantService,
CurrencySpendService, ViewerEntitlements, and CardAcquisitionService
with a single scoped-transaction facade IInventoryService.

- BeginAsync loads viewer with canonical inventory graph + extras
- TrySpendAsync/TryDebitAsync/GrantAsync queue ops; CommitAsync saves
- Result carries RewardList (post-state, currency-collision-resolved)
  + Deltas (verbatim queued) for distinct wire fields
- Freeplay logic folded into the tx surface
- 14 callers ported (Load, BuildDeck, Pack, LeaderSkin, Sleeve,
  ItemPurchase, SpotCardExchange, Gift, Achievement, Puzzle, Story,
  BattlePass, ArenaTwoPick, GachaPoint); CardInventoryRepository.Create
  ported, Destruct deferred
- 8 old service files + 4 test files deleted
- 713/713 tests pass

Spec: docs/superpowers/specs/2026-05-31-inventory-service-design.md
Plan: docs/superpowers/plans/2026-05-31-inventory-service.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 19:25:43 -04:00
gamer147
a033bf361a fix(battle-pass): remove redundant SaveChanges after CommitAsync
CommitAsync's inner SaveChangesAsync already flushes the AddClaim
rows + progress.IsPremium mutation alongside the inventory grants
(same scoped DbContext). The trailing _db.SaveChangesAsync was a
no-op in BuyPremium and only meaningful in AddPoints when no level
crossed (no tx opened) — restructured to an else branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 18:48:26 -04:00
gamer147
2ee40c6df7 test(inventory): wire-shape regression for spend+grant+cascade
Serializes result.RewardList with snake_case+WhenWritingNull options and
asserts the three entries come out in expected first-touch order:
Crystal post-state (500), Card post-state count (3), Sleeve cascade (1).
Also verifies snake_case key names are actually emitted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 17:12:16 -04:00
gamer147
2c62a7be80 refactor(inventory): delete old primitives after InventoryService cutover
Removed RewardGrantService, CurrencySpendService, ICurrencySpendService,
ViewerEntitlements, IViewerEntitlements, CardAcquisitionService,
ICardAcquisitionService, CardGrantResult and their tests
(RewardGrantServiceTests, CurrencySpendServiceTests,
CardAcquisitionServiceTests, ViewerEntitlementsTests). Removed four DI
registrations from Program.cs. No caller references any deleted type;
GrantedReward and EffectiveCosmetics were pre-moved to InventoryGrantTypes.cs
in the prior commit. Build clean, 712/712 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 17:07:30 -04:00
gamer147
df0e132459 refactor(inventory): move GrantedReward + EffectiveCosmetics into Inventory namespace folder
Both types stay in namespace SVSim.Database.Services so existing using directives
in controllers, services, and tests resolve without change. Their definitions are
extracted to SVSim.Database/Services/Inventory/InventoryGrantTypes.cs; the empty
husks in RewardGrantService.cs and IViewerEntitlements.cs will be deleted in the
next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 17:03:06 -04:00
gamer147
c37c04c1b7 refactor(gacha-point): route TryExchangeAsync through IInventoryTransaction
Change signature from (Viewer, packId, cardId) to (IInventoryTransaction, packId, cardId).
Drop RewardGrantService from GachaPointService ctor. PackController.ExchangeGachaPoint opens
tx with GachaPointBalances/Received extra includes, passes tx, commits on success.
Update GachaPointServiceTests to use inv.BeginAsync + tx pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:55:08 -04:00
gamer147
b6bf9b7495 refactor(arena-two-pick): route entry/finish through InventoryService
Replace RewardGrantService + ICurrencySpendService + IViewerEntitlements with
IInventoryService. tx.IsFreeplay replaces FakeEntitlements.IsFreeplay; debit
helpers take IInventoryTransaction. ComputePostStateRewardList deleted (replaced
by result.RewardList from CommitAsync). Update 5 test files to new 8-arg ctor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:51:03 -04:00
gamer147
26bc4fe2ab refactor(battle-pass): route BuyPremiumAsync and AddPointsAsync through InventoryService
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>
2026-05-31 16:46:13 -04:00
gamer147
7c4bc2966f refactor(story): route FinishAsync rewards through InventoryService
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>
2026-05-31 16:42:38 -04:00
gamer147
a310697830 refactor(puzzle): route finish rewards through InventoryService
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>
2026-05-31 16:40:16 -04:00
gamer147
4ba7d8f6d0 refactor(achievement): route receive_reward through InventoryService
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>
2026-05-31 16:39:07 -04:00
gamer147
369edd4537 refactor(gift): route tutorial gift_receive through InventoryService
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>
2026-05-31 16:37:53 -04:00
gamer147
a2cec7c99e refactor(spot-card-exchange): route through InventoryService
Replace RewardGrantService + ICurrencySpendService with IInventoryService
tx pattern. BeginAsync loads viewer, TrySpendAsync debits SpotPoint,
GrantAsync grants card + cascade, CommitAsync saves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:35:53 -04:00
gamer147
ad4d4e0646 refactor(item-purchase): route through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:26:34 -04:00
gamer147
9436a0d21b refactor(sleeve): route buy through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:25:10 -04:00
gamer147
45fa3d75bf refactor(leader-skin): route shop through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:23:50 -04:00
gamer147
4d6da23443 refactor(pack): route Open through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:20:23 -04:00
gamer147
57dd524d9f refactor(build-deck): route Buy through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:17:31 -04:00
gamer147
61013fcf5c refactor(card-inventory): route Create/Destruct through InventoryService
RedEther debit now goes through tx.TrySpendAsync (freeplay-aware);
Card grants route through tx.GrantAsync (cosmetic cascade for first-time
owners). Validation phase unchanged. DestructCards left on direct-viewer
path (structural mismatch: validation on one viewer, mutation on same
instance — clean tx port deferred to follow-up).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:15:40 -04:00
gamer147
1113e52f94 refactor(load): switch to InventoryService for entitlements
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:12:27 -04:00
gamer147
91909c5755 feat(inventory): read-side methods on IInventoryService + tx
EffectiveBalance/OwnsCard/OwnsCosmetic on the tx are freeplay-aware
against the live viewer. EffectiveOwnedCardsAsync/EffectiveCosmeticsAsync
on the service mirror today's ViewerEntitlements projections (used by
/load/index).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:05:35 -04:00
gamer147
ea340cde21 test(inventory): lifecycle — dispose rollback + use-after-commit
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:03:06 -04:00
gamer147
b0b9901c42 feat(inventory): CommitAsync + currency-collision rule
Last post-state per currency wins; non-currency grants collapse to final
count per (type, id). Deltas are verbatim queued, no cascade. SaveChanges
+ DB tx commit happen atomically inside Commit; failure leaves rollback
to DisposeAsync. CS0649 warning on _committed is now resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:02:39 -04:00
gamer147
1ba3f57709 feat(inventory): BackfillCardCosmeticsAsync
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:01:15 -04:00
gamer147
46d8239d5a feat(inventory): TryDebitAsync dispatches currencies + Item
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 16:00:24 -04:00
gamer147
301da9eeca feat(inventory): TrySpendAsync covers all 4 wallets + freeplay
Crystal/Rupy/RedEther freeplay no-op (returns configured amount,
balance unchanged); SpotPoint always real. Insufficient returns
current balance; success returns post-deduction balance.
SVSimTestFactory gains freeplayEnabled ctor overload that upserts
the Freeplay GameConfigSection row after EnsureSeedData.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:59:26 -04:00
gamer147
a821b7f6b4 feat(inventory): GrantAsync handles Card + cosmetic cascade
Card grants produce a post-state-total entry and run the CardCosmeticReward
cascade (foil twin → id-1 lookup). Cascade additions are skipped when the
viewer already owns the cosmetic; missing-master-row failures are logged
and dropped without failing the parent grant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:54:36 -04:00
gamer147
1f3f81d878 feat(inventory): GrantAsync handles Item branch
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:52:42 -04:00
gamer147
a1cf1d7519 feat(inventory): GrantAsync handles cosmetic branches
Sleeve/Emblem/Skin/Degree/MyPageBG grants are idempotent on the viewer's
owned-collection but always emit a wire entry at the top level (preserves
"+1 sleeve" purchase popup). Unknown ids throw InventoryCatalogException.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:51:46 -04:00
gamer147
3bc38b407b feat(inventory): GrantAsync handles currency branches
Crystal/Rupy/RedEther/SpotCardPoint grants mutate ViewerCurrency in place
and emit post-state-total wire entries. Op log records the post-state for
later currency-collision resolution in CommitAsync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:50:27 -04:00
gamer147
02e86cf16c feat(inventory): BeginAsync loads viewer with canonical graph
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>
2026-05-31 15:46:20 -04:00
gamer147
b181257aaa docs(inventory): XML docs for TrySpend/TryDebit/EffectiveBalance
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:42:41 -04:00
gamer147
220e5699cd feat(inventory): scaffold InventoryService namespace types
Empty interfaces + records for IInventoryService, IInventoryTransaction,
InventoryCommitResult, InventoryLoadConfig, InventoryCatalogException.
Implementation lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:38:51 -04:00
gamer147
fc504af496 feat(tk2): weighted-group reward picking
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>
2026-05-31 13:44:33 -04:00
gamer147
8e017c9d10 feat(check): stub /check/check_time_slip_card_master_hash
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>
2026-05-31 13:29:27 -04:00
gamer147
ac2f31103d fix(arena): match prod get_challenge_info wire shape; stub ranking_history
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>
2026-05-31 13:21:44 -04:00
gamer147
1af56b4ec4 fix(tk2): per-viewer is_join in arena_info + stub /arena/get_challenge_info
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>
2026-05-31 13:13:11 -04:00
gamer147
1e2e18e828 fix(tk2): rewards array uses ReceivedReward shape (reward_detail_id/item_type/is_usable)
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>
2026-05-31 12:56:05 -04:00
gamer147
6381e4da51 fix(tk2): match original SV (5-battle cap, no loss limit)
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>
2026-05-31 12:47:43 -04:00
gamer147
dc19289818 fix(tk2): honor consume_item_type (ticket/crystal/rupy/free) + correct entry ticket id
- ArenaTwoPickConfig: add TicketItemId=1, TicketCost=1, CrystalCost=150, RupyCost=150 scalars
- ArenaTwoPickService.EntryAsync: switch on eARENA_PAY (1/3/4/5); crystal/rupy go through
  ICurrencySpendService.TrySpendAsync; ticket uses item id 1 (challenge ticket, not 80001);
  free entry returns empty reward_list; invalid type throws
- Tests: fix ticket id 80001→1 in entry/e2e; add 4 new path tests; update ctor (10th arg)
  across all 4 service test files; fix e2e retire assertion (reward ticket 80001 post-state=1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:26:57 -04:00
gamer147
668779e8a4 fix(http): inherit BaseRequest on all TK2 + Colosseum request DTOs
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>
2026-05-31 12:06:50 -04:00
gamer147
f8ca4a0ae9 feat(http): stub /arena_colosseum/get_fee_info (is_colosseum_period:false) 2026-05-31 11:58:18 -04:00
gamer147
98fb3c5fcd fix(svc): default MaxBattleCount=7 with warn-log on empty reward catalog 2026-05-31 11:41:57 -04:00
gamer147
2aa0bdefec test(tk2): routing smoke + end-to-end draft→retire
Adds 8 TestCase entries to Authenticated_route_resolves for all
arena_two_pick and arena_two_pick_battle endpoints, and a full
integration test exercising entry → class_choose → 15×card_choose →
retire, verifying seed reward grants (1 ticket + 100 rupies).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 11:37:47 -04:00
gamer147
65e0e0fb09 test(config): update section count from 10 → 11 (ArenaTwoPick) 2026-05-31 11:27:56 -04:00
gamer147
09b8c49743 feat(http): ArenaTwoPickBattleController (do_matching stub + finish)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:27:02 -04:00
gamer147
f272690a31 feat(http): ArenaTwoPickController (6 actions)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:15:53 -04:00
gamer147
e245d5b158 feat(svc): Retire + Finish + RecordBattleResult
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 11:10:41 -04:00