diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index d9ca396..36c8a80 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -463,6 +463,19 @@ `tools/RunSessionTest.ps1`:8000 个 s2c 文件回放,**1338 条记录全部回编码一致**。 - 下一步:动作 ↔ 协议方法映射表(`ProtocolMetadata` 里的名字仍是 `UserPropertiesCCxxxx` 这类混淆名, 需要从 module_18334 的 command / DI 装配里抽语义)、浏览器 WebSocket 传输与大厅渲染。 + - 18334 与下行映射(本轮进展): + - 18334 实际只做 DI 装配与 UI 动作派发(509 处 `uar(new …)`),**协议下行 → 动作**的转换不在里面; + 转换代码与动作类同处 module_10032(动作直接持有 CC,例如 `SetUserPropertiesAction(user: UserPropertiesCC)`)。 + - 新增 `tools/gen_downlink_actions.js`:`method_table.json` → `model_binding.json` → `codec_fields.json` + 的 `ModelName`,再与 `Inventory.kt` 的「单字段动作」对齐,生成 + `names/downlink_actions.json` 与 `App/State/DownlinkActions.kt`。 + 覆盖:681 个 methodId 中 **477 个能解析出协议模型名**,其中 **13 个 methodId 自动匹配到动作** + (`UserPropertiesCC`×9 → `SetUserPropertiesAction`、`ReferralEventUserModelCC`×3、`GaragePreviewModelCC`×1)。 + - `App/Session/SessionRouter.kt`:解码记录 → `DownlinkActions`(或 Pt 容器的 `DownlinkModelActions`)→ Store。 + - 已知缺口:抓包里的 Pt 容器 item id(如 `2114179554921095892`)在 `model_binding.json` 里查不到, + 说明现有 modelId→codec 绑定只覆盖 363 个模型;需要补「modelId → 模块/类」的运行时注册表, + 才能把大厅容器的模型路由进状态层。当前 `RunSessionTest.ps1` 只断言「会话解码 + 回编码」, + 路由计数(已派发/未覆盖)作为诊断输出。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); `launchParams` 是纯透传(`(pass-through)`,无 reducer),已把字段收口为 `Map`。 diff --git a/3DTank/names/downlink_actions.json b/3DTank/names/downlink_actions.json new file mode 100644 index 0000000..08b0587 --- /dev/null +++ b/3DTank/names/downlink_actions.json @@ -0,0 +1,912 @@ +{ + "Note": "下行 methodId → 协议模型 → 客户端动作。模型名来自 codec_fields.json 的 ModelName;动作来自 Inventory.kt 里“只有一个字段且字段类型等于该模型”的 data class。未匹配的模型在 Models 里保留候选,留给后续手工对照(多为 CC → 多个基本字段的转换动作)。", + "Source": [ + "method_table.json", + "model_binding.json", + "codec_fields.json", + "Inventory.kt" + ], + "Methods": 681, + "Resolved": 477, + "Matched": 13, + "MatchedModels": [ + { + "Model": "GaragePreviewModelCC", + "Action": "InitGarageResources", + "Field": "params" + }, + { + "Model": "ReferralEventUserModelCC", + "Action": "SetReferralEventData", + "Field": "eventData" + }, + { + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + } + ], + "Models": [ + { + "Model": "AndroidPayCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "AntiAddictionRealNameValidatorModelCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "AppleAppStoreCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "AresUltimateProjectileCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "ArmsRaceCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "ArtilleryElevatingBarrelCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattleBotControlCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattleChatCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "BattleCreateCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "BattleDMInfoCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "BattleGearScoreCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattleGoldBonusCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattleInfoCC", + "Methods": 7, + "Actions": [] + }, + { + "Model": "BattleInviteCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "BattleMineCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "BattlePassPurchaseNotifierCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattleStartDelayCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattleTutorialCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "BattleUltimateRadarCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BattlefieldBonusesCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "BattlefieldCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "BonkWeaponCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "BossStateCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "BotNavigationCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "CaptchaCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "ChallengeClientCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ChatCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "ClanChatCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ClanCreateCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "ClanFriendsCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "ClanIncomingCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ClanMembersCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ClanNotifierData", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ClanOutgoingCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ClanPanelNotificationCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "ClanPermissionsCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ClanUserCC", + "Methods": 15, + "Actions": [] + }, + { + "Model": "CoinInfoCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "CommonFacilityCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "CompositePartnerCC", + "Methods": 11, + "Actions": [] + }, + { + "Model": "ContainerCC", + "Methods": 16, + "Actions": [] + }, + { + "Model": "ContextTutorialModelCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ContractUserModelCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "CrisisEffectCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "CrusaderUltimateCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "DamageIndicatorCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "DebtInfoCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "DeferredOverdriveCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "DemomanSfxCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "DictatorUltimateCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "DominationCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "DonationAlertCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "DroneIndicatorCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "EffectDescriptionCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "ExternalEntranceCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "FacebookPaymentCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "FlameThrowerCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "FreezeCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "FriendsCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "GaragePreviewModelCC", + "Methods": 1, + "Actions": [ + "InitGarageResources" + ] + }, + { + "Model": "GaussCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "GlobeLightningCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "GoldBonusCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "GrenadeCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "HopperUltimateCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "HornetUltimateCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "HoverChassisCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "HunterUltimateCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "InventoryItemCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "InviteEntranceCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "IsisCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "JuggernautCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "JuggernautUltimateCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "KeysInfoCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "KillStreakCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "LaserPointerCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "LicenseClanUserCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "MachineGunCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "MainQuestUserModelCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "MammothUltimateCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "MatchmakingGroupCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "MatchmakingLayoutCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "MeteorStormCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ModerationToolInteropCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "MountedResistancesCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "NewsShowingCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "NotificationsByCountryModelCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "NuclearRechargeCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "PaidRenameCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "PaladinAuraVisualCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "PayGardenPaymentCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "PayPalPayModelCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "PeriodicRewardCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "PointBasedBattleCC", + "Methods": 7, + "Actions": [] + }, + { + "Model": "PremiumNotifierCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "RailgunCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "RankNotifierData", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ReferralEventUserModelCC", + "Methods": 3, + "Actions": [ + "SetReferralEventData" + ] + }, + { + "Model": "RegistrationModelCC", + "Methods": 7, + "Actions": [] + }, + { + "Model": "RemoteTankStateUpdatingCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "RicochetCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "RotatingTurretCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "RuStorePayCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "RubyInfoCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ScorpioCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "ServerHaltCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ServerHaltEntranceCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "ShaftCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "ShotCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ShotgunShotCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "ShowcaseViewCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "SmokyCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "SpectatorsModelCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "SpeedCharacteristicsCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "StarsInfoCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "StatisticsDMCC", + "Methods": 4, + "Actions": [] + }, + { + "Model": "StatisticsModelCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "StatisticsTeamCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "StatusEffectCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "StreamWeaponCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "SuicideCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "SuppliesTutorialModelCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TankCC", + "Methods": 6, + "Actions": [] + }, + { + "Model": "TankCritCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TankEffectSFXCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TankHasGrenadeCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TankPointersCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TankResistancesCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TankSpawnerCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TankStunCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TankSuperchargeCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TeamBattleInfoCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "TeamJuggernautCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TerminatorCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "TeslaBazookaCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TeslaCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "ThunderCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TitanUltimateGeneratorCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TrackedChassisCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "TsunamiCC", + "Methods": 3, + "Actions": [] + }, + { + "Model": "TwinsCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TwitchAccountModelCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "TwoStepVerificationModelCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "UidCheckCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "UidNotifierData", + "Methods": 1, + "Actions": [] + }, + { + "Model": "UltimateCC", + "Methods": 5, + "Actions": [] + }, + { + "Model": "UpgradeGarageItemCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "UserAccountRemovalModelCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "UserAchievementCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "UserActionsLoggerCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "UserContainerCC", + "Methods": 8, + "Actions": [] + }, + { + "Model": "UserEmailCC", + "Methods": 10, + "Actions": [] + }, + { + "Model": "UserPropertiesCC", + "Methods": 9, + "Actions": [ + "SetUserPropertiesAction" + ] + }, + { + "Model": "UserReportsModelCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "VikingUltimateCC", + "Methods": 2, + "Actions": [] + }, + { + "Model": "WaspUltimateBombCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "WeaponCommonCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "WeaponWeakeningCC", + "Methods": 1, + "Actions": [] + }, + { + "Model": "at", + "Methods": 4, + "Actions": [] + } + ], + "Actions": [ + { + "MethodId": "8836555743203700004", + "Model": "GaragePreviewModelCC", + "Action": "InitGarageResources", + "Field": "params" + }, + { + "MethodId": "2940606982196178690", + "Model": "ReferralEventUserModelCC", + "Action": "SetReferralEventData", + "Field": "eventData" + }, + { + "MethodId": "2940608322101861340", + "Model": "ReferralEventUserModelCC", + "Action": "SetReferralEventData", + "Field": "eventData" + }, + { + "MethodId": "1880027114297758713", + "Model": "ReferralEventUserModelCC", + "Action": "SetReferralEventData", + "Field": "eventData" + }, + { + "MethodId": "276076212290602640", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "7864195052961084235", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "3982372259754314184", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "6492680006777979140", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "5628691596950015452", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "7864034006236084309", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "3977381235093458189", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "848734776772436751", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + }, + { + "MethodId": "8468360636859619961", + "Model": "UserPropertiesCC", + "Action": "SetUserPropertiesAction", + "Field": "user" + } + ] +} \ No newline at end of file diff --git a/3DTank/tools/gen_downlink_actions.js b/3DTank/tools/gen_downlink_actions.js new file mode 100644 index 0000000..262d281 --- /dev/null +++ b/3DTank/tools/gen_downlink_actions.js @@ -0,0 +1,153 @@ +// gen_downlink_actions.js —— 生成「下行 methodId → 协议模型 → 客户端动作」表 +// +// 数据源(都不猜,全部来自既有产物): +// 1. names/method_table.json methodId → modelId / codec +// 2. names/model_binding.json modelId → module::codec +// 3. names/codec_fields.json module::codec → ModelName(协议模型名,例如 UserPropertiesCC) +// 4. TankiOffline/.../Inventory.kt 动作类字段表(`data class Xxx(...)`) +// +// 输出: +// names/downlink_actions.json { Note, Resolved, Matched, Models: [...], Actions: [...] } +// TankiOffline/.../App/State/DownlinkActions.kt +// val DownlinkActions: Map Any?> +// +// 用法:cd 3DTank/tools && node gen_downlink_actions.js +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const P = path.resolve(__dirname, '..'); +const Names = path.join(P, 'names'); +const KotlinRoot = path.resolve(P, '..', 'TankiOffline', 'app', 'src', 'jsMain', 'kotlin', 'App', 'State'); + +const MethodTable = JSON.parse(fs.readFileSync(path.join(Names, 'method_table.json'), 'utf8')).Rows; +const CodecFields = JSON.parse(fs.readFileSync(path.join(Names, 'codec_fields.json'), 'utf8')).Codecs; +const ModelBinding = JSON.parse(fs.readFileSync(path.join(Names, 'model_binding.json'), 'utf8')).Entries; +const Inventory = fs.readFileSync(path.join(KotlinRoot, 'Inventory.kt'), 'utf8'); + +// ---------------------------------------------------------------- 模型名解析 +const ByModuleCodec = new Map(); +const ByCodec = new Map(); +for (const C of Object.values(CodecFields)) { + if (!C.ModelName) continue; + ByModuleCodec.set(C.Module + '::' + C.Codec, C.ModelName); + if (!ByCodec.has(C.Codec)) ByCodec.set(C.Codec, C.ModelName); +} +const ModelIdToKey = new Map(ModelBinding.map((E) => [E.ModelId, E.Module + '::' + E.Codec])); + +const Rows = []; +for (const R of MethodTable) { + const Key = ModelIdToKey.get(R.ModelId); + const Model = (Key && ByModuleCodec.get(Key)) || ByCodec.get(R.Codec) || null; + Rows.push({ MethodId: R.MethodId, ModelId: R.ModelId, Codec: R.Codec, Model }); +} + +// ---------------------------------------------------------------- 动作字段表 +/** 解析 Inventory.kt 的 data class:name → [{ field, type }] */ +function ParseActions(Text) { + const Out = new Map(); + const Re = /data class (\w+)\(\n([\s\S]*?)\n\)/g; + let M; + while ((M = Re.exec(Text))) { + const Fields = []; + const FRe = /val (\w+): ([A-Za-z0-9_.]+)(<[^>]*>)?\??,/g; + let F; + while ((F = FRe.exec(M[2]))) Fields.push({ Field: F[1], Type: F[2], Generic: F[3] || '' }); + Out.set(M[1], Fields); + } + return Out; +} + +const Actions = ParseActions(Inventory); +const ModelNames = new Set(Rows.map((R) => R.Model).filter(Boolean)); + +// 模型名 → 候选动作(字段类型恰为该模型,且动作只有这一个字段) +const ActionByModel = new Map(); +for (const [Name, Fields] of Actions) { + if (Fields.length !== 1) continue; + const T = Fields[0].Type; + if (!ModelNames.has(T)) continue; + if (!ActionByModel.has(T)) ActionByModel.set(T, []); + ActionByModel.get(T).push({ Action: Name, Field: Fields[0].Field }); +} + +// ---------------------------------------------------------------- 汇总 +const Matched = []; +const MatchedModels = []; +const Models = []; +for (const Name of [...ModelNames].sort()) { + const Cand = ActionByModel.get(Name) || []; + const Methods = Rows.filter((R) => R.Model === Name).map((R) => R.MethodId); + Models.push({ Model: Name, Methods: Methods.length, Actions: Cand.map((C) => C.Action) }); + if (Cand.length === 1) { + for (const Id of Methods) { + Matched.push({ MethodId: Id, Model: Name, Action: Cand[0].Action, Field: Cand[0].Field }); + } + MatchedModels.push({ Model: Name, Action: Cand[0].Action, Field: Cand[0].Field }); + } +} + +fs.writeFileSync( + path.join(Names, 'downlink_actions.json'), + JSON.stringify( + { + Note: + '下行 methodId → 协议模型 → 客户端动作。模型名来自 codec_fields.json 的 ModelName;' + + '动作来自 Inventory.kt 里“只有一个字段且字段类型等于该模型”的 data class。' + + '未匹配的模型在 Models 里保留候选,留给后续手工对照(多为 CC → 多个基本字段的转换动作)。', + Source: ['method_table.json', 'model_binding.json', 'codec_fields.json', 'Inventory.kt'], + Methods: Rows.length, + Resolved: Rows.filter((R) => R.Model).length, + Matched: Matched.length, + MatchedModels, + Models, + Actions: Matched, + }, + null, + 1, + ), + 'utf8', +); + +// ---------------------------------------------------------------- Kotlin 表 +const Lines = []; +Lines.push('// DownlinkActions.kt —— 下行 methodId → 客户端动作(生成物,不要手改)'); +Lines.push('//'); +Lines.push('// 由 3DTank/tools/gen_downlink_actions.js 生成:'); +Lines.push('// methodId(method_table.json)→ modelId → module::codec(model_binding.json)'); +Lines.push('// → 协议模型名(codec_fields.json 的 ModelName)'); +Lines.push('// → Inventory.kt 里唯一一个「只带该模型字段」的动作。'); +Lines.push('//'); +Lines.push('// 覆盖范围:仅自动可判定的组合;其余协议模型需要 CC → 多字段的转换动作,'); +Lines.push('// 见 names/downlink_actions.json 的 Models 清单(后续按协议逐个补转换器)。'); +Lines.push('package App.State'); +Lines.push(''); +Lines.push('/** 每条记录解码后,用 (Any) -> Any? 构造对应动作;未覆盖的 methodId 不在表里。 */'); +Lines.push('val DownlinkActions: Map Any?> = mapOf('); +for (const M of Matched) { + Lines.push( + ' ' + M.MethodId + 'L to { Value -> ' + M.Action + '(Value as ' + M.Model + ') },', + ); +} +Lines.push(')'); +Lines.push(''); +Lines.push('/** Pt/Lt 容器里的 modelId → 动作(同一批协议模型的容器形态)。 */'); +Lines.push('val DownlinkModelActions: Map Any?> = mapOf('); +for (const M of MatchedModels) { + const Ids = ModelBinding.filter((E) => { + const Key = E.Module + '::' + E.Codec; + return ByModuleCodec.get(Key) === M.Model; + }).map((E) => E.ModelId); + for (const Id of Ids) { + Lines.push(' ' + Id + 'L to { Value -> ' + M.Action + '(Value as ' + M.Model + ') },'); + } +} +Lines.push(')'); +Lines.push(''); +fs.writeFileSync(path.join(KotlinRoot, 'DownlinkActions.kt'), Lines.join('\n'), 'utf8'); + +console.log( + `methodId ${Rows.length} 个,解析出协议模型 ${Rows.filter((R) => R.Model).length} 个;` + + `自动匹配动作 ${Matched.length} 条,涉及模型 ${new Set(Matched.map((M) => M.Model)).size} 个。`, +); diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionCheck.kt index 61667ba..1ecda9d 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionCheck.kt @@ -71,6 +71,7 @@ object SessionCheck { println("抓包目录:$Root(${Files.size} 个 s2c 文件)") val Session = SpaceSession() + val Router = SessionRouter() var Frames = 0 var Records = 0 var Ok = 0 @@ -95,9 +96,15 @@ object SessionCheck { Methods[R.MethodId] = (Methods[R.MethodId] ?: 0) + 1 if (R.Payload.contentEquals(R.Reencoded)) Ok++ else Bad++ } + Router.RouteAll(List) } println("文件 $Frames|记录 $Records|回编码一致 $Ok|不一致 $Bad|记录头截断 $HeaderErrors|其它错误 $Errors|别名表=${Session.AliasCount}") + println("路由:已派发 ${Router.Applied}|未覆盖 ${Router.Unmapped}|失败 ${Router.Failed}|用户=${Router.Store.State.User.Uid}|屏幕=${Router.Store.State.Screen.Current}") + val TopUnmapped = Router.UnmappedIds.entries.sortedByDescending { it.value }.take(6) + if (TopUnmapped.isNotEmpty()) { + println("未覆盖 id:" + TopUnmapped.joinToString(",") { "${it.key}×${it.value}" }) + } val Top = Methods.entries.sortedByDescending { it.value }.take(5) if (Top.isNotEmpty()) { println("高频 methodId:" + Top.joinToString(",") { "${it.key}×${it.value}" }) diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionRouter.kt b/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionRouter.kt new file mode 100644 index 0000000..f288836 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/Session/SessionRouter.kt @@ -0,0 +1,82 @@ +// SessionRouter.kt —— 解码记录 → 客户端动作 → Store +// +// 映射表是生成物 `App.State.DownlinkActions`(methodId → (CC) -> Action); +// 未覆盖的 methodId 计入 Unmapped,便于按覆盖清单逐步补转换器。 +package App.Session + +import App.State.DownlinkActions +import App.State.DownlinkModelActions +import App.State.ReduceRoot +import App.State.RootState +import Net.Protocol.DecodedRecord +import Net.Protocol.PtPayload +import State.ReduxStore + +class SessionRouter(InitialState: RootState = RootState()) { + + val Store: ReduxStore = ReduxStore(InitialState, ::ReduceRoot) + + var Applied: Int = 0 + private set + var Unmapped: Int = 0 + private set + var Failed: Int = 0 + private set + + /** 未覆盖的 id(methodId 或 Pt item modelId)计数,自检时打印头部。 */ + val UnmappedIds: MutableMap = LinkedHashMap() + + /** 已应用动作的类名(调试 / 自检用)。 */ + val AppliedNames: MutableList = ArrayList() + + /** 路由一条记录;返回实际派发的动作(未覆盖 / 失败时为 null)。 */ + fun Route(Record: DecodedRecord): Any? { + // Pt 容器:里面的 Item.Id 是 modelId,按模型表路由 + val Value = Record.Value + if (Value is PtPayload) { + var Last: Any? = null + for (Item in Value.Items) { + val MakeModel = DownlinkModelActions[Item.Id] + if (MakeModel == null) { + Unmapped++ + UnmappedIds[Item.Id] = (UnmappedIds[Item.Id] ?: 0) + 1 + continue + } + try { + val Action = MakeModel(Item.Value) + if (Action != null) { + Store.Dispatch(Action) + Applied++ + AppliedNames.add(Action::class.simpleName ?: "?") + Last = Action + } + } catch (E: Throwable) { + Failed++ + } + } + return Last + } + val Make = DownlinkActions[Record.MethodId] + if (Make == null) { + Unmapped++ + UnmappedIds[Record.MethodId] = (UnmappedIds[Record.MethodId] ?: 0) + 1 + return null + } + return try { + val Action = Make(Record.Value) + if (Action != null) { + Store.Dispatch(Action) + Applied++ + AppliedNames.add(Action::class.simpleName ?: "?") + } + Action + } catch (E: Throwable) { + Failed++ + null + } + } + + fun RouteAll(Records: List) { + for (R in Records) Route(R) + } +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/DownlinkActions.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/DownlinkActions.kt new file mode 100644 index 0000000..ab7e2c5 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/DownlinkActions.kt @@ -0,0 +1,34 @@ +// DownlinkActions.kt —— 下行 methodId → 客户端动作(生成物,不要手改) +// +// 由 3DTank/tools/gen_downlink_actions.js 生成: +// methodId(method_table.json)→ modelId → module::codec(model_binding.json) +// → 协议模型名(codec_fields.json 的 ModelName) +// → Inventory.kt 里唯一一个「只带该模型字段」的动作。 +// +// 覆盖范围:仅自动可判定的组合;其余协议模型需要 CC → 多字段的转换动作, +// 见 names/downlink_actions.json 的 Models 清单(后续按协议逐个补转换器)。 +package App.State + +/** 每条记录解码后,用 (Any) -> Any? 构造对应动作;未覆盖的 methodId 不在表里。 */ +val DownlinkActions: Map Any?> = mapOf( + 8836555743203700004L to { Value -> InitGarageResources(Value as GaragePreviewModelCC) }, + 2940606982196178690L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) }, + 2940608322101861340L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) }, + 1880027114297758713L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) }, + 276076212290602640L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 7864195052961084235L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 3982372259754314184L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 6492680006777979140L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 5628691596950015452L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 7864034006236084309L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 3977381235093458189L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 848734776772436751L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, + 8468360636859619961L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, +) + +/** Pt/Lt 容器里的 modelId → 动作(同一批协议模型的容器形态)。 */ +val DownlinkModelActions: Map Any?> = mapOf( + 3730185794267514852L to { Value -> InitGarageResources(Value as GaragePreviewModelCC) }, + 5167916989664044333L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) }, + 1893408944113965505L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) }, +)