Commit Graph

49 Commits

Author SHA1 Message Date
gamer147
b47ec3b64d feat(pack): /pack/exchange_gacha_point endpoint
Wires IGachaPointService.TryExchangeAsync into a controller endpoint.
Loads the viewer with the full cosmetic-grant graph (Cards, Sleeves,
Emblems, Degrees, LeaderSkins, MyPageBackgrounds) plus the gacha-point
balance + received marker, with AsSplitQuery to avoid the cartesian
explosion documented in project_ef_split_query. Returns BadRequest with
the outcome's error code on validation failure, or the post-state
reward_list on success.

Two integration tests: happy-path verifies card grant + balance debit +
received-marker persistence + post-state reward_list shape; insufficient-
balance path returns 400.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:57:39 -04:00
gamer147
9e7b7eed27 feat(pack): /pack/get_gacha_point_rewards endpoint 2026-05-28 23:49:08 -04:00
gamer147
b18bb9502a fix(pack): /pack/info reads ItemId via shadow FK, not nav property
PackController.Info's ownedItemsByItemId projection used `i.Item.Id` to
key the dict — EF translates that to the FK column today, but any future
model change that breaks the nav→column mapping would fall back to client
eval and collapse every key to 0 (the default Item constructor's Id),
silently hiding every tutorial pack via item_number=0. EF.Property<int>
reads the shadow FK directly and is robust to nav changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:15:40 -04:00
gamer147
91412ff821 fix(account): /account/update_name validates name input
Reject empty / whitespace / explicit-null / over-cap names with 400
instead of NREing on null assignment or storing arbitrarily-long
strings the DB column has no cap on. 24-char limit is a conservative
backstop against direct API abuse; the client UI enforces its own
keyboard limit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:11:27 -04:00
gamer147
d13082a8ca fix(gift): receive response is idempotent and echoes persisted state
Three coupled correctness fixes to /tutorial/gift_receive's response:

- received_ids / total_receive_count_list / reward_list are now built
  from `toClaim` (the gifts THIS call granted), not from `requestedIds`.
  Echoing the client's request meant idempotent re-claims re-fired the
  "+N received" popup and direct-assigned the same post-state totals
  again, breaking the documented idempotency contract.
- is_unreceived_present is now `unclaimedPresents.Count > 0`. The
  hardcoded false hid the inbox badge after partial claims even when
  present_list still carried unclaimed gifts.
- tutorial_step echoes the persisted (max-preserved) state instead of
  a hardcoded 41. A replay against a state>=41 viewer used to surface
  41 on the wire and regress the client's tutorial state machine.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:10:06 -04:00
gamer147
86759125a9 fix(gift): tutorial gift_receive ThenIncludes OwnedItemEntry.Item
Same project_ef_nav_include_pitfall as 27ebb51's tutorial pack_open fix
but in the gift path: without .ThenInclude(i => i.Item), the existing
OwnedItemEntry's Item nav defaults to a new ItemEntry() (Id=0), so
RewardGrantService.ApplyAsync's `FirstOrDefault(i => i.Item.Id == detailId)`
misses pre-existing rows. It falls through to add a new entry, and the
(ViewerId, ItemId) unique index added 2026-05-25 throws on SaveChanges →
500 to the client, no tutorial advancement, no currency grant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:08:19 -04:00
gamer147
82d9668c9b fix(pack): /tutorial/pack_open restricted to starter pack + pre-END viewer
The tutorial alias bypassed the currency / type_detail / open-count guards
unconditionally. Combined with the unconditional TutorialState=100 write, any
authenticated viewer could send /tutorial/pack_open with any parent_gacha_id
to draw a pack for free and clobber their state down to 100.

Two gates: parent_gacha_id MUST be 99047 (the legendary starter), and the
viewer's TutorialState MUST be below 100. The state write is also max-preserved
as a belt-and-braces backstop. Mirrors the 31→41 guard in GiftController.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:31:23 -04:00
gamer147
6fd8705990 fix(tutorial): /tutorial/update preserves max TutorialState
The endpoint used to write the client-supplied step verbatim, so a stale or
replayed request with tutorial_step=0 against any later-stage viewer would
regress the persisted state to 0. NextSceneSwitcher routes step==0 to
AreaSelect section 0, which has no chapter data — the client LINQ-Single()
crashes on next /load/index, bricking the viewer. Math.Max-preserve matches
the 31→41 pattern in GiftController.TutorialGiftReceive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:22:59 -04:00
gamer147
ac077dfc13 fix(pack): tutorial pack_open ThenIncludes OwnedItemEntry.Item
Without .ThenInclude(i => i.Item), the OwnedItemEntry.Item nav defaults to a
new ItemEntry() with Id=0 (project_ef_nav_include_pitfall), so the
FirstOrDefault(i => i.Item.Id == ticketItemId) lookup never matched. The
ticket was never decremented and reward_list omitted the post-state entry —
on the next /tutorial/pack_info the pack stayed visible and the client
re-clicked into plain /pack/open, which 501s on type_detail=5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 20:15:31 -04:00
gamer147
ad5c9e91ae feat(download_time): stub start/end endpoints
Spec at docs/api-spec/endpoints/post-login/download_time-{start,end}.md
already documented both endpoints fully against the decompiled
Wizard/DownloadStartTask.cs and Wizard/DownloadFinishTask.cs — the
controller side was the gap.

The client fires /download_time/start before kicking off an Akamai
asset bundle download and /download_time/end on completion. Both are
pure telemetry from our perspective. When NukeIdentityOnStartup wipes
PlayerPrefs broadly (the pre-narrow loader behaviour), the client
decides it needs to download tutorial assets, calls /download_time/start,
and a 404 there surfaces as an HTTP error popup before the download
proceeds. Stubbing with empty data:{} bodies plus result_code:1 is the
documented minimum-viable response.

Acts as belt-and-suspenders against the narrow IdentityWipe (which
preserves the cache index so downloads shouldn't trigger) ever being
bypassed by a different code path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 18:04:28 -04:00
gamer147
190b50cbaf fix(tutorial): gift_receive reward_list carries post-state totals, not deltas
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).
2026-05-28 13:16:51 -04:00
gamer147
5d8a6626bb test(tutorial): add end-to-end capture-replay smoke
Adds TutorialFlowEndToEndTests covering the full fresh-signup flow:
account/update_name → tutorial/update (11→21→31) → update_action →
gift_top → gift_receive (verifies 400 crystal + 100 rupy delta and
31→41 state advance) → pack_info → pack_open (verifies tutorial_step=100
and viewer state END). Includes the set-90001 card-pool seed required by
the tutorial pack resolver (mirrors PackControllerTests pattern).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:00:18 -04:00
gamer147
6819e65160 feat(tutorial): /tutorial/pack_open emits tutorial_step=100, advances viewer state
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>
2026-05-28 12:55:08 -04:00
gamer147
ca678b56d1 feat(tutorial): alias /tutorial/pack_info to /pack/info
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>
2026-05-28 12:27:35 -04:00
gamer147
f6f9216162 feat(tutorial): add /tutorial/gift_receive — grant + receipt + idempotent re-claim
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 12:22:37 -04:00
gamer147
2034034c1b feat(tutorial): add /tutorial/gift_top with hardcoded starter present list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 12:02:54 -04:00
gamer147
0f6b3f231a feat(account): add /account/update_name endpoint
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>
2026-05-28 11:56:59 -04:00
gamer147
bc9ffe1d31 feat(tutorial): add /tutorial/update — echo step + persist to viewer
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>
2026-05-28 11:47:09 -04:00
gamer147
703f7ff3d7 feat(tutorial): add /tutorial/update_action fire-and-forget endpoint
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>
2026-05-28 11:41:11 -04:00
gamer147
5aac24d2b9 feat(deck-builder): /deck_code mint + /deck resolve with 3-min in-memory TTL
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>
2026-05-28 09:11:21 -04:00
gamer147
71b0c66631 test(card): snapshot-mismatch + protect-load round-trip
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>
2026-05-28 02:21:57 -04:00
gamer147
433408dddb test(card): /card/protect controller integration
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>
2026-05-28 02:05:28 -04:00
gamer147
bac10b91ff test(card): /card/create controller integration
Adds CreateBody helper and 15 test runs (7 methods + 8 parametrized
cases) covering happy path, 401 unauthenticated, malformed inner JSON,
empty object, unknown card, not_craftable, would_exceed_max_copies,
and insufficient_vials error paths for POST /card/create.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 01:38:42 -04:00
gamer147
39b38e3c80 Battlepass fix 2026-05-28 00:54:46 -04:00
gamer147
7ef5f03eb3 feat(spot-card-exchange): /spot_card_exchange/{top,exchange} + SpotPoints currency
Final shop family. Schema additions:
- ViewerCurrency.SpotPoints (ulong) — new currency column on Viewers.
- SpotCardExchangeEntry — catalog (distinct from the pre-existing
  SpotCardEntry, which is the /load/index rental-cost concept).
- ViewerSpotCardExchange — standalone composite-PK table tracking
  (viewer, card, exchanged_at, is_pre_release_snapshot). Standalone
  avoids cartesian-explode on viewer-graph reads.

RewardGrantService gains a SpotCardPoint=12 currency case mirroring
the RedEther/Crystal pattern. Doc comment refreshed; SpotCard=11 and
SpotCardOnlyLatestCardPack=13 remain unimplemented with explanatory
NotSupportedException — captures show emitters always use Card=5 with
the spot-card-specific id.

Controller:
- /top: emits exactly 9 clan buckets [{"1": [cards]}, ...] matching
  prod's arbitrary single-key shape. exchange_status per-card (0=
  available, 1=already-exchanged, 2=LimitOver after pre-release cap).
  pre_relase_info WIRE TYPO PRESERVED ("relase" not "release").
- /exchange: server-authoritative price (client-supplied
  exchange_point ignored); debits SpotPoints with post-state-total
  reward_list entry; grants card via RewardGrantService.ApplyAsync
  (cosmetic cascade included); persists ViewerSpotCardExchange row.
  Insufficient points / already-exchanged / pre-release-limit all
  return 400 without partial state.

LoadController now populates /load/index spot_point from
viewer.Currency.SpotPoints (was always 0).

PreReleaseLimit hardcoded to 2 matching capture; promote to GameConfig
when captures show variance.

504 tests pass (was 496; +8 spot-card-exchange tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:23:07 -04:00
gamer147
a5999a3e9c feat(leader-skin): shop catalog + 5 endpoints (/products, /buy, /buy_set, /buy_set_item, /ids)
Schema: LeaderSkinShopSeries -> Products (owned rewards) + owned
SetCompletionRewards on the series; ViewerLeaderSkinSetClaim composite
PK (ViewerId, SeriesId) backs the /buy_set_item idempotent-claim check.

Importer mirrors SleeveShopImporter: idempotent find-or-create, owned
collections rewritten wholesale on rerun. 16 series, 104 products.

Controller (extends existing /set with 5 new endpoints):
- /products: dict-keyed-by-series_id-string wire shape. is_completed
  per-viewer, rewards.status from ViewerLeaderSkinSetClaim (0=no set
  sale, 1=available, 2=claimed) matching client RewardStatus enum.
- /buy: single skin, sales_type 1/2 dispatch, 3=>501.
- /buy_set: whole series at SetPrice; requires set_sales_status != 0;
  grants every product's rewards (RewardGrantService idempotent on
  already-owned cosmetics, so partial-set buys don't double-add).
- /buy_set_item: requires viewer owns every skin in series; idempotent
  on re-claim (returns 200 + empty reward_list, not 400) so client
  retries don't error.
- /ids: flat owned-skin-id list for badge refresh.

496 tests pass (was 486; +10 leader-skin-shop tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:55:09 -04:00
gamer147
559a170957 feat(item-purchase): /item_purchase/{info,purchase} + catalog
Schema: ItemPurchaseCatalogEntry (single table). Per-viewer quota tracked
via existing ViewerEventCounter keyed by "item_purchase:<id>" with period
JstPeriod.MonthKey when IsMonthlyReset else AllTime.

Controller:
- /info returns catalog + per-period rest (server-computed
  max(0, PurchaseLimit - counter)) + user_card_pack_ticket_list (every
  Items.Type==2 row joined to viewer count, zeros included — client
  unconditionally UpdateItemNum's each entry).
- /purchase: sold_out check before currency check (no counter increment
  on currency failure), inline TryDebit covers RedEther/Crystal/Rupy/Item
  with post-state-total reward_list entry, grant via RewardGrantService.
  Request `rest` accepted but ignored (server counter is canonical).

Importer mirrors PaymentItemImporter shape — idempotent find-or-create,
seed-missing rows preserved. 3 entries from the prod capture.

486 tests pass (was 476; +10 item_purchase tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:41:02 -04:00
gamer147
f237851e42 feat(sleeve): shop catalog + /sleeve/{info,buy} endpoints
Schema: SleeveShopSeries -> SleeveShopProducts -> Rewards (owned).
Migration AddSleeveShop creates 3 tables with FK cascade.

Importer mirrors BuildDeck pattern: find-or-create per series/product,
rewards replaced wholesale on rerun (owned collection). 10 series,
270 products imported from seeds/sleeve-shop.json.

Controller:
- /sleeve/info returns wire-faithful dict-keyed shape
  ({sleeve_list: {<series_id>: {product_info: {<product_id>: ...}}}}).
  is_purchased_product derived from viewer.Sleeves.Contains(sleeve_id).
- /sleeve/buy: sales_type 0=free / 1=crystal / 2=rupy / 3=ticket(501).
  Validates series_product mismatch, currency, already-purchased.
  Currency debited with post-state-total reward_list entry; cosmetic
  grants dispatched through RewardGrantService.ApplyAsync (covers
  sleeve + emblem bundled grants per product).

476 tests pass (was 466; +10 sleeve tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:09:45 -04:00
gamer147
68367db214 feat(tool/signup): anonymous viewer creation keyed on UDID
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>
2026-05-27 14:24:55 -04:00
gamer147
640a77ec6c feat(achievements): /achievement/receive_reward — RewardGrantService + level advance + cap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:41:49 -04:00
gamer147
b65a437102 feat(missions): /mission/info, /mission/retire, /mission/change_receive_setting
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>
2026-05-27 10:35:40 -04:00
gamer147
2cb8c271a8 feat(bp): /battle_pass/buy — crystal-cost + retroactive premium grants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:36:18 -04:00
gamer147
0ceab721e9 feat(bp): /battle_pass/item_list — derives product per active season
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.
2026-05-26 23:26:46 -04:00
gamer147
8a35f8c40b feat(bp): /battle_pass/info — service + controller + 3 tests
Also fixes BattlePassRepository.GetActiveSeasonAsync to use client-side
DateTimeOffset filtering (SQLite provider cannot translate DateTimeOffset
comparisons in LINQ WHERE/ORDER BY clauses).
2026-05-26 23:14:26 -04:00
gamer147
9bec1df52f feat(load-index): populate battle_pass_level_info from IBattlePassService
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>
2026-05-26 22:56:41 -04:00
gamer147
141f34f817 chore(bootstrap): refresh stale GlobalsImporter references in docs/test names 2026-05-26 16:44:54 -04:00
gamer147
a71bf6c62b refactor(bootstrap): migrate /pack/info to seed file 2026-05-26 15:02:49 -04:00
gamer147
b6966ece6e Prebuilt deck purchasing and fixes 2026-05-26 09:16:21 -04:00
gamer147
558e8288eb Puzzles 2026-05-25 12:03:47 -04:00
gamer147
c14408ba06 Seeding reorg 2026-05-24 21:13:15 -04:00
gamer147
34bcc579a5 Additional card content 2026-05-24 17:07:05 -04:00
gamer147
12fb2f4801 Card liquefication 2026-05-24 14:42:44 -04:00
gamer147
79209bd70b Pack opening 2026-05-24 02:03:13 -04:00
gamer147
bdff142d16 Practice/deck editing mostly there 2026-05-24 00:17:28 -04:00
gamer147
21b97269ff Practice battles work 2026-05-23 22:46:11 -04:00
gamer147
66184b3685 Things were working, suddenly regressed 2026-05-23 18:14:42 -04:00
gamer147
5f44ee0c7e Getting ready to seed more data 2026-05-23 15:47:23 -04:00
gamer147
631e42289a Need to fix index load issues 2026-05-23 14:50:16 -04:00
gamer147
bf6ddf5428 Forgot unversioned xd 2026-05-23 14:18:18 -04:00