using SVSim.BattleNode.Protocol; namespace SVSim.BattleNode.Lifecycle; /// /// v1 Path-A scripted opponent. Hand-rolled static frames good enough to land the client on /// the mulligan screen and let them play turn 1. Templates derived from /// data_dumps/captures/battle-traffic_tk2_regular.ndjson. /// public static class ScriptedLifecycle { /// 30 dummy cardIds โ€” repeats of a stable neutral card. public static readonly long DummyCardId = 100011010; public const long FakeOpponentViewerId = 999_999_999L; public static MsgEnvelope BuildMatched(long playerViewerId, long opponentViewerId, string battleId) { var body = new Dictionary { ["selfInfo"] = new Dictionary { ["country_code"] = "KOR", ["userName"] = "Player", ["sleeveId"] = "3000011", ["emblemId"] = "701441011", ["degreeId"] = "300003", ["fieldId"] = 43, ["isOfficial"] = 0, ["oppoId"] = opponentViewerId, ["seed"] = 17548138L, }, ["oppoInfo"] = new Dictionary { ["country_code"] = "JPN", ["userName"] = "Opponent", ["sleeveId"] = "704141010", ["emblemId"] = "400001100", ["degreeId"] = "120027", ["fieldId"] = 5, ["isOfficial"] = 0, ["oppoId"] = playerViewerId, ["seed"] = 17548138L, ["oppoDeckCount"] = 30, }, ["selfDeck"] = BuildDummyDeck(), }; return EnvelopeForPush(NetworkBattleUri.Matched, body, bid: battleId); } public static MsgEnvelope BuildBattleStart(long playerViewerId) { var body = new Dictionary { ["turnState"] = 0, // player goes first ["battleType"] = 11, // TK2 NetworkBattleType ["selfInfo"] = new Dictionary { ["rank"] = "10", ["battlePoint"] = "6270", ["classId"] = "1", ["charaId"] = "1", ["cardMasterName"] = "card_master_node_10015", }, ["oppoInfo"] = new Dictionary { ["rank"] = "1", ["isMasterRank"] = "0", ["battlePoint"] = 0, ["masterPoint"] = "0", ["classId"] = "8", ["charaId"] = "8", ["cardMasterName"] = "card_master_node_10015", }, }; return EnvelopeForPush(NetworkBattleUri.BattleStart, body); } public static MsgEnvelope BuildDeal() { var body = new Dictionary { ["self"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, ["oppo"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, }; return EnvelopeForPush(NetworkBattleUri.Deal, body); } public static MsgEnvelope BuildSwapResponse(IReadOnlyList swapIndices) { // v1: ignore the player's chosen indices and echo the same hand back. // (Acceptable because the client doesn't validate which idxs come back โ€” it just renders them.) _ = swapIndices; var body = new Dictionary { ["self"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, }; return EnvelopeForPush(NetworkBattleUri.Swap, body); } public static MsgEnvelope BuildReady() { var body = new Dictionary { ["self"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, ["oppo"] = new List { new Dictionary { ["pos"] = 0, ["idx"] = 1 }, new Dictionary { ["pos"] = 1, ["idx"] = 2 }, new Dictionary { ["pos"] = 2, ["idx"] = 3 }, }, ["idxChangeSeed"] = 771335280, ["spin"] = 243, }; return EnvelopeForPush(NetworkBattleUri.Ready, body); } private static List BuildDummyDeck() { var deck = new List(30); for (var i = 1; i <= 30; i++) { deck.Add(new Dictionary { ["idx"] = i, ["cardId"] = DummyCardId, }); } return deck; } private static MsgEnvelope EnvelopeForPush(NetworkBattleUri uri, Dictionary body, string? bid = null) { // Synchronize-push routing in the client's OnReceived drops any frame whose // resultCode != Success (1). Absent counts as 0 (None) and is also dropped โ€” so we // MUST include it on every scripted push, not just InitNetwork ack / BattleFinish. // See server-to-client.md ยง"Routing in OnReceived" and the matching prod captures. body["resultCode"] = (int)ReceiveNodeResultCode.Success; return new MsgEnvelope(uri, ViewerId: FakeOpponentViewerId, Uuid: "node-stub", Bid: bid, Try: 0, Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null, // OutboundSequencer.AssignAndArchive stamps this Body: body); } }