Files
TankIC/3DTank/tools/gen_root_wiring.js
T

1050 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// gen_root_wiring.js —— 从 module_10032 抽取「根状态 → 切片 reducer」委派表
//
// 产出:
// 3DTank/names/root_wiring.json 证据表(字段 / 槽位 / 切片类 / reducer / 动作名)
// TankiOffline/app/.../App/State/RootState.kt 原 z6 的 89 个切片字段(已迁移的用强类型,其余保留 Any?)
// TankiOffline/app/.../App/State/RootWiring.kt 委派表 + 迁移覆盖统计
//
// 解析口径(全部取自源码,不猜):
// · 根 reducer:body 含 `return n.cti(` 的 function(当前是 k6)
// · 根状态类:定义 .cti 的类(当前是 z6),字段顺序 = 构造函数参数顺序
// · 槽位:构造函数里的 `(this.<slot>_1 = <param>)`
// · 字段名:toString 里的 `"<field>=" + this.<slot>_1`
// · 委派:根 reducer 顶层赋值 `x = <reducer>(t, n.<slot>…)`;hF(t, n) 这种传整个状态的,
// 到被调函数体里找它实际读的 n.<slot>
// · 未具名形态:内联 IIFE 记 `(inline@<field>)` 并到函数体抓顶层动作名,纯 `n.<slot>` 透传记 `(pass-through)`
// · 动作:被调 reducer 顶层 `instanceof <Local>` → action_inventory 里的类名;
// 没 toString 的少数默认动作由 AnonymousActionNames 按「效果 + 派发点」定名(证据同时写进 JSON)
//
// 用法:cd 3DTank/tools && node gen_root_wiring.js
'use strict';
const fs = require('fs');
const path = require('path');
const P = path.resolve(__dirname, '..');
const T = fs.readFileSync(path.join(P, 'src', 'module_10032.js'), 'utf8');
const Inv = JSON.parse(fs.readFileSync(path.join(P, 'names', 'action_inventory_10032.json'), 'utf8'));
const LocalToName = new Map();
for (const R of Inv.Rows) if (!LocalToName.has(R.Local)) LocalToName.set(R.Local, R.Name);
// module_10849 的 Redux 初始化动作(本模块只 import,不在 action_inventory_10032 里)
LocalToName.set('iz', 'InitState');
// 没有 toString / 不在 action inventory 的动作类:按「reducer 效果 + 派发点」人工定名。
// 每条都必须能在源码里指出证据,否则保持 `?<Local>`。
const AnonymousActionNames = new Map([
// o1:无字段、无 toString;10032 以 `$_$.gl` 导出单例 `Eh`;
// module_18334:153600 的 `nt(Qx).qge = function () { this.dwg_1.uar(c_); }` 派发它
// (Qx 继承 module_47243 的 my,my.u7j 按面板项 id 选择第 2 个可选项时调用 qge)。
// 归约效果:根 reducer home 分支 `n.f7j(!0)`,而 `ld(s1).f7j = function (t) { return new s1(t); }`、
// `ld(s1).toString = "HomeScreen(shopDiscountNotification=" + this.esm_1 + ")"`,
// 即把 HomeScreen.shopDiscountNotification 置 true(主菜单商店项上的折扣提示)。
['o1', 'ShowShopDiscountNotification'],
// 商店切片里没有 toString / 不在 action_inventory 的类:按 reducer 效果定名(原名见 ShopReducer.kt 注释)。
['Yut', 'ShopLoaded'],
['F5', 'ResetShop'],
['g4', 'PaymentUrlReceiveFailed'],
['X5', 'ResetPaymentState'],
['w5', 'SetGoToHomeAfterBuy'],
['o4', 'PaymentProviderInProgress'],
['m4', 'PaymentProviderFinishSuccess'],
['k4', 'PaymentProviderFinishFail'],
['i4', 'BlockBackButton'],
['t4', 'UnblockBackButton'],
['f5', 'ClearPreviousScreen'],
['v5', 'ClearTargetCategory'],
['p5', 'ClearTargetProductId'],
['H7', 'AddCategoriesWithBonus'],
['s5', 'InitPaymentModeAsShopItem'],
['o5', 'SetupDiscountItemForItem'],
['_5', 'RemoveDiscountItemForItem'],
// 屏幕切片(WK / wY.ks5 第 34950 行):没有 toString / 不在 action_inventory 的 9 个类。
['sC', 'DisableForeground'],
['oC', 'EnableForeground'],
['eY', 'ClearScreenStack'],
['uY', 'ResetScreenStackOnNextGoTo'],
['sY', 'DelayNextGoTo'],
['oY', 'FilterScreenStack'],
['lY', 'EnableBack'],
['cY', 'DisableBack'],
['fY', 'PopScreen'],
// 补给箱切片(aH / EH.wrv):没有 toString / 不在 action_inventory 的 6 个类。
['mH', 'SetTimeToGiveLootBox'],
['kH', 'SetPeriodOfGiveLootBox'],
['zH', 'ClearRewards'],
['gH', 'LootBoxOpeningFailed'],
['CH', 'SetIsReopening'],
['SH', 'SetIsBlockOpenButtons'],
// garage 切片(UB / hF):没有 toString / 不在 action_inventory 的 25 个类。
// 括号里是原 reducer 的效果;实现见 GarageReducer.kt。
['rF', 'ResetGarage'],
['EU', 'SetGarageObject'],
['RU', 'SetItemsOnDepot'],
['PU', 'SetItemsOnMarket'],
['NU', 'SetMountedItems'],
['eF', 'GarageLoaded'],
['VB', 'SetGarageNotLoaded'],
['JB', 'SelectGarageCategory'],
['QU', 'AddGarageItemCount'],
['SU', 'GarageMountItem'],
['iU', 'GarageUnmountItem'],
['MU', 'GarageMountItems'],
['dU', 'SetMountedSkinForItems'],
['wU', 'SetMountedShotSkinForItems'],
['aF', 'SetNewItems'],
['cF', 'AddNewItem'],
['lF', 'RemoveNewItem'],
['fF', 'SetResistanceReadyToMount'],
['bU', 'ResetMountDelay'],
['vF', 'StartCooldownReduce'],
['pF', 'SetCooldownReduceEndTime'],
['yU', 'ShowGarageDiscountNotification'],
['WB', 'HideVideoInDescription'],
['OU', 'RemoveWaitServerItemMount'],
['FB', 'ClearWaitServerItemMount'],
// garagePreview 切片(IG / fs.eru):12 个没有 toString 的 instanceof 动作 + 3 个单例动作。
// 括号里是原 reducer 的效果;实现见 GaragePreviewReducer.kt。
['BG', 'CaptureScreenshot'],
['UG', 'CaptureVideo'],
['JG', 'ClearHasGrenades'],
['KG', 'SetWeaponData'],
['OG', 'UpdateViewStyle'],
['QG', 'ClearHullData'],
['VG', 'SetHullData'],
['XG', 'SetHasGrenades'],
['YG', 'SetGrenadeData'],
['ZG', 'ClearWeaponData'],
['nH', 'ClearGrenadeData'],
['tH', 'ClearDroneData'],
['is', 'ClearGaragePreviewParams'],
['es', 'ShowGaragePreview'],
['rs', 'HideGaragePreview'],
// entrance 切片(hD / Bv.pth):6 个没有 toString / 不在 action_inventory 的类。
// 括号里是原 reducer 的效果;实现见 EntranceReducer.kt。
['eit', 'HideSocialNetworks'],
['uit', 'SetControlsForChina'],
['sit', 'SetRunningInChinaDataCenter'],
['oit', 'SetRunningInChinaPartners'],
['lit', 'SetAuthorizedByChinaSdkToken'],
['fit', 'SetClassic'],
// user 切片(t8 / M8().qtd):3 个没有 toString / 不在 action_inventory 的类。
// 括号里是原 reducer 的效果;实现见 UserReducer.kt。
['R8', 'DisableLobbyTutorial'],
['v8', 'SetInDebt'],
['p8', 'ClearInDebt'],
// settings 切片(Uut / xw.ltg):2 个没有 toString / 不在 action_inventory 的类。
// 括号里是原 reducer 的效果;实现见 SettingsReducer.kt。
['Fut', 'SetSettingsCategory'],
['Gut', 'DisableAccountSettings'],
// accountSettings 切片(oet / Lp.mtg):11 个没有 toString / 不在 action_inventory 的类。
// 括号里是原 reducer 的效果;实现见 AccountSettingsReducer.kt。
['aet', 'ResetPasswordFields'],
['fet', 'SetCurrentPassword'],
['vet', 'SetNewPassword'],
['pet', 'SetRepeatNewPassword'],
['wet', 'SetPasswordStatus'],
['det', 'SetCurrentPasswordMandatory'],
['get', 'SetEmail'],
['met', 'SetEmailConfirmed'],
['cet', 'SetCountry'],
['ket', 'SetUnlockButtonForRenameUid'],
['xet', 'ClearAccountRemovalTimer'],
// language 切片(m6().jtg):1 个没有 toString / 不在 action_inventory 的类。
// 实现见 LanguageReducer.kt。
['d6', 'SetLanguage'],
// announcement 切片(lC / PC().rq5):3 个没有 toString / 不在 action_inventory 的类。
// CC 在 action_inventory 里叫 Init,但生成物 App.State.Init 是另一个切片的同名类,这里改名。
// 实现见 AnnouncementReducer.kt。
['bC', 'DisableAnnouncement'],
['qC', 'SetAnnouncementModelLoaded'],
['CC', 'SetAnnouncementItems'],
// notifications 切片(jJ / B_.ise):2 个没有 toString / 不在 action_inventory 的类。
// PJ 是带谓词的移除,RJ 是丢掉第一条;实现见 NotificationsReducer.kt。
['PJ', 'RemoveNotifications'],
['RJ', 'RemoveFirstNotification'],
// quests 切片(Frt / Yrt.otg):1 个没有 toString / 不在 action_inventory 的类。
// 实现见 QuestsReducer.kt。
['Hrt', 'SetNotCompletedQuests'],
// news 切片(ZX / E_.asd):hJ 没有 toString;两个单例 x_/j_ 按效果定名。
// 实现见 NewsReducer.kt。
['hJ', 'SetNewsFlag'],
['x_', 'OpenNews'],
['j_', 'CloseNews'],
// lobbyLoader 切片(KW / VW.es1):FW/GW 没有 toString,按 reducer 效果定名。
['FW', 'ShowLoader'],
['GW', 'HideLoader'],
// purchaseDialogState 切片(CO / DO.ur2):NO 没有 toString,按 reducer 效果定名。
['NO', 'ResetPurchaseDialog'],
// supportUrl 切片(I4 / P4.ct7):R4 没有 toString,按 reducer 效果定名。
['R4', 'ClearSupportUrl'],
// notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 效果定名。
['qJ', 'MarkHomeScreenDialogShown'],
// mainQuest 切片(WZ / ih.bsk):VZ 没有 toString,按 reducer 效果定名。
['VZ', 'ResetQuestsLoaded'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
// 它们从 Actions 列表里剔除,改记到 root_wiring.json 的 Markers。
const NonActionInstanceof = new Map([
['LX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Entrance:`function LX(){ this.gs5_1="Entrance" }`,`ld(LX).m()` 返回 "Entrance";GoTo 分支用它判断 analytics 事件是否 isModal。' }],
['RX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 System.Loader:`function RX(){ this.ysb_1="System.Loader" }`,`PX()` 返回该单例,也是 WK.current 的默认值;GoTo 到它时不发 screen_show。' }],
['FX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Garage:`function FX(t){ … this.irm_1 = t; this.jrm_1 = "Garage" }`,默认分类 `BB().sq5_1`;garage 的 GoTo 分支用它把 currentCategory 复位。' }],
['UB', { Kind: 'state-class', Evidence: 'garage 切片状态类:`function UB(…31 字段)`,toString 名 "Garage(...)";InitState 分支用 `h instanceof UB` 校验灌入状态,不是动作。' }],
['tX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Lobby.Quests.QuestCategory:`function tX(categoryType, id){ … this.gs8_1 = "Lobby.Quests.QuestCategory." + … }`;quests 的 GoTo 分支用它判断 section,不是 dispatch 的动作。' }],
['nX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Lobby.Quests.Challenge:`function nX(t){ … this.js8_1 = "Lobby.Quests.Challenge" }`;quests 的 GoTo 分支命中它时把 section 置 CHALLENGE,不是 dispatch 的动作。' }],
]);
/** 动作局部类名 → 动作名;未收录的返回 null。 */
function ActionNameOf(Local) {
// 匿名表优先:个别同名类(如 announcement 的 CC=Init)会用生成物里的名字,
// 但该生成物属于另一个切片,必须在匿名表里显式改名。
return AnonymousActionNames.get(Local) || LocalToName.get(Local) || null;
}
// ---------------------------------------------------------------- 源码扫描基础
/** 字符串/注释感知的花括号配对。 */
function BracePairs(Text) {
const Pairs = new Map();
const Stack = [];
let I = 0;
while (I < Text.length) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C;
I++;
while (I < Text.length) {
if (Text[I] === '\\') { I += 2; continue; }
if (Text[I] === Q) { I++; break; }
I++;
}
continue;
}
if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); I = N < 0 ? Text.length : N; continue; }
if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); I = N < 0 ? Text.length : N + 2; continue; }
if (C === '{') Stack.push(I);
else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); }
I++;
}
return Pairs;
}
const Pairs = BracePairs(T);
/** 收集所有 function 与 `X.m = function` 的体区间。 */
const Fns = [];
for (const M of T.matchAll(/(?<![\w$.])function ([A-Za-z0-9_$]+)\(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End !== undefined) Fns.push({ Name: M[1], Params: M[2], Start: M.index, Body: Open + 1, End });
}
const Methods = [];
for (const M of T.matchAll(/\(([A-Za-z_$][\w$]*)\(([A-Za-z0-9_$]+)\)\.([A-Za-z0-9_$]+) = function \(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End !== undefined) {
Methods.push({ Obj: M[2], Method: M[3], Params: M[4], Start: M.index, Body: Open + 1, End });
}
}
function FindFn(Name) { return Fns.find((F) => F.Name === Name) || null; }
function FindMethod(Ref) {
const Dot = Ref.lastIndexOf('.');
if (Dot < 0) return null;
const Obj = Ref.slice(0, Dot);
const Method = Ref.slice(Dot + 1);
return Methods.find((M) => M.Obj === Obj && M.Method === Method) || null;
}
/** 把 [From, To) 内的嵌套 function 体挖空,只留顶层分支(避免把 IIFE 里的 instanceof 算进来)。 */
function MaskNested(Text, From, To) {
const Out = Text.split('');
for (const F of Fns) {
if (F.Start > From && F.End < To) {
for (let I = F.Start; I <= F.End; I++) Out[I] = ' ';
}
}
for (const M of Methods) {
if (M.Start > From && M.End < To) {
for (let I = M.Start; I <= M.End; I++) Out[I] = ' ';
}
}
return Out.slice(From, To).join('');
}
/** 顶层 `name = expr` 赋值;返回 [{Name, Expr}](expr 已 trim)。 */
function TopLevelAssignments(Body) {
const Out = [];
let I = 0;
let Depth = 0;
const Strings = [];
while (I < Body.length) {
const C = Body[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C;
I++;
while (I < Body.length) {
if (Body[I] === '\\') { I += 2; continue; }
if (Body[I] === Q) { I++; break; }
I++;
}
Strings.push(I);
continue;
}
if (C === '(' || C === '[' || C === '{') { Depth++; I++; continue; }
if (C === ')' || C === ']' || C === '}') { Depth--; I++; continue; }
if (Depth === 0) {
const M = /^([A-Za-z_$][\w$]*)\s*=\s*/.exec(Body.slice(I));
if (M && (I === 0 || /[;{}\s]/.test(Body[I - 1]))) {
const Name = M[1];
let J = I + M[0].length;
let D = 0;
while (J < Body.length) {
const D0 = Body[J];
if (D0 === '"' || D0 === "'" || D0 === '`') {
const Q = D0;
J++;
while (J < Body.length) {
if (Body[J] === '\\') { J += 2; continue; }
if (Body[J] === Q) { J++; break; }
J++;
}
continue;
}
if (D0 === '(' || D0 === '[' || D0 === '{') D++;
else if (D0 === ')' || D0 === ']' || D0 === '}') D--;
else if (D0 === ',' && D === 0) break;
else if (D0 === ';' && D === 0) break;
J++;
}
Out.push({ Name, Expr: Body.slice(I + M[0].length, J).trim() });
I = J + 1;
continue;
}
}
I++;
}
return Out;
}
/** 定位根 reducer / 根状态类。 */
const RootReducerFn = Fns.find((F) => T.slice(F.Body, F.End).includes('return n.cti('));
if (!RootReducerFn) throw new Error('找不到根 reducer(body 含 return n.cti()');
const CopyMethod = Methods.find((M) => M.Method === 'cti');
if (!CopyMethod) throw new Error('找不到根状态类的 cti');
const RootClass = CopyMethod.Obj;
const RootClassFn = FindFn(RootClass);
if (!RootClassFn) throw new Error('找不到根状态类构造函数 ' + RootClass);
// ---------------------------------------------------------------- 根状态:参数 / 槽位 / 字段
const Params = RootClassFn.Params.split(',').map((S) => S.trim()).filter(Boolean);
const CtorBody = T.slice(RootClassFn.Body, RootClassFn.End);
const SlotOfParam = new Map();
for (const M of CtorBody.matchAll(/\(this\.(\w+) = (\w+)\)/g)) SlotOfParam.set(M[2], M[1]);
const DefaultOfParam = new Map();
for (const M of CtorBody.matchAll(/\((\w+) = \1 === cd \? ([\s\S]*?) : \1\)/g)) {
DefaultOfParam.set(M[1], M[2].replace(/\s+/g, ' ').trim());
}
for (const M of CtorBody.matchAll(/\((\w+) = \1 !== cd && \1\)/g)) DefaultOfParam.set(M[1], 'false');
const ToStringStart = T.indexOf(`(ld(${RootClass}).toString = function`);
if (ToStringStart < 0) throw new Error('找不到根状态类 toString');
const ToStringEnd = T.indexOf(`(ld(${RootClass}).hashCode`, ToStringStart);
const ToStringBody = T.slice(ToStringStart, ToStringEnd);
const Fields = [];
for (const M of ToStringBody.matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
Fields.push({ Field: M[2], Slot: M[3] });
}
if (Fields.length !== Params.length) {
throw new Error(`字段数 ${Fields.length} 与构造参数数 ${Params.length} 不一致`);
}
// ---------------------------------------------------------------- 委派表达式
const ReduceBody = T.slice(RootReducerFn.Body, RootReducerFn.End);
const Assignments = TopLevelAssignments(ReduceBody);
/** 从表达式里找出所有 n.<slot> 引用。 */
function SlotsIn(Expr) {
return [...new Set([...Expr.matchAll(/\bn\.([A-Za-z0-9_$]+)/g)].map((M) => M[1]))];
}
/** 单例持有者 → 实际类,例如 `(Ke = new pD())`、`(nu = new MD())`。 */
const SingletonClass = new Map();
for (const M of T.matchAll(/\(([A-Za-z_$][\w$]*) = new ([A-Za-z_$][\w$]*)\(\)\)/g)) {
SingletonClass.set(M[1], M[2]);
}
/** `X()` 形式的工厂 → 它返回的类,例如 `function M8() { return (null == X && new N8(), X); }`。 */
function FactoryClass(Name) {
const F = FindFn(Name);
if (!F) return null;
const Body = T.slice(F.Body, F.End);
const M = /new ([A-Za-z_$][\w$]*)\(/.exec(Body);
return M ? M[1] : null;
}
/**
* 表达式对应的 reducer 名字(`M8().qtd(t, n.ypu_1)` → `M8().qtd`)。
* 表达式以别的调用开头、但结尾仍是 `<holder>.<method>(t, n.<slot>)` 时取末尾调用,
* 例如 quests 的 `(null == zp && new Yrt(), zp).otg(t, n.npv_1)` → `zp.otg`;真 IIFE → `(inline)`。
*/
function ReducerRef(Expr) {
// 懒汉单例:`(null == zp && new Yrt(), zp).otg(t, n.npv_1)` → `Yrt.otg`
const Lazy = /^\(null == [A-Za-z_$][\w$]* && new ([A-Za-z_$][\w$]*)\(\), [A-Za-z_$][\w$]*\)\.([A-Za-z_$][\w$]*)\(/.exec(Expr);
if (Lazy) return Lazy[1] + '.' + Lazy[2];
// 纯透传:`at = n.hpx_1`(切片没有 reducer,值原样带过去)
if (/^n\.[A-Za-z0-9_$]+$/.test(Expr)) return '(pass-through)';
const Head = /^([A-Za-z_$][\w$]*)(\(\))?(?:\.([A-Za-z_$][\w$]*))?\s*\(/.exec(Expr);
if (Head && (Head[3] || /^\(\s*t\s*,/.test(Expr.slice(Head[0].length - 1)))) {
if (Head[3]) return Head[1] + (Head[2] || '') + '.' + Head[3];
return Head[1];
}
const Tail = /([A-Za-z_$][\w$]*(?:\(\))?\.[A-Za-z_$][\w$]*)\(t,\s*n\.\w+\)\s*$/.exec(Expr);
if (Tail) return Tail[1];
return Head ? Head[1] + (Head[2] || '') : '(inline)';
}
/** 取 reducer 体(function / X().m / X.m;hF 这种传整个状态的也能处理)。 */
function BodyOfRef(Ref) {
if (Ref.startsWith('(')) return null;
const Dot = Ref.lastIndexOf('.');
if (Dot < 0) {
const F = FindFn(Ref);
return F ? { From: F.Body, To: F.End } : null;
}
const ObjRaw = Ref.slice(0, Dot);
const Method = Ref.slice(Dot + 1).replace(/\(\)$/, '');
const IsFactory = /\(\)$/.test(ObjRaw);
const Obj = ObjRaw.replace(/\(\)$/, '');
let ClassName = null;
if (IsFactory) ClassName = FactoryClass(Obj);
if (!ClassName) ClassName = SingletonClass.get(Obj) || null;
if (!ClassName) ClassName = Obj;
const M = Methods.find((X) => X.Obj === ClassName && X.Method === Method) ||
Methods.find((X) => X.Obj === Obj && X.Method === Method);
return M ? { From: M.Body, To: M.End } : null;
}
/** 从已屏蔽嵌套 function 的片段里收集顶层 instanceof 命中的动作名。 */
function NamesInMasked(Masked) {
const Names = new Set();
for (const M of Masked.matchAll(/instanceof ([A-Za-z0-9_$]+)/g)) {
if (NonActionInstanceof.has(M[1])) continue;
const N = ActionNameOf(M[1]);
Names.add(N || '?' + M[1]);
}
return [...Names].sort();
}
/** reducer 顶层 instanceof 命中的动作名(没有 toString 的类记作 `?<Local>`)。 */
function ActionsOf(Ref) {
const B = BodyOfRef(Ref);
if (!B) return [];
return NamesInMasked(MaskNested(T, B.From, B.To));
}
/**
* 内联 IIFE 的动作名:表达式形如 `(function (t, n) { … })(t, n.<slot>)`,
* 到源码里找到它的函数体,屏蔽嵌套 function 后收集顶层 instanceof。
*/
function InlineActions(Pos, Expr) {
if (Pos < 0) return [];
const FnAt = T.indexOf('function', Pos);
if (FnAt < 0 || FnAt > Pos + Expr.length) return [];
const Open = T.indexOf('{', FnAt);
if (Open < 0 || Open > Pos + Expr.length) return [];
const End = Pairs.get(Open);
if (End === undefined) return [];
return NamesInMasked(MaskNested(T, Open + 1, End));
}
// 槽位 → 委派 { Reducer, Expr, Slots }
const WiringBySlot = new Map();
for (const A of Assignments) {
const Slots = SlotsIn(A.Expr);
if (!Slots.length) continue;
const Pos = T.indexOf(A.Expr, RootReducerFn.Body);
for (const S of Slots) {
if (!WiringBySlot.has(S)) {
WiringBySlot.set(S, {
Reducer: ReducerRef(A.Expr),
Expr: A.Expr.replace(/\s+/g, ' ').trim(),
Pos,
Local: A.Name,
});
}
}
}
// hF(t, n):表达式里没有 n.<slot>,到被调体里找它读的槽位
for (const A of Assignments) {
if (SlotsIn(A.Expr).length) continue;
const Ref = ReducerRef(A.Expr);
const B = BodyOfRef(Ref);
if (!B) continue;
const Slots = SlotsIn(T.slice(B.From, B.To));
for (const S of Slots) {
if (!WiringBySlot.has(S)) {
WiringBySlot.set(S, {
Reducer: Ref,
Expr: A.Expr.replace(/\s+/g, ' ').trim(),
Pos: -1,
Local: A.Name,
});
}
}
}
// ---------------------------------------------------------------- 汇总
const Rows = Fields.map((F) => {
const W = WiringBySlot.get(F.Slot);
const Param = Params.find((P) => SlotOfParam.get(P) === F.Slot) || null;
return {
Field: F.Field,
Slot: F.Slot,
SliceClass: null, // 由下面补(切片类名在构造函数默认值 / toString 里没有,靠 reducer 签名查)
Reducer: W ? W.Reducer : '(未识别)',
Delegate: W ? W.Expr.replace(/\s+/g, ' ').trim().slice(0, 500) : '',
Actions: [],
Default: Param ? DefaultOfParam.get(Param) || '' : '',
_W: W,
};
});
// 内联 IIFE 没有源码符号,按字段具名成 `(inline@<field>)`,动作名到函数体里现抓
for (const R of Rows) {
const W = R._W;
delete R._W;
if (!W) continue;
if (W.Reducer === '(inline)') {
R.Reducer = '(inline@' + R.Field + ')';
R.Actions = InlineActions(W.Pos, W.Expr);
} else {
R.Actions = ActionsOf(W.Reducer);
}
if (R.Delegate.length === 500) R.Delegate += ' …';
}
// 切片类:从 reducer 所在类的 `X(new <Slice>)` 或根状态构造默认 `new <Slice>()` 取
const SliceClassOfParam = new Map();
for (const [Param, Default] of DefaultOfParam) {
const M = /^new ([A-Za-z0-9_$]+)\(/.exec(Default);
if (M) SliceClassOfParam.set(Param, M[1]);
}
for (const R of Rows) {
const Param = Params.find((P) => SlotOfParam.get(P) === R.Slot);
R.SliceClass = Param ? SliceClassOfParam.get(Param) || null : null;
}
// 根 reducer 里没有出现过的槽位(例如被包在更大表达式里)
for (const R of Rows) if (R.Reducer === '(未识别)') console.warn('未识别委派: ' + R.Field + ' / ' + R.Slot);
// ---------------------------------------------------------------- 迁移标记(与 Kotlin 侧保持一致)
const Migrated = {
inviteState: { Type: 'InviteStateSlice', Reduce: 'ReduceInvite' },
loginByPassword: { Type: 'LoginByPasswordStateSlice', Reduce: 'ReduceLoginByPassword' },
twitchState: { Type: 'TwitchStateSlice', Reduce: 'ReduceTwitch' },
home: { Type: 'HomeStateSlice', Reduce: 'ReduceHome' },
localBattleUserState: { Type: 'LocalBattleUserStateSlice', Reduce: 'ReduceLocalBattleUser' },
shop: { Type: 'ShopStateSlice', Reduce: 'ReduceShop' },
screen: { Type: 'ScreenStateSlice', Reduce: 'ReduceScreen' },
lootBoxes: { Type: 'LootBoxesStateSlice', Reduce: 'ReduceLootBoxes' },
garage: { Type: 'GarageStateSlice', Reduce: 'ReduceGarage' },
garagePreview: { Type: 'GaragePreviewStateSlice', Reduce: 'ReduceGaragePreview' },
entrance: { Type: 'EntranceStateSlice', Reduce: 'ReduceEntrance' },
user: { Type: 'UserStateSlice', Reduce: 'ReduceUser' },
settings: { Type: 'SettingsStateSlice', Reduce: 'ReduceSettings' },
accountSettings: { Type: 'AccountSettingsStateSlice', Reduce: 'ReduceAccountSettings' },
proBattle: { Type: 'ProBattleStateSlice', Reduce: 'ReduceProBattle' },
// language 在原根状态里是枚举对象本身,不是对象切片;Default/Import 显式指定。
language: {
Type: 'Language',
Default: 'App.State.Language.EN',
Reduce: 'ReduceLanguage',
Import: false,
},
announcement: { Type: 'AnnouncementStateSlice', Reduce: 'ReduceAnnouncement' },
// 原根 reducer 对这一片没有调用,只有默认值 `ag()`(空 Map);这里仅做类型收口。
launchParams: {
Type: 'LaunchParamsStateSlice',
Default: 'emptyMap()',
Import: false,
PassThrough: true,
},
news: { Type: 'NewsStateSlice', Reduce: 'ReduceNews' },
// 原根状态的这一片是 `new jJ()`,生成器没认出切片类,默认值在这里显式补。
notifications: {
Type: 'NotificationsStateSlice',
Default: 'NotificationsStateSlice()',
SliceClass: 'jJ',
Reduce: 'ReduceNotifications',
},
quests: { Type: 'QuestsStateSlice', Reduce: 'ReduceQuests' },
lobbyLoader: { Type: 'LobbyLoaderStateSlice', Reduce: 'ReduceLobbyLoader' },
purchaseDialogState: { Type: 'PurchaseDialogStateSlice', Reduce: 'ReducePurchaseDialog' },
promoCode: { Type: 'PromoCodeStateSlice', Reduce: 'ReducePromoCode' },
supportUrl: { Type: 'SupportUrlStateSlice', Reduce: 'ReduceSupportUrl' },
rulesUpdated: { Type: 'RulesUpdatedStateSlice', Reduce: 'ReduceRulesUpdated' },
notificationByCountry: {
Type: 'NotificationByCountryStateSlice',
Reduce: 'ReduceNotificationByCountry',
},
availabilityByRank: { Type: 'AvailabilityByRankStateSlice', Reduce: 'ReduceAvailabilityByRank' },
icons: { Type: 'IconsStateSlice', Reduce: 'ReduceIcons' },
questCategories: { Type: 'QuestCategoriesStateSlice', Reduce: 'ReduceQuestCategories' },
mainQuest: { Type: 'MainQuestStateSlice', Reduce: 'ReduceMainQuest' },
securitySettings: { Type: 'SecuritySettingsStateSlice', Reduce: 'ReduceSecuritySettings' },
referralEvent: { Type: 'ReferralEventStateSlice', Reduce: 'ReduceReferralEvent' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
// 少数切片的类名生成器认不出来(例如根构造里是 `new jJ()` 但字段默认值挡住了推断),
// 迁移表里可以显式补一个 SliceClass。
for (const R of Rows) {
const M = Migrated[R.Field];
if (M && M.SliceClass) R.SliceClass = M.SliceClass;
}
// 字段名 → Kotlin 属性名(首字母大写,保留原有大小写与数字)
function KotlinName(Field) { return Field.charAt(0).toUpperCase() + Field.slice(1); }
/** JSON 字符串 → Kotlin 字符串字面量(`$` 要转义,否则会被当成模板起点)。 */
function KStr(S) { return JSON.stringify(String(S)).replace(/\$/g, '\\$'); }
const SeenProps = new Set();
for (const R of Rows) {
const N = KotlinName(R.Field);
if (SeenProps.has(N)) throw new Error('Kotlin 属性名重复: ' + N);
SeenProps.add(N);
}
// ---------------------------------------------------------------- 输出
// shop 切片的匿名动作:没有 toString / 不在 action_inventory,按 reducer 分支效果定名。
// 证据统一来自 module_10032 的 `(inline@shop)` 分支与 U7/w3 两个子 reducer(行号见 ShopReducer.kt)。
const ShopAnonymousEffects = {
Yut: 'isLoaded = true(t instanceof Yut → n.qt6(…, !0))',
F5: 'new j4(…) 整片复位,只保留 coinPaymentProviderUrl(第 7 个构造参数 n.qq3_1)',
g4: 'paymentUrl = null、receivePaymentUrlFailed = true',
X5: 'paymentUrl = null、receivePaymentUrlFailed = false、mobilePaymentState = NONE、paymentProviderState = NONE',
w5: 'goToHomeAfterBuy = true',
o4: 'paymentProviderState = IN_PROGRESS(V5().Rl)',
m4: 'paymentProviderState = FINISH_SUCCESS(y4())',
k4: 'paymentProviderState = FINISH_FAIL(V5().Nl)',
i4: 'isBlockedBackButton = true',
t4: 'isBlockedBackButton = false',
f5: 'previousScreen = null',
v5: 'targetCategory = null',
p5: 'targetProductId = null',
H7: 'U7.categoriesWithBonus = Zg(现有集合, t.zsw_1)',
s5: 'U7/w3.paymentModeAsShopItem[ft3_1] = gt3_1',
o5: 'U7/w3.discountItems[dt3_1] = et3_1',
_5: 'U7/w3.discountItems 删除键 ct3_1',
};
// screen 切片(WK / wY.ks5)的匿名动作:按 reducer 分支效果定名。
const ScreenAnonymousEffects = {
sC: 'foreground = false(`n.vs4(cd, cd, !1)`)',
oC: 'foreground = true(`n.vs4(cd, cd, !0)`)',
eY: 'screenStack = emptyList()(`n.vs4(cd, cd, cd, md())`)',
uY: 'isResetBackStackOnNextGoTo = true(第 6 个构造参数 !0)',
sY: 'isDelayedGoto = true(第 7 个构造参数 !0)',
oY: 'screenStack = stack.filter(predicate)(`for (…) e(o) && u.y(o)`,默认谓词 YK 恒 false)',
lY: 'enableBack = true(第 5 个构造参数 !0)',
cY: 'enableBack = false(第 5 个构造参数 !1)',
fY: 'screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)',
};
// 补给箱切片(aH / EH.wrv)的匿名动作:按 reducer 分支效果定名。
const LootBoxesAnonymousEffects = {
mH: 'timeToGiveLootBox = Date.now() + seconds * 1000(第 9 个构造参数 `Fg(Md(), Qk.y2o(Wg(t.zrv_1)))`)',
kH: 'periodOfGiveLootBoxInSec = action.yrv_1(第 10 个构造参数)',
zH: 'rewards = emptyList()、shownRewards = emptyList()(第 6、7 个构造参数 `md(), md()`)',
gH: 'openingFailed = true(第 11 个构造参数 !0)',
CH: 'isReopening = true(第 17 个构造参数 !0)',
SH: 'isBlockOpenButtons = action.xrv_1(第 18 个构造参数)',
};
// garage 切片(UB / hF):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const GarageAnonymousEffects = {
rF: 'devices = new UF()、upgradeCoolDownEndTimes = emptyMap()、upgradeItemInProcessingId = null',
EU: 'garageObject = action.nrk_1',
RU: 'itemsOnDepot = action.rrk_1',
PU: 'itemsOnMarket = action.srk_1',
NU: 'mountedItems = action.trk_1',
eF: '按 itemsOnDepot / mountedItems / itemsOnMarket / skinsItems 重建 items(PB),isLoaded = true',
VB: 'isLoaded = false',
JB: 'currentCategory = action.qri_1、currentItemId = null',
QU: 'items.grg(item, copy(owned=true, count += action.trl_1, devicePreview = 改装等级对应项))',
SU: 'items.jrg(action.lrk_1)(同 viewCategory 先卸下已装配项,再装配本项)',
iU: 'items.mrg(action.crj_1)(mounted = false)',
MU: 'items.krg(action.urk_1)(批量 mounted = true)',
dU: 'items.qrg(action.vrj_1, action.wrj_1)(整组 mountedSkin = skinId)',
wU: 'items.rrg(action.srj_1, action.trj_1)(整组 mountedShotSkin = skinId)',
aF: 'newItems = action.srm_1',
cF: 'newItems[itemId] += value(Set 并集)',
lF: 'newItems[itemId] -= value,空集则删键',
fF: 'resistanceReadyToMount = action.nrm_1',
bU: 'delayMountTimeMs = 0',
vF: 'cooldownReduceEndTime = now + seconds*1000、timeToReduceUpdateInMin = action.mrm_1',
pF: 'cooldownReduceEndTime = now + seconds*1000',
yU: 'garageDiscountNotification = true',
WB: 'showVideoInDescription = false',
OU: 'waitServerItemMount 的给定键全部置 false',
FB: 'waitServerItemMount = emptyMap()',
};
// garagePreview 切片(IG / fs.eru):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const GaragePreviewAnonymousEffects = {
BG: '调用 captureScreenshotAction(),状态不变(`null == y || y(), i = n`)',
UG: '调用 captureVideoAction(),状态不变(`null == $ || $(), i = n`)',
JG: 'hasGrenades = false(第 13 个构造参数 !1)',
KG: '若 expectedItemId[WEAPON] 命中,则 weaponData = TankPartData(resource, emissionColors)、turretMechanism = action;并缓存动作、从 requestedItems 删除 itemId',
OG: 'UpdateViewStyle:动作类没有字段,原 `t.fru_1` 为 undefined,`trs` 视作省略,状态不变',
QG: 'hullData = null(第 9 个构造参数 null)',
VG: '若 expectedItemId[ARMOR] 命中,则 hullData = TankPartData(6 字段)、hullEngineType = action;并缓存动作、从 requestedItems 删除 itemId',
XG: 'hasGrenades = true(第 13 个构造参数 !0)',
YG: 'grenadeData = resource == null ? null : TankPartData(resource, emissionColors)(第 12 个构造参数)',
ZG: 'weaponData = null(第 10 个构造参数 null)',
nH: 'grenadeData = null(第 12 个构造参数 null)',
tH: 'droneData = null(第 11 个构造参数 null)',
is: 'params = null(第 8 个构造参数 null)',
es: 'isVisible = true、needVisible = false(`n.trs(!0, cd, !1)`)',
rs: 'isVisible = false(`n.trs(!1)`)',
};
// entrance 切片(hD / Bv.pth):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const EntranceAnonymousEffects = {
eit: 'showSocialNetworks = false(`n.ur4(!1)`)',
uit: 'controlsForChina = true(`n.ur4(cd, !0)`)',
sit: 'isRunningInChinaDataCenter = true(`n.ur4(cd, cd, !0)`)',
oit: 'isRunningInChinaPartners = true(`n.ur4(cd, cd, cd, !0)`)',
lit: 'authorizedByChinaSdkToken = true(`n.ur4(cd, cd, cd, cd, cd, !0)`)',
fit: 'isClassic = true(`n.ur4(cd, cd, cd, cd, cd, cd, !0)`)',
};
// user 切片(t8 / M8().qtd):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const UserAnonymousEffects = {
R8: 'lobbyTutorialEnabled = false(`n.qtb(cd × 29, !1)`)',
v8: 'inDebt = true(`n.qtb(cd × 30, !0)`)',
p8: 'inDebt = false(`n.qtb(cd × 30, !1)`)',
};
// settings 切片(Uut / xw.ltg):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const SettingsAnonymousEffects = {
Fut: 'currentCategory = action.category(`n.bu8(t.cu8_1)`)',
Gut: 'disableAccountSettings = true(`n.bu8(cd, !0)`)',
};
// accountSettings 切片(oet / Lp.mtg):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const AccountSettingsAnonymousEffects = {
aet: 'currentPassword/newPassword/repeatNewPassword = "",newPasswordStatus = NONE,isUnlockButtonForRenameUid = false',
fet: 'currentPassword = action.currentPassword',
vet: 'newPassword = action.newPassword',
pet: 'repeatNewPassword = action.repeatNewPassword',
wet: 'newPasswordStatus = action.status',
det: 'isCurrentPasswordMandatory = action.isMandatory',
get: 'email = action.email',
met: 'emailConfirmed = action.emailConfirmed',
cet: 'countryCode / countryName = action 的两个字段',
ket: 'isUnlockButtonForRenameUid = action.isUnlockButtonForRenameUid',
xet: 'removalTimer = null',
};
// language 切片(m6().jtg):只把动作里的语言枚举写回。
const LanguageAnonymousEffects = {
d6: 'language = action.language(`t instanceof d6 ? t.itg_1 : n`)',
};
// announcement 切片(lC / PC().rq5):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const AnnouncementAnonymousEffects = {
bC: 'isEnabled = false',
qC: 'modelLoaded = true',
CC: 'items = action.items',
};
// news 切片(ZX / E_.asd):没有 toString / 不在 action_inventory 的类与单例,按 reducer 分支效果定名。
const NewsAnonymousEffects = {
x_: 'isOpen = true(`yd(t, x_)` → `n.psc(!0)`;x_ = new QX() 单例)',
j_: 'isOpen = false(`yd(t, j_)` → `n.psc(!1)`;j_ = new tJ() 单例)',
hJ: 'p = true(`n.psc(cd, cd, cd, !0)`;动作类无字段,字段语义未定)',
};
// notifications 切片(jJ / NJ.ise):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const NotificationsAnonymousEffects = {
PJ: 'notifications = notifications.filter {!action.predicate(it)}(`t.hse_1(s) || e.y(s)`)',
RJ: 'notifications = notifications.drop(1)(`w$(n.tsd_1, 1)`)',
};
// quests 切片(Frt / Yrt.otg):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const QuestsAnonymousEffects = {
Hrt: '状态不变(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,`t.iu5_1` 与 `cd` 都是 undefined,三个参数全走默认值 → no-op)',
};
// lobbyLoader 切片(KW / VW.es1):FW/GW 没有 toString,按 reducer 分支效果定名。
const LobbyLoaderAnonymousEffects = {
FW: 'isShowLoader=true、activeLoadersCount = count + 1(`n.fs1(!0, (n.ss0_1 + 1) | 0)`)',
GW: 'activeLoadersCount = max(count - 1, 0)、isShowLoader = count != 0(`n.fs1(!(0 === e), e)`)',
};
// purchaseDialogState 切片(CO / DO.ur2):NO 没有 toString,按 reducer 分支效果定名。
const PurchaseDialogAnonymousEffects = {
NO: 'withEmail=false、goods=emptyList()、emailStatus=null、time=null(`n.jr2(!1, md(), null, null)`)',
};
// supportUrl 切片(I4 / P4.ct7):R4 没有 toString,按 reducer 分支效果定名。
const SupportUrlAnonymousEffects = {
R4: 'url=""(`n.at7()`)',
};
// notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 分支效果定名。
const NotificationByCountryAnonymousEffects = {
qJ: 'homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)',
};
// mainQuest 切片(WZ / ih.bsk):VZ 没有 toString / 不在 action_inventory,按 reducer 分支效果定名。
const MainQuestAnonymousEffects = {
VZ: 'isLoaded=false(`n.qsj(!1)`;动作类 rsj_1 在 reducer 里未被读取)',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
JSON.stringify(
{
generatedAt: new Date().toISOString(),
module: 'module_10032.js',
RootState: { Class: RootClass, Reducer: RootReducerFn.Name, FieldCount: Rows.length },
Migrated: Object.keys(Migrated),
AnonymousActions: [
{
Local: 'o1',
Name: 'ShowShopDiscountNotification',
Export: '$_$.gl(module_10032 单例 Eh)',
DispatchedAt: 'module_18334 的 nt(Qx).qge(Qx 继承 module_47243 的 my,u7j 选中 nge_1 时调用)',
Effect: 'HomeScreen(shopDiscountNotification = true)(根 reducer home 分支 n.f7j(!0))',
},
...Object.entries(ShopAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(ScreenAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(LootBoxesAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(GarageAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(GaragePreviewAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(EntranceAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(UserAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(SettingsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(AccountSettingsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(LanguageAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(AnnouncementAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(NewsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(NotificationsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(QuestsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(LobbyLoaderAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(PurchaseDialogAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(SupportUrlAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(NotificationByCountryAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(MainQuestAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
],
Markers: [
{
Local: '$4',
Kind: 'marker-interface',
Evidence: 'module_10032 的 `fd(O7, "", cd, cd, [$4])` 等注册:InitCategory/InitShopCard/… 等商店内容动作都实现它;根 reducer 用 `Xd(t, $4)` 走 U7+w3 快路径。',
},
{
Local: 'x4',
Kind: 'marker-action',
Evidence: '`fd(x4, "", cd, Wm)`;根 reducer 的 `Xd(t, $4) || t instanceof x4` 同样走 U7+w3 快路径,本身无字段、无独立效果。',
},
...[...NonActionInstanceof].map(([Local, Info]) => ({
Local,
Kind: Info.Kind,
Evidence: Info.Evidence,
})),
],
Rows,
},
null,
2
) + '\n'
);
const KT = path.resolve(__dirname, '..', '..', 'TankiOffline', 'app', 'src', 'jsMain', 'kotlin', 'App', 'State');
fs.mkdirSync(KT, { recursive: true });
const Lines = [];
Lines.push('// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。');
Lines.push(`// 来源:module_10032 的根状态 ${RootClass}(toString 名 TOState,${Rows.length} 个切片)`);
Lines.push('// 与根 reducer ' + RootReducerFn.Name + '(逐切片归约后 copy 出新状态)。');
Lines.push('// 已迁移的切片用强类型;其余保持 Any?(原样透传),迁移一个改一个再重新生成。');
Lines.push('package App.State');
Lines.push('');
for (const F of Object.keys(Migrated).sort()) {
if (Migrated[F].Import === false) continue;
Lines.push('import App.State.Reducers.' + Migrated[F].Type);
}
Lines.push('');
Lines.push('/**');
Lines.push(' * 原 `' + RootClass + '` / `TOState`:' + Rows.length + ' 个切片字段,顺序与 toString 一致。');
Lines.push(' * 未迁移字段默认 null —— 原包构造时会给它们 `new <Slice>()`,这里等对应切片迁移后再补默认值。');
Lines.push(' */');
Lines.push('data class RootState(');
for (const R of Rows) {
const Prop = KotlinName(R.Field);
const M = Migrated[R.Field];
if (M) {
const Kind = M.PassThrough ? '已迁移(透传,强类型)' : '已迁移';
const Init = M.Default || M.Type + '()';
Lines.push(` /** 原 ${R.Field}(${R.SliceClass || '?'} / ${R.Reducer})。${Kind}。 */`);
Lines.push(` val ${Prop}: ${M.Type} = ${Init},`);
} else {
Lines.push(` /** 原 ${R.Field}(${R.SliceClass || '?'} / ${R.Reducer})。未迁移:原样透传。 */`);
Lines.push(` val ${Prop}: Any? = null,`);
}
}
Lines.push(')');
Lines.push('');
fs.writeFileSync(path.join(KT, 'RootState.kt'), Lines.join('\n'));
const WLines = [];
WLines.push('// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。');
WLines.push('// 证据表:module_10032 根 reducer ' + RootReducerFn.Name + ' 的逐切片委派(字段 / 槽位 / 切片类 / reducer / 动作)。');
WLines.push('package App.State');
WLines.push('');
WLines.push('/** 一个切片在根状态里的委派项。 */');
WLines.push('data class SliceWiring(');
WLines.push(' val Field: String,');
WLines.push(' val Slot: String,');
WLines.push(' val SliceClass: String,');
WLines.push(' val Reducer: String,');
WLines.push(' val Delegate: String,');
WLines.push(' val Actions: List<String>,');
WLines.push(')');
WLines.push('');
WLines.push('object RootWiring {');
WLines.push(' val All: List<SliceWiring> = listOf(');
for (const R of Rows) {
const Acts = R.Actions.map((A) => KStr(A)).join(', ');
const Parts = [
KStr(R.Field),
KStr(R.Slot),
KStr(R.SliceClass || '?'),
KStr(R.Reducer),
KStr(R.Delegate),
'listOf(' + Acts + ')',
];
WLines.push(' SliceWiring(' + Parts.join(', ') + '),');
}
WLines.push(' )');
WLines.push('');
WLines.push(' /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */');
WLines.push(' val Migrated: Set<String> = setOf(' + Object.keys(Migrated).map((F) => KStr(F)).join(', ') + ')');
WLines.push('');
WLines.push(' /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */');
WLines.push(' val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }');
WLines.push('');
WLines.push(' fun ByField(Field: String): SliceWiring? = All.firstOrNull { it.Field == Field }');
WLines.push('}');
WLines.push('');
fs.writeFileSync(path.join(KT, 'RootWiring.kt'), WLines.join('\n'));
console.log(`根状态 ${RootClass}:${Rows.length} 个切片;已迁移 ${Object.keys(Migrated).length} 个,未迁移 ${Rows.length - Object.keys(Migrated).length} 个`);
console.log('未迁移清单:' + Rows.filter((R) => !R.Migrated).map((R) => R.Field).join(', '));