diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 7ed9f1a..0c74446 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` 动作名。 - - 已迁移切片 **6/89**: + - 已迁移切片 **7/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) @@ -153,9 +153,14 @@ 嵌套 `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` **120 项全绿**,含 89 槽位完整性、六片一次归约互不串片、未迁移字段透传, + - `screen` → `WK` / `dY().ks5`(`ScreenReducer.kt`):10 字段,13 个 `instanceof` 臂全量翻译。 + `GoTo` 的新栈规则 = `isResetBackStackOnNextGoTo ? [] : history ? stack + current : stack`, + `current` 换成目标屏并清 `isResetBackStackOnNextGoTo` / `isDelayedGoto`;`screen_show` analytics 是副作用,纯 reducer 只保留状态。 + 屏幕身份用 `App.State.ScreenId`(`EntranceScreen` / `LoaderScreen` / `NamedScreen(name)`); + `LX` / `RX` 是屏幕标识类,已从动作表剔除并记入 `root_wiring.json` 的 `Markers`(`screen-id-class`)。 + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述七片,其余 **82 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 7/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **141 项全绿**,含 89 槽位完整性、七片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 2d1d74e..abd8cb0 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-22T18:11:10.717Z", + "generatedAt": "2026-10-22T19:22:01.390Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -12,7 +12,8 @@ "twitchState", "home", "localBattleUserState", - "shop" + "shop", + "screen" ], "AnonymousActions": [ { @@ -123,6 +124,60 @@ "Name": "RemoveDiscountItemForItem", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "U7/w3.discountItems 删除键 ct3_1" + }, + { + "Local": "sC", + "Name": "DisableForeground", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "foreground = false(`n.vs4(cd, cd, !1)`)" + }, + { + "Local": "oC", + "Name": "EnableForeground", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "foreground = true(`n.vs4(cd, cd, !0)`)" + }, + { + "Local": "eY", + "Name": "ClearScreenStack", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "screenStack = emptyList()(`n.vs4(cd, cd, cd, md())`)" + }, + { + "Local": "uY", + "Name": "ResetScreenStackOnNextGoTo", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isResetBackStackOnNextGoTo = true(第 6 个构造参数 !0)" + }, + { + "Local": "sY", + "Name": "DelayNextGoTo", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isDelayedGoto = true(第 7 个构造参数 !0)" + }, + { + "Local": "oY", + "Name": "FilterScreenStack", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "screenStack = stack.filter(predicate)(`for (…) e(o) && u.y(o)`,默认谓词 YK 恒 false)" + }, + { + "Local": "lY", + "Name": "EnableBack", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "enableBack = true(第 5 个构造参数 !0)" + }, + { + "Local": "cY", + "Name": "DisableBack", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "enableBack = false(第 5 个构造参数 !1)" + }, + { + "Local": "fY", + "Name": "PopScreen", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)" } ], "Markers": [ @@ -135,6 +190,16 @@ "Local": "x4", "Kind": "marker-action", "Evidence": "`fd(x4, \"\", cd, Wm)`;根 reducer 的 `Xd(t, $4) || t instanceof x4` 同样走 U7+w3 快路径,本身无字段、无独立效果。" + }, + { + "Local": "LX", + "Kind": "screen-id-class", + "Evidence": "屏幕标识类 Entrance:`function LX(){ this.gs5_1=\"Entrance\" }`,`ld(LX).m()` 返回 \"Entrance\";GoTo 分支用它判断 analytics 事件是否 isModal。" + }, + { + "Local": "RX", + "Kind": "screen-id-class", + "Evidence": "屏幕标识类 System.Loader:`function RX(){ this.ysb_1=\"System.Loader\" }`,`PX()` 返回该单例,也是 WK.current 的默认值;GoTo 到它时不发 screen_show。" } ], "Rows": [ @@ -183,24 +248,22 @@ "Reducer": "dY().ks5", "Delegate": "dY().ks5(t, n.zpu_1)", "Actions": [ - "?LX", - "?RX", - "?cY", - "?eY", - "?fY", - "?lY", - "?oC", - "?oY", - "?sC", - "?sY", - "?uY", + "ClearScreenStack", + "DelayNextGoTo", + "DisableBack", + "DisableForeground", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", + "EnableBack", + "EnableForeground", + "FilterScreenStack", "GoTo", - "PressedAnyButton" + "PopScreen", + "PressedAnyButton", + "ResetScreenStackOnNextGoTo" ], "Default": "new WK(PX())", - "Migrated": false + "Migrated": true }, { "Field": "modalScreen", diff --git a/3DTank/tools/gen_action_kt.js b/3DTank/tools/gen_action_kt.js index 538cb89..8a5493b 100644 --- a/3DTank/tools/gen_action_kt.js +++ b/3DTank/tools/gen_action_kt.js @@ -563,6 +563,14 @@ function InferDeep(Expr, Mod, Off, Depth) { return null; } +// 个别字段的调用点/运行时证据只能是 Any?(例如 GoTo 样例里的 screen=[object Object]), +// 但切片 reducer 已经能给出强类型时在这里显式指定。键:`类名.字段`。 +// GoTo.screen:QK.grm_1 在 screen 切片里只按 ScreenId 使用(instanceof LX/RX + m()/equals), +// 类型定义见 TankiOffline/.../App/State/ScreenId.kt。 +const FieldTypeOverrides = new Map([ + ['GoTo.screen', 'ScreenId'], +]); + const Emitted = []; // 动作目录:类名 + 字段(名, 类型),用于生成 ActionCatalog.kt(运行时按名字查类) const Catalog = []; @@ -639,6 +647,8 @@ for (const R of Inventory.Rows) { if (C.Ok) Type = C.Type; else { Note = '// 未收录类型: ' + C.Bad + '(' + Source + ')'; Type = null; } } + const Override = FieldTypeOverrides.get(R.Name + '.' + F); + if (Override) { Type = Override; Source = 'override'; Note = ''; } if (Type) TypedFields++; if (Stats && !Type) { for (const S of Sites) { diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 9fce9f4..d054255 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -58,6 +58,23 @@ const AnonymousActionNames = new Map([ ['s5', 'InitPaymentModeAsShopItem'], ['o5', 'SetupDiscountItemForItem'], ['_5', 'RemoveDiscountItemForItem'], + // 屏幕切片(WK / wY.ks5 第 34950 行):没有 toString / 不在 action_inventory 的 9 个类。 + ['sC', 'DisableForeground'], + ['oC', 'EnableForeground'], + ['eY', 'ClearScreenStack'], + ['uY', 'ResetScreenStackOnNextGoTo'], + ['sY', 'DelayNextGoTo'], + ['oY', 'FilterScreenStack'], + ['lY', 'EnableBack'], + ['cY', 'DisableBack'], + ['fY', 'PopScreen'], +]); + +// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 +// 它们从 Actions 列表里剔除,改记到 root_wiring.json 的 Markers。 +const NonActionInstanceof = new Map([ + ['LX', '屏幕标识类 Entrance:`function LX(){ this.gs5_1="Entrance" }`,`ld(LX).m()` 返回 "Entrance";GoTo 分支用它判断 analytics 事件是否 isModal。'], + ['RX', '屏幕标识类 System.Loader:`function RX(){ this.ysb_1="System.Loader" }`,`PX()` 返回该单例,也是 WK.current 的默认值;GoTo 到它时不发 screen_show。'], ]); /** 动作局部类名 → 动作名;未收录的返回 null。 */ @@ -296,6 +313,7 @@ function BodyOfRef(Ref) { function NamesInMasked(Masked) { const Names = new Set(); for (const M of Masked.matchAll(/instanceof ([A-Za-z0-9_$]+)/g)) { + if (NonActionInstanceof.has(M[1])) continue; const N = ActionNameOf(M[1]); Names.add(N || '?' + M[1]); } @@ -414,6 +432,7 @@ const Migrated = { home: { Type: 'HomeStateSlice', Reduce: 'ReduceHome' }, localBattleUserState: { Type: 'LocalBattleUserStateSlice', Reduce: 'ReduceLocalBattleUser' }, shop: { Type: 'ShopStateSlice', Reduce: 'ReduceShop' }, + screen: { Type: 'ScreenStateSlice', Reduce: 'ReduceScreen' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -455,6 +474,19 @@ const ShopAnonymousEffects = { _5: 'U7/w3.discountItems 删除键 ct3_1', }; +// screen 切片(WK / wY.ks5)的匿名动作:按 reducer 分支效果定名。 +const ScreenAnonymousEffects = { + sC: 'foreground = false(`n.vs4(cd, cd, !1)`)', + oC: 'foreground = true(`n.vs4(cd, cd, !0)`)', + eY: 'screenStack = emptyList()(`n.vs4(cd, cd, cd, md())`)', + uY: 'isResetBackStackOnNextGoTo = true(第 6 个构造参数 !0)', + sY: 'isDelayedGoto = true(第 7 个构造参数 !0)', + oY: 'screenStack = stack.filter(predicate)(`for (…) e(o) && u.y(o)`,默认谓词 YK 恒 false)', + lY: 'enableBack = true(第 5 个构造参数 !0)', + cY: 'enableBack = false(第 5 个构造参数 !1)', + fY: 'screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -477,6 +509,12 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(ScreenAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), ], Markers: [ { @@ -489,6 +527,11 @@ fs.writeFileSync( Kind: 'marker-action', Evidence: '`fd(x4, "", cd, Wm)`;根 reducer 的 `Xd(t, $4) || t instanceof x4` 同样走 U7+w3 快路径,本身无字段、无独立效果。', }, + ...[...NonActionInstanceof].map(([Local, Evidence]) => ({ + Local, + Kind: 'screen-id-class', + Evidence, + })), ], Rows, }, diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt index a39908b..9f83f46 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt @@ -422,7 +422,7 @@ object ActionCatalog { ActionMeta("ReferralEventEnterPromoCodeStatus", ReferralEventEnterPromoCodeStatus::class, listOf(ActionFieldMeta("referralEventEnterCodeStatus", "Any?"))), ActionMeta("TemporaryCashExpiredInfo", TemporaryCashExpiredInfo::class, listOf(ActionFieldMeta("temporaryCashData", "Any?"))), ActionMeta("Screen", Screen::class, listOf(ActionFieldMeta("current", "Any?"), ActionFieldMeta("loaderBackgroundImage", "Any?"), ActionFieldMeta("foreground", "Any?"), ActionFieldMeta("screenStack", "Any?"), ActionFieldMeta("enableBack", "Any?"), ActionFieldMeta("isResetBackStackOnNextGoTo", "Any?"), ActionFieldMeta("isDelayedGoto", "Any?"), ActionFieldMeta("pressedAnyButton", "Any?"), ActionFieldMeta("displayMainEntranceScreen", "Any?"), ActionFieldMeta("needReauthorization", "Any?"))), - ActionMeta("GoTo", GoTo::class, listOf(ActionFieldMeta("screen", "Any?"), ActionFieldMeta("history", "Boolean"))), + ActionMeta("GoTo", GoTo::class, listOf(ActionFieldMeta("screen", "ScreenId"), ActionFieldMeta("history", "Boolean"))), ActionMeta("DisplayMainEntranceScreen", DisplayMainEntranceScreen::class, listOf(ActionFieldMeta("isDisplayed", "Boolean"))), ActionMeta("PressedAnyButton", PressedAnyButton::class, listOf(ActionFieldMeta("isPressed", "Boolean"))), ActionMeta("DisplayReauthorizationMessage", DisplayReauthorizationMessage::class, listOf(ActionFieldMeta("isDisplay", "Boolean"))), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt index ec32e58..2e6204b 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 个(已定类型 508 个) +// 动作/状态类共 816 个,字段 2150 个(已定类型 509 个) data class AnnouncementState( val isEnabled: Any?, // 仅 copy 转发(无类型信息) val modelLoaded: Any?, // 仅 copy 转发(无类型信息) @@ -2408,7 +2408,7 @@ data class Screen( ) data class GoTo( - val screen: Any?, // 未收录类型: Any?(runtime) + val screen: ScreenId, // override val history: Boolean, // runtime ) diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ScreenReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ScreenReducer.kt new file mode 100644 index 0000000..4530e70 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ScreenReducer.kt @@ -0,0 +1,112 @@ +// ScreenReducer.kt —— 屏幕导航切片(对应 module_10032 的 WK 状态 + wY.ks5 reducer) +// +// 证据链: +// 状态类 WK:`function WK(t, n, i, r, e, u, s, o, _, h)`,10 个字段,toString 名为 `Screen(...)`。 +// 默认值取自构造:current = PX()(LoaderScreen)、loaderBackgroundImage = null、 +// foreground = false、screenStack = md()(emptyList)、enableBack = true(`e === cd || e`)、 +// isResetBackStackOnNextGoTo / isDelayedGoto / pressedAnyButton / displayMainEntranceScreen / +// needReauthorization 均 false。 +// reducer:`ld(wY).ks5`(module_10032 第 34950 行)13 个 instanceof 臂。 +// +// sC → foreground = false oC → foreground = true +// eY → screenStack = [] uY → isResetBackStackOnNextGoTo = true +// sY → isDelayedGoto = true lY → enableBack = true +// cY → enableBack = false fY → screenStack = dropLast(1)(原 `f$(stack, 1)`) +// oY → screenStack = stack.filter(predicate)(原 `e(o) && u.y(o)`,kd() 是 ArrayList) +// QK → GoTo(见下) hY → pressedAnyButton = action.isPressed +// _Y → displayMainEntranceScreen = action.isDisplayed +// aY → needReauthorization = action.isDisplay +// +// GoTo 分支(原 IIFE,`ks5` 内调用参数为 (0, t, n)): +// 新栈 = isResetBackStackOnNextGoTo ? [] : history ? stack + current : stack +// current = 目标;enableBack 不变;isResetBackStackOnNextGoTo / isDelayedGoto 归 false。 +// 原代码还会用 `rk().boz(u)` 发一条 screen_show analytics(目标等于当前时不发, +// Entrance 带 isModal=true,System.Loader 不发);这是副作用,纯 reducer 只保留状态变化。 +// +// 无 toString 的 9 个类按 reducer 效果定名,原名写在注释里;名字同时登记在 +// 3DTank/tools/gen_root_wiring.js 的 AnonymousActionNames,重新生成 RootWiring 后显示。 +package App.State.Reducers + +import App.State.DisplayMainEntranceScreen +import App.State.DisplayReauthorizationMessage +import App.State.GoTo +import App.State.LoaderScreen +import App.State.PressedAnyButton +import App.State.ScreenId + +/** 原 `sC`:无字段;`foreground = false`。 */ +object DisableForeground + +/** 原 `oC`:无字段;`foreground = true`。 */ +object EnableForeground + +/** 原 `eY`:无字段;`screenStack = emptyList()`。 */ +object ClearScreenStack + +/** 原 `uY`:无字段;`isResetBackStackOnNextGoTo = true`。 */ +object ResetScreenStackOnNextGoTo + +/** 原 `sY`:无字段;`isDelayedGoto = true`。 */ +object DelayNextGoTo + +/** + * 原 `oY(predicate)`:只保留 predicate 为 true 的屏幕(`for (…) e(o) && u.y(o)`)。 + * 默认谓词是原 `YK`(恒 false),所以缺省调用会清空历史栈。 + */ +data class FilterScreenStack(val predicate: (ScreenId) -> Boolean = { false }) + +/** 原 `lY`:无字段;`enableBack = true`。 */ +object EnableBack + +/** 原 `cY`:无字段;`enableBack = false`。 */ +object DisableBack + +/** 原 `fY`:无字段;`screenStack = dropLast(1)`。 */ +object PopScreen + +/** + * 原 `WK` / `Screen`:`Screen(current, loaderBackgroundImage, foreground, screenStack, enableBack, + * isResetBackStackOnNextGoTo, isDelayedGoto, pressedAnyButton, displayMainEntranceScreen, needReauthorization)`。 + */ +data class ScreenStateSlice( + val Current: ScreenId = LoaderScreen, + /** 本 reducer 只 copy 转发、从不写;源码没有给出元素类型,保留 Any?。 */ + val LoaderBackgroundImage: Any? = null, + val Foreground: Boolean = false, + val ScreenStack: List = emptyList(), + val EnableBack: Boolean = true, + val IsResetBackStackOnNextGoTo: Boolean = false, + val IsDelayedGoto: Boolean = false, + val PressedAnyButton: Boolean = false, + val DisplayMainEntranceScreen: Boolean = false, + val NeedReauthorization: Boolean = false, +) + +/** 根 reducer 的 screen 分支(原 `dY().ks5(t, n.zpu_1)`)。 */ +fun ReduceScreen(State0: ScreenStateSlice, A: Any): ScreenStateSlice = when (A) { + is DisableForeground -> State0.copy(Foreground = false) + is EnableForeground -> State0.copy(Foreground = true) + is ClearScreenStack -> State0.copy(ScreenStack = emptyList()) + is ResetScreenStackOnNextGoTo -> State0.copy(IsResetBackStackOnNextGoTo = true) + is DelayNextGoTo -> State0.copy(IsDelayedGoto = true) + + is GoTo -> State0.copy( + Current = A.screen, + ScreenStack = when { + State0.IsResetBackStackOnNextGoTo -> emptyList() + A.history -> State0.ScreenStack + State0.Current + else -> State0.ScreenStack + }, + IsResetBackStackOnNextGoTo = false, + IsDelayedGoto = false, + ) + + is FilterScreenStack -> State0.copy(ScreenStack = State0.ScreenStack.filter(A.predicate)) + is EnableBack -> State0.copy(EnableBack = true) + is DisableBack -> State0.copy(EnableBack = false) + is PopScreen -> State0.copy(ScreenStack = State0.ScreenStack.dropLast(1)) + is PressedAnyButton -> State0.copy(PressedAnyButton = A.isPressed) + is DisplayMainEntranceScreen -> State0.copy(DisplayMainEntranceScreen = A.isDisplayed) + is DisplayReauthorizationMessage -> State0.copy(NeedReauthorization = A.isDisplay) + 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 0362fda..55a805f 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -9,8 +9,9 @@ // home → ReduceHome(s1 / 根 reducer 的 `(inline@home)` 分支) // localBattleUserState → ReduceLocalBattleUser(lE / `(inline@localBattleUserState)` 分支) // shop → ReduceShop(j4 / `(inline@shop)` 分支) +// screen → ReduceScreen(WK / dY().ks5) // -// 其余 83 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 82 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -19,6 +20,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.ReduceScreen import App.State.Reducers.ReduceShop import App.State.Reducers.ReduceTwitch @@ -41,6 +43,7 @@ object RootReducer { Home = ReduceHome(State0.Home, Action), LocalBattleUserState = ReduceLocalBattleUser(State0.LocalBattleUserState, Action), Shop = ReduceShop(State0.Shop, Action), + Screen = ReduceScreen(State0.Screen, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index efb7af7..f6c6788 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.ScreenStateSlice import App.State.Reducers.ShopStateSlice import App.State.Reducers.TwitchStateSlice @@ -18,8 +19,8 @@ import App.State.Reducers.TwitchStateSlice data class RootState( /** 原 user(t8 / M8().qtd)。未迁移:原样透传。 */ val User: Any? = null, - /** 原 screen(WK / dY().ks5)。未迁移:原样透传。 */ - val Screen: Any? = null, + /** 原 screen(WK / dY().ks5)。已迁移。 */ + val Screen: ScreenStateSlice = ScreenStateSlice(), /** 原 modalScreen(nV / Fs.js1)。未迁移:原样透传。 */ val ModalScreen: Any? = null, /** 原 battleStatistics(jR / rr.fqr)。未迁移:原样透传。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 315e89a..12b6f4a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -15,7 +15,7 @@ data class SliceWiring( object RootWiring { val All: List = listOf( SliceWiring("user", "ypu_1", "t8", "M8().qtd", "M8().qtd(t, n.ypu_1)", listOf("?R8", "?p8", "?v8", "AddCoins", "AddRubies", "SetBatteryLevel", "SetClanInfo", "SetCoins", "SetDisabledWithoutPremiumLocations", "SetHasTemporaryCash", "SetPremiumExpirationTime", "SetRankNames", "SetRubies", "SetUserId", "SetUserPropertiesAction", "SpendCoins", "SpendCrystals", "SpendRubies", "UpdateClanTag", "UpdateCrystals", "UpdateEventPass", "UpdateGearScore", "UpdatePremium", "UpdateRank", "UpdateScore", "UpdateUid", "UpdateUserRating")), - SliceWiring("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("?LX", "?RX", "?cY", "?eY", "?fY", "?lY", "?oC", "?oY", "?sC", "?sY", "?uY", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "GoTo", "PressedAnyButton")), + SliceWiring("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("ClearScreenStack", "DelayNextGoTo", "DisableBack", "DisableForeground", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "EnableBack", "EnableForeground", "FilterScreenStack", "GoTo", "PopScreen", "PressedAnyButton", "ResetScreenStackOnNextGoTo")), SliceWiring("modalScreen", "apv_1", "nV", "Fs.js1", "Fs.js1(t, n.apv_1)", listOf("?XW", "AddModal", "RemoveCurrentByName", "RemoveModal")), SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("?DR", "?HR", "?KR", "?PR", "?QR", "?UR", "?XR", "?YR", "?ZR", "?nP", "?tP", "Init", "InitBattleModeWithLevels", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "UpdateFund", "UpdateScore")), SliceWiring("battleTutorial", "cpv_1", "rE", "Q.sq8", "Q.sq8(t, n.cpv_1)", listOf("?oE", "?sE", "SetFragsCount", "ShowMessage")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(由 RootReducer 真正归约)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ val Unmigrated: List get() = All.filter { it.Field !in Migrated } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/ScreenId.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/ScreenId.kt new file mode 100644 index 0000000..9362dbc --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/ScreenId.kt @@ -0,0 +1,30 @@ +// ScreenId.kt —— 屏幕标识(原 module_10032 的 XX 体系里,screen 切片真正用到的部分) +// +// 证据链: +// · WK.current 默认 `PX()` → `RX` 单例,`ld(RX).m()` 返回 "System.Loader"。 +// · 根 reducer 的 GoTo 分支只对目标做 `instanceof LX`(Entrance,`m()` = "Entrance") +// 与 `instanceof RX`(System.Loader)判别,其余屏幕只取 `m()` 当 analytics 的 screen 名。 +// · 其余屏幕类(UX=Shop、FX=Garage、eX=Lobby.Home …)的字段只在各自切片里使用; +// 在那些切片迁移之前,这里先用「名字」承载身份,避免凭空补一层用不到的字段。 +// +// 因此 ScreenId 只保留该 reducer 需要的三种能力:名字(analytics)、Entrance/Loader 判别、相等性。 +// 注意:同名的两个不同 Shop 类别在真实包里有不同字段,这里会折叠成同一个 NamedScreen; +// 等 shop 屏幕对象(UX)本身成为状态切片时再替换成带字段的类。 +package App.State + +sealed interface ScreenId { + val name: String +} + +/** 原 `LX`:`m() = "Entrance"`。 */ +object EntranceScreen : ScreenId { + override val name: String = "Entrance" +} + +/** 原 `RX`:`m() = "System.Loader"`,也是 `WK` 的默认 current(`PX()`)。 */ +object LoaderScreen : ScreenId { + override val name: String = "System.Loader" +} + +/** 其余屏幕:`m()` 返回带层级的路径,例如 "Lobby.Home" / "Lobby.Quests" / "Shop" / "Garage"。 */ +data class NamedScreen(override val name: String) : ScreenId diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index f3d9971..74d656d 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -9,8 +9,8 @@ // ⑥ 动作目录:816 个类与 138 个真实派发名对齐 // ⑦ 回放:真实 toString 样例的类/字段/类型校验 // ⑧ 全目录动作类都能按 KClass 路由 -// ⑨~⑮ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / -// Shop(含 U7 类别表与 w3 物品表两个子 reducer);⑭ 根 reducer 的 89 槽位委派 +// ⑨~⑰ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / +// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) package App @@ -126,6 +126,24 @@ import App.State.SetReArmorTemporaryDisabled import App.State.SetRememberMe import App.State.SetStatus import App.State.SetTwitchEnabled +import App.State.DisplayMainEntranceScreen +import App.State.DisplayReauthorizationMessage +import App.State.EntranceScreen +import App.State.GoTo +import App.State.LoaderScreen +import App.State.NamedScreen +import App.State.PressedAnyButton +import App.State.Reducers.ClearScreenStack +import App.State.Reducers.DelayNextGoTo +import App.State.Reducers.DisableBack +import App.State.Reducers.DisableForeground +import App.State.Reducers.EnableBack +import App.State.Reducers.EnableForeground +import App.State.Reducers.FilterScreenStack +import App.State.Reducers.PopScreen +import App.State.Reducers.ReduceScreen +import App.State.Reducers.ResetScreenStackOnNextGoTo +import App.State.Reducers.ScreenStateSlice private data class AddFrags(val fragsCount: Int) private data class SetScreen(val screen: String) @@ -563,14 +581,14 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 六个已迁移字段,其余 83 个待迁", + "根:迁移表 = 七个已迁移字段,其余 82 个待迁", RootReducer.Migrated == - setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop") && - RootWiring.Unmigrated.size == 83 + setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen") && + RootWiring.Unmigrated.size == 82 ) - // 一次 ReduceRoot 里五个切片各归约自己的动作,互不串片 - val S0 = RootState(User = "user-1", Screen = "Preloader") + // 一次 ReduceRoot 里六个切片各归约自己的动作,互不串片 + val S0 = RootState(User = "user-1", Screen = ScreenStateSlice(Current = NamedScreen("Preloader"))) val S1 = ReduceRoot(S0, ApplyInvite("uid-1")) Check( "根:ApplyInvite 只动 inviteState", @@ -619,10 +637,10 @@ object StateCheck { // 未迁移字段原样透传;无关动作不改变任何字段 Check( "根:未迁移字段原样透传", - S5.User == "user-1" && S5.Screen == "Preloader" + S5.User == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 6/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 7/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -924,6 +942,131 @@ object StateCheck { // 69 个动作里只有 x4 是标记接口(没有独立效果),其余都已有名字 val ShopUnknown = RootWiring.ByField("shop")!!.Actions.filter { it.startsWith("?") } Check("shop 回放:唯一未具名的是标记 x4", ShopUnknown == listOf("?x4")) + + // ⑰ 切片 reducer:Screen(原 module_10032 的 WK / wY.ks5,13 个 instanceof 臂) + run { + val Lobby = NamedScreen("Lobby.Home") + val Shop = NamedScreen("Shop") + + val S0 = ScreenStateSlice() + Check( + "切片 Screen:默认值与原构造一致(current=System.Loader、enableBack=true)", + S0.Current == LoaderScreen && S0.LoaderBackgroundImage == null && !S0.Foreground && + S0.ScreenStack.isEmpty() && S0.EnableBack && !S0.IsResetBackStackOnNextGoTo && + !S0.IsDelayedGoto && !S0.PressedAnyButton && !S0.DisplayMainEntranceScreen && + !S0.NeedReauthorization + ) + + Check( + "切片 Screen:前景开关只改 foreground", + !ReduceScreen(S0, DisableForeground).Foreground && + ReduceScreen(S0, EnableForeground).Foreground && + ReduceScreen(S0, EnableForeground).Current == LoaderScreen + ) + + val S1 = S0.copy(Current = Lobby, ScreenStack = listOf(NamedScreen("Preloader"))) + Check( + "切片 Screen:ClearScreenStack 清历史但保留 current", + ReduceScreen(S1, ClearScreenStack).let { it.ScreenStack.isEmpty() && it.Current == Lobby } + ) + + val S2 = ReduceScreen(S1, GoTo(Shop, false)) + Check( + "切片 Screen:GoTo 无 history 只换 current", + S2.Current == Shop && S2.ScreenStack == S1.ScreenStack + ) + + val S3 = ReduceScreen(S1, GoTo(Shop, true)) + Check( + "切片 Screen:GoTo history 把原 current 压栈", + S3.Current == Shop && S3.ScreenStack == S1.ScreenStack + Lobby + ) + Check( + "切片 Screen:GoTo 到当前屏仍按 history 压栈(原包只在 analytics 里去重)", + ReduceScreen(S1, GoTo(Lobby, true)).ScreenStack == listOf(NamedScreen("Preloader"), Lobby) + ) + + val S4 = ReduceScreen(S3.copy(IsResetBackStackOnNextGoTo = true), GoTo(Lobby, true)) + Check( + "切片 Screen:复位标志优先于 history,且 GoTo 后归 false", + S4.Current == Lobby && S4.ScreenStack.isEmpty() && !S4.IsResetBackStackOnNextGoTo + ) + Check( + "切片 Screen:GoTo 清 isDelayedGoto", + !ReduceScreen(S3.copy(IsDelayedGoto = true), GoTo(Shop, false)).IsDelayedGoto + ) + + Check( + "切片 Screen:ResetScreenStackOnNextGoTo / DelayNextGoTo 各置本标志", + ReduceScreen(S0, ResetScreenStackOnNextGoTo).IsResetBackStackOnNextGoTo && + ReduceScreen(S0, DelayNextGoTo).IsDelayedGoto + ) + + val S5 = S1.copy(ScreenStack = listOf(NamedScreen("Battle"), Lobby, Shop)) + Check( + "切片 Screen:FilterScreenStack 只保留 predicate 命中项", + ReduceScreen(S5, FilterScreenStack { it.name == "Battle" }).ScreenStack == + listOf(NamedScreen("Battle")) + ) + Check( + "切片 Screen:FilterScreenStack 缺省谓词(原 YK)清空历史", + ReduceScreen(S5, FilterScreenStack()).ScreenStack.isEmpty() + ) + + Check( + "切片 Screen:EnableBack / DisableBack 切返回键", + ReduceScreen(S0.copy(EnableBack = false), EnableBack).EnableBack && + !ReduceScreen(S0, DisableBack).EnableBack + ) + Check( + "切片 Screen:PopScreen 丢最后一个,空栈安全", + ReduceScreen(S1, PopScreen).ScreenStack.isEmpty() && + ReduceScreen(S0, PopScreen).ScreenStack.isEmpty() + ) + Check( + "切片 Screen:PressedAnyButton 写 action.isPressed(false 也生效)", + !ReduceScreen(S0.copy(PressedAnyButton = true), PressedAnyButton(false)).PressedAnyButton && + ReduceScreen(S0, PressedAnyButton(true)).PressedAnyButton + ) + Check( + "切片 Screen:DisplayMainEntranceScreen / DisplayReauthorizationMessage 各写本字段", + ReduceScreen(S0, DisplayMainEntranceScreen(true)).DisplayMainEntranceScreen && + ReduceScreen(S0, DisplayReauthorizationMessage(true)).NeedReauthorization + ) + Check("切片 Screen:无关动作不改状态", ReduceScreen(S1, AddFrags(1)) == S1) + + // 根:screen 动作只动 screen,且与已迁移的其余六片共存 + val R0 = RootState() + val R1 = ReduceRoot(R0, GoTo(Shop, false)) + Check( + "根:GoTo 只动 screen", + R1.Screen.Current == Shop && R1.User == null && R1.Shop == R0.Shop && + R1.InviteState == R0.InviteState && R1.Home == R0.Home + ) + val R2 = ReduceRoot(R1, EnableForeground) + Check( + "根:EnableForeground 只动 screen 前景位", + R2.Screen.Foreground && R2.Screen.Current == Shop && + R2.Screen.ScreenStack == R1.Screen.ScreenStack && R2.Shop == R1.Shop + ) + Check("根:screen 迁移后无关动作仍返回等值状态", ReduceRoot(R2, AddFrags(1)) == R2) + + val W = RootWiring.ByField("screen")!! + Check( + "根:screen 委派到 WK / dY().ks5 且 13 个动作全部具名", + W.SliceClass == "WK" && W.Reducer == "dY().ks5" && W.Actions.size == 13 && + W.Actions.none { it.startsWith("?") } + ) + Check( + "根:screen 的动作名覆盖 13 个 instanceof 臂", + listOf( + "DisableForeground", "EnableForeground", "ClearScreenStack", + "ResetScreenStackOnNextGoTo", "DelayNextGoTo", "GoTo", "FilterScreenStack", + "EnableBack", "DisableBack", "PopScreen", "PressedAnyButton", + "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", + ).all { it in W.Actions } + ) + } } val Failed = Results.filter { !it.second }