feat(replay): wire finish hook for rank-battle family
Finish now consumes the stashed BattleContext, records a ViewerBattleHistory row (idempotent + retention-capped), and calls IPlayedTogetherWriter for human PvP (skipped for AI). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using SVSim.BattleNode.Bridge;
|
using SVSim.BattleNode.Bridge;
|
||||||
using SVSim.BattleNode.Sessions;
|
using SVSim.BattleNode.Sessions;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Services.Friend;
|
||||||
using SVSim.Database.Services.Replay;
|
using SVSim.Database.Services.Replay;
|
||||||
using SVSim.EmulatedEntrypoint.Constants;
|
using SVSim.EmulatedEntrypoint.Constants;
|
||||||
using SVSim.EmulatedEntrypoint.Extensions;
|
using SVSim.EmulatedEntrypoint.Extensions;
|
||||||
@@ -30,6 +31,8 @@ public sealed class RankBattleController : ControllerBase
|
|||||||
private readonly IMatchContextBuilder _ctxBuilder;
|
private readonly IMatchContextBuilder _ctxBuilder;
|
||||||
private readonly IBotRoster _botRoster;
|
private readonly IBotRoster _botRoster;
|
||||||
private readonly IBattleContextStore _battleContextStore;
|
private readonly IBattleContextStore _battleContextStore;
|
||||||
|
private readonly IBattleHistoryWriter _historyWriter;
|
||||||
|
private readonly IPlayedTogetherWriter _playedTogetherWriter;
|
||||||
private readonly ILogger<RankBattleController> _log;
|
private readonly ILogger<RankBattleController> _log;
|
||||||
|
|
||||||
public RankBattleController(
|
public RankBattleController(
|
||||||
@@ -38,6 +41,8 @@ public sealed class RankBattleController : ControllerBase
|
|||||||
IMatchContextBuilder ctxBuilder,
|
IMatchContextBuilder ctxBuilder,
|
||||||
IBotRoster botRoster,
|
IBotRoster botRoster,
|
||||||
IBattleContextStore battleContextStore,
|
IBattleContextStore battleContextStore,
|
||||||
|
IBattleHistoryWriter historyWriter,
|
||||||
|
IPlayedTogetherWriter playedTogetherWriter,
|
||||||
ILogger<RankBattleController> log)
|
ILogger<RankBattleController> log)
|
||||||
{
|
{
|
||||||
_resolver = resolver;
|
_resolver = resolver;
|
||||||
@@ -45,6 +50,8 @@ public sealed class RankBattleController : ControllerBase
|
|||||||
_ctxBuilder = ctxBuilder;
|
_ctxBuilder = ctxBuilder;
|
||||||
_botRoster = botRoster;
|
_botRoster = botRoster;
|
||||||
_battleContextStore = battleContextStore;
|
_battleContextStore = battleContextStore;
|
||||||
|
_historyWriter = historyWriter;
|
||||||
|
_playedTogetherWriter = playedTogetherWriter;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +91,29 @@ public sealed class RankBattleController : ControllerBase
|
|||||||
[HttpPost("/unlimited_rank_battle/finish")]
|
[HttpPost("/unlimited_rank_battle/finish")]
|
||||||
[HttpPost("/ai_rotation_rank_battle/finish")]
|
[HttpPost("/ai_rotation_rank_battle/finish")]
|
||||||
[HttpPost("/ai_unlimited_rank_battle/finish")]
|
[HttpPost("/ai_unlimited_rank_battle/finish")]
|
||||||
public IActionResult Finish([FromBody] RankBattleFinishRequestDto req)
|
public async Task<IActionResult> Finish([FromBody] RankBattleFinishRequestDto req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!TryGetViewerId(out var _)) return Unauthorized();
|
if (!TryGetViewerId(out var vid)) return Unauthorized();
|
||||||
|
|
||||||
|
var ctx = _battleContextStore.TakeFor(vid);
|
||||||
|
bool isWin = req.BattleResult == 1;
|
||||||
|
|
||||||
|
await _historyWriter.RecordAsync(vid, ctx, isWin, ct);
|
||||||
|
|
||||||
|
// Played-together only fires for human PvP. AI bots have OpponentViewerId=0.
|
||||||
|
if (ctx is { OpponentViewerId: > 0 })
|
||||||
|
{
|
||||||
|
await _playedTogetherWriter.RecordAsync(
|
||||||
|
vid,
|
||||||
|
ctx.OpponentViewerId,
|
||||||
|
new BattleParticipationContext(
|
||||||
|
PlayedMode: 0,
|
||||||
|
BattleType: ctx.BattleType,
|
||||||
|
DeckFormat: ctx.DeckFormat,
|
||||||
|
TwoPickType: ctx.TwoPickType),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(new RankBattleFinishResponseDto
|
return Ok(new RankBattleFinishResponseDto
|
||||||
{
|
{
|
||||||
BattleResult = req.BattleResult,
|
BattleResult = req.BattleResult,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services.Replay;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Replay;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Replay;
|
||||||
using SVSim.UnitTests.Infrastructure;
|
using SVSim.UnitTests.Infrastructure;
|
||||||
|
|
||||||
@@ -102,6 +103,76 @@ public class ReplayControllerTests
|
|||||||
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AiRankFinish_writes_history_row_visible_from_ReplayInfo()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL);
|
||||||
|
|
||||||
|
// Inject a pre-stashed BattleContext as if /ai_rotation_rank_battle/start had run.
|
||||||
|
var store = factory.Services.GetRequiredService<IBattleContextStore>();
|
||||||
|
store.Set(viewerId, new BattleContext(
|
||||||
|
BattleId: 234_471_983_876L,
|
||||||
|
BattleType: 2, DeckFormat: 0, TwoPickType: 0,
|
||||||
|
SelfClassId: 8, SelfSubClassId: 0, SelfCharaId: 8, SelfRotationId: "0",
|
||||||
|
OpponentViewerId: 0, OpponentName: "BotName", OpponentClassId: 5,
|
||||||
|
OpponentSubClassId: 0, OpponentCharaId: 805, OpponentCountryCode: "",
|
||||||
|
OpponentEmblemId: 721_341_010L, OpponentDegreeId: 120_023L,
|
||||||
|
OpponentRotationId: "0",
|
||||||
|
BattleStartTime: new DateTime(2026, 6, 4, 17, 13, 13, DateTimeKind.Utc)));
|
||||||
|
|
||||||
|
var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
|
var finishResp = await client.PostAsJsonAsync("/ai_rotation_rank_battle/finish", new
|
||||||
|
{
|
||||||
|
viewer_id = "0",
|
||||||
|
steam_id = 0,
|
||||||
|
steam_session_ticket = "",
|
||||||
|
battle_result = 1, // win
|
||||||
|
class_id = 8,
|
||||||
|
total_turn = 7,
|
||||||
|
evolve_count = 1,
|
||||||
|
enemy_evolve_count = 0,
|
||||||
|
sdtrb = 0,
|
||||||
|
});
|
||||||
|
Assert.That(finishResp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
var infoBody = await client.PostAsJsonAsync("/replay/info", EmptyBody())
|
||||||
|
.ContinueWith(t => t.Result.Content.ReadFromJsonAsync<ReplayInfoResponseDto>())
|
||||||
|
.Unwrap();
|
||||||
|
|
||||||
|
Assert.That(infoBody!.ReplayList, Has.Count.EqualTo(1));
|
||||||
|
var row = infoBody.ReplayList[0];
|
||||||
|
Assert.That(row.BattleId, Is.EqualTo("234471983876"));
|
||||||
|
Assert.That(row.OpponentName, Is.EqualTo("BotName"));
|
||||||
|
Assert.That(row.IsWin, Is.EqualTo("1"));
|
||||||
|
Assert.That(row.OpponentEmblemId, Is.EqualTo("721341010"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task AiRankFinish_with_no_stashed_context_does_not_crash()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long viewerId = await factory.SeedViewerAsync(steamId: 76_561_198_000_000_001UL);
|
||||||
|
|
||||||
|
var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
|
var finishResp = await client.PostAsJsonAsync("/ai_rotation_rank_battle/finish", new
|
||||||
|
{
|
||||||
|
viewer_id = "0",
|
||||||
|
steam_id = 0,
|
||||||
|
steam_session_ticket = "",
|
||||||
|
battle_result = 0,
|
||||||
|
class_id = 1, total_turn = 1, evolve_count = 0, enemy_evolve_count = 0, sdtrb = 0,
|
||||||
|
});
|
||||||
|
Assert.That(finishResp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
var infoBody = await client.PostAsJsonAsync("/replay/info", EmptyBody())
|
||||||
|
.ContinueWith(t => t.Result.Content.ReadFromJsonAsync<ReplayInfoResponseDto>())
|
||||||
|
.Unwrap();
|
||||||
|
Assert.That(infoBody!.ReplayList, Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
private static ViewerBattleHistory NewRow(long viewerId, long battleId, DateTime createTime) => new()
|
private static ViewerBattleHistory NewRow(long viewerId, long battleId, DateTime createTime) => new()
|
||||||
{
|
{
|
||||||
ViewerId = viewerId,
|
ViewerId = viewerId,
|
||||||
|
|||||||
Reference in New Issue
Block a user