feat(arena-tk2): SoloDefaultsToScripted config flag for dev convenience
Adds BattleNodeOptions.SoloDefaultsToScripted (default false). When true, the TK2 do_matching controller treats every solo poll as if ?scripted=1 were passed and returns a Scripted 3004 match immediately — useful for the live client (which can't append query params) to drive the scripted bot without needing a second player. Toggle via "BattleNode:SoloDefaultsToScripted" in appsettings*.json (Program.cs now binds the BattleNode section over the AddBattleNode defaults). Turn off to test real PvP with two clients. Trade-off documented on the option: while on, two simultaneous pollers each get their own Scripted match instead of pairing, so PvP is effectively disabled until the flag is flipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,4 +15,15 @@ public sealed class BattleNodeOptions
|
||||
/// in tests via the factory.
|
||||
/// </summary>
|
||||
public TimeSpan WaitingRoomTimeout { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>
|
||||
/// Dev convenience: when true, matchmaking endpoints that would otherwise park
|
||||
/// a solo poller (returning 3002 RETRY until a partner arrives) instead return
|
||||
/// a Scripted match immediately — equivalent to passing <c>?scripted=1</c> on
|
||||
/// every request. Turn off to test real PvP with two clients. Default false.
|
||||
/// <para>Trade-off: while on, two viewers polling simultaneously each get
|
||||
/// their own Scripted match instead of pairing with each other. Toggling off
|
||||
/// is the only way to get PvP behavior.</para>
|
||||
/// </summary>
|
||||
public bool SoloDefaultsToScripted { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -14,17 +14,20 @@ public class ArenaTwoPickBattleController : SVSimController
|
||||
private readonly IMatchingBridge _matching;
|
||||
private readonly IMatchContextBuilder _matchContextBuilder;
|
||||
private readonly IMatchingPairUpService _pairUp;
|
||||
private readonly BattleNodeOptions _battleNodeOptions;
|
||||
|
||||
public ArenaTwoPickBattleController(
|
||||
IArenaTwoPickService svc,
|
||||
IMatchingBridge matching,
|
||||
IMatchContextBuilder matchContextBuilder,
|
||||
IMatchingPairUpService pairUp)
|
||||
IMatchingPairUpService pairUp,
|
||||
BattleNodeOptions battleNodeOptions)
|
||||
{
|
||||
_svc = svc;
|
||||
_matching = matching;
|
||||
_matchContextBuilder = matchContextBuilder;
|
||||
_pairUp = pairUp;
|
||||
_battleNodeOptions = battleNodeOptions;
|
||||
}
|
||||
|
||||
[HttpPost("do_matching")]
|
||||
@@ -36,8 +39,12 @@ public class ArenaTwoPickBattleController : SVSimController
|
||||
if (!TryGetViewerId(out var vid)) return Unauthorized();
|
||||
// Accept "1" or "true" (case-insensitive) as opt-in for the legacy Scripted path.
|
||||
// ASP.NET's default bool binder rejects "1", so do a permissive parse here.
|
||||
var useScripted = scripted is not null
|
||||
&& (scripted == "1" || string.Equals(scripted, "true", StringComparison.OrdinalIgnoreCase));
|
||||
// The server-side BattleNodeOptions.SoloDefaultsToScripted flag is the other
|
||||
// route — it bypasses pair-up for every solo poll, useful when the live client
|
||||
// (which can't append query params) needs a Scripted match.
|
||||
var useScripted = (scripted is not null
|
||||
&& (scripted == "1" || string.Equals(scripted, "true", StringComparison.OrdinalIgnoreCase)))
|
||||
|| _battleNodeOptions.SoloDefaultsToScripted;
|
||||
try
|
||||
{
|
||||
var ctx = await _matchContextBuilder.BuildForTwoPickAsync(vid);
|
||||
|
||||
@@ -124,6 +124,9 @@ public class Program
|
||||
// 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 SoloDefaultsToScripted.
|
||||
builder.Configuration.GetSection("BattleNode").Bind(opt);
|
||||
});
|
||||
// In-process FCFS pair-up for TK2 PvP /do_matching. Singleton: per-mode state is
|
||||
// process-wide. Proper queue API is a separate spec; this is enough to actually
|
||||
|
||||
@@ -4,5 +4,8 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"BattleNode": {
|
||||
"SoloDefaultsToScripted": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
@@ -136,6 +137,36 @@ public class ArenaTwoPickBattleControllerTests
|
||||
"Owner and joiner must see the same node_server_url.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DoMatching_SoloDefaultsToScripted_flag_makes_solo_poll_return_3004_without_query_param()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
// BattleNodeOptions is a singleton in DI; flipping it before the request takes
|
||||
// effect immediately for this factory. Real deployments toggle it via the
|
||||
// "BattleNode:SoloDefaultsToScripted" key in appsettings*.json.
|
||||
factory.Services.GetRequiredService<BattleNodeOptions>().SoloDefaultsToScripted = true;
|
||||
|
||||
var vid = await factory.SeedViewerAsync();
|
||||
await SeedCompleteTwoPickRunAsync(factory, vid);
|
||||
using var client = factory.CreateAuthenticatedClient(vid);
|
||||
|
||||
var req = new {
|
||||
deck_no = 1L, need_init = 1, log = 1, excluded_field_id_list = new long[] { }, use_stage_select = 1, is_default_skin = 0,
|
||||
viewer_id = "0", steam_id = 0, steam_session_ticket = "",
|
||||
};
|
||||
// No ?scripted=1 — the flag alone should drive the Scripted branch.
|
||||
var resp = await client.PostAsync("/arena_two_pick_battle/do_matching", JsonContent.Create(req));
|
||||
|
||||
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.That(root.GetProperty("matching_state").GetInt32(), Is.EqualTo(3004),
|
||||
"SoloDefaultsToScripted=true should bypass pair-up and return a Scripted 3004 SUCCEEDED.");
|
||||
Assert.That(root.GetProperty("battle_id").GetString(), Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(root.GetProperty("node_server_url").GetString(), Does.Contain("/socket.io/"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DoMatching_NoActiveRun_Returns400WithErrorCode()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user