From 94374d2f3610b716cbee08bfedac019be9e388c3 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 23 Oct 2026 02:13:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E5=85=A8=E9=87=8F=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=95=86=E5=BA=97=E5=88=87=E7=89=87=EF=BC=8869=20?= =?UTF-8?q?=E4=B8=AA=E5=8A=A8=E4=BD=9C=20+=20U7/w3=20=E5=AD=90=E7=8A=B6?= =?UTF-8?q?=E6=80=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 15 +- 3DTank/names/root_wiring.json | 157 +- 3DTank/names/slice_shop.json | 1406 +++++++++++++++++ 3DTank/tools/gen_root_wiring.js | 59 + 3DTank/tools/gen_slice_plan.js | 462 ++++++ .../kotlin/App/State/Reducers/ShopReducer.kt | 514 ++++++ .../jsMain/kotlin/App/State/RootReducer.kt | 5 +- .../src/jsMain/kotlin/App/State/RootState.kt | 5 +- .../src/jsMain/kotlin/App/State/RootWiring.kt | 4 +- .../app/src/jsMain/kotlin/App/StateCheck.kt | 339 +++- 10 files changed, 2929 insertions(+), 37 deletions(-) create mode 100644 3DTank/names/slice_shop.json create mode 100644 3DTank/tools/gen_slice_plan.js create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ShopReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 88224f2..df93775 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -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@)` 并从函数体抓到顶层动作名(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(输入) diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 4552e0e..2d1d74e 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -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", diff --git a/3DTank/names/slice_shop.json b/3DTank/names/slice_shop.json new file mode 100644 index 0000000..e287f63 --- /dev/null +++ b/3DTank/names/slice_shop.json @@ -0,0 +1,1406 @@ +{ + "generatedAt": "2026-10-22T18:12:16.800Z", + "module": "module_10032.js", + "Field": "shop", + "Slot": "kpw_1", + "Class": "j4", + "ClassSource": "new j4()", + "Params": [ + "t", + "n", + "i", + "r", + "e", + "u", + "s", + "o", + "_", + "h", + "a", + "c", + "l", + "f", + "v", + "p", + "w", + "d", + "g", + "m", + "k", + "z", + "b", + "q", + "y", + "$", + "x", + "j", + "S", + "C" + ], + "Fields": [ + { + "Field": "shopCategories", + "Slot": "jq3_1", + "Kotlin": "ShopCategories", + "Default": "new U7()" + }, + { + "Field": "shopItems", + "Slot": "kq3_1", + "Kotlin": "ShopItems", + "Default": "new w3()" + }, + { + "Field": "initShopItemId", + "Slot": "lq3_1", + "Kotlin": "InitShopItemId", + "Default": "null" + }, + { + "Field": "selectedItemId", + "Slot": "mq3_1", + "Kotlin": "SelectedItemId", + "Default": "null" + }, + { + "Field": "isLoaded", + "Slot": "nq3_1", + "Kotlin": "IsLoaded", + "Default": "false" + }, + { + "Field": "itemForPurchase", + "Slot": "oq3_1", + "Kotlin": "ItemForPurchase", + "Default": "null" + }, + { + "Field": "enabled", + "Slot": "pq3_1", + "Kotlin": "Enabled", + "Default": "true" + }, + { + "Field": "coinPaymentProviderUrl", + "Slot": "qq3_1", + "Kotlin": "CoinPaymentProviderUrl", + "Default": "null" + }, + { + "Field": "payModes", + "Slot": "rq3_1", + "Kotlin": "PayModes", + "Default": "Yg()" + }, + { + "Field": "selectedPayModeId", + "Slot": "sq3_1", + "Kotlin": "SelectedPayModeId", + "Default": "null" + }, + { + "Field": "paymentViewType", + "Slot": "tq3_1", + "Kotlin": "PaymentViewType", + "Default": "ag()" + }, + { + "Field": "paymentUrl", + "Slot": "uq3_1", + "Kotlin": "PaymentUrl", + "Default": "null" + }, + { + "Field": "receivePaymentUrlFailed", + "Slot": "vq3_1", + "Kotlin": "ReceivePaymentUrlFailed", + "Default": "false" + }, + { + "Field": "mobileCountryCode", + "Slot": "wq3_1", + "Kotlin": "MobileCountryCode", + "Default": "\"+7 \"" + }, + { + "Field": "mobilePhoneLength", + "Slot": "xq3_1", + "Kotlin": "MobilePhoneLength", + "Default": "13" + }, + { + "Field": "mobilePaymentState", + "Slot": "yq3_1", + "Kotlin": "MobilePaymentState", + "Default": "b4()" + }, + { + "Field": "paymentProviderState", + "Slot": "zq3_1", + "Kotlin": "PaymentProviderState", + "Default": "q4()" + }, + { + "Field": "asiassoftAmountInAtc", + "Slot": "aq4_1", + "Kotlin": "AsiassoftAmountInAtc", + "Default": "0" + }, + { + "Field": "asiassoftEnoughMoney", + "Slot": "bq4_1", + "Kotlin": "AsiassoftEnoughMoney", + "Default": "null" + }, + { + "Field": "braintreeToken", + "Slot": "cq4_1", + "Kotlin": "BraintreeToken", + "Default": "null" + }, + { + "Field": "stripeClientSecret", + "Slot": "dq4_1", + "Kotlin": "StripeClientSecret", + "Default": "null" + }, + { + "Field": "stripePublishKey", + "Slot": "eq4_1", + "Kotlin": "StripePublishKey", + "Default": "null" + }, + { + "Field": "isBlockedBackButton", + "Slot": "fq4_1", + "Kotlin": "IsBlockedBackButton", + "Default": "false" + }, + { + "Field": "braintreeLoadingInterrupt", + "Slot": "gq4_1", + "Kotlin": "BraintreeLoadingInterrupt", + "Default": "false" + }, + { + "Field": "goToHomeAfterBuy", + "Slot": "hq4_1", + "Kotlin": "GoToHomeAfterBuy", + "Default": "false" + }, + { + "Field": "previousScreen", + "Slot": "iq4_1", + "Kotlin": "PreviousScreen", + "Default": "null" + }, + { + "Field": "targetCategory", + "Slot": "jq4_1", + "Kotlin": "TargetCategory", + "Default": "null" + }, + { + "Field": "targetProductId", + "Slot": "kq4_1", + "Kotlin": "TargetProductId", + "Default": "null" + }, + { + "Field": "lastPurchasedItem", + "Slot": "lq4_1", + "Kotlin": "LastPurchasedItem", + "Default": "null" + }, + { + "Field": "ordersInProcessing", + "Slot": "mq4_1", + "Kotlin": "OrdersInProcessing", + "Default": "ag()" + } + ], + "CopyMethod": "qt6", + "Branches": [ + { + "Guard": "t instanceof w4", + "Locals": [ + "w4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 7, + "Field": "coinPaymentProviderUrl", + "Slot": "qq3_1", + "Kotlin": "CoinPaymentProviderUrl", + "Value": "t.lt6_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, t.lt6_1)" + }, + { + "Guard": "t instanceof Yut", + "Locals": [ + "Yut" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 4, + "Field": "isLoaded", + "Slot": "nq3_1", + "Kotlin": "IsLoaded", + "Value": "!0" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, !0)" + }, + { + "Guard": "t instanceof F5", + "Locals": [ + "F5" + ], + "Kind": "reset", + "Changed": [], + "Expr": "new j4(cd, cd, cd, cd, cd, cd, cd, n.qq3_1)" + }, + { + "Guard": "t instanceof h4", + "Locals": [ + "h4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 11, + "Field": "paymentUrl", + "Slot": "uq3_1", + "Kotlin": "PaymentUrl", + "Value": "t.st5_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.st5_1)" + }, + { + "Guard": "t instanceof c4", + "Locals": [ + "c4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 19, + "Field": "braintreeToken", + "Slot": "cq4_1", + "Kotlin": "BraintreeToken", + "Value": "t.ut5_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ut5_1)" + }, + { + "Guard": "t instanceof u4", + "Locals": [ + "u4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 20, + "Field": "stripeClientSecret", + "Slot": "dq4_1", + "Kotlin": "StripeClientSecret", + "Value": "t.mt5_1" + }, + { + "Index": 21, + "Field": "stripePublishKey", + "Slot": "eq4_1", + "Kotlin": "StripePublishKey", + "Value": "t.nt5_1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.mt5_1, t.nt5_1, )" + }, + { + "Guard": "t instanceof n4", + "Locals": [ + "n4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 23, + "Field": "braintreeLoadingInterrupt", + "Slot": "gq4_1", + "Kotlin": "BraintreeLoadingInterrupt", + "Value": "t.at5_1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.at5_1, )" + }, + { + "Guard": "t instanceof g4", + "Locals": [ + "g4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 11, + "Field": "paymentUrl", + "Slot": "uq3_1", + "Kotlin": "PaymentUrl", + "Value": "null" + }, + { + "Index": 12, + "Field": "receivePaymentUrlFailed", + "Slot": "vq3_1", + "Kotlin": "ReceivePaymentUrlFailed", + "Value": "!0" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null, !0)" + }, + { + "Guard": "t instanceof O5", + "Locals": [ + "O5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 9, + "Field": "selectedPayModeId", + "Slot": "sq3_1", + "Kotlin": "SelectedPayModeId", + "Value": "t.mt4_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, t.mt4_1)" + }, + { + "Guard": "t instanceof X5", + "Locals": [ + "X5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 11, + "Field": "paymentUrl", + "Slot": "uq3_1", + "Kotlin": "PaymentUrl", + "Value": "null" + }, + { + "Index": 12, + "Field": "receivePaymentUrlFailed", + "Slot": "vq3_1", + "Kotlin": "ReceivePaymentUrlFailed", + "Value": "!1" + }, + { + "Index": 15, + "Field": "mobilePaymentState", + "Slot": "yq3_1", + "Kotlin": "MobilePaymentState", + "Value": "r" + }, + { + "Index": 16, + "Field": "paymentProviderState", + "Slot": "zq3_1", + "Kotlin": "PaymentProviderState", + "Value": "e" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null, !1, cd, cd, r, e)" + }, + { + "Guard": "t instanceof J5", + "Locals": [ + "J5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 15, + "Field": "mobilePaymentState", + "Slot": "yq3_1", + "Kotlin": "MobilePaymentState", + "Value": "t.st4_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.st4_1)" + }, + { + "Guard": "t instanceof a4", + "Locals": [ + "a4" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 8, + "Field": "payModes", + "Slot": "rq3_1", + "Kotlin": "PayModes", + "Value": "Xg(n.rq3_1, t.tt5_1)" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, Xg(n.rq3_1, t.tt5_1))" + }, + { + "Guard": "t instanceof _4", + "Locals": [ + "_4" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 10, + "Field": "paymentViewType", + "Slot": "tq3_1", + "Kotlin": "PaymentViewType", + "Value": "cg(n.tq3_1, new Km(t.qt5_1, t.rt5_1))" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.tq3_1, new Km(t.qt5_1, t.rt5_1)))" + }, + { + "Guard": "t instanceof M5", + "Locals": [ + "M5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 3, + "Field": "selectedItemId", + "Slot": "mq3_1", + "Kotlin": "SelectedItemId", + "Value": "t.kt4_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, t.kt4_1)" + }, + { + "Guard": "t instanceof d5", + "Locals": [ + "d5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 2, + "Field": "initShopItemId", + "Slot": "lq3_1", + "Kotlin": "InitShopItemId", + "Value": "t.nt3_1" + } + ], + "Expr": "n.qt6(cd, cd, t.nt3_1)" + }, + { + "Guard": "t instanceof d4", + "Locals": [ + "d4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 6, + "Field": "enabled", + "Slot": "pq3_1", + "Kotlin": "Enabled", + "Value": "t.mt6_1" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, t.mt6_1)" + }, + { + "Guard": "Xd(t, $4) || t instanceof x4", + "Locals": [ + "x4", + "$4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 0, + "Field": "shopCategories", + "Slot": "jq3_1", + "Kotlin": "ShopCategories", + "Value": "s" + }, + { + "Index": 1, + "Field": "shopItems", + "Slot": "kq3_1", + "Kotlin": "ShopItems", + "Value": "u" + } + ], + "Expr": "n.qt6(s, u)" + }, + { + "Guard": "t instanceof w5", + "Locals": [ + "w5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 24, + "Field": "goToHomeAfterBuy", + "Slot": "hq4_1", + "Kotlin": "GoToHomeAfterBuy", + "Value": "!0" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, !0, )" + }, + { + "Guard": "t instanceof o4", + "Locals": [ + "o4" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 16, + "Field": "paymentProviderState", + "Slot": "zq3_1", + "Kotlin": "PaymentProviderState", + "Value": "(V5(), Rl)" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, (V5(), Rl))" + }, + { + "Guard": "t instanceof m4", + "Locals": [ + "m4" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 16, + "Field": "paymentProviderState", + "Slot": "zq3_1", + "Kotlin": "PaymentProviderState", + "Value": "y4()" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, y4())" + }, + { + "Guard": "t instanceof k4", + "Locals": [ + "k4" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 16, + "Field": "paymentProviderState", + "Slot": "zq3_1", + "Kotlin": "PaymentProviderState", + "Value": "(V5(), Nl)" + } + ], + "Expr": "n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, (V5(), Nl))" + }, + { + "Guard": "t instanceof i4", + "Locals": [ + "i4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 22, + "Field": "isBlockedBackButton", + "Slot": "fq4_1", + "Kotlin": "IsBlockedBackButton", + "Value": "!0" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, !0, )" + }, + { + "Guard": "t instanceof t4", + "Locals": [ + "t4" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 22, + "Field": "isBlockedBackButton", + "Slot": "fq4_1", + "Kotlin": "IsBlockedBackButton", + "Value": "!1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, !1, )" + }, + { + "Guard": "t instanceof HB", + "Locals": [ + "HB" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 29, + "Field": "ordersInProcessing", + "Slot": "mq4_1", + "Kotlin": "OrdersInProcessing", + "Value": "cg(n.mq4_1, Vd(t.iri_1, t.jri_1))" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.mq4_1, Vd(t.iri_1, t.jri_1)), )" + }, + { + "Guard": "t instanceof KU", + "Locals": [ + "KU" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 29, + "Field": "ordersInProcessing", + "Slot": "mq4_1", + "Kotlin": "OrdersInProcessing", + "Value": "am(n.mq4_1, o)" + } + ], + "Expr": "null == o ? n : n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, am(n.mq4_1, o), )" + }, + { + "Guard": "t instanceof m5", + "Locals": [ + "m5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 25, + "Field": "previousScreen", + "Slot": "iq4_1", + "Kotlin": "PreviousScreen", + "Value": "t.pt3_1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.pt3_1, )" + }, + { + "Guard": "t instanceof f5", + "Locals": [ + "f5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 25, + "Field": "previousScreen", + "Slot": "iq4_1", + "Kotlin": "PreviousScreen", + "Value": "null" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null, )" + }, + { + "Guard": "t instanceof k5", + "Locals": [ + "k5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 26, + "Field": "targetCategory", + "Slot": "jq4_1", + "Kotlin": "TargetCategory", + "Value": "t.qt3_1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.qt3_1, )" + }, + { + "Guard": "t instanceof v5", + "Locals": [ + "v5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 26, + "Field": "targetCategory", + "Slot": "jq4_1", + "Kotlin": "TargetCategory", + "Value": "null" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null, )" + }, + { + "Guard": "t instanceof z5", + "Locals": [ + "z5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 27, + "Field": "targetProductId", + "Slot": "kq4_1", + "Kotlin": "TargetProductId", + "Value": "t.rt3_1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.rt3_1, )" + }, + { + "Guard": "t instanceof p5", + "Locals": [ + "p5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 27, + "Field": "targetProductId", + "Slot": "kq4_1", + "Kotlin": "TargetProductId", + "Value": "null" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null, )" + }, + { + "Guard": "t instanceof g5", + "Locals": [ + "g5" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 28, + "Field": "lastPurchasedItem", + "Slot": "lq4_1", + "Kotlin": "LastPurchasedItem", + "Value": "t.ot3_1" + } + ], + "Expr": "n.qt6( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ot3_1, ) : n" + } + ], + "SubReducers": [ + { + "Var": "u", + "Arg": "n.kq3_1", + "Style": "chain", + "ArmCount": 32, + "Locals": [ + { + "Local": "F3", + "Name": "InitShopItem" + }, + { + "Local": "J3", + "Name": "InitShopCard" + }, + { + "Local": "Q3", + "Name": "InitShopCardDiscount" + }, + { + "Local": "Z3", + "Name": "InitShopCardView" + }, + { + "Local": "n5", + "Name": "ShopCardSold" + }, + { + "Local": "Y3", + "Name": "ShopCardExpired" + }, + { + "Local": "t5", + "Name": "ShopCardRemoved" + }, + { + "Local": "C3", + "Name": "SetupDiscountItem" + }, + { + "Local": "r5", + "Name": "InitPaymentProviderItems" + }, + { + "Local": "A3", + "Name": "InitFeatureItem" + }, + { + "Local": "R3", + "Name": "InitSpecialKitPackage" + }, + { + "Local": "T3", + "Name": "InitItemViewType" + }, + { + "Local": "E3", + "Name": "InitCrystalKitPackage" + }, + { + "Local": "V3", + "Name": "InitCoinPackage" + }, + { + "Local": "K3", + "Name": "InitKeysPackage" + }, + { + "Local": "D3", + "Name": "InitShopItemName" + }, + { + "Local": "i5", + "Name": "InitAmountString" + }, + { + "Local": "O3", + "Name": "InitKitPackage" + }, + { + "Local": "s5", + "Name": null + }, + { + "Local": "P3", + "Name": "InitRestrictionByPayMode" + }, + { + "Local": "N3", + "Name": "InitSingleItemKit" + }, + { + "Local": "L3", + "Name": "InitOneTimePurchaseItem" + }, + { + "Local": "M3", + "Name": "InitCompensation" + }, + { + "Local": "o5", + "Name": null + }, + { + "Local": "_5", + "Name": null + }, + { + "Local": "I3", + "Name": "InitAdditionalDescription" + }, + { + "Local": "W3", + "Name": "InitRewardedVideosBlocks" + }, + { + "Local": "H3", + "Name": "InitLootBoxAndPaint" + }, + { + "Local": "U3", + "Name": "InitKitBundle" + }, + { + "Local": "e5", + "Name": "TryToBuyOneTimePurchase" + }, + { + "Local": "X3", + "Name": "DeleteOneTimePurchaseItem" + }, + { + "Local": "B3", + "Name": "InitGroup" + } + ], + "Arms": [ + { + "Local": "F3", + "Guard": "(h5(), t instanceof F3)", + "Kind": "reset", + "Expr": "n.osz(cg(n.mq2_1, new Km(t.zt1_1, t.at2_1)))" + }, + { + "Local": "J3", + "Guard": "t instanceof J3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.hq3_1, new Km(t.ot2_1, r)), )" + }, + { + "Local": "Q3", + "Guard": "t instanceof Q3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.hq3_1, new Km(t.ut2_1, e)), )" + }, + { + "Local": "Z3", + "Guard": "t instanceof Z3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.hq3_1, new Km(t.st2_1, u)), )" + }, + { + "Local": "n5", + "Guard": "t instanceof n5", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.hq3_1, new Km(t.xt2_1, s)), )" + }, + { + "Local": "Y3", + "Guard": "t instanceof Y3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.hq3_1, new Km(t.mt2_1, o)), )" + }, + { + "Local": "t5", + "Guard": "t instanceof t5", + "Kind": "conditional", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, am(n.hq3_1, t.wt2_1), )" + }, + { + "Local": "C3", + "Guard": "t instanceof C3", + "Kind": "reset", + "Expr": "n.osz(cd, cg(n.nq2_1, new Km(t.vt0_1, t.wt0_1)))" + }, + { + "Local": "r5", + "Guard": "t instanceof r5", + "Kind": "conditional", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, a)" + }, + { + "Local": "A3", + "Guard": "t instanceof A3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cg(n.oq2_1, new Km(t.ft1_1, t.gt1_1)))" + }, + { + "Local": "R3", + "Guard": "t instanceof R3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cg(n.pq2_1, new Km(t.ht1_1, t.it1_1)))" + }, + { + "Local": "T3", + "Guard": "t instanceof T3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cd, cg(n.tq2_1, new Km(t.zt0_1, t.at1_1)))" + }, + { + "Local": "E3", + "Guard": "t instanceof E3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cg(n.qq2_1, new Km(t.xt0_1, t.yt0_1)))" + }, + { + "Local": "V3", + "Guard": "t instanceof V3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cd, cg(n.rq2_1, new Km(t.it2_1, t.jt2_1)))" + }, + { + "Local": "K3", + "Guard": "t instanceof K3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cg(n.sq2_1, new Km(t.kt2_1, t.lt2_1)))" + }, + { + "Local": "D3", + "Guard": "t instanceof D3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cd, cd, cg(n.uq2_1, new Km(t.rt1_1, t.st1_1)))" + }, + { + "Local": "i5", + "Guard": "t instanceof i5", + "Kind": "conditional", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.xq2_1, Vd(t.yt2_1, t.zt2_1)))" + }, + { + "Local": "O3", + "Guard": "t instanceof O3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.vq2_1, new Km(t.pt1_1, t.qt1_1)))" + }, + { + "Local": "s5", + "Guard": "t instanceof s5", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.yq2_1, new Km(t.ft3_1, t.gt3_1)), )" + }, + { + "Local": "P3", + "Guard": "t instanceof P3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.zq2_1, new Km(t.jt1_1, t.kt1_1)), )" + }, + { + "Local": "N3", + "Guard": "t instanceof N3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.aq3_1, new Km(t.lt1_1, t.mt1_1)), )" + }, + { + "Local": "L3", + "Guard": "t instanceof L3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.cq3_1, new Km(t.tt1_1, t.ut1_1)), )" + }, + { + "Local": "M3", + "Guard": "t instanceof M3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.dq3_1, new Km(t.nt1_1, t.ot1_1)), )" + }, + { + "Local": "o5", + "Guard": "t instanceof o5", + "Kind": "reset", + "Expr": "n.osz(cd, cg(n.nq2_1, new Km(t.dt3_1, t.et3_1)))" + }, + { + "Local": "_5", + "Guard": "t instanceof _5", + "Kind": "conditional", + "Expr": "n.osz(cd, am(n.nq2_1, t.ct3_1))" + }, + { + "Local": "I3", + "Guard": "t instanceof I3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.fq3_1, new Km(t.bt1_1, new j3(t.ct1_1, t.dt1_1, t.et1_1))), )" + }, + { + "Local": "W3", + "Guard": "t instanceof W3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.gq3_1, new Km(t.gt2_1, t.ht2_1)), )" + }, + { + "Local": "H3", + "Guard": "t instanceof H3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.bq3_1, new Km(t.et2_1, t.ft2_1)), )" + }, + { + "Local": "U3", + "Guard": "t instanceof U3", + "Kind": "reset", + "Expr": "n.osz(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.wq2_1, new Km(t.xt1_1, t.yt1_1)))" + }, + { + "Local": "e5", + "Guard": "t instanceof e5", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.cq3_1, new Km(t.bt3_1, v.rt0(!0))), )" + }, + { + "Local": "X3", + "Guard": "t instanceof X3", + "Kind": "conditional", + "Expr": "n.osz( p, cd, w, d, cd, cd, cd, cd, cd, m, g, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, k, )" + }, + { + "Local": "B3", + "Guard": "t instanceof B3", + "Kind": "reset", + "Expr": "n.osz( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.iq3_1, new Km(t.vt1_1, t.wt1_1)), ) : n" + } + ] + }, + { + "Var": null, + "Arg": "n.jq3_1", + "Style": "ternary", + "ArmCount": 5, + "Locals": [ + { + "Local": "F7", + "Name": "InitCategory" + }, + { + "Local": "H7", + "Name": null + }, + { + "Local": "G7", + "Name": "SetupDiscountCategoryByItem" + }, + { + "Local": "O7", + "Name": "SelectCategory" + }, + { + "Local": "D7", + "Name": "SelectCategoryId" + } + ], + "Arms": [ + { + "Local": "F7", + "Expr": "n.wsw( cg(n.osw_1, Vd(t.xsw_1.ysv_1, t.xsw_1)), cg(n.psw_1, Vd(t.xsw_1.csw_1, t.xsw_1.ysv_1)), cg(n.qsw_1, Vd(t.xsw_1.ysv_1, t.xsw_1.csw_1)), )" + }, + { + "Local": "H7", + "Expr": "n.wsw(cd, cd, cd, cd, Zg(n.ssw_1, t.zsw_1))" + }, + { + "Local": "G7", + "Expr": "n.wsw(cd, cd, cd, Xg(n.rsw_1, t.ysw_1))" + }, + { + "Local": "O7", + "Expr": "n.wsw(cd, cd, cd, cd, cd, t.lsw_1)" + }, + { + "Local": "D7", + "Expr": "n.wsw(cd, cd, cd, cd, cd, cd, t.msw_1) : n;" + } + ] + } + ], + "Counts": { + "TotalInstanceof": 69, + "TopLevelBranches": 32, + "SubReducerArms": 37 + }, + "Locals": { + "w4": { + "Name": "SetCoinPaymentProviderUrl" + }, + "Yut": { + "Name": null + }, + "F5": { + "Name": null + }, + "h4": { + "Name": "PaymentUrlReceived" + }, + "c4": { + "Name": "SetBraintreeToken" + }, + "u4": { + "Name": "SetStripeIntentParams" + }, + "n4": { + "Name": "BraintreeLoadingInterrupt" + }, + "g4": { + "Name": null + }, + "O5": { + "Name": "CurrentPayMode" + }, + "X5": { + "Name": null + }, + "J5": { + "Name": "MobilePaymentStateChanged" + }, + "a4": { + "Name": "InitPayModeAction" + }, + "_4": { + "Name": "InitPaymentViewType" + }, + "M5": { + "Name": "SelectItem" + }, + "d5": { + "Name": "SetInitShopItemId" + }, + "d4": { + "Name": "ToggleShopActivity" + }, + "x4": { + "Name": null + }, + "$4": { + "Name": null + }, + "w5": { + "Name": null + }, + "o4": { + "Name": null + }, + "m4": { + "Name": null + }, + "k4": { + "Name": null + }, + "i4": { + "Name": null + }, + "t4": { + "Name": null + }, + "HB": { + "Name": "AddItemToOrderProcessing" + }, + "KU": { + "Name": "RemoveItemFromOrderProcessing" + }, + "m5": { + "Name": "SavePreviousScreen" + }, + "f5": { + "Name": null + }, + "k5": { + "Name": "SaveTargetVisitedCategory" + }, + "v5": { + "Name": null + }, + "z5": { + "Name": "SaveTargetProductId" + }, + "p5": { + "Name": null + }, + "g5": { + "Name": "SetPurchaseCompleted" + }, + "F3": { + "Name": "InitShopItem" + }, + "J3": { + "Name": "InitShopCard" + }, + "Q3": { + "Name": "InitShopCardDiscount" + }, + "Z3": { + "Name": "InitShopCardView" + }, + "n5": { + "Name": "ShopCardSold" + }, + "Y3": { + "Name": "ShopCardExpired" + }, + "t5": { + "Name": "ShopCardRemoved" + }, + "C3": { + "Name": "SetupDiscountItem" + }, + "r5": { + "Name": "InitPaymentProviderItems" + }, + "A3": { + "Name": "InitFeatureItem" + }, + "R3": { + "Name": "InitSpecialKitPackage" + }, + "T3": { + "Name": "InitItemViewType" + }, + "E3": { + "Name": "InitCrystalKitPackage" + }, + "V3": { + "Name": "InitCoinPackage" + }, + "K3": { + "Name": "InitKeysPackage" + }, + "D3": { + "Name": "InitShopItemName" + }, + "i5": { + "Name": "InitAmountString" + }, + "O3": { + "Name": "InitKitPackage" + }, + "s5": { + "Name": null + }, + "P3": { + "Name": "InitRestrictionByPayMode" + }, + "N3": { + "Name": "InitSingleItemKit" + }, + "L3": { + "Name": "InitOneTimePurchaseItem" + }, + "M3": { + "Name": "InitCompensation" + }, + "o5": { + "Name": null + }, + "_5": { + "Name": null + }, + "I3": { + "Name": "InitAdditionalDescription" + }, + "W3": { + "Name": "InitRewardedVideosBlocks" + }, + "H3": { + "Name": "InitLootBoxAndPaint" + }, + "U3": { + "Name": "InitKitBundle" + }, + "e5": { + "Name": "TryToBuyOneTimePurchase" + }, + "X3": { + "Name": "DeleteOneTimePurchaseItem" + }, + "B3": { + "Name": "InitGroup" + }, + "F7": { + "Name": "InitCategory" + }, + "H7": { + "Name": null + }, + "G7": { + "Name": "SetupDiscountCategoryByItem" + }, + "O7": { + "Name": "SelectCategory" + }, + "D7": { + "Name": "SelectCategoryId" + } + }, + "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", + "CopyParamCount": 31, + "CopyMatchesFields": false +} diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 7bdf39f..9fce9f4 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -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, }, diff --git a/3DTank/tools/gen_slice_plan.js b/3DTank/tools/gen_slice_plan.js new file mode 100644 index 0000000..0852189 --- /dev/null +++ b/3DTank/tools/gen_slice_plan.js @@ -0,0 +1,462 @@ +// gen_slice_plan.js —— 从 module_10032 抽取某个根状态切片的「状态字段 + reducer 分支表」 +// +// 用途:为逐个迁移大切片(shop / user / screen / garage …)提供机器可读证据, +// 避免人工在 1 万行 reducer 里数位置。产出 3DTank/names/slice_.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(/(? 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 = `(掩码判深度)。 */ +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, )` 子 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 ...]'); +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(', ')); +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ShopReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ShopReducer.kt new file mode 100644 index 0000000..8dc2286 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ShopReducer.kt @@ -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) + +/** + * 原 `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 = emptyMap(), + val CategoryTypeToId: Map = emptyMap(), + val CategoryIdToType: Map = emptyMap(), + val CategoriesWithDiscount: Set = emptySet(), + val CategoriesWithBonus: Set = 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 = emptyMap(), + val DiscountItems: Map = emptyMap(), + val FeatureItems: Map = emptyMap(), + val SpecialKitPackage: Map = emptyMap(), + val CrystalKitPackage: Map = emptyMap(), + val CoinPackage: Map = emptyMap(), + val KeysPackage: Map = emptyMap(), + val ItemViewType: Map = emptyMap(), + val ShopItemName: Map = emptyMap(), + val KitPackage: Map = emptyMap(), + val KitBanner: Map = emptyMap(), + val AmountString: Map = emptyMap(), + val PaymentModeAsShopItem: Map = emptyMap(), + val RestrictionByPayMode: Map = emptyMap(), + val SingleItemKit: Map = emptyMap(), + val LootBoxAndPaint: Map = emptyMap(), + val OneTimePurchaseItems: Map = emptyMap(), + val CompensationItems: Map = emptyMap(), + val PaymentProviderItems: Map = emptyMap(), + val AdditionalDescription: Map = emptyMap(), + val RewardedVideosBlocks: Map = emptyMap(), + val ShopCards: Map = emptyMap(), + val Groups: Map = 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 = emptySet(), + val SelectedPayModeId: Any? = null, + val PaymentViewType: Map = 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> = 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() + ?.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), + ) +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt index 50ab64c..0362fda 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -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), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 031b6f3..efb7af7 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -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)。未迁移:原样透传。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index bf8ab91..315e89a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -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 = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ val Unmigrated: List get() = All.filter { it.Field !in Migrated } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index d90b4bc..0e93b1c 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -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(1L) && C3.CategoriesWithBonus == setOf(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)