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 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 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 }); } } }