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:
@@ -1,4 +1,3 @@
|
||||
// SVSim.BattleNode/Hosting/BattleNodeExtensions.cs
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
@@ -6,8 +5,24 @@ using SVSim.BattleNode.Sessions;
|
||||
|
||||
namespace SVSim.BattleNode.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Registration + pipeline extensions that turn an arbitrary ASP.NET Core host into a battle
|
||||
/// node. The library has no dependency on any specific host project — call both methods from
|
||||
/// wherever you build your <see cref="WebApplication"/>.
|
||||
/// </summary>
|
||||
public static class BattleNodeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Register the battle node's services in DI. All four are singletons because none of them
|
||||
/// carry per-request state — per-battle state lives on the <see cref="BattleSession"/>
|
||||
/// instance the WebSocket handler constructs on connect.
|
||||
/// </summary>
|
||||
/// <param name="configure">
|
||||
/// Optional callback to override <see cref="BattleNodeOptions"/> defaults. The default
|
||||
/// <c>NodeServerUrl</c> assumes the EmulatedEntrypoint host on
|
||||
/// <c>http://localhost:5148</c> and shares the port for the Socket.IO endpoint. Override
|
||||
/// when the node runs on a different port/host or behind a reverse proxy.
|
||||
/// </param>
|
||||
public static IServiceCollection AddBattleNode(this IServiceCollection services, Action<BattleNodeOptions>? configure = null)
|
||||
{
|
||||
var options = new BattleNodeOptions();
|
||||
@@ -19,6 +34,20 @@ public static class BattleNodeExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wire up the WebSocket middleware and map the Socket.IO endpoint at <c>/socket.io/</c>.
|
||||
/// Call this AFTER any HTTP middleware that should still see non-WS requests (auth,
|
||||
/// routing, controllers) and BEFORE <c>MapControllers()</c>. The endpoint accepts any
|
||||
/// path under <c>/socket.io</c>; the handler doesn't read the sub-path, so default
|
||||
/// Socket.IO clients targeting <c>/socket.io/?EIO=3&transport=websocket</c> work
|
||||
/// without configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Steam auth gets a free pass on WS upgrades — see
|
||||
/// <c>SteamSessionAuthenticationHandler</c>'s header-based bypass. The node has its own
|
||||
/// per-connection auth (encrypted viewerId in the upgrade headers, validated against the
|
||||
/// matched battle id in <see cref="BattleNodeWebSocketHandler.HandleAsync"/>).
|
||||
/// </remarks>
|
||||
public static IApplicationBuilder UseBattleNode(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseWebSockets();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user