docs(battle-node): project README + docstrings on hosting/lifecycle

Add a per-project README in SVSim.BattleNode/ that covers:
- Architecture (the six concern folders)
- The connect-handshake sequence verified end-to-end at smoke
- A wire-format-gotchas table for the spec divergences caught during
  v1 (headers vs query for credentials, schemeless node URL with
  /socket.io/ path, required card_master_id, required resultCode=1,
  Matched in response to InitBattle not InitNetwork, EIO3 0x04 prefix
  on binary frames, FromJson conditional-expression number-boxing)
- What the v1 scripted opponent does and what is hardcoded
- A "where to extend" table for v2 work
- The full test layout and cross-references to specs/plans

Fill in XML docs on the public surface that previously had none:
- BattleNodeExtensions.AddBattleNode / UseBattleNode (DI + middleware
  wiring, including the pipeline-order note that auth runs before
  UseWebSockets)
- BattleNodeWebSocketHandler class + HandleAsync (the validation chain)
- BattleSession.ComputeResponses (the lifecycle state machine, with
  the NoStock flag's meaning)
- ScriptedLifecycle class (v1 scope, resultCode injection rule,
  pointer to the "where to extend" section)
- MatchingBridge class (mint-id + register flow)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-01 08:57:15 -04:00
parent 9e8ebd1b2b
commit c279b811ad
6 changed files with 240 additions and 6 deletions

View File

@@ -1,4 +1,3 @@
// SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using SVSim.BattleNode.Sessions;
@@ -6,6 +5,28 @@ using SVSim.BattleNode.Wire;
namespace SVSim.BattleNode.Hosting;
/// <summary>
/// Validates an incoming WebSocket upgrade request, accepts it, and hands off to a fresh
/// <see cref="BattleSession"/>. Singleton; no per-request state.
/// </summary>
/// <remarks>
/// <para>The validation chain — cheapest checks first, crypto only after both params are
/// present, WS accept only after the store lookup confirms the credentials match an outstanding
/// pending battle:</para>
/// <list type="number">
/// <item>Reject non-WS requests with 400 (someone hit <c>/socket.io/</c> via plain HTTP).</item>
/// <item>Read <c>BattleId</c> and encrypted <c>viewerId</c> from request headers, falling back
/// to query string. The real client puts them on headers despite BestHTTP's
/// <c>AdditionalQueryParams</c> API name — see project README §Wire-format gotchas.</item>
/// <item>Decrypt the viewerId with <see cref="NodeCrypto.DecryptForNode"/>; reject on
/// parse/decrypt failure.</item>
/// <item>Look up the <see cref="PendingBattle"/> in the store and verify the decrypted viewer
/// matches the one the <see cref="Bridge.IMatchingBridge"/> registered.</item>
/// <item>AcceptWebSocketAsync, remove the pending entry (it's now an active session), construct
/// <see cref="BattleSession"/>, await <see cref="BattleSession.RunAsync"/> until the WS
/// closes.</item>
/// </list>
/// </remarks>
public sealed class BattleNodeWebSocketHandler
{
private readonly IBattleSessionStore _store;
@@ -19,6 +40,11 @@ public sealed class BattleNodeWebSocketHandler
_log = loggerFactory.CreateLogger<BattleNodeWebSocketHandler>();
}
/// <summary>
/// Endpoint entry point. Sets <see cref="HttpContext.Response"/> to 400 on any validation
/// failure; otherwise upgrades to a WebSocket and awaits
/// <see cref="BattleSession.RunAsync"/> until the connection closes.
/// </summary>
public async Task HandleAsync(HttpContext ctx)
{
if (!ctx.WebSockets.IsWebSocketRequest)