fix(battle-node): revert Bot Matched/BattleStart push (corrupts OppoBattleStartInfo)
Previous commit 51e9dd2 changed Bot's InitBattle to push Matched and
Loaded to push BattleStart+Deal, on the theory that the architecture
spec's "no Matched in Bot mode" claim was wrong. That theory was based
on misreading Matching.cs:400 (the Matched handler) as a required
state-machine trigger.
End-to-end trace of the AI client flow shows:
1. _initNetworkSuccess (set when the client receives uri=InitNetwork,
i.e., our ack) is the actual trigger — MatchingNetworkConnectChecker
phase 3 sees it and calls MatchingInitBattle.
2. MatchingInitBattle (Matching.cs:298) for IsAINetwork IMMEDIATELY
calls StartBattleLoad + GotoBattle right after emitting InitBattle.
It does NOT wait for any wire envelope.
3. The Matched handler at Matching.cs:400 is gated on
status == Connect and is already past Prepared by the time the
wire round-trip completes — sending Matched is harmless but
unnecessary.
4. The BattleStart handler at Matching.cs:417 runs UNCONDITIONALLY and
SetNetworkInfo at RealTimeNetworkAgent.cs:1562 overwrites
OppoBattleStartInfo with the wire envelope's oppoInfo. Our oppoInfo
comes from NoOpBotParticipant.Context placeholders (classId/emblemId
etc. = 0), corrupting the good values the client set from the HTTP
AIBattleStart response.
The "Waiting for opponent" hang was caused by SBattleLoad.LoadOpponentAssets
trying to fetch emblemId=0, degreeId=0, etc. after BattleStart corrupted
OppoBattleStartInfo. The asset group load silently hangs on missing
assets, no error logged.
Restored the spec's original Bot arms: InitBattle ack-only, Loaded silent,
TurnEnd Judge-to-sender. ai-passive.md updated with the corrected reasoning
and a discovery-history note.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -135,19 +135,45 @@ public sealed class BattleSession
|
||||
phaseFrom!.Phase = BattleSessionPhase.AwaitingInitBattle;
|
||||
break;
|
||||
|
||||
// --- Phase 3 Bot arms — placed BEFORE the existing handshake arms so the
|
||||
// Bot-specific TurnEnd wins pattern matching on Type == Bot. InitBattle and
|
||||
// Loaded fall through to the regular handshake arms below (which push Matched
|
||||
// and BattleStart+Deal) — the architecture spec's "no Matched in Bot mode"
|
||||
// claim was wrong: the client's Matching.ReactionReceiveUri (Matching.cs:400)
|
||||
// gates StartBattleLoad on receiving Matched, and BattleStart triggers
|
||||
// GotoBattle. Without those envelopes the client hangs in matching status
|
||||
// Connect ("Waiting for opponent"). AIBattleStart HTTP only provides
|
||||
// opponent cosmetics, not the state-machine triggers.
|
||||
// --- Phase 3 Bot arms — placed BEFORE the existing handshake arms so they
|
||||
// win pattern matching on Type == Bot. Bot mode: ack handshake, silent
|
||||
// Loaded, Judge-to-sender on TurnEnd. The rest reuse Scripted's arms
|
||||
// (Retire/Kill → BattleFinishNoContest, Swap → per-sender response,
|
||||
// default → drop). Reference: docs/api-spec/in-battle/ai-passive.md.
|
||||
//
|
||||
// Bot's Swap arm reuses the existing Scripted Swap arm (per-sender
|
||||
// SwapResponse + Ready), and Retire/Kill reuses BattleFinishNoContest.
|
||||
// Reference: docs/api-spec/in-battle/ai-passive.md.
|
||||
// Critically, do NOT push Matched or BattleStart for Bot mode. The
|
||||
// architecture spec was right about this:
|
||||
// 1. The client's MatchingInitBattle (Matching.cs:298) immediately calls
|
||||
// StartBattleLoad + GotoBattle on the IsAINetwork branch right after
|
||||
// emitting InitBattle — it does NOT wait for a wire Matched or
|
||||
// BattleStart envelope. The state-machine trigger is _initNetworkSuccess
|
||||
// (set when InitNetwork uri is received, i.e., our ack).
|
||||
// 2. Sending Matched is harmless (gated on status == Connect, which is
|
||||
// already past by the time the wire round-trip completes).
|
||||
// 3. Sending BattleStart is ACTIVELY HARMFUL: its handler at
|
||||
// Matching.cs:417 runs unconditionally and SetNetworkInfo
|
||||
// (RealTimeNetworkAgent.cs:1553-1564) overwrites OppoBattleStartInfo
|
||||
// with the wire envelope's oppoInfo. Our oppoInfo comes from
|
||||
// NoOpBotParticipant.Context placeholders (classId:0, emblemId:0,
|
||||
// etc.), corrupting the good values the client just set from the
|
||||
// HTTP /ai_<fmt>_rank_battle/start response — subsequent asset
|
||||
// loads (LoadOpponentAssets at SBattleLoad.cs:933) then look up
|
||||
// non-existent assets and silently hang on "Waiting for opponent."
|
||||
|
||||
case NetworkBattleUri.InitBattle
|
||||
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
|
||||
// Ack only — NO Matched push.
|
||||
result.Add((from, BuildAck(NetworkBattleUri.InitBattle), true));
|
||||
phaseFrom!.Phase = BattleSessionPhase.AwaitingLoaded;
|
||||
break;
|
||||
|
||||
case NetworkBattleUri.Loaded
|
||||
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AwaitingLoaded:
|
||||
// Silent — no BattleStart, no Deal. The client's AINetworkBattleManager
|
||||
// populates opponent state from AIBattleStart HTTP data; pushing
|
||||
// BattleStart here overwrites that state with zeros.
|
||||
phaseFrom!.Phase = BattleSessionPhase.AwaitingSwap;
|
||||
break;
|
||||
|
||||
case NetworkBattleUri.TurnEnd
|
||||
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AfterReady:
|
||||
|
||||
Reference in New Issue
Block a user