feat(state): 迁密码登录与 Twitch 切片,拼出 89 槽位根 reducer

This commit is contained in:
TermiNexus committed 2026-10-23 00:38:10 +08:00
1 parent b4ecb80b23
commit 0d4f12effb
11 files changed
+2783 -1

No files matched your search

+14
View File
@@ -138,6 +138,20 @@
- 模块:10032(Redux 核心)、18334(DI 装配)、61860(连接与对象模型)、13431(启动)、52943(工具库)、26482(SW 注册)
- **目标**:新工程能完成"启动 → 连接 → 进入大厅"。
- **验收**:L2 的一半(登录 + 大厅)。
- **进度(Kotlin/JS,2026-10)**:
- 根状态 / 委派表由 `3DTank/tools/gen_root_wiring.js` 从当前 `module_10032.js` 现场解析:
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **3/89**:
- `inviteState` → `aD` / `pD.er5`(`InviteReducer.kt`)
- `loginByPassword` → `gD` / `MD.cr6`(`LoginReducer.kt`,含 `SetLogin`/`SetPassword` 把 status 打回 `NONE` 的原语义)
- `twitchState` → `g9` / `y9.gta`(`TwitchReducer.kt`;`SetAccount` 是 Twitch 账号,不是 UID)
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述三片,其余 **86 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 3/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **61 项全绿**,含 89 槽位完整性、三片一次归约互不串片、未迁移字段透传。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE(记为 `(inline)`,动作名暂缺);
`launchParams` 是纯透传(`(pass-through)`,无 reducer)。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+5
View File
@@ -1,5 +1,10 @@
# TOState 字段 ↔ reducer 映射表
> **版本说明(2026-10)**:本文依据旧版 `names/tostate-map.json`(根状态 `w8` / 根 reducer `v8`、
> 槽位 `app_1` / `bpp_1`)写成,与当前 `src/module_10032.js`(根状态 `z6` / `TOState`、根 reducer `k6`、
> 槽位 `<名>_1`)符号名不同,**只用于交叉验证动作归属**。当前源码口径以 `3DTank/tools/gen_root_wiring.js`
> 现场解析出的 `names/root_wiring.json` 与 Kotlin 侧 `App/State/RootWiring.kt` 为准。
本文是 `names/tostate-map.json` 的人类可读版,用途:**重写客户端时对照根状态的每个分片该由哪个 reducer / 哪些 action 驱动**。
## 1. TOState 是什么
File diff suppressed because it is too large. Load diff
+444
View File
@@ -0,0 +1,444 @@
// 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)`,纯 `n.<slot>` 透传记 `(pass-through)`
// · 动作:被调 reducer 顶层 `instanceof <Local>` → action_inventory 里的类名
//
// 用法: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');
// ---------------------------------------------------------------- 源码扫描基础
/** 字符串/注释感知的花括号配对。 */
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;
}
/** reducer 顶层 instanceof 命中的动作名(没有 toString 的类记作 `?<Local>`)。 */
function ActionsOf(Ref) {
const B = BodyOfRef(Ref);
if (!B) return [];
const Masked = MaskNested(T, B.From, B.To);
const Names = new Set();
for (const M of Masked.matchAll(/instanceof ([A-Za-z0-9_$]+)/g)) {
const N = LocalToName.get(M[1]);
Names.add(N || '?' + M[1]);
}
return [...Names].sort();
}
// 槽位 → 委派 { Reducer, Expr, Slots }
const WiringBySlot = new Map();
for (const A of Assignments) {
const Slots = SlotsIn(A.Expr);
if (!Slots.length) continue;
for (const S of Slots) {
if (!WiringBySlot.has(S)) {
WiringBySlot.set(S, { Reducer: ReducerRef(A.Expr), Expr: A.Expr.replace(/\s+/g, ' ').trim(), 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(), 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: W ? ActionsOf(W.Reducer) : [],
Default: Param ? DefaultOfParam.get(Param) || '' : '',
};
});
// 切片类:从 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' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
// 字段名 → 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);
}
// ---------------------------------------------------------------- 输出
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),
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('');
Lines.push('import App.State.Reducers.InviteStateSlice');
Lines.push('import App.State.Reducers.LoginByPasswordStateSlice');
Lines.push('import App.State.Reducers.TwitchStateSlice');
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) {
Lines.push(` /** 原 ${R.Field}(${R.SliceClass || '?'} / ${R.Reducer})。已迁移。 */`);
Lines.push(` val ${Prop}: ${M.Type} = ${M.Type}(),`);
} 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(' /** 已迁移(由 RootReducer 真正归约)的字段。 */');
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(', '));
@@ -0,0 +1,87 @@
// LoginReducer.kt —— 密码登录切片(对应 module_10032 的 gD 状态 + MD.cr6 reducer)
//
// 证据链:
// 状态类 gD:`function gD(t, n, i, r, e, u, s) { t = t === cd ? "" : t; n = n === cd ? "" : n;
// i = i === cd || i; r = r === cd ? OD() : r; e = e !== cd && e; u = u !== cd && u;
// s = s === cd ? 0 : s; … }`
// toString:`LoginByPasswordState(login=…, password=…, rememberMe=…, status=…,
// onlyLogin=…, incorrectLogin2FA=…, requestsNumber2FA=…)`
// 枚举 kD(由 mD() 初始化):NONE / LOGGING_IN / WRONG_PASSWORD / WRONG_CAPTCHA;OD() 返回 NONE
// reducer MD.cr6(module_10032:28051 附近):
// PD → new gD()(整个切片复位)
// RD → copy(status = NONE) // 原包导出为 $_$ff,UI 在 82347 派发
// SetLogin → copy(login, status = NONE) // 注意:改 login 会把 status 打回 NONE
// SetPassword → copy(password, status = NONE)
// SetRememberMe → copy(rememberMe)
// SetStatus → copy(status)
// ND → copy(login = t.dr6_1, onlyLogin = true)
// IncorrectLogin2FA → copy(incorrectLogin2FA)
// RequestsNumber2FA → copy(requestsNumber2FA)
// 其它 → 原状态
//
// 原类的 PD / RD / ND 都没有 toString(不是动作目录里的具名动作),这里按语义命名并在注释里保留原名。
package App.State.Reducers
import App.State.IncorrectLogin2FA
import App.State.RequestsNumber2FA
import App.State.SetLogin
import App.State.SetPassword
import App.State.SetRememberMe
import App.State.SetStatus
/** 原 `kD`(module_10032 的密码登录状态枚举);`SetStatus` 的字段名是 `byPasswordStatus`。 */
enum class ByPasswordStatus {
NONE,
LOGGING_IN,
WRONG_PASSWORD,
WRONG_CAPTCHA,
;
companion object {
/**
* 动作字段在运行期是 JS 枚举对象,toString 就是成员名(`SetStatus(byPasswordStatus=NONE)`)。
* 与原包一致:认不出时落到默认值 NONE。
*/
fun FromText(Text: Any?): ByPasswordStatus =
entries.firstOrNull { it.name == Text?.toString() } ?: NONE
}
}
/** 原 `PD`:无字段、无 toString,reducer 分支里整片复位。 */
class ResetLoginByPassword
/** 原 `RD`:无字段、无 toString;原包以 `$_$ff` 导出,UI 派发它把状态打回 NONE。 */
class ResetLoginByPasswordStatus
/** 原 `ND`:只有一个字段 `dr6_1`;reducer 用它做「只登录」模式(login + onlyLogin=true)。 */
class LoginOnly(val login: String)
/**
* 原 `gD`:`LoginByPasswordState(login, password, rememberMe, status, onlyLogin, incorrectLogin2FA, requestsNumber2FA)`。
* 默认值全部取自原构造函数:`"" / "" / true / NONE / false / false / 0`。
*/
data class LoginByPasswordStateSlice(
val Login: String = "",
val Password: String = "",
val RememberMe: Boolean = true,
val Status: ByPasswordStatus = ByPasswordStatus.NONE,
val OnlyLogin: Boolean = false,
val IncorrectLogin2FA: Boolean = false,
val RequestsNumber2FA: Int = 0,
)
/** 原 `MD.cr6`。 */
fun ReduceLoginByPassword(State0: LoginByPasswordStateSlice, A: Any): LoginByPasswordStateSlice = when (A) {
is ResetLoginByPassword -> LoginByPasswordStateSlice()
// RD:只把 status 复位,其余字段保留
is ResetLoginByPasswordStatus -> State0.copy(Status = ByPasswordStatus.NONE)
// SetLogin / SetPassword 都会顺带把 status 打回 NONE(原包 `pr5(…, OD())`)
is SetLogin -> State0.copy(Login = A.login?.toString() ?: "", Status = ByPasswordStatus.NONE)
is SetPassword -> State0.copy(Password = A.password?.toString() ?: "", Status = ByPasswordStatus.NONE)
is SetRememberMe -> State0.copy(RememberMe = A.rememberMe)
is SetStatus -> State0.copy(Status = ByPasswordStatus.FromText(A.byPasswordStatus))
is LoginOnly -> State0.copy(Login = A.login, OnlyLogin = true)
is IncorrectLogin2FA -> State0.copy(IncorrectLogin2FA = A.incorrectLogin2FA)
is RequestsNumber2FA -> State0.copy(RequestsNumber2FA = A.number)
else -> State0
}
@@ -0,0 +1,36 @@
// TwitchReducer.kt —— Twitch 绑定切片(对应 module_10032 的 g9 状态 + y9.gta reducer)
//
// 证据链:
// 状态类 g9:`function g9(t, n, i) { t = t !== cd && t; n = n === cd ? null : n; i = i === cd ? 0 : i; … }`
// toString:`TwitchState(isTwitchEnabled=…, twitchAccount=…, daysForNextLinking=…)`
// reducer y9.gta(module_10032:41442):
// SetTwitchEnabled → copy(isTwitchEnabled = t.dta_1)
// SetAccount → copy(twitchAccount = t.eta_1) // 这里的 SetAccount 是「Twitch 账号」,不是用户 UID
// SetDaysForNextLinking → copy(daysForNextLinking = t.fta_1)
// 其它 → 原状态
//
// 注意:动作目录里另有一个同名 `SetAccount` 的候选,靠样例 `SetAccount(account=null)` + 本 reducer 的
// `instanceof z9` 才能锁定;`SetAccountRemovalDays` 之类属于别的切片。
package App.State.Reducers
import App.State.SetAccount
import App.State.SetDaysForNextLinking
import App.State.SetTwitchEnabled
/**
* 原 `g9`:`TwitchState(isTwitchEnabled, twitchAccount, daysForNextLinking)`。
* 默认值取自原构造函数:`false / null / 0`。
*/
data class TwitchStateSlice(
val IsTwitchEnabled: Boolean = false,
val TwitchAccount: String? = null,
val DaysForNextLinking: Int = 0,
)
/** 原 `y9.gta`。 */
fun ReduceTwitch(State0: TwitchStateSlice, A: Any): TwitchStateSlice = when (A) {
is SetTwitchEnabled -> State0.copy(IsTwitchEnabled = A.twitchEnabled)
is SetAccount -> State0.copy(TwitchAccount = A.account)
is SetDaysForNextLinking -> State0.copy(DaysForNextLinking = A.days)
else -> State0
}
@@ -0,0 +1,39 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状,但只真正归约已迁移的切片:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
// twitchState → ReduceTwitch(g9 / y9.gta)
//
// 其余 86 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceTwitch
/** 根 reducer:`k6`。 */
object RootReducer {
/** 已迁移(真正归约)的字段名,来自生成物 RootWiring.Migrated。 */
val Migrated: Set<String> get() = RootWiring.Migrated
/** 形如「已迁移 3/89」,给日志与自检用。 */
val Coverage: String get() = "已迁移 ${Migrated.size}/${RootWiring.All.size}"
/**
* 归约一次。未迁移字段原样带过去(`copy` 只改已迁移的三个)。
*/
fun Reduce(State0: RootState, Action: Any): RootState = State0.copy(
InviteState = ReduceInvite(State0.InviteState, Action),
LoginByPassword = ReduceLoginByPassword(State0.LoginByPassword, Action),
TwitchState = ReduceTwitch(State0.TwitchState, Action),
)
}
/** 顶层入口,与切片 reducer 的 `ReduceXxx(State0, Action)` 命名保持一致。 */
fun ReduceRoot(State0: RootState, Action: Any): RootState = RootReducer.Reduce(State0, Action)
@@ -0,0 +1,194 @@
// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。
// 来源:module_10032 的根状态 z6(toString 名 TOState,89 个切片)
// 与根 reducer k6(逐切片归约后 copy 出新状态)。
// 已迁移的切片用强类型;其余保持 Any?(原样透传),迁移一个改一个再重新生成。
package App.State
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.TwitchStateSlice
/**
* 原 `z6` / `TOState`:89 个切片字段,顺序与 toString 一致。
* 未迁移字段默认 null —— 原包构造时会给它们 `new <Slice>()`,这里等对应切片迁移后再补默认值。
*/
data class RootState(
/** 原 user(t8 / M8().qtd)。未迁移:原样透传。 */
val User: Any? = null,
/** 原 screen(WK / dY().ks5)。未迁移:原样透传。 */
val Screen: Any? = null,
/** 原 modalScreen(nV / Fs.js1)。未迁移:原样透传。 */
val ModalScreen: Any? = null,
/** 原 battleStatistics(jR / rr.fqr)。未迁移:原样透传。 */
val BattleStatistics: Any? = null,
/** 原 battleTutorial(rE / Q.sq8)。未迁移:原样透传。 */
val BattleTutorial: Any? = null,
/** 原 garage(UB / hF)。未迁移:原样透传。 */
val Garage: Any? = null,
/** 原 garagePreview(IG / fs.eru)。未迁移:原样透传。 */
val GaragePreview: Any? = null,
/** 原 challenges(CZ / nh.hsj)。未迁移:原样透传。 */
val Challenges: Any? = null,
/** 原 loginByPassword(gD / nu.cr6)。已迁移。 */
val LoginByPassword: LoginByPasswordStateSlice = LoginByPasswordStateSlice(),
/** 原 register(Cit / cp.stg)。未迁移:原样透传。 */
val Register: Any? = null,
/** 原 migrationState($it / Uv.rtg)。未迁移:原样透传。 */
val MigrationState: Any? = null,
/** 原 inviteState(aD / Ke.er5)。已迁移。 */
val InviteState: InviteStateSlice = InviteStateSlice(),
/** 原 captchaState(jP / ar.iqt)。未迁移:原样透传。 */
val CaptchaState: Any? = null,
/** 原 passwordChangeState(VD / uu.pr6)。未迁移:原样透传。 */
val PasswordChangeState: Any? = null,
/** 原 system(_9 / w9().xt9)。未迁移:原样透传。 */
val System: Any? = null,
/** 原 quests(Frt / Yrt.otg)。未迁移:原样透传。 */
val Quests: Any? = null,
/** 原 chatState(LP / $r.wqv)。未迁移:原样透传。 */
val ChatState: Any? = null,
/** 原 clanChatState(qnt / yv.qtg)。未迁移:原样透传。 */
val ClanChatState: Any? = null,
/** 原 clan(jN / we.hr0)。未迁移:原样透传。 */
val Clan: Any? = null,
/** 原 clansStorage(lO / me.vr1)。未迁移:原样透传。 */
val ClansStorage: Any? = null,
/** 原 tipsState(dst / Hw.vth)。未迁移:原样透传。 */
val TipsState: Any? = null,
/** 原 news(ZX / E_.asd)。未迁移:原样透传。 */
val News: Any? = null,
/** 原 settings(Uut / xw.ltg)。未迁移:原样透传。 */
val Settings: Any? = null,
/** 原 accountSettings(oet / Lp.mtg)。未迁移:原样透传。 */
val AccountSettings: Any? = null,
/** 原 lootBoxes(aH / ws.wrv)。未迁移:原样透传。 */
val LootBoxes: Any? = null,
/** 原 notifications(? / B_.ise)。未迁移:原样透传。 */
val Notifications: Any? = null,
/** 原 friends(KD / NL().cr9)。未迁移:原样透传。 */
val Friends: Any? = null,
/** 原 remoteUserDataStorage($Q / vh.isl)。未迁移:原样透传。 */
val RemoteUserDataStorage: Any? = null,
/** 原 battlesSelect(fA / Mi.vqk)。未迁移:原样透传。 */
val BattlesSelect: Any? = null,
/** 原 maps(OA / Oi.sql)。未迁移:原样透传。 */
val Maps: Any? = null,
/** 原 matchmaking(TH / JH().qrx)。未迁移:原样透传。 */
val Matchmaking: Any? = null,
/** 原 battleCreateState($6 / Df.ttg)。未迁移:原样透传。 */
val BattleCreateState: Any? = null,
/** 原 battleSearchState(Itt / Htt().vtg)。未迁移:原样透传。 */
val BattleSearchState: Any? = null,
/** 原 lobbyLoader(KW / Bs.es1)。未迁移:原样透传。 */
val LobbyLoader: Any? = null,
/** 原 battleResult(sR / Fi.aqp)。未迁移:原样透传。 */
val BattleResult: Any? = null,
/** 原 battleUsers(uP / sr.hqs)。未迁移:原样透传。 */
val BattleUsers: Any? = null,
/** 原 suppliesState(wE / $t.fq9)。未迁移:原样透传。 */
val SuppliesState: Any? = null,
/** 原 matchmakingGroup(rW / NW().is0)。未迁移:原样透传。 */
val MatchmakingGroup: Any? = null,
/** 原 shop(j4 / (inline))。未迁移:原样透传。 */
val Shop: Any? = null,
/** 原 battlePauseState(dT / NT().mqc)。未迁移:原样透传。 */
val BattlePauseState: Any? = null,
/** 原 battleViewState(DT / sI.jqd)。未迁移:原样透传。 */
val BattleViewState: Any? = null,
/** 原 rankUpDialogState(LO / Se.br3)。未迁移:原样透传。 */
val RankUpDialogState: Any? = null,
/** 原 clanUserInfo(hO / de.gr1)。未迁移:原样透传。 */
val ClanUserInfo: Any? = null,
/** 原 tankActionsLogState(vI / wi.gqe)。未迁移:原样透传。 */
val TankActionsLogState: Any? = null,
/** 原 battleMessage(FE / vT().rqb)。未迁移:原样透传。 */
val BattleMessage: Any? = null,
/** 原 battleChat(Q6 / Hf.wtg)。未迁移:原样透传。 */
val BattleChat: Any? = null,
/** 原 droppableGold(Vet / Wp.kth)。未迁移:原样透传。 */
val DroppableGold: Any? = null,
/** 原 home(s1 / (inline))。未迁移:原样透传。 */
val Home: Any? = null,
/** 原 localBattleUserState(lE / (inline))。未迁移:原样透传。 */
val LocalBattleUserState: Any? = null,
/** 原 soundSettings(Det / Wet().jth)。未迁移:原样透传。 */
val SoundSettings: Any? = null,
/** 原 gameSettings(c2 / g2().zss)。未迁移:原样透传。 */
val GameSettings: Any? = null,
/** 原 battleSettings(W0 / X0().esq)。未迁移:原样透传。 */
val BattleSettings: Any? = null,
/** 原 graphicsSettings(? / lc.isu)。未迁移:原样透传。 */
val GraphicsSettings: Any? = null,
/** 原 controlSettings(i2 / _2().ess)。未迁移:原样透传。 */
val ControlSettings: Any? = null,
/** 原 securitySettings(V2 / pc.qsv)。未迁移:原样透传。 */
val SecuritySettings: Any? = null,
/** 原 purchaseDialogState(CO / je.ur2)。未迁移:原样透传。 */
val PurchaseDialogState: Any? = null,
/** 原 battleInviteState(att / Ttt().nth)。未迁移:原样透传。 */
val BattleInviteState: Any? = null,
/** 原 keyboardSettings(Sut / bw.oth)。未迁移:原样透传。 */
val KeyboardSettings: Any? = null,
/** 原 touchscreenHudSettings(j7 / N7().xsv)。未迁移:原样透传。 */
val TouchscreenHudSettings: Any? = null,
/** 原 mainQuest(WZ / ih.bsk)。未迁移:原样透传。 */
val MainQuest: Any? = null,
/** 原 language(? / m6().jtg)。未迁移:原样透传。 */
val Language: Any? = null,
/** 原 launchParams(? / (pass-through))。未迁移:原样透传。 */
val LaunchParams: Any? = null,
/** 原 partner(UJ / (inline))。未迁移:原样透传。 */
val Partner: Any? = null,
/** 原 entrance(hD / Bv.pth)。未迁移:原样透传。 */
val Entrance: Any? = null,
/** 原 antiAddiction(UC / W.iq8)。未迁移:原样透传。 */
val AntiAddiction: Any? = null,
/** 原 spectator(HA / Li.jqm)。未迁移:原样透传。 */
val Spectator: Any? = null,
/** 原 moderation(NI / Ei.oqh)。未迁移:原样透传。 */
val Moderation: Any? = null,
/** 原 rulesUpdated(i1 / Ch.dsm)。未迁移:原样透传。 */
val RulesUpdated: Any? = null,
/** 原 announcement(lC / PC().rq5)。未迁移:原样透传。 */
val Announcement: Any? = null,
/** 原 notificationByCountry(gJ / R_.ssd)。未迁移:原样透传。 */
val NotificationByCountry: Any? = null,
/** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */
val UserRenameState: Any? = null,
/** 原 eventGoods(W7 / Wc.esx)。未迁移:原样透传。 */
val EventGoods: Any? = null,
/** 原 creatorTagState(qO / ze.er2)。未迁移:原样透传。 */
val CreatorTagState: Any? = null,
/** 原 twoStepVerificationState(x9 / _f.atb)。未迁移:原样透传。 */
val TwoStepVerificationState: Any? = null,
/** 原 rewardedVideosState(JQ / xh.zsl)。未迁移:原样透传。 */
val RewardedVideosState: Any? = null,
/** 原 twitchState(g9 / uf.gta)。已迁移。 */
val TwitchState: TwitchStateSlice = TwitchStateSlice(),
/** 原 icons(oH / vs.kru)。未迁移:原样透传。 */
val Icons: Any? = null,
/** 原 itemNotifications(_1 / A1().dso)。未迁移:原样透传。 */
val ItemNotifications: Any? = null,
/** 原 contextTutorial(Mst / Jst().tth)。未迁移:原样透传。 */
val ContextTutorial: Any? = null,
/** 原 suppliesTutorial(? / Uw.uth)。未迁移:原样透传。 */
val SuppliesTutorial: Any? = null,
/** 原 questCategories(zZ / th.lsh)。未迁移:原样透传。 */
val QuestCategories: Any? = null,
/** 原 proBattle(Wtt / uv.wth)。未迁移:原样透传。 */
val ProBattle: Any? = null,
/** 原 availabilityByRank(tet / $p.xth)。未迁移:原样透传。 */
val AvailabilityByRank: Any? = null,
/** 原 userReportsState(Zst / qot.zth)。未迁移:原样透传。 */
val UserReportsState: Any? = null,
/** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */
val TankBuilderState: Any? = null,
/** 原 contractState(Ont / xv.bti)。未迁移:原样透传。 */
val ContractState: Any? = null,
/** 原 promoCode(cZ / Q_.ysg)。未迁移:原样透传。 */
val PromoCode: Any? = null,
/** 原 referralEvent(pQ / lh.osk)。未迁移:原样透传。 */
val ReferralEvent: Any? = null,
/** 原 supportUrl(I4 / Yl.ct7)。未迁移:原样透传。 */
val SupportUrl: Any? = null,
)
@@ -0,0 +1,115 @@
// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。
// 证据表:module_10032 根 reducer k6 的逐切片委派(字段 / 槽位 / 切片类 / reducer / 动作)。
package App.State
/** 一个切片在根状态里的委派项。 */
data class SliceWiring(
val Field: String,
val Slot: String,
val SliceClass: String,
val Reducer: String,
val Delegate: String,
val Actions: List<String>,
)
object RootWiring {
val All: List<SliceWiring> = listOf(
SliceWiring("user", "ypu_1", "t8", "M8().qtd", "M8().qtd(t, n.ypu_1)", listOf("?R8", "?p8", "?v8", "AddCoins", "AddRubies", "SetBatteryLevel", "SetClanInfo", "SetCoins", "SetDisabledWithoutPremiumLocations", "SetHasTemporaryCash", "SetPremiumExpirationTime", "SetRankNames", "SetRubies", "SetUserId", "SetUserPropertiesAction", "SpendCoins", "SpendCrystals", "SpendRubies", "UpdateClanTag", "UpdateCrystals", "UpdateEventPass", "UpdateGearScore", "UpdatePremium", "UpdateRank", "UpdateScore", "UpdateUid", "UpdateUserRating")),
SliceWiring("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("?LX", "?RX", "?cY", "?eY", "?fY", "?lY", "?oC", "?oY", "?sC", "?sY", "?uY", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "GoTo", "PressedAnyButton")),
SliceWiring("modalScreen", "apv_1", "nV", "Fs.js1", "Fs.js1(t, n.apv_1)", listOf("?XW", "AddModal", "RemoveCurrentByName", "RemoveModal")),
SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("?DR", "?HR", "?KR", "?PR", "?QR", "?UR", "?XR", "?YR", "?ZR", "?nP", "?tP", "Init", "InitBattleModeWithLevels", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "UpdateFund", "UpdateScore")),
SliceWiring("battleTutorial", "cpv_1", "rE", "Q.sq8", "Q.sq8(t, n.cpv_1)", listOf("?oE", "?sE", "SetFragsCount", "ShowMessage")),
SliceWiring("garage", "dpv_1", "UB", "hF", "hF(t, n)", listOf("?EU", "?FB", "?FX", "?JB", "?MU", "?NU", "?OU", "?PU", "?QU", "?RU", "?SU", "?VB", "?WB", "?aF", "?bU", "?cF", "?dU", "?eF", "?fF", "?iU", "?lF", "?pF", "?rF", "?vF", "?wU", "?yU", "AddOverdriveDescription", "AddRelatedBazooka", "AddRelatedResistanceModule", "AddToFavoriteItem", "BuyProtectionSlot", "Garage", "GarageAddItemToDepot", "GarageApplyResistanceMount", "GarageBuyDevice", "GarageDeviceTrySetInfinityLifetime", "GarageDevicesLoadedAction", "GarageRemoveDevice", "GarageRemoveDiscountForDevice", "GarageRemoveItemFromDepot", "GarageRemoveItemFromStore", "GarageResistanceUnMount", "GarageSelectAlteration", "GarageSelectItem", "GarageSelectShotSkin", "GarageSelectSkin", "GarageUpdateItemCount", "GarageUpgradeItemAction", "GarageUpgradeItemConfirm", "GarageUpgradeItemCoolDownFinished", "GarageUpgradeItemCoolDownStarted", "GoTo", "InitState", "RemoveFromFavoriteItem", "RemoveFromSale", "SetGarageSkinsAction", "SetItemRarity", "SetRestrictionMount", "SwitchingRowsScroll", "UpdateDiscount")),
SliceWiring("garagePreview", "epv_1", "IG", "fs.eru", "fs.eru(t, n.epv_1)", listOf("?BG", "?JG", "?KG", "?OG", "?QG", "?UG", "?VG", "?XG", "?YG", "?ZG", "?nH", "?tH", "AddRequestedItem", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", "SetMountedItemsProperties", "SetPaint", "ShowRequest", "UpdatePreviewSettings")),
SliceWiring("challenges", "fpv_1", "CZ", "nh.hsj", "nh.hsj(t, n.fpv_1)", listOf("?LZ", "RemoveChallenge", "SetBattlePass", "SetBattlePassPackages", "SetChallenge", "SetRewardForDialog", "SetTakenBattlePassRewardIndex", "SetTakenFreeRewardIndex", "UpdateStars")),
SliceWiring("loginByPassword", "gpv_1", "gD", "nu.cr6", "nu.cr6(t, n.gpv_1)", listOf("?ND", "?PD", "?RD", "IncorrectLogin2FA", "RequestsNumber2FA", "SetLogin", "SetPassword", "SetRememberMe", "SetStatus")),
SliceWiring("register", "hpv_1", "Cit", "cp.stg", "cp.stg(t, n.hpv_1)", listOf("?Bit", "?Dit", "?Fit", "?Git", "?Hit", "?Lit", "?Mit", "?Uit", "?_rt", "?hrt", "EnteredUidIsBusy", "ForceSetUid", "ReceiveOAuthUri", "SetOpenedPassword", "SetPassword", "SetPasswordRestrictions", "SetUid", "SocialNetworkAvailability", "SocialNetworkClientIds")),
SliceWiring("migrationState", "ipv_1", "\$it", "Uv.rtg", "Uv.rtg(t, n.ipv_1)", listOf("?Bit", "?Dit", "?Fit", "?Git", "?Hit", "?Lit", "?_rt", "EnteredUidIsBusy", "ForceSetUid", "SetPassword", "SetUid")),
SliceWiring("inviteState", "jpv_1", "aD", "Ke.er5", "Ke.er5(t, n.jpv_1)", listOf("ApplyInvite", "SetInviteNeed", "SetInviteStatus")),
SliceWiring("captchaState", "kpv_1", "jP", "ar.iqt", "ar.iqt(t, n.kpv_1)", listOf("?AP", "?IP", "?PP", "?RP", "?SP", "LocationsUpdated", "SetLocations")),
SliceWiring("passwordChangeState", "lpv_1", "VD", "uu.pr6", "uu.pr6(t, n.lpv_1)", listOf("SetPasswordChangeHash", "ShowEmailAndPasswordChangeDialog")),
SliceWiring("system", "mpv_1", "_9", "w9().xt9", "w9().xt9(t, n.mpv_1)", listOf("DoShowMessage", "SetFatalError")),
SliceWiring("quests", "npv_1", "Frt", "Yrt.otg", "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", listOf("?Hrt", "GoTo", "QuestCategory", "SetCurrentCategory")),
SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("?QP", "?XP", "?_C", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")),
SliceWiring("clanChatState", "ppv_1", "qnt", "yv.qtg", "yv.qtg(t, n.ppv_1)", listOf("?\$nt", "?Tnt", "?xnt", "Configure", "DoAddMessages")),
SliceWiring("clan", "qpv_1", "jN", "we.hr0", "we.hr0(t, n.qpv_1)", listOf("?FM", "?IM", "?KM", "?PM", "?RM", "?VM", "?YM", "?iM", "AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", "AddNewIncomingClan", "AddOutgoingClan", "AddUsersToAccepted", "AddUsersToNewAccepted", "ClanBanned", "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", "NewIncomingUserNotification", "OutgoingAdded", "OutgoingRemoved", "RemoveIncomingClan", "RemoveNewIncoming", "RemoveNewIncomingClan", "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", "SetClanEditPrice", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", "UpdateTimeToJoinRestriction", "UpdateValidation")),
SliceWiring("clansStorage", "rpv_1", "lO", "me.vr1", "me.vr1(t, n.rpv_1)", listOf("?mO", "AddUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", "ClanLoaded", "ClanUnloaded", "ForeignClanLoaded", "MinimalRankChanged", "RecruitmentChanged", "RemoveUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan")),
SliceWiring("tipsState", "spv_1", "dst", "Hw.vth", "Hw.vth(t, n.spv_1)", listOf("UpdateTips")),
SliceWiring("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("?hJ", "AddNewsItemAction", "RemoveNewsItemAction", "SetNewsItemAction", "SetUnreadNewsCounter")),
SliceWiring("settings", "upv_1", "Uut", "xw.ltg", "xw.ltg(t, n.upv_1)", listOf("?Fut", "?Gut")),
SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("?aet", "?cet", "?det", "?fet", "?get", "?ket", "?met", "?pet", "?vet", "?wet", "?xet", "ClearInputUid", "SetAccountRemovalDays")),
SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("?CH", "?SH", "?gH", "?kH", "?mH", "?zH", "AddKeys", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsOpening", "SetIsShopVisited", "SetKeys", "SetPossibleRewards", "SetRewards", "ShownRewards")),
SliceWiring("notifications", "xpv_1", "?", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("?PJ", "?RJ", "AddNotification", "InitNotificationSound")),
SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("?_L", "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetLimits", "SetValidationStatus")),
SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")),
SliceWiring("battlesSelect", "apw_1", "fA", "Mi.vqk", "Mi.vqk(t, n.apw_1)", listOf("?dA", "?gA", "AddBattle", "AddUser", "AddUsers", "ChangeName", "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", "RemoveBattle", "RemoveUser", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", "SwapTeam", "UpdateFund", "UpdateSuspicion", "UpdateTeamScore", "UpdateUserScore", "UpdateUserSuspicion")),
SliceWiring("maps", "bpw_1", "OA", "Oi.sql", "Oi.sql(t, n.bpw_1)", listOf("AddMap")),
SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("?WH", "?nW", "?tW", "Configure", "EnterToBattleApply", "Play", "SelectMatchmakingMode", "SetRankIndexToMinGS", "WasRegistered")),
SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")),
SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("?Dtt", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")),
SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("?FW", "?GW")),
SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("?cR", "?fR", "?lR", "DoInit", "SetHasBattleResult", "SetRewards")),
SliceWiring("battleUsers", "hpw_1", "uP", "sr.hqs", "sr.hqs(t, n.hpw_1)", listOf("?_P", "?hP", "?lP", "?oP", "?qP", "?sP", "InitSuspiciousState", "InitUserLevels", "RemoveTankInfo", "SetGearScore", "SetTankInfo", "SetTankResistance", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")),
SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("?qE")),
SliceWiring("matchmakingGroup", "jpw_1", "rW", "NW().is0", "NW().is0(t, n.jpw_1)", listOf("?zW", "AcceptedInviteToGroup", "AddUser", "AddUserToSelected", "InitMatchmakingGroup", "Invite", "RejectedInviteToGroup", "RemoveUserToSelected", "SetAllowedFromFriends", "SetSelectedUsers", "UpdateMountItems", "UpdateUserReadiness", "UserRemoved")),
SliceWiring("shop", "kpw_1", "j4", "(inline)", "(function (t, n) { var i; if (t instanceof w4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, t.lt6_1); else if (t instanceof Yut) i = n.qt6(cd, cd, cd, cd, !0); else if (t instanceof F5) i = new j4(cd, cd, cd, cd, cd, cd, cd, n.qq3_1); else if (t instanceof h4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.st5_1); else if (t instanceof c4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ut5_1); else if (t instanceof u4) i = n.qt6( cd, cd, cd, cd, cd, cd,", listOf()),
SliceWiring("battlePauseState", "lpw_1", "dT", "NT().mqc", "NT().mqc(t, n.lpw_1)", listOf("?TT", "?xT", "OnPauseMode", "SetIdleKickPeriod")),
SliceWiring("battleViewState", "mpw_1", "DT", "sI.jqd", "(null == \$n && new sI(), \$n).jqd(t, n.mpw_1)", listOf("?JT", "?QT", "?WT", "?YT", "?ZT", "?eI", "?iI", "?nE", "?nI", "?rI", "?tE", "?tI", "SetElementsVisibility", "SetVisibility")),
SliceWiring("rankUpDialogState", "npw_1", "LO", "Se.br3", "Se.br3(t, n.npw_1)", listOf("?FO", "ShowAvailableItems")),
SliceWiring("clanUserInfo", "opw_1", "hO", "de.gr1", "de.gr1(t, n.opw_1)", listOf("Loaded")),
SliceWiring("tankActionsLogState", "ppw_1", "vI", "wi.gqe", "wi.gqe(t, n.ppw_1)", listOf("?dI", "OnUserJoinTheBattle", "OnUserLeaveTheBattle", "UpdateLog")),
SliceWiring("battleMessage", "qpw_1", "FE", "vT().rqb", "vT().rqb(t, n.qpw_1)", listOf("?sT", "DoAddLocalizedMessage", "DoAddLocalizedTeamMessage", "DoAddLocalizedUserMessage", "DoAddTextMessage", "HideMessage", "RemoveMessage")),
SliceWiring("battleChat", "rpw_1", "Q6", "Hf.wtg", "Hf.wtg(t, n.rpw_1)", listOf("?ett", "?rtt", "?stt", "?utt", "AddMessage", "ChatLoaded")),
SliceWiring("droppableGold", "spw_1", "Vet", "Wp.kth", "Wp.kth(t, n.spw_1)", listOf("?Ket", "?Yet")),
SliceWiring("home", "tpw_1", "s1", "(inline)", "(function (t, n) { return t instanceof o1 ? n.f7j(!0) : n; })(t, n.tpw_1)", listOf()),
SliceWiring("localBattleUserState", "upw_1", "lE", "(inline)", "(function (t, n) { return t instanceof fE ? n.xq8(t.yq8_1) : t instanceof vE ? n.xq8(cd, t.zq8_1) : n; })(t, n.upw_1)", listOf()),
SliceWiring("soundSettings", "vpw_1", "Det", "Wet().jth", "Wet().jth(t, n.vpw_1)", listOf("?Uet", "?_C", "SetEntranceSoundEnabled", "UpdateSettings")),
SliceWiring("gameSettings", "wpw_1", "c2", "g2().zss", "g2().zss(t, n.wpw_1)", listOf("?_C", "UpdateSettings")),
SliceWiring("battleSettings", "xpw_1", "W0", "X0().esq", "X0().esq(t, n.xpw_1)", listOf("?_C", "UpdateSettings")),
SliceWiring("graphicsSettings", "ypw_1", "?", "lc.isu", "lc.isu(t, n.ypw_1)", listOf("UpdateSettings")),
SliceWiring("controlSettings", "zpw_1", "i2", "_2().ess", "_2().ess(t, n.zpw_1)", listOf("?_C", "UpdateSettings")),
SliceWiring("securitySettings", "apx_1", "V2", "pc.qsv", "pc.qsv(t, n.apx_1)", listOf("SetProfileBlockStatus")),
SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("?NO", "ChangeEmailStatus", "SuccessfullyPurchase")),
SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("?dtt", "EnableSetBattleInviteNotifications", "InvitationResponse", "InviteToBattle", "RemoveUserFromInvited", "SelectUserToInvite")),
SliceWiring("keyboardSettings", "dpx_1", "Sut", "bw.oth", "bw.oth(t, n.dpx_1)", listOf("?Iut", "?_C", "ChangeAction", "RemoveAction", "ResetAction")),
SliceWiring("touchscreenHudSettings", "epx_1", "j7", "N7().xsv", "N7().xsv(t, n.epx_1)", listOf("?E7", "?_C", "UpdateSettings")),
SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("?VZ", "AddRewardedQuest", "QuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")),
SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("?d6")),
SliceWiring("launchParams", "hpx_1", "?", "(pass-through)", "n.hpx_1", listOf()),
SliceWiring("partner", "ipx_1", "UJ", "(inline)", "(function (t, n) { var i; if (t instanceof VJ) i = n.osf(t.rsf_1, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ssf_1); else if (t instanceof nZ) { var r = t.bsg_1, e = t.csg_1, u = t.dsg_1, s = t.esg_1, o = t.fsg_1, _ = t.gsg_1; i = n.osf(cd, cd, cd, r, e, u, cd, cd, o, _, s); } else i = t instanceof iZ ? n.osf(cd, t.hsg_1) : t instanceof uZ ? n.osf(cd, cd, t.ksg_1) : t instanceof eZ ? n.osf(cd, cd, cd, cd, cd, cd, cd, t.jsg_1) : t instanceof _Z ? n.osf(cd, cd, cd, cd, cd, cd, t.osg_1) : t instanceof X", listOf()),
SliceWiring("entrance", "jpx_1", "hD", "Bv.pth", "Bv.pth(t, n.jpx_1)", listOf("?eit", "?fit", "?lit", "?oit", "?sit", "?uit", "SetAgeWarnShow")),
SliceWiring("antiAddiction", "kpx_1", "UC", "W.iq8", "W.iq8(t, n.kpx_1)", listOf("?FC", "?XC", "?YC", "SetRealNameAndIdNumber", "SetValidateAntiAddictionUserDataStatus")),
SliceWiring("spectator", "lpx_1", "HA", "Li.jqm", "Li.jqm(t, n.lpx_1)", listOf("?JA", "?XA", "?YA", "?ZA", "SetSpectator", "UpdateHudLayout")),
SliceWiring("moderation", "mpx_1", "NI", "Ei.oqh", "Ei.oqh(t, n.mpx_1)", listOf("?FI", "?GI", "?JI", "?KI", "?QI", "?UI", "?WI", "?XI", "?YI", "?ZI", "?nA", "?tA", "?uA")),
SliceWiring("rulesUpdated", "npx_1", "i1", "Ch.dsm", "Ch.dsm(t, n.npx_1)", listOf("UpdateRules")),
SliceWiring("announcement", "opx_1", "lC", "PC().rq5", "PC().rq5(t, n.opx_1)", listOf("?bC", "?qC", "Init", "SetSkipNextPromotedAnnounce")),
SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("?qJ", "Init")),
SliceWiring("userRenameState", "qpx_1", "O8", "Rf.pte", "Rf.pte(t, n.qpx_1)", listOf("?B8", "?F8", "?G8", "?Q8", "?U8", "ChangeUidInputState", "ResponseActual", "ResponseDelivered", "SetChangeUidParams", "SetRubiesForRename", "SetValidationStatus")),
SliceWiring("eventGoods", "rpx_1", "W7", "Wc.esx", "Wc.esx(t, n.rpx_1)", listOf("InitEventGoods")),
SliceWiring("creatorTagState", "spx_1", "qO", "ze.er2", "ze.er2(t, n.spx_1)", listOf("?jO", "SetAttachedCreatorTag", "SetCreatorTag", "SetCreatorTags")),
SliceWiring("twoStepVerificationState", "tpx_1", "x9", "_f.atb", "_f.atb(t, n.tpx_1)", listOf("ActivationStatus2FA", "Enable2FaRequest", "SetSecretAndQRCode", "SetUse2FA", "VerificationStatus")),
SliceWiring("rewardedVideosState", "upx_1", "JQ", "xh.zsl", "xh.zsl(t, n.upx_1)", listOf("SetDisplayState")),
SliceWiring("twitchState", "vpx_1", "g9", "uf.gta", "uf.gta(t, n.vpx_1)", listOf("SetAccount", "SetDaysForNextLinking", "SetTwitchEnabled")),
SliceWiring("icons", "wpx_1", "oH", "vs.kru", "vs.kru(t, n.wpx_1)", listOf("SetIcons")),
SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("?S1", "?_C", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", "UpdateItemNotification")),
SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")),
SliceWiring("suppliesTutorial", "zpx_1", "?", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")),
SliceWiring("questCategories", "apy_1", "zZ", "th.lsh", "th.lsh(t, n.apy_1)", listOf("RemoveQuestCategory", "RequestQuestCategoriesStatistic", "SetQuestCategories", "SetQuestCategoriesStatistic")),
SliceWiring("proBattle", "bpy_1", "Wtt", "uv.wth", "uv.wth(t, n.bpy_1)", listOf("Fight", "SetPassword")),
SliceWiring("availabilityByRank", "cpy_1", "tet", "\$p.xth", "\$p.xth(t, n.cpy_1)", listOf("SetLoadedData")),
SliceWiring("userReportsState", "dpy_1", "Zst", "qot.zth", "(null == sd && new qot(), sd).zth(t, n.dpy_1)", listOf("InitUserReports", "ReportBattleChatMessage", "ReportLobbyChatMessage", "ReportProBattleName", "SetAvailableGameReports", "SetAvailableReports")),
SliceWiring("tankBuilderState", "epy_1", "brt", "wp.ati", "wp.ati(t, n.epy_1)", listOf("?Trt", "SetKode", "SetParts", "SetPartsInfo")),
SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "Init", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")),
SliceWiring("promoCode", "gpy_1", "cZ", "Q_.ysg", "Q_.ysg(t, n.gpy_1)", listOf("ActivatePromoCode", "SetCodeValidateStatus")),
SliceWiring("referralEvent", "hpy_1", "pQ", "lh.osk", "lh.osk(t, n.hpy_1)", listOf("SetPromoCode", "SetReferralEventData", "SetReferrals", "SetReferrer")),
SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("?R4", "SetUrl")),
)
/** 已迁移(由 RootReducer 真正归约)的字段。 */
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
fun ByField(Field: String): SliceWiring? = All.firstOrNull { it.Field == Field }
}
@@ -6,6 +6,10 @@
// ③ 网关:按动作类的监听器在状态更新之前触发
// ④ 网关:模型订阅可退订;重复订阅直接失败
// ⑤ 网关:调度器负责把派发排进引擎循环(这里用同步调度器验证)
// ⑥ 动作目录:816 个类与 138 个真实派发名对齐
// ⑦ 回放:真实 toString 样例的类/字段/类型校验
// ⑧ 全目录动作类都能按 KClass 路由
// ⑨~⑫ 切片 reducer:InviteState / LoginByPasswordState / TwitchState;⑫ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
package App
@@ -17,11 +21,32 @@ import State.StoreListener
import App.State.ActionCatalog
import App.State.ActionSamples
import App.State.ApplyInvite
import App.State.Reducers.ByPasswordStatus
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.InviteStatus
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.LoginOnly
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceTwitch
import App.State.Reducers.ResetLoginByPassword
import App.State.Reducers.ResetLoginByPasswordStatus
import App.State.Reducers.TwitchStateSlice
import App.State.ReduceRoot
import App.State.RootReducer
import App.State.RootState
import App.State.RootWiring
import App.State.IncorrectLogin2FA
import App.State.RequestsNumber2FA
import App.State.SetAccount
import App.State.SetDaysForNextLinking
import App.State.SetInviteNeed
import App.State.SetInviteStatus
import App.State.SetLogin
import App.State.SetPassword
import App.State.SetRememberMe
import App.State.SetStatus
import App.State.SetTwitchEnabled
private data class AddFrags(val fragsCount: Int)
private data class SetScreen(val screen: String)
@@ -259,6 +284,188 @@ object StateCheck {
)
}
// ⑩ 切片 reducer:LoginByPasswordState(原 module_10032 的 gD / MD.cr6,枚举 kD)
run {
val S0 = LoginByPasswordStateSlice()
Check(
"切片 Login:默认值与原构造一致",
S0.Login == "" && S0.Password == "" && S0.RememberMe && S0.Status == ByPasswordStatus.NONE &&
!S0.OnlyLogin && !S0.IncorrectLogin2FA && S0.RequestsNumber2FA == 0
)
// 原包 SetLogin 分支是 `pr5(t.qr5_1, cd, cd, OD())`:写 login 的同时把 status 打回 NONE
val S1 = ReduceLoginByPassword(
S0.copy(Status = ByPasswordStatus.WRONG_PASSWORD),
SetLogin("pilot"),
)
Check("切片 Login:SetLogin 写 login 并把 status 复位为 NONE", S1.Login == "pilot" && S1.Status == ByPasswordStatus.NONE)
val S2 = ReduceLoginByPassword(S1.copy(Status = ByPasswordStatus.LOGGING_IN), SetPassword("secret"))
Check(
"切片 Login:SetPassword 写 password 并把 status 复位为 NONE",
S2.Password == "secret" && S2.Status == ByPasswordStatus.NONE && S2.Login == "pilot"
)
val S3 = ReduceLoginByPassword(S2.copy(Status = ByPasswordStatus.WRONG_CAPTCHA), SetRememberMe(false))
Check("切片 Login:SetRememberMe 只改 rememberMe", !S3.RememberMe && S3.Status == ByPasswordStatus.WRONG_CAPTCHA)
val S4 = ReduceLoginByPassword(S3, SetStatus("WRONG_PASSWORD"))
Check("切片 Login:SetStatus 按枚举名解析", S4.Status == ByPasswordStatus.WRONG_PASSWORD)
Check(
"切片 Login:SetStatus 认不出时落到 NONE",
ReduceLoginByPassword(S4, SetStatus("SOMETHING_ELSE")).Status == ByPasswordStatus.NONE
)
val S5 = ReduceLoginByPassword(S4, LoginOnly("uid-only"))
Check(
"切片 Login:LoginOnly 写 login/onlyLogin 且保留 status",
S5.Login == "uid-only" && S5.OnlyLogin && S5.Status == ByPasswordStatus.WRONG_PASSWORD
)
val S6 = ReduceLoginByPassword(S5, IncorrectLogin2FA(true))
val S7 = ReduceLoginByPassword(S6, RequestsNumber2FA(3))
Check(
"切片 Login:IncorrectLogin2FA / RequestsNumber2FA 各自只改本字段",
S7.IncorrectLogin2FA && S7.RequestsNumber2FA == 3 && S7.Login == "uid-only"
)
val S8 = ReduceLoginByPassword(S7, ResetLoginByPasswordStatus())
Check(
"切片 Login:ResetLoginByPasswordStatus 只清 status",
S8.Status == ByPasswordStatus.NONE && S8.Login == "uid-only" && S8.RequestsNumber2FA == 3
)
val S9 = ReduceLoginByPassword(S8, ResetLoginByPassword())
Check("切片 Login:ResetLoginByPassword 整片复位", S9 == LoginByPasswordStateSlice())
val SA = ReduceLoginByPassword(S4, AddFrags(1))
Check("切片 Login:无关动作不改状态", SA == S4)
val Store = ReduxStore(S0, { S, A -> ReduceLoginByPassword(S, A) })
var Notified = false
Store.Subscribe(StoreListener { Notified = true })
Store.Dispatch(SetPassword("p"))
Store.Dispatch(SetStatus("LOGGING_IN"))
Check(
"切片 Login:接 Store 派发后状态已归约",
Store.State.Password == "p" && Store.State.Status == ByPasswordStatus.LOGGING_IN && Notified
)
}
// ⑪ 切片 reducer:TwitchState(原 module_10032 的 g9 / y9.gta)
run {
val S0 = TwitchStateSlice()
Check(
"切片 Twitch:默认值与原构造一致",
!S0.IsTwitchEnabled && S0.TwitchAccount == null && S0.DaysForNextLinking == 0
)
val S1 = ReduceTwitch(S0, SetTwitchEnabled(true))
Check("切片 Twitch:SetTwitchEnabled 只改开关", S1.IsTwitchEnabled && S1.TwitchAccount == null)
// 样例 SetAccount(account=null) 就落在这一片:SetAccount 写的是 twitchAccount
val S2 = ReduceTwitch(S1, SetAccount("tank_tv"))
Check("切片 Twitch:SetAccount 写 twitchAccount", S2.TwitchAccount == "tank_tv" && S2.IsTwitchEnabled)
Check("切片 Twitch:SetAccount(null) 清空账号", ReduceTwitch(S2, SetAccount(null)).TwitchAccount == null)
val S3 = ReduceTwitch(S2, SetDaysForNextLinking(7))
Check("切片 Twitch:SetDaysForNextLinking 只改天数", S3.DaysForNextLinking == 7 && S3.TwitchAccount == "tank_tv")
Check("切片 Twitch:无关动作不改状态", ReduceTwitch(S3, AddFrags(1)) == S3)
val Store = ReduxStore(S0, { S, A -> ReduceTwitch(S, A) })
Store.Dispatch(SetDaysForNextLinking(2))
Check("切片 Twitch:接 Store 派发后状态已归约", Store.State.DaysForNextLinking == 2)
}
// ⑫ 根 reducer:89 个槽位的委派表 + 三个已迁移切片一次归约
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
"根:字段名与槽位都唯一",
RootWiring.All.map { it.Field }.toSet().size == RootWiring.All.size &&
RootWiring.All.map { it.Slot }.toSet().size == RootWiring.All.size
)
Check(
"根:每个槽位都有 reducer 证据",
RootWiring.All.all { it.Reducer.isNotBlank() && it.Reducer != "(未识别)" }
)
Check(
"根:只有 4 个内联 IIFE + 1 个纯透传未具名",
RootWiring.All.count { it.Reducer == "(inline)" } == 4 &&
RootWiring.All.count { it.Reducer == "(pass-through)" } == 1
)
// 已迁移三片的委派与原包一致:nu.cr6 / Ke.er5 / uf.gta
val LoginW = RootWiring.ByField("loginByPassword")
val InviteW = RootWiring.ByField("inviteState")
val TwitchW = RootWiring.ByField("twitchState")
Check(
"根:loginByPassword 委派到 gD / nu.cr6",
LoginW != null && LoginW.SliceClass == "gD" && LoginW.Reducer == "nu.cr6"
)
Check(
"根:inviteState 委派到 aD / Ke.er5",
InviteW != null && InviteW.SliceClass == "aD" && InviteW.Reducer == "Ke.er5"
)
Check(
"根:twitchState 委派到 g9 / uf.gta",
TwitchW != null && TwitchW.SliceClass == "g9" && TwitchW.Reducer == "uf.gta"
)
Check(
"根:迁移表 = 三个已迁移字段,其余 86 个待迁",
RootReducer.Migrated == setOf("inviteState", "loginByPassword", "twitchState") &&
RootWiring.Unmigrated.size == 86
)
// 一次 ReduceRoot 里三个切片各归约自己的动作,互不串片
val S0 = RootState(User = "user-1", Screen = "Preloader")
val S1 = ReduceRoot(S0, ApplyInvite("uid-1"))
Check(
"根:ApplyInvite 只动 inviteState",
S1.InviteState.Invite == "uid-1" &&
S1.LoginByPassword == S0.LoginByPassword &&
S1.TwitchState == S0.TwitchState
)
val S2 = ReduceRoot(S1, SetLogin("pilot"))
Check(
"根:SetLogin 只动 loginByPassword",
S2.LoginByPassword.Login == "pilot" &&
S2.InviteState == S1.InviteState &&
S2.TwitchState == S1.TwitchState
)
val S3 = ReduceRoot(S2, SetAccount("tank_tv"))
Check(
"根:SetAccount 只动 twitchState",
S3.TwitchState.TwitchAccount == "tank_tv" &&
S3.InviteState == S2.InviteState &&
S3.LoginByPassword == S2.LoginByPassword
)
Check(
"根:三次归约后三个切片各自保留结果",
S3.InviteState.Invite == "uid-1" &&
S3.LoginByPassword.Login == "pilot" &&
S3.TwitchState.TwitchAccount == "tank_tv"
)
// 未迁移字段原样透传;无关动作不改变任何字段
Check(
"根:未迁移字段原样透传",
S3.User == "user-1" && S3.Screen == "Preloader"
)
Check("根:无关动作返回等值状态", ReduceRoot(S3, AddFrags(1)) == S3)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 3/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
Store.Dispatch(SetTwitchEnabled(true))
Check(
"根:接 Store 派发后两片同时归约",
Store.State.LoginByPassword.Status == ByPasswordStatus.LOGGING_IN && Store.State.TwitchState.IsTwitchEnabled
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)
+1 -1
View File
@@ -1,4 +1,4 @@
# RunStateTest.ps1 —— 跑状态层内核自检(Store / 中间件 / 网关)
# RunStateTest.ps1 —— 跑状态层内核自检(Store / 中间件 / 网关)
#
# 覆盖:归约、监听器通知、中间件顺序、动作监听器时序、模型订阅退订、调度器。
# 用法:powershell -ExecutionPolicy Bypass -File .\tools\RunStateTest.ps1