Two client-crash bugs in the do_matching response when no partner is waiting: 1. matching_state was 3001 (RC_BATTLE_MATCHING_ILLEGAL); the client's Matching.OnFinishedDoMatching switch maps that to an error dialog, not a retry. The retry state is 3002 (RC_BATTLE_MATCHING_RETRY). 2. node_server_url was omitted entirely. The client's DoMatchingBase.SettingDoMatchingData reads it via data["node_server_url"].ToString() with no Keys.Contains guard, so absence throws KeyNotFoundException out of NetworkManager.Connect before the matching_state switch is even reached. Prod RETRY captures send "" while waiting and the real URL only on SUCCEEDED; match that. battle_id stays absent; its accessor IS guarded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
118 lines
4.4 KiB
C#
118 lines
4.4 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;
|
|
|
|
public ArenaTwoPickBattleController(
|
|
IArenaTwoPickService svc,
|
|
IMatchingBridge matching,
|
|
IMatchContextBuilder matchContextBuilder,
|
|
IMatchingPairUpService pairUp)
|
|
{
|
|
_svc = svc;
|
|
_matching = matching;
|
|
_matchContextBuilder = matchContextBuilder;
|
|
_pairUp = pairUp;
|
|
}
|
|
|
|
[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.
|
|
var useScripted = scripted is not null
|
|
&& (scripted == "1" || string.Equals(scripted, "true", StringComparison.OrdinalIgnoreCase));
|
|
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 = "",
|
|
});
|
|
}
|
|
|
|
return Ok(new DoMatchingResponseDto
|
|
{
|
|
MatchingState = 3004,
|
|
BattleId = paired.BattleId,
|
|
NodeServerUrl = paired.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 });
|
|
}
|
|
}
|
|
}
|