diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 4ae5ddf..1d56d00 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` 动作名。 - - 已迁移切片 **21/89**: + - 已迁移切片 **28/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) @@ -224,12 +224,28 @@ `Hrt` 类体为空、reducer 读的 `t.iu5_1` 是 `undefined`(`cd` 也是 `undefined`),三个 copy 参数全走默认值, 这一支实际是 no-op;Kotlin 侧保留 `SetNotCompletedQuests` 动作但归约时原样返回。 `tX` / `nX` 是屏幕标识类,已从动作表剔除并记入 `Markers`(`screen-id-class`)。 + - `lobbyLoader` → `KW` / `Bs.es1`(`LobbyLoaderReducer.kt`):2 字段,3 个分支。 + 缺省 `isShowLoader=false`、`activeLoadersCount=0`;`FW`→`ShowLoader` 计数 +1 并显示, + `GW`→`HideLoader` 计数 `max(count-1,0)`,显示取 `count != 0`(计数夹到 0 后自动隐藏)。 + - `purchaseDialogState` → `CO` / `je.ur2`(`PurchaseDialogReducer.kt`):4 字段,4 个分支。 + `NO`→`ResetPurchaseDialog` 复位四字段;`ChangeEmailStatus` 只写 `emailStatus`(`PurchaseEmailStatus`: + `BUSY/FORBIDDEN/FREE`);`SuccessfullyPurchase` 写 withEmail/time,`goods` 是**追加**(`Im` = List.plus)。 + - `promoCode` → `cZ` / `Q_.ysg`(`PromoCodeReducer.kt`):1 字段,2 个实例臂。 + `ActivatePromoCode` 置 `WAIT`,`SetCodeValidateStatus` 写入服务端状态; + `PromoCodeStatus` = `NONE/INVALID/WAIT/NOT_ACTIVATED/ACTIVATED`,缺省 `NONE`。 + - `supportUrl` → `I4` / `Yl.ct7`(`SupportUrlReducer.kt`):1 字段,2 个分支。 + `SetUrl` 写链接,`R4`→`ClearSupportUrl` 清成空串。 + - `rulesUpdated` → `i1` / `Ch.dsm`(`RulesUpdatedReducer.kt`):1 字段,`UpdateRules` 写正文,缺省 null。 + - `notificationByCountry` → `gJ` / `R_.ssd`(`NotificationByCountryReducer.kt`):4 字段,2 个分支。 + `Init` 写三条国家提示消息,`qJ`→`MarkHomeScreenDialogShown` 只把 `homeScreenDialogShown` 置 true。 + - `availabilityByRank` → `tet` / `$p.xth`(`AvailabilityByRankReducer.kt`):1 字段, + `SetLoadedData` 整体替换等级可购买表,缺省空 Map。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 20 个有 reducer 的切片, - `launchParams` 与其余 **68 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 21/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **376 项全绿**,含 89 槽位完整性、20 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 27 个有 reducer 的切片, + `launchParams` 与其余 **61 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 28/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **419 项全绿**,含 89 槽位完整性、27 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -241,7 +257,8 @@ screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、 entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、 language 的 1 个、announcement 的 3 个、news 的 3 个(含两个单例)、notifications 的 2 个、 - quests 的 1 个匿名类 / 单例同样按效果定名并登记。 + quests 的 1 个、lobbyLoader 的 2 个、purchaseDialogState 的 1 个、supportUrl 的 1 个、 + notificationByCountry 的 1 个匿名类 / 单例同样按效果定名并登记。 ### 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 d0ebca4..bfb8745 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T05:13:42.080Z", + "generatedAt": "2026-10-24T05:17:45.346Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -27,7 +27,14 @@ "launchParams", "news", "notifications", - "quests" + "quests", + "lobbyLoader", + "purchaseDialogState", + "promoCode", + "supportUrl", + "rulesUpdated", + "notificationByCountry", + "availabilityByRank" ], "AnonymousActions": [ { @@ -660,6 +667,36 @@ "Name": "SetNotCompletedQuests", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "状态不变(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,`t.iu5_1` 与 `cd` 都是 undefined,三个参数全走默认值 → no-op)" + }, + { + "Local": "FW", + "Name": "ShowLoader", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isShowLoader=true、activeLoadersCount = count + 1(`n.fs1(!0, (n.ss0_1 + 1) | 0)`)" + }, + { + "Local": "GW", + "Name": "HideLoader", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "activeLoadersCount = max(count - 1, 0)、isShowLoader = count != 0(`n.fs1(!(0 === e), e)`)" + }, + { + "Local": "NO", + "Name": "ResetPurchaseDialog", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "withEmail=false、goods=emptyList()、emailStatus=null、time=null(`n.jr2(!1, md(), null, null)`)" + }, + { + "Local": "R4", + "Name": "ClearSupportUrl", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "url=\"\"(`n.at7()`)" + }, + { + "Local": "qJ", + "Name": "MarkHomeScreenDialogShown", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)" } ], "Markers": [ @@ -1495,11 +1532,11 @@ "Reducer": "Bs.es1", "Delegate": "Bs.es1(t, n.fpw_1)", "Actions": [ - "?FW", - "?GW" + "HideLoader", + "ShowLoader" ], "Default": "new KW()", - "Migrated": false + "Migrated": true }, { "Field": "battleResult", @@ -1901,12 +1938,12 @@ "Reducer": "je.ur2", "Delegate": "je.ur2(t, n.bpx_1)", "Actions": [ - "?NO", "ChangeEmailStatus", + "ResetPurchaseDialog", "SuccessfullyPurchase" ], "Default": "new CO()", - "Migrated": false + "Migrated": true }, { "Field": "battleInviteState", @@ -2102,7 +2139,7 @@ "UpdateRules" ], "Default": "new i1()", - "Migrated": false + "Migrated": true }, { "Field": "announcement", @@ -2126,11 +2163,11 @@ "Reducer": "R_.ssd", "Delegate": "R_.ssd(t, n.ppx_1)", "Actions": [ - "?qJ", - "Init" + "Init", + "MarkHomeScreenDialogShown" ], "Default": "new gJ()", - "Migrated": false + "Migrated": true }, { "Field": "userRenameState", @@ -2319,7 +2356,7 @@ "SetLoadedData" ], "Default": "new tet()", - "Migrated": false + "Migrated": true }, { "Field": "userReportsState", @@ -2382,7 +2419,7 @@ "SetCodeValidateStatus" ], "Default": "new cZ()", - "Migrated": false + "Migrated": true }, { "Field": "referralEvent", @@ -2406,11 +2443,11 @@ "Reducer": "Yl.ct7", "Delegate": "Yl.ct7(t, n.ipy_1)", "Actions": [ - "?R4", + "ClearSupportUrl", "SetUrl" ], "Default": "new I4()", - "Migrated": false + "Migrated": true } ] } diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 1c077c0..8830681 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -170,6 +170,15 @@ const AnonymousActionNames = new Map([ ['hJ', 'SetNewsFlag'], ['x_', 'OpenNews'], ['j_', 'CloseNews'], + // lobbyLoader 切片(KW / VW.es1):FW/GW 没有 toString,按 reducer 效果定名。 + ['FW', 'ShowLoader'], + ['GW', 'HideLoader'], + // purchaseDialogState 切片(CO / DO.ur2):NO 没有 toString,按 reducer 效果定名。 + ['NO', 'ResetPurchaseDialog'], + // supportUrl 切片(I4 / P4.ct7):R4 没有 toString,按 reducer 效果定名。 + ['R4', 'ClearSupportUrl'], + // notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 效果定名。 + ['qJ', 'MarkHomeScreenDialogShown'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -573,6 +582,16 @@ const Migrated = { Reduce: 'ReduceNotifications', }, quests: { Type: 'QuestsStateSlice', Reduce: 'ReduceQuests' }, + lobbyLoader: { Type: 'LobbyLoaderStateSlice', Reduce: 'ReduceLobbyLoader' }, + purchaseDialogState: { Type: 'PurchaseDialogStateSlice', Reduce: 'ReducePurchaseDialog' }, + promoCode: { Type: 'PromoCodeStateSlice', Reduce: 'ReducePromoCode' }, + supportUrl: { Type: 'SupportUrlStateSlice', Reduce: 'ReduceSupportUrl' }, + rulesUpdated: { Type: 'RulesUpdatedStateSlice', Reduce: 'ReduceRulesUpdated' }, + notificationByCountry: { + Type: 'NotificationByCountryStateSlice', + Reduce: 'ReduceNotificationByCountry', + }, + availabilityByRank: { Type: 'AvailabilityByRankStateSlice', Reduce: 'ReduceAvailabilityByRank' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -759,6 +778,27 @@ const QuestsAnonymousEffects = { Hrt: '状态不变(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,`t.iu5_1` 与 `cd` 都是 undefined,三个参数全走默认值 → no-op)', }; +// lobbyLoader 切片(KW / VW.es1):FW/GW 没有 toString,按 reducer 分支效果定名。 +const LobbyLoaderAnonymousEffects = { + FW: 'isShowLoader=true、activeLoadersCount = count + 1(`n.fs1(!0, (n.ss0_1 + 1) | 0)`)', + GW: 'activeLoadersCount = max(count - 1, 0)、isShowLoader = count != 0(`n.fs1(!(0 === e), e)`)', +}; + +// purchaseDialogState 切片(CO / DO.ur2):NO 没有 toString,按 reducer 分支效果定名。 +const PurchaseDialogAnonymousEffects = { + NO: 'withEmail=false、goods=emptyList()、emailStatus=null、time=null(`n.jr2(!1, md(), null, null)`)', +}; + +// supportUrl 切片(I4 / P4.ct7):R4 没有 toString,按 reducer 分支效果定名。 +const SupportUrlAnonymousEffects = { + R4: 'url=""(`n.at7()`)', +}; + +// notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 分支效果定名。 +const NotificationByCountryAnonymousEffects = { + qJ: 'homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -859,6 +899,30 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(LobbyLoaderAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(PurchaseDialogAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(SupportUrlAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(NotificationByCountryAnonymousEffects).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/Reducers/AvailabilityByRankReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AvailabilityByRankReducer.kt new file mode 100644 index 0000000..51b6f31 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/AvailabilityByRankReducer.kt @@ -0,0 +1,26 @@ +// AvailabilityByRankReducer.kt —— 按等级可购买项切片(对应 module_10032 的 tet 状态 + set.xth) +// +// 证据链: +// 状态类 tet:1 个字段,toString 名 "AvailabilityByRankState(data=…)";copy 是 pu5(1 字段)。 +// 默认:data = ag() = emptyMap()。 +// 根委派:`$p.xth(t, n.cpy_1)`。 +// reducer:`t instanceof eet ? n.pu5(t.wu5_1) : n`,`eet` 是 SetLoadedData。 +package App.State.Reducers + +import App.State.SetLoadedData + +/* ------------------------------------------------------------------ 状态 */ + +data class AvailabilityByRankStateSlice( + val Data: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceAvailabilityByRank( + State0: AvailabilityByRankStateSlice, + Action: Any, +): AvailabilityByRankStateSlice = when (Action) { + is SetLoadedData -> State0.copy(Data = Action.loadedData as Map) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LobbyLoaderReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LobbyLoaderReducer.kt new file mode 100644 index 0000000..14b5e35 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LobbyLoaderReducer.kt @@ -0,0 +1,42 @@ +// LobbyLoaderReducer.kt —— 大厅加载遮罩切片(对应 module_10032 的 KW 状态 + VW.es1) +// +// 证据链: +// 状态类 KW:2 个字段,toString 名 "LobbyLoader(isShowLoader=…, activeLoadersCount=…)";copy 是 fs1(2 字段)。 +// 默认:isShowLoader=false、activeLoadersCount=0。 +// 根委派:`Bs.es1(t, n.fpw_1)`。 +// reducer: +// FW → `n.fs1(!0, (n.ss0_1 + 1) | 0)` 显示并计数 +1 +// GW → `n.fs1(!(0 === e), e)`,e = max(count - 1, 0) 计数 -1(夹到 0),显示 = 计数非 0 +package App.State.Reducers + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `FW`:有东西开始加载。 */ +object ShowLoader + +/** 原 `GW`:有东西结束加载。 */ +object HideLoader + +/* ------------------------------------------------------------------ 状态 */ + +data class LobbyLoaderStateSlice( + val IsShowLoader: Boolean = false, + val ActiveLoadersCount: Int = 0, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceLobbyLoader( + State0: LobbyLoaderStateSlice, + Action: Any, +): LobbyLoaderStateSlice = when (Action) { + is ShowLoader -> State0.copy( + IsShowLoader = true, + ActiveLoadersCount = State0.ActiveLoadersCount + 1, + ) + is HideLoader -> { + val Count = maxOf(State0.ActiveLoadersCount - 1, 0) + State0.copy(IsShowLoader = Count != 0, ActiveLoadersCount = Count) + } + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/NotificationByCountryReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/NotificationByCountryReducer.kt new file mode 100644 index 0000000..79524b8 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/NotificationByCountryReducer.kt @@ -0,0 +1,41 @@ +// NotificationByCountryReducer.kt —— 按国家屏蔽提示切片(对应 module_10032 的 gJ 状态 + $J.ssd) +// +// 证据链: +// 状态类 gJ:4 个字段,toString 名 "NotificationByCountry(...)";copy 是 isd(4 字段 + 回调)。 +// 默认:三条消息都是 null、homeScreenDialogShown=false。 +// 根委派:`R_.ssd(t, n.ppx_1)`。 +// reducer: +// bJ(Init)→ `n.isd(t.psd_1, t.qsd_1, t.rsd_1)`,写三条消息 +// qJ → `n.isd(cd, cd, cd, !0)`,只把 homeScreenDialogShown 置 true +package App.State.Reducers + +import App.State.Init + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `qJ`:首页的国家提示弹窗已经展示过。 */ +object MarkHomeScreenDialogShown + +/* ------------------------------------------------------------------ 状态 */ + +data class NotificationByCountryStateSlice( + val DisableRegistrationMessage: String? = null, + val DisableShopMessage: String? = null, + val HomeScreenMessage: String? = null, + val HomeScreenDialogShown: Boolean = false, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceNotificationByCountry( + State0: NotificationByCountryStateSlice, + Action: Any, +): NotificationByCountryStateSlice = when (Action) { + is Init -> State0.copy( + DisableRegistrationMessage = Action.disableRegistrationMessage, + DisableShopMessage = Action.disableShopMessage, + HomeScreenMessage = Action.homeScreenMessage, + ) + is MarkHomeScreenDialogShown -> State0.copy(HomeScreenDialogShown = true) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PromoCodeReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PromoCodeReducer.kt new file mode 100644 index 0000000..4a20ba4 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PromoCodeReducer.kt @@ -0,0 +1,36 @@ +// PromoCodeReducer.kt —— 促销码切片(对应 module_10032 的 cZ 状态 + wZ.ysg) +// +// 证据链: +// 状态类 cZ:1 个字段,toString 名 "PromoCodeState(...)";copy 是 vsg(1 字段)。 +// 默认:validateStatus = dZ() = NONE。 +// 根委派:`Q_.ysg(t, n.gpy_1)`。 +// reducer: +// vZ → `n.vsg((lZ(), Y_))`,状态置 WAIT(提交校验中) +// pZ → `n.vsg(t.xsg_1)`,写入服务端返回的状态 +// `dZ()` 是 `fZ` 枚举:NONE / INVALID / WAIT / NOT_ACTIVATED / ACTIVATED。 +package App.State.Reducers + +import App.State.ActivatePromoCode +import App.State.SetCodeValidateStatus + +/* ------------------------------------------------------------------ 枚举 */ + +/** 原 `fZ`:促销码校验状态。 */ +enum class PromoCodeStatus { NONE, INVALID, WAIT, NOT_ACTIVATED, ACTIVATED } + +/* ------------------------------------------------------------------ 状态 */ + +data class PromoCodeStateSlice( + val ValidateStatus: PromoCodeStatus = PromoCodeStatus.NONE, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReducePromoCode( + State0: PromoCodeStateSlice, + Action: Any, +): PromoCodeStateSlice = when (Action) { + is ActivatePromoCode -> State0.copy(ValidateStatus = PromoCodeStatus.WAIT) + is SetCodeValidateStatus -> State0.copy(ValidateStatus = Action.promoCodeStatus as PromoCodeStatus) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PurchaseDialogReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PurchaseDialogReducer.kt new file mode 100644 index 0000000..cb70fa6 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PurchaseDialogReducer.kt @@ -0,0 +1,53 @@ +// PurchaseDialogReducer.kt —— 购买对话框切片(对应 module_10032 的 CO 状态 + DO.ur2) +// +// 证据链: +// 状态类 CO:4 个字段,toString 名 "PurchaseDialogState(...)";copy 是 kr2(4 字段 + 回调)。 +// 默认:withEmail=false、goods=emptyList()、emailStatus=null、time=null。 +// 根委派:`je.ur2(t, n.bpx_1)`。 +// reducer: +// NO → `n.jr2(!1, md(), null, null)`,四个字段全部复位 +// MO → `n.kr2(cd, cd, t.pr2_1)`,只写 emailStatus +// OO → `n.kr2(u, Im(n.gr2_1, t.rr2_1), cd, s)`,withEmail/goods/time 三个字段; +// `Im` 是 List.plus,goods 是「追加」而不是替换 +// emailStatus 是 `TO` 枚举(BUSY / FORBIDDEN / FREE)。 +package App.State.Reducers + +import App.State.ChangeEmailStatus +import App.State.SuccessfullyPurchase + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `NO`:关闭购买对话框并清空。 */ +object ResetPurchaseDialog + +/* ------------------------------------------------------------------ 枚举 */ + +/** 原 `TO`:邮箱状态。 */ +enum class PurchaseEmailStatus { BUSY, FORBIDDEN, FREE } + +/* ------------------------------------------------------------------ 状态 */ + +data class PurchaseDialogStateSlice( + val WithEmail: Boolean = false, + val Goods: List = emptyList(), + val EmailStatus: PurchaseEmailStatus? = null, + val Time: Long? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReducePurchaseDialog( + State0: PurchaseDialogStateSlice, + Action: Any, +): PurchaseDialogStateSlice = when (Action) { + is ResetPurchaseDialog -> + PurchaseDialogStateSlice() + is ChangeEmailStatus -> + State0.copy(EmailStatus = Action.status as PurchaseEmailStatus?) + is SuccessfullyPurchase -> State0.copy( + WithEmail = Action.withEmail, + Goods = State0.Goods + (Action.goods as List), + Time = Action.time, + ) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RulesUpdatedReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RulesUpdatedReducer.kt new file mode 100644 index 0000000..3276070 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RulesUpdatedReducer.kt @@ -0,0 +1,26 @@ +// RulesUpdatedReducer.kt —— 规则更新提示切片(对应 module_10032 的 i1 状态 + u1.dsm) +// +// 证据链: +// 状态类 i1:1 个字段,toString 名 "RulesUpdatedState(text=…)";copy 是 bsm(1 字段)。 +// 默认:text=null。 +// 根委派:`Ch.dsm(t, n.npx_1)`。 +// reducer:`t instanceof e1 ? n.bsm(t.csm_1) : n`,`e1` 是 UpdateRules。 +package App.State.Reducers + +import App.State.UpdateRules + +/* ------------------------------------------------------------------ 状态 */ + +data class RulesUpdatedStateSlice( + val Text: String? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceRulesUpdated( + State0: RulesUpdatedStateSlice, + Action: Any, +): RulesUpdatedStateSlice = when (Action) { + is UpdateRules -> State0.copy(Text = Action.text as String?) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SupportUrlReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SupportUrlReducer.kt new file mode 100644 index 0000000..8573eef --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SupportUrlReducer.kt @@ -0,0 +1,34 @@ +// SupportUrlReducer.kt —— 客服链接切片(对应 module_10032 的 I4 状态 + P4.ct7) +// +// 证据链: +// 状态类 I4:1 个字段,toString 名 "SupportUrlState(url=…)";copy 是 at7(1 字段)。 +// 默认:url=""。 +// 根委派:`Yl.ct7(t, n.ipy_1)`。 +// reducer: +// A4 → `n.zt6(t.bt7_1)`,写入链接 +// R4 → `n.at7()`,清成空串 +package App.State.Reducers + +import App.State.SetUrl + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `R4`:清空客服链接。 */ +object ClearSupportUrl + +/* ------------------------------------------------------------------ 状态 */ + +data class SupportUrlStateSlice( + val Url: String = "", +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceSupportUrl( + State0: SupportUrlStateSlice, + Action: Any, +): SupportUrlStateSlice = when (Action) { + is SetUrl -> State0.copy(Url = Action.url as String) + is ClearSupportUrl -> State0.copy(Url = "") + 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 58b81c9..83f5f74 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 出新状态)。 -// 这里采用同一形状。已迁移的 21 个切片里,20 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 28 个切片里,27 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -23,9 +23,16 @@ // news → ReduceNews(ZX / E_.asd,根委派 `E_.asd(t, n.tpv_1)`) // notifications → ReduceNotifications(jJ / B_.ise,根委派 `B_.ise(t, n.xpv_1)`) // quests → ReduceQuests(Frt / Yrt.otg,根委派 `(null == zp && new Yrt(), zp).otg(t, n.npv_1)`) +// lobbyLoader → ReduceLobbyLoader(KW / VW.es1,根委派 `Bs.es1(t, n.fpw_1)`) +// purchaseDialogState → ReducePurchaseDialog(CO / DO.ur2,根委派 `je.ur2(t, n.bpx_1)`) +// promoCode → ReducePromoCode(cZ / wZ.ysg,根委派 `Q_.ysg(t, n.gpy_1)`) +// supportUrl → ReduceSupportUrl(I4 / P4.ct7,根委派 `Yl.ct7(t, n.ipy_1)`) +// rulesUpdated → ReduceRulesUpdated(i1 / u1.dsm,根委派 `Ch.dsm(t, n.npx_1)`) +// notificationByCountry → ReduceNotificationByCountry(gJ / $J.ssd,根委派 `R_.ssd(t, n.ppx_1)`) +// availabilityByRank → ReduceAvailabilityByRank(tet / set.xth,根委派 `$p.xth(t, n.cpy_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 68 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 61 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -41,11 +48,18 @@ import App.State.Reducers.ReduceLanguage import App.State.Reducers.ReduceLocalBattleUser import App.State.Reducers.ReduceLoginByPassword import App.State.Reducers.ReduceLootBoxes +import App.State.Reducers.ReduceAvailabilityByRank +import App.State.Reducers.ReduceLobbyLoader import App.State.Reducers.ReduceNews +import App.State.Reducers.ReduceNotificationByCountry import App.State.Reducers.ReduceNotifications import App.State.Reducers.ReduceProBattle +import App.State.Reducers.ReducePromoCode +import App.State.Reducers.ReducePurchaseDialog import App.State.Reducers.ReduceQuests +import App.State.Reducers.ReduceRulesUpdated import App.State.Reducers.ReduceScreen +import App.State.Reducers.ReduceSupportUrl import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceShop import App.State.Reducers.ReduceTwitch @@ -84,6 +98,13 @@ object RootReducer { News = ReduceNews(State0.News, Action), Notifications = ReduceNotifications(State0.Notifications, Action), Quests = ReduceQuests(State0.Quests, Action), + LobbyLoader = ReduceLobbyLoader(State0.LobbyLoader, Action), + PurchaseDialogState = ReducePurchaseDialog(State0.PurchaseDialogState, Action), + PromoCode = ReducePromoCode(State0.PromoCode, Action), + SupportUrl = ReduceSupportUrl(State0.SupportUrl, Action), + RulesUpdated = ReduceRulesUpdated(State0.RulesUpdated, Action), + NotificationByCountry = ReduceNotificationByCountry(State0.NotificationByCountry, Action), + AvailabilityByRank = ReduceAvailabilityByRank(State0.AvailabilityByRank, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 79a1a22..a2bd0d3 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -6,21 +6,28 @@ package App.State import App.State.Reducers.AccountSettingsStateSlice import App.State.Reducers.AnnouncementStateSlice +import App.State.Reducers.AvailabilityByRankStateSlice import App.State.Reducers.EntranceStateSlice import App.State.Reducers.GarageStateSlice import App.State.Reducers.GaragePreviewStateSlice import App.State.Reducers.HomeStateSlice import App.State.Reducers.InviteStateSlice +import App.State.Reducers.LobbyLoaderStateSlice import App.State.Reducers.LocalBattleUserStateSlice import App.State.Reducers.LoginByPasswordStateSlice import App.State.Reducers.LootBoxesStateSlice import App.State.Reducers.NewsStateSlice +import App.State.Reducers.NotificationByCountryStateSlice import App.State.Reducers.NotificationsStateSlice import App.State.Reducers.ProBattleStateSlice +import App.State.Reducers.PromoCodeStateSlice +import App.State.Reducers.PurchaseDialogStateSlice import App.State.Reducers.QuestsStateSlice +import App.State.Reducers.RulesUpdatedStateSlice import App.State.Reducers.ScreenStateSlice import App.State.Reducers.SettingsStateSlice import App.State.Reducers.ShopStateSlice +import App.State.Reducers.SupportUrlStateSlice import App.State.Reducers.TwitchStateSlice import App.State.Reducers.UserStateSlice @@ -95,8 +102,8 @@ data class RootState( val BattleCreateState: Any? = null, /** 原 battleSearchState(Itt / Htt().vtg)。未迁移:原样透传。 */ val BattleSearchState: Any? = null, - /** 原 lobbyLoader(KW / Bs.es1)。未迁移:原样透传。 */ - val LobbyLoader: Any? = null, + /** 原 lobbyLoader(KW / Bs.es1)。已迁移。 */ + val LobbyLoader: LobbyLoaderStateSlice = LobbyLoaderStateSlice(), /** 原 battleResult(sR / Fi.aqp)。未迁移:原样透传。 */ val BattleResult: Any? = null, /** 原 battleUsers(uP / sr.hqs)。未迁移:原样透传。 */ @@ -139,8 +146,8 @@ data class RootState( val ControlSettings: Any? = null, /** 原 securitySettings(V2 / pc.qsv)。未迁移:原样透传。 */ val SecuritySettings: Any? = null, - /** 原 purchaseDialogState(CO / je.ur2)。未迁移:原样透传。 */ - val PurchaseDialogState: Any? = null, + /** 原 purchaseDialogState(CO / je.ur2)。已迁移。 */ + val PurchaseDialogState: PurchaseDialogStateSlice = PurchaseDialogStateSlice(), /** 原 battleInviteState(att / Ttt().nth)。未迁移:原样透传。 */ val BattleInviteState: Any? = null, /** 原 keyboardSettings(Sut / bw.oth)。未迁移:原样透传。 */ @@ -163,12 +170,12 @@ data class RootState( val Spectator: Any? = null, /** 原 moderation(NI / Ei.oqh)。未迁移:原样透传。 */ val Moderation: Any? = null, - /** 原 rulesUpdated(i1 / Ch.dsm)。未迁移:原样透传。 */ - val RulesUpdated: Any? = null, + /** 原 rulesUpdated(i1 / Ch.dsm)。已迁移。 */ + val RulesUpdated: RulesUpdatedStateSlice = RulesUpdatedStateSlice(), /** 原 announcement(lC / PC().rq5)。已迁移。 */ val Announcement: AnnouncementStateSlice = AnnouncementStateSlice(), - /** 原 notificationByCountry(gJ / R_.ssd)。未迁移:原样透传。 */ - val NotificationByCountry: Any? = null, + /** 原 notificationByCountry(gJ / R_.ssd)。已迁移。 */ + val NotificationByCountry: NotificationByCountryStateSlice = NotificationByCountryStateSlice(), /** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */ val UserRenameState: Any? = null, /** 原 eventGoods(W7 / Wc.esx)。未迁移:原样透传。 */ @@ -193,18 +200,18 @@ data class RootState( val QuestCategories: Any? = null, /** 原 proBattle(Wtt / uv.wth)。已迁移。 */ val ProBattle: ProBattleStateSlice = ProBattleStateSlice(), - /** 原 availabilityByRank(tet / $p.xth)。未迁移:原样透传。 */ - val AvailabilityByRank: Any? = null, + /** 原 availabilityByRank(tet / $p.xth)。已迁移。 */ + val AvailabilityByRank: AvailabilityByRankStateSlice = AvailabilityByRankStateSlice(), /** 原 userReportsState(Zst / qot.zth)。未迁移:原样透传。 */ val UserReportsState: Any? = null, /** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */ val TankBuilderState: Any? = null, /** 原 contractState(Ont / xv.bti)。未迁移:原样透传。 */ val ContractState: Any? = null, - /** 原 promoCode(cZ / Q_.ysg)。未迁移:原样透传。 */ - val PromoCode: Any? = null, + /** 原 promoCode(cZ / Q_.ysg)。已迁移。 */ + val PromoCode: PromoCodeStateSlice = PromoCodeStateSlice(), /** 原 referralEvent(pQ / lh.osk)。未迁移:原样透传。 */ val ReferralEvent: Any? = null, - /** 原 supportUrl(I4 / Yl.ct7)。未迁移:原样透传。 */ - val SupportUrl: Any? = null, + /** 原 supportUrl(I4 / Yl.ct7)。已迁移。 */ + val SupportUrl: SupportUrlStateSlice = SupportUrlStateSlice(), ) diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 750b21b..69742bd 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -47,7 +47,7 @@ object RootWiring { SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("?WH", "?nW", "?tW", "Configure", "EnterToBattleApply", "Play", "SelectMatchmakingMode", "SetRankIndexToMinGS", "WasRegistered")), SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")), SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("?Dtt", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")), - SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("?FW", "?GW")), + SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("HideLoader", "ShowLoader")), SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("?cR", "?fR", "?lR", "DoInit", "SetHasBattleResult", "SetRewards")), SliceWiring("battleUsers", "hpw_1", "uP", "sr.hqs", "sr.hqs(t, n.hpw_1)", listOf("?_P", "?hP", "?lP", "?oP", "?qP", "?sP", "InitSuspiciousState", "InitUserLevels", "RemoveTankInfo", "SetGearScore", "SetTankInfo", "SetTankResistance", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")), SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("?qE")), @@ -69,7 +69,7 @@ object RootWiring { SliceWiring("graphicsSettings", "ypw_1", "?", "lc.isu", "lc.isu(t, n.ypw_1)", listOf("UpdateSettings")), SliceWiring("controlSettings", "zpw_1", "i2", "_2().ess", "_2().ess(t, n.zpw_1)", listOf("?_C", "UpdateSettings")), SliceWiring("securitySettings", "apx_1", "V2", "pc.qsv", "pc.qsv(t, n.apx_1)", listOf("SetProfileBlockStatus")), - SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("?NO", "ChangeEmailStatus", "SuccessfullyPurchase")), + SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase")), SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("?dtt", "EnableSetBattleInviteNotifications", "InvitationResponse", "InviteToBattle", "RemoveUserFromInvited", "SelectUserToInvite")), 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")), @@ -83,7 +83,7 @@ object RootWiring { 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("DisableAnnouncement", "SetAnnouncementItems", "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce")), - SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("?qJ", "Init")), + SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("Init", "MarkHomeScreenDialogShown")), 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")), SliceWiring("creatorTagState", "spx_1", "qO", "ze.er2", "ze.er2(t, n.spx_1)", listOf("?jO", "SetAttachedCreatorTag", "SetCreatorTag", "SetCreatorTags")), @@ -102,11 +102,11 @@ object RootWiring { SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "Init", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")), SliceWiring("promoCode", "gpy_1", "cZ", "Q_.ysg", "Q_.ysg(t, n.gpy_1)", listOf("ActivatePromoCode", "SetCodeValidateStatus")), SliceWiring("referralEvent", "hpy_1", "pQ", "lh.osk", "lh.osk(t, n.hpy_1)", listOf("SetPromoCode", "SetReferralEventData", "SetReferrals", "SetReferrer")), - SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("?R4", "SetUrl")), + SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("ClearSupportUrl", "SetUrl")), ) /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 bffdd20..e9b32dc 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -9,9 +9,10 @@ // ⑥ 动作目录:816 个类与 138 个真实派发名对齐 // ⑦ 回放:真实 toString 样例的类/字段/类型校验 // ⑧ 全目录动作类都能按 KClass 路由 -// ⑨~㉛ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / +// ⑨~㊳ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / // Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings / -// AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests; +// AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests / +// LobbyLoader / PurchaseDialogState / PromoCode / SupportUrl / RulesUpdated / NotificationByCountry / AvailabilityByRank; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -165,14 +166,22 @@ import App.State.DisplayMainEntranceScreen import App.State.DisplayReauthorizationMessage import App.State.EntranceScreen import App.State.GoTo +import App.State.ActivatePromoCode import App.State.AddNewsItemAction import App.State.AddNotification +import App.State.ChangeEmailStatus +import App.State.Init as InitNotificationByCountry import App.State.InitNotificationSound import App.State.NewsItemData import App.State.RemoveNewsItemAction +import App.State.SetCodeValidateStatus import App.State.SetCurrentCategory +import App.State.SetLoadedData import App.State.SetNewsItemAction import App.State.SetUnreadNewsCounter +import App.State.SetUrl +import App.State.SuccessfullyPurchase +import App.State.UpdateRules import App.State.LoaderScreen import App.State.NamedScreen import App.State.PressedAnyButton @@ -325,6 +334,27 @@ import App.State.Reducers.RemoveFirstNotification import App.State.Reducers.RemoveNotifications import App.State.Reducers.SetNewsFlag import App.State.Reducers.SetNotCompletedQuests +import App.State.Reducers.AvailabilityByRankStateSlice +import App.State.Reducers.ClearSupportUrl +import App.State.Reducers.HideLoader +import App.State.Reducers.LobbyLoaderStateSlice +import App.State.Reducers.MarkHomeScreenDialogShown +import App.State.Reducers.NotificationByCountryStateSlice +import App.State.Reducers.PromoCodeStateSlice +import App.State.Reducers.PromoCodeStatus +import App.State.Reducers.PurchaseDialogStateSlice +import App.State.Reducers.PurchaseEmailStatus +import App.State.Reducers.ReduceAvailabilityByRank +import App.State.Reducers.ReduceLobbyLoader +import App.State.Reducers.ReduceNotificationByCountry +import App.State.Reducers.ReducePromoCode +import App.State.Reducers.ReducePurchaseDialog +import App.State.Reducers.ReduceRulesUpdated +import App.State.Reducers.ReduceSupportUrl +import App.State.Reducers.ResetPurchaseDialog +import App.State.Reducers.RulesUpdatedStateSlice +import App.State.Reducers.ShowLoader +import App.State.Reducers.SupportUrlStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -752,7 +782,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 21 个已迁移切片(20 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 28 个已迁移切片(27 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -816,7 +846,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 21 个已迁移字段,其余 68 个待迁", + "根:迁移表 = 28 个已迁移字段,其余 61 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -824,8 +854,10 @@ object StateCheck { "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", + "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", + "rulesUpdated", "notificationByCountry", "availabilityByRank", ) && - RootWiring.Unmigrated.size == 68 + RootWiring.Unmigrated.size == 61 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -881,7 +913,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 21/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 28/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -3103,6 +3135,238 @@ object StateCheck { ) } + // ㉜ 切片 reducer:LobbyLoader(KW / VW.es1,2 字段;FW/GW 两个匿名类) + run { + val S0 = LobbyLoaderStateSlice() + Check("切片 LobbyLoader:默认不显示、计数 0", !S0.IsShowLoader && S0.ActiveLoadersCount == 0) + val S1 = ReduceLobbyLoader(ReduceLobbyLoader(S0, ShowLoader), ShowLoader) + Check( + "切片 LobbyLoader:ShowLoader 计数 +1 并显示", + S1.IsShowLoader && S1.ActiveLoadersCount == 2 + ) + val S2 = ReduceLobbyLoader(S1, HideLoader) + Check( + "切片 LobbyLoader:HideLoader 计数 -1,仍大于 0 时继续显示", + S2.IsShowLoader && S2.ActiveLoadersCount == 1 + ) + val S3 = ReduceLobbyLoader(ReduceLobbyLoader(S2, HideLoader), HideLoader) + Check( + "切片 LobbyLoader:计数夹到 0 并隐藏", + !S3.IsShowLoader && S3.ActiveLoadersCount == 0 + ) + Check("切片 LobbyLoader:无关动作不改状态", ReduceLobbyLoader(S1, AddFrags(1)) == S1) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, ShowLoader) + Check( + "根:ShowLoader 只动 lobbyLoader", + Root1.LobbyLoader.IsShowLoader && Root1.PurchaseDialogState == Root0.PurchaseDialogState + ) + val W = RootWiring.ByField("lobbyLoader")!! + Check( + "根:lobbyLoader 委派到 KW / Bs.es1 且 2 个动作全部具名", + W.SliceClass == "KW" && W.Reducer == "Bs.es1" && + W.Actions.toSet() == setOf("ShowLoader", "HideLoader") + ) + } + + // ㉝ 切片 reducer:PurchaseDialogState(CO / DO.ur2,4 字段;NO + 2 个实例臂) + run { + val S0 = PurchaseDialogStateSlice() + Check( + "切片 PurchaseDialog:默认值完整", + !S0.WithEmail && S0.Goods.isEmpty() && S0.EmailStatus == null && S0.Time == null + ) + val S1 = ReducePurchaseDialog(S0, ChangeEmailStatus(PurchaseEmailStatus.BUSY)) + Check( + "切片 PurchaseDialog:ChangeEmailStatus 只写邮箱状态", + S1.EmailStatus == PurchaseEmailStatus.BUSY && S1.Goods.isEmpty() && S1.Time == null + ) + val S2 = ReducePurchaseDialog(S1, SuccessfullyPurchase(listOf("g1"), true, 123L)) + Check( + "切片 PurchaseDialog:SuccessfullyPurchase 追加商品并写时间", + S2.WithEmail && S2.Goods == listOf("g1") && S2.Time == 123L && + S2.EmailStatus == PurchaseEmailStatus.BUSY + ) + val S3 = ReducePurchaseDialog(S2, SuccessfullyPurchase(listOf("g2"), false, 456L)) + Check( + "切片 PurchaseDialog:商品是追加而不是替换", + S3.Goods == listOf("g1", "g2") && !S3.WithEmail && S3.Time == 456L + ) + Check( + "切片 PurchaseDialog:ResetPurchaseDialog 复位四个字段", + ReducePurchaseDialog(S3, ResetPurchaseDialog) == PurchaseDialogStateSlice() + ) + Check("切片 PurchaseDialog:无关动作不改状态", ReducePurchaseDialog(S3, AddFrags(1)) == S3) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, ChangeEmailStatus(PurchaseEmailStatus.FREE)) + Check( + "根:ChangeEmailStatus 只动 purchaseDialogState", + Root1.PurchaseDialogState.EmailStatus == PurchaseEmailStatus.FREE && + Root1.LobbyLoader == Root0.LobbyLoader + ) + val W = RootWiring.ByField("purchaseDialogState")!! + Check( + "根:purchaseDialogState 委派到 CO / je.ur2 且 3 个动作全部具名", + W.SliceClass == "CO" && W.Reducer == "je.ur2" && + W.Actions.toSet() == setOf( + "ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase", + ) + ) + } + + // ㉞ 切片 reducer:PromoCode(cZ / wZ.ysg,1 字段;2 个实例臂) + run { + val S0 = PromoCodeStateSlice() + Check("切片 PromoCode:默认 NONE", S0.ValidateStatus == PromoCodeStatus.NONE) + Check( + "切片 PromoCode:ActivatePromoCode 进入 WAIT", + ReducePromoCode(S0, ActivatePromoCode("CODE-1")).ValidateStatus == PromoCodeStatus.WAIT + ) + Check( + "切片 PromoCode:SetCodeValidateStatus 写服务端状态", + ReducePromoCode( + PromoCodeStateSlice(PromoCodeStatus.WAIT), + SetCodeValidateStatus(PromoCodeStatus.ACTIVATED), + ).ValidateStatus == PromoCodeStatus.ACTIVATED + ) + Check("切片 PromoCode:无关动作不改状态", ReducePromoCode(S0, AddFrags(1)) == S0) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, ActivatePromoCode("X")) + Check( + "根:ActivatePromoCode 只动 promoCode", + Root1.PromoCode.ValidateStatus == PromoCodeStatus.WAIT && + Root1.SupportUrl == Root0.SupportUrl + ) + val W = RootWiring.ByField("promoCode")!! + Check( + "根:promoCode 委派到 cZ / Q_.ysg 且 2 个动作", + W.SliceClass == "cZ" && W.Reducer == "Q_.ysg" && + W.Actions == listOf("ActivatePromoCode", "SetCodeValidateStatus") + ) + } + + // ㉟ 切片 reducer:SupportUrl(I4 / P4.ct7,1 字段;A4 + R4) + run { + val S0 = SupportUrlStateSlice() + Check("切片 SupportUrl:默认为空串", S0.Url == "") + Check( + "切片 SupportUrl:SetUrl 写入链接", + ReduceSupportUrl(S0, SetUrl("https://support")).Url == "https://support" + ) + Check( + "切片 SupportUrl:ClearSupportUrl 清空", + ReduceSupportUrl(SupportUrlStateSlice("https://support"), ClearSupportUrl).Url == "" + ) + Check("切片 SupportUrl:无关动作不改状态", ReduceSupportUrl(S0, AddFrags(1)) == S0) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetUrl("u")) + Check( + "根:SetUrl 只动 supportUrl", + Root1.SupportUrl.Url == "u" && Root1.RulesUpdated == Root0.RulesUpdated + ) + val W = RootWiring.ByField("supportUrl")!! + Check( + "根:supportUrl 委派到 I4 / Yl.ct7 且 2 个动作", + W.SliceClass == "I4" && W.Reducer == "Yl.ct7" && + W.Actions.toSet() == setOf("ClearSupportUrl", "SetUrl") + ) + } + + // ㊱ 切片 reducer:RulesUpdated(i1 / u1.dsm,1 字段;1 个实例臂) + run { + val S0 = RulesUpdatedStateSlice() + Check("切片 RulesUpdated:默认 text = null", S0.Text == null) + Check( + "切片 RulesUpdated:UpdateRules 写入正文", + ReduceRulesUpdated(S0, UpdateRules("新规则")).Text == "新规则" + ) + Check("切片 RulesUpdated:无关动作不改状态", ReduceRulesUpdated(S0, AddFrags(1)) == S0) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, UpdateRules("t")) + Check( + "根:UpdateRules 只动 rulesUpdated", + Root1.RulesUpdated.Text == "t" && Root1.NotificationByCountry == Root0.NotificationByCountry + ) + val W = RootWiring.ByField("rulesUpdated")!! + Check( + "根:rulesUpdated 委派到 i1 / Ch.dsm", + W.SliceClass == "i1" && W.Reducer == "Ch.dsm" && W.Actions == listOf("UpdateRules") + ) + } + + // ㊲ 切片 reducer:NotificationByCountry(gJ / $J.ssd,4 字段;Init + qJ) + run { + val S0 = NotificationByCountryStateSlice() + Check( + "切片 NotificationByCountry:默认三条消息为 null、弹窗未展示", + S0.DisableRegistrationMessage == null && S0.DisableShopMessage == null && + S0.HomeScreenMessage == null && !S0.HomeScreenDialogShown + ) + val S1 = ReduceNotificationByCountry( + S0, + InitNotificationByCountry("r", "s", "h"), + ) + Check( + "切片 NotificationByCountry:Init 写三条消息且不动弹窗标志", + S1.DisableRegistrationMessage == "r" && S1.DisableShopMessage == "s" && + S1.HomeScreenMessage == "h" && !S1.HomeScreenDialogShown + ) + Check( + "切片 NotificationByCountry:qJ 只把弹窗标志置 true", + ReduceNotificationByCountry(S1, MarkHomeScreenDialogShown) == + S1.copy(HomeScreenDialogShown = true) + ) + Check( + "切片 NotificationByCountry:无关动作不改状态", + ReduceNotificationByCountry(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, MarkHomeScreenDialogShown) + Check( + "根:qJ 只动 notificationByCountry", + Root1.NotificationByCountry.HomeScreenDialogShown && + Root1.RulesUpdated == Root0.RulesUpdated + ) + val W = RootWiring.ByField("notificationByCountry")!! + Check( + "根:notificationByCountry 委派到 gJ / R_.ssd 且 2 个动作", + W.SliceClass == "gJ" && W.Reducer == "R_.ssd" && + W.Actions.toSet() == setOf("Init", "MarkHomeScreenDialogShown") + ) + } + + // ㊳ 切片 reducer:AvailabilityByRank(tet / set.xth,1 字段;1 个实例臂) + run { + val S0 = AvailabilityByRankStateSlice() + Check("切片 AvailabilityByRank:默认空表", S0.Data.isEmpty()) + val S1 = ReduceAvailabilityByRank(S0, SetLoadedData(mapOf(1 to "a", 2 to "b"))) + Check( + "切片 AvailabilityByRank:SetLoadedData 写整张表", + S1.Data == mapOf(1 to "a", 2 to "b") + ) + Check("切片 AvailabilityByRank:无关动作不改状态", ReduceAvailabilityByRank(S1, AddFrags(1)) == S1) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetLoadedData(mapOf(3 to "c"))) + Check( + "根:SetLoadedData 只动 availabilityByRank", + Root1.AvailabilityByRank.Data == mapOf(3 to "c") && + Root1.PromoCode == Root0.PromoCode + ) + val W = RootWiring.ByField("availabilityByRank")!! + Check( + "根:availabilityByRank 委派到 tet / \$p.xth", + W.SliceClass == "tet" && W.Reducer == "\$p.xth" && + W.Actions == listOf("SetLoadedData") + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)