From ca42614056d0a5a68d1e7cff6d5778482ed7646d Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 21 Oct 2026 01:16:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(protocol):=20=E7=A6=BB=E7=BA=BF=E5=9B=9E?= =?UTF-8?q?=E6=94=BE=E6=94=AF=E6=8C=81=E5=A4=9A=E7=9B=AE=E5=BD=95=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=20pt=20=E8=A7=A3=E7=A0=81=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/jsMain/kotlin/App/Main.kt | 109 ++++++++++++++++++ .../jsMain/kotlin/Net/Generated/Registry.kt | 40 +++---- .../jsMain/kotlin/Net/Protocol/Containers.kt | 25 +++- TankiOffline/tools/GenerateProtocol.py | 33 ++++-- 4 files changed, 173 insertions(+), 34 deletions(-) diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Main.kt b/TankiOffline/app/src/jsMain/kotlin/App/Main.kt index 863a061..b6eecc1 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/Main.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/Main.kt @@ -8,10 +8,13 @@ // 环境变量 TANKIOFFLINE_PACKETS 可覆盖抓包目录(默认 ../3DTank/packets)。 package App +import Core.ByteReader import Core.Frame +import Core.Nullmap import Net.Generated.DownlinkRegistry import Net.Generated.ProtocolMetadata import Net.Generated.UplinkRegistry +import Net.Protocol.S2cDecoder import Net.Protocol.SpaceFrame import Net.Wire.ErasedCodec @@ -120,8 +123,114 @@ private fun RunOne(Path: String, Registry: Map, S: Stats) { } } +private val ExitProcessImpl: (Int) -> Unit = js("(function (C) { process.exit(C); })") + +// 离线回放:把抓包目录里的 s2c_*.bin 按顺序喂给 S2cDecoder(自动明文/密文探测), +// 解出每条记录并回编码比对。用于不依赖在线会话验证 pt/lt 容器链路。 +// TANKIOFFLINE_REPLAY 支持用 ';' 分隔多个目录(每个目录视为独立连接)。 +object Replay { + + private class Totals { + var Dirs = 0 + var Files = 0 + var Frames = 0 + var Pings = 0 + var Records = 0 + var Ok = 0 + var Bad = 0 + var Short = 0 + var Unknown = 0 + var HeaderErrors = 0 + var Errors = 0 + var Keys = "" + val Containers = LinkedHashMap() + } + + fun Wanted(): Boolean = js("!!process.env.TANKIOFFLINE_REPLAY") as Boolean + + fun Run() { + val Raw = js("process.env.TANKIOFFLINE_REPLAY") as String + val Dirs = Raw.split(';').filter { it.isNotBlank() } + val T = Totals() + for (Dir in Dirs) { + RunDir(Dir, T) + } + println("合计:目录 ${T.Dirs}|文件 ${T.Files}|帧 ${T.Frames}|保活 ${T.Pings}|记录 ${T.Records}") + println("回编码一致 ${T.Ok}|不一致 ${T.Bad}|短命令 ${T.Short}|未知 methodId ${T.Unknown}") + println("记录头截断 ${T.HeaderErrors}|其它解码错误 ${T.Errors}|密钥=${T.Keys}") + if (T.Containers.isNotEmpty()) { + println("容器命中:" + T.Containers.entries.joinToString(",") { "${it.key}×${it.value}" }) + } + val Passed = T.Bad == 0 && T.Errors == 0 && T.Records > 0 + println(if (Passed) "结果:通过" else "结果:失败") + ExitProcessImpl(if (Passed) 0 else 1) + } + + private fun RunDir(Dir: String, T: Totals) { + val Files = ListBinFiles(Dir, 10000) + .filter { it.contains("s2c_") } + .sorted() + // 密钥与别名表按目录(= 一次独立连接)重置 + val Decoder = S2cDecoder() + val Aliases = HashMap>() + val BeforeBad = T.Bad + val BeforeErrors = T.Errors + val BeforeFrames = T.Frames + val BeforeRecords = T.Records + T.Dirs++ + + println("TankiOffline 离线回放:$Dir(${Files.size} 个 s2c 文件)") + for (Path in Files) { + T.Files++ + val Bytes = ReadFile(Path) + val Bodies = Decoder.Feed(Bytes) { Line -> println(" $Line") } + for (Body in Bodies) { + T.Frames++ + val Nm = Nullmap.Size(Body, 0) + if (Nm >= Body.size) { + T.Pings++ + continue + } + val R = SpaceFrame.DecodeBody(Body, DownlinkRegistry.ByMethodId, Aliases) + T.Records += R.Records.size + T.Short += R.ShortCommandCount + T.Unknown += R.UnknownMethodIds.size + val Err = R.Error + if (Err != null) { + if (Err.startsWith("记录头截断")) T.HeaderErrors++ else T.Errors++ + println(" ⚠ $Path:$Err") + } + for (Rec in R.Records) { + if (Rec.Payload.contentEquals(Rec.Reencoded)) T.Ok++ else { + T.Bad++ + println(" ⚠ $Path methodId=${Rec.MethodId} 回编码不一致") + } + val Meta = ProtocolMetadata.Downlink[Rec.MethodId] + val Label = Meta?.Name ?: Rec.MethodId.toString() + if (Rec.MethodId == 3216143066888387731L || Rec.MethodId == 7640916300855664666L || + Rec.MethodId == 9038756763225879447L || Rec.MethodId == 8179162314198150518L + ) { + T.Containers[Label] = (T.Containers[Label] ?: 0) + 1 + } + } + } + } + val KeyText = Decoder.Key?.let { "$it" } ?: "null" + T.Keys = if (T.Keys.isEmpty()) KeyText else "${T.Keys}/$KeyText" + val Delta = (T.Bad - BeforeBad) + (T.Errors - BeforeErrors) + println( + " 小结:文件 ${Files.size}|帧 ${T.Frames - BeforeFrames}|记录 ${T.Records - BeforeRecords}" + + "|密钥=$KeyText|错误 $Delta" + ) + } +} + fun main() { if (!IsNode()) return + if (Replay.Wanted()) { + Replay.Run() + return + } if (AutoTest.Wanted()) { AutoTest.Run() return diff --git a/TankiOffline/net/src/jsMain/kotlin/Net/Generated/Registry.kt b/TankiOffline/net/src/jsMain/kotlin/Net/Generated/Registry.kt index 959177f..903f622 100644 --- a/TankiOffline/net/src/jsMain/kotlin/Net/Generated/Registry.kt +++ b/TankiOffline/net/src/jsMain/kotlin/Net/Generated/Registry.kt @@ -1082,12 +1082,12 @@ object ModelRegistry { 8618289906968528240L to RegistrationModelCCCodec, 2474458842977623992L to RegistrationModelCCCodec, 7300500077035374704L to EntranceResourcesCCCodec, - 6627517808336136295L to MaterialsCodec, 6403067749608679876L to UserNotifierCCCodec, 1850509145998626828L to UserActionsLoggerCCCodec, 258682513334072065L to RankNotifierDataCodec, 4255726087158005051L to UidNotifierDataCodec, - 3527205014716342329L to RankInfoCodec, + 3527205014716342329L to RankLoaderCCCodec, + 6627517808336136295L to UidCheckCCCodec, 216902005816818019L to SpectatorsModelCCCodec, 2085529474881905981L to StatisticsModelCCCodec, 787936358514072261L to StatisticsDMCCCodec, @@ -1118,15 +1118,13 @@ object ModelRegistry { 7588919819099098108L to BattleInviteCCCodec, 2855626038259597130L to BattlePassPurchaseNotifierCCCodec, 7805766859473669177L to ChallengeClientCCCodec, - 3349786357139026014L to TierCodec, - 8237913720896985151L to TierItemCodec, 1987602940683756146L to StarsInfoCCCodec, 8677996546985988365L to StarsInfoCCCodec, + 3349786357139026014L to ContractUserModelCCCodec, 6514486185481319524L to CreatorTagCCCodec, 965113061940997187L to CreatorTagsCCCodec, 7341286352739641155L to DebtInfoCCCodec, 3395764563215792356L to DonationAlertCCCodec, - 2108103923322474513L to DonationDataCodec, 8464700182634408374L to UserDroppableGoldSettingsCCCodec, 3503382027414679973L to TankHasGrenadeCCCodec, 5801057999588514016L to ResistancesCCCodec, @@ -1156,13 +1154,11 @@ object ModelRegistry { 1893408944113965505L to UserPropertiesCCCodec, 9006361281849290638L to UserPushNotificationSettingsModelCCCodec, 5895522515282663537L to UserAchievementCCCodec, - 7295738902766942560L to MainQuestBattleProgressCodec, 701801920209410257L to MainQuestUserModelCCCodec, 7105110962241677955L to MainQuestUserModelCCCodec, 5167916989664044333L to ReferralEventUserModelCCCodec, 4038465370530850950L to QuestCategoriesModelCCCodec, 462434031489766566L to QuestCategoriesModelCCCodec, - 6680653332718076818L to QuestCategoryStatisticCodec, 6966229709169193455L to RewardedVideosCCCodec, 8659306545527654916L to RubyInfoCCCodec, 2767408225195389970L to RubyInfoCCCodec, @@ -1206,17 +1202,17 @@ object ModelRegistry { 5256681735304582291L to TwitchAccountModelCCCodec, 2252704231082045810L to TwoStepVerificationModelCCCodec, 2080556892932195426L to UserCountryCCCodec, - 1851605205665890036L to RewardInfoDataCodec, 8517963146836182364L to RewardsInfoCCCodec, + 1851605205665890036L to RewardsInfoCCCodec, 6071565290933648049L to ChatCCCodec, 891137447706578619L to ClanChatCCCodec, 868364325972433765L to NewsShowingCCCodec, - 7970528904804811572L to OiCodec, - 7267982595724632282L to MiM61860FrCodec, 6359692933626160397L to SocialNetworkParametersCCCodec, 7762582768388221832L to FlagBattleSFXCCCodec, + 7970528904804811572L to ArmsRaceCCCodec, 7401419333842694749L to BattlefieldCCCodec, 2101100305743734614L to BillboardCCCodec, + 7267982595724632282L to BillboardImageCCCodec, 5958419804845264849L to BotNavigationCCCodec, 2464705242400585109L to FacilitySphericalZoneCCCodec, 5242494537362061802L to CommonFacilityCCCodec, @@ -1224,6 +1220,7 @@ object ModelRegistry { 9172358609541951983L to MeteorStormCCCodec, 2057320715890332864L to BattleMineCCCodec, 8980131978880882207L to BattleStartDelayCCCodec, + 8237913720896985151L to DominationCCCodec, 5385894712778438726L to BattleChatCCCodec, 6445399806655453890L to InventoryCCCodec, 3364392936313775902L to TargetHighlightingCCCodec, @@ -1233,6 +1230,7 @@ object ModelRegistry { 2618111250571302204L to RugbyCCCodec, 2269636304551395302L to BallExplosionCCCodec, 6141842138930446750L to StatusEffectBattleCCCodec, + 7295738902766942560L to TeamJuggernautCCCodec, 1475210252818583689L to BattleTutorialCCCodec, 9114375113924783411L to BattleTutorialCCCodec, 2145315487377727727L to BattlefieldBonusesCCCodec, @@ -1246,7 +1244,6 @@ object ModelRegistry { 1661364280719338177L to DemomanSfxCCCodec, 8316165996190533130L to DroneIndicatorCCCodec, 4553925959338838726L to CrisisEffectCCCodec, - 5207857306014573244L to XCCodec, 6424605984954645392L to TankEffectSFXCCCodec, 8166810800706377715L to ActiveAfterDeathCCCodec, 5779075235189237790L to EffectDescriptionCCCodec, @@ -1264,7 +1261,7 @@ object ModelRegistry { 4693095212037357021L to BattleMapCCCodec, 1688856467034284073L to MapBonusLightCCCodec, 4885574251230758729L to ModerationToolInteropCCCodec, - 7894174029759745197L to ClassicParachuteCCCodec, + 7894174029759745197L to DroneParachuteCCCodec, 4778752313022424015L to RadarCCCodec, 3719295044981767592L to StatusEffectCCCodec, 8889030940127130747L to StatusEffectCCCodec, @@ -1298,8 +1295,6 @@ object ModelRegistry { 5194655544597440038L to LaserPointerCCCodec, 7266201540452328992L to LaserPointerCCCodec, 1663344340563853103L to RailgunCCCodec, - 1092307613749405757L to RicochetCCCodec, - 1130027771466355435L to ScorpioCCCodec, 313670832122918648L to ScorpioSfxCCCodec, 732152133982227177L to ShaftCCCodec, 7700337090161867199L to ShotCCCodec, @@ -1311,6 +1306,8 @@ object ModelRegistry { 6855550629220038001L to TerminatorCCCodec, 6210980129845088505L to TerminatorSFXCCCodec, 573691036321759779L to TeslaCCCodec, + 1092307613749405757L to ThunderCCCodec, + 1130027771466355435L to CassetteShootingCCCodec, 8212033390179430143L to TsunamiCCCodec, 5604117679922858821L to TsunamiShootSFXCCCodec, 2803166101624878775L to RotatingTurretCCCodec, @@ -1329,8 +1326,8 @@ object ModelRegistry { 5867914731324680416L to ShotgunShotCCCodec, 2660691061343351669L to TeamLightCCCodec, 171896995500198357L to TeamLightCCCodec, - 1500120709087433677L to TeamLightColorParamsCodec, 6455142242556985012L to UltimateCCCodec, + 1500120709087433677L to AresUltimateCCCodec, 7899544742191273818L to AresUltimateProjectileCCCodec, 6210079079280471693L to CrusaderEffectCCCodec, 3907559303616283277L to CrusaderUltimateCCCodec, @@ -1356,12 +1353,12 @@ object ModelRegistry { 5543162332469864498L to TankInfoCCCodec, 260111248825415414L to BattleBotControlCCCodec, 2560105521800470759L to RemoteTankStateUpdatingCCCodec, - 4206032257972775630L to TankResistanceCodec, 7127632667958387441L to TankResistancesCCCodec, 2104999573658810427L to TankSpawnerCCCodec, 7163171834328288344L to SpeedCharacteristicsCCCodec, 2271787261806799336L to SuicideCCCodec, 2150802556932617880L to TankCCCodec, + 4206032257972775630L to TurnOverCCCodec, 6589716392600418185L to AvailableForAllCCCodec, 9168562037264832365L to PassToShopCCCodec, 3122964582367261689L to TemperatureCCCodec, @@ -1372,10 +1369,10 @@ object ModelRegistry { 7391892068905080676L to DelayMountCategoryCCCodec, 7109632754421804521L to DelayMountCategoryCCCodec, 7640081207058999117L to DevicePropertiesCCCodec, - 6111098822553419499L to PaCodec, 4692610703461978415L to ProAbonementRankDiscountCCCodec, 6430862429652735997L to EmissionColorCCCodec, 1534251830977989099L to EmissionColorCCCodec, + 6111098822553419499L to BazookaSupplyCCCodec, 1865478986390468300L to GroupedCCCodec, 658797348514407988L to ItemModelCCCodec, 3438505192580052999L to ItemModelCCCodec, @@ -1397,15 +1394,18 @@ object ModelRegistry { 6117306895090150422L to ItemViewCategoryCCCodec, 4817031001612283794L to ProtectionSlotsCCCodec, 2433120830275396692L to PresentsCCCodec, - 5988756521593027564L to DwCodec, 7563487786199453443L to OverdriveDescriptionCCCodec, + 5988756521593027564L to RelatedResistanceModuleCCCodec, 149643551432886459L to TurretMechanismCCCodec, + 5207857306014573244L to AvailableSkinsCCCodec, 905970599512839002L to MountedSkinCCCodec, 834829861153964627L to MountedSkinCCCodec, 1164458258646872337L to AvailableShotSkinsCCCodec, - 7175193612424807833L to VuCodec, + 6680653332718076818L to SkinPreviewCCCodec, + 2108103923322474513L to CaptchaCCCodec, + 7175193612424807833L to ServerHaltCCCodec, 4068618447512888231L to ColoringCCCodec, - 5255782026414758079L to BsCodec, + 5255782026414758079L to LobbyLayoutModelCCCodec, 1259328549938381181L to VoCodec, 7272122855372520854L to FriendsCCCodec, 7286757952195114814L to UserContainerCCCodec, diff --git a/TankiOffline/net/src/jsMain/kotlin/Net/Protocol/Containers.kt b/TankiOffline/net/src/jsMain/kotlin/Net/Protocol/Containers.kt index aa80a26..c06a72b 100644 --- a/TankiOffline/net/src/jsMain/kotlin/Net/Protocol/Containers.kt +++ b/TankiOffline/net/src/jsMain/kotlin/Net/Protocol/Containers.kt @@ -51,19 +51,38 @@ object PtCodec : ModelCodec { val CItems = R.ReadInt() if (CItems < 0 || CItems > 1 shl 20) throw ProtocolException("pt 项数不合法:$CItems") val Items = ArrayList(CItems) - repeat(CItems) { + // 最近几项的「modelId@起点..终点」轨迹:解码失败时一并抛出,便于判断是不是前面某项吃错字节导致漂移 + val Trace = ArrayList() + repeat(CItems) { Index -> + val Start = R.Position val Id = R.ReadLong() if (Id == 0L) { Items.add(PtItem(Id, R.ReadLong())) } else { val Codec = ModelRegistry.ByModelId[Id] - ?: throw ProtocolException("pt 项 modelId=$Id 没有 codec") - Items.add(PtItem(Id, Codec.DecodeAny(R, N))) + ?: throw ProtocolException( + "pt 第 ${Index + 1}/$CItems 项 modelId=$Id 没有 codec(偏移 $Start)" + + TraceHint(Trace) + ) + val Value = try { + Codec.DecodeAny(R, N) + } catch (E: Throwable) { + throw ProtocolException( + "pt 第 ${Index + 1}/$CItems 项 modelId=$Id(${Codec::class.simpleName})解码失败" + + "(偏移 $Start):${E.message}" + TraceHint(Trace) + ) + } + Items.add(PtItem(Id, Value)) } + if (Trace.size >= 4) Trace.removeAt(0) + Trace.add("#${Index + 1} $Id@$Start..${R.Position}") } return PtPayload(Objects, Items) } + private fun TraceHint(Trace: List): String = + if (Trace.isEmpty()) "" else ";前几项:" + Trace.joinToString(",") + override fun Encode(W: ByteWriter, N: NullmapWriter, V: PtPayload) { W.WriteInt(V.Objects.size) for (O in V.Objects) { diff --git a/TankiOffline/tools/GenerateProtocol.py b/TankiOffline/tools/GenerateProtocol.py index cda2afb..2a76e2b 100644 --- a/TankiOffline/tools/GenerateProtocol.py +++ b/TankiOffline/tools/GenerateProtocol.py @@ -433,18 +433,29 @@ def Main(): def RegistryWithModelIds(ResolvedMap, NameMap): Lines = ["object ModelRegistry {", " val ByModelId: Map = mapOf("] Seen = set() - for Key, (Def, Nodes) in ResolvedMap.items(): - C = Codecs[Key] - Expr = OVERLAY_NAMES.get(Key, NameMap[Key] + "Codec") - Ids = [] - if C.get("ModelId"): - Ids.append(str(C["ModelId"])) - Ids += [str(M) for M in (C.get("ModelIds") or [])] - for Mid in Ids: - if Mid in Seen: + + def Emit(OnlySource: bool): + for Key, (Def, Nodes) in ResolvedMap.items(): + C = Codecs[Key] + HasSource = bool(C.get("ModelHow")) + if OnlySource != HasSource: continue - Seen.add(Mid) - Lines.append(f" {Mid}L to {Expr},") + Expr = OVERLAY_NAMES.get(Key, NameMap[Key] + "Codec") + Ids = [] + if C.get("ModelId"): + Ids.append(str(C["ModelId"])) + Ids += [str(M) for M in (C.get("ModelIds") or [])] + for Mid in Ids: + if Mid in Seen: + continue + Seen.add(Mid) + Lines.append(f" {Mid}L to {Expr},") + + # 先信「源码登记点」(ModelHow 非空),再用指纹/旧 join 兜底: + # 同一个 modelId 可能被两个 codec 认领(如 2108103923322474513 = CaptchaCC / DonationData), + # 源码登记点才是真正上线的那一个。 + Emit(True) + Emit(False) Lines.append(" )") Lines.append("}") return Lines