diff --git a/SVSim.BattleNode/Bridge/BattleNodeOptions.cs b/SVSim.BattleNode/Bridge/BattleNodeOptions.cs
index 1cd35971..b9f7d374 100644
--- a/SVSim.BattleNode/Bridge/BattleNodeOptions.cs
+++ b/SVSim.BattleNode/Bridge/BattleNodeOptions.cs
@@ -1,13 +1,25 @@
namespace SVSim.BattleNode.Bridge;
///
-/// DI-injected options for the battle node. NodeServerUrl matches the prod
-/// do_matching wire format: host:port/socket.io/, no scheme prefix.
-/// BestHTTP's SocketManager parses it as the Socket.IO v2 endpoint URL.
+/// DI-injected options for the battle node.
///
public sealed class BattleNodeOptions
{
- public string NodeServerUrl { get; set; } = "localhost:5148/socket.io/";
+ ///
+ /// The Socket.IO v2 endpoint URL echoed back to the client on /*/do_matching
+ /// success (matching_state 3004/3007/3011). Matches the prod do_matching wire format:
+ /// host[:port]/socket.io/ — no scheme prefix, must end with /socket.io/.
+ /// The client's BestHTTP SocketManager parses this string directly; a leading
+ /// http:///https:// or a missing trailing slash will make the client
+ /// fail to connect. Host may be an IP, hostname, or FQDN.
+ ///
+ /// Deployment-time value — no hardcoded fallback. Must be provided via the
+ /// "BattleNode:NodeServerUrl" key in appsettings.json (or an
+ /// equivalently-named env var);
+ /// validates presence at startup and throws if empty.
+ ///
+ ///
+ public string NodeServerUrl { get; set; } = "";
///
/// How long the first arriver's WS waits for a partner before disconnecting.
diff --git a/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs b/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs
index 8c07fb8f..f2aa5125 100644
--- a/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs
+++ b/SVSim.BattleNode/Hosting/BattleNodeExtensions.cs
@@ -18,15 +18,26 @@ public static class BattleNodeExtensions
/// instance the WebSocket handler constructs on connect.
///
///
- /// Optional callback to override defaults. The default
- /// NodeServerUrl assumes the EmulatedEntrypoint host on
- /// http://localhost:5148 and shares the port for the Socket.IO endpoint. Override
- /// when the node runs on a different port/host or behind a reverse proxy.
+ /// Callback to populate . Must set
+ /// — there is no hardcoded fallback; the
+ /// value is a deployment concern (localhost during dev, a real host[:port]/socket.io/
+ /// when the node runs behind a reverse proxy or on a separate box). Startup throws
+ /// if it's still empty after the callback runs.
///
+ ///
+ /// is null, empty, or whitespace after
+ /// runs.
+ ///
public static IServiceCollection AddBattleNode(this IServiceCollection services, Action? configure = null)
{
var options = new BattleNodeOptions();
configure?.Invoke(options);
+ if (string.IsNullOrWhiteSpace(options.NodeServerUrl))
+ {
+ throw new InvalidOperationException(
+ "BattleNode:NodeServerUrl is not configured. Set it in appsettings.json under " +
+ "the \"BattleNode\" section (format: \"host[:port]/socket.io/\", no scheme prefix).");
+ }
services.AddSingleton(options);
services.AddSingleton();
services.AddSingleton();
diff --git a/SVSim.EmulatedEntrypoint/Program.cs b/SVSim.EmulatedEntrypoint/Program.cs
index 2acb5338..6c5c9164 100644
--- a/SVSim.EmulatedEntrypoint/Program.cs
+++ b/SVSim.EmulatedEntrypoint/Program.cs
@@ -165,11 +165,10 @@ public class Program
builder.Services.AddBattleNode(opt =>
{
- // Matches the prod do_matching wire format: host:port/socket.io/, no scheme prefix.
- // BestHTTP's SocketManager parses this as the Socket.IO v2 endpoint URL.
- opt.NodeServerUrl = "localhost:5148/socket.io/";
- // Any field in BattleNodeOptions can be overridden via the "BattleNode" section
- // in appsettings*.json — see appsettings.Development.json for DiagnosticLogging.
+ // Every field on BattleNodeOptions is populated from the "BattleNode" section in
+ // appsettings*.json. NodeServerUrl has no hardcoded fallback — AddBattleNode
+ // throws at startup if it's missing/blank. See BattleNodeOptions.NodeServerUrl
+ // for the required wire format.
builder.Configuration.GetSection("BattleNode").Bind(opt);
});
// In-process FCFS pair-up for TK2 PvP /do_matching, plus rank-battle's AI-fallback
diff --git a/SVSim.EmulatedEntrypoint/appsettings.json b/SVSim.EmulatedEntrypoint/appsettings.json
index dee87515..d0efef73 100644
--- a/SVSim.EmulatedEntrypoint/appsettings.json
+++ b/SVSim.EmulatedEntrypoint/appsettings.json
@@ -27,5 +27,13 @@
"Deck": {
"MaxDeckSlots": 36
},
+ "BattleNode": {
+ // Socket.IO v2 endpoint URL echoed to the client on /*/do_matching success.
+ // Wire format: "host[:port]/socket.io/" — no scheme prefix, trailing slash REQUIRED.
+ // Host may be an IP, hostname, or FQDN. BestHTTP's SocketManager parses this string
+ // directly; a leading http:// or https:// will make the client fail to connect.
+ // Startup throws if this value is missing or blank; there is no hardcoded fallback.
+ "NodeServerUrl": "localhost:5148/socket.io/"
+ },
"AllowedHosts": "*"
}