Per-BattleId slot keyed dict. Pair returns the first arriver to the second; ParkAsync awaits a TCS and returns the second arriver. Timeout defaults to BattleNodeOptions.WaitingRoomTimeout (60s); evict on timeout keeps the dict clean. Singleton in DI; consumed by the handler in the next task.
71 lines
3.3 KiB
C#
71 lines
3.3 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<IWaitingRoom, WaitingRoom>();
|
|
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&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(HandleSocketIoAsync));
|
|
return app;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Terminal handler for <c>/socket.io/*</c> — resolves the singleton
|
|
/// <see cref="BattleNodeWebSocketHandler"/> from DI and hands the request over.
|
|
/// Extracted from the inline lambda in <see cref="UseBattleNode"/> so stack traces
|
|
/// show a real method name during WS connect failures.
|
|
/// </summary>
|
|
private static async Task HandleSocketIoAsync(Microsoft.AspNetCore.Http.HttpContext ctx)
|
|
{
|
|
var handler = ctx.RequestServices.GetRequiredService<BattleNodeWebSocketHandler>();
|
|
await handler.HandleAsync(ctx);
|
|
}
|
|
}
|