From 1194c395b4082b442d03017d841ed6c36f036a73 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 23 Oct 2026 15:12:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20garagePrevi?= =?UTF-8?q?ew=20=E5=88=87=E7=89=87=EF=BC=8822=20=E5=AD=97=E6=AE=B5=20/=202?= =?UTF-8?q?5=20=E5=88=86=E6=94=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 19 +- 3DTank/names/root_wiring.json | 123 +++- 3DTank/names/slice_garagePreview.json | 696 ++++++++++++++++++ 3DTank/tools/gen_action_kt.js | 6 + 3DTank/tools/gen_root_wiring.js | 43 ++ 3DTank/tools/gen_slice_plan.js | 33 +- .../jsMain/kotlin/App/State/ActionCatalog.kt | 8 +- .../kotlin/App/State/GaragePreviewModels.kt | 61 ++ .../src/jsMain/kotlin/App/State/Inventory.kt | 12 +- .../State/Reducers/GaragePreviewReducer.kt | 258 +++++++ .../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 | 350 ++++++++- 14 files changed, 1574 insertions(+), 49 deletions(-) create mode 100644 3DTank/names/slice_garagePreview.json create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/GaragePreviewModels.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/GaragePreviewReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 09b3df5..b44f855 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -143,7 +143,7 @@ `names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。 - 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。 - 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。 - - 已迁移切片 **9/89**: + - 已迁移切片 **10/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) @@ -172,9 +172,18 @@ `dU`/`wU` 写整组 `mountedSkin`/`mountedShotSkin`;`GoTo` 仅在目标屏幕为 Garage 时复位 `currentCategory`。 无 `toString` 的 25 个匿名类按效果定名(`rF`→`ResetGarage` … `FB`→`ClearWaitServerItemMount`); `FX`(Garage 屏幕)与 `UB`(状态类)已从动作表剔除并记入 `Markers`(`screen-id-class` / `state-class`)。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述九片,其余 **80 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 9/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **234 项全绿**,含 89 槽位完整性、九片一次归约互不串片、未迁移字段透传, + - `garagePreview` → `IG` / `fs.eru`(`GaragePreviewReducer.kt`):22 字段,25 个顶层臂 + (22 个 `instanceof` + 3 个单例 `yd(t, is/es/rs)`)。`ShowRequest` 按 `withDelay` 决定 + `isVisible = !withDelay && 原值`、`needVisible = withDelay || 原值`;`UpdateViewStyle` 无字段, + 原 `t.fru_1` 是 `undefined`,`trs` 视作省略,状态不变。 + `SetHullData` / `SetWeaponData` / `SetDrone` / `SetPaint` 先查 `rrs(category, itemId)` + (`expectedItemId[category] == null || == itemId`);命中才写 3D 数据,但无论命中都缓存动作并删 `requestedItems`。 + `TankPartData` / `TankPreviewSettings` / `HullEngineType` / `TurretMechanism` 见 `GaragePreviewModels.kt`; + 12 个匿名动作按效果定名(`BG`→`CaptureScreenshot` … `tH`→`ClearDroneData`), + 3 个单例臂定名 `ClearGaragePreviewParams` / `ShowGaragePreview` / `HideGaragePreview`,不在 `Actions` 表里。 + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述十片,其余 **79 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 10/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **275 项全绿**,含 89 槽位完整性、十片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -183,7 +192,7 @@ `o1` → `ShowShopDiscountNotification`(`$_$.gl` 单例,由 module_18334 的 `Qx.qge` 派发; 效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示); shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等); - screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个匿名类同样按效果定名并登记。 + screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个匿名类同样按效果定名并登记。 ### 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 6a7da27..d3bccca 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-23T05:35:58.037Z", + "generatedAt": "2026-10-23T06:35:15.369Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -15,7 +15,8 @@ "shop", "screen", "lootBoxes", - "garage" + "garage", + "garagePreview" ], "AnonymousActions": [ { @@ -366,6 +367,96 @@ "Name": "ClearWaitServerItemMount", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "waitServerItemMount = emptyMap()" + }, + { + "Local": "BG", + "Name": "CaptureScreenshot", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "调用 captureScreenshotAction(),状态不变(`null == y || y(), i = n`)" + }, + { + "Local": "UG", + "Name": "CaptureVideo", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "调用 captureVideoAction(),状态不变(`null == $ || $(), i = n`)" + }, + { + "Local": "JG", + "Name": "ClearHasGrenades", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "hasGrenades = false(第 13 个构造参数 !1)" + }, + { + "Local": "KG", + "Name": "SetWeaponData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "若 expectedItemId[WEAPON] 命中,则 weaponData = TankPartData(resource, emissionColors)、turretMechanism = action;并缓存动作、从 requestedItems 删除 itemId" + }, + { + "Local": "OG", + "Name": "UpdateViewStyle", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "UpdateViewStyle:动作类没有字段,原 `t.fru_1` 为 undefined,`trs` 视作省略,状态不变" + }, + { + "Local": "QG", + "Name": "ClearHullData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "hullData = null(第 9 个构造参数 null)" + }, + { + "Local": "VG", + "Name": "SetHullData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "若 expectedItemId[ARMOR] 命中,则 hullData = TankPartData(6 字段)、hullEngineType = action;并缓存动作、从 requestedItems 删除 itemId" + }, + { + "Local": "XG", + "Name": "SetHasGrenades", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "hasGrenades = true(第 13 个构造参数 !0)" + }, + { + "Local": "YG", + "Name": "SetGrenadeData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "grenadeData = resource == null ? null : TankPartData(resource, emissionColors)(第 12 个构造参数)" + }, + { + "Local": "ZG", + "Name": "ClearWeaponData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "weaponData = null(第 10 个构造参数 null)" + }, + { + "Local": "nH", + "Name": "ClearGrenadeData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "grenadeData = null(第 12 个构造参数 null)" + }, + { + "Local": "tH", + "Name": "ClearDroneData", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "droneData = null(第 11 个构造参数 null)" + }, + { + "Local": "is", + "Name": "ClearGaragePreviewParams", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "params = null(第 8 个构造参数 null)" + }, + { + "Local": "es", + "Name": "ShowGaragePreview", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isVisible = true、needVisible = false(`n.trs(!0, cd, !1)`)" + }, + { + "Local": "rs", + "Name": "HideGaragePreview", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isVisible = false(`n.trs(!1)`)" } ], "Markers": [ @@ -602,31 +693,31 @@ "Reducer": "fs.eru", "Delegate": "fs.eru(t, n.epv_1)", "Actions": [ - "?BG", - "?JG", - "?KG", - "?OG", - "?QG", - "?UG", - "?VG", - "?XG", - "?YG", - "?ZG", - "?nH", - "?tH", "AddRequestedItem", + "CaptureScreenshot", + "CaptureVideo", + "ClearDroneData", + "ClearGrenadeData", + "ClearHasGrenades", + "ClearHullData", + "ClearWeaponData", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", + "SetGrenadeData", + "SetHasGrenades", + "SetHullData", "SetMountedItemsProperties", "SetPaint", + "SetWeaponData", "ShowRequest", - "UpdatePreviewSettings" + "UpdatePreviewSettings", + "UpdateViewStyle" ], "Default": "new IG()", - "Migrated": false + "Migrated": true }, { "Field": "challenges", diff --git a/3DTank/names/slice_garagePreview.json b/3DTank/names/slice_garagePreview.json new file mode 100644 index 0000000..cd060cd --- /dev/null +++ b/3DTank/names/slice_garagePreview.json @@ -0,0 +1,696 @@ +{ + "generatedAt": "2026-10-23T07:01:46.681Z", + "module": "module_10032.js", + "Field": "garagePreview", + "Slot": "epv_1", + "Class": "IG", + "ClassSource": "new IG()", + "NamedReducer": "fs.eru", + "Params": [ + "t", + "n", + "i", + "r", + "e", + "u", + "s", + "o", + "_", + "h", + "a", + "c", + "l", + "f", + "v", + "p", + "w", + "d", + "g", + "m", + "k", + "z" + ], + "Fields": [ + { + "Field": "isVisible", + "Slot": "vrr_1", + "Kotlin": "IsVisible", + "Default": "false" + }, + { + "Field": "photoMode", + "Slot": "wrr_1", + "Kotlin": "PhotoMode", + "Default": "false" + }, + { + "Field": "needVisible", + "Slot": "xrr_1", + "Kotlin": "NeedVisible", + "Default": "false" + }, + { + "Field": "viewStyle", + "Slot": "yrr_1", + "Kotlin": "ViewStyle", + "Default": "null" + }, + { + "Field": "dynViewStyle", + "Slot": "zrr_1", + "Kotlin": "DynViewStyle", + "Default": "null" + }, + { + "Field": "tankPreviewSettings", + "Slot": "ars_1", + "Kotlin": "TankPreviewSettings", + "Default": "new Ny()" + }, + { + "Field": "needShowDrone", + "Slot": "brs_1", + "Kotlin": "NeedShowDrone", + "Default": "false" + }, + { + "Field": "params", + "Slot": "crs_1", + "Kotlin": "Params", + "Default": "null" + }, + { + "Field": "hullData", + "Slot": "drs_1", + "Kotlin": "HullData", + "Default": "null" + }, + { + "Field": "weaponData", + "Slot": "ers_1", + "Kotlin": "WeaponData", + "Default": "null" + }, + { + "Field": "droneData", + "Slot": "frs_1", + "Kotlin": "DroneData", + "Default": "null" + }, + { + "Field": "grenadeData", + "Slot": "grs_1", + "Kotlin": "GrenadeData", + "Default": "null" + }, + { + "Field": "hasGrenades", + "Slot": "hrs_1", + "Kotlin": "HasGrenades", + "Default": "false" + }, + { + "Field": "hullEngineType", + "Slot": "irs_1", + "Kotlin": "HullEngineType", + "Default": "My()" + }, + { + "Field": "turretMechanism", + "Slot": "jrs_1", + "Kotlin": "TurretMechanism", + "Default": "Oy()" + }, + { + "Field": "paint", + "Slot": "krs_1", + "Kotlin": "Paint", + "Default": "null" + }, + { + "Field": "properties", + "Slot": "lrs_1", + "Kotlin": "Properties", + "Default": "md()" + }, + { + "Field": "captureScreenshotAction", + "Slot": "mrs_1", + "Kotlin": "CaptureScreenshotAction", + "Default": "null" + }, + { + "Field": "captureVideoAction", + "Slot": "nrs_1", + "Kotlin": "CaptureVideoAction", + "Default": "null" + }, + { + "Field": "item3dModelActionCache", + "Slot": "ors_1", + "Kotlin": "Item3dModelActionCache", + "Default": "ag()" + }, + { + "Field": "requestedItems", + "Slot": "prs_1", + "Kotlin": "RequestedItems", + "Default": "Yg()" + }, + { + "Field": "expectedItemId", + "Slot": "qrs_1", + "Kotlin": "ExpectedItemId", + "Default": "ag()" + } + ], + "CopyMethod": "trs", + "Branches": [ + { + "Guard": "yd(t, is)", + "Locals": [], + "Kind": "copy", + "Changed": [ + { + "Index": 7, + "Field": "params", + "Slot": "crs_1", + "Kotlin": "Params", + "Value": "null" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, null)" + }, + { + "Guard": "t instanceof NG", + "Locals": [ + "NG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 0, + "Field": "isVisible", + "Slot": "vrr_1", + "Kotlin": "IsVisible", + "Value": "r" + }, + { + "Index": 1, + "Field": "photoMode", + "Slot": "wrr_1", + "Kotlin": "PhotoMode", + "Value": "_" + }, + { + "Index": 2, + "Field": "needVisible", + "Slot": "xrr_1", + "Kotlin": "NeedVisible", + "Value": "e" + }, + { + "Index": 3, + "Field": "viewStyle", + "Slot": "yrr_1", + "Kotlin": "ViewStyle", + "Value": "u" + }, + { + "Index": 5, + "Field": "tankPreviewSettings", + "Slot": "ars_1", + "Kotlin": "TankPreviewSettings", + "Value": "s" + }, + { + "Index": 6, + "Field": "needShowDrone", + "Slot": "brs_1", + "Kotlin": "NeedShowDrone", + "Value": "o" + } + ], + "Expr": "n.trs(r, _, e, u, cd, s, o)" + }, + { + "Guard": "t instanceof MG", + "Locals": [ + "MG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 5, + "Field": "tankPreviewSettings", + "Slot": "ars_1", + "Kotlin": "TankPreviewSettings", + "Value": "t.zrs_1" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, t.zrs_1)" + }, + { + "Guard": "t instanceof OG", + "Locals": [ + "OG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 4, + "Field": "dynViewStyle", + "Slot": "zrr_1", + "Kotlin": "DynViewStyle", + "Value": "t.fru_1" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, t.fru_1)" + }, + { + "Guard": "yd(t, es)", + "Locals": [], + "Kind": "copy", + "Changed": [ + { + "Index": 0, + "Field": "isVisible", + "Slot": "vrr_1", + "Kotlin": "IsVisible", + "Value": "!0" + }, + { + "Index": 2, + "Field": "needVisible", + "Slot": "xrr_1", + "Kotlin": "NeedVisible", + "Value": "!1" + } + ], + "Expr": "n.trs(!0, cd, !1)" + }, + { + "Guard": "yd(t, rs)", + "Locals": [], + "Kind": "copy", + "Changed": [ + { + "Index": 0, + "Field": "isVisible", + "Slot": "vrr_1", + "Kotlin": "IsVisible", + "Value": "!1" + } + ], + "Expr": "n.trs(!1)" + }, + { + "Guard": "t instanceof FG", + "Locals": [ + "FG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 7, + "Field": "params", + "Slot": "crs_1", + "Kotlin": "Params", + "Value": "t.crt_1" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, t.crt_1)" + }, + { + "Guard": "t instanceof HG", + "Locals": [ + "HG" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 21, + "Field": "expectedItemId", + "Slot": "qrs_1", + "Kotlin": "ExpectedItemId", + "Value": "am(n.qrs_1, t.grt_1)" + } + ], + "Expr": "null == t.hrt_1 ? n.trs( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, am(n.qrs_1, t.grt_1), ) : n.trs( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.qrs_1, Vd(t.grt_1, t.hrt_1)), )" + }, + { + "Guard": "t instanceof WG", + "Locals": [ + "WG" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 20, + "Field": "requestedItems", + "Slot": "prs_1", + "Kotlin": "RequestedItems", + "Value": "Xg(n.prs_1, t.irt_1)" + } + ], + "Expr": "n.trs( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, Xg(n.prs_1, t.irt_1), )" + }, + { + "Guard": "t instanceof VG", + "Locals": [ + "VG" + ], + "Kind": "conditional", + "Changed": [], + "Expr": "h" + }, + { + "Guard": "t instanceof KG", + "Locals": [ + "KG" + ], + "Kind": "conditional", + "Changed": [], + "Expr": "v" + }, + { + "Guard": "t instanceof iH", + "Locals": [ + "iH" + ], + "Kind": "conditional", + "Changed": [], + "Expr": "m" + }, + { + "Guard": "t instanceof ZG", + "Locals": [ + "ZG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 9, + "Field": "weaponData", + "Slot": "ers_1", + "Kotlin": "WeaponData", + "Value": "null" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, null)" + }, + { + "Guard": "t instanceof QG", + "Locals": [ + "QG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 8, + "Field": "hullData", + "Slot": "drs_1", + "Kotlin": "HullData", + "Value": "null" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, null)" + }, + { + "Guard": "t instanceof tH", + "Locals": [ + "tH" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 10, + "Field": "droneData", + "Slot": "frs_1", + "Kotlin": "DroneData", + "Value": "null" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null)" + }, + { + "Guard": "t instanceof nH", + "Locals": [ + "nH" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 11, + "Field": "grenadeData", + "Slot": "grs_1", + "Kotlin": "GrenadeData", + "Value": "null" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, null)" + }, + { + "Guard": "t instanceof YG", + "Locals": [ + "YG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 11, + "Field": "grenadeData", + "Slot": "grs_1", + "Kotlin": "GrenadeData", + "Value": "b" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, b)" + }, + { + "Guard": "t instanceof XG", + "Locals": [ + "XG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 12, + "Field": "hasGrenades", + "Slot": "hrs_1", + "Kotlin": "HasGrenades", + "Value": "!0" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, !0)" + }, + { + "Guard": "t instanceof JG", + "Locals": [ + "JG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 12, + "Field": "hasGrenades", + "Slot": "hrs_1", + "Kotlin": "HasGrenades", + "Value": "!1" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, !1)" + }, + { + "Guard": "t instanceof rH", + "Locals": [ + "rH" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 15, + "Field": "paint", + "Slot": "krs_1", + "Kotlin": "Paint", + "Value": "t.bru_1" + }, + { + "Index": 19, + "Field": "item3dModelActionCache", + "Slot": "ors_1", + "Kotlin": "Item3dModelActionCache", + "Value": "cg(n.ors_1, Vd(t.aru_1, t))" + }, + { + "Index": 20, + "Field": "requestedItems", + "Slot": "prs_1", + "Kotlin": "RequestedItems", + "Value": "Jg(n.prs_1, t.aru_1)" + } + ], + "Expr": "n.rrs(Vk(), t.aru_1) ? n.trs( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.bru_1, cd, cd, cd, cg(n.ors_1, Vd(t.aru_1, t)), Jg(n.prs_1, t.aru_1), ) : n.trs( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cg(n.ors_1, Vd(t.aru_1, t)), Jg(n.prs_1, t.aru_1), " + }, + { + "Guard": "t instanceof eH", + "Locals": [ + "eH" + ], + "Kind": "copy-helper", + "Changed": [ + { + "Index": 19, + "Field": "item3dModelActionCache", + "Slot": "ors_1", + "Kotlin": "Item3dModelActionCache", + "Value": "am(n.ors_1, t.cru_1)" + } + ], + "Expr": "n.trs( cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, am(n.ors_1, t.cru_1), )" + }, + { + "Guard": "t instanceof uH", + "Locals": [ + "uH" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 16, + "Field": "properties", + "Slot": "lrs_1", + "Kotlin": "Properties", + "Value": "t.dru_1" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.dru_1)" + }, + { + "Guard": "t instanceof LG", + "Locals": [ + "LG" + ], + "Kind": "copy", + "Changed": [ + { + "Index": 17, + "Field": "captureScreenshotAction", + "Slot": "mrs_1", + "Kotlin": "CaptureScreenshotAction", + "Value": "t.art_1" + }, + { + "Index": 18, + "Field": "captureVideoAction", + "Slot": "nrs_1", + "Kotlin": "CaptureVideoAction", + "Value": "t.brt_1" + } + ], + "Expr": "n.trs(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.art_1, t.brt_1)" + }, + { + "Guard": "t instanceof BG", + "Locals": [ + "BG" + ], + "Kind": "noop", + "Changed": [], + "Expr": "n" + }, + { + "Guard": "t instanceof UG", + "Locals": [ + "UG" + ], + "Kind": "noop", + "Changed": [], + "Expr": "n" + }, + { + "Guard": "(else)", + "Locals": [], + "Kind": "noop", + "Changed": [], + "Expr": "n" + } + ], + "SubReducers": [], + "Counts": { + "TotalInstanceof": 22, + "TopLevelBranches": 26, + "SubReducerArms": 0 + }, + "Locals": { + "NG": { + "Name": "ShowRequest" + }, + "MG": { + "Name": "UpdatePreviewSettings" + }, + "OG": { + "Name": null + }, + "FG": { + "Name": "InitGarageResources" + }, + "HG": { + "Name": "SetExpectedItemId" + }, + "WG": { + "Name": "AddRequestedItem" + }, + "VG": { + "Name": null + }, + "KG": { + "Name": null + }, + "iH": { + "Name": "SetDrone" + }, + "ZG": { + "Name": null + }, + "QG": { + "Name": null + }, + "tH": { + "Name": null + }, + "nH": { + "Name": null + }, + "YG": { + "Name": null + }, + "XG": { + "Name": null + }, + "JG": { + "Name": null + }, + "rH": { + "Name": "SetPaint" + }, + "eH": { + "Name": "ResetItem3dModelActionCache" + }, + "uH": { + "Name": "SetMountedItemsProperties" + }, + "LG": { + "Name": "SetCaptureActions" + }, + "BG": { + "Name": null + }, + "UG": { + "Name": null + } + }, + "Delegate": "fs.eru(t, n.epv_1)", + "CopyParamCount": 23, + "CopyMatchesFields": false +} diff --git a/3DTank/tools/gen_action_kt.js b/3DTank/tools/gen_action_kt.js index 016dda3..514d206 100644 --- a/3DTank/tools/gen_action_kt.js +++ b/3DTank/tools/gen_action_kt.js @@ -635,6 +635,12 @@ const FieldTypeOverrides = new Map([ ['AddToFavoriteItem.itemId', 'Long'], ['RemoveFromFavoriteItem.itemId', 'Long'], ['BuyProtectionSlot.slotToUnlock', 'Int'], + // garagePreview 切片:状态里这些字段参与 Long/Boolean/Set 运算;动作字段来自真实派发。 + ['ShowRequest.needShowDrone', 'Boolean'], + ['AddRequestedItem.itemId', 'Long'], + ['ResetItem3dModelActionCache.itemId', 'Long'], + ['SetCaptureActions.screenshotAction', '(() -> Unit)?'], + ['SetCaptureActions.videoAction', '(() -> Unit)?'], ]); const Emitted = []; diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 0d9b22f..2f222ba 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -102,6 +102,23 @@ const AnonymousActionNames = new Map([ ['WB', 'HideVideoInDescription'], ['OU', 'RemoveWaitServerItemMount'], ['FB', 'ClearWaitServerItemMount'], + // garagePreview 切片(IG / fs.eru):12 个没有 toString 的 instanceof 动作 + 3 个单例动作。 + // 括号里是原 reducer 的效果;实现见 GaragePreviewReducer.kt。 + ['BG', 'CaptureScreenshot'], + ['UG', 'CaptureVideo'], + ['JG', 'ClearHasGrenades'], + ['KG', 'SetWeaponData'], + ['OG', 'UpdateViewStyle'], + ['QG', 'ClearHullData'], + ['VG', 'SetHullData'], + ['XG', 'SetHasGrenades'], + ['YG', 'SetGrenadeData'], + ['ZG', 'ClearWeaponData'], + ['nH', 'ClearGrenadeData'], + ['tH', 'ClearDroneData'], + ['is', 'ClearGaragePreviewParams'], + ['es', 'ShowGaragePreview'], + ['rs', 'HideGaragePreview'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -471,6 +488,7 @@ const Migrated = { screen: { Type: 'ScreenStateSlice', Reduce: 'ReduceScreen' }, lootBoxes: { Type: 'LootBoxesStateSlice', Reduce: 'ReduceLootBoxes' }, garage: { Type: 'GarageStateSlice', Reduce: 'ReduceGarage' }, + garagePreview: { Type: 'GaragePreviewStateSlice', Reduce: 'ReduceGaragePreview' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -564,6 +582,25 @@ const GarageAnonymousEffects = { FB: 'waitServerItemMount = emptyMap()', }; +// garagePreview 切片(IG / fs.eru):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。 +const GaragePreviewAnonymousEffects = { + BG: '调用 captureScreenshotAction(),状态不变(`null == y || y(), i = n`)', + UG: '调用 captureVideoAction(),状态不变(`null == $ || $(), i = n`)', + JG: 'hasGrenades = false(第 13 个构造参数 !1)', + KG: '若 expectedItemId[WEAPON] 命中,则 weaponData = TankPartData(resource, emissionColors)、turretMechanism = action;并缓存动作、从 requestedItems 删除 itemId', + OG: 'UpdateViewStyle:动作类没有字段,原 `t.fru_1` 为 undefined,`trs` 视作省略,状态不变', + QG: 'hullData = null(第 9 个构造参数 null)', + VG: '若 expectedItemId[ARMOR] 命中,则 hullData = TankPartData(6 字段)、hullEngineType = action;并缓存动作、从 requestedItems 删除 itemId', + XG: 'hasGrenades = true(第 13 个构造参数 !0)', + YG: 'grenadeData = resource == null ? null : TankPartData(resource, emissionColors)(第 12 个构造参数)', + ZG: 'weaponData = null(第 10 个构造参数 null)', + nH: 'grenadeData = null(第 12 个构造参数 null)', + tH: 'droneData = null(第 11 个构造参数 null)', + is: 'params = null(第 8 个构造参数 null)', + es: 'isVisible = true、needVisible = false(`n.trs(!0, cd, !1)`)', + rs: 'isVisible = false(`n.trs(!1)`)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -604,6 +641,12 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(GaragePreviewAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), ], Markers: [ { diff --git a/3DTank/tools/gen_slice_plan.js b/3DTank/tools/gen_slice_plan.js index 02a3442..2eac242 100644 --- a/3DTank/tools/gen_slice_plan.js +++ b/3DTank/tools/gen_slice_plan.js @@ -201,7 +201,11 @@ function FindGuards(Text) { 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')) { + if ( + Depth === 0 && + M.startsWith('if (', I) && + (M.slice(I, I + 80).includes('instanceof') || M.slice(I, I + 80).includes('yd(t,')) + ) { 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; } } @@ -274,6 +278,7 @@ function PlanSlice(Field) { // 第 2 种需要回查被调函数的函数体,确认它确实读写 `n.`。 let A = Assignments.find((X) => new RegExp('\\bn\\.' + F.Slot + '\\b').test(X.Expr)); let NamedReducer = null; + let NamedBody = null; if (!A) { for (const X of Assignments) { const Call = /^([A-Za-z0-9_$]+)\(t,\s*n\)$/.exec(X.Expr.replace(/\s+/g, ' ').trim()); @@ -282,12 +287,28 @@ function PlanSlice(Field) { if (new RegExp('\\bn\\.' + F.Slot + '\\b').test(T.slice(Fn.Body, Fn.End))) { A = X; NamedReducer = Call[1]; + NamedBody = T.slice(Fn.Body, Fn.End); break; } } } if (!A) throw new Error(Field + ' 在根 reducer 里找不到委派'); + // 第 3 种:根 reducer 直接调用对象方法,`fs.eru(t, n.epv_1)`(garagePreview)。 + // 方法体在 Methods 表里,和具名 reducer 一样按字段数锁定 copy 方法。 + if (!NamedBody) { + const MCall = /^([A-Za-z0-9_$]+)\.([A-Za-z0-9_$]+)\(t,\s*n\.[A-Za-z0-9_$]+\)$/.exec( + A.Expr.replace(/\s+/g, ' ').trim(), + ); + if (MCall) { + const M = FindMethod(MCall[1], MCall[2]) || Methods.find((X) => X.Method === MCall[2]); + if (M) { + NamedReducer = MCall[1] + '.' + MCall[2]; + NamedBody = T.slice(M.Body, M.End); + } + } + } + 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; @@ -319,13 +340,11 @@ function PlanSlice(Field) { } const CopyName = (A.Expr.match(/\bn\.([A-Za-z0-9_$]+)\(/) || [])[1] || null; - // 具名 reducer 的副本方法只在函数体里(`o.hri(...)`),按「参数个数 == 字段数」锁定。 - let NamedBody = null; - if (NamedReducer) { - const Fn = Fns.get(NamedReducer); - NamedBody = T.slice(Fn.Body, Fn.End); + // 具名 reducer / 对象方法 reducer 的副本方法只在函数体里(`o.hri(...)` 或 `n.trs(...)`), + // 按「参数个数 == 字段数」锁定。 + if (NamedBody) { const ByArity = []; - for (const M of NamedBody.matchAll(/\bo\.([A-Za-z0-9_$]+)\(/g)) { + for (const M of NamedBody.matchAll(/\b(?:o|n)\.([A-Za-z0-9_$]+)\(/g)) { let O = M.index + M[0].length, D = 0, E = -1; for (let J = O; J < NamedBody.length; J++) { const C2 = NamedBody[J]; diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt index 7ea1641..805af4a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt @@ -331,16 +331,16 @@ object ActionCatalog { ActionMeta("UpgradableItemParams", UpgradableItemParams::class, listOf(ActionFieldMeta("itemPropertyParams", "Any?"), ActionFieldMeta("properties", "Any?"), ActionFieldMeta("currentLevel", "Int"), ActionFieldMeta("upgradeParams", "Any?"), ActionFieldMeta("upgradeDiscount", "Int"), ActionFieldMeta("upgradeConfirmed", "Any?"))), ActionMeta("ItemPropertyParams", ItemPropertyParams::class, listOf(ActionFieldMeta("sortIndex", "Int"), ActionFieldMeta("name", "Any?"), ActionFieldMeta("properties", "Any?"), ActionFieldMeta("aggregationMode", "Any?"), ActionFieldMeta("multiplier", "Any?"), ActionFieldMeta("additiveShift", "Any?"), ActionFieldMeta("isKernelProperty", "Boolean"), ActionFieldMeta("unitName", "Any?"))), ActionMeta("GaragePreview", GaragePreview::class, listOf(ActionFieldMeta("isVisible", "Any?"), ActionFieldMeta("photoMode", "Any?"), ActionFieldMeta("needVisible", "Any?"), ActionFieldMeta("viewStyle", "Any?"), ActionFieldMeta("dynViewStyle", "Any?"), ActionFieldMeta("tankPreviewSettings", "Any?"), ActionFieldMeta("needShowDrone", "Any?"), ActionFieldMeta("params", "Any?"), ActionFieldMeta("hullData", "Any?"), ActionFieldMeta("weaponData", "Any?"), ActionFieldMeta("droneData", "Any?"), ActionFieldMeta("grenadeData", "Any?"), ActionFieldMeta("hasGrenades", "Any?"), ActionFieldMeta("hullEngineType", "Any?"), ActionFieldMeta("turretMechanism", "Any?"), ActionFieldMeta("paint", "Any?"), ActionFieldMeta("properties", "Any?"), ActionFieldMeta("captureScreenshotAction", "Any?"), ActionFieldMeta("captureVideoAction", "Any?"), ActionFieldMeta("item3dModelActionCache", "Any?"), ActionFieldMeta("requestedItems", "Any?"), ActionFieldMeta("expectedItemId", "Any?"))), - ActionMeta("ShowRequest", ShowRequest::class, listOf(ActionFieldMeta("viewStyle", "Any?"), ActionFieldMeta("tankPreviewSettings", "Any?"), ActionFieldMeta("needShowDrone", "Any?"), ActionFieldMeta("photoMode", "Boolean"), ActionFieldMeta("withDelay", "Boolean"))), + ActionMeta("ShowRequest", ShowRequest::class, listOf(ActionFieldMeta("viewStyle", "Any?"), ActionFieldMeta("tankPreviewSettings", "Any?"), ActionFieldMeta("needShowDrone", "Boolean"), ActionFieldMeta("photoMode", "Boolean"), ActionFieldMeta("withDelay", "Boolean"))), ActionMeta("UpdatePreviewSettings", UpdatePreviewSettings::class, listOf(ActionFieldMeta("tankPreviewSettings", "Any?"))), - ActionMeta("SetCaptureActions", SetCaptureActions::class, listOf(ActionFieldMeta("screenshotAction", "Any?"), ActionFieldMeta("videoAction", "Any?"))), + ActionMeta("SetCaptureActions", SetCaptureActions::class, listOf(ActionFieldMeta("screenshotAction", "(() -> Unit)?"), ActionFieldMeta("videoAction", "(() -> Unit)?"))), ActionMeta("InitGarageResources", InitGarageResources::class, listOf(ActionFieldMeta("params", "GaragePreviewModelCC"))), ActionMeta("SetFromCacheOrRequest", SetFromCacheOrRequest::class, listOf(ActionFieldMeta("item", "Any?"), ActionFieldMeta("request", "Any?"))), ActionMeta("SetExpectedItemId", SetExpectedItemId::class, listOf(ActionFieldMeta("category", "Any?"), ActionFieldMeta("itemId", "Any?"))), - ActionMeta("AddRequestedItem", AddRequestedItem::class, listOf(ActionFieldMeta("itemId", "Any?"))), + ActionMeta("AddRequestedItem", AddRequestedItem::class, listOf(ActionFieldMeta("itemId", "Long"))), ActionMeta("SetDrone", SetDrone::class, listOf(ActionFieldMeta("itemId", "Long"), ActionFieldMeta("resource", "Any?"), ActionFieldMeta("emissionColors", "EmissionColors"))), ActionMeta("SetPaint", SetPaint::class, listOf(ActionFieldMeta("itemId", "Long"), ActionFieldMeta("paint", "Coloring"))), - ActionMeta("ResetItem3dModelActionCache", ResetItem3dModelActionCache::class, listOf(ActionFieldMeta("itemId", "Any?"))), + ActionMeta("ResetItem3dModelActionCache", ResetItem3dModelActionCache::class, listOf(ActionFieldMeta("itemId", "Long"))), ActionMeta("SetMountedItemsProperties", SetMountedItemsProperties::class, listOf(ActionFieldMeta("properties", "List"))), ActionMeta("IconsState", IconsState::class, listOf(ActionFieldMeta("icons", "Any?"))), ActionMeta("SetIcons", SetIcons::class, listOf(ActionFieldMeta("icons", "List"))), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/GaragePreviewModels.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/GaragePreviewModels.kt new file mode 100644 index 0000000..f29c6a0 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/GaragePreviewModels.kt @@ -0,0 +1,61 @@ +// GaragePreviewModels.kt —— garagePreview 切片里来自其它模块的嵌套模型(原 module_10032 的 IG / fs.eru) +// +// 证据链: +// · TankPartData(module_03569 的 th,导出名 s7):构造 6 字段 +// `function th(t, n, i, r, e, u)`,toString 为 +// `TankPartData(resource, emissionColors, turretMountPoint, skinOffset, +// leftTrackTexToWorldRatio, rightTrackTexToWorldRatio)`。 +// 原 reducer 的 VG/KG/iH/YG 分支都用 `new Dy(…)` 构造它。 +// · TankPreviewSettings(module_03569 的 lh,导出名 v7):构造 10 字段, +// toString 为 `TankPreviewSettings(cameraHeight, distanceToTank, lookAtZOffset, +// lookAtXOffset, cameraFov, tankZ, lightSettings, hasReflection, +// reflectionAlpha, reflectionAlphaDistant)`。 +// · HullEngineType:module_85196 的 `zw` 枚举,`TRACKED(0)` / `HOVER(1)`; +// 原 `My = p.$_$.k3` 返回 `TRACKED`,是 IG 的默认值。 +// · TurretMechanism:module_85196 的 `Fw` 枚举,`ROTATING_TURRET(0)` / `ELEVATING_BARREL(1)`; +// 原 `Oy = p.$_$.m3` 返回 `ROTATING_TURRET`,是 IG 的默认值。 +package App.State + +/** 原 `TankPreviewLightSettings`(module_03569 的 fh):只占位,本切片不读取其字段。 */ +data class TankPreviewLightSettings( + val ambientColor: Any? = null, + val lightColor: Any? = null, + val lightDirection: Any? = null, + val additionalLightDirection: Any? = null, +) + +/** 原 `TankPreviewSettings`(module_03569 的 lh):默认值取构造函数。 */ +data class TankPreviewSettings( + val cameraHeight: Int = 300, + val distanceToTank: Int = 1750, + val lookAtZOffset: Int = 90, + val lookAtXOffset: Int = 0, + val cameraFov: Double = 0.5, + val tankZ: Int = 120, + val lightSettings: TankPreviewLightSettings = TankPreviewLightSettings(), + val hasReflection: Boolean = true, + val reflectionAlpha: Double = 0.2, + val reflectionAlphaDistant: Double = 0.08, +) + +/** 原 `TankPartData`(module_03569 的 th):车体 / 炮塔 / 无人机 / 手雷的 3D 资源数据。 */ +data class TankPartData( + val resource: Any? = null, + val emissionColors: EmissionColors? = null, + val turretMountPoint: Any? = null, + val skinOffset: Any? = null, + val leftTrackTexToWorldRatio: Int = 0, + val rightTrackTexToWorldRatio: Int = 0, +) + +/** 原 `zw` 枚举:履带式 / 悬浮式。 */ +enum class HullEngineType { + TRACKED, + HOVER, +} + +/** 原 `Fw` 枚举:旋转炮塔 / 俯仰炮管。 */ +enum class TurretMechanism { + ROTATING_TURRET, + ELEVATING_BARREL, +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt index 42a999d..a936d94 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt @@ -5,7 +5,7 @@ package App.State import Core.Vector3 -// 动作/状态类共 816 个,字段 2150 个(已定类型 567 个) +// 动作/状态类共 816 个,字段 2150 个(已定类型 572 个) data class AnnouncementState( val isEnabled: Any?, // 仅 copy 转发(无类型信息) val modelLoaded: Any?, // 仅 copy 转发(无类型信息) @@ -1934,7 +1934,7 @@ data class GaragePreview( data class ShowRequest( val viewStyle: Any?, // 调用点: new nE(hNn().x1iy()) | t val tankPreviewSettings: Any?, // 调用点: new tE(350, 1200, 180, $q, 0.6, 0, $q, ! | Mrt(this) - val needShowDrone: Any?, // 调用点: $q | $q + val needShowDrone: Boolean, // override val photoMode: Boolean, // callsite val withDelay: Boolean, // callsite ) @@ -1944,8 +1944,8 @@ data class UpdatePreviewSettings( ) data class SetCaptureActions( - val screenshotAction: Any?, // 调用点: r | null - val videoAction: Any?, // 调用点: (function (t) { var n = func | null + val screenshotAction: (() -> Unit)?, // override + val videoAction: (() -> Unit)?, // override ) data class InitGarageResources( @@ -1963,7 +1963,7 @@ data class SetExpectedItemId( ) data class AddRequestedItem( - val itemId: Any?, // 调用点: n | n + val itemId: Long, // override ) data class SetDrone( @@ -1978,7 +1978,7 @@ data class SetPaint( ) data class ResetItem3dModelActionCache( - val itemId: Any?, // 调用点: t.zas_1.r1pm_1.zr9_1 + val itemId: Long, // override ) data class SetMountedItemsProperties( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/GaragePreviewReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/GaragePreviewReducer.kt new file mode 100644 index 0000000..6fa42d7 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/GaragePreviewReducer.kt @@ -0,0 +1,258 @@ +// GaragePreviewReducer.kt —— 车库 3D 预览切片(对应 module_10032 的 IG 状态 + fs.eru reducer) +// +// 证据链: +// 状态类 IG:`function IG(t, n, i, r, e, u, s, o, _, h, a, c, l, f, v, p, w, d, g, m, k, z)`, +// 22 个字段,toString 名为 `GaragePreview(isVisible=…, …, expectedItemId=…)`。默认值取自构造: +// isVisible / photoMode / needVisible = false,viewStyle / dynViewStyle = null, +// tankPreviewSettings = new Ny()(TankPreviewSettings()),needShowDrone = false, +// params / hullData / weaponData / droneData / grenadeData / paint = null,hasGrenades = false, +// hullEngineType = My()(TRACKED),turretMechanism = Oy()(ROTATING_TURRET), +// properties = md()(emptyList),captureScreenshotAction / captureVideoAction = null, +// item3dModelActionCache / expectedItemId = ag()(emptyMap),requestedItems = Yg()(HashSet)。 +// +// 根委派:`fs.eru(t, n.epv_1)`。eru 有 25 个顶层臂: +// 22 个 instanceof 动作(10 个具名 + 12 个匿名) + 3 个单例动作 +// (`yd(t, is)` / `yd(t, es)` / `yd(t, rs)`,不在 root_wiring 的 Actions 表里)。 +// +// 关键语义(`trs` 是 IG.copy,参数省略时保留原字段): +// · ShowRequest 不是简单展示:withDelay 为 true 时 isVisible=false、needVisible=true。 +// · UpdateViewStyle(OG)没有字段,原 `t.fru_1` 是 undefined,`trs` 把它当省略,状态不变。 +// · SetHullData / SetWeaponData / SetDrone / SetPaint 先查 `rrs(category, itemId)`: +// 仅当 expectedItemId[category] 为 null 或等于 itemId 时才写 3D 数据; +// 无论是否命中,都会把动作写入 item3dModelActionCache 并从 requestedItems 删除 itemId。 +// · ClearHullData / ClearWeaponData / ClearDroneData / ClearGrenadeData 分别置空对应数据。 +// · SetGrenadeData 不写缓存、不删 requestedItems,只按 resource 是否为空写 grenadeData。 +// · CaptureScreenshot / CaptureVideo 只调用回调,返回原状态。 +// · ClearGaragePreviewParams / ShowGaragePreview / HideGaragePreview 是三个单例臂。 +package App.State.Reducers + +import App.State.AddRequestedItem +import App.State.Coloring +import App.State.EmissionColors +import App.State.GaragePreviewModelCC +import App.State.HullEngineType +import App.State.InitGarageResources +import App.State.ResetItem3dModelActionCache +import App.State.SetCaptureActions +import App.State.SetDrone +import App.State.SetExpectedItemId +import App.State.SetMountedItemsProperties +import App.State.SetPaint +import App.State.ShowRequest +import App.State.TankPartData +import App.State.TankPreviewSettings +import App.State.TurretMechanism +import App.State.UpdatePreviewSettings + +/** 原 `vo` 物品分类枚举里本切片用到的键(module_85667 的 `Wt/Vt/Kt/on`)。 */ +const val PreviewWeaponCategory: Int = 0 +const val PreviewHullCategory: Int = 1 +const val PreviewPaintCategory: Int = 2 +const val PreviewDroneCategory: Int = 14 + +/* ------------------------------------------------------------------ 匿名动作 */ +// 12 个没有 toString 的 instanceof 动作,按 reducer 分支效果定名(证据见 gen_root_wiring.js)。 +class CaptureScreenshot +class CaptureVideo +class ClearHasGrenades +class SetHasGrenades +class UpdateViewStyle +class ClearHullData +class ClearWeaponData +class ClearDroneData +class ClearGrenadeData + +data class SetHullData( + val itemId: Long, + val hullEngineType: HullEngineType, + val resource: Any?, + val emissionColors: EmissionColors?, + val turretMountPoint: Any?, + val skinOffset: Any?, + val leftTrackTexToWorldRatio: Int, + val rightTrackTexToWorldRatio: Int, +) { + fun ToTankPartData(): TankPartData = TankPartData( + resource = resource, + emissionColors = emissionColors, + turretMountPoint = turretMountPoint, + skinOffset = skinOffset, + leftTrackTexToWorldRatio = leftTrackTexToWorldRatio, + rightTrackTexToWorldRatio = rightTrackTexToWorldRatio, + ) +} + +data class SetWeaponData( + val itemId: Long, + val turretMechanism: TurretMechanism, + val resource: Any?, + val emissionColors: EmissionColors?, +) { + fun ToTankPartData(): TankPartData = TankPartData( + resource = resource, + emissionColors = emissionColors, + ) +} + +data class SetGrenadeData( + val resource: Any?, + val emissionColors: EmissionColors?, +) { + fun ToTankPartData(): TankPartData = TankPartData( + resource = resource, + emissionColors = emissionColors, + ) +} + +/** 三个单例动作(原 `is` / `es` / `rs`,不在 root_wiring 的 Actions 表里)。 */ +class ClearGaragePreviewParams +class ShowGaragePreview +class HideGaragePreview + +/* ------------------------------------------------------------------ 状态 */ + +data class GaragePreviewStateSlice( + val IsVisible: Boolean = false, + val PhotoMode: Boolean = false, + val NeedVisible: Boolean = false, + val ViewStyle: Any? = null, + val DynViewStyle: Any? = null, + val TankPreviewSettings: Any? = TankPreviewSettings(), + val NeedShowDrone: Boolean = false, + val Params: GaragePreviewModelCC? = null, + val HullData: TankPartData? = null, + val WeaponData: TankPartData? = null, + val DroneData: TankPartData? = null, + val GrenadeData: TankPartData? = null, + val HasGrenades: Boolean = false, + val HullEngineType: HullEngineType = App.State.HullEngineType.TRACKED, + val TurretMechanism: TurretMechanism = App.State.TurretMechanism.ROTATING_TURRET, + val Paint: Coloring? = null, + val Properties: List = emptyList(), + val CaptureScreenshotAction: (() -> Unit)? = null, + val CaptureVideoAction: (() -> Unit)? = null, + val Item3dModelActionCache: Map = emptyMap(), + val RequestedItems: Set = emptySet(), + val ExpectedItemId: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ 小工具 */ + +/** 原 `IG.rrs(category, itemId)`:expectedItemId[category] 为 null 或等于 itemId。 */ +private fun GaragePreviewStateSlice.Rrs(Category: Any?, ItemId: Any?): Boolean { + val Expected = ExpectedItemId[Category] + return Expected == null || Expected == ItemId +} + +private fun Map.With(ItemId: Long, Value: T): Map = this + (ItemId to Value) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceGaragePreview(State0: GaragePreviewStateSlice, Action: Any): GaragePreviewStateSlice = when (Action) { + is ClearGaragePreviewParams -> State0.copy(Params = null) + is ShowGaragePreview -> State0.copy(IsVisible = true, NeedVisible = false) + is HideGaragePreview -> State0.copy(IsVisible = false) + + is ShowRequest -> State0.copy( + // 原:`var r = !t.yrs_1 && n.vrr_1, e = !!t.yrs_1 || n.xrr_1`。 + IsVisible = !Action.withDelay && State0.IsVisible, + PhotoMode = Action.photoMode, + NeedVisible = Action.withDelay || State0.NeedVisible, + ViewStyle = Action.viewStyle, + TankPreviewSettings = Action.tankPreviewSettings, + NeedShowDrone = Action.needShowDrone, + ) + + is UpdatePreviewSettings -> State0.copy(TankPreviewSettings = Action.tankPreviewSettings) + is UpdateViewStyle -> State0 + + is InitGarageResources -> State0.copy(Params = Action.params) + + is SetExpectedItemId -> { + // 原:itemId == null ? am(map, category) : cg(map, category to itemId)。 + val NewExpected = if (Action.itemId == null) { + State0.ExpectedItemId - Action.category + } else { + State0.ExpectedItemId + (Action.category to Action.itemId) + } + State0.copy(ExpectedItemId = NewExpected) + } + + is AddRequestedItem -> State0.copy(RequestedItems = State0.RequestedItems + Action.itemId) + + is SetHullData -> { + val Matches = State0.Rrs(PreviewHullCategory, Action.itemId) + State0.copy( + HullData = if (Matches) Action.ToTankPartData() else State0.HullData, + HullEngineType = if (Matches) Action.hullEngineType else State0.HullEngineType, + Item3dModelActionCache = State0.Item3dModelActionCache.With(Action.itemId, Action), + RequestedItems = State0.RequestedItems - Action.itemId, + ) + } + + is SetWeaponData -> { + val Matches = State0.Rrs(PreviewWeaponCategory, Action.itemId) + State0.copy( + WeaponData = if (Matches) Action.ToTankPartData() else State0.WeaponData, + TurretMechanism = if (Matches) Action.turretMechanism else State0.TurretMechanism, + Item3dModelActionCache = State0.Item3dModelActionCache.With(Action.itemId, Action), + RequestedItems = State0.RequestedItems - Action.itemId, + ) + } + + is SetDrone -> { + val Matches = State0.Rrs(PreviewDroneCategory, Action.itemId) + val Data = if (Action.resource == null) null else TankPartData( + resource = Action.resource, + emissionColors = Action.emissionColors, + ) + State0.copy( + DroneData = if (Matches) Data else State0.DroneData, + Item3dModelActionCache = State0.Item3dModelActionCache.With(Action.itemId, Action), + RequestedItems = State0.RequestedItems - Action.itemId, + ) + } + + is SetGrenadeData -> State0.copy( + GrenadeData = if (Action.resource == null) null else Action.ToTankPartData(), + ) + + is ClearWeaponData -> State0.copy(WeaponData = null) + is ClearHullData -> State0.copy(HullData = null) + is ClearDroneData -> State0.copy(DroneData = null) + is ClearGrenadeData -> State0.copy(GrenadeData = null) + + is SetHasGrenades -> State0.copy(HasGrenades = true) + is ClearHasGrenades -> State0.copy(HasGrenades = false) + + is SetPaint -> { + val Matches = State0.Rrs(PreviewPaintCategory, Action.itemId) + State0.copy( + Paint = if (Matches) Action.paint else State0.Paint, + Item3dModelActionCache = State0.Item3dModelActionCache.With(Action.itemId, Action), + RequestedItems = State0.RequestedItems - Action.itemId, + ) + } + + is ResetItem3dModelActionCache -> + State0.copy(Item3dModelActionCache = State0.Item3dModelActionCache - Action.itemId) + + is SetMountedItemsProperties -> State0.copy(Properties = Action.properties) + + is SetCaptureActions -> State0.copy( + CaptureScreenshotAction = Action.screenshotAction, + CaptureVideoAction = Action.videoAction, + ) + + is CaptureScreenshot -> { + State0.CaptureScreenshotAction?.invoke() + State0 + } + + is CaptureVideo -> { + State0.CaptureVideoAction?.invoke() + State0 + } + + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt index 582cc81..0deaa61 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -12,13 +12,15 @@ // screen → ReduceScreen(WK / dY().ks5) // lootBoxes → ReduceLootBoxes(aH / EH.wrv) // garage → ReduceGarage(UB / hF,根委派 `i = hF(t, n)`) +// garagePreview → ReduceGaragePreview(IG / fs.eru,根委派 `fs.eru(t, n.epv_1)`) // -// 其余 80 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 79 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State import App.State.Reducers.ReduceGarage +import App.State.Reducers.ReduceGaragePreview import App.State.Reducers.ReduceHome import App.State.Reducers.ReduceInvite import App.State.Reducers.ReduceLocalBattleUser @@ -50,6 +52,7 @@ object RootReducer { Screen = ReduceScreen(State0.Screen, Action), LootBoxes = ReduceLootBoxes(State0.LootBoxes, Action), Garage = ReduceGarage(State0.Garage, Action), + GaragePreview = ReduceGaragePreview(State0.GaragePreview, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 9252e63..d42b3dc 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -5,6 +5,7 @@ package App.State import App.State.Reducers.GarageStateSlice +import App.State.Reducers.GaragePreviewStateSlice import App.State.Reducers.HomeStateSlice import App.State.Reducers.InviteStateSlice import App.State.Reducers.LocalBattleUserStateSlice @@ -31,8 +32,8 @@ data class RootState( val BattleTutorial: Any? = null, /** 原 garage(UB / hF)。已迁移。 */ val Garage: GarageStateSlice = GarageStateSlice(), - /** 原 garagePreview(IG / fs.eru)。未迁移:原样透传。 */ - val GaragePreview: Any? = null, + /** 原 garagePreview(IG / fs.eru)。已迁移。 */ + val GaragePreview: GaragePreviewStateSlice = GaragePreviewStateSlice(), /** 原 challenges(CZ / nh.hsj)。未迁移:原样透传。 */ val Challenges: Any? = null, /** 原 loginByPassword(gD / nu.cr6)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 1104887..dd140cd 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -20,7 +20,7 @@ object RootWiring { SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("?DR", "?HR", "?KR", "?PR", "?QR", "?UR", "?XR", "?YR", "?ZR", "?nP", "?tP", "Init", "InitBattleModeWithLevels", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "UpdateFund", "UpdateScore")), SliceWiring("battleTutorial", "cpv_1", "rE", "Q.sq8", "Q.sq8(t, n.cpv_1)", listOf("?oE", "?sE", "SetFragsCount", "ShowMessage")), SliceWiring("garage", "dpv_1", "UB", "hF", "hF(t, n)", listOf("AddGarageItemCount", "AddNewItem", "AddOverdriveDescription", "AddRelatedBazooka", "AddRelatedResistanceModule", "AddToFavoriteItem", "BuyProtectionSlot", "ClearWaitServerItemMount", "GarageAddItemToDepot", "GarageApplyResistanceMount", "GarageBuyDevice", "GarageDeviceTrySetInfinityLifetime", "GarageDevicesLoadedAction", "GarageLoaded", "GarageMountItem", "GarageMountItems", "GarageRemoveDevice", "GarageRemoveDiscountForDevice", "GarageRemoveItemFromDepot", "GarageRemoveItemFromStore", "GarageResistanceUnMount", "GarageSelectAlteration", "GarageSelectItem", "GarageSelectShotSkin", "GarageSelectSkin", "GarageUnmountItem", "GarageUpdateItemCount", "GarageUpgradeItemAction", "GarageUpgradeItemConfirm", "GarageUpgradeItemCoolDownFinished", "GarageUpgradeItemCoolDownStarted", "GoTo", "HideVideoInDescription", "InitState", "RemoveFromFavoriteItem", "RemoveFromSale", "RemoveNewItem", "RemoveWaitServerItemMount", "ResetGarage", "ResetMountDelay", "SelectGarageCategory", "SetCooldownReduceEndTime", "SetGarageNotLoaded", "SetGarageObject", "SetGarageSkinsAction", "SetItemRarity", "SetItemsOnDepot", "SetItemsOnMarket", "SetMountedItems", "SetMountedShotSkinForItems", "SetMountedSkinForItems", "SetNewItems", "SetResistanceReadyToMount", "SetRestrictionMount", "ShowGarageDiscountNotification", "StartCooldownReduce", "SwitchingRowsScroll", "UpdateDiscount")), - SliceWiring("garagePreview", "epv_1", "IG", "fs.eru", "fs.eru(t, n.epv_1)", listOf("?BG", "?JG", "?KG", "?OG", "?QG", "?UG", "?VG", "?XG", "?YG", "?ZG", "?nH", "?tH", "AddRequestedItem", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", "SetMountedItemsProperties", "SetPaint", "ShowRequest", "UpdatePreviewSettings")), + SliceWiring("garagePreview", "epv_1", "IG", "fs.eru", "fs.eru(t, n.epv_1)", listOf("AddRequestedItem", "CaptureScreenshot", "CaptureVideo", "ClearDroneData", "ClearGrenadeData", "ClearHasGrenades", "ClearHullData", "ClearWeaponData", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", "SetGrenadeData", "SetHasGrenades", "SetHullData", "SetMountedItemsProperties", "SetPaint", "SetWeaponData", "ShowRequest", "UpdatePreviewSettings", "UpdateViewStyle")), SliceWiring("challenges", "fpv_1", "CZ", "nh.hsj", "nh.hsj(t, n.fpv_1)", listOf("?LZ", "RemoveChallenge", "SetBattlePass", "SetBattlePassPackages", "SetChallenge", "SetRewardForDialog", "SetTakenBattlePassRewardIndex", "SetTakenFreeRewardIndex", "UpdateStars")), SliceWiring("loginByPassword", "gpv_1", "gD", "nu.cr6", "nu.cr6(t, n.gpv_1)", listOf("?ND", "?PD", "?RD", "IncorrectLogin2FA", "RequestsNumber2FA", "SetLogin", "SetPassword", "SetRememberMe", "SetStatus")), SliceWiring("register", "hpv_1", "Cit", "cp.stg", "cp.stg(t, n.hpv_1)", listOf("?Bit", "?Dit", "?Fit", "?Git", "?Hit", "?Lit", "?Mit", "?Uit", "?_rt", "?hrt", "EnteredUidIsBusy", "ForceSetUid", "ReceiveOAuthUri", "SetOpenedPassword", "SetPassword", "SetPasswordRestrictions", "SetUid", "SocialNetworkAvailability", "SocialNetworkClientIds")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(由 RootReducer 真正归约)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 26f2212..3b0198d 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -9,8 +9,9 @@ // ⑥ 动作目录:816 个类与 138 个真实派发名对齐 // ⑦ 回放:真实 toString 样例的类/字段/类型校验 // ⑧ 全目录动作类都能按 KClass 路由 -// ⑨~⑲ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / -// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage;⑭ 根 reducer 的 89 槽位委派 +// ⑨~⑳ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / +// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview; +// ⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) package App @@ -209,19 +210,52 @@ import App.State.SetRestrictionMount import App.State.SwitchingRowsScroll import App.State.UpdateDiscount import App.State.UpgradableItemParams +import App.State.AddRequestedItem +import App.State.Coloring +import App.State.EmissionColors +import App.State.GaragePreviewModelCC +import App.State.HullEngineType +import App.State.InitGarageResources +import App.State.ResetItem3dModelActionCache +import App.State.SetCaptureActions +import App.State.SetDrone +import App.State.SetExpectedItemId +import App.State.SetMountedItemsProperties +import App.State.SetPaint +import App.State.ShowRequest +import App.State.TankPartData +import App.State.TankPreviewSettings +import App.State.TextureFrameAnimation +import App.State.TurretMechanism +import App.State.UpdatePreviewSettings import App.State.Reducers.AddGarageItemCount import App.State.Reducers.AddNewItem +import App.State.Reducers.CaptureScreenshot +import App.State.Reducers.CaptureVideo +import App.State.Reducers.ClearDroneData +import App.State.Reducers.ClearGaragePreviewParams +import App.State.Reducers.ClearGrenadeData +import App.State.Reducers.ClearHasGrenades +import App.State.Reducers.ClearHullData import App.State.Reducers.ClearUpgradeItemInProcessing import App.State.Reducers.ClearWaitServerItemMount +import App.State.Reducers.ClearWeaponData import App.State.Reducers.GarageDefaultCategory import App.State.Reducers.GarageLoaded import App.State.Reducers.GarageMountItem import App.State.Reducers.GarageMountItems +import App.State.Reducers.GaragePreviewStateSlice import App.State.Reducers.GarageResistanceCategoryKey import App.State.Reducers.GarageStateSlice import App.State.Reducers.GarageUnmountItem +import App.State.Reducers.HideGaragePreview import App.State.Reducers.HideVideoInDescription +import App.State.Reducers.PreviewDroneCategory +import App.State.Reducers.PreviewHullCategory +import App.State.Reducers.PreviewPaintCategory +import App.State.Reducers.PreviewWeaponCategory import App.State.Reducers.ReduceGarage +import App.State.Reducers.ReduceGaragePreview import App.State.Reducers.RemoveNewItem import App.State.Reducers.RemoveWaitServerItemMount import App.State.Reducers.ResetGarage @@ -230,6 +264,9 @@ import App.State.Reducers.SelectGarageCategory import App.State.Reducers.SetCooldownReduceEndTime import App.State.Reducers.SetGarageNotLoaded import App.State.Reducers.SetGarageObject +import App.State.Reducers.SetGrenadeData +import App.State.Reducers.SetHasGrenades +import App.State.Reducers.SetHullData import App.State.Reducers.SetItemsOnDepot import App.State.Reducers.SetItemsOnMarket import App.State.Reducers.SetMountedItems @@ -237,8 +274,11 @@ import App.State.Reducers.SetMountedShotSkinForItems import App.State.Reducers.SetMountedSkinForItems import App.State.Reducers.SetNewItems import App.State.Reducers.SetResistanceReadyToMount +import App.State.Reducers.SetWeaponData import App.State.Reducers.ShowGarageDiscountNotification +import App.State.Reducers.ShowGaragePreview import App.State.Reducers.StartCooldownReduce +import App.State.Reducers.UpdateViewStyle import State.InitState private data class AddFrags(val fragsCount: Int) @@ -613,7 +653,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 九个已迁移切片一次归约 + // ⑭ 根 reducer:89 个槽位的委派表 + 十个已迁移切片一次归约 run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -677,13 +717,14 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 九个已迁移字段,其余 80 个待迁", + "根:迁移表 = 十个已迁移字段,其余 79 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", + "garagePreview", ) && - RootWiring.Unmigrated.size == 80 + RootWiring.Unmigrated.size == 79 ) // 一次 ReduceRoot 里八个切片各归约自己的动作,互不串片 @@ -739,7 +780,7 @@ object StateCheck { S5.User == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 9/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 10/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -1954,6 +1995,303 @@ object StateCheck { ) } + // ⑳ 切片 reducer:GaragePreview(原 module_10032 的 IG / fs.eru,22 字段;25 个顶层臂) + run { + val S0 = GaragePreviewStateSlice() + Check( + "切片 GaragePreview:默认值与原构造一致(22 字段)", + !S0.IsVisible && !S0.PhotoMode && !S0.NeedVisible && + S0.ViewStyle == null && S0.DynViewStyle == null && + S0.TankPreviewSettings is TankPreviewSettings && !S0.NeedShowDrone && + S0.Params == null && S0.HullData == null && S0.WeaponData == null && + S0.DroneData == null && S0.GrenadeData == null && !S0.HasGrenades && + S0.HullEngineType == HullEngineType.TRACKED && + S0.TurretMechanism == TurretMechanism.ROTATING_TURRET && + S0.Paint == null && S0.Properties.isEmpty() && + S0.CaptureScreenshotAction == null && S0.CaptureVideoAction == null && + S0.Item3dModelActionCache.isEmpty() && S0.RequestedItems.isEmpty() && + S0.ExpectedItemId.isEmpty() + ) + + val Colors = EmissionColors(1, 2, 3, 4, "STRICT") + val OldSettings = TankPreviewSettings(cameraHeight = 111) + val NewSettings = TankPreviewSettings(cameraHeight = 777) + val Params = GaragePreviewModelCC(80, 1000, 1.57, -110, null, null) + val OldPaint = Coloring(null, TextureFrameAnimation(32, 4, 8, 16)) + val NewPaint = Coloring(null, TextureFrameAnimation(64, 8, 8, 32)) + val OldPart = TankPartData(resource = "old") + val Base = S0.copy( + IsVisible = true, + NeedVisible = false, + ViewStyle = "old", + DynViewStyle = "dyn-old", + TankPreviewSettings = OldSettings, + Params = Params, + HullData = OldPart, + WeaponData = OldPart, + DroneData = OldPart, + GrenadeData = OldPart, + Paint = OldPaint, + Properties = listOf("old"), + ) + + val ClearParams = ReduceGaragePreview(Base, ClearGaragePreviewParams()) + Check("切片 GaragePreview:ClearGaragePreviewParams 只清 params", ClearParams.Params == null) + + val Shown = ReduceGaragePreview(Base.copy(NeedVisible = true), ShowGaragePreview()) + Check( + "切片 GaragePreview:ShowGaragePreview 置 isVisible=true、needVisible=false", + Shown.IsVisible && !Shown.NeedVisible + ) + Check( + "切片 GaragePreview:HideGaragePreview 置 isVisible=false", + !ReduceGaragePreview(Base, HideGaragePreview()).IsVisible + ) + + val Req = ShowRequest( + viewStyle = "new", + tankPreviewSettings = NewSettings, + needShowDrone = true, + photoMode = true, + withDelay = false, + ) + val S1 = ReduceGaragePreview(Base, Req) + Check( + "切片 GaragePreview:ShowRequest(无延迟)保留 isVisible 并写 view/settings/photo/drone", + S1.IsVisible && !S1.NeedVisible && S1.ViewStyle == "new" && + S1.TankPreviewSettings == NewSettings && S1.PhotoMode && + S1.NeedShowDrone && S1.DynViewStyle == "dyn-old" && + S1.Params == Params && S1.HullData == OldPart + ) + val S2 = ReduceGaragePreview(Base, Req.copy(withDelay = true)) + Check( + "切片 GaragePreview:ShowRequest(延迟)置 isVisible=false、needVisible=true", + !S2.IsVisible && S2.NeedVisible && S2.ViewStyle == "new" + ) + + Check( + "切片 GaragePreview:UpdatePreviewSettings 只写 tankPreviewSettings", + ReduceGaragePreview(Base, UpdatePreviewSettings(NewSettings)).TankPreviewSettings == NewSettings + ) + Check( + "切片 GaragePreview:UpdateViewStyle 无字段,原 fru_1=undefined 被 trs 视作省略", + ReduceGaragePreview(Base, UpdateViewStyle()) == Base + ) + + val Init = ReduceGaragePreview(S0, InitGarageResources(Params)) + Check("切片 GaragePreview:InitGarageResources 写 params", Init.Params == Params) + + val Expected1 = ReduceGaragePreview(S0, SetExpectedItemId(PreviewHullCategory, 42L)) + Check( + "切片 GaragePreview:SetExpectedItemId 写分类键", + Expected1.ExpectedItemId[PreviewHullCategory] == 42L + ) + val Expected2 = ReduceGaragePreview(Expected1, SetExpectedItemId(PreviewHullCategory, null)) + Check( + "切片 GaragePreview:SetExpectedItemId(itemId=null) 删除分类键", + !Expected2.ExpectedItemId.containsKey(PreviewHullCategory) + ) + + Check( + "切片 GaragePreview:AddRequestedItem 加入 requestedItems", + ReduceGaragePreview(S0, AddRequestedItem(42L)).RequestedItems == setOf(42L) + ) + + val HullAction = SetHullData( + itemId = 42L, + hullEngineType = HullEngineType.HOVER, + resource = "hull", + emissionColors = Colors, + turretMountPoint = "mount", + skinOffset = "skin", + leftTrackTexToWorldRatio = 7, + rightTrackTexToWorldRatio = 8, + ) + val HullBase = S0.copy(RequestedItems = setOf(42L)) + val HullHit = ReduceGaragePreview(HullBase, HullAction) + Check( + "切片 GaragePreview:SetHullData 命中时写 hullData/hullEngineType、缓存动作并删 requested", + HullHit.HullData == TankPartData("hull", Colors, "mount", "skin", 7, 8) && + HullHit.HullEngineType == HullEngineType.HOVER && + HullHit.Item3dModelActionCache[42L] == HullAction && + HullHit.RequestedItems.isEmpty() + ) + val HullMiss = ReduceGaragePreview( + HullBase.copy(ExpectedItemId = mapOf(PreviewHullCategory to 99L)), + HullAction, + ) + Check( + "切片 GaragePreview:SetHullData 未命中只缓存动作,不写 hullData", + HullMiss.HullData == null && HullMiss.HullEngineType == HullEngineType.TRACKED && + HullMiss.Item3dModelActionCache[42L] == HullAction && + HullMiss.RequestedItems.isEmpty() + ) + + val WeaponAction = SetWeaponData( + itemId = 43L, + turretMechanism = TurretMechanism.ELEVATING_BARREL, + resource = "weapon", + emissionColors = Colors, + ) + val WeaponBase = S0.copy(RequestedItems = setOf(43L)) + val WeaponHit = ReduceGaragePreview(WeaponBase, WeaponAction) + Check( + "切片 GaragePreview:SetWeaponData 命中时写 weaponData/turretMechanism、缓存动作并删 requested", + WeaponHit.WeaponData == TankPartData("weapon", Colors) && + WeaponHit.TurretMechanism == TurretMechanism.ELEVATING_BARREL && + WeaponHit.Item3dModelActionCache[43L] == WeaponAction && + WeaponHit.RequestedItems.isEmpty() + ) + val WeaponMiss = ReduceGaragePreview( + WeaponBase.copy(ExpectedItemId = mapOf(PreviewWeaponCategory to 99L)), + WeaponAction, + ) + Check( + "切片 GaragePreview:SetWeaponData 未命中只缓存动作,不写 weaponData", + WeaponMiss.WeaponData == null && + WeaponMiss.TurretMechanism == TurretMechanism.ROTATING_TURRET && + WeaponMiss.Item3dModelActionCache[43L] == WeaponAction + ) + + val DroneAction = SetDrone(44L, "drone", Colors) + val DroneHit = ReduceGaragePreview(S0.copy(RequestedItems = setOf(44L)), DroneAction) + Check( + "切片 GaragePreview:SetDrone 命中时写 droneData 并删 requested", + DroneHit.DroneData == TankPartData("drone", Colors) && + DroneHit.Item3dModelActionCache[44L] == DroneAction && + DroneHit.RequestedItems.isEmpty() + ) + val DroneMiss = ReduceGaragePreview( + S0.copy( + DroneData = OldPart, + ExpectedItemId = mapOf(PreviewDroneCategory to 99L), + ), + DroneAction, + ) + Check( + "切片 GaragePreview:SetDrone 未命中保留 droneData", + DroneMiss.DroneData == OldPart && DroneMiss.Item3dModelActionCache[44L] == DroneAction + ) + Check( + "切片 GaragePreview:SetDrone(resource=null) 命中时写 null", + ReduceGaragePreview(S0.copy(DroneData = OldPart), SetDrone(45L, null, Colors)).DroneData == null + ) + + Check( + "切片 GaragePreview:SetGrenadeData 写 grenadeData", + ReduceGaragePreview(S0, SetGrenadeData("grenade", Colors)).GrenadeData == + TankPartData("grenade", Colors) + ) + Check( + "切片 GaragePreview:SetGrenadeData(resource=null) 写 null", + ReduceGaragePreview(S0.copy(GrenadeData = OldPart), SetGrenadeData(null, Colors)).GrenadeData == null + ) + + Check("切片 GaragePreview:ClearHullData", ReduceGaragePreview(Base, ClearHullData()).HullData == null) + Check("切片 GaragePreview:ClearWeaponData", ReduceGaragePreview(Base, ClearWeaponData()).WeaponData == null) + Check("切片 GaragePreview:ClearDroneData", ReduceGaragePreview(Base, ClearDroneData()).DroneData == null) + Check("切片 GaragePreview:ClearGrenadeData", ReduceGaragePreview(Base, ClearGrenadeData()).GrenadeData == null) + + Check( + "切片 GaragePreview:SetHasGrenades", + ReduceGaragePreview(S0, SetHasGrenades()).HasGrenades + ) + Check( + "切片 GaragePreview:ClearHasGrenades", + !ReduceGaragePreview(S0.copy(HasGrenades = true), ClearHasGrenades()).HasGrenades + ) + + val PaintAction = SetPaint(46L, NewPaint) + val PaintHit = ReduceGaragePreview(S0.copy(RequestedItems = setOf(46L)), PaintAction) + Check( + "切片 GaragePreview:SetPaint 命中时写 paint、缓存动作并删 requested", + PaintHit.Paint == NewPaint && PaintHit.Item3dModelActionCache[46L] == PaintAction && + PaintHit.RequestedItems.isEmpty() + ) + val PaintMiss = ReduceGaragePreview( + S0.copy( + Paint = OldPaint, + ExpectedItemId = mapOf(PreviewPaintCategory to 99L), + ), + PaintAction, + ) + Check( + "切片 GaragePreview:SetPaint 未命中保留 paint", + PaintMiss.Paint == OldPaint && PaintMiss.Item3dModelActionCache[46L] == PaintAction + ) + + Check( + "切片 GaragePreview:ResetItem3dModelActionCache 删缓存", + !ReduceGaragePreview(PaintHit, ResetItem3dModelActionCache(46L)) + .Item3dModelActionCache.containsKey(46L) + ) + Check( + "切片 GaragePreview:SetMountedItemsProperties 写 properties", + ReduceGaragePreview(S0, SetMountedItemsProperties(listOf("ROTATING_WEAPON", "HOVER_CHASSIS"))) + .Properties == listOf("ROTATING_WEAPON", "HOVER_CHASSIS") + ) + + var Shots = 0 + var Videos = 0 + val Capture = ReduceGaragePreview(S0, SetCaptureActions({ Shots += 1 }, { Videos += 1 })) + Check( + "切片 GaragePreview:SetCaptureActions 写两个回调", + Capture.CaptureScreenshotAction != null && Capture.CaptureVideoAction != null + ) + Check( + "切片 GaragePreview:CaptureScreenshot 只调用回调、状态不变", + ReduceGaragePreview(Capture, CaptureScreenshot()) == Capture && Shots == 1 + ) + Check( + "切片 GaragePreview:CaptureVideo 只调用回调、状态不变", + ReduceGaragePreview(Capture, CaptureVideo()) == Capture && Videos == 1 + ) + + Check( + "切片 GaragePreview:无关动作返回等值状态", + ReduceGaragePreview(Base, AddFrags(1)) == Base + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, AddRequestedItem(7L)) + Check( + "根:AddRequestedItem 只动 garagePreview", + Root1.GaragePreview.RequestedItems == setOf(7L) && Root1.Garage == Root0.Garage && + Root1.Shop == Root0.Shop && Root1.Screen == Root0.Screen && Root1.LootBoxes == Root0.LootBoxes + ) + val Root2 = ReduceRoot(Root1, SetHasGrenades()) + Check( + "根:SetHasGrenades 保留 garagePreview 前一次归约", + Root2.GaragePreview.HasGrenades && Root2.GaragePreview.RequestedItems == setOf(7L) + ) + Check("根:garagePreview 迁移后无关动作仍返回等值状态", ReduceRoot(Root2, AddFrags(1)) == Root2) + + val W = RootWiring.ByField("garagePreview")!! + Check( + "根:garagePreview 委派到 IG / fs.eru 且 22 个动作全部具名", + W.SliceClass == "IG" && W.Reducer == "fs.eru" && W.Actions.size == 22 && + W.Actions.none { it.startsWith("?") } + ) + Check( + "根:garagePreview 的动作名覆盖 22 个 instanceof 臂", + listOf( + "AddRequestedItem", "CaptureScreenshot", "CaptureVideo", "ClearDroneData", + "ClearGrenadeData", "ClearHasGrenades", "ClearHullData", "ClearWeaponData", + "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", + "SetDrone", "SetExpectedItemId", "SetGrenadeData", "SetHasGrenades", + "SetHullData", "SetMountedItemsProperties", "SetPaint", "SetWeaponData", + "ShowRequest", "UpdatePreviewSettings", "UpdateViewStyle", + ).all { it in W.Actions } + ) + Check( + "根:garagePreview 的动作表不含 IG 标识和 3 个单例臂", + W.Actions.none { + it == "IG" || it == "GaragePreview" || + it == "ClearGaragePreviewParams" || it == "ShowGaragePreview" || it == "HideGaragePreview" + } + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)