From a81289311f59a48c4cbd170053c7299ee590b7a4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 10 Jun 2026 08:10:01 -0400 Subject: [PATCH] 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 --- .../Controllers/RankBattleController.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs b/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs index 2a88d00..f33d275 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs @@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Mvc; using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Sessions; using SVSim.Database.Enums; +using SVSim.Database.Services.Replay; using SVSim.EmulatedEntrypoint.Constants; +using SVSim.EmulatedEntrypoint.Extensions; using SVSim.EmulatedEntrypoint.Matching; using SVSim.EmulatedEntrypoint.Models.Dtos.RankBattle; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; @@ -27,6 +29,7 @@ public sealed class RankBattleController : ControllerBase private readonly IBattleSessionStore _sessionStore; private readonly IMatchContextBuilder _ctxBuilder; private readonly IBotRoster _botRoster; + private readonly IBattleContextStore _battleContextStore; private readonly ILogger _log; public RankBattleController( @@ -34,12 +37,14 @@ public sealed class RankBattleController : ControllerBase IBattleSessionStore sessionStore, IMatchContextBuilder ctxBuilder, IBotRoster botRoster, + IBattleContextStore battleContextStore, ILogger log) { _resolver = resolver; _sessionStore = sessionStore; _ctxBuilder = ctxBuilder; _botRoster = botRoster; + _battleContextStore = battleContextStore; _log = log; } @@ -165,6 +170,33 @@ public sealed class RankBattleController : ControllerBase var bot = await _botRoster.PickAsync(selfCtx, pending.BattleId, ct); 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). return Ok(new AiBattleStartResponseDto {