From 0095bdf0cf8d462d16df75bdf216b2d3a56c3044 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 1 Jun 2026 23:48:14 -0400 Subject: [PATCH] feat(arena-tk2): SoloDefaultsToScripted config flag for dev convenience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- SVSim.BattleNode/Bridge/BattleNodeOptions.cs | 11 +++++++ .../ArenaTwoPickBattleController.cs | 13 ++++++-- SVSim.EmulatedEntrypoint/Program.cs | 3 ++ .../appsettings.Development.json | 3 ++ .../ArenaTwoPickBattleControllerTests.cs | 31 +++++++++++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/SVSim.BattleNode/Bridge/BattleNodeOptions.cs b/SVSim.BattleNode/Bridge/BattleNodeOptions.cs index dbf448f..40306ca 100644 --- a/SVSim.BattleNode/Bridge/BattleNodeOptions.cs +++ b/SVSim.BattleNode/Bridge/BattleNodeOptions.cs @@ -15,4 +15,15 @@ public sealed class BattleNodeOptions /// in tests via the factory. /// public TimeSpan WaitingRoomTimeout { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// 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 ?scripted=1 on + /// every request. Turn off to test real PvP with two clients. Default false. + /// 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. + /// + public bool SoloDefaultsToScripted { get; set; } = false; } diff --git a/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs b/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs index 8bf8357..3dab706 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs @@ -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); diff --git a/SVSim.EmulatedEntrypoint/Program.cs b/SVSim.EmulatedEntrypoint/Program.cs index ffb8078..12492fd 100644 --- a/SVSim.EmulatedEntrypoint/Program.cs +++ b/SVSim.EmulatedEntrypoint/Program.cs @@ -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 diff --git a/SVSim.EmulatedEntrypoint/appsettings.Development.json b/SVSim.EmulatedEntrypoint/appsettings.Development.json index 0c208ae..b61e819 100644 --- a/SVSim.EmulatedEntrypoint/appsettings.Development.json +++ b/SVSim.EmulatedEntrypoint/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "BattleNode": { + "SoloDefaultsToScripted": false } } diff --git a/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs b/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs index 461ba9f..a2967c4 100644 --- a/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs @@ -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().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() {