feat(state): 全量迁移商店切片(69 个动作 + U7/w3 子状态)

This commit is contained in:
TermiNexus committed 2026-10-23 02:13:31 +08:00
1 parent 86d79eddad
commit 94374d2f36
10 files changed
+2929 -37

No files matched your search

+10 -5
View File
@@ -143,21 +143,26 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **5/89**:
- 已迁移切片 **6/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)
- `home` → `s1` / 根 reducer `(inline@home)` 分支(`HomeReducer.kt`;`ShopDiscountNotification`)
- `localBattleUserState` → `lE` / `(inline@localBattleUserState)` 分支(`LocalBattleUserReducer.kt`)
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述五片,其余 **84 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 5/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **78 项全绿**,含 89 槽位完整性、五片一次归约互不串片、未迁移字段透传。
- `shop` → `j4` / `(inline@shop)` 分支(`ShopReducer.kt`):顶层 30 字段 32 分支;
嵌套 `U7`(`Categories`,7 字段)5 臂、`w3`(`ShopItems`,23 字段)32 臂,共 **69 个 instanceof 动作** 全量翻译。
无 `toString` 的 `Yut/F5/g4/X5/w5/o4/m4/k4/i4/t4/f5/v5/p5/H7/s5/o5/_5` 按效果定名
(`ShopLoaded`/`ResetShop`/…/`AddCategoriesWithBonus`/…),原名保留在 `ShopReducer.kt` 注释里。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述六片,其余 **83 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 6/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **113 项全绿**,含 89 槽位完整性、六片一次归约互不串片、未迁移字段透传。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
`launchParams` 是纯透传(`(pass-through)`,无 reducer)。
- 无 `toString` 的默认动作按证据定名(`root_wiring.json` 的 `AnonymousActions`):
`o1` → `ShowShopDiscountNotification`(`$_$.gl` 单例,由 module_18334 的 `Qx.qge` 派发;
效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示)。
效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示);
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等)。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+136 -21
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-22T17:21:56.934Z",
"generatedAt": "2026-10-22T18:11:10.717Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -11,7 +11,8 @@
"loginByPassword",
"twitchState",
"home",
"localBattleUserState"
"localBattleUserState",
"shop"
],
"AnonymousActions": [
{
@@ -20,6 +21,120 @@
"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))"
},
{
"Local": "Yut",
"Name": "ShopLoaded",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isLoaded = true(t instanceof Yut → n.qt6(…, !0))"
},
{
"Local": "F5",
"Name": "ResetShop",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "new j4(…) 整片复位,只保留 coinPaymentProviderUrl(第 7 个构造参数 n.qq3_1)"
},
{
"Local": "g4",
"Name": "PaymentUrlReceiveFailed",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "paymentUrl = null、receivePaymentUrlFailed = true"
},
{
"Local": "X5",
"Name": "ResetPaymentState",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "paymentUrl = null、receivePaymentUrlFailed = false、mobilePaymentState = NONE、paymentProviderState = NONE"
},
{
"Local": "w5",
"Name": "SetGoToHomeAfterBuy",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "goToHomeAfterBuy = true"
},
{
"Local": "o4",
"Name": "PaymentProviderInProgress",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "paymentProviderState = IN_PROGRESS(V5().Rl)"
},
{
"Local": "m4",
"Name": "PaymentProviderFinishSuccess",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "paymentProviderState = FINISH_SUCCESS(y4())"
},
{
"Local": "k4",
"Name": "PaymentProviderFinishFail",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "paymentProviderState = FINISH_FAIL(V5().Nl)"
},
{
"Local": "i4",
"Name": "BlockBackButton",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isBlockedBackButton = true"
},
{
"Local": "t4",
"Name": "UnblockBackButton",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isBlockedBackButton = false"
},
{
"Local": "f5",
"Name": "ClearPreviousScreen",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "previousScreen = null"
},
{
"Local": "v5",
"Name": "ClearTargetCategory",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "targetCategory = null"
},
{
"Local": "p5",
"Name": "ClearTargetProductId",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "targetProductId = null"
},
{
"Local": "H7",
"Name": "AddCategoriesWithBonus",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "U7.categoriesWithBonus = Zg(现有集合, t.zsw_1)"
},
{
"Local": "s5",
"Name": "InitPaymentModeAsShopItem",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "U7/w3.paymentModeAsShopItem[ft3_1] = gt3_1"
},
{
"Local": "o5",
"Name": "SetupDiscountItemForItem",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "U7/w3.discountItems[dt3_1] = et3_1"
},
{
"Local": "_5",
"Name": "RemoveDiscountItemForItem",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "U7/w3.discountItems 删除键 ct3_1"
}
],
"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 快路径,本身无字段、无独立效果。"
}
],
"Rows": [
@@ -913,26 +1028,14 @@
"Reducer": "(inline@shop)",
"Delegate": "(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, …",
"Actions": [
"?F5",
"?H7",
"?X5",
"?Yut",
"?_5",
"?f5",
"?g4",
"?i4",
"?k4",
"?m4",
"?o4",
"?o5",
"?p5",
"?s5",
"?t4",
"?v5",
"?w5",
"?x4",
"AddCategoriesWithBonus",
"AddItemToOrderProcessing",
"BlockBackButton",
"BraintreeLoadingInterrupt",
"ClearPreviousScreen",
"ClearTargetCategory",
"ClearTargetProductId",
"CurrentPayMode",
"DeleteOneTimePurchaseItem",
"InitAdditionalDescription",
@@ -950,6 +1053,7 @@
"InitLootBoxAndPaint",
"InitOneTimePurchaseItem",
"InitPayModeAction",
"InitPaymentModeAsShopItem",
"InitPaymentProviderItems",
"InitPaymentViewType",
"InitRestrictionByPayMode",
@@ -962,8 +1066,15 @@
"InitSingleItemKit",
"InitSpecialKitPackage",
"MobilePaymentStateChanged",
"PaymentProviderFinishFail",
"PaymentProviderFinishSuccess",
"PaymentProviderInProgress",
"PaymentUrlReceiveFailed",
"PaymentUrlReceived",
"RemoveDiscountItemForItem",
"RemoveItemFromOrderProcessing",
"ResetPaymentState",
"ResetShop",
"SavePreviousScreen",
"SaveTargetProductId",
"SaveTargetVisitedCategory",
@@ -972,19 +1083,23 @@
"SelectItem",
"SetBraintreeToken",
"SetCoinPaymentProviderUrl",
"SetGoToHomeAfterBuy",
"SetInitShopItemId",
"SetPurchaseCompleted",
"SetStripeIntentParams",
"SetupDiscountCategoryByItem",
"SetupDiscountItem",
"SetupDiscountItemForItem",
"ShopCardExpired",
"ShopCardRemoved",
"ShopCardSold",
"ShopLoaded",
"ToggleShopActivity",
"TryToBuyOneTimePurchase"
"TryToBuyOneTimePurchase",
"UnblockBackButton"
],
"Default": "new j4()",
"Migrated": false
"Migrated": true
},
{
"Field": "battlePauseState",
File diff suppressed because it is too large. Load diff
+59
View File
@@ -40,6 +40,24 @@ const AnonymousActionNames = new Map([
// `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'],
]);
/** 动作局部类名 → 动作名;未收录的返回 null。 */
@@ -395,6 +413,7 @@ const Migrated = {
twitchState: { Type: 'TwitchStateSlice', Reduce: 'ReduceTwitch' },
home: { Type: 'HomeStateSlice', Reduce: 'ReduceHome' },
localBattleUserState: { Type: 'LocalBattleUserStateSlice', Reduce: 'ReduceLocalBattleUser' },
shop: { Type: 'ShopStateSlice', Reduce: 'ReduceShop' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -414,6 +433,28 @@ for (const R of Rows) {
// ---------------------------------------------------------------- 输出
// 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',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
JSON.stringify(
@@ -430,6 +471,24 @@ fs.writeFileSync(
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,
})),
],
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 快路径,本身无字段、无独立效果。',
},
],
Rows,
},
+462
View File
@@ -0,0 +1,462 @@
// gen_slice_plan.js —— 从 module_10032 抽取某个根状态切片的「状态字段 + reducer 分支表」
//
// 用途:为逐个迁移大切片(shop / user / screen / garage …)提供机器可读证据,
// 避免人工在 1 万行 reducer 里数位置。产出 3DTank/names/slice_<field>.json:
// State : 状态类(class / 构造参数 / 槽位 / toString 字段 / 默认值 / copy 方法)
// Copy : copy 方法的参数个数(== 字段数才认为可信)
// Branches : 每个顶层动作分支 { Local, Name, Kind, Changed:[{Field, Slot, Index, Value}], Expr }
// Kind = copy / reset / noop / conditional;Value 保留原表达式文本,供人工翻译
// SubReducers : 顶层分支里内嵌的 `(function (t, n) { … })(t, n.<子状态>)` 子 reducer
// (例如 shop 的 x4 分支会把 shopCategories/shopItems 两个嵌套模型交给子 reducer)
// { Var, Arg, Style(chain|ternary), ArmCount, Locals:[{Local,Name}], Arms:[{Local,Expr}] }
// Counts : TotalInstanceof / TopLevelBranches / SubReducerArms,用来说明「为什么不是一句话」
// Locals : 局部动作类 → 动作名(action_inventory 里查不到则为 null)
//
// 用法:cd 3DTank/tools && node gen_slice_plan.js shop [user screen ...]
'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);
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);
const Fns = new Map();
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.set(M[1], { Name: M[1], Params: M[2], 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 FindMethod(Obj, Method) { return Methods.find((M) => M.Obj === Obj && M.Method === Method) || null; }
// ---------------------------------------------------------------- 根 reducer / 根状态
const RootReducerFn = [...Fns.values()].find((F) => T.slice(F.Body, F.End).includes('return n.cti('));
if (!RootReducerFn) throw new Error('找不到根 reducer');
const CopyMethod = Methods.find((M) => M.Method === 'cti');
const RootClass = CopyMethod.Obj;
const RootClassFn = Fns.get(RootClass);
const RootBody = T.slice(RootClassFn.Body, RootClassFn.End);
const SlotOfParam = new Map();
for (const M of RootBody.matchAll(/\(this\.(\w+) = (\w+)\)/g)) SlotOfParam.set(M[2], M[1]);
const DefaultOfParam = new Map();
for (const M of RootBody.matchAll(/\((\w+) = \1 === cd \? ([\s\S]*?) : \1\)/g)) DefaultOfParam.set(M[1], M[2].replace(/\s+/g, ' ').trim());
for (const M of RootBody.matchAll(/\((\w+) = \1 !== cd && \1\)/g)) DefaultOfParam.set(M[1], 'false');
for (const M of RootBody.matchAll(/\((\w+) = \1 === cd \|\| \1\)/g)) DefaultOfParam.set(M[1], 'true');
const ToStringStart = T.indexOf(`(ld(${RootClass}).toString = function`);
const ToStringEnd = T.indexOf(`(ld(${RootClass}).hashCode`, ToStringStart);
const RootFields = [];
for (const M of T.slice(ToStringStart, ToStringEnd).matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
RootFields.push({ Field: M[2], Slot: M[3] });
}
/** 根 reducer 顶层赋值 [{Name, Expr}]。 */
function TopLevelAssignments(Body) {
const Out = [];
let I = 0, Depth = 0;
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++; }
continue;
}
if ('([{'.includes(C)) { Depth++; I++; continue; }
if (')]}'.includes(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]))) {
let J = I + M[0].length, 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 ('([{'.includes(D0)) D++;
else if (')]}'.includes(D0)) D--;
else if ((D0 === ',' || D0 === ';') && D === 0) break;
J++;
}
Out.push({ Name: M[1], Expr: Body.slice(I + M[0].length, J).trim() });
I = J + 1; continue;
}
}
I++;
}
return Out;
}
const Assignments = TopLevelAssignments(T.slice(RootReducerFn.Body, RootReducerFn.End));
// ---------------------------------------------------------------- 工具
/** 顶层逗号切分(按掩码文本判定深度,字符串里的逗号不切)。 */
function SplitTop(Text) {
const M = MaskStrings(Text);
const Out = []; let D = 0, Cur = '';
for (let I = 0; I < Text.length; I++) {
const C = M[I];
if ('([{'.includes(C)) D++;
else if (')]}'.includes(C)) D--;
if (C === ',' && D === 0) { Out.push(Cur.trim()); Cur = ''; } else Cur += Text[I];
}
if (Cur.trim()) Out.push(Cur.trim());
return Out;
}
/**
* 把字符串/注释内容替换成同长度空格(保留长度,便于用同一套下标扫描括号)。
* 不做这一步的话,字符串里的 `[` `{` 会让括号深度错位,漏掉后面的分支。
*/
function MaskStrings(Text) {
const Out = Text.split('');
let I = 0;
while (I < Text.length) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C;
Out[I] = ' ';
I++;
while (I < Text.length) {
if (Text[I] === '\\') { Out[I] = ' '; Out[I + 1] = ' '; I += 2; continue; }
if (Text[I] === Q) { Out[I] = ' '; I++; break; }
if (Text[I] !== '\n') Out[I] = ' ';
I++;
}
continue;
}
if (C === '/' && Text[I + 1] === '/') {
const N = Text.indexOf('\n', I);
const E = N < 0 ? Text.length : N;
for (let J = I; J < E; J++) Out[J] = ' ';
I = E;
continue;
}
if (C === '/' && Text[I + 1] === '*') {
const N = Text.indexOf('*/', I + 2);
const E = N < 0 ? Text.length : N + 2;
for (let J = I; J < E; J++) if (Text[J] !== '\n') Out[J] = ' ';
I = E;
continue;
}
I++;
}
return Out.join('');
}
/** 从 Text[Open] 的 `{` 找到配对的 `}` 下标(掩码判定)。 */
function MatchBrace(Text, Open) {
const M = MaskStrings(Text);
let D = 0;
for (let I = Open; I < Text.length; I++) {
const C = M[I];
if (C === '{') D++;
else if (C === '}') { D--; if (D === 0) return I; }
}
return -1;
}
/** 找顶层 `if (… instanceof …)` 守卫(掩码判深度)。 */
function FindGuards(Text) {
const M = MaskStrings(Text);
const Out = [];
let I = 0, Depth = 0;
while (I < Text.length) {
const C = M[I];
if ('([{'.includes(C)) { Depth++; I++; continue; }
if (')]}'.includes(C)) { Depth--; I++; continue; }
if (Depth === 0 && M.startsWith('if (', I) && M.slice(I, I + 80).includes('instanceof')) {
const OpenP = I + 3;
let D = 0, J = OpenP;
for (; J < Text.length; J++) { const D0 = M[J]; if (D0 === '(') D++; else if (D0 === ')') { D--; if (D === 0) break; } }
Out.push({ At: I, GuardEnd: J, Cond: Text.slice(OpenP + 1, J).replace(/\s+/g, ' ').trim() });
I = J + 1; continue;
}
I++;
}
return Out;
}
/** 从 chunk 里取最浅层的 `i = <expr>`(掩码判深度)。 */
function OuterAssign(Chunk) {
const MC = MaskStrings(Chunk);
let Best = null, BestDepth = Infinity, I2 = 0, D2 = 0;
while (I2 < Chunk.length) {
const C2 = MC[I2];
if ('([{'.includes(C2)) { D2++; I2++; continue; }
if (')]}'.includes(C2)) { D2--; I2++; continue; }
if (MC.startsWith('i =', I2)) {
let J2 = I2 + 3, DD = 0;
while (J2 < Chunk.length) {
const D0 = MC[J2];
if ('([{'.includes(D0)) DD++;
else if (')]}'.includes(D0)) { if (DD === 0) break; DD--; }
else if (D0 === ';' && DD === 0) break;
J2++;
}
if (D2 < BestDepth) { BestDepth = D2; Best = Chunk.slice(I2 + 3, J2).trim(); }
I2 = J2; continue;
}
I2++;
}
return Best;
}
function LocalsIn(Text) {
const Out = [];
for (const M of Text.matchAll(/t instanceof ([A-Za-z0-9_$]+)/g)) Out.push(M[1]);
for (const M of Text.matchAll(/Xd\(t, ([A-Za-z0-9_$]+)\)/g)) Out.push(M[1]);
return [...new Set(Out)];
}
/** 把 `t instanceof A ? e1 : t instanceof B ? e2 : e3` 切成手。 */
function TernaryArms(Text) {
const Hits = [...Text.matchAll(/t instanceof ([A-Za-z0-9_$]+)\s*\?/g)];
return Hits.map((H, Hi) => {
const From = H.index + H[0].length;
const To = Hi + 1 < Hits.length ? Hits[Hi + 1].index : Text.length;
return {
Local: H[1],
Expr: Text.slice(From, To).replace(/^\s*/, '').replace(/[,\s]*:\s*$/, '').replace(/\s*:\s*$/, '').trim(),
};
});
}
/** 槽位 → 字段名 / 字段名 → 索引(按 toString 顺序)。 */
const FieldOfSlot = new Map(RootFields.map((F) => [F.Slot, F.Field]));
function KotlinName(Field) { return Field.charAt(0).toUpperCase() + Field.slice(1); }
// ---------------------------------------------------------------- 单切片计划
function PlanSlice(Field) {
const F = RootFields.find((X) => X.Field === Field);
if (!F) throw new Error('根状态里没有字段 ' + Field);
const A = Assignments.find((X) => new RegExp('\\bn\\.' + F.Slot + '\\b').test(X.Expr));
if (!A) throw new Error(Field + ' 在根 reducer 里找不到委派');
const Param = [...SlotOfParam.keys()].find((P2) => SlotOfParam.get(P2) === F.Slot);
const SliceClass = (DefaultOfParam.get(Param) || '').match(/^new ([A-Za-z0-9_$]+)\(/);
const ClassName = SliceClass ? SliceClass[1] : null;
const Info = {
Field, Slot: F.Slot, Class: ClassName, ClassSource: DefaultOfParam.get(Param) || '',
Params: [], Fields: [], CopyMethod: null, Branches: [], SubReducers: [], Counts: {}, Locals: {},
Delegate: A.Expr.replace(/\s+/g, ' ').trim().slice(0, 400),
};
if (ClassName) {
const CFn = Fns.get(ClassName);
Info.Params = CFn ? CFn.Params.split(',').map((S) => S.trim()).filter(Boolean) : [];
const CB = CFn ? T.slice(CFn.Body, CFn.End) : '';
const SOfP = new Map();
for (const M of CB.matchAll(/\(this\.([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\)/g)) SOfP.set(M[2], M[1]);
const DOfP = new Map();
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 === cd \? ([\s\S]*?) : \1\)/g)) DOfP.set(M[1], M[2].replace(/\s+/g, ' ').trim());
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 !== cd && \1\)/g)) DOfP.set(M[1], 'false');
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 === cd \|\| \1\)/g)) DOfP.set(M[1], 'true');
const S = T.indexOf(`(ld(${ClassName}).toString = function`);
const E = T.indexOf(`(ld(${ClassName}).hashCode`, S);
if (S >= 0 && E > S) {
for (const M of T.slice(S, E).matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
const P2 = [...SOfP.keys()].find((K) => SOfP.get(K) === M[3]);
Info.Fields.push({ Field: M[2], Slot: M[3], Kotlin: KotlinName(M[2]), Default: P2 ? DOfP.get(P2) || '' : '' });
}
}
}
const CopyName = (A.Expr.match(/\bn\.([A-Za-z0-9_$]+)\(/) || [])[1] || null;
Info.CopyMethod = CopyName;
if (CopyName && ClassName) {
const M = Methods.find((X) => X.Obj === ClassName && X.Method === CopyName);
Info.CopyParamCount = M ? M.Params.split(',').map((S) => S.trim()).filter(Boolean).length : null;
Info.CopyMatchesFields = Info.CopyParamCount === Info.Fields.length;
}
// ---- 分支抽取:只处理内联 IIFE;具名 reducer 请用 gen_root_wiring 的证据表
const Expr = A.Expr;
if (!/^\(function \(t, n\)/.test(Expr)) {
Info.Branches = [{ Guard: '(具名 reducer)', Locals: [], Kind: 'named', Expr: Expr.replace(/\s+/g, ' ').slice(0, 200) }];
Info.Counts = { TotalInstanceof: null, TopLevelBranches: 1, SubReducerArms: 0 };
return Info;
}
// IIFE 的 { 位置要对到全局源码:用 Pos 定位
const Pos = T.indexOf(A.Expr, RootReducerFn.Body);
const GlobalOpen = T.indexOf('{', T.indexOf('function', Pos));
const GlobalClose = Pairs.get(GlobalOpen);
const Body = T.slice(GlobalOpen + 1, GlobalClose);
function Decode(BranchExpr) {
const E = BranchExpr.replace(/\s+/g, ' ').trim();
const Fields = [];
if (/^n$/.test(E)) return { Kind: 'noop', Changed: [] };
const CopyCall = new RegExp('\\bn\\.' + (CopyName || 'qt6') + '\\(');
const M = CopyCall.exec(E);
if (/new [A-Za-z0-9_$]+\(/.test(E) && !M) return { Kind: 'reset', Changed: [] };
if (!M) return { Kind: 'conditional', Changed: [] };
// 找该调用的参数(最外层)
let O = E.indexOf('(', M.index), D = 0, End = -1;
for (let J = O; J < E.length; J++) { const C2 = E[J]; if (C2 === '(') D++; else if (C2 === ')') { D--; if (D === 0) { End = J; break; } } }
const Args = SplitTop(E.slice(O + 1, End));
Args.forEach((Arg, Index) => {
if (Arg === 'cd') return;
const FI = Info.Fields[Index];
Fields.push({ Index, Field: FI ? FI.Field : null, Slot: FI ? FI.Slot : null, Kotlin: FI ? FI.Kotlin : null, Value: Arg });
});
const HasHelper = Fields.some((X) => /[A-Za-z_$][\w$]*\(/.test(X.Value) && !/^t\.[A-Za-z0-9_$]+$/.test(X.Value));
const Simple = Fields.length > 0 && Fields.every((X) => /^t\.[A-Za-z0-9_$]+$/.test(X.Value) || /^(null|!0|!1|true|false|-?\d+)$/.test(X.Value));
return { Kind: Simple || !HasHelper ? 'copy' : 'copy-helper', Changed: Fields };
}
/** 顶层 if/else-if 链 → 分支表。 */
function ChainBranches(Text) {
const Guards = FindGuards(Text);
const Out = [];
Guards.forEach((G, Gi) => {
const NextGuard = Guards[Gi + 1];
const ChunkEnd = NextGuard ? Text.lastIndexOf('else', NextGuard.At) : Text.length;
const Chunk = Text.slice(G.GuardEnd + 1, ChunkEnd);
let BranchExpr = OuterAssign(Chunk);
// 块体分支(例如 KU):`{ var o = t.irl_1; i = null == o ? n : n.qt6(…); }`
// 先剥掉最外层花括号,再取块内最后一个顶层赋值,避免被后续 else 链的三元污染。
const ChunkTrim = Chunk.trim();
if (ChunkTrim.startsWith('{')) {
const Open = Chunk.indexOf('{');
const Close = MatchBrace(Chunk, Open);
if (Close > Open) {
const Inner = OuterAssign(Chunk.slice(Open + 1, Close));
if (Inner !== null) BranchExpr = Inner;
}
}
if (BranchExpr === null) return;
Out.push({ Guard: G.Cond, Locals: LocalsIn(G.Cond), ...Decode(BranchExpr), Expr: BranchExpr.replace(/\s+/g, ' ').trim().slice(0, 300), _Chunk: Chunk });
});
// 末尾 else:`else i = t instanceof m5 ? … : t instanceof f5 ? … : n;`
if (Guards.length) {
const LastEnd = Guards[Guards.length - 1].GuardEnd;
const Tail = Text.slice(LastEnd);
const ElseAt = Tail.lastIndexOf('else');
if (ElseAt >= 0) {
const BranchExpr = OuterAssign(Tail.slice(ElseAt));
if (BranchExpr) {
const Arms = TernaryArms(BranchExpr);
for (const Arm of Arms) {
Out.push({ Guard: 't instanceof ' + Arm.Local, Locals: [Arm.Local], ...Decode(Arm.Expr), Expr: Arm.Expr.replace(/\s+/g, ' ').trim().slice(0, 300) });
}
if (!Arms.length) Out.push({ Guard: '(else)', Locals: [], ...Decode(BranchExpr), Expr: BranchExpr.replace(/\s+/g, ' ').trim().slice(0, 300) });
}
}
}
return Out;
}
/** 分支里内嵌的 `(function (t, n) { … })(t, <arg>)` 子 reducer。 */
function NestedReducers(Chunk) {
const Out = [];
const Re = /(?:var\s+([A-Za-z0-9_$]+)\s*=\s*)?\(function \(t, n\)\s*\{/g;
let M;
while ((M = Re.exec(Chunk))) {
const Open = Chunk.indexOf('{', M.index + M[0].length - 1);
const Close = MatchBrace(Chunk, Open);
if (Close < 0) continue;
const Body2 = Chunk.slice(Open + 1, Close);
const After = Chunk.slice(Close + 1, Close + 120);
const CallArg = (After.match(/^\)\(t,\s*([^)]+)\)/) || [])[1] || null;
const IsTernary = /\breturn\s+t instanceof [A-Za-z0-9_$]+\s*\?/.test(Body2);
let Arms;
if (IsTernary) {
const Ret = Body2.slice(Body2.search(/\breturn\s+t instanceof /));
Arms = TernaryArms(Ret).map((Arm) => ({ Local: Arm.Local, Expr: Arm.Expr.replace(/\s+/g, ' ').slice(0, 200) }));
} else {
Arms = ChainBranches(Body2).map((B) => ({
Local: (B.Locals || [])[0] || null,
Guard: B.Guard,
Kind: B.Kind,
Expr: B.Expr,
}));
}
Out.push({
Var: M[1] || null,
Arg: CallArg ? CallArg.trim() : null,
Style: IsTernary ? 'ternary' : 'chain',
ArmCount: Arms.length,
Locals: Arms.map((X) => X.Local).filter(Boolean).map((L) => ({ Local: L, Name: LocalToName.get(L) || null })),
Arms,
});
Re.lastIndex = Close + 1;
}
return Out;
}
const Top = ChainBranches(Body);
Info.Branches = Top.map(({ _Chunk, ...Rest }) => Rest);
for (const B of Top) {
if (B._Chunk) Info.SubReducers.push(...NestedReducers(B._Chunk));
}
Info.Counts = {
TotalInstanceof: (Body.match(/\bt instanceof [A-Za-z0-9_$]+/g) || []).length,
TopLevelBranches: Info.Branches.length,
SubReducerArms: Info.SubReducers.reduce((N, S) => N + S.ArmCount, 0),
};
// 局部类命名情况
const Collect = (L) => { if (!Info.Locals[L]) Info.Locals[L] = { Name: LocalToName.get(L) || null }; };
for (const B of Info.Branches) for (const L of B.Locals || []) Collect(L);
for (const S of Info.SubReducers) for (const X of S.Locals) Collect(X.Local);
return Info;
}
// ---------------------------------------------------------------- 输出
const Want = process.argv.slice(2);
if (!Want.length) throw new Error('用法:node gen_slice_plan.js <field> [field ...]');
for (const Field of Want) {
const Info = PlanSlice(Field);
const OutFile = path.join(P, 'names', 'slice_' + Field + '.json');
fs.writeFileSync(OutFile, JSON.stringify({ generatedAt: new Date().toISOString(), module: 'module_10032.js', ...Info }, null, 2) + '\n');
const Kinds = {};
for (const B of Info.Branches) Kinds[B.Kind] = (Kinds[B.Kind] || 0) + 1;
const Anon = Object.entries(Info.Locals).filter(([, V]) => !V.Name).map(([K]) => K);
const C = Info.Counts || {};
console.log(`${Field}: class=${Info.Class} 字段=${Info.Fields.length} copy=${Info.CopyMethod}(${Info.CopyParamCount}) 顶层分支=${Info.Branches.length} ${JSON.stringify(Kinds)}`);
console.log(` 实例判定=${C.TotalInstanceof} 子reducer=${(Info.SubReducers || []).length} 子reducer分支=${C.SubReducerArms}`);
for (const S of Info.SubReducers || []) console.log(` 子reducer var=${S.Var} arg=${S.Arg} style=${S.Style} arms=${S.ArmCount}`);
if (Anon.length) console.log(' 未具名局部类: ' + Anon.join(', '));
}
@@ -0,0 +1,514 @@
// ShopReducer.kt —— 商店切片(对应 module_10032 的 j4 状态 + 根 reducer 的 `(inline@shop)` 分支)
//
// 证据链:
// 状态类 j4:`function j4(t, n, …, C)`,30 个字段,toString 名为 `Shop(shopCategories=…, …)`。
// 默认值取自构造:shopCategories=new U7()、shopItems=new w3()、enabled 缺省为 true(`s === cd || s`)、
// previousScreen 缺省为 null、mobileCountryCode="+7 "、mobilePhoneLength=13、其余 null/false/空表。
// 嵌套状态类:
// U7 = Categories(categories, categoryTypeToId, categoryIdToType, categoriesWithDiscount,
// categoriesWithBonus, selectedCategoryType, selectedCategoryId)
// w3 = ShopItems(items, discountItems, featureItems, … , shopCards, groups),23 个字段
// 嵌套 reducer:
// U7:5 臂(InitCategory / add-bonus / add-discount / select-category / select-category-id)
// w3:32 臂(init-* / shop-card 的增删改 / one-time-purchase / group)
// 顶层 reducer:32 个分支,其中 `Xd(t, $4) || t instanceof x4` 是「动作实现了商店内容标记接口」的
// 快路径,内部再跑 U7/w3 两个子 reducer;其余 31 个分支直接改 30 个字段中的某一个。
// 无关动作返回原状态;复位分支保留 coinPaymentProviderUrl(`new j4(…, n.qq3_1)`)。
//
// 无 toString / 不在动作目录里的类(根 reducer 的 `?F5` 等)按「reducer 效果」定名,原名写进注释;
// 名字同时登记在 3DTank/tools/gen_root_wiring.js 的 AnonymousActionNames 里,重新生成 RootWiring 后会显示。
package App.State.Reducers
import App.State.AddItemToOrderProcessing
import App.State.AdditionalDescription
import App.State.BraintreeLoadingInterrupt
import App.State.Category
import App.State.CurrentPayMode
import App.State.DeleteOneTimePurchaseItem
import App.State.InitAdditionalDescription
import App.State.InitAmountString
import App.State.InitCategory
import App.State.InitCoinPackage
import App.State.InitCompensation
import App.State.InitCrystalKitPackage
import App.State.InitFeatureItem
import App.State.InitGroup
import App.State.InitItemViewType
import App.State.InitKeysPackage
import App.State.InitKitBundle
import App.State.InitKitPackage
import App.State.InitLootBoxAndPaint
import App.State.InitOneTimePurchaseItem
import App.State.InitPayModeAction
import App.State.InitPaymentProviderItems
import App.State.InitPaymentViewType
import App.State.InitRestrictionByPayMode
import App.State.InitRewardedVideosBlocks
import App.State.InitShopCard
import App.State.InitShopCardDiscount
import App.State.InitShopCardView
import App.State.InitShopItem
import App.State.InitShopItemName
import App.State.InitSingleItemKit
import App.State.InitSpecialKitPackage
import App.State.MobilePaymentStateChanged
import App.State.OneTimePurchaseItem
import App.State.PayMode
import App.State.PaymentProviderItem
import App.State.PaymentUrlReceived
import App.State.RemoveItemFromOrderProcessing
import App.State.SavePreviousScreen
import App.State.SaveTargetProductId
import App.State.SaveTargetVisitedCategory
import App.State.SelectCategoryId
import App.State.SelectItem
import App.State.SetBraintreeToken
import App.State.SetCoinPaymentProviderUrl
import App.State.SetInitShopItemId
import App.State.SetPurchaseCompleted
import App.State.SetStripeIntentParams
import App.State.SetupDiscountCategoryByItem
import App.State.SetupDiscountItem
import App.State.ShopCard
import App.State.ShopCardExpired
import App.State.ShopCardRemoved
import App.State.ShopCardSold
import App.State.ShopItem
import App.State.ToggleShopActivity
import App.State.TryToBuyOneTimePurchase
// ---------------------------------------------------------------- 原 U7 / w3 用到的枚举
/** 原 `Gh`(module_85667 的 CategoryType 枚举,17 个成员);默认 `NO_CATEGORY`(原 `v$()`)。 */
enum class CategoryType {
CRYSTALS,
COINS,
PREMIUM,
GOLD_BOXES,
EARLY_ACCESS,
STARS,
NO_CATEGORY,
TEMPORARY,
EPIC_KEYS,
LIMITED_OFFERS,
SHOW_CASE,
PROMO_CODE,
DAILY_RUBIES,
DAILY_COINS,
CONTRACTS,
ADVENT,
OTHER,
;
companion object {
/** 运行期是 JS 枚举对象,toString 就是成员名;认不出时与原包默认值一致落到 NO_CATEGORY。 */
fun FromText(Text: Any?): CategoryType =
entries.firstOrNull { it.name == Text?.toString() } ?: NO_CATEGORY
}
}
/** 原 `K5`:NONE / IN_PROGRESS / FINISH_SUCCESS / FINISH_FAIL(`q4()` 返回 NONE)。 */
enum class PaymentProviderState {
NONE,
IN_PROGRESS,
FINISH_SUCCESS,
FINISH_FAIL,
;
companion object {
fun FromText(Text: Any?): PaymentProviderState =
entries.firstOrNull { it.name == Text?.toString() } ?: NONE
}
}
/** 原 `W5`:NONE / PHONE_VALIDATION_IN_PROGRESS / PHONE_VALID / PHONE_INVALID / PAYMENT_IN_PROGRESS / PAYMENT_INITED / PAYMENT_ERROR。 */
enum class MobilePaymentState {
NONE,
PHONE_VALIDATION_IN_PROGRESS,
PHONE_VALID,
PHONE_INVALID,
PAYMENT_IN_PROGRESS,
PAYMENT_INITED,
PAYMENT_ERROR,
;
companion object {
fun FromText(Text: Any?): MobilePaymentState =
entries.firstOrNull { it.name == Text?.toString() } ?: NONE
}
}
// ---------------------------------------------------------------- 无 toString 的动作类(原名 -> 效果名)
/** 原 `Yut`(继承 aC):load-shop 完成,只把 isLoaded 置 true。 */
class ShopLoaded
/** 原 `F5`:整片复位,但保留 coinPaymentProviderUrl(`new j4(cd, …, n.qq3_1)`)。 */
class ResetShop
/** 原 `g4`:支付回调失败,paymentUrl 清空、receivePaymentUrlFailed=true。 */
class PaymentUrlReceiveFailed
/** 原 `X5`:支付状态复位(paymentUrl=null、receivePaymentUrlFailed=false、两种 payment state=NONE)。 */
class ResetPaymentState
/** 原 `w5`:goToHomeAfterBuy=true。 */
class SetGoToHomeAfterBuy
/** 原 `o4`:paymentProviderState=IN_PROGRESS(`(V5(), Rl)`)。 */
class PaymentProviderInProgress
/** 原 `m4`:paymentProviderState=FINISH_SUCCESS(`y4()`)。 */
class PaymentProviderFinishSuccess
/** 原 `k4`:paymentProviderState=FINISH_FAIL(`(V5(), Nl)`)。 */
class PaymentProviderFinishFail
/** 原 `i4`:isBlockedBackButton=true。 */
class BlockBackButton
/** 原 `t4`:isBlockedBackButton=false。 */
class UnblockBackButton
/** 原 `f5`:previousScreen=null。 */
class ClearPreviousScreen
/** 原 `v5`:targetCategory=null。 */
class ClearTargetCategory
/** 原 `p5`:targetProductId=null。 */
class ClearTargetProductId
/**
* 原 `H7`:无构造字段,reducer 读 `t.zsw_1`,对应 U7 的 `categoriesWithBonus = Zg(现有集合, t.zsw_1)`。
* 字段按同族动作补名为 categoryIds。
*/
class AddCategoriesWithBonus(val categoryIds: Set<Any?>)
/**
* 原 `s5`:无构造字段,reducer 把 `(t.ft3_1, t.gt3_1)` 写进 paymentModeAsShopItem。
* 字段按同族动作补名为 itemId / shopItem。
*/
class InitPaymentModeAsShopItem(val itemId: Any?, val shopItem: Any?)
/**
* 原 `o5`:无构造字段,reducer 把 `(t.dt3_1, t.et3_1)` 写进 discountItems。
* 字段按同族动作补名为 itemId / discountItem。
*/
class SetupDiscountItemForItem(val itemId: Any?, val discountItem: Any?)
/** 原 `_5`:无构造字段,reducer 从 discountItems 删除 `t.ct3_1`。字段按同族动作补名为 itemId。 */
class RemoveDiscountItemForItem(val itemId: Any?)
/** 原 `O7`(toString 也是 `SelectCategory(categoryType=…)`,与 lootBoxes 的同名动作不是同一个类)。 */
class SelectShopCategory(val categoryType: Any?)
// ---------------------------------------------------------------- 两个嵌套状态
/**
* 原 `U7`:`Categories(categories, categoryTypeToId, categoryIdToType, categoriesWithDiscount,
* categoriesWithBonus, selectedCategoryType, selectedCategoryId)`。
* 三张表默认空 Map、两个集合默认空 Set、selectedCategoryType 默认 NO_CATEGORY、selectedCategoryId 默认 null。
*/
data class ShopCategoriesState(
val Categories: Map<Any?, Category> = emptyMap(),
val CategoryTypeToId: Map<Any?, Any?> = emptyMap(),
val CategoryIdToType: Map<Any?, Any?> = emptyMap(),
val CategoriesWithDiscount: Set<Any?> = emptySet(),
val CategoriesWithBonus: Set<Any?> = emptySet(),
val SelectedCategoryType: CategoryType = CategoryType.NO_CATEGORY,
val SelectedCategoryId: Any? = null,
)
/**
* 原 `w3`:`ShopItems(items, discountItems, …, shopCards, groups)`,23 个字段全部默认空 Map。
* 需要逐项改写的表(shopCards / oneTimePurchaseItems / paymentProviderItems)用强类型;
* 其余表原包就是「原样存进对象」,保持 Any?,避免编造模型。
*/
data class ShopItemsState(
val Items: Map<Any?, Any?> = emptyMap(),
val DiscountItems: Map<Any?, Any?> = emptyMap(),
val FeatureItems: Map<Any?, Any?> = emptyMap(),
val SpecialKitPackage: Map<Any?, Any?> = emptyMap(),
val CrystalKitPackage: Map<Any?, Any?> = emptyMap(),
val CoinPackage: Map<Any?, Any?> = emptyMap(),
val KeysPackage: Map<Any?, Any?> = emptyMap(),
val ItemViewType: Map<Any?, Any?> = emptyMap(),
val ShopItemName: Map<Any?, Any?> = emptyMap(),
val KitPackage: Map<Any?, Any?> = emptyMap(),
val KitBanner: Map<Any?, Any?> = emptyMap(),
val AmountString: Map<Any?, Any?> = emptyMap(),
val PaymentModeAsShopItem: Map<Any?, Any?> = emptyMap(),
val RestrictionByPayMode: Map<Any?, Any?> = emptyMap(),
val SingleItemKit: Map<Any?, Any?> = emptyMap(),
val LootBoxAndPaint: Map<Any?, Any?> = emptyMap(),
val OneTimePurchaseItems: Map<Any?, OneTimePurchaseItem> = emptyMap(),
val CompensationItems: Map<Any?, Any?> = emptyMap(),
val PaymentProviderItems: Map<Any?, PaymentProviderItem> = emptyMap(),
val AdditionalDescription: Map<Any?, Any?> = emptyMap(),
val RewardedVideosBlocks: Map<Any?, Any?> = emptyMap(),
val ShopCards: Map<Any?, ShopCard> = emptyMap(),
val Groups: Map<Any?, Any?> = emptyMap(),
)
// ---------------------------------------------------------------- 顶层状态
/**
* 原 `j4`:`Shop(shopCategories, shopItems, initShopItemId, selectedItemId, isLoaded, itemForPurchase,
* enabled, coinPaymentProviderUrl, payModes, selectedPayModeId, paymentViewType, paymentUrl,
* receivePaymentUrlFailed, mobileCountryCode, mobilePhoneLength, mobilePaymentState, paymentProviderState,
* asiassoftAmountInAtc, asiassoftEnoughMoney, braintreeToken, stripeClientSecret, stripePublishKey,
* isBlockedBackButton, braintreeLoadingInterrupt, goToHomeAfterBuy, previousScreen, targetCategory,
* targetProductId, lastPurchasedItem, ordersInProcessing)`。
*/
data class ShopStateSlice(
val ShopCategories: ShopCategoriesState = ShopCategoriesState(),
val ShopItems: ShopItemsState = ShopItemsState(),
val InitShopItemId: Any? = null,
val SelectedItemId: Any? = null,
val IsLoaded: Boolean = false,
val ItemForPurchase: ShopItem? = null,
/** 原构造 `s = s === cd || s`:缺省 true(不是 false/空串)。 */
val Enabled: Boolean = true,
val CoinPaymentProviderUrl: String? = null,
val PayModes: Set<PayMode> = emptySet(),
val SelectedPayModeId: Any? = null,
val PaymentViewType: Map<Any?, Any?> = emptyMap(),
val PaymentUrl: Any? = null,
val ReceivePaymentUrlFailed: Boolean = false,
val MobileCountryCode: String = "+7 ",
val MobilePhoneLength: Int = 13,
val MobilePaymentState: MobilePaymentState = App.State.Reducers.MobilePaymentState.NONE,
val PaymentProviderState: PaymentProviderState = App.State.Reducers.PaymentProviderState.NONE,
val AsiassoftAmountInAtc: Int = 0,
val AsiassoftEnoughMoney: Any? = null,
val BraintreeToken: Any? = null,
val StripeClientSecret: Any? = null,
val StripePublishKey: Any? = null,
val IsBlockedBackButton: Boolean = false,
val BraintreeLoadingInterrupt: Boolean = false,
val GoToHomeAfterBuy: Boolean = false,
val PreviousScreen: Any? = null,
val TargetCategory: Any? = null,
val TargetProductId: Any? = null,
val LastPurchasedItem: Any? = null,
val OrdersInProcessing: Map<Any?, Pair<Any?, Any?>> = emptyMap(),
)
// ---------------------------------------------------------------- 嵌套 reducer
/** 原 U7 的 5 臂。 */
fun ReduceShopCategories(State0: ShopCategoriesState, A: Any): ShopCategoriesState = when (A) {
// F7:一次性写入 categories / categoryTypeToId / categoryIdToType(三个 Map 的键分别是 id / type→id / id→type)
is InitCategory -> {
val C = A.category as? Category
if (C == null) {
State0
} else {
val Type = CategoryType.FromText(C.type)
State0.copy(
Categories = State0.Categories + (C.id to C),
CategoryTypeToId = State0.CategoryTypeToId + (Type to C.id),
CategoryIdToType = State0.CategoryIdToType + (C.id to Type),
)
}
}
// H7:categoriesWithBonus = 现有集合 ∪ 动作里的集合(原 `Zg`)
is AddCategoriesWithBonus -> State0.copy(CategoriesWithBonus = State0.CategoriesWithBonus + A.categoryIds)
// G7:categoriesWithDiscount = 现有集合 + 单个 categoryId(原 `Xg`)
is SetupDiscountCategoryByItem -> State0.copy(CategoriesWithDiscount = State0.CategoriesWithDiscount + A.categoryId)
// O7:selectedCategoryType = 动作里的枚举
// 原类 toString 也是 SelectCategory,但字段是 categoryType;与 lootBoxes 的 SelectCategory(containerCategory) 同名不同类。
is SelectShopCategory -> State0.copy(SelectedCategoryType = CategoryType.FromText(A.categoryType))
// D7:selectedCategoryId = 动作里的 id
is SelectCategoryId -> State0.copy(SelectedCategoryId = A.categoryId)
else -> State0
}
/** 原 w3 的 32 臂。 */
fun ReduceShopItems(State0: ShopItemsState, A: Any): ShopItemsState = when (A) {
is InitShopItem -> State0.copy(Items = State0.Items + (A.itemId to A.shopItem))
is InitShopCard -> {
val Card = A.shopCard as? ShopCard
if (Card == null) {
State0
} else {
// 原 `new f3(t.pt2_1.peb_1, t.qt2_1, t.pt2_1.m(), …)`:view/discount 显式给 null
val NewCard = ShopCard(
activeUntil = Card.activeUntil,
goodInfo = A.goods,
name = Card.name,
sold = Card.sold,
pinned = Card.pinned,
type = Card.type,
view = null,
discount = null,
sortIndex = Card.sortIndex,
shopCategoryId = A.shopItemCategoryId,
)
State0.copy(ShopCards = State0.ShopCards + (A.itemId to NewCard))
}
}
is InitShopCardDiscount -> {
val Card = State0.ShopCards[A.itemId]
if (Card == null) State0
else State0.copy(ShopCards = State0.ShopCards + (A.itemId to Card.copy(discount = A.shopCardDiscount)))
}
is InitShopCardView -> {
val Card = State0.ShopCards[A.itemId]
if (Card == null) State0
else State0.copy(ShopCards = State0.ShopCards + (A.itemId to Card.copy(view = A.shopCardView)))
}
is ShopCardSold -> {
val Card = State0.ShopCards[A.itemId]
if (Card == null) State0
else State0.copy(ShopCards = State0.ShopCards + (A.itemId to Card.copy(sold = true)))
}
// Y3:activeUntil = new Dd(0, 0)
is ShopCardExpired -> {
val Card = State0.ShopCards[A.itemId]
if (Card == null) State0
else State0.copy(ShopCards = State0.ShopCards + (A.itemId to Card.copy(activeUntil = 0L)))
}
is ShopCardRemoved -> State0.copy(ShopCards = State0.ShopCards - A.itemId)
is SetupDiscountItem -> State0.copy(DiscountItems = State0.DiscountItems + (A.itemId to A.discountItem))
// r5:把动作里的集合按 csz_1(PaymentProviderItem.itemId)建成 Map
is InitPaymentProviderItems -> State0.copy(
PaymentProviderItems = (A.items as? Collection<*>)
?.filterIsInstance<PaymentProviderItem>()
?.associateBy { it.itemId }
?: emptyMap(),
)
is InitFeatureItem -> State0.copy(FeatureItems = State0.FeatureItems + (A.itemId to A.item))
is InitSpecialKitPackage -> State0.copy(SpecialKitPackage = State0.SpecialKitPackage + (A.itemId to A.specialKitPackage))
is InitItemViewType -> State0.copy(ItemViewType = State0.ItemViewType + (A.itemId to A.itemViewType))
is InitCrystalKitPackage -> State0.copy(CrystalKitPackage = State0.CrystalKitPackage + (A.itemId to A.crystalKitPackage))
is InitCoinPackage -> State0.copy(CoinPackage = State0.CoinPackage + (A.itemId to A.coinPackage))
is InitKeysPackage -> State0.copy(KeysPackage = State0.KeysPackage + (A.itemId to A.keysPackage))
is InitShopItemName -> State0.copy(ShopItemName = State0.ShopItemName + (A.itemId to A.shopItemName))
// i5:cg(map, Vd(itemId, amountString)),值取 Pair 的第二个
is InitAmountString -> State0.copy(AmountString = State0.AmountString + (A.itemId to A.amountString))
is InitKitPackage -> State0.copy(KitPackage = State0.KitPackage + (A.itemId to A.kitPackage))
is InitPaymentModeAsShopItem -> State0.copy(PaymentModeAsShopItem = State0.PaymentModeAsShopItem + (A.itemId to A.shopItem))
is InitRestrictionByPayMode -> State0.copy(RestrictionByPayMode = State0.RestrictionByPayMode + (A.itemId to A.paymenModeId))
is InitSingleItemKit -> State0.copy(SingleItemKit = State0.SingleItemKit + (A.itemId to A.singleItemKit))
is InitOneTimePurchaseItem -> {
val Item = A.oneTimePurchaseItem as? OneTimePurchaseItem
if (Item == null) State0
else State0.copy(OneTimePurchaseItems = State0.OneTimePurchaseItems + (A.itemId to Item))
}
is InitCompensation -> State0.copy(CompensationItems = State0.CompensationItems + (A.itemId to A.compensation))
is SetupDiscountItemForItem -> State0.copy(DiscountItems = State0.DiscountItems + (A.itemId to A.discountItem))
is RemoveDiscountItemForItem -> State0.copy(DiscountItems = State0.DiscountItems - A.itemId)
// I3:additionalDescription = new j3(title, description, image)
is InitAdditionalDescription -> State0.copy(
AdditionalDescription = State0.AdditionalDescription +
(A.itemId to AdditionalDescription(A.title, A.description, A.image)),
)
is InitRewardedVideosBlocks -> State0.copy(RewardedVideosBlocks = State0.RewardedVideosBlocks + (A.itemId to A.blocks))
is InitLootBoxAndPaint -> State0.copy(LootBoxAndPaint = State0.LootBoxAndPaint + (A.itemId to A.lootBoxAndPain))
is InitKitBundle -> State0.copy(KitBanner = State0.KitBanner + (A.itemId to A.kitBanner))
// e5:取出已有 OneTimePurchaseItem,copy(isTriedToBuy = true)(原 `v.rt0(!0)`)
is TryToBuyOneTimePurchase -> {
val Item = State0.OneTimePurchaseItems[A.shopItemId]
if (Item == null) State0
else State0.copy(OneTimePurchaseItems = State0.OneTimePurchaseItems + (A.shopItemId to Item.copy(isTriedToBuy = true)))
}
// X3:从 items / featureItems / specialKitPackage / kitBanner / kitPackage / groups 六个表删同一个键
is DeleteOneTimePurchaseItem -> State0.copy(
Items = State0.Items - A.shopItemId,
FeatureItems = State0.FeatureItems - A.shopItemId,
SpecialKitPackage = State0.SpecialKitPackage - A.shopItemId,
KitBanner = State0.KitBanner - A.shopItemId,
KitPackage = State0.KitPackage - A.shopItemId,
Groups = State0.Groups - A.shopItemId,
)
is InitGroup -> State0.copy(Groups = State0.Groups + (A.id to A.group))
else -> State0
}
// ---------------------------------------------------------------- 顶层 reducer
/** 原 `(inline@shop)` 的 32 个分支 + 两个子 reducer。 */
fun ReduceShop(State0: ShopStateSlice, A: Any): ShopStateSlice {
val Base = when (A) {
// w4
is SetCoinPaymentProviderUrl -> State0.copy(CoinPaymentProviderUrl = A.url)
// Yut
is ShopLoaded -> State0.copy(IsLoaded = true)
// F5:复位但保留 coinPaymentProviderUrl
is ResetShop -> ShopStateSlice(CoinPaymentProviderUrl = State0.CoinPaymentProviderUrl)
// h4
is PaymentUrlReceived -> State0.copy(PaymentUrl = A.url)
// c4
is SetBraintreeToken -> State0.copy(BraintreeToken = A.token)
// u4
is SetStripeIntentParams -> State0.copy(StripeClientSecret = A.clientSecret, StripePublishKey = A.publishKey)
// n4
is BraintreeLoadingInterrupt -> State0.copy(BraintreeLoadingInterrupt = A.interrupt)
// g4
is PaymentUrlReceiveFailed -> State0.copy(PaymentUrl = null, ReceivePaymentUrlFailed = true)
// O5
is CurrentPayMode -> State0.copy(SelectedPayModeId = A.payModeId)
// X5
is ResetPaymentState -> State0.copy(
PaymentUrl = null,
ReceivePaymentUrlFailed = false,
MobilePaymentState = MobilePaymentState.NONE,
PaymentProviderState = PaymentProviderState.NONE,
)
// J5
is MobilePaymentStateChanged -> State0.copy(
MobilePaymentState = MobilePaymentState.FromText(A.mobilePaymentState),
)
// a4:payModes = 现有集合 + 单个 payMode(原 `Xg`)
is InitPayModeAction -> {
val Mode = A.payMode as? PayMode
if (Mode == null) State0 else State0.copy(PayModes = State0.PayModes + Mode)
}
// _4:paymentViewType = cg(现有 Map, Km(payModeId, paymentViewType))
is InitPaymentViewType -> State0.copy(
PaymentViewType = State0.PaymentViewType + (A.payModeId to A.paymentViewType),
)
// M5
is SelectItem -> State0.copy(SelectedItemId = A.selectedItemId)
// d5
is SetInitShopItemId -> State0.copy(InitShopItemId = A.shopItemId)
// d4
is ToggleShopActivity -> State0.copy(Enabled = A.enabled)
// w5
is SetGoToHomeAfterBuy -> State0.copy(GoToHomeAfterBuy = true)
// o4 / m4 / k4
is PaymentProviderInProgress -> State0.copy(PaymentProviderState = PaymentProviderState.IN_PROGRESS)
is PaymentProviderFinishSuccess -> State0.copy(PaymentProviderState = PaymentProviderState.FINISH_SUCCESS)
is PaymentProviderFinishFail -> State0.copy(PaymentProviderState = PaymentProviderState.FINISH_FAIL)
// i4 / t4
is BlockBackButton -> State0.copy(IsBlockedBackButton = true)
is UnblockBackButton -> State0.copy(IsBlockedBackButton = false)
// HB:ordersInProcessing[itemId] = Km(itemId, onSuccess)
is AddItemToOrderProcessing -> State0.copy(
OrdersInProcessing = State0.OrdersInProcessing + (A.itemId to (A.itemId to A.onSuccess)),
)
// KU:itemId == null 时原样返回,否则删键
is RemoveItemFromOrderProcessing ->
if (A.itemId == null) State0
else State0.copy(OrdersInProcessing = State0.OrdersInProcessing - A.itemId)
// m5 / f5
is SavePreviousScreen -> State0.copy(PreviousScreen = A.screen)
is ClearPreviousScreen -> State0.copy(PreviousScreen = null)
// k5 / v5
is SaveTargetVisitedCategory -> State0.copy(TargetCategory = A.targetCategory)
is ClearTargetCategory -> State0.copy(TargetCategory = null)
// z5 / p5
is SaveTargetProductId -> State0.copy(TargetProductId = A.targetProductId)
is ClearTargetProductId -> State0.copy(TargetProductId = null)
// g5
is SetPurchaseCompleted -> State0.copy(LastPurchasedItem = A.shopItem)
// `Xd(t, $4) || t instanceof x4`:商店内容动作的快路径,U7/w3 两个子 reducer 在下面统一跑
else -> State0
}
return Base.copy(
ShopCategories = ReduceShopCategories(Base.ShopCategories, A),
ShopItems = ReduceShopItems(Base.ShopItems, A),
)
}
@@ -8,8 +8,9 @@
// twitchState → ReduceTwitch(g9 / y9.gta)
// home → ReduceHome(s1 / 根 reducer 的 `(inline@home)` 分支)
// localBattleUserState → ReduceLocalBattleUser(lE / `(inline@localBattleUserState)` 分支)
// shop → ReduceShop(j4 / `(inline@shop)` 分支)
//
// 其余 84 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 83 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -18,6 +19,7 @@ import App.State.Reducers.ReduceHome
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceLocalBattleUser
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceShop
import App.State.Reducers.ReduceTwitch
/** 根 reducer:`k6`。 */
@@ -38,6 +40,7 @@ object RootReducer {
TwitchState = ReduceTwitch(State0.TwitchState, Action),
Home = ReduceHome(State0.Home, Action),
LocalBattleUserState = ReduceLocalBattleUser(State0.LocalBattleUserState, Action),
Shop = ReduceShop(State0.Shop, Action),
)
}
@@ -8,6 +8,7 @@ import App.State.Reducers.HomeStateSlice
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.LocalBattleUserStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.ShopStateSlice
import App.State.Reducers.TwitchStateSlice
/**
@@ -91,8 +92,8 @@ data class RootState(
val SuppliesState: Any? = null,
/** 原 matchmakingGroup(rW / NW().is0)。未迁移:原样透传。 */
val MatchmakingGroup: Any? = null,
/** 原 shop(j4 / (inline@shop))。未迁移:原样透传。 */
val Shop: Any? = null,
/** 原 shop(j4 / (inline@shop))。已迁移。 */
val Shop: ShopStateSlice = ShopStateSlice(),
/** 原 battlePauseState(dT / NT().mqc)。未迁移:原样透传。 */
val BattlePauseState: Any? = null,
/** 原 battleViewState(DT / sI.jqd)。未迁移:原样透传。 */
@@ -52,7 +52,7 @@ object RootWiring {
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@shop)", "(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("?F5", "?H7", "?X5", "?Yut", "?_5", "?f5", "?g4", "?i4", "?k4", "?m4", "?o4", "?o5", "?p5", "?s5", "?t4", "?v5", "?w5", "?x4", "AddItemToOrderProcessing", "BraintreeLoadingInterrupt", "CurrentPayMode", "DeleteOneTimePurchaseItem", "InitAdditionalDescription", "InitAmountString", "InitCategory", "InitCoinPackage", "InitCompensation", "InitCrystalKitPackage", "InitFeatureItem", "InitGroup", "InitItemViewType", "InitKeysPackage", "InitKitBundle", "InitKitPackage", "InitLootBoxAndPaint", "InitOneTimePurchaseItem", "InitPayModeAction", "InitPaymentProviderItems", "InitPaymentViewType", "InitRestrictionByPayMode", "InitRewardedVideosBlocks", "InitShopCard", "InitShopCardDiscount", "InitShopCardView", "InitShopItem", "InitShopItemName", "InitSingleItemKit", "InitSpecialKitPackage", "MobilePaymentStateChanged", "PaymentUrlReceived", "RemoveItemFromOrderProcessing", "SavePreviousScreen", "SaveTargetProductId", "SaveTargetVisitedCategory", "SelectCategory", "SelectCategoryId", "SelectItem", "SetBraintreeToken", "SetCoinPaymentProviderUrl", "SetInitShopItemId", "SetPurchaseCompleted", "SetStripeIntentParams", "SetupDiscountCategoryByItem", "SetupDiscountItem", "ShopCardExpired", "ShopCardRemoved", "ShopCardSold", "ToggleShopActivity", "TryToBuyOneTimePurchase")),
SliceWiring("shop", "kpw_1", "j4", "(inline@shop)", "(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("?x4", "AddCategoriesWithBonus", "AddItemToOrderProcessing", "BlockBackButton", "BraintreeLoadingInterrupt", "ClearPreviousScreen", "ClearTargetCategory", "ClearTargetProductId", "CurrentPayMode", "DeleteOneTimePurchaseItem", "InitAdditionalDescription", "InitAmountString", "InitCategory", "InitCoinPackage", "InitCompensation", "InitCrystalKitPackage", "InitFeatureItem", "InitGroup", "InitItemViewType", "InitKeysPackage", "InitKitBundle", "InitKitPackage", "InitLootBoxAndPaint", "InitOneTimePurchaseItem", "InitPayModeAction", "InitPaymentModeAsShopItem", "InitPaymentProviderItems", "InitPaymentViewType", "InitRestrictionByPayMode", "InitRewardedVideosBlocks", "InitShopCard", "InitShopCardDiscount", "InitShopCardView", "InitShopItem", "InitShopItemName", "InitSingleItemKit", "InitSpecialKitPackage", "MobilePaymentStateChanged", "PaymentProviderFinishFail", "PaymentProviderFinishSuccess", "PaymentProviderInProgress", "PaymentUrlReceiveFailed", "PaymentUrlReceived", "RemoveDiscountItemForItem", "RemoveItemFromOrderProcessing", "ResetPaymentState", "ResetShop", "SavePreviousScreen", "SaveTargetProductId", "SaveTargetVisitedCategory", "SelectCategory", "SelectCategoryId", "SelectItem", "SetBraintreeToken", "SetCoinPaymentProviderUrl", "SetGoToHomeAfterBuy", "SetInitShopItemId", "SetPurchaseCompleted", "SetStripeIntentParams", "SetupDiscountCategoryByItem", "SetupDiscountItem", "SetupDiscountItemForItem", "ShopCardExpired", "ShopCardRemoved", "ShopCardSold", "ShopLoaded", "ToggleShopActivity", "TryToBuyOneTimePurchase", "UnblockBackButton")),
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")),
@@ -106,7 +106,7 @@ object RootWiring {
)
/** 已迁移(由 RootReducer 真正归约)的字段。 */
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState")
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -9,7 +9,8 @@
// ⑥ 动作目录:816 个类与 138 个真实派发名对齐
// ⑦ 回放:真实 toString 样例的类/字段/类型校验
// ⑧ 全目录动作类都能按 KClass 路由
// ⑨~⑭ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState;⑭ 根 reducer 的 89 槽位委派
// ⑨~⑮ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer);⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
package App
@@ -42,6 +43,77 @@ import App.State.RootReducer
import App.State.RootState
import App.State.RootWiring
import App.State.IncorrectLogin2FA
import App.State.AddItemToOrderProcessing
import App.State.AdditionalDescription
import App.State.BraintreeLoadingInterrupt
import App.State.Category
import App.State.CurrentPayMode
import App.State.DeleteOneTimePurchaseItem
import App.State.DiscountItem
import App.State.InitAdditionalDescription
import App.State.InitAmountString
import App.State.InitCategory
import App.State.InitGroup
import App.State.InitOneTimePurchaseItem
import App.State.InitPayModeAction
import App.State.InitPaymentProviderItems
import App.State.InitPaymentViewType
import App.State.InitShopCard
import App.State.InitShopCardDiscount
import App.State.InitShopCardView
import App.State.InitShopItem
import App.State.InitShopItemName
import App.State.MobilePaymentStateChanged
import App.State.OneTimePurchaseItem
import App.State.PayMode
import App.State.PaymentProviderItem
import App.State.RemoveItemFromOrderProcessing
import App.State.SavePreviousScreen
import App.State.SaveTargetProductId
import App.State.SaveTargetVisitedCategory
import App.State.SelectCategoryId
import App.State.SelectItem
import App.State.SetBraintreeToken
import App.State.SetCoinPaymentProviderUrl
import App.State.SetInitShopItemId
import App.State.SetPurchaseCompleted
import App.State.SetStripeIntentParams
import App.State.SetupDiscountCategoryByItem
import App.State.SetupDiscountItem
import App.State.ShopCard
import App.State.ShopCardExpired
import App.State.ShopCardRemoved
import App.State.ShopCardSold
import App.State.ShopItem
import App.State.ToggleShopActivity
import App.State.TryToBuyOneTimePurchase
import App.State.Reducers.AddCategoriesWithBonus
import App.State.Reducers.BlockBackButton
import App.State.Reducers.CategoryType
import App.State.Reducers.ClearPreviousScreen
import App.State.Reducers.ClearTargetCategory
import App.State.Reducers.ClearTargetProductId
import App.State.Reducers.InitPaymentModeAsShopItem
import App.State.Reducers.MobilePaymentState
import App.State.Reducers.PaymentProviderFinishFail
import App.State.Reducers.PaymentProviderFinishSuccess
import App.State.Reducers.PaymentProviderInProgress
import App.State.Reducers.PaymentProviderState
import App.State.Reducers.PaymentUrlReceiveFailed
import App.State.Reducers.ReduceShop
import App.State.Reducers.ReduceShopCategories
import App.State.Reducers.ReduceShopItems
import App.State.Reducers.RemoveDiscountItemForItem
import App.State.Reducers.ResetPaymentState
import App.State.Reducers.ResetShop
import App.State.Reducers.SelectShopCategory
import App.State.Reducers.SetGoToHomeAfterBuy
import App.State.Reducers.SetupDiscountItemForItem
import App.State.Reducers.ShopCategoriesState
import App.State.Reducers.ShopItemsState
import App.State.Reducers.ShopLoaded
import App.State.Reducers.ShopStateSlice
import App.State.Reducers.UnblockBackButton
import App.State.RequestsNumber2FA
import App.State.SetAccount
import App.State.SetDaysForNextLinking
@@ -427,7 +499,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 五个已迁移切片一次归约
// ⑭ 根 reducer:89 个槽位的委派表 + 六个已迁移切片一次归约
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -491,10 +563,10 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 五个已迁移字段,其余 84 个待迁",
"根:迁移表 = 六个已迁移字段,其余 83 个待迁",
RootReducer.Migrated ==
setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState") &&
RootWiring.Unmigrated.size == 84
setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop") &&
RootWiring.Unmigrated.size == 83
)
// 一次 ReduceRoot 里五个切片各归约自己的动作,互不串片
@@ -550,7 +622,7 @@ object StateCheck {
S5.User == "user-1" && S5.Screen == "Preloader"
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 5/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 6/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -564,6 +636,261 @@ object StateCheck {
)
}
// ⑮ 切片 reducer:Shop(原 module_10032 的 j4 + `(inline@shop)`;U7 类别 7 字段、w3 物品 23 字段)
run {
val S0 = ShopStateSlice()
Check(
"切片 Shop:默认值与原构造一致(enabled 缺省 true、previousScreen 缺省 null)",
S0.ShopCategories == ShopCategoriesState() && S0.ShopItems == ShopItemsState() &&
S0.InitShopItemId == null && S0.SelectedItemId == null && !S0.IsLoaded &&
S0.ItemForPurchase == null && S0.Enabled && S0.CoinPaymentProviderUrl == null &&
S0.PayModes.isEmpty() && S0.SelectedPayModeId == null && S0.PaymentViewType.isEmpty() &&
S0.PaymentUrl == null && !S0.ReceivePaymentUrlFailed && S0.MobileCountryCode == "+7 " &&
S0.MobilePhoneLength == 13 && S0.MobilePaymentState == MobilePaymentState.NONE &&
S0.PaymentProviderState == PaymentProviderState.NONE && S0.AsiassoftAmountInAtc == 0 &&
S0.AsiassoftEnoughMoney == null && S0.BraintreeToken == null && !S0.IsBlockedBackButton &&
!S0.BraintreeLoadingInterrupt && !S0.GoToHomeAfterBuy && S0.PreviousScreen == null &&
S0.TargetCategory == null && S0.TargetProductId == null && S0.LastPurchasedItem == null &&
S0.OrdersInProcessing.isEmpty()
)
val S1 = ReduceShop(S0, SetCoinPaymentProviderUrl("https://pay"))
val S2 = ReduceShop(S1, ShopLoaded())
Check("切片 Shop:SetCoinPaymentProviderUrl / ShopLoaded 各改本字段", S1.CoinPaymentProviderUrl == "https://pay" && S2.IsLoaded)
// F5:复位但保留 coinPaymentProviderUrl
val S3 = ReduceShop(S2.copy(PaymentUrl = "u", MobilePaymentState = MobilePaymentState.PAYMENT_INITED), ResetShop())
Check(
"切片 Shop:ResetShop 复位其余字段但保留 coinPaymentProviderUrl",
S3.CoinPaymentProviderUrl == "https://pay" && S3.PaymentUrl == null && !S3.IsLoaded &&
S3.MobilePaymentState == MobilePaymentState.NONE
)
val S4 = ReduceShop(S3, PaymentUrlReceiveFailed())
val S5 = ReduceShop(S4, ResetPaymentState())
Check(
"切片 Shop:支付失败后 ResetPaymentState 清 url/失败位/两个状态",
S4.PaymentUrl == null && S4.ReceivePaymentUrlFailed &&
!S5.ReceivePaymentUrlFailed && S5.MobilePaymentState == MobilePaymentState.NONE &&
S5.PaymentProviderState == PaymentProviderState.NONE
)
val S6 = ReduceShop(S5, MobilePaymentStateChanged("PHONE_VALID"))
Check("切片 Shop:MobilePaymentStateChanged 按枚举名解析", S6.MobilePaymentState == MobilePaymentState.PHONE_VALID)
Check(
"切片 Shop:三种 provider 状态各自落到枚举",
ReduceShop(S6, PaymentProviderInProgress()).PaymentProviderState == PaymentProviderState.IN_PROGRESS &&
ReduceShop(S6, PaymentProviderFinishSuccess()).PaymentProviderState == PaymentProviderState.FINISH_SUCCESS &&
ReduceShop(S6, PaymentProviderFinishFail()).PaymentProviderState == PaymentProviderState.FINISH_FAIL
)
val Mode = PayMode(3L, "card", "desc", 1, 0L, "")
val S7 = ReduceShop(S6, InitPayModeAction(Mode))
val S8 = ReduceShop(S7, InitPaymentViewType(3L, "BANK_CARD"))
Check(
"切片 Shop:InitPayModeAction 加集合项、InitPaymentViewType 按 payModeId 建表",
S8.PayModes.contains(Mode) && S8.PaymentViewType[3L] == "BANK_CARD"
)
val S9 = ReduceShop(S8, AddItemToOrderProcessing(11L, "ok"))
Check(
"切片 Shop:AddItemToOrderProcessing 写 itemId → (itemId, onSuccess)",
S9.OrdersInProcessing[11L] == (11L to "ok")
)
Check(
"切片 Shop:RemoveItemFromOrderProcessing itemId=null 时原样返回",
ReduceShop(S9, RemoveItemFromOrderProcessing(null, false)) == S9
)
Check(
"切片 Shop:RemoveItemFromOrderProcessing 删键",
ReduceShop(S9, RemoveItemFromOrderProcessing(11L, false)).OrdersInProcessing.isEmpty()
)
val S10 = ReduceShop(S9, SavePreviousScreen("Garage"))
val S11 = ReduceShop(S10, SaveTargetVisitedCategory("CRYSTALS"))
val S12 = ReduceShop(S11, SaveTargetProductId(77L))
Check(
"切片 Shop:SavePreviousScreen / SaveTargetVisitedCategory / SaveTargetProductId",
S12.PreviousScreen == "Garage" && S12.TargetCategory == "CRYSTALS" && S12.TargetProductId == 77L
)
Check(
"切片 Shop:ClearPreviousScreen / ClearTargetCategory / ClearTargetProductId 置空",
ReduceShop(S12, ClearPreviousScreen()).PreviousScreen == null &&
ReduceShop(S12, ClearTargetCategory()).TargetCategory == null &&
ReduceShop(S12, ClearTargetProductId()).TargetProductId == null
)
Check(
"切片 Shop:ToggleShopActivity / SelectItem / SetInitShopItemId 各改本字段",
!ReduceShop(S12, ToggleShopActivity(false)).Enabled &&
ReduceShop(S12, SelectItem(42L, true)).SelectedItemId == 42L &&
ReduceShop(S12, SetInitShopItemId(7L)).InitShopItemId == 7L
)
Check(
"切片 Shop:BlockBackButton / UnblockBackButton 切换返回键锁",
ReduceShop(S12, BlockBackButton()).IsBlockedBackButton &&
!ReduceShop(S12, UnblockBackButton()).IsBlockedBackButton &&
ReduceShop(S12, SetGoToHomeAfterBuy()).GoToHomeAfterBuy
)
// U7:类别表 7 字段 + 5 臂
val C = Category(
id = 1L,
orderIndex = 0,
name = "Crystals",
description = "desc",
type = "CRYSTALS",
preview = null,
isSpecial = false,
icon = null,
categoryBackground = null,
cardHoverBackground = 0L,
confirmPurchaseForCrystals = false,
canBeShownWhenUserIsInDebt = false,
showTimerOnCard = false,
)
val C1 = ReduceShopCategories(ShopCategoriesState(), InitCategory(C))
Check(
"切片 Shop/Categories:InitCategory 同时写 categories / type→id / id→type",
C1.Categories[1L] == C && C1.CategoryTypeToId[CategoryType.CRYSTALS] == 1L &&
C1.CategoryIdToType[1L] == CategoryType.CRYSTALS
)
val C2 = ReduceShopCategories(C1, SetupDiscountCategoryByItem(1L))
val C3 = ReduceShopCategories(C2, AddCategoriesWithBonus(setOf(2L, 3L)))
Check(
"切片 Shop/Categories:折扣集合加单个 id、奖励集合并入多个 id",
C3.CategoriesWithDiscount == setOf<Any?>(1L) && C3.CategoriesWithBonus == setOf<Any?>(2L, 3L)
)
val C4 = ReduceShopCategories(C3, SelectShopCategory("COINS"))
val C5 = ReduceShopCategories(C4, SelectCategoryId(9L))
Check(
"切片 Shop/Categories:SelectShopCategory 解析枚举、SelectCategoryId 写 id",
C5.SelectedCategoryType == CategoryType.COINS && C5.SelectedCategoryId == 9L
)
// w3:物品表 23 字段 + 32 臂(抽 12 臂覆盖增删改)
val Item = ShopItem(
id = 1L,
gameObject = null,
categoryId = 1L,
orderIndex = 0,
price = 100,
originalPrice = 100.0,
currencyName = "coin",
currencyId = 1L,
roundingPrecision = 0,
previewImageURL = 0L,
isSellingForCoin = true,
isSellingForCrystal = false,
isSellingForRuby = false,
isSellingForMoney = false,
isSellingForADS = false,
needPaymentProviderItem = false,
)
val I1 = ReduceShopItems(ShopItemsState(), InitShopItem(1L, Item))
Check("切片 Shop/Items:InitShopItem 写 items", I1.Items[1L] == Item)
val Card = ShopCard(
activeUntil = 100L,
goodInfo = null,
name = "card",
sold = false,
pinned = false,
type = 0,
view = null,
discount = null,
sortIndex = 1,
shopCategoryId = 1L,
)
val I2 = ReduceShopItems(I1, InitShopCard(2L, Card, "goods", 1L))
Check(
"切片 Shop/Items:InitShopCard 重建卡片(goodInfo=goods、view/discount 置空)",
I2.ShopCards[2L]?.name == "card" && I2.ShopCards[2L]?.goodInfo == "goods" &&
I2.ShopCards[2L]?.view == null && I2.ShopCards[2L]?.discount == null
)
val Discount = DiscountItem(90, 10, 5)
val I3 = ReduceShopItems(I2, InitShopCardView(2L, "view"))
val I4 = ReduceShopItems(I3, InitShopCardDiscount(2L, Discount))
Check(
"切片 Shop/Items:InitShopCardView / InitShopCardDiscount 只改对应字段",
I4.ShopCards[2L]?.view == "view" && I4.ShopCards[2L]?.discount == Discount
)
val I5 = ReduceShopItems(I4, ShopCardSold(2L))
val I6 = ReduceShopItems(I5, ShopCardExpired(2L))
Check(
"切片 Shop/Items:ShopCardSold 置 sold、ShopCardExpired 把 activeUntil 归零",
I6.ShopCards[2L]?.sold == true && I6.ShopCards[2L]?.activeUntil == 0L
)
Check("切片 Shop/Items:ShopCardRemoved 删卡片", ReduceShopItems(I6, ShopCardRemoved(2L)).ShopCards.isEmpty())
val I7 = ReduceShopItems(I6, SetupDiscountItem(1L, Discount))
Check("切片 Shop/Items:SetupDiscountItem 写 discountItems", I7.DiscountItems[1L] == Discount)
val I8 = ReduceShopItems(I7, SetupDiscountItemForItem(4L, Discount))
Check(
"切片 Shop/Items:o5 同族动作写/删 discountItems",
I8.DiscountItems[4L] == Discount &&
ReduceShopItems(I8, RemoveDiscountItemForItem(4L)).DiscountItems[4L] == null
)
val ProviderItem = PaymentProviderItem(5L, 1.0, "USD", "US", "name", "desc", "1 USD")
val I9 = ReduceShopItems(I8, InitPaymentProviderItems(listOf(ProviderItem)))
Check(
"切片 Shop/Items:InitPaymentProviderItems 按 itemId 建表",
I9.PaymentProviderItems[5L] == ProviderItem
)
val I10 = ReduceShopItems(I9, InitAdditionalDescription(1L, "title", "desc", 0L))
Check(
"切片 Shop/Items:InitAdditionalDescription 组装 AdditionalDescription",
I10.AdditionalDescription[1L] == AdditionalDescription("title", "desc", 0L)
)
val I11 = ReduceShopItems(I10, InitOneTimePurchaseItem(3L, OneTimePurchaseItem(false, 1)))
val I12 = ReduceShopItems(I11, TryToBuyOneTimePurchase(3L))
Check(
"切片 Shop/Items:TryToBuyOneTimePurchase 复制已有项并置 isTriedToBuy",
I12.OneTimePurchaseItems[3L]?.isTriedToBuy == true
)
val I13 = ReduceShopItems(I12, InitGroup(4L, 2))
Check("切片 Shop/Items:InitGroup 写 groups", I13.Groups[4L] == 2)
Check(
"切片 Shop/Items:InitAmountString / InitShopItemName 写各自表",
ReduceShopItems(I13, InitAmountString(1L, "100")).AmountString[1L] == "100" &&
ReduceShopItems(I13, InitShopItemName(1L, "name")).ShopItemName[1L] == "name"
)
val I14 = ReduceShopItems(I13, DeleteOneTimePurchaseItem(1L))
Check(
"切片 Shop/Items:DeleteOneTimePurchaseItem 从六个表删同一键",
I14.Items[1L] == null && I14.FeatureItems[1L] == null && I14.SpecialKitPackage[1L] == null &&
I14.KitBanner[1L] == null && I14.KitPackage[1L] == null && I14.Groups[1L] == null
)
Check("切片 Shop:无关动作不改状态", ReduceShop(S12, AddFrags(1)) == S12)
// 根:一次 ReduceRoot 里商店动作只动 shop
val R0 = RootState()
val R1 = ReduceRoot(R0, InitShopItem(1L, Item))
Check(
"根:InitShopItem 只动 shop",
R1.Shop.ShopItems.Items[1L] == Item && R1.User == null && R1.Home == R0.Home
)
val R2 = ReduceRoot(R1, InitCategory(C))
Check(
"根:InitCategory 与 InitShopItem 在同一片的两张子表里共存",
R2.Shop.ShopCategories.Categories[1L] == C && R2.Shop.ShopItems.Items[1L] == Item
)
val R3 = ReduceRoot(R2, SetCoinPaymentProviderUrl("https://pay"))
Check(
"根:SetCoinPaymentProviderUrl 只动 shop 顶层字段",
R3.Shop.CoinPaymentProviderUrl == "https://pay" &&
R3.Shop.ShopItems == R2.Shop.ShopItems && R3.LoginByPassword == R0.LoginByPassword
)
Check("根:shop 迁移后无关动作仍返回等值状态", ReduceRoot(R3, AddFrags(1)) == R3)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)