feat(replay): stash battle context at /ai_*/start time

AiStartInternal now writes a BattleContext keyed by viewer id; the next
commit consumes it in /finish to write a ViewerBattleHistory row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-10 08:10:01 -04:00
parent b44354315a
commit a81289311f

View File

@@ -3,7 +3,9 @@ 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.Replay;
using SVSim.EmulatedEntrypoint.Constants; using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Extensions;
using SVSim.EmulatedEntrypoint.Matching; using SVSim.EmulatedEntrypoint.Matching;
using SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle; using SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
@@ -27,6 +29,7 @@ public sealed class RankBattleController : ControllerBase
private readonly IBattleSessionStore _sessionStore; private readonly IBattleSessionStore _sessionStore;
private readonly IMatchContextBuilder _ctxBuilder; private readonly IMatchContextBuilder _ctxBuilder;
private readonly IBotRoster _botRoster; private readonly IBotRoster _botRoster;
private readonly IBattleContextStore _battleContextStore;
private readonly ILogger<RankBattleController> _log; private readonly ILogger<RankBattleController> _log;
public RankBattleController( public RankBattleController(
@@ -34,12 +37,14 @@ public sealed class RankBattleController : ControllerBase
IBattleSessionStore sessionStore, IBattleSessionStore sessionStore,
IMatchContextBuilder ctxBuilder, IMatchContextBuilder ctxBuilder,
IBotRoster botRoster, IBotRoster botRoster,
IBattleContextStore battleContextStore,
ILogger<RankBattleController> log) ILogger<RankBattleController> log)
{ {
_resolver = resolver; _resolver = resolver;
_sessionStore = sessionStore; _sessionStore = sessionStore;
_ctxBuilder = ctxBuilder; _ctxBuilder = ctxBuilder;
_botRoster = botRoster; _botRoster = botRoster;
_battleContextStore = battleContextStore;
_log = log; _log = log;
} }
@@ -165,6 +170,33 @@ public sealed class RankBattleController : ControllerBase
var bot = await _botRoster.PickAsync(selfCtx, pending.BattleId, ct); var bot = await _botRoster.PickAsync(selfCtx, pending.BattleId, ct);
var seed = Random.Shared.Next(); var seed = Random.Shared.Next();
// Stash battle context for the upcoming /finish so the replay-history hook can
// compose a ViewerBattleHistory row. See docs/superpowers/specs/2026-06-10-replay-info-design.md.
if (long.TryParse(pending.BattleId, out var battleIdLong))
{
_battleContextStore.Set(vid, new BattleContext(
BattleId: battleIdLong,
// Wire battle_type: 2 = rank battle (per docs/api-spec/common/types.ts.md
// #battle-types). AI variant shares the rank-battle wire id.
BattleType: 2,
DeckFormat: format.ToApi(), // wire-int via existing converter
TwoPickType: 0,
SelfClassId: (int)selfCtx.ClassId, // CardClass enum
SelfSubClassId: 0,
SelfCharaId: int.TryParse(selfCtx.CharaId, out var ch) ? ch : 0, // CharaId is string on MatchContext
SelfRotationId: "0",
OpponentViewerId: 0, // AI bot — not a real viewer
OpponentName: bot.UserName,
OpponentClassId: bot.ClassId, // int on AIBotProfile
OpponentSubClassId: 0,
OpponentCharaId: bot.CharaId, // int on AIBotProfile
OpponentCountryCode: bot.CountryCode,
OpponentEmblemId: bot.EmblemId, // int → long widen
OpponentDegreeId: bot.DegreeId, // int → long widen
OpponentRotationId: "0",
BattleStartTime: DateTime.UtcNow));
}
// Per spec, ai-start.md TODO: turnState semantics unclear. Default 0 (player first). // Per spec, ai-start.md TODO: turnState semantics unclear. Default 0 (player first).
return Ok(new AiBattleStartResponseDto return Ok(new AiBattleStartResponseDto
{ {