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