feat(state): 全量迁移商店切片(69 个动作 + U7/w3 子状态)
This commit is contained in:
1 parent
86d79eddad
commit
94374d2f36
10 files changed
+2929
-37
No files matched your search
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user