1050 lines
48 KiB
JavaScript
1050 lines
48 KiB
JavaScript
// 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(', '));
|