feat(protocol): 离线回放支持多目录并补齐 pt 解码失败定位信息

This commit is contained in:
TermiNexus committed 2026-10-21 01:16:21 +08:00
1 parent 353eb9b0a9
commit ca42614056
4 files changed
+173 -34

No files matched your search

@@ -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<Long, ErasedCodec>, 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<String, Int>()
}
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<Int, Pair<Long, Long>>()
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
@@ -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,
@@ -51,19 +51,38 @@ object PtCodec : ModelCodec<PtPayload> {
val CItems = R.ReadInt()
if (CItems < 0 || CItems > 1 shl 20) throw ProtocolException("pt 项数不合法:$CItems")
val Items = ArrayList<PtItem>(CItems)
repeat(CItems) {
// 最近几项的「modelId@起点..终点」轨迹:解码失败时一并抛出,便于判断是不是前面某项吃错字节导致漂移
val Trace = ArrayList<String>()
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>): 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) {
+22 -11
View File
@@ -433,18 +433,29 @@ def Main():
def RegistryWithModelIds(ResolvedMap, NameMap):
Lines = ["object ModelRegistry {", " val ByModelId: Map<Long, ErasedCodec> = 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