Scripted-bot softlock root cause: client-stocked SELECT_SKILL_URI /
SLIDE_OBJECT_URI hand emits (e.g. target selection on unit play / leader
attack) arrive as SIO BinaryEvent("hand", ...) with an ack-id. Our
DispatchSocketIo only had cases for "msg" and "alive" — "hand" fell to
the default Debug-drop with no SIO ack going back. Client's
stockEmitMessageMgr (RealTimeNetworkAgent.cs:1463) blocks subsequent
emits until the previous one is acked, so all follow-up PlayActions /
TurnEndActions / TurnEnd frames were stocked but never transmitted. The
loader hooks at EmitMsg (intent) not the socket layer, which is why
battle-traffic.ndjson shows the frames as sent while the server never
received them. ~10s later the client gives up and aborts the WS.
Wire-shape proof from data_dumps/captures/logs/websocket_output.txt:
line 619: [sio-in] uri=TurnStart pubSeq=17 ackId=16 ... (T3 start)
line 689: [ws-rx-text] preview=451-26["hand", {...}] ← unhandled
line 691: [ws-rx-bin] binLen=58 pendingFrame=hand
(no further [sio-in] entries — server received nothing else)
line 709: [ws-recv-exit] reason=OperationCanceled wsState=Aborted
New HandleHandEventAsync (RealParticipant.cs):
- Fire-and-forget hand frames (no ack-id; TOUCH_URI / SELECT_OBJECT_URI /
TURN_END_READY_URI) are silently swallowed — no queue-blocking risk
- Stocked hand frames decode the binary attachment via the same
msgpack-string + NodeCrypto.Decrypt pipeline as HandleMsgEventAsync,
parse the JSON, extract top-level "pubSeq", and SendSioAckAsync with
that pubSeq as the ack arg (matches what stockEmitMessageMgr.GetSelectData
expects to look up)
- Body shape is {"StockHandData":[uri_int, viewerId, udid, ...params,
pubSeq], "try":0, "pubSeq":N} — NOT a MsgEnvelope (no top-level "uri"),
so we can't reuse HandleMsgEventAsync as-is
- Missing-pubSeq fallback acks with arg=0 (rare path, logged at Warning)
so we never softlock from a malformed body
WireConstants gets the HandEvent = "hand" constant for the dispatch case.
In scripted/Bot mode the ack-only handler is correct (no opponent to
forward touches to). PvP-side forwarding semantics are unverified — see
docs/audits/battle-node-sio-events-2026-06-02.md (outer repo) for the
full event inventory and remaining gaps.
Tests:
- RealParticipantHandEventTests covers the three paths: stocked-with-ack,
fire-and-forget (no ack expected), missing-pubSeq fallback (arg=0). Each
drives a real hand frame through RunAsync via TestWebSocket and asserts
the SIO ack frame shape (43<ackId>[<arg>]) in outbound sends.
- 175 battle-node tests passing (was 172; +3 new). Full suite green.
Diagnostic logs ([sio-in] / [sio-out] / [ws-rx-text] / [ws-rx-bin] /
[ws-recv-exit] / [ws-loop-exit]) are left in place for one verification
cycle. After a live re-run confirms the fix, they should be stripped per
the audit doc's recommended-order step 2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
37 lines
1.6 KiB
C#
37 lines
1.6 KiB
C#
namespace SVSim.BattleNode.Protocol;
|
|
|
|
/// <summary>
|
|
/// String constants that show up on the wire as opaque tags. Lifting them out of
|
|
/// inline string literals gives each one a single source of truth and a name that
|
|
/// reads at the use site.
|
|
/// </summary>
|
|
internal static class WireConstants
|
|
{
|
|
/// <summary>SIO event name for ordered server-pushed frames (the lifecycle channel).</summary>
|
|
public const string SynchronizeEvent = "synchronize";
|
|
|
|
/// <summary>SIO event name for client-emitted msg frames + their ack-responses.</summary>
|
|
public const string MsgEvent = "msg";
|
|
|
|
/// <summary>SIO event name for Gungnir keepalive frames (both directions).</summary>
|
|
public const string AliveEvent = "alive";
|
|
|
|
/// <summary>
|
|
/// SIO event name for client-emitted hand frames (touches + skill/object selection).
|
|
/// Stocked variants (<c>SELECT_SKILL_URI</c>, <c>SLIDE_OBJECT_URI</c>) carry an ack-id;
|
|
/// fire-and-forget variants (<c>TOUCH_URI</c>, <c>SELECT_OBJECT_URI</c>,
|
|
/// <c>TURN_END_READY_URI</c>) do not. The body wire shape differs from <c>msg</c>
|
|
/// frames — see <c>HandleHandEventAsync</c>.
|
|
/// </summary>
|
|
public const string HandEvent = "hand";
|
|
|
|
/// <summary>
|
|
/// Placeholder UUID we stamp on every server-originated envelope. Prod servers stamp a
|
|
/// real per-request UUID; the client doesn't validate it.
|
|
/// </summary>
|
|
public const string ServerUuid = "node-stub";
|
|
|
|
/// <summary>Gungnir scs/ocs value the v1 server reports unconditionally.</summary>
|
|
public const string OnlineStatus = "ONLINE";
|
|
}
|