feat(session): 生成下行动作映射表并接入会话路由

This commit is contained in:
TermiNexus committed 2026-10-24 22:58:04 +08:00
1 parent 780373180d
commit 87539a9645
6 files changed
+1201

No files matched your search

+13
View File
@@ -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@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
`launchParams` 是纯透传(`(pass-through)`,无 reducer),已把字段收口为 `Map<String, String>`。
+912
View File
@@ -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"
}
]
}
+153
View File
@@ -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<Long, (Any) -> 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<Long, (Any) -> 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<Long, (Any) -> 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} 个。`,
);
@@ -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}" })
@@ -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<RootState> = 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<Long, Int> = LinkedHashMap()
/** 已应用动作的类名(调试 / 自检用)。 */
val AppliedNames: MutableList<String> = 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<DecodedRecord>) {
for (R in Records) Route(R)
}
}
@@ -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<Long, (Any) -> 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<Long, (Any) -> Any?> = mapOf(
3730185794267514852L to { Value -> InitGarageResources(Value as GaragePreviewModelCC) },
5167916989664044333L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) },
1893408944113965505L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
)