feat(arena-tk2): PvP pair-up trigger via /do_matching, ?scripted=1 opt-in

Solo pollers park (3001 RETRY); two concurrent pollers pair and both
receive 3004 + same BattleId. Cache hits on the first arriver's next
poll. ?scripted=1 retains today's solo Scripted path for dev work.
Response DTO's BattleId/NodeServerUrl become nullable so 3001 omits
them on the wire (WhenWritingNull policy drops them).

ASP.NET's default bool binder rejects "1" as a value, so the scripted
opt-in is bound as string? and parsed permissively (accepts "1" and
"true"/"True"/etc.) rather than relying on built-in bool binding.
This commit is contained in:
gamer147
2026-06-01 22:14:04 -04:00
parent 28b1d7531a
commit 225c20daeb
3 changed files with 132 additions and 10 deletions

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using SVSim.BattleNode.Bridge;
using SVSim.EmulatedEntrypoint.Matching;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ArenaTwoPick;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ArenaTwoPick;
using SVSim.EmulatedEntrypoint.Services;
@@ -12,33 +13,67 @@ public class ArenaTwoPickBattleController : SVSimController
private readonly IArenaTwoPickService _svc;
private readonly IMatchingBridge _matching;
private readonly IMatchContextBuilder _matchContextBuilder;
private readonly IMatchingPairUpService _pairUp;
public ArenaTwoPickBattleController(
IArenaTwoPickService svc,
IMatchingBridge matching,
IMatchContextBuilder matchContextBuilder)
IMatchContextBuilder matchContextBuilder,
IMatchingPairUpService pairUp)
{
_svc = svc;
_matching = matching;
_matchContextBuilder = matchContextBuilder;
_pairUp = pairUp;
}
[HttpPost("do_matching")]
public async Task<IActionResult> DoMatching([FromBody] DoMatchingRequest req)
public async Task<IActionResult> DoMatching(
[FromBody] DoMatchingRequest req,
[FromQuery(Name = "scripted")] string? scripted = null,
CancellationToken ct = default)
{
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));
try
{
var ctx = await _matchContextBuilder.BuildForTwoPickAsync(vid);
var match = _matching.RegisterBattle(
if (useScripted)
{
var scriptedMatch = _matching.RegisterBattle(
new SVSim.BattleNode.Bridge.BattlePlayer(vid, ctx),
p2: null,
SVSim.BattleNode.Sessions.BattleType.Scripted);
return Ok(new DoMatchingResponseDto
{
MatchingState = 3004,
BattleId = scriptedMatch.BattleId,
NodeServerUrl = scriptedMatch.NodeServerUrl,
});
}
var paired = await _pairUp.TryPairAsync(
"arena_two_pick_battle",
new SVSim.BattleNode.Bridge.BattlePlayer(vid, ctx),
p2: null,
SVSim.BattleNode.Sessions.BattleType.Scripted);
ct);
if (paired is null)
{
return Ok(new DoMatchingResponseDto
{
MatchingState = 3001,
// BattleId / NodeServerUrl null — client polls again.
});
}
return Ok(new DoMatchingResponseDto
{
MatchingState = 3004,
BattleId = match.BattleId,
NodeServerUrl = match.NodeServerUrl,
BattleId = paired.BattleId,
NodeServerUrl = paired.NodeServerUrl,
});
}
catch (ArenaTwoPickException ex)

View File

@@ -17,10 +17,10 @@ public sealed class DoMatchingResponseDto
public int RetryPeriod { get; set; } = 3;
[JsonPropertyName("battle_id")] [Key("battle_id")]
public string BattleId { get; set; } = "";
public string? BattleId { get; set; }
[JsonPropertyName("node_server_url")] [Key("node_server_url")]
public string NodeServerUrl { get; set; } = "";
public string? NodeServerUrl { get; set; }
// Required by the client when matching_state ∈ {3004, 3007, 3011} —
// DoMatchingBase.SettingCardMasterId does jsonData["card_master_id"].ToInt()