diff --git a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs index c88d7cf..395db5e 100644 --- a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs +++ b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs @@ -1,3 +1,4 @@ +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Protocol.Bodies; @@ -9,29 +10,32 @@ namespace SVSim.BattleNode.Lifecycle; /// the dispatch pushes after the player's TurnEnd. The values are templated from the TK2 /// captures at data_dumps/captures/battle-traffic_tk2_regular.ndjson — anything /// hardcoded here came from a real prod frame, with names + provenance in -/// . +/// . The player-half of Matched/BattleStart now reads from +/// instead of . /// public static class ScriptedLifecycle { - /// - /// CardId used for all 30 entries in the dummy deck. A stable neutral card that exists in - /// every card-master version we care about, so the client can render it without - /// triggering a card-master-mismatch error. - /// - public const long DummyCardId = 100011010L; - /// /// Viewer id we present as the opponent on every scripted push. Out-of-range vs. real /// viewer ids so it can't collide with a real account in the auth pipeline. /// public const long FakeOpponentViewerId = 999_999_999L; - public static MsgEnvelope BuildMatched(long playerViewerId, long opponentViewerId, string battleId) => + public static MsgEnvelope BuildMatched(MatchContext ctx, long playerViewerId, long opponentViewerId, string battleId) => EnvelopeForPush(NetworkBattleUri.Matched, new MatchedBody( - SelfInfo: ScriptedProfiles.PlayerMatchedProfile with { OppoId = opponentViewerId }, + SelfInfo: new MatchedSelfInfo( + CountryCode: ctx.CountryCode, + UserName: ctx.UserName, + SleeveId: ctx.SleeveId, + EmblemId: ctx.EmblemId, + DegreeId: ctx.DegreeId, + FieldId: ctx.FieldId, + IsOfficial: ctx.IsOfficial, + OppoId: opponentViewerId, + Seed: ScriptedProfiles.BattleSeed), OppoInfo: ScriptedProfiles.OpponentMatchedProfile with { OppoId = playerViewerId }, - SelfDeck: BuildDummyDeck()), + SelfDeck: BuildPlayerDeck(ctx.SelfDeckCardIds)), bid: battleId); public static MsgEnvelope BuildBattleStart(long playerViewerId) => @@ -104,12 +108,12 @@ public static class ScriptedLifecycle return list; } - private static IReadOnlyList BuildDummyDeck() + private static IReadOnlyList BuildPlayerDeck(IReadOnlyList cardIds) { - var deck = new List(30); - for (var i = 1; i <= 30; i++) + var deck = new List(cardIds.Count); + for (var i = 0; i < cardIds.Count; i++) { - deck.Add(new DeckCardRef(Idx: i, CardId: DummyCardId)); + deck.Add(new DeckCardRef(Idx: i + 1, CardId: cardIds[i])); } return deck; } diff --git a/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs b/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs index 081bb4b..8697eed 100644 --- a/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs +++ b/SVSim.BattleNode/Lifecycle/ScriptedProfiles.cs @@ -15,18 +15,6 @@ internal static class ScriptedProfiles // From frame[2] (Matched). public const long BattleSeed = 17_548_138L; - // From frame[2] (Matched). OppoId is overwritten per battle from the bridge. - public static readonly MatchedSelfInfo PlayerMatchedProfile = new( - CountryCode: "KOR", - UserName: "Player", - SleeveId: "3000011", - EmblemId: "701441011", - DegreeId: "300003", - FieldId: 43, - IsOfficial: 0, - OppoId: 0, - Seed: BattleSeed); - public static readonly MatchedOppoInfo OpponentMatchedProfile = new( CountryCode: "JPN", UserName: "Opponent", diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index 0e62791..d22a622 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -235,7 +235,7 @@ public sealed class BattleSession Phase = BattleSessionPhase.AwaitingInitBattle; break; case NetworkBattleUri.InitBattle when Phase == BattleSessionPhase.AwaitingInitBattle: - result.Add((ScriptedLifecycle.BuildMatched(ViewerId, ScriptedLifecycle.FakeOpponentViewerId, BattleId), NoStock: false)); + result.Add((ScriptedLifecycle.BuildMatched(Context, ViewerId, ScriptedLifecycle.FakeOpponentViewerId, BattleId), NoStock: false)); Phase = BattleSessionPhase.AwaitingLoaded; break; case NetworkBattleUri.Loaded when Phase == BattleSessionPhase.AwaitingLoaded: diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs index c232553..11093fb 100644 --- a/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs +++ b/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Lifecycle; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Protocol.Bodies; @@ -11,26 +12,61 @@ public class ScriptedLifecycleTests [Test] public void BuildMatched_PutsOppoIdInSelfInfoEqualToTheRealOpponentVid() { - var env = ScriptedLifecycle.BuildMatched(playerViewerId: 906243102, opponentViewerId: 847666884, battleId: "b"); + var env = ScriptedLifecycle.BuildMatched(FixtureCtx(), + playerViewerId: 906243102, opponentViewerId: 847666884, battleId: "b"); Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.Matched)); var body = (MatchedBody)env.Body; Assert.That(body.SelfInfo.OppoId, Is.EqualTo(847666884L)); Assert.That(body.OppoInfo.OppoId, Is.EqualTo(906243102L)); - - // Bid travels in the envelope, not the Body — protect against the reserved-keys regression. Assert.That(env.Bid, Is.EqualTo("b")); - // Typed bodies can't carry an envelope-level "bid" key by construction (no such property). } [Test] public void BuildMatched_ContainsThirtyCardSelfDeck() { - var env = ScriptedLifecycle.BuildMatched(1, 2, "b"); + var env = ScriptedLifecycle.BuildMatched(FixtureCtx(), 1, 2, "b"); var body = (MatchedBody)env.Body; Assert.That(body.SelfDeck.Count, Is.EqualTo(30)); } + [Test] + public void BuildMatched_deck_idxs_pair_1to30_with_context_card_ids() + { + var draftedDeck = Enumerable.Range(1, 30).Select(i => 200_000_000L + i).ToList(); + var env = ScriptedLifecycle.BuildMatched(FixtureCtx(draftedDeck), 1, 2, "b"); + var body = (MatchedBody)env.Body; + + for (int i = 0; i < 30; i++) + { + Assert.That(body.SelfDeck[i].Idx, Is.EqualTo(i + 1), + $"slot {i}: idx should be 1-based position"); + Assert.That(body.SelfDeck[i].CardId, Is.EqualTo(200_000_000L + i + 1), + $"slot {i}: cardId should be the drafted card"); + } + } + + [Test] + public void BuildMatched_selfInfo_cosmetics_flow_from_context() + { + var ctx = FixtureCtx() with + { + CountryCode = "JPN", UserName = "Drafter", SleeveId = "999", + EmblemId = "888", DegreeId = "777", FieldId = 42, IsOfficial = 1, + }; + + var env = ScriptedLifecycle.BuildMatched(ctx, 1, 2, "b"); + var body = (MatchedBody)env.Body; + + Assert.That(body.SelfInfo.CountryCode, Is.EqualTo("JPN")); + Assert.That(body.SelfInfo.UserName, Is.EqualTo("Drafter")); + Assert.That(body.SelfInfo.SleeveId, Is.EqualTo("999")); + Assert.That(body.SelfInfo.EmblemId, Is.EqualTo("888")); + Assert.That(body.SelfInfo.DegreeId, Is.EqualTo("777")); + Assert.That(body.SelfInfo.FieldId, Is.EqualTo(42)); + Assert.That(body.SelfInfo.IsOfficial, Is.EqualTo(1)); + } + [Test] public void BuildBattleStart_HasTurnStateZeroAndBattleTypeEleven() { @@ -60,7 +96,6 @@ public class ScriptedLifecycleTests public void ComputeHandAfterSwap_SwapMiddleCard_ReplacesWithFreshDeckIdx() { var hand = ScriptedLifecycle.ComputeHandAfterSwap(new long[] { 2 }); - // pos 0 keeps idx 1; pos 1 (was idx 2) gets next deck idx (4); pos 2 keeps idx 3. Assert.That(hand, Is.EqualTo(new long[] { 1, 4, 3 })); } @@ -98,4 +133,11 @@ public class ScriptedLifecycleTests var body = (OpponentTurnStartBody)env.Body; Assert.That(body.Spin, Is.EqualTo(100)); } + + private 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/Lifecycle/TypedBodyWireShapeTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs index e4fa8fc..662552d 100644 --- a/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs +++ b/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using NUnit.Framework; +using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Lifecycle; using SVSim.BattleNode.Protocol; @@ -26,7 +27,7 @@ public class TypedBodyWireShapeTests // Matching.StartBattleLoad reads it back, and GetSelfDeck().Select(...) crashes // with "Value cannot be null. Parameter name: source". The prod wire format // emits envelope keys (uri first) before body keys; we must too. - var env = ScriptedLifecycle.BuildMatched( + var env = ScriptedLifecycle.BuildMatched(FixtureCtx(), playerViewerId: 1, opponentViewerId: 2, battleId: "b"); var json = MsgEnvelope.ToJson(env); @@ -44,7 +45,7 @@ public class TypedBodyWireShapeTests [Test] public void BuildMatched_SerializesAllWireKeysExpectedByTheClient() { - var env = ScriptedLifecycle.BuildMatched( + var env = ScriptedLifecycle.BuildMatched(FixtureCtx(), playerViewerId: 906243102, opponentViewerId: 847666884, battleId: "597830888107"); var json = MsgEnvelope.ToJson(env); @@ -156,4 +157,16 @@ public class TypedBodyWireShapeTests Assert.That(node["resultCode"]!.GetValue(), Is.EqualTo(1)); Assert.That(node["uri"]!.GetValue(), Is.EqualTo("TurnStart")); } + + /// + /// Wire-shape fixture: 30 copies of the legacy DummyCardId (100_011_010L) so the + /// existing literal assertions on selfDeck[0].cardId (line 81 above) keep working + /// after the MatchContext migration deletes the const. + /// + private static MatchContext FixtureCtx() => new( + SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 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); }