Commit Graph

135 Commits

Author SHA1 Message Date
gamer147
025c2a8c56 feat(mission): /mission/receive_reward stub (refresh-only, no decomp)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 23:33:50 -04:00
gamer147
e54e63a762 feat(mission): /mission/buy_additional_right stub (refresh-only, no decomp)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 23:29:55 -04:00
gamer147
30b8af2d16 feat(pack): /pack/get_leader_skin_owned_status (empty per-class buckets) 2026-06-12 23:26:29 -04:00
gamer147
c870b95be9 feat(sleeve): /sleeve/favorite accept-only (persistence deferred) 2026-06-12 23:22:25 -04:00
gamer147
f879c05dca feat(sleeve): /sleeve/sleeve_list returns owned ids 2026-06-12 23:17:10 -04:00
gamer147
e2f5786b60 feat(mypage): /mypage/get_special_crystal_info (empty — no offers) 2026-06-12 23:12:49 -04:00
gamer147
43e4c06a2c feat(mypage): /mypage/finish_battle (returns check=0 — no in-flight state yet) 2026-06-12 23:08:42 -04:00
gamer147
b4a87792dc feat(account): /account/update_region_code (accept-only) 2026-06-12 23:01:53 -04:00
gamer147
56ef994275 style(profile): drop file-path header comments from quick-wins task 1 2026-06-12 22:59:35 -04:00
gamer147
398764a4cc feat(profile): /profile/update_official_mark_display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 22:54:52 -04:00
gamer147
37cb8b5eeb test(free-battle): literal-prod-JSON parity for /do_matching 2026-06-12 21:30:19 -04:00
gamer147
55ec279f73 test(free-battle): force_finish no-op returns empty object 2026-06-12 21:29:52 -04:00
gamer147
31ecda95f7 test(free-battle): consistency battle_result echoes on rotation URL 2026-06-12 21:29:26 -04:00
gamer147
523ac605a1 test(free-battle): /finish echoes battle_result with no rank fields 2026-06-12 21:29:02 -04:00
gamer147
487a86fbbb test(free-battle): 3001 when viewer has no deck for format 2026-06-12 21:28:35 -04:00
gamer147
db298a0652 test(free-battle): two-viewer PvP pairing flow 2026-06-12 21:28:00 -04:00
gamer147
4a09b03820 test(free-battle): DoMatching first poll returns 3002 2026-06-12 21:27:33 -04:00
gamer147
b4a279ef0c feat(load-index): emit data.card_master_hash on mismatch (tier-1 freshness gate) 2026-06-12 12:27:40 -04:00
gamer147
d74bae9ba0 feat(card-master): implement POST /immutable_data/card_master serving captured blob 2026-06-12 12:11:15 -04:00
gamer147
f743b27696 feat(ranking): stub /ranking/* (6 endpoints)
Rankings menu opens. Period picker shows deterministic monthly schedule.
Every leaderboard returns { period, ranking: [] }.

Endpoints:
- /ranking/get_viewable_ranking_period_list
- /ranking/master_point_{rotation,unlimited}_info
- /ranking/rank_match_class_win_{rotation,unlimited}_info
- /ranking/two_pick_win_info

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:46:00 -04:00
gamer147
b4aa07577f fix(friend): add BaseRequest body param to 6 body-less actions
ShadowverseTranslationMiddleware throws InvalidOperationException when
a Unity client posts an encrypted msgpack body to an action with zero
[FromBody] parameters — it has no target type for the deserializer.
Tests pass because they post JSON directly with no UnityPlayer UA and
the middleware short-circuits. Same defect already fixed on /replay/info
in 216dcab; this catches up the friend system shipped 2026-06-09.

Fixed actions: info, receive_apply_info, send_apply_info,
played_together_info, reject_apply_all, cancel_apply_all.

Tests updated to post the BaseRequest auth fields so [ApiController]
model validation passes (BaseRequest.ViewerId is non-nullable string).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:54:50 -04:00
gamer147
0996074287 feat(replay): wire arena two-pick finish hook
Same pattern as rank-battle: DoMatching stashes context; Finish takes
it and records history + played-together. Opponent identity is left
as placeholder fields until the resolver carries it through.

Test seeds an active ViewerArenaTwoPickRun so RecordBattleResultAsync
does not throw no_active_run during the e2e flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:22:19 -04:00
gamer147
81aac701f4 feat(replay): wire finish hook for rank-battle family
Finish now consumes the stashed BattleContext, records a
ViewerBattleHistory row (idempotent + retention-capped), and
calls IPlayedTogetherWriter for human PvP (skipped for AI).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:15:12 -04:00
gamer147
b44354315a fix(replay-tests): supply BaseRequest auth fields in PostAsJsonAsync bodies
Following the 216dcab fix that added [FromBody] BaseRequest _ to the
Info action, the existing tests' empty new {} payloads no longer satisfy
[ApiController] model validation (BaseRequest.ViewerId is non-nullable
string). Use the same EmptyBody() shape as RankBattleControllerTests
to mirror the production wire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 08:06:36 -04:00
gamer147
c6259e5a14 feat(replay): add ReplayController for /replay/{info,detail}
/replay/info reads from ReplayHistoryReader, newest-first, capped at 50.
/replay/detail returns 400 with result_code=99 - local cache is the
canonical playback source so this endpoint is cache-miss fallback only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 07:59:51 -04:00
gamer147
2d65fcd91c test(friend): end-to-end multi-viewer apply→approve→friendship flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:14:45 -04:00
gamer147
2d13f0b72d feat(friend): FriendController with 12 endpoints + integration tests 2026-06-09 22:12:00 -04:00
gamer147
c1eec9057a test(gift): claim Card/Sleeve presents; reject unsupported types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:58:53 -04:00
gamer147
742058403c refactor(campaign): delegate gift-reward-type check to GiftRewardTypes
Delete local IsSupportedGiftRewardType and replace its single call site with
GiftRewardTypes.IsSupported — Card (5) and Sleeve (6) are now accepted.
Update unsupported-type test sentinel from 5 (Card) to 11 (SpotCard).
Add Card and Sleeve success-path tests; full suite 1152/1152.
2026-06-09 20:52:47 -04:00
gamer147
b2a5b69423 fix(gift): wire reward_type is UserGoodsType integer, not legacy 1/4/9 encoding
Replace WireRewardTypeToUserGoodsType switch with a validating identity cast backed
by GiftRewardTypes.IsSupported. Wire type 1 is RedEther (UserGoodsType.RedEther),
not Crystal (UserGoodsType.Crystal=2); the old switch silently granted the wrong
wallet for every tutorial-completion claim. Update all 5 GiftControllerTests assertions
and 1 TutorialFlowEndToEndTests assertion to expect RedEther instead of Crystals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:45:49 -04:00
gamer147
0d32d2167b feat(campaign): CampaignController.RegisterSerialCode + DTOs + tests
Adds POST /campaign/regist_serial_code with 9 integration tests covering
success path, all disqualifier conditions (unknown/disabled/expired/future/
already-redeemed/unsupported-reward-type), 401 on missing auth, and case-
sensitivity. IsSupportedGiftRewardType uses gift-wire literals (1/4/9) not
UserGoodsType enum values, matching GiftController.WireRewardTypeToUserGoodsType.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:51:03 -04:00
gamer147
11215bd69f feat(profile): ProfileController + DTOs + integration tests
Add /profile/index endpoint that returns user_rank_match_total_win (stubbed 0)
and user_class_list built from viewer Classes + owned LeaderSkins. Six NUnit
integration tests cover zero wins, all classes present, level/exp/default skin,
leader_skin_id_list population, is_random_leader_skin round-trip, and 401 on
unauthenticated access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:37:35 -04:00
gamer147
b50d69d3a5 test(user-mypage): assert /user_mypage/update returns 401 without auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:04:12 -04:00
gamer147
483cc1c1e0 feat(mypage): /mypage/index reflects persisted bg selection
Wires MyPageController.Index to read Viewer.MyPageBgId/SelectType/BgRotation
instead of emitting an empty MyPageBgSetting placeholder; adds Include for
MyPageBgRotation in ViewerRepository.GetViewerByShortUdid; adds fresh-viewer
zero-defaults test and write→read round-trip test (9/9 controller tests pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:53:34 -04:00
gamer147
6123a64b37 test(user-mypage): explain Clear() + cover unparseable rotation entries
Add a comment above MyPageBgRotation.Clear() explaining the EF OwnsMany
delete-on-clear semantics. Add a 7th test covering garbage/"" entries in
mypage_id_list falling back to BgId=0 while adjacent valid entries are
unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:50:38 -04:00
gamer147
9ff6c70faf feat(user-mypage): UserMyPageController + DTOs + persistence tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:46:49 -04:00
gamer147
f9a971a546 feat(item-acquire-history): controller + DTOs
Add ItemAcquireHistoryController (POST /item_acquire_history/info) with its
three DTOs and two integration tests (ordering + empty-viewer). The endpoint
reads ViewerAcquireHistory rows written by InventoryTransaction.CommitAsync,
ordered newest-first, capped at 300. Tests access doc.RootElement.histories
directly (no envelope wrapper in the test path — middleware skips non-UnityPlayer UA).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:49:43 -04:00
gamer147
7118b92522 refactor(pack): type PackChildGachaEntry.TypeDetail as CardPackType enum 2026-06-09 08:48:16 -04:00
gamer147
57d231cd56 feat(pack): /pack/open supports type_detail=10 FREE_PACKS with per-campaign daily quota 2026-06-08 21:43:04 -04:00
gamer147
6c7e8ae8ad feat(pack): /pack/info filters spent free-pack children and emits campaign metadata 2026-06-08 21:38:26 -04:00
gamer147
feee6e7c09 test(tutorial-e2e): seed tutorial presents — RegisterViewer no longer auto-seeds 2026-06-08 20:51:42 -04:00
gamer147
83e89455e2 test(signup): assert tutorial presents seeded by RegisterAnonymousViewer 2026-06-08 20:46:27 -04:00
gamer147
7a582f310e test(gift): prod-URL coverage and state=3 delete behavior 2026-06-08 20:45:49 -04:00
gamer147
f1d881b26a fix(gift): drop RowVersion (SQLite incompatible) + restore wire reward_type map
[Timestamp] byte[] doesn't work under SQLite (the test backend) — EF
expects the DB to populate it on insert, but SQLite has no equivalent
of Postgres's xmin. The WHERE Status = Unclaimed filter plus
IInventoryService's viewer-level concurrency is the practical defense;
RowVersion was only a backstop. Regenerated the migration without the
RowVersion column.

Wire reward_type on the gift endpoint uses a gift-specific scheme that
diverges from UserGoodsType for currencies: wire 1 = Crystal (enum=2),
wire 9 = Rupy (enum=9), wire 4 = Item (enum=4). A naked cast resolves
wire 1 to UserGoodsType.RedEther and silently grants the wrong wallet
— restored the explicit WireRewardTypeToUserGoodsType map from the old
tutorial controller.

Retrofits existing GiftControllerTests to call SeedTutorialPresentsAsync
on the new helper (RegisterViewer doesn't auto-seed; only the prod
signup path does). All 7 existing tests pass.
2026-06-08 20:44:52 -04:00
gamer147
9d6a5cc3b9 feat(home-dialog): populate home_dialog_list on /mypage/index
Walk-down behavior: each call emits the highest-priority unfired
active dialog; subsequent calls walk to the next-priority entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:55:48 -04:00
gamer147
f21ab7a38c refactor(battle-node): remove ScriptedBotParticipant and dev-affordance wiring
Deletes the scripted opponent and every entry point that created a
BattleType.Scripted session (the ?scripted=1 query opt-in, the
SoloDefaultsToScripted toggle, the resolver short-circuit, the WS handler case,
the bridge validation arm). Real two-client PvP and the Bot matchmaking-timeout
fallback are untouched. ResolveAsync drops its scriptedOptIn parameter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:15:48 -04:00
gamer147
05d8169012 refactor: type reward_type columns as UserGoodsType enum
Replace bare `int RewardType` on 12 catalog/reward entities and GrantedReward
with the existing UserGoodsType enum. Verified against the decompiled client:
every wire reward_type decodes through the single Wizard.UserGoods.Type enum, so
one enum is correct across all endpoint families (item_type is a separate
Item.Type axis, left untouched). EF stores the enum as the same int column, so
there is no migration.

- Importers cast seed int -> UserGoodsType at the ingest boundary.
- New GrantedReward.ToRewardList() extension replaces 8 copy-pasted
  GrantedReward -> RewardListEntry projections.
- Fix 3 .ToString() sites that would otherwise emit enum names ("Crystal")
  instead of the int wire value ("2").
- Wire DTOs keep int; the enum is widened to int at the wire boundary only.

Build green; 962/962 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:50:49 -04:00
gamer147
672a89ed46 refactor(matching): IMatchingResolver shared by every do_matching family
SoloDefaultsToScripted was only consulted by ArenaTwoPickBattleController;
RankBattleController did its own inline pair-up + state-code mapping and
ignored the flag entirely. Result: turning on the flag globally only
short-circuited TK2 polls, while rank-battle polls still parked for the
PvpFirstThenAiFallback threshold (15s) before resolving — surfaced today
when the user set the flag and saw rank-battle still queue, then bot-
battle via the client-side AI (not the server-side Scripted lifecycle we
need to test WS traffic against).

New IMatchingResolver owns the cross-cutting decisions:
- honor scriptedOptIn (per-request) OR options.SoloDefaultsToScripted
  (process-wide) — bypass pair-up, register Scripted, return 3004
- otherwise call IMatchingPairUpService.TryPairAsync and translate the
  PairUpResult to the 3002/3004/3007/3011 vocabulary

Family controllers shed the duplicated logic:
- ArenaTwoPickBattleController: ~50 LOC → ~25; preserves ?scripted=1
  query opt-in (parsed permissively for "1"/"true") and the
  ArenaTwoPickException catch
- RankBattleController: ~30 LOC → ~12; preserves the 3001 mapping for
  InvalidOperationException (no deck for format) and card_master_id
  emission

DoMatchingContractTests is the durable enforcement: parametrized over
TK2 + rotation + unlimited rank, asserts SoloDefaultsToScripted=true
makes every family's first poll skip 3002 and return SUCCEEDED with a
battle_id + node_server_url. Adding a fourth family that forgets to
route through IMatchingResolver fails this test — that's the point.

MatchingResolverTests covers the six resolver paths in isolation with
mocks; per-test Harness locals (not fixture-level fields) because the
assembly is [Parallelizable(ParallelScope.All)] and shared mocks race.

957 tests passing (was 948; +9: 6 resolver + 3 contract parametrizations).
No regressions in the existing TK2 / rank-battle controller suites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:18:48 -04:00
gamer147
898b872edd fix(rank-battle): route ai-start through the queue-time MatchContext
Live-smoke bug 2026-06-02: queued Bloodcraft (deck #5), wire showed
classId=2 (Swordcraft) for self_info on the /ai_unlimited_rank_battle/start
response — client rendered the wrong leader.

Two layers of the same bug:

1. MatchContextBuilder.BuildForRankBattleAsync hardcoded deckNo=1 instead
   of taking it from the do_matching request — verified against
   data_dumps/captures/traffic.ndjson L17 where deck_no=5 was on the wire.
   Signature changes to (viewerId, format, deckNo); DoMatchingInternal
   passes req.DeckNo.

2. AiStartInternal rebuilt MatchContext from scratch — but the /ai_*/start
   request body is BaseRequest only, no deck_no on the wire. The fix uses
   the MatchContext the bridge already stored at do_matching resolution time
   (in the Bot PendingBattle), so deck/cosmetic data is consistent end-to-end.
   New IBattleSessionStore.TryFindPendingForViewer(viewerId) finds the
   viewer's pending battle for lookup. The store entry persists across
   ai_start (idempotent reads are fine — the WS handler removes on connect).
   No-pending sentinel: ai_id=-1 surfaces the "no AI assigned" error in the
   client.

Tests: 936 → 939 passing.
- MatchContextBuilderTests.BuildForRankBattle_uses_the_caller_supplied_deck_number
  seeds deck #1 (class 1) and deck #5 (class 6) and asserts the deckNo
  argument picks the right one.
- RankBattleControllerTests.AiStart_self_info_class_matches_queued_deck_number
  is the end-to-end regression: register Bot battle with deck #5, hit
  /ai_unlimited_rank_battle/start, assert self_info.classId == 6.
- RankBattleControllerTests.AiStart_without_pending_battle_returns_neg1_sentinel
  locks the defensive ai_id=-1 path.
- Existing AiStart_* tests bypass do_matching, so adapted to call a new
  RegisterBotBattleAsync helper that mirrors what InProcessPairUp does on
  AI-fallback resolution.

SeedDeckAsync gains an optional classId so test cases can differentiate
decks by class (was always picking Classes.First()).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 12:28:42 -04:00
gamer147
51e9dd2094 fix(battle-node): Bot mode must push Matched + BattleStart (client state-machine triggers)
Phase 3 shipped a Bot dispatch table that ack'd InitBattle without
pushing Matched and stayed silent on Loaded, per the architecture spec's
inference that "the client uses AIBattleStart HTTP data instead of
Matched in Bot mode." That inference was wrong.

The client's matching state machine (Matching.ReactionReceiveUri,
Matching.cs:400) gates StartBattleLoad() on the Matched envelope, and
BattleStart at Matching.cs:417 triggers GotoBattle. Without those
envelopes the client never transitions out of MatchingStatus.Connect —
which renders as the "Waiting for opponent" hang on the loading screen.
AIBattleStart HTTP only provides opponent cosmetics, not state-machine
triggers.

Fix: drop the Bot-specific InitBattle ack-only and Loaded silent arms;
let Bot fall through to the existing handshake arms that push Matched
and BattleStart + Deal. Only TurnEnd stays Bot-specific (Judge to
sender, not broadcast — there's no real other side to broadcast to).

Tests updated to match the corrected contract. ai-passive.md doc
amended with a correction note.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:56:22 -04:00