Files
SVSimServer/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs
gamer147 672a89ed46 refactor(matching): IMatchingResolver shared by every do_matching family
SoloDefaultsToScripted was only consulted by ArenaTwoPickBattleController;
RankBattleController did its own inline pair-up + state-code mapping and
ignored the flag entirely. Result: turning on the flag globally only
short-circuited TK2 polls, while rank-battle polls still parked for the
PvpFirstThenAiFallback threshold (15s) before resolving — surfaced today
when the user set the flag and saw rank-battle still queue, then bot-
battle via the client-side AI (not the server-side Scripted lifecycle we
need to test WS traffic against).

New IMatchingResolver owns the cross-cutting decisions:
- honor scriptedOptIn (per-request) OR options.SoloDefaultsToScripted
  (process-wide) — bypass pair-up, register Scripted, return 3004
- otherwise call IMatchingPairUpService.TryPairAsync and translate the
  PairUpResult to the 3002/3004/3007/3011 vocabulary

Family controllers shed the duplicated logic:
- ArenaTwoPickBattleController: ~50 LOC → ~25; preserves ?scripted=1
  query opt-in (parsed permissively for "1"/"true") and the
  ArenaTwoPickException catch
- RankBattleController: ~30 LOC → ~12; preserves the 3001 mapping for
  InvalidOperationException (no deck for format) and card_master_id
  emission

DoMatchingContractTests is the durable enforcement: parametrized over
TK2 + rotation + unlimited rank, asserts SoloDefaultsToScripted=true
makes every family's first poll skip 3002 and return SUCCEEDED with a
battle_id + node_server_url. Adding a fourth family that forgets to
route through IMatchingResolver fails this test — that's the point.

MatchingResolverTests covers the six resolver paths in isolation with
mocks; per-test Harness locals (not fixture-level fields) because the
assembly is [Parallelizable(ParallelScope.All)] and shared mocks race.

957 tests passing (was 948; +9: 6 resolver + 3 contract parametrizations).
No regressions in the existing TK2 / rank-battle controller suites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:18:48 -04:00

84 lines
3.1 KiB
C#

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;
namespace SVSim.EmulatedEntrypoint.Controllers;
[Route("arena_two_pick_battle")]
public class ArenaTwoPickBattleController : SVSimController
{
private readonly IArenaTwoPickService _svc;
private readonly IMatchContextBuilder _matchContextBuilder;
private readonly IMatchingResolver _resolver;
public ArenaTwoPickBattleController(
IArenaTwoPickService svc,
IMatchContextBuilder matchContextBuilder,
IMatchingResolver resolver)
{
_svc = svc;
_matchContextBuilder = matchContextBuilder;
_resolver = resolver;
}
[HttpPost("do_matching")]
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 per-request opt-in for the Scripted
// path. ASP.NET's default bool binder rejects "1", so parse permissively here.
// BattleNodeOptions.SoloDefaultsToScripted is the process-wide equivalent and is
// applied inside the resolver.
var scriptedOptIn = scripted is not null
&& (scripted == "1" || string.Equals(scripted, "true", StringComparison.OrdinalIgnoreCase));
try
{
var ctx = await _matchContextBuilder.BuildForTwoPickAsync(vid);
var r = await _resolver.ResolveAsync("arena_two_pick_battle", new BattlePlayer(vid, ctx), scriptedOptIn, ct);
return Ok(new DoMatchingResponseDto
{
MatchingState = r.MatchingState,
BattleId = r.BattleId,
NodeServerUrl = r.NodeServerUrl,
});
}
catch (ArenaTwoPickException ex)
{
return BadRequest(new { error_code = ex.ErrorCode });
}
}
[HttpPost("finish")]
public async Task<IActionResult> Finish([FromBody] BattleFinishRequest req)
{
if (!TryGetViewerId(out var vid)) return Unauthorized();
try
{
var result = await _svc.RecordBattleResultAsync(vid, req.BattleResult == 1);
return Ok(new BattleFinishResponseDto
{
BattleResult = result.BattleResult,
GetClassExperience = result.GetClassExperience,
ClassExperience = result.ClassExperience,
ClassLevel = result.ClassLevel,
SpotPointInfo = new SpotPointInfoDto
{
BeforeSpotPoint = result.BeforeSpotPoint,
AddSpotPoint = result.AddSpotPoint,
AfterSpotPoint = result.AfterSpotPoint,
},
});
}
catch (ArenaTwoPickException ex)
{
return BadRequest(new { error_code = ex.ErrorCode });
}
}
}