Files
SVSimServer/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs
gamer147 0095bdf0cf 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>
2026-06-01 23:48:14 -04:00

128 lines
5.2 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 IMatchingBridge _matching;
private readonly IMatchContextBuilder _matchContextBuilder;
private readonly IMatchingPairUpService _pairUp;
private readonly BattleNodeOptions _battleNodeOptions;
public ArenaTwoPickBattleController(
IArenaTwoPickService svc,
IMatchingBridge matching,
IMatchContextBuilder matchContextBuilder,
IMatchingPairUpService pairUp,
BattleNodeOptions battleNodeOptions)
{
_svc = svc;
_matching = matching;
_matchContextBuilder = matchContextBuilder;
_pairUp = pairUp;
_battleNodeOptions = battleNodeOptions;
}
[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 opt-in for the legacy Scripted path.
// ASP.NET's default bool binder rejects "1", so do a permissive parse here.
// 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);
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),
ct);
if (paired is null)
{
// 3002 = RC_BATTLE_MATCHING_RETRY: client polls again. 3001 is ILLEGAL
// and shows an error dialog on the client side. node_server_url must be
// present (the client's DoMatchingBase.SettingDoMatchingData calls
// .ToString() on it without a Keys.Contains guard); prod sends "" while
// waiting and the real URL only on SUCCEEDED. battle_id stays absent
// (its accessor IS guarded).
return Ok(new DoMatchingResponseDto
{
MatchingState = 3002,
NodeServerUrl = "",
});
}
// Owner (first arriver, cache hit) gets 3007 = RC_BATTLE_MATCHING_SUCCEEDED_OWNER;
// joiner (second arriver who triggered the pair) gets 3004 = RC_BATTLE_MATCHING_SUCCEEDED.
// See PairUpResult docs for why this split is observationally inert in TK2 today.
return Ok(new DoMatchingResponseDto
{
MatchingState = paired.IsOwner ? 3007 : 3004,
BattleId = paired.Match.BattleId,
NodeServerUrl = paired.Match.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 });
}
}
}