Commit Graph

50 Commits

Author SHA1 Message Date
gamer147
479548fa56 test(battle-node): integration test expects three frames per cycle
End-to-end exercises the v1.2 burst: each TurnEnd from the client now
produces TurnStart + TurnEnd + Judge through the real WS pump.
2026-06-01 17:42:44 -04:00
gamer147
136149ed6b test(battle-node): wire-shape test for BuildOpponentJudge
Mirrors BuildOpponentTurnEnd_SerializesTurnStateAndResultCode. Guards
JudgeBody's JsonPropertyName keys against rename-induced wire breakage
(per feedback_wire_shape_tests pattern).
2026-06-01 17:40:33 -04:00
gamer147
007513e55c test(battle-node): TurnEnd dispatch tests expect three-frame burst (TDD red)
Both single-cycle and consecutive-cycles tests now assert the v1.2
three-frame burst (TurnStart + TurnEnd + Judge). Currently failing —
ComputeResponses still pushes only two frames. Implementation follows.
2026-06-01 17:34:59 -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
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
ff8e4abea8 test(battle-node): integration test drives two opponent-turn cycles
End-to-end through the real WS pump: after Ready, the test sends two
consecutive TurnEnd msgs and asserts the server pushes
TurnStart+TurnEnd for each. Exercises OutboundSequencer's playSeq
assignment across multiple cycles.
2026-06-01 15:04:21 -04:00
gamer147
decdef29cf test(battle-node): TurnEnd cycle can fire multiple times
Locks the loop invariant: after the first cycle the phase resets to
AfterReady, so the next player TurnEnd matches the same case arm and
produces the same two-frame burst.
2026-06-01 15:01:19 -04:00
gamer147
96ae090a3a test(battle-node): rewrite TurnEnd dispatch test for two-frame cycle
Replaces the v1.0 single-envelope/OpponentTurn-phase invariant with
the v1.1 two-envelope/AfterReady invariant. Currently failing —
ComputeResponses still does the v1.0 thing. Implementation follows.
2026-06-01 14:53:32 -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
e3cc745a61 test(battle-node): end-to-end drafted deck flows into Matched frame
Seeds a viewer + completed TK2 run, drives the WS handshake to Matched, and
asserts every cardId in selfDeck matches the run's SelectedCardIdsJson. Read
from RawBody (codec's wire-form deserialization) — not from MatchedBody —
since the test client gets the JSON-roundtripped envelope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:51:33 -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
133346e3e8 refactor(battle-node): SocketIoFrame throws on namespace; typed JSON construction 2026-06-01 11:48:17 -04:00
gamer147
677b1f1392 feat(battle-node): BattleResult enum for BattleFinish.result wire codes 2026-06-01 11:41:16 -04:00
gamer147
34c4ca0237 fix(battle-node): NodeCrypto.GenerateKey masks rand source with & 0xF 2026-06-01 11:35:53 -04:00
gamer147
e4fbb155e4 test(battle-node): pump-level tests for async-Task dispatch, CT, Md5 clip 2026-06-01 11:15:10 -04:00
gamer147
21b7ddf6ae test(battle-node): TestWebSocket mock for pump-level unit tests 2026-06-01 11:13:54 -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
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
gamer147
19cc7980d1 test(battle-node): envelope-level wire-shape regression for scripted bodies 2026-06-01 10:40:54 -04:00
gamer147
5ee270eb16 refactor(battle-node): switch MsgEnvelope.Body to IMsgBody, migrate all sites 2026-06-01 10:40:09 -04:00
gamer147
c7745d8785 feat(battle-node): typed OpponentTurnStart/ResultCodeOnly/BattleFinish/AlivePush bodies 2026-06-01 10:35:18 -04:00
gamer147
97b9b6fe42 feat(battle-node): typed Deal/Swap/Ready bodies + PosIdx 2026-06-01 10:34:44 -04:00
gamer147
78a6fe93fb feat(battle-node): typed BattleStartBody + Self/Oppo info records 2026-06-01 10:34:07 -04:00
gamer147
d9fbb67f0c feat(battle-node): typed MatchedBody + Self/Oppo info records 2026-06-01 10:33:34 -04:00
gamer147
9e8ebd1b2b fix(battle-node): preserve long type on numeric array elements in FromJson
Root cause for the lingering mulligan failure: the inline conditional
expression in MsgEnvelope.ToObject

    JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(),

unified its branches to the common implicit-convertible type. long→double
is implicit, so both branches collapsed to double and the integer value
silently widened. Inside an array (idxList:[2]), each element came back
as boxed double; OfType<long> in ExtractIdxList then filtered every
entry out, so swapIndices arrived empty and BuildSwapResponse echoed
the unchanged hand — exactly the diff-against-Deal mismatch the client
flagged as "Card swap failed: AbandonCards[2]/DrawCards[]".

Extract a ParseNumber helper that returns object explicitly so each
branch boxes its own runtime type. Also harden ExtractIdxList to accept
any boxed numeric type (long/int/double/decimal/string) so a future
JSON-parser drift can't silently regress this path again.

Two regression tests:
- FromJson_NumericArray_PreservesLongTypeOnEachElement: confirms the
  fix at the JSON-parse layer with a hardcoded "{\"idxList\":[2,3]}".
- Swap_WithIdxListContainingTwo_ProducesHandWithFreshIdxAtPosition1:
  exercises the dispatch end-to-end with a Body holding a real boxed
  long; asserts position 1 of the response hand is the fresh deck idx 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:40:50 -04:00
gamer147
77fb93f3ea fix(battle-node): real mulligan card replacement + opponent TurnStart push
Two issues caught during v1 smoke at the mulligan / first-turn boundary:

1) BuildSwapResponse ignored the player's idxList and echoed the same
   3-card hand back. The client diffs the new self[] against the Deal
   to compute "drawn cards" — empty diff against the same hand throws
   "Card swap failed: AbandonCards[X]/DrawCards[]". Replace swapped
   idxs with fresh deck idxs (initial hand was 1/2/3, deck has 4..30
   still available). Same hand must flow into Ready since the client
   diffs again there. Move the hand computation into a new helper
   ComputeHandAfterSwap and have ComputeResponses thread it through
   both BuildSwapResponse and BuildReady.

2) The client doesn't transition to the "Opponent's turn…" display
   on its own after sending TurnEnd — it waits for the server to push
   an opponent TurnStart (per prod TK2 capture line 14). Without it
   the UI just sits on the end-of-turn frame. Add a TurnEnd handler
   that pushes a minimal TurnStart{spin} and transitions to a new
   OpponentTurn phase, which IS the documented v1 stopping point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:30:44 -04:00
gamer147
e06d97ef6f fix(battle-node): respond to InitBattle/Loaded, not InitNetwork
Pushing Matched in response to InitNetwork lands it before
MatchingInitBattle() finishes wiring up the OnReceivedEvent handler
and setting status=Connect. The client's Matched-case in
ReactionReceiveUri only transitions to StartLoad when status is
Connect at the moment of receipt; otherwise the frame is silently
dropped at the state machine and the matchmaking UI never advances.

The real connect-handshake sequence (per MatchingNetworkConnectChecker
+ Matching.cs):
  1. WS opens.
  2. Client emits InitNetwork (cat=general).
  3. Server replies InitNetwork ack → _initNetworkSuccess = true.
  4. MatchingInitBattle: status=Connect; emit InitBattle; subscribe
     OnReceivedEvent matching handler.
  5. Server replies Matched → status=StartLoad, StartBattleLoad.
  6. Asset load done → client emits Loaded.
  7. Server replies BattleStart + Deal → status=Prepared, GotoBattle.

Add AwaitingInitBattle phase, gate Matched on InitBattle receipt, and
gate BattleStart+Deal on Loaded receipt. Update dispatch and
integration tests to walk the new sequence; InitBattle's wire cat is
Matching(2), not Battle(1).

Caught during v1 smoke walkthrough — battle-traffic.ndjson showed the
client receiving Matched/BattleStart at sub-millisecond gaps after
InitNetwork ack, but never advancing past matchmaking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 02:08:04 -04:00
gamer147
cc32223d7d fix(battle-node): strip/prepend EIO3 type byte on binary WS frames
Engine.IO v3 frames over WebSocket prepend the packet-type byte (0x04
for Message) to BINARY frames, the binary analog of the leading digit
on text frames. The real client honors this and our session was
treating the entire binary frame as the Socket.IO attachment payload —
the msgpack decoder saw 0x04 as a positive fixint and failed
deserialization on every inbound msg event.

Symmetric fix: strip 0x04 from inbound binary frames in
BattleSession.RunAsync, prepend 0x04 to outbound binary frames in
EncodeAndSendAsync. RawSocketIoTestClient gets the same on both
directions so the integration test still exercises the same wire
shape as a real client.

Caught during v1 smoke walkthrough, after the WS upgrade started
succeeding (101 Switching Protocols).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:48:52 -04:00
gamer147
5525dbee24 fix(battle-node): node_server_url matches prod wire format (no scheme, with path)
Prod do_matching captures (data_dumps/captures/traffic_prod_tk2_*) send
the node URL as host:port/socket.io/ with no scheme prefix —
e.g. "node06.shadowverse.jp:13560/socket.io/". BestHTTP's SocketManager
expects this exact shape; the leading ws:// we were sending plus the
missing /socket.io/ path was preventing the client from completing the
post-do_matching connect (eventually times out with "connection timed
out").

Update BattleNodeOptions default, Program.cs override, and both
controller and bridge tests to use "localhost:5148/socket.io/".

Discovered during v1 smoke walkthrough.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:06:40 -04:00
gamer147
905fdc780a test(battle-node): end-to-end flow test through Ready via WebApplicationFactory
Boots SVSimTestFactory (in-memory SQLite + reference-data CSV import),
mints a battle via IMatchingBridge, opens a raw Socket.IO v2 client
against the in-process TestServer, drives InitNetwork → Loaded → Swap,
and asserts the right scripted frames come back in order.

Verifies the full transport stack end-to-end: EIO3+SIO2 framing,
encryptForNode codec, MsgPayloadCodec roundtrip, InboundTracker
pubSeq dedup + ack echo, OutboundSequencer playSeq assignment, and
ScriptedLifecycle's Path-A frame builders.

Note: RawSocketIoTestClient.DisposeAsync skips the graceful CloseAsync
handshake — TestServer's in-process WebSocket implementation can hang
on it. Abrupt Dispose is fine: the server's ReceiveAsync throws
WebSocketException, BattleSession.RunAsync returns, and the handler
completes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 23:37:31 -04:00
gamer147
f19da481c3 fix(battle-node): MatchingBridge avoids Math.Abs(int.MinValue) overflow
Cast GetHashCode() result to long before Math.Abs to prevent OverflowException
on the ~1-in-4B case where GetHashCode returns int.MinValue. Adds a regression
test pinning the 12-digit decimal format end-to-end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 22:33:35 -04:00
gamer147
d3c4b3083e feat(battle-node): IMatchingBridge + MatchingBridge mint battle id + node url 2026-05-31 22:31:04 -04:00
gamer147
680630050b fix(battle-node): BattleSession crash safety, fresh-key per push, phase guards
- Wrap HandleMsgEventAsync / HandleAliveEventAsync bodies in try/catch(Exception)
  logging at Error, eliminating async-void unobserved-exception crash risk (Issue 1).
- Replace deterministic seq-based key generator with RandomNumberGenerator.GetInt32
  so each EncodeAndSendAsync call uses a fresh random key (Issue 2).
- Add `when Phase == …` guards to InitNetwork / Loaded / Swap cases in
  ComputeResponses; add default arm that logs+drops out-of-order URIs (Issue 3).
- Widen SendSioAckAsync arg from int to long; drop (int) cast at call site;
  boundary cast to int is now checked() for defensive overflow detection (Issue 4).
- Update RunAsync doc comment (was stale Task-13 placeholder) (Issue 5).
- Add Kill and out-of-order-Swap-before-Loaded tests (Issue 6).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 22:28:13 -04:00
gamer147
f6aee5b0f8 feat(battle-node): BattleSession routes lifecycle URIs through ScriptedLifecycle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 22:21:55 -04:00
gamer147
30b457c9a0 fix(battle-node): assert Bid is in envelope (not Body) on BuildMatched
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 22:18:12 -04:00
gamer147
0fd4f5f9f7 feat(battle-node): ScriptedLifecycle frame builders (Path-A static opponent) 2026-05-31 22:15:44 -04:00
gamer147
22a4825265 feat(battle-node): Gungnir alive-body builders (scs/ocs ONLINE placeholders) 2026-05-31 22:07:31 -04:00
gamer147
82b7d1e940 feat(battle-node): OutboundSequencer assigns playSeq + archives for Resume 2026-05-31 22:05:16 -04:00
gamer147
87051737da feat(battle-node): InboundTracker dedupes client pubSeq + tracks high-water 2026-05-31 22:02:56 -04:00
gamer147
3ade8ff4f5 feat(battle-node): in-memory IBattleSessionStore + PendingBattle 2026-05-31 22:00:40 -04:00
gamer147
c0c2bb5772 feat(battle-node): MsgPayloadCodec encodes/decodes msgpack↔envelope chain 2026-05-31 21:58:06 -04:00
gamer147
4cc8b3c01c fix(battle-node): MsgEnvelope rejects reserved Body keys + complete ReceiveNodeResultCode
ToJson now throws ArgumentException when a Body key collides with a reserved
envelope field (uri/viewerId/uuid/bid/try/cat/pubSeq/playSeq); FromJson reuses
the same shared ReservedEnvelopeKeys HashSet. ReceiveNodeResultCode expanded
from 9 to 31 codes to mirror the full enums.md catalog. Two regression tests
added for the collision guard and PascalCase uri serialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:55:11 -04:00
gamer147
383044dd8f feat(battle-node): NetworkBattleUri / EmitCategory enums and MsgEnvelope record
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:50:17 -04:00
gamer147
6ff4f70f1a fix(battle-node): SocketIoFrame disposal safety + escaping + empty-args encoding
- Wrap all JsonDocument.Parse calls in using blocks and Clone() each
  retained JsonElement to eliminate UAF hazard after GC.
- Use JsonSerializer.Serialize with UnsafeRelaxedJsonEscaping so event
  names with " or \ produce \" / \ rather than " / plain \;
  avoids malformed JSON on Encode().
- Guard the [ ] block in Encode() behind EventName-or-args check so
  Connect/Disconnect packets round-trip as bare "0"/"1" not "0[]".
- Add three regression tests: Connect no-bracket, Event round-trip,
  special-char event name escaping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:46:02 -04:00
gamer147
8b1f613407 feat(battle-node): SocketIoFrame parse/encode for SIO2 incl. binary attachments 2026-05-31 21:39:53 -04:00
gamer147
6c6664f011 feat(battle-node): EngineIoFrame parse/encode for EIO3 packets 2026-05-31 21:34:11 -04:00
gamer147
a786599416 fix(battle-node): clarify NodeCrypto.GenerateKey contract + add fixed-vector regression test
Replace inaccurate GenerateKey docstring (it claimed to port Cryptographer.generateKeyString
directly but the input shape differs: server uses one hex digit per call, client uses
Random.Next(0,65535) per call). New doc is honest about the difference and explains why
it's safe. Add EncryptForNode_FixedVector_ProducesStableOutput: a pinned AES-CBC vector
that catches encoding/IV/padding regressions that would slip past the roundtrip test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:31:22 -04:00
gamer147
0a2eddd920 feat(battle-node): port AES-256-CBC encryptForNode/decryptForNode codec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 21:26:05 -04:00