diff --git a/SVSim.BattleNode/Bridge/IMatchingBridge.cs b/SVSim.BattleNode/Bridge/IMatchingBridge.cs index bc41966..3a3f577 100644 --- a/SVSim.BattleNode/Bridge/IMatchingBridge.cs +++ b/SVSim.BattleNode/Bridge/IMatchingBridge.cs @@ -3,10 +3,10 @@ namespace SVSim.BattleNode.Bridge; public interface IMatchingBridge { /// - /// Mint a battle id, register a pending session for the given viewer, and return the - /// URL the client should open a socket to. + /// Mint a battle id, register a pending session for the given viewer with their per-battle + /// MatchContext snapshot, and return the URL the client should open a socket to. /// - PendingMatch RegisterPendingBattle(long viewerId); + PendingMatch RegisterPendingBattle(long viewerId, MatchContext context); } public sealed record PendingMatch(string BattleId, string NodeServerUrl); diff --git a/SVSim.BattleNode/Bridge/MatchingBridge.cs b/SVSim.BattleNode/Bridge/MatchingBridge.cs index 2d4e419..706c2d4 100644 --- a/SVSim.BattleNode/Bridge/MatchingBridge.cs +++ b/SVSim.BattleNode/Bridge/MatchingBridge.cs @@ -21,7 +21,7 @@ public sealed class MatchingBridge : IMatchingBridge _options = options; } - public PendingMatch RegisterPendingBattle(long viewerId) + public PendingMatch RegisterPendingBattle(long viewerId, MatchContext context) { // 12-digit decimal battle id mirrors the captures (e.g. "975695075012"). // Two unbiased 6-digit draws concatenated — RandomNumberGenerator.GetInt32 uses @@ -31,7 +31,7 @@ public sealed class MatchingBridge : IMatchingBridge var hi = RandomNumberGenerator.GetInt32(0, 1_000_000); var lo = RandomNumberGenerator.GetInt32(0, 1_000_000); var battleId = $"{hi:D6}{lo:D6}"; - _store.RegisterPending(new PendingBattle(battleId, viewerId)); + _store.RegisterPending(new PendingBattle(battleId, viewerId, context)); return new PendingMatch(battleId, _options.NodeServerUrl); } } diff --git a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs index dc3a1fa..79e81f1 100644 --- a/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs +++ b/SVSim.BattleNode/Hosting/BattleNodeWebSocketHandler.cs @@ -104,7 +104,7 @@ public sealed class BattleNodeWebSocketHandler var ws = await ctx.WebSockets.AcceptWebSocketAsync(); _store.RemovePending(battleId); - var session = new BattleSession(ws, battleId, viewerId, _loggerFactory.CreateLogger()); + var session = new BattleSession(ws, battleId, viewerId, pending.Context, _loggerFactory.CreateLogger()); await session.RunAsync(ctx.RequestAborted); } diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index 43b89c9..0e62791 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -2,6 +2,7 @@ using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Lifecycle; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Protocol.Bodies; @@ -37,12 +38,19 @@ public sealed class BattleSession public InboundTracker Inbound { get; } = new(); public OutboundSequencer Outbound { get; } = new(); - public BattleSession(WebSocket ws, string battleId, long viewerId, ILogger log) + /// + /// Player-side snapshot captured at do_matching time. ScriptedLifecycle reads the player + /// half of Matched/BattleStart frames from here; opponent half stays in ScriptedProfiles. + /// + internal MatchContext Context { get; } + + public BattleSession(WebSocket ws, string battleId, long viewerId, MatchContext context, ILogger log) { _ws = ws; _log = log; BattleId = battleId; ViewerId = viewerId; + Context = context; } /// diff --git a/SVSim.BattleNode/Sessions/PendingBattle.cs b/SVSim.BattleNode/Sessions/PendingBattle.cs index 28dbb88..5a6bc0a 100644 --- a/SVSim.BattleNode/Sessions/PendingBattle.cs +++ b/SVSim.BattleNode/Sessions/PendingBattle.cs @@ -1,7 +1,10 @@ +using SVSim.BattleNode.Bridge; + namespace SVSim.BattleNode.Sessions; /// -/// Sparse pre-connect record: enough to validate the incoming WS connect and resolve -/// the viewer. Full BattleSession is created on connect. +/// Sparse pre-connect record: viewer id + the per-battle MatchContext snapshot. Enough to +/// validate the incoming WS connect, resolve the viewer, and seed the BattleSession with the +/// player-half lifecycle data. Full BattleSession is created on connect. /// -public sealed record PendingBattle(string BattleId, long ViewerId); +public sealed record PendingBattle(string BattleId, long ViewerId, MatchContext Context); diff --git a/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs b/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs index c2f2322..06d14d4 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/ArenaTwoPickBattleController.cs @@ -11,24 +11,37 @@ public class ArenaTwoPickBattleController : SVSimController { private readonly IArenaTwoPickService _svc; private readonly IMatchingBridge _matching; + private readonly IMatchContextBuilder _matchContextBuilder; - public ArenaTwoPickBattleController(IArenaTwoPickService svc, IMatchingBridge matching) + public ArenaTwoPickBattleController( + IArenaTwoPickService svc, + IMatchingBridge matching, + IMatchContextBuilder matchContextBuilder) { _svc = svc; _matching = matching; + _matchContextBuilder = matchContextBuilder; } [HttpPost("do_matching")] - public IActionResult DoMatching([FromBody] DoMatchingRequest req) + public async Task DoMatching([FromBody] DoMatchingRequest req) { if (!TryGetViewerId(out var vid)) return Unauthorized(); - var match = _matching.RegisterPendingBattle(vid); - return Ok(new DoMatchingResponseDto + try { - MatchingState = 3004, - BattleId = match.BattleId, - NodeServerUrl = match.NodeServerUrl, - }); + var ctx = await _matchContextBuilder.BuildForTwoPickAsync(vid); + var match = _matching.RegisterPendingBattle(vid, ctx); + return Ok(new DoMatchingResponseDto + { + MatchingState = 3004, + BattleId = match.BattleId, + NodeServerUrl = match.NodeServerUrl, + }); + } + catch (ArenaTwoPickException ex) + { + return BadRequest(new { error_code = ex.ErrorCode }); + } } [HttpPost("finish")] diff --git a/SVSim.UnitTests/BattleNode/Bridge/MatchingBridgeTests.cs b/SVSim.UnitTests/BattleNode/Bridge/MatchingBridgeTests.cs index b4c7473..b37e5ce 100644 --- a/SVSim.UnitTests/BattleNode/Bridge/MatchingBridgeTests.cs +++ b/SVSim.UnitTests/BattleNode/Bridge/MatchingBridgeTests.cs @@ -12,14 +12,16 @@ public class MatchingBridgeTests { var store = new InMemoryBattleSessionStore(); var bridge = new MatchingBridge(store, new BattleNodeOptions { NodeServerUrl = "localhost:5148/socket.io/" }); + var ctx = FixtureCtx(); - var match = bridge.RegisterPendingBattle(viewerId: 906243102); + var match = bridge.RegisterPendingBattle(viewerId: 906243102, context: ctx); Assert.That(match.NodeServerUrl, Is.EqualTo("localhost:5148/socket.io/")); Assert.That(match.BattleId, Is.Not.Empty); var pending = store.TryGetPending(match.BattleId); Assert.That(pending, Is.Not.Null); Assert.That(pending!.ViewerId, Is.EqualTo(906243102)); + Assert.That(pending.Context, Is.SameAs(ctx)); } [Test] @@ -27,8 +29,8 @@ public class MatchingBridgeTests { var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions()); - var a = bridge.RegisterPendingBattle(1); - var b = bridge.RegisterPendingBattle(2); + var a = bridge.RegisterPendingBattle(1, FixtureCtx()); + var b = bridge.RegisterPendingBattle(2, FixtureCtx()); Assert.That(a.BattleId, Is.Not.EqualTo(b.BattleId)); } @@ -38,9 +40,16 @@ public class MatchingBridgeTests { var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions()); - var match = bridge.RegisterPendingBattle(viewerId: 1); + var match = bridge.RegisterPendingBattle(viewerId: 1, context: FixtureCtx()); Assert.That(match.BattleId, Has.Length.EqualTo(12)); Assert.That(match.BattleId, Does.Match("^[0-9]{12}$")); } + + private static MatchContext FixtureCtx() => new( + SelfDeckCardIds: Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(), + ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015", + CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", + EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0, + BattleType: 11); } diff --git a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs index be89b4c..58216cf 100644 --- a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs +++ b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs @@ -14,10 +14,9 @@ public class BattleNodeFlowTests { /// /// End-to-end smoke for the v1 scripted lifecycle. Boots the EmulatedEntrypoint via - /// SVSimTestFactory (in-memory SQLite + reference-data CSV import), mints a battle - /// through IMatchingBridge, opens a raw Socket.IO v2 client against the in-process - /// TestServer, and drives InitNetwork → Loaded → Swap, asserting the right scripted - /// frames come back in order. + /// SVSimTestFactory, mints a battle through IMatchingBridge with a fixture MatchContext, + /// opens a raw Socket.IO v2 client against the in-process TestServer, and drives + /// InitNetwork → Loaded → Swap, asserting the right scripted frames come back in order. /// [Test] [Timeout(30000)] @@ -28,11 +27,10 @@ public class BattleNodeFlowTests using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var ct = cts.Token; - var pending = bridge.RegisterPendingBattle(viewerId: 906243102); + var pending = bridge.RegisterPendingBattle(viewerId: 906243102, context: FixtureCtx()); var key = MakeKey(); var encryptedVid = NodeCrypto.EncryptForNode("906243102", key); - // TestServer ignores the host portion of the URI — only the path + query route. var wsUri = new Uri($"ws://localhost/socket.io/?BattleId={pending.BattleId}&viewerId={Uri.EscapeDataString(encryptedVid)}&EIO=3&transport=websocket"); var wsClient = factory.Server.CreateWebSocketClient(); @@ -40,20 +38,16 @@ public class BattleNodeFlowTests await using var client = new RawSocketIoTestClient(ws); await client.ConsumeHandshakeAsync(ct); - // 1. InitNetwork → expect InitNetwork ack push only. await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.InitNetwork)); - // 2. InitBattle → expect Matched (handler is now subscribed on the client side). await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitBattle, pubSeq: 2), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Matched)); - // 3. Loaded → expect BattleStart + Deal. await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 3), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.BattleStart)); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal)); - // 4. Swap with empty idxList → expect Swap response + Ready. await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 4, body: new Dictionary { ["idxList"] = new List() }), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Swap)); @@ -62,7 +56,6 @@ public class BattleNodeFlowTests private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri, long pubSeq, Dictionary? body = null) => new(uri, ViewerId: 906243102, Uuid: "udid-test", Bid: null, Try: 0, - // EmitMsgPack: InitNetwork → general(99); other matching URIs → matching(2); else battle(1). Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General : uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching : EmitCategory.Battle, @@ -73,4 +66,11 @@ public class BattleNodeFlowTests var seq = 0; return NodeCrypto.GenerateKey(() => (seq++ * 13) % 16); } + + internal static MatchContext FixtureCtx(IReadOnlyList? deck = null) => new( + SelfDeckCardIds: deck ?? Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(), + ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015", + CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", + EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0, + BattleType: 11); } diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs index 50b28ce..35e01d5 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs @@ -1,6 +1,7 @@ // SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Protocol.Bodies; using SVSim.BattleNode.Sessions; @@ -13,9 +14,16 @@ public class BattleSessionDispatchTests private static BattleSession NewSession() { // ws is unused by ComputeResponses; pass null! and rely on the test never invoking the pump. - return new BattleSession(ws: null!, battleId: "bid-1", viewerId: 1, log: NullLogger.Instance); + return new BattleSession(ws: null!, battleId: "bid-1", viewerId: 1, context: FixtureCtx(), log: NullLogger.Instance); } + private static MatchContext FixtureCtx() => new( + SelfDeckCardIds: Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(), + ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015", + CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", + EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0, + BattleType: 11); + private static MsgEnvelope NewEnvelope(NetworkBattleUri uri) => new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0, Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null, Body: new RawBody(new Dictionary())); diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionPumpTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionPumpTests.cs index 106accd..c173caa 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionPumpTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionPumpTests.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Sessions; using SVSim.BattleNode.Wire; @@ -39,7 +40,7 @@ public class BattleSessionPumpTests { var ws = new TestWebSocket(); var session = new BattleSession( - ws: ws, battleId: "bid-pump", viewerId: 906243102, + ws: ws, battleId: "bid-pump", viewerId: 906243102, context: FixtureCtx(), log: NullLogger.Instance); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8)); @@ -84,7 +85,7 @@ public class BattleSessionPumpTests { var ws = new TestWebSocket(); var session = new BattleSession( - ws: ws, battleId: "bid-cancel", viewerId: 906243102, + ws: ws, battleId: "bid-cancel", viewerId: 906243102, context: FixtureCtx(), log: NullLogger.Instance); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8)); @@ -127,7 +128,7 @@ public class BattleSessionPumpTests var ws = new TestWebSocket(); var logCapture = new CapturingLogger(); var session = new BattleSession( - ws: ws, battleId: "bid-clip", viewerId: 906243102, log: logCapture); + ws: ws, battleId: "bid-clip", viewerId: 906243102, context: FixtureCtx(), log: logCapture); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8)); var runTask = session.RunAsync(cts.Token); @@ -208,6 +209,13 @@ public class BattleSessionPumpTests return NodeCrypto.GenerateKey(() => (seq++ * 7) % 16); } + private static MatchContext FixtureCtx() => new( + SelfDeckCardIds: Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(), + ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015", + CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", + EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0, + BattleType: 11); + private sealed class CapturingLogger : ILogger { public List Warnings { get; } = new(); diff --git a/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs b/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs index 7582ea0..02d863f 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Sessions; namespace SVSim.UnitTests.BattleNode.Sessions; @@ -13,7 +14,7 @@ public class InMemoryBattleSessionStoreTests [Test] public void RegisterThenGet_ReturnsRegisteredBattle() { - var battle = new PendingBattle("bid-1", 906243102); + var battle = new PendingBattle("bid-1", 906243102, FixtureCtx()); _store.RegisterPending(battle); Assert.That(_store.TryGetPending("bid-1"), Is.EqualTo(battle)); @@ -28,7 +29,7 @@ public class InMemoryBattleSessionStoreTests [Test] public void Remove_ReturnsTrueWhenPresent_FalseWhenAbsent() { - _store.RegisterPending(new PendingBattle("bid", 1)); + _store.RegisterPending(new PendingBattle("bid", 1, FixtureCtx())); Assert.That(_store.RemovePending("bid"), Is.True); Assert.That(_store.RemovePending("bid"), Is.False); } @@ -36,8 +37,15 @@ public class InMemoryBattleSessionStoreTests [Test] public void Register_DuplicateBattleId_OverwritesPrior() { - _store.RegisterPending(new PendingBattle("bid", 1)); - _store.RegisterPending(new PendingBattle("bid", 2)); + _store.RegisterPending(new PendingBattle("bid", 1, FixtureCtx())); + _store.RegisterPending(new PendingBattle("bid", 2, FixtureCtx())); Assert.That(_store.TryGetPending("bid")!.ViewerId, Is.EqualTo(2)); } + + private static MatchContext FixtureCtx() => new( + SelfDeckCardIds: Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(), + ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015", + CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", + EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0, + BattleType: 11); } diff --git a/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs b/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs index 9aa6b4a..edee5e3 100644 --- a/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/ArenaTwoPickBattleControllerTests.cs @@ -1,6 +1,9 @@ using System.Net; using System.Net.Http.Json; using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Models; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Controllers; @@ -12,6 +15,8 @@ public class ArenaTwoPickBattleControllerTests { using var factory = new SVSimTestFactory(); var viewerId = await factory.SeedViewerAsync(); + await SeedCompleteTwoPickRunAsync(factory, viewerId); + using var client = factory.CreateAuthenticatedClient(viewerId); var req = new { deck_no = 1L, need_init = 1, log = 1, excluded_field_id_list = new long[] { }, use_stage_select = 1, is_default_skin = 0, @@ -28,12 +33,50 @@ public class ArenaTwoPickBattleControllerTests var battleId = root.GetProperty("battle_id").GetString(); Assert.That(battleId, Is.Not.Null.And.Not.Empty); var nodeUrl = root.GetProperty("node_server_url").GetString(); - // Matches prod wire format: host:port/socket.io/, no scheme prefix. Assert.That(nodeUrl, Does.Contain("/socket.io/")); Assert.That(nodeUrl, Does.Not.StartWith("ws://")); Assert.That(nodeUrl, Does.Not.StartWith("http://")); - // Required when matching_state ∈ {3004,3007,3011} per - // DoMatchingBase.SettingCardMasterId; client throws KeyNotFoundException without it. Assert.That(root.GetProperty("card_master_id").GetInt32(), Is.EqualTo(1)); } + + [Test] + public async Task DoMatching_NoActiveRun_Returns400WithErrorCode() + { + using var factory = new SVSimTestFactory(); + var viewerId = await factory.SeedViewerAsync(); + using var client = factory.CreateAuthenticatedClient(viewerId); + var req = new { + deck_no = 1L, need_init = 1, log = 1, excluded_field_id_list = new long[] { }, use_stage_select = 1, is_default_skin = 0, + viewer_id = "0", steam_id = 0, steam_session_ticket = "", + }; + var resp = await client.PostAsync("/arena_two_pick_battle/do_matching", JsonContent.Create(req)); + + Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + var body = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(body); + Assert.That(doc.RootElement.GetProperty("error_code").GetString(), + Is.EqualTo("arena_two_pick_no_active_run")); + } + + private static async Task SeedCompleteTwoPickRunAsync(SVSimTestFactory factory, long viewerId) + { + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var deck = Enumerable.Range(1, 30).Select(i => 100_011_000L + i).ToList(); + db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun + { + ViewerId = viewerId, + EntryId = 1, + ClassId = 1, + LeaderSkinId = 1, + SelectedCardIdsJson = JsonSerializer.Serialize(deck), + IsSelectCompleted = true, + MaxBattleCount = 5, + CandidateClassIdsJson = "[1,2,3]", + PendingPickSetsJson = "[]", + ResultListJson = "[]", + NextCandidateId = 1, + }); + await db.SaveChangesAsync(); + } }