Commit Graph

5 Commits

Author SHA1 Message Date
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
gamer147
bf783639c1 fix(rank-battle): inherit BaseRequest so auth fields survive translation roundtrip
The translation middleware decrypts + msgpack-decodes the request body
into the action's first-parameter type, then re-serializes that DTO to
JSON for the auth handler to read. Phase 3's DoMatchingRequestDto and
RankBattleFinishRequestDto didn't inherit BaseRequest, so viewer_id /
steam_id / steam_session_ticket were dropped during the msgpack → DTO
→ JSON pivot — the auth handler then saw a body with no auth fields
and 401'd every request.

Fixed by making both DTOs extend BaseRequest, mirroring the Phase 2 TK2
DoMatchingRequest pattern.

Also added [FromBody] BaseRequest parameters to the previously body-less
actions (AiStart × 2, ForceFinish, AddClientLog, GetLatestMasterPoint).
The translation middleware explicitly requires at least one parameter
to bind the decrypted msgpack body (see L130-136 of the middleware);
without it the request would throw InvalidOperationException at runtime.

Tests updated to post viewer_id / steam_id / steam_session_ticket
placeholder values in the request body, matching the existing TK2 test
pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 09:29:48 -04:00
gamer147
07eb6f1c05 feat(rank-battle): AiStart returns ai_id + camelCase self/oppo_info
AiStartInternal builds the self MatchContext, picks a bot from
IBotRoster, projects to the AiBattleStartResponseDto with camelCase
wire keys (sleeveId, emblemId, ... — see ai-start.md). turnState=0
(player first) is the safe default per the ai-start.md TODO; live
capture would clarify the enum.

No deck → ai_id=-1 fallback (the documented "no AI assigned" sentinel
per AIBattleStartTask.cs:21). 3 new wire-shape tests assert the
camelCase keys land verbatim in the JSON, plus self/oppo info come from
the right sources.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 01:24:04 -04:00
gamer147
bb63b0df2f feat(rank-battle): real DoMatching with PvP pair + AI fallback mapping
DoMatchingInternal calls IMatchingPairUpService.TryPairAsync, then maps:
- null result → 3002 RETRY (empty node_server_url, no battle_id)
- IsAiFallback → 3011 AI_BATTLE_MATCHING_SUCCEEDED
- IsOwner → 3007 SUCCEEDED_OWNER (cache pickup)
- joiner → 3004 SUCCEEDED

BuildForRankBattleAsync's InvalidOperationException (typically "no deck
for format") surfaces as 3001 ILLEGAL so the client shows the
matchmaking-error dialog rather than retrying.

card_master_id is a placeholder (0) per the per-battle card-master
split deferral. AI-fallback timing is covered by InProcessPairUp unit
tests; controller tests focus on the wire mapping (3002, 3004, 3007).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 01:21:38 -04:00