Files
SVSimServer/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs
gamer147 c279b811ad 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>
2026-06-01 08:57:15 -04:00

62 lines
2.9 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SVSim.BattleNode.Bridge;
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();
configure?.Invoke(options);
services.AddSingleton(options);
services.AddSingleton<IBattleSessionStore, InMemoryBattleSessionStore>();
services.AddSingleton<IMatchingBridge, MatchingBridge>();
services.AddSingleton<BattleNodeWebSocketHandler>();
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&amp;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();
app.Map("/socket.io", branch => branch.Run(async ctx =>
{
var handler = ctx.RequestServices.GetRequiredService<BattleNodeWebSocketHandler>();
await handler.HandleAsync(ctx);
}));
return app;
}
}