Commit Graph

86 Commits

Author SHA1 Message Date
gamer147
3991bcc653 feat(battle-node): bound InboundTracker with watermark-guarded sliding window
Audit Md11 (part 1 of 2). Replace unbounded HashSet<long> _seen with a
WindowSize=256 ring (HashSet + Queue, LRU eviction). The stale-below-window
guard (pubSeq <= HighWaterMark - WindowSize) prevents window eviction from
re-admitting old seqs as novel — the load-bearing invariant.

pubSeq is client-monotonic and SIO retransmit horizons are seconds-scale, so
256 covers realistic retries by a wide margin. HighWaterMark semantics
preserved (Gungnir still reports it).

Tests: 5 new InboundTrackerTests covering below-window guard, evicted-seq
rejection, within-window dedup after eviction, memory bound, and watermark
monotonicity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 13:06:08 -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
8aead62116 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>
2026-06-02 11:23:13 -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
fee84cca24 feat(battle-node): wire WS handler's case BattleType.Bot to real (Real, NoOp) session
Replaces the Phase-2 log-and-return stub with a real session
construction. P2 is always null for Bot (bridge contract), so no
WaitingRoom flow needed — single real WS, Phase-1 WhenAll-everything
RunAsync semantics work because NoOp.RunAsync completes immediately.
Integration test follows in the next task.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 01:29:11 -04:00
gamer147
a4685a9188 feat(battle-node): Bot dispatch arms in ComputeFrames
Three new arms gated on Type == BattleType.Bot, placed before the
existing PvP / Scripted arms:
- InitBattle → ack to sender (no Matched push — client uses AIBattleStart HTTP data)
- Loaded → silent (no BattleStart, no Deal — client short-circuits to GotoBattle)
- TurnEnd / TurnEndFinal → Judge to sender only (not broadcast)

Other URIs in Bot mode fall through existing arms: Swap is Type-agnostic
(per-sender SwapResponse + Ready), Retire/Kill hits the existing
Scripted no-contest BattleFinish(Win), gameplay forwarders are gated on
Pvp so Bot's PlayActions/Echo/etc. fall through default (drop). 8 new
dispatch tests cover the wire contract.

Reference: docs/api-spec/in-battle/ai-passive.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 01:27:08 -04:00
gamer147
0095bdf0cf feat(arena-tk2): SoloDefaultsToScripted config flag for dev convenience
Adds BattleNodeOptions.SoloDefaultsToScripted (default false). When true,
the TK2 do_matching controller treats every solo poll as if ?scripted=1
were passed and returns a Scripted 3004 match immediately — useful for
the live client (which can't append query params) to drive the scripted
bot without needing a second player.

Toggle via "BattleNode:SoloDefaultsToScripted" in appsettings*.json
(Program.cs now binds the BattleNode section over the AddBattleNode
defaults). Turn off to test real PvP with two clients.

Trade-off documented on the option: while on, two simultaneous pollers
each get their own Scripted match instead of pairing, so PvP is
effectively disabled until the flag is flipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 23:48:14 -04:00
gamer147
0bb19320df feat(battle-node): WS handler Pvp branch with WaitingRoom
Pvp arrivers Pair-or-Park: second arriver constructs the session;
first arriver awaits self.AwaitSessionFinishedAsync (never calls
self.RunAsync directly because the session does). Park-race retries
Pair once. Bot type still stubbed for Phase 3. Scripted path unchanged.

Viewer-id validation extended to accept either P1 or P2 (PvP sessions
have both).
2026-06-01 22:02:21 -04:00
gamer147
ca5a1e926d feat(battle-node): RealParticipant session-finished signal + Pvp cascade
RealParticipant gains _sessionFinished TCS + MarkSessionFinished /
AwaitSessionFinishedAsync. PvP first-arriver's handler awaits the
signal instead of calling self.RunAsync (which the session does
internally on the same instance — double-call would race the WS read).

BattleSession.RunAsync branches on Type: Pvp uses WhenAny + synthesize
BattleFinish(Win) to survivor + WhenAll(drain); Scripted/Bot keep
Phase 1's WhenAll-everything semantics. Disconnect cascade now drives
end-of-battle when a WS drops without a graceful Retire.
2026-06-01 21:58:47 -04:00
gamer147
2789dc08cb feat(battle-node): WaitingRoom for PvP WS rendezvous
Per-BattleId slot keyed dict. Pair returns the first arriver to the
second; ParkAsync awaits a TCS and returns the second arriver. Timeout
defaults to BattleNodeOptions.WaitingRoomTimeout (60s); evict on timeout
keeps the dict clean. Singleton in DI; consumed by the handler in the
next task.
2026-06-01 21:55:11 -04:00
gamer147
db054205b3 feat(battle-node): PvP TurnEnd broadcast + flipped Retire/Kill result
Pvp TurnEnd/TurnEndFinal broadcasts TurnEnd+Judge to BOTH so each
client's JudgeOperation advances. Pvp Retire/Kill pushes BattleFinish
with flipped result (sender=Lose, other=Win). Scripted Retire keeps
Phase 1 behaviour (sender-only Win via BuildBattleFinishNoContest).
2026-06-01 21:51:27 -04:00
gamer147
72dc1887d9 feat(battle-node): PvP gameplay-frame forwarding arms
TurnStart / PlayActions / Echo / TurnEndActions / JudgeResult from a
real participant in Pvp mode forward to the other participant once
BothAfterReady. Scripted's bot-burst case arms (gated on
FakeOpponentViewerId) precede the PvP forwarder so they're unaffected.

The bot-emission TurnStart/TurnEnd/Judge guard was tightened from
`ReferenceEquals(from, A or B)` (always true) to call
IsRealForwardableFromScripted directly in the `when` clause. The prior
shape used `goto default` to drop non-bot senders, which would have
short-circuited the new PvP forwarder for TurnStart in PvP mode.
2026-06-01 21:46:28 -04:00
gamer147
875a4baa29 refactor(battle-node): move handshake phase reads to per-participant
ComputeFrames now reads (from as IHasHandshakePhase)?.Phase for the
four handshake arms (InitNetwork, InitBattle, Loaded, Swap) and the
TurnEnd gate, transitioning the participant's Phase instead of the
session's. RealParticipant implements IHasHandshakePhase via the new
Phase property; the session-level BattleSession.Phase stays for the
Terminal short-circuit.

Scripted dispatch + wire shape unchanged (single-Real-participant case
collapses to Phase 1 semantics). Test fixture migrates FakeParticipant
to FakeRealParticipant for the side that drives handshake states. The
bot's TurnEnd previously rode the session-level AfterReady arm; with
that arm now gated on the sender's per-participant Phase (which the
bot lacks), TurnEnd joins TurnStart/Judge in the scripted-bot
forwarder arm so the v1.2 burst still reaches the real participant.
2026-06-01 21:33:17 -04:00
gamer147
ac78473a3e feat(battle-node): add RealParticipant.Phase for per-side handshake state
Internal setter; defaults to AwaitingInitNetwork. PvP needs A and B to
progress through the handshake states independently, which the
session-level BattleSession.Phase can't model. Session migration to read
realFrom.Phase is the next task.
2026-06-01 21:25:11 -04:00
gamer147
b75eb512ea docs(battle-node): refresh ScriptedBotParticipant <remarks> to match Phase 2 wiring
Task 1's refactor made BattleSession read other.Context for the
Matched / BattleStart opponent half, but the class doc still claimed
the Context was ignored. Update it to match the new wiring.
2026-06-01 21:23:09 -04:00
gamer147
560feb231a refactor(battle-node): generalise BuildMatched/BuildBattleStart for PvP
Both helpers now take the opponent's MatchContext + an explicit seed
instead of pulling ScriptedProfiles.OpponentMatchedProfile / OpponentBattleStartProfile
internally. ScriptedBotParticipant.Context fixture absorbs the cosmetic
fields previously hardcoded in ScriptedProfiles so Scripted's wire bytes
stay identical - verified by integration tests still green.

Phase 2 prep: PvP arms will call the same helpers with the real opponent
participant's Context.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 21:16:11 -04:00
gamer147
2d7cee38d3 refactor(battle-node): drop old BattleSession; rename V2 -> BattleSession
Old single-WS BattleSession + its dispatch/pump/ClipAckArg tests are
obsolete after the Task 9 handler cutover. ClipAckArg overflow + boundary
coverage moved into RealParticipantTests. BattleSessionV2 renamed back
to BattleSession; the V2 suffix was a placeholder during the parallel
-build refactor.
2026-06-01 20:10:14 -04:00
gamer147
91472df6fc refactor(battle-node): cut handler over to BattleSessionV2 + participants
Production WS path now constructs RealParticipant + ScriptedBotParticipant
and hands them to BattleSessionV2 instead of the old single-WS
BattleSession. Wire behaviour preserved end-to-end (BattleNodeFlowTests
still pass).

Also fixes a RunAsync bug uncovered by the cutover: WhenAny would
terminate the session as soon as the scripted bot's no-op RunAsync
resolved, killing the live WS read loop before any traffic arrived.
Phase 1 semantics are simpler — wait for ALL participants. Phase 2's
Pvp disconnect propagation will revisit this.
2026-06-01 20:07:45 -04:00
gamer147
b2f3d25be0 feat(battle-node): add BattleSessionV2 broker (unused yet)
Parallel to existing BattleSession. Subscribes to both participants'
FrameEmitted, dispatches via ComputeFrames(from, env) returning
(target, frame, noStock) routing tuples. Dispatch table currently only
covers Scripted-mode behaviour (preserves v1.2). Phase 2 adds Pvp arms;
Phase 3 adds Bot. Not yet wired into the handler — Task 9 cuts over.
2026-06-01 20:01:54 -04:00
gamer147
d665f88067 refactor(battle-node): unify IMatchingBridge.RegisterBattle signature
Single RegisterBattle(p1, p2?, type) with contract validation throws on
invalid combinations (Pvp requires both; Bot requires p2==null; Scripted
accepts either). PendingBattle carries Type + P1 + nullable P2. Handler
+ controller adapt; v1.2 behaviour preserved because Scripted is the
only type used today (Phase 2 adds Pvp, Phase 3 adds Bot).
2026-06-01 20:00:52 -04:00
gamer147
acd0997cfb feat(battle-node): add RealParticipant wrapping WS + sequencers
Lifts the WS read loop, SIO encode/decode, per-WS OutboundSequencer +
InboundTracker, and SIO ack out of BattleSession into a participant.
PushAsync(noStock=false) assigns playSeq via the sequencer; noStock=true
bypasses it. FrameEmitted fires on each deduplicated inbound envelope.
The existing BattleSession keeps its own copy of the WS code for now;
Task 9 cuts the handler over to use BattleSessionV2 + RealParticipant
and Task 10 deletes the old BattleSession + duplicate code.
2026-06-01 19:57:45 -04:00
gamer147
fcdcc5d590 feat(battle-node): add ScriptedBotParticipant wrapping v1.2 burst
PushAsync(TurnEnd|TurnEndFinal) fires FrameEmitted three times:
OpponentTurnStart + OpponentTurnEnd + OpponentJudge. Behaviour-identical
to the v1.2 case arm in BattleSession.ComputeResponses; just repackaged
as a participant. Other URIs are swallowed. Used by Phase 1 to preserve
v1.2 behaviour under the new abstraction; replaces the case-arm logic
in BattleSession in Task 7.
2026-06-01 19:56:01 -04:00
gamer147
553a79c795 feat(battle-node): add NoOpBotParticipant
Silent participant for the Phase 3 Bot type. PushAsync swallows;
FrameEmitted never fires; RunAsync completes immediately. ViewerId is
the existing FakeOpponentViewerId const for consistency with scripted
lifecycle builders. Three tests lock the no-op contract.
2026-06-01 19:55:00 -04:00
gamer147
9079715da6 feat(battle-node): add IBattleParticipant interface
Central abstraction for v2 broker. PushAsync (session -> participant),
FrameEmitted (participant -> session), RunAsync (drives inbound),
TerminateAsync (cleanup). Three impls land in Tasks 3-5.
2026-06-01 19:54:03 -04:00
gamer147
ae7ff25af0 feat(battle-node): add BattleType, BattleFinishReason, BattlePlayer
Phase 1 foundation types for the v2 broker architecture. Nothing uses
them yet; they land alongside the existing v1.2 code so subsequent
tasks can extract the participant interface and impls.
2026-06-01 19:53:31 -04:00
gamer147
1ef101f851 feat(battle-node): push Judge after opponent TurnEnd so client transitions
Third frame in the burst, per prod TurnEnd -> Judge pairing observed in
battle-traffic_tk2_regular.ndjson (positions 10->11, 17->18, etc.).
The client's TurnEndOperation sends its own Judge and gates the next turn
on a server-pushed Judge via JudgeOperation -> ControlTurnStartPlayer.
Closes the v1.1 'Opponent's turn... forever' hang caught during smoke.
2026-06-01 17:37:55 -04:00
gamer147
8a5b8b747d feat(battle-node): BuildOpponentJudge builder for v1.2 turn-end Judge
Adds the third frame of the burst. Wire shape from prod (spin + resultCode).
OpponentJudgeSpin const next to OpponentTurnStartSpin for consistency.
Single test locks uri, ViewerId, Cat, and body shape.
2026-06-01 17:32:22 -04:00
gamer147
70b2872589 feat(battle-node): add JudgeBody record for opponent turn-end Judge push
Mirrors OpponentTurnStartBody — JsonPropertyName-cased spin + resultCode
(default 1). First piece of the v1.2 three-frame turn-end burst; nothing
references it yet.
2026-06-01 17:30:00 -04:00
gamer147
5021217134 test(battle-node): wire-shape test + refresh stale comment for v1.1
Final-review follow-ups:
- BuildOpponentTurnStart's doc comment claimed the v1 client sits
  indefinitely — true before the loop closure, false after. Updated
  to describe the pair with BuildOpponentTurnEnd.
- TypedBodyWireShapeTests had no coverage for BuildOpponentTurnEnd;
  added the literal-JSON test so a future JsonPropertyName rename
  on TurnEndBody is caught.
2026-06-01 15:20:48 -04:00
gamer147
e30fdb7570 feat(battle-node): scripted opponent turn loop pushes TurnStart + TurnEnd
The TurnEnd/TurnEndFinal case in ComputeResponses now returns two envelopes
back-to-back — opponent TurnStart followed by opponent TurnEnd. Phase enters
OpponentTurn transiently then resets to AfterReady within the same call so
the next player TurnEnd can fire the cycle again. Closes the v1 'stays at
Opponent's turn… forever' stall.
2026-06-01 14:57:49 -04:00
gamer147
f24fc7c643 feat(battle-node): BuildOpponentTurnEnd builder for v1.1 turn loop
Pairs with BuildOpponentTurnStart. Wire shape from prod capture
(turnState=0, resultCode=1). Single test locks uri, ViewerId, Cat,
and body shape.
2026-06-01 14:49:52 -04:00
gamer147
d4926e31d6 feat(battle-node): add TurnEndBody record for opponent turn-end push
Mirrors OpponentTurnStartBody — JsonPropertyName-cased turnState +
resultCode (default 1). First piece of the scripted opponent turn-end
loop; nothing references it yet.
2026-06-01 14:46:21 -04:00
gamer147
1904ae4c0c refactor(battle-node): ScriptedLifecycle.InitialHand as ImmutableArray<long>
Audit Md4 cleanup: the prior long[] allowed in-place modification by any
caller with the field reference. ImmutableArray<long> enforces the constant
contract at the type level. ComputeHandAfterSwap uses ToArray() to produce
its mutable working copy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 13:02:23 -04:00
gamer147
6077844ee8 docs(battle-node): README reflects real drafted deck + cosmetics
Player-side fictions (dummy deck, classId=1) removed; the section now
documents which fields are real (deck, leader, cosmetics) vs still hardcoded
(rank, battlePoint, cardMaster, fieldId, seed) with a pointer to the spec's
§Deferred plumbing for each. "Where to extend" table loses the two done
items and gains a row for wiring future modes via IMatchContextBuilder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 13:00:22 -04:00
gamer147
b0488e3f2e feat(battle-node): BuildBattleStart consumes MatchContext for player half
ClassId/CharaId/CardMasterName/BattleType flow from ctx. PlayerBattleStart
Profile removed; Rank/BattlePoint remain as standalone consts pending real
per-viewer rank tracker. One test updated, one new test added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:49:54 -04:00
gamer147
f589283572 feat(battle-node): BuildMatched consumes MatchContext for player half
selfInfo cosmetics + 30-card selfDeck now read from MatchContext. Opponent
half stays in ScriptedProfiles. DummyCardId / BuildDummyDeck / PlayerMatched
Profile removed. Two new tests lock the deck-idx pairing and cosmetic
flow-through; TypedBodyWireShapeTests + lifecycle tests thread a fixture ctx.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:48:04 -04:00
gamer147
01f9bb722a feat(battle-node): thread MatchContext through bridge to BattleSession
IMatchingBridge.RegisterPendingBattle now takes a MatchContext; PendingBattle
carries it; BattleSession stores it. ArenaTwoPickBattleController builds ctx
from IMatchContextBuilder. ScriptedLifecycle still uses ScriptedProfiles for
the player half — Tasks 5/6 migrate the lifecycle.

Existing tests updated: MatchingBridgeTests, BattleNodeFlowTests,
InMemoryBattleSessionStoreTests, BattleSessionDispatchTests, BattleSession
PumpTests, ArenaTwoPickBattleControllerTests (which now seeds a TK2 run +
adds a no-active-run 400 case).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:44:42 -04:00
gamer147
0e8f5427c3 feat(battle-node): add MatchContext record for per-mode player snapshot
Public contract between HTTP-side do_matching controllers (assemble) and
SVSim.BattleNode (consume). First piece of the real-drafted-deck wiring;
nothing references it yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:35:43 -04:00
gamer147
ef3d7bb82b refactor(battle-node): WireConstants for SIO event names + crypto RNG battle id 2026-06-01 11:53:01 -04:00
gamer147
133346e3e8 refactor(battle-node): SocketIoFrame throws on namespace; typed JSON construction 2026-06-01 11:48:17 -04:00
gamer147
2588388d9d refactor(battle-node): distinct WS auth status codes + named handler delegate 2026-06-01 11:45:50 -04:00
gamer147
a364f539ad refactor(battle-node): tighten Phase setter to private; document sid opacity 2026-06-01 11:41:47 -04:00
gamer147
677b1f1392 feat(battle-node): BattleResult enum for BattleFinish.result wire codes 2026-06-01 11:41:16 -04:00
gamer147
eaf6d7160b refactor(battle-node): dedupe NodeCrypto AES setup into BuildAes helper 2026-06-01 11:36:48 -04:00
gamer147
34c4ca0237 fix(battle-node): NodeCrypto.GenerateKey masks rand source with & 0xF 2026-06-01 11:35:53 -04:00
gamer147
4dd61343aa fix(battle-node): clip SIO ack arg instead of checked-cast throwing on overflow 2026-06-01 11:13:24 -04:00
gamer147
453865ade2 fix(battle-node): thread session CT through every send instead of None 2026-06-01 11:12:26 -04:00
gamer147
8cce667e02 fix(battle-node): await DispatchSocketIo instead of async-void fire-and-forget 2026-06-01 11:11:58 -04:00
gamer147
0764b8646f feat(battle-node): capture session-scoped CT in BattleSession.RunAsync 2026-06-01 11:11:31 -04:00
gamer147
e4691d616b fix(battle-node): emit envelope keys before body keys in MsgEnvelope.ToJson
Client RealTimeNetworkAgent.SetNetworkInfo iterates the synchronize-data
dict in insertion order. The "uri" key, when recognized as Matched, calls
GameMgr.InitializeSelfInfo which sets _selfDeck = null. Any "selfDeck"
processed before "uri" gets wiped; Matching.StartBattleLoad then crashes
on null.Select(...). Pre-refactor ToJson built a Dictionary envelope-first
then appended body keys, so the bug never surfaced. The typed-body rewrite
inverted the order — restoring envelope-first matches the prod wire.

Regression test BuildMatched_KeyOrder_PutsUriBeforeSelfDeckAndSelfInfo
locks the contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:53:51 -04:00