diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index bf0f9d1..3aa3561 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` 动作名。 - - 已迁移切片 **14/89**: + - 已迁移切片 **18/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) @@ -197,12 +197,25 @@ - `settings` → `Uut` / `xw.ltg`(`SettingsReducer.kt`):2 个字段,2 个 `instanceof` 臂。 `currentCategory` 是设置页分类枚举(`ACCOUNT`…`SUPPORT`,默认 `GAME`), `disableAccountSettings` 默认 false;`Fut`→`SetSettingsCategory`、`Gut`→`DisableAccountSettings`。 + - `accountSettings` → `oet` / `Lp.mtg`(`AccountSettingsReducer.kt`):13 字段,13 个 `instanceof` 臂。 + `newPasswordStatus` 用 `PasswordStatus`(`NONE/OK/SIMPLE/NOT_EQUALS`); + `ResetPasswordFields` 只清三个密码输入、状态和解锁按钮;`removalTimer` 存时间戳 `Long?`; + `locationCheckEnabled` 没有写入臂;11 个匿名动作按效果定名并登记。 + - `proBattle` → `Wtt` / `uv.wth`(`ProBattleReducer.kt`):3 字段,2 个 `instanceof` 臂。 + 默认 `lastFightBattleId=-1`、`lastFightBattleTeam=NONE`、`battlePasswords=emptyMap()`; + `Fight` 写 id/team,`SetPassword` 空串删键、非空覆盖。登录切片的 `SetPassword` 是不同类, + 这里用 `App.State.Reducers.SetPassword(battleId, password)`,避免丢 `battleId`。 + - `language` → 枚举值 / `m6().jtg`(`LanguageReducer.kt`):`language` 在根状态里是枚举对象本身, + 默认 `EN`;`d6`→`SetLanguage` 只写回语言,不包 StateSlice。 + - `announcement` → `lC` / `PC().rq5`(`AnnouncementReducer.kt`):5 字段,4 个 `instanceof` 臂。 + `isEnabled` 缺省 true,其余 false/空列表;`bC`→`DisableAnnouncement`、`qC`→`SetAnnouncementModelLoaded`、 + `CC`→`SetAnnouncementItems`(生成物 `Init` 属于另一个切片,已改名)、`$C`→`SetSkipNextPromotedAnnounce`。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 13 个有 reducer 的切片, - `launchParams` 与其余 **75 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 14/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **325 项全绿**,含 89 槽位完整性、13 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 17 个有 reducer 的切片, + `launchParams` 与其余 **71 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 18/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **350 项全绿**,含 89 槽位完整性、17 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -212,7 +225,8 @@ 效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示); shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等); screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、 - entrance 的 6 个、user 的 3 个、settings 的 2 个匿名类同样按效果定名并登记。 + entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、 + language 的 1 个、announcement 的 3 个匿名类同样按效果定名并登记。 ### 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 5fa5d31..3a956d4 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-23T09:11:41.133Z", + "generatedAt": "2026-10-23T09:52:15.365Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -20,6 +20,10 @@ "entrance", "user", "settings", + "accountSettings", + "proBattle", + "language", + "announcement", "launchParams" ], "AnonymousActions": [ @@ -527,6 +531,96 @@ "Name": "DisableAccountSettings", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "disableAccountSettings = true(`n.bu8(cd, !0)`)" + }, + { + "Local": "aet", + "Name": "ResetPasswordFields", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "currentPassword/newPassword/repeatNewPassword = \"\",newPasswordStatus = NONE,isUnlockButtonForRenameUid = false" + }, + { + "Local": "fet", + "Name": "SetCurrentPassword", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "currentPassword = action.currentPassword" + }, + { + "Local": "vet", + "Name": "SetNewPassword", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "newPassword = action.newPassword" + }, + { + "Local": "pet", + "Name": "SetRepeatNewPassword", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "repeatNewPassword = action.repeatNewPassword" + }, + { + "Local": "wet", + "Name": "SetPasswordStatus", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "newPasswordStatus = action.status" + }, + { + "Local": "det", + "Name": "SetCurrentPasswordMandatory", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isCurrentPasswordMandatory = action.isMandatory" + }, + { + "Local": "get", + "Name": "SetEmail", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "email = action.email" + }, + { + "Local": "met", + "Name": "SetEmailConfirmed", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "emailConfirmed = action.emailConfirmed" + }, + { + "Local": "cet", + "Name": "SetCountry", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "countryCode / countryName = action 的两个字段" + }, + { + "Local": "ket", + "Name": "SetUnlockButtonForRenameUid", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isUnlockButtonForRenameUid = action.isUnlockButtonForRenameUid" + }, + { + "Local": "xet", + "Name": "ClearAccountRemovalTimer", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "removalTimer = null" + }, + { + "Local": "d6", + "Name": "SetLanguage", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "language = action.language(`t instanceof d6 ? t.itg_1 : n`)" + }, + { + "Local": "bC", + "Name": "DisableAnnouncement", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isEnabled = false" + }, + { + "Local": "qC", + "Name": "SetAnnouncementModelLoaded", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "modelLoaded = true" + }, + { + "Local": "CC", + "Name": "SetAnnouncementItems", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "items = action.items" } ], "Markers": [ @@ -1131,22 +1225,22 @@ "Reducer": "Lp.mtg", "Delegate": "Lp.mtg(t, n.vpv_1)", "Actions": [ - "?aet", - "?cet", - "?det", - "?fet", - "?get", - "?ket", - "?met", - "?pet", - "?vet", - "?wet", - "?xet", + "ClearAccountRemovalTimer", "ClearInputUid", - "SetAccountRemovalDays" + "ResetPasswordFields", + "SetAccountRemovalDays", + "SetCountry", + "SetCurrentPassword", + "SetCurrentPasswordMandatory", + "SetEmail", + "SetEmailConfirmed", + "SetNewPassword", + "SetPasswordStatus", + "SetRepeatNewPassword", + "SetUnlockButtonForRenameUid" ], "Default": "new oet()", - "Migrated": false + "Migrated": true }, { "Field": "lootBoxes", @@ -1838,10 +1932,10 @@ "Reducer": "m6().jtg", "Delegate": "m6().jtg(t, n.gpx_1)", "Actions": [ - "?d6" + "SetLanguage" ], "Default": "tj()", - "Migrated": false + "Migrated": true }, { "Field": "launchParams", @@ -1969,13 +2063,13 @@ "Reducer": "PC().rq5", "Delegate": "PC().rq5(t, n.opx_1)", "Actions": [ - "?bC", - "?qC", - "Init", + "DisableAnnouncement", + "SetAnnouncementItems", + "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce" ], "Default": "new lC()", - "Migrated": false + "Migrated": true }, { "Field": "notificationByCountry", @@ -2165,7 +2259,7 @@ "SetPassword" ], "Default": "new Wtt()", - "Migrated": false + "Migrated": true }, { "Field": "availabilityByRank", diff --git a/3DTank/tools/gen_action_kt.js b/3DTank/tools/gen_action_kt.js index b724a00..477c490 100644 --- a/3DTank/tools/gen_action_kt.js +++ b/3DTank/tools/gen_action_kt.js @@ -569,6 +569,10 @@ function InferDeep(Expr, Mod, Off, Depth) { // 类型定义见 TankiOffline/.../App/State/ScreenId.kt。 const FieldTypeOverrides = new Map([ ['GoTo.screen', 'ScreenId'], + // proBattle 切片:Fight 的字段在别处调用点只能推成 Any?,reducer 需要真实类型。 + // battleId → Long,battleTeam → BattleTeam(module_31908 的 TEAM_A/TEAM_B/NONE)。 + ['Fight.battleId', 'Long'], + ['Fight.battleTeam', 'BattleTeam'], // lootBoxes 切片:lH/qH/yH 的字段在别处的调用点只能给出 Boolean / Any?,但 reducer 里分别是 // ContainerType(默认 Ly()=STANDARD)、可空 ContainerType、及补给箱奖励列表。 ['SetCurrentContainer.type', 'ContainerType'], diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 45d10a2..0868871 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -136,6 +136,28 @@ const AnonymousActionNames = new Map([ // 括号里是原 reducer 的效果;实现见 SettingsReducer.kt。 ['Fut', 'SetSettingsCategory'], ['Gut', 'DisableAccountSettings'], + // accountSettings 切片(oet / Lp.mtg):11 个没有 toString / 不在 action_inventory 的类。 + // 括号里是原 reducer 的效果;实现见 AccountSettingsReducer.kt。 + ['aet', 'ResetPasswordFields'], + ['fet', 'SetCurrentPassword'], + ['vet', 'SetNewPassword'], + ['pet', 'SetRepeatNewPassword'], + ['wet', 'SetPasswordStatus'], + ['det', 'SetCurrentPasswordMandatory'], + ['get', 'SetEmail'], + ['met', 'SetEmailConfirmed'], + ['cet', 'SetCountry'], + ['ket', 'SetUnlockButtonForRenameUid'], + ['xet', 'ClearAccountRemovalTimer'], + // language 切片(m6().jtg):1 个没有 toString / 不在 action_inventory 的类。 + // 实现见 LanguageReducer.kt。 + ['d6', 'SetLanguage'], + // announcement 切片(lC / PC().rq5):3 个没有 toString / 不在 action_inventory 的类。 + // CC 在 action_inventory 里叫 Init,但生成物 App.State.Init 是另一个切片的同名类,这里改名。 + // 实现见 AnnouncementReducer.kt。 + ['bC', 'DisableAnnouncement'], + ['qC', 'SetAnnouncementModelLoaded'], + ['CC', 'SetAnnouncementItems'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -149,7 +171,9 @@ const NonActionInstanceof = new Map([ /** 动作局部类名 → 动作名;未收录的返回 null。 */ function ActionNameOf(Local) { - return LocalToName.get(Local) || AnonymousActionNames.get(Local) || null; + // 匿名表优先:个别同名类(如 announcement 的 CC=Init)会用生成物里的名字, + // 但该生成物属于另一个切片,必须在匿名表里显式改名。 + return AnonymousActionNames.get(Local) || LocalToName.get(Local) || null; } // ---------------------------------------------------------------- 源码扫描基础 @@ -509,6 +533,16 @@ const Migrated = { entrance: { Type: 'EntranceStateSlice', Reduce: 'ReduceEntrance' }, user: { Type: 'UserStateSlice', Reduce: 'ReduceUser' }, settings: { Type: 'SettingsStateSlice', Reduce: 'ReduceSettings' }, + accountSettings: { Type: 'AccountSettingsStateSlice', Reduce: 'ReduceAccountSettings' }, + proBattle: { Type: 'ProBattleStateSlice', Reduce: 'ReduceProBattle' }, + // language 在原根状态里是枚举对象本身,不是对象切片;Default/Import 显式指定。 + language: { + Type: 'Language', + Default: 'App.State.Language.EN', + Reduce: 'ReduceLanguage', + Import: false, + }, + announcement: { Type: 'AnnouncementStateSlice', Reduce: 'ReduceAnnouncement' }, // 原根 reducer 对这一片没有调用,只有默认值 `ag()`(空 Map);这里仅做类型收口。 launchParams: { Type: 'LaunchParamsStateSlice', @@ -651,6 +685,33 @@ const SettingsAnonymousEffects = { Gut: 'disableAccountSettings = true(`n.bu8(cd, !0)`)', }; +// accountSettings 切片(oet / Lp.mtg):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。 +const AccountSettingsAnonymousEffects = { + aet: 'currentPassword/newPassword/repeatNewPassword = "",newPasswordStatus = NONE,isUnlockButtonForRenameUid = false', + fet: 'currentPassword = action.currentPassword', + vet: 'newPassword = action.newPassword', + pet: 'repeatNewPassword = action.repeatNewPassword', + wet: 'newPasswordStatus = action.status', + det: 'isCurrentPasswordMandatory = action.isMandatory', + get: 'email = action.email', + met: 'emailConfirmed = action.emailConfirmed', + cet: 'countryCode / countryName = action 的两个字段', + ket: 'isUnlockButtonForRenameUid = action.isUnlockButtonForRenameUid', + xet: 'removalTimer = null', +}; + +// language 切片(m6().jtg):只把动作里的语言枚举写回。 +const LanguageAnonymousEffects = { + d6: 'language = action.language(`t instanceof d6 ? t.itg_1 : n`)', +}; + +// announcement 切片(lC / PC().rq5):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。 +const AnnouncementAnonymousEffects = { + bC: 'isEnabled = false', + qC: 'modelLoaded = true', + CC: 'items = action.items', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -715,6 +776,24 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(AccountSettingsAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(LanguageAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(AnnouncementAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), ], Markers: [ { diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/AccountSettingsModels.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/AccountSettingsModels.kt new file mode 100644 index 0000000..6ba7291 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/AccountSettingsModels.kt @@ -0,0 +1,14 @@ +// AccountSettingsModels.kt —— accountSettings 切片用到的枚举(App.State 包内) +// +// 证据链:module_10032 的 `_et()` 初始化 +// `NONE=0, OK=1, SIMPLE=2, NOT_EQUALS=3`;`Ret()` 返回 NONE, +// 即 `oet.newPasswordStatus` 的默认值。 +package App.State + +/** 原 `het`:新密码校验状态,顺序与枚举 ordinal 一致。 */ +enum class PasswordStatus { + NONE, + OK, + SIMPLE, + NOT_EQUALS, +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt index a0634b3..af55db3 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/ActionCatalog.kt @@ -721,7 +721,7 @@ object ActionCatalog { ActionMeta("SelectBattle", SelectBattle::class, listOf(ActionFieldMeta("battleId", "Long"))), ActionMeta("SelectBattleFormat", SelectBattleFormat::class, listOf(ActionFieldMeta("format", "BattleFormat"))), ActionMeta("ProBattleState", ProBattleState::class, listOf(ActionFieldMeta("lastFightBattleId", "Any?"), ActionFieldMeta("lastFightBattleTeam", "Any?"), ActionFieldMeta("battlePasswords", "Any?"))), - ActionMeta("Fight", Fight::class, listOf(ActionFieldMeta("battleId", "Any?"), ActionFieldMeta("battleTeam", "Any?"))), + ActionMeta("Fight", Fight::class, listOf(ActionFieldMeta("battleId", "Long"), ActionFieldMeta("battleTeam", "BattleTeam"))), ActionMeta("Spectate", Spectate::class, listOf(ActionFieldMeta("battleId", "Any?"))), ActionMeta("CheckPassword", CheckPassword::class, listOf(ActionFieldMeta("battleId", "Any?"), ActionFieldMeta("password", "Any?"))), ActionMeta("GoToBattleInfo", GoToBattleInfo::class, listOf(ActionFieldMeta("battleId", "Any?"))), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt index bdfc1d7..8411280 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 个(已定类型 584 个) +// 动作/状态类共 816 个,字段 2150 个(已定类型 586 个) data class AnnouncementState( val isEnabled: Any?, // 仅 copy 转发(无类型信息) val modelLoaded: Any?, // 仅 copy 转发(无类型信息) @@ -4077,8 +4077,8 @@ data class ProBattleState( ) data class Fight( - val battleId: Any?, // 调用点: P2t().kar().paq_1.bpy_1.fto_1 | t.props.battleId - val battleTeam: Any?, // 调用点: P2t().kar().paq_1.bpy_1.gto_1 | t.props.battleTeam + val battleId: Long, // override + val battleTeam: BattleTeam, // override ) data class Spectate( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/LanguageModels.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/LanguageModels.kt new file mode 100644 index 0000000..d339a42 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/LanguageModels.kt @@ -0,0 +1,23 @@ +// LanguageModels.kt —— language 切片的值类型(App.State 包内) +// +// 证据链:module_52943 的 `lo()` 初始化 +// `EN=0, RU=1, DE=2, ES=3, PL=4, PT=5, ZH=6, FR=7, JA=8, TR=9, CS=10, HI=11`; +// `tj()` 返回 `lt`,即根状态 language 的默认值 EN。 +// language 在根状态里是枚举对象本身,不是对象切片,因此不额外包一层 StateSlice。 +package App.State + +/** 原 `fo`:客户端语言,顺序与枚举 ordinal 一致。 */ +enum class Language { + EN, + RU, + DE, + ES, + PL, + PT, + ZH, + FR, + JA, + TR, + CS, + HI, +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/ProBattleModels.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/ProBattleModels.kt new file mode 100644 index 0000000..f44beb7 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/ProBattleModels.kt @@ -0,0 +1,13 @@ +// ProBattleModels.kt —— proBattle 切片用到的枚举(App.State 包内) +// +// 证据链:module_31908 的 `Ir()` 初始化 +// `TEAM_A=0, TEAM_B=1, NONE=2`;module_10032 的 `em()` 返回 NONE, +// 即 `Wtt.lastFightBattleTeam` 的默认值。 +package App.State + +/** 原 `Ar`:上一场战斗的队伍,顺序与枚举 ordinal 一致。 */ +enum class BattleTeam { + TEAM_A, + TEAM_B, + NONE, +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AccountSettingsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AccountSettingsReducer.kt new file mode 100644 index 0000000..4dd3849 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AccountSettingsReducer.kt @@ -0,0 +1,88 @@ +// AccountSettingsReducer.kt —— 账户设置切片(对应 module_10032 的 oet 状态 + Lp.mtg reducer) +// +// 证据链: +// 状态类 oet:13 个字段,toString 名 "AccountSettings(...)";copy 是 nu6(13 字段 + 回调)。 +// 默认:isCurrentPasswordMandatory=false、三个密码字段空串、newPasswordStatus=NONE、 +// countryCode/countryName/email=null、emailConfirmed/locationCheckEnabled=false、 +// isUnlockButtonForRenameUid/isClearInputUid=false、removalTimer=null。 +// 根委派:`Lp.mtg(t, n.vpv_1)`。 +// 13 个 instanceof 臂见下方 reducer;`locationCheckEnabled` 没有写入臂。 +// `removalTimer` 原值由 `cm(t.au7_1)` 生成 Kotlin Date,这里存时间戳 Long?,语义仍是同一时刻。 +package App.State.Reducers + +import App.State.ClearInputUid +import App.State.PasswordStatus +import App.State.SetAccountRemovalDays + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `aet`:清空三个密码输入,把校验状态打回 NONE,并复位改名解锁按钮。 */ +object ResetPasswordFields + +class SetCurrentPassword(val currentPassword: String) + +class SetNewPassword(val newPassword: String) + +class SetRepeatNewPassword(val repeatNewPassword: String) + +class SetPasswordStatus(val status: PasswordStatus) + +class SetCurrentPasswordMandatory(val isMandatory: Boolean) + +class SetEmail(val email: String?) + +class SetEmailConfirmed(val emailConfirmed: Boolean) + +class SetCountry(val countryCode: String?, val countryName: String?) + +class SetUnlockButtonForRenameUid(val isUnlockButtonForRenameUid: Boolean) + +/** 原 `xet`:只把 removalTimer 置 null。 */ +object ClearAccountRemovalTimer + +/* ------------------------------------------------------------------ 状态 */ + +data class AccountSettingsStateSlice( + val IsCurrentPasswordMandatory: Boolean = false, + val CurrentPassword: String = "", + val NewPassword: String = "", + val RepeatNewPassword: String = "", + val NewPasswordStatus: PasswordStatus = PasswordStatus.NONE, + val CountryCode: String? = null, + val CountryName: String? = null, + val Email: String? = null, + val EmailConfirmed: Boolean = false, + val LocationCheckEnabled: Boolean = false, + val IsUnlockButtonForRenameUid: Boolean = false, + val IsClearInputUid: Boolean = false, + val RemovalTimer: Long? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceAccountSettings( + State0: AccountSettingsStateSlice, + Action: Any, +): AccountSettingsStateSlice = when (Action) { + is ResetPasswordFields -> State0.copy( + CurrentPassword = "", + NewPassword = "", + RepeatNewPassword = "", + NewPasswordStatus = PasswordStatus.NONE, + IsUnlockButtonForRenameUid = false, + ) + is SetCurrentPassword -> State0.copy(CurrentPassword = Action.currentPassword) + is SetNewPassword -> State0.copy(NewPassword = Action.newPassword) + is SetRepeatNewPassword -> State0.copy(RepeatNewPassword = Action.repeatNewPassword) + is SetPasswordStatus -> State0.copy(NewPasswordStatus = Action.status) + is SetCurrentPasswordMandatory -> State0.copy(IsCurrentPasswordMandatory = Action.isMandatory) + is SetEmail -> State0.copy(Email = Action.email) + is SetEmailConfirmed -> State0.copy(EmailConfirmed = Action.emailConfirmed) + is SetCountry -> State0.copy(CountryCode = Action.countryCode, CountryName = Action.countryName) + is SetUnlockButtonForRenameUid -> + State0.copy(IsUnlockButtonForRenameUid = Action.isUnlockButtonForRenameUid) + is ClearInputUid -> State0.copy(IsClearInputUid = Action.isClearInputUid) + is SetAccountRemovalDays -> State0.copy(RemovalTimer = Action.removalTimer) + is ClearAccountRemovalTimer -> State0.copy(RemovalTimer = null) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AnnouncementReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AnnouncementReducer.kt new file mode 100644 index 0000000..0719d3a --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AnnouncementReducer.kt @@ -0,0 +1,51 @@ +// AnnouncementReducer.kt —— 公告切片(对应 module_10032 的 lC 状态 + RC.rq5) +// +// 证据链: +// 状态类 lC:5 个字段,toString 名 "AnnouncementState(...)";copy 是 cq0(5 字段 + 回调)。 +// 默认:isEnabled=true、modelLoaded=false、items=emptyList()、wasInBattle=false、 +// skipNextPromotedAnnounce=false。 +// 根委派:`PC().rq5(t, n.opx_1)`。 +// reducer 四个 instanceof 臂: +// bC → isEnabled=false +// qC → modelLoaded=true +// CC → items=action.announcementItems(生成物里的 Init 是另一个切片的同名类,故单独建类) +// $C → skipNextPromotedAnnounce=action.skipShowNextAnnounce +// `wasInBattle` 没有写入臂。 +package App.State.Reducers + +import App.State.SetSkipNextPromotedAnnounce + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `bC`:关闭公告。 */ +object DisableAnnouncement + +/** 原 `qC`:公告模型已加载。 */ +object SetAnnouncementModelLoaded + +/** 原 `CC`:写入公告列表。 */ +class SetAnnouncementItems(val items: List) + +/* ------------------------------------------------------------------ 状态 */ + +data class AnnouncementStateSlice( + val IsEnabled: Boolean = true, + val ModelLoaded: Boolean = false, + val Items: List = emptyList(), + val WasInBattle: Boolean = false, + val SkipNextPromotedAnnounce: Boolean = false, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceAnnouncement( + State0: AnnouncementStateSlice, + Action: Any, +): AnnouncementStateSlice = when (Action) { + is DisableAnnouncement -> State0.copy(IsEnabled = false) + is SetAnnouncementModelLoaded -> State0.copy(ModelLoaded = true) + is SetAnnouncementItems -> State0.copy(Items = Action.items) + is SetSkipNextPromotedAnnounce -> + State0.copy(SkipNextPromotedAnnounce = Action.skipShowNextAnnounce) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LanguageReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LanguageReducer.kt new file mode 100644 index 0000000..f1d8817 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LanguageReducer.kt @@ -0,0 +1,20 @@ +// LanguageReducer.kt —— 语言切片(对应 module_10032 的 language 值 + g6.jtg) +// +// 证据链: +// 根状态 language 的默认值是 `tj()` → module_52943 的 `vo()` → EN。 +// reducer:`t instanceof d6 ? t.itg_1 : n`,即只把动作里的语言枚举写回。 +// language 在原根状态里就是枚举对象本身,因此 reducer 直接 (Language, Any) → Language。 +package App.State.Reducers + +import App.State.Language + +/* ------------------------------------------------------------------ 动作 */ + +class SetLanguage(val language: Language) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceLanguage(State0: Language, Action: Any): Language = when (Action) { + is SetLanguage -> Action.language + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ProBattleReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ProBattleReducer.kt new file mode 100644 index 0000000..58b026b --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ProBattleReducer.kt @@ -0,0 +1,48 @@ +// ProBattleReducer.kt —— 上一场战斗 / 战斗密码切片(对应 module_10032 的 Wtt 状态 + Jtt.wth) +// +// 证据链: +// 状态类 Wtt:3 个字段,toString 名 "ProBattleState(...)";copy 是 jto(3 字段 + 回调)。 +// 默认:lastFightBattleId=-1、lastFightBattleTeam=NONE、battlePasswords=emptyMap()。 +// 根委派:`uv.wth(t, n.bpy_1)`。 +// reducer: +// `t instanceof Vtt ? n.jto(t.lto_1, t.mto_1)`(Fight 只写 id + team) +// `t instanceof Xtt ? password 为空 ? n.jto(cd, cd, am(map, id)) : n.jto(cd, cd, cg(map, Vd(id, password)))` +// `SetPassword` 在登录切片也有同名类(qD,只有 password),这里按 proBattle 的 Xtt 建独立类, +// 避免复用生成物 App.State.SetPassword 导致丢 battleId。 +package App.State.Reducers + +import App.State.BattleTeam +import App.State.Fight + +/* ------------------------------------------------------------------ 动作 */ + +/** + * 原 `Xtt`:i = battleId,password = 新密码。 + * 密码为空时从 map 删除,否则覆盖写入。 + */ +class SetPassword(val battleId: Long, val password: String) + +/* ------------------------------------------------------------------ 状态 */ + +data class ProBattleStateSlice( + val LastFightBattleId: Long = -1L, + val LastFightBattleTeam: BattleTeam = BattleTeam.NONE, + val BattlePasswords: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceProBattle(State0: ProBattleStateSlice, Action: Any): ProBattleStateSlice = when (Action) { + is Fight -> State0.copy( + LastFightBattleId = Action.battleId, + LastFightBattleTeam = Action.battleTeam, + ) + is SetPassword -> State0.copy( + BattlePasswords = if (Action.password.isEmpty()) { + State0.BattlePasswords - Action.battleId + } else { + State0.BattlePasswords + (Action.battleId to Action.password) + }, + ) + 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 78bf877..7478e6a 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -1,7 +1,7 @@ // RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6) // // 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。 -// 这里采用同一形状。已迁移的 14 个切片里,13 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 18 个切片里,17 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -16,21 +16,29 @@ // entrance → ReduceEntrance(hD / Bv.pth,根委派 `Bv.pth(t, n.jpx_1)`) // user → ReduceUser(t8 / M8().qtd,根委派 `M8().qtd(t, n.ypu_1)`) // settings → ReduceSettings(Uut / xw.ltg,根委派 `xw.ltg(t, n.upv_1)`) +// accountSettings → ReduceAccountSettings(oet / Lp.mtg,根委派 `Lp.mtg(t, n.vpv_1)`) +// proBattle → ReduceProBattle(Wtt / uv.wth,根委派 `uv.wth(t, n.bpy_1)`) +// language → ReduceLanguage(枚举值 / m6().jtg,根委派 `m6().jtg(t, n.gpx_1)`) +// announcement → ReduceAnnouncement(lC / PC().rq5,根委派 `PC().rq5(t, n.opx_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 75 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 71 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State +import App.State.Reducers.ReduceAccountSettings +import App.State.Reducers.ReduceAnnouncement import App.State.Reducers.ReduceEntrance import App.State.Reducers.ReduceGarage import App.State.Reducers.ReduceGaragePreview import App.State.Reducers.ReduceHome import App.State.Reducers.ReduceInvite +import App.State.Reducers.ReduceLanguage import App.State.Reducers.ReduceLocalBattleUser import App.State.Reducers.ReduceLoginByPassword import App.State.Reducers.ReduceLootBoxes +import App.State.Reducers.ReduceProBattle import App.State.Reducers.ReduceScreen import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceShop @@ -63,6 +71,10 @@ object RootReducer { Entrance = ReduceEntrance(State0.Entrance, Action), User = ReduceUser(State0.User, Action), Settings = ReduceSettings(State0.Settings, Action), + AccountSettings = ReduceAccountSettings(State0.AccountSettings, Action), + ProBattle = ReduceProBattle(State0.ProBattle, Action), + Language = ReduceLanguage(State0.Language, Action), + Announcement = ReduceAnnouncement(State0.Announcement, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 7920899..9438809 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -4,6 +4,8 @@ // 已迁移的切片用强类型;其余保持 Any?(原样透传),迁移一个改一个再重新生成。 package App.State +import App.State.Reducers.AccountSettingsStateSlice +import App.State.Reducers.AnnouncementStateSlice import App.State.Reducers.EntranceStateSlice import App.State.Reducers.GarageStateSlice import App.State.Reducers.GaragePreviewStateSlice @@ -12,6 +14,7 @@ import App.State.Reducers.InviteStateSlice import App.State.Reducers.LocalBattleUserStateSlice import App.State.Reducers.LoginByPasswordStateSlice import App.State.Reducers.LootBoxesStateSlice +import App.State.Reducers.ProBattleStateSlice import App.State.Reducers.ScreenStateSlice import App.State.Reducers.SettingsStateSlice import App.State.Reducers.ShopStateSlice @@ -69,8 +72,8 @@ data class RootState( val News: Any? = null, /** 原 settings(Uut / xw.ltg)。已迁移。 */ val Settings: SettingsStateSlice = SettingsStateSlice(), - /** 原 accountSettings(oet / Lp.mtg)。未迁移:原样透传。 */ - val AccountSettings: Any? = null, + /** 原 accountSettings(oet / Lp.mtg)。已迁移。 */ + val AccountSettings: AccountSettingsStateSlice = AccountSettingsStateSlice(), /** 原 lootBoxes(aH / ws.wrv)。已迁移。 */ val LootBoxes: LootBoxesStateSlice = LootBoxesStateSlice(), /** 原 notifications(? / B_.ise)。未迁移:原样透传。 */ @@ -143,8 +146,8 @@ data class RootState( val TouchscreenHudSettings: Any? = null, /** 原 mainQuest(WZ / ih.bsk)。未迁移:原样透传。 */ val MainQuest: Any? = null, - /** 原 language(? / m6().jtg)。未迁移:原样透传。 */ - val Language: Any? = null, + /** 原 language(? / m6().jtg)。已迁移。 */ + val Language: Language = App.State.Language.EN, /** 原 launchParams(? / (pass-through))。已迁移(透传,强类型)。 */ val LaunchParams: LaunchParamsStateSlice = emptyMap(), /** 原 partner(UJ / (inline@partner))。未迁移:原样透传。 */ @@ -159,8 +162,8 @@ data class RootState( val Moderation: Any? = null, /** 原 rulesUpdated(i1 / Ch.dsm)。未迁移:原样透传。 */ val RulesUpdated: Any? = null, - /** 原 announcement(lC / PC().rq5)。未迁移:原样透传。 */ - val Announcement: Any? = null, + /** 原 announcement(lC / PC().rq5)。已迁移。 */ + val Announcement: AnnouncementStateSlice = AnnouncementStateSlice(), /** 原 notificationByCountry(gJ / R_.ssd)。未迁移:原样透传。 */ val NotificationByCountry: Any? = null, /** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */ @@ -185,8 +188,8 @@ data class RootState( val SuppliesTutorial: Any? = null, /** 原 questCategories(zZ / th.lsh)。未迁移:原样透传。 */ val QuestCategories: Any? = null, - /** 原 proBattle(Wtt / uv.wth)。未迁移:原样透传。 */ - val ProBattle: Any? = null, + /** 原 proBattle(Wtt / uv.wth)。已迁移。 */ + val ProBattle: ProBattleStateSlice = ProBattleStateSlice(), /** 原 availabilityByRank(tet / $p.xth)。未迁移:原样透传。 */ val AvailabilityByRank: Any? = null, /** 原 userReportsState(Zst / qot.zth)。未迁移:原样透传。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index eaf1980..3d15adf 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -37,7 +37,7 @@ object RootWiring { SliceWiring("tipsState", "spv_1", "dst", "Hw.vth", "Hw.vth(t, n.spv_1)", listOf("UpdateTips")), SliceWiring("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("?hJ", "AddNewsItemAction", "RemoveNewsItemAction", "SetNewsItemAction", "SetUnreadNewsCounter")), SliceWiring("settings", "upv_1", "Uut", "xw.ltg", "xw.ltg(t, n.upv_1)", listOf("DisableAccountSettings", "SetSettingsCategory")), - SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("?aet", "?cet", "?det", "?fet", "?get", "?ket", "?met", "?pet", "?vet", "?wet", "?xet", "ClearInputUid", "SetAccountRemovalDays")), + SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("ClearAccountRemovalTimer", "ClearInputUid", "ResetPasswordFields", "SetAccountRemovalDays", "SetCountry", "SetCurrentPassword", "SetCurrentPasswordMandatory", "SetEmail", "SetEmailConfirmed", "SetNewPassword", "SetPasswordStatus", "SetRepeatNewPassword", "SetUnlockButtonForRenameUid")), SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("AddKeys", "ClearRewards", "LootBoxOpeningFailed", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsBlockOpenButtons", "SetIsOpening", "SetIsReopening", "SetIsShopVisited", "SetKeys", "SetPeriodOfGiveLootBox", "SetPossibleRewards", "SetRewards", "SetTimeToGiveLootBox", "ShownRewards")), SliceWiring("notifications", "xpv_1", "?", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("?PJ", "?RJ", "AddNotification", "InitNotificationSound")), SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("?_L", "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetLimits", "SetValidationStatus")), @@ -74,7 +74,7 @@ object RootWiring { SliceWiring("keyboardSettings", "dpx_1", "Sut", "bw.oth", "bw.oth(t, n.dpx_1)", listOf("?Iut", "?_C", "ChangeAction", "RemoveAction", "ResetAction")), SliceWiring("touchscreenHudSettings", "epx_1", "j7", "N7().xsv", "N7().xsv(t, n.epx_1)", listOf("?E7", "?_C", "UpdateSettings")), SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("?VZ", "AddRewardedQuest", "QuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")), - SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("?d6")), + SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("SetLanguage")), SliceWiring("launchParams", "hpx_1", "?", "(pass-through)", "n.hpx_1", listOf()), SliceWiring("partner", "ipx_1", "UJ", "(inline@partner)", "(function (t, n) { var i; if (t instanceof VJ) i = n.osf(t.rsf_1, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ssf_1); else if (t instanceof nZ) { var r = t.bsg_1, e = t.csg_1, u = t.dsg_1, s = t.esg_1, o = t.fsg_1, _ = t.gsg_1; i = n.osf(cd, cd, cd, r, e, u, cd, cd, o, _, s); } else i = t instanceof iZ ? n.osf(cd, t.hsg_1) : t instanceof uZ ? n.osf(cd, cd, t.ksg_1) : t instanceof eZ ? n.osf(cd, cd, cd, cd, cd, cd, cd, t.jsg_1) : t instanceof _Z ? n.osf(cd, cd, cd, cd, cd, cd, t.osg_1) : t instanceof X …", listOf("?JJ", "?_Z", "?hZ", "?tZ", "InitPartnerInviteAction", "InitSocialNetworkParameters", "SetLoginParams", "SetLoginStatusForBinding", "SetPartnerId", "SetPartnerParameters", "SetPartnerReady")), SliceWiring("entrance", "jpx_1", "hD", "Bv.pth", "Bv.pth(t, n.jpx_1)", listOf("HideSocialNetworks", "SetAgeWarnShow", "SetAuthorizedByChinaSdkToken", "SetClassic", "SetControlsForChina", "SetRunningInChinaDataCenter", "SetRunningInChinaPartners")), @@ -82,7 +82,7 @@ object RootWiring { SliceWiring("spectator", "lpx_1", "HA", "Li.jqm", "Li.jqm(t, n.lpx_1)", listOf("?JA", "?XA", "?YA", "?ZA", "SetSpectator", "UpdateHudLayout")), SliceWiring("moderation", "mpx_1", "NI", "Ei.oqh", "Ei.oqh(t, n.mpx_1)", listOf("?FI", "?GI", "?JI", "?KI", "?QI", "?UI", "?WI", "?XI", "?YI", "?ZI", "?nA", "?tA", "?uA")), SliceWiring("rulesUpdated", "npx_1", "i1", "Ch.dsm", "Ch.dsm(t, n.npx_1)", listOf("UpdateRules")), - SliceWiring("announcement", "opx_1", "lC", "PC().rq5", "PC().rq5(t, n.opx_1)", listOf("?bC", "?qC", "Init", "SetSkipNextPromotedAnnounce")), + SliceWiring("announcement", "opx_1", "lC", "PC().rq5", "PC().rq5(t, n.opx_1)", listOf("DisableAnnouncement", "SetAnnouncementItems", "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce")), SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("?qJ", "Init")), SliceWiring("userRenameState", "qpx_1", "O8", "Rf.pte", "Rf.pte(t, n.qpx_1)", listOf("?B8", "?F8", "?G8", "?Q8", "?U8", "ChangeUidInputState", "ResponseActual", "ResponseDelivered", "SetChangeUidParams", "SetRubiesForRename", "SetValidationStatus")), SliceWiring("eventGoods", "rpx_1", "W7", "Wc.esx", "Wc.esx(t, n.rpx_1)", listOf("InitEventGoods")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "launchParams") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 269c701..1f2b31e 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 / GaragePreview / Entrance / User / Settings; +// ⑨~㉘ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / +// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings / +// AccountSettings / ProBattle / Language / Announcement; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -100,6 +101,13 @@ import App.State.SetRankNames import App.State.SetRubies import App.State.SetUserPropertiesAction import App.State.SetUserId +import App.State.BattleTeam +import App.State.ClearInputUid +import App.State.Fight +import App.State.Language +import App.State.PasswordStatus +import App.State.SetAccountRemovalDays +import App.State.SetSkipNextPromotedAnnounce import App.State.SettingsCategory import App.State.SpendCoins import App.State.SpendCrystals @@ -268,8 +276,31 @@ import App.State.Reducers.DisableAccountSettings import App.State.Reducers.DisableLobbyTutorial import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceUser +import App.State.Reducers.AccountSettingsStateSlice +import App.State.Reducers.AnnouncementStateSlice +import App.State.Reducers.ClearAccountRemovalTimer +import App.State.Reducers.DisableAnnouncement +import App.State.Reducers.ProBattleStateSlice +import App.State.Reducers.ReduceAccountSettings +import App.State.Reducers.ReduceAnnouncement +import App.State.Reducers.ReduceLanguage +import App.State.Reducers.ReduceProBattle +import App.State.Reducers.ResetPasswordFields +import App.State.Reducers.SetAnnouncementItems +import App.State.Reducers.SetAnnouncementModelLoaded +import App.State.Reducers.SetCountry +import App.State.Reducers.SetCurrentPassword +import App.State.Reducers.SetCurrentPasswordMandatory +import App.State.Reducers.SetEmail +import App.State.Reducers.SetEmailConfirmed import App.State.Reducers.SetInDebt +import App.State.Reducers.SetLanguage +import App.State.Reducers.SetNewPassword +import App.State.Reducers.SetPassword as SetBattlePassword +import App.State.Reducers.SetPasswordStatus +import App.State.Reducers.SetRepeatNewPassword import App.State.Reducers.SetSettingsCategory +import App.State.Reducers.SetUnlockButtonForRenameUid import App.State.Reducers.SettingsStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice @@ -698,7 +729,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 14 个已迁移切片(13 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 18 个已迁移切片(17 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -762,14 +793,15 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 14 个已迁移字段,其余 75 个待迁", + "根:迁移表 = 18 个已迁移字段,其余 71 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", - "garagePreview", "entrance", "user", "settings", "launchParams", + "garagePreview", "entrance", "user", "settings", "accountSettings", + "proBattle", "language", "announcement", "launchParams", ) && - RootWiring.Unmigrated.size == 75 + RootWiring.Unmigrated.size == 71 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -825,7 +857,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 14/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 18/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -2692,6 +2724,207 @@ object StateCheck { ) } + // ㉕ 切片 reducer:AccountSettings(oet / Lp.mtg,13 字段;13 个 instanceof 臂) + run { + val S0 = AccountSettingsStateSlice() + Check( + "切片 AccountSettings:默认值完整", + !S0.IsCurrentPasswordMandatory && S0.CurrentPassword == "" && + S0.NewPassword == "" && S0.RepeatNewPassword == "" && + S0.NewPasswordStatus == PasswordStatus.NONE && S0.CountryCode == null && + S0.CountryName == null && S0.Email == null && !S0.EmailConfirmed && + !S0.LocationCheckEnabled && !S0.IsUnlockButtonForRenameUid && + !S0.IsClearInputUid && S0.RemovalTimer == null + ) + val Reset = ReduceAccountSettings( + S0.copy( + CurrentPassword = "old", + NewPassword = "new", + RepeatNewPassword = "new", + NewPasswordStatus = PasswordStatus.OK, + IsUnlockButtonForRenameUid = true, + Email = "keep@example.com", + RemovalTimer = 42L, + ), + ResetPasswordFields, + ) + Check( + "切片 AccountSettings:ResetPasswordFields 只清密码三件套/状态/解锁按钮", + Reset.CurrentPassword == "" && Reset.NewPassword == "" && + Reset.RepeatNewPassword == "" && Reset.NewPasswordStatus == PasswordStatus.NONE && + !Reset.IsUnlockButtonForRenameUid && Reset.Email == "keep@example.com" && + Reset.RemovalTimer == 42L + ) + val S1 = listOf( + SetCurrentPassword("cur"), + SetNewPassword("next"), + SetRepeatNewPassword("next"), + SetPasswordStatus(PasswordStatus.SIMPLE), + SetCurrentPasswordMandatory(true), + SetEmail("user@example.com"), + SetEmailConfirmed(true), + SetCountry("RU", "Russia"), + SetUnlockButtonForRenameUid(true), + ClearInputUid(true), + SetAccountRemovalDays(1_700_000_000_000L), + ).fold(S0) { S, A -> ReduceAccountSettings(S, A) } + Check( + "切片 AccountSettings:11 个单字段动作按顺序写入", + S1.CurrentPassword == "cur" && S1.NewPassword == "next" && + S1.RepeatNewPassword == "next" && S1.NewPasswordStatus == PasswordStatus.SIMPLE && + S1.IsCurrentPasswordMandatory && S1.Email == "user@example.com" && + S1.EmailConfirmed && S1.CountryCode == "RU" && S1.CountryName == "Russia" && + S1.IsUnlockButtonForRenameUid && S1.IsClearInputUid && + S1.RemovalTimer == 1_700_000_000_000L + ) + Check( + "切片 AccountSettings:ClearAccountRemovalTimer 只清 removalTimer", + ReduceAccountSettings(S1, ClearAccountRemovalTimer).let { + it.RemovalTimer == null && it.Email == "user@example.com" + } + ) + Check( + "切片 AccountSettings:无关动作不改状态", + ReduceAccountSettings(S0, AddFrags(1)) == S0 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetCurrentPassword("root-pass")) + Check( + "根:SetCurrentPassword 只动 accountSettings", + Root1.AccountSettings.CurrentPassword == "root-pass" && + Root1.Language == Root0.Language && Root1.ProBattle == Root0.ProBattle + ) + Check( + "根:accountSettings 无关动作保留前一次归约", + ReduceRoot(Root1, AddFrags(1)).AccountSettings == Root1.AccountSettings + ) + val W = RootWiring.ByField("accountSettings")!! + Check( + "根:accountSettings 委派到 oet / Lp.mtg 且 13 个动作全部具名", + W.SliceClass == "oet" && W.Reducer == "Lp.mtg" && W.Actions.size == 13 && + W.Actions.containsAll( + setOf( + "ClearAccountRemovalTimer", "ClearInputUid", "ResetPasswordFields", + "SetAccountRemovalDays", "SetCountry", "SetCurrentPassword", + "SetCurrentPasswordMandatory", "SetEmail", "SetEmailConfirmed", + "SetNewPassword", "SetPasswordStatus", "SetRepeatNewPassword", + "SetUnlockButtonForRenameUid", + ) + ) + ) + } + + // ㉖ 切片 reducer:ProBattle(Wtt / uv.wth,3 字段;2 个 instanceof 臂) + run { + val S0 = ProBattleStateSlice() + Check( + "切片 ProBattle:默认 -1 / NONE / 空密码表", + S0.LastFightBattleId == -1L && S0.LastFightBattleTeam == BattleTeam.NONE && + S0.BattlePasswords.isEmpty() + ) + val Fight1 = ReduceProBattle(S0, Fight(10L, BattleTeam.TEAM_A)) + Check( + "切片 ProBattle:Fight 写上一场 id + team", + Fight1.LastFightBattleId == 10L && Fight1.LastFightBattleTeam == BattleTeam.TEAM_A + ) + val WithPass = ReduceProBattle(Fight1, SetBattlePassword(10L, "secret")) + Check( + "切片 ProBattle:SetPassword 非空时写入密码表", + WithPass.BattlePasswords == mapOf(10L to "secret") + ) + Check( + "切片 ProBattle:SetPassword 空串时删除密码表项", + ReduceProBattle(WithPass, SetBattlePassword(10L, "")).BattlePasswords.isEmpty() + ) + Check("切片 ProBattle:无关动作不改状态", ReduceProBattle(S0, AddFrags(1)) == S0) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, Fight(77L, BattleTeam.TEAM_B)) + Check( + "根:Fight 只动 proBattle", + Root1.ProBattle.LastFightBattleId == 77L && + Root1.ProBattle.LastFightBattleTeam == BattleTeam.TEAM_B && + Root1.AccountSettings == Root0.AccountSettings + ) + val W = RootWiring.ByField("proBattle")!! + Check( + "根:proBattle 委派到 Wtt / uv.wth 且 2 个动作", + W.SliceClass == "Wtt" && W.Reducer == "uv.wth" && + W.Actions.toSet() == setOf("Fight", "SetPassword") + ) + } + + // ㉗ 切片 reducer:Language(枚举值 / m6().jtg,1 个 instanceof 臂) + run { + Check("切片 Language:根状态默认 EN", RootState().Language == Language.EN) + Check( + "切片 Language:SetLanguage 写回枚举", + ReduceLanguage(Language.EN, SetLanguage(Language.ZH)) == Language.ZH + ) + Check( + "切片 Language:无关动作保留当前语言", + ReduceLanguage(Language.ZH, AddFrags(1)) == Language.ZH + ) + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetLanguage(Language.DE)) + Check( + "根:SetLanguage 只动 language", + Root1.Language == Language.DE && Root1.Announcement == Root0.Announcement + ) + val W = RootWiring.ByField("language")!! + Check( + "根:language 委派到 m6().jtg 且动作具名", + W.SliceClass == "?" && W.Reducer == "m6().jtg" && + W.Actions == listOf("SetLanguage") + ) + } + + // ㉘ 切片 reducer:Announcement(lC / PC().rq5,5 字段;4 个 instanceof 臂) + run { + val S0 = AnnouncementStateSlice() + Check( + "切片 Announcement:默认值完整", + S0.IsEnabled && !S0.ModelLoaded && S0.Items.isEmpty() && + !S0.WasInBattle && !S0.SkipNextPromotedAnnounce + ) + val S1 = ReduceAnnouncement( + ReduceAnnouncement( + ReduceAnnouncement( + ReduceAnnouncement(S0, DisableAnnouncement), + SetAnnouncementModelLoaded, + ), + SetAnnouncementItems(listOf("a", "b")), + ), + SetSkipNextPromotedAnnounce(true), + ) + Check( + "切片 Announcement:4 个臂分别写 enabled/modelLoaded/items/skip", + !S1.IsEnabled && S1.ModelLoaded && S1.Items == listOf("a", "b") && + S1.SkipNextPromotedAnnounce + ) + Check( + "切片 Announcement:无关动作不改状态", + ReduceAnnouncement(S0, AddFrags(1)) == S0 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, DisableAnnouncement) + Check( + "根:DisableAnnouncement 只动 announcement", + !Root1.Announcement.IsEnabled && Root1.AccountSettings == Root0.AccountSettings + ) + val W = RootWiring.ByField("announcement")!! + Check( + "根:announcement 委派到 lC / PC().rq5 且 4 个动作全部具名", + W.SliceClass == "lC" && W.Reducer == "PC().rq5" && + W.Actions.toSet() == setOf( + "DisableAnnouncement", "SetAnnouncementItems", + "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce", + ) + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)