diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 1d56d00..4d72aa1 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` 动作名。 - - 已迁移切片 **28/89**: + - 已迁移切片 **33/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) @@ -240,12 +240,28 @@ `Init` 写三条国家提示消息,`qJ`→`MarkHomeScreenDialogShown` 只把 `homeScreenDialogShown` 置 true。 - `availabilityByRank` → `tet` / `$p.xth`(`AvailabilityByRankReducer.kt`):1 字段, `SetLoadedData` 整体替换等级可购买表,缺省空 Map。 + - `icons` → `oH` / `vs.kru`(`IconsReducer.kt`):1 字段,2 个分支。 + `SetIcons` 把 `List` 收成 `icon → resource` 的 Map(原 `HashMap.put`),缺省空 Map。 + - `questCategories` → `zZ` / `th.lsh`(`QuestCategoriesReducer.kt`):3 字段,5 个分支。 + `SetQuestCategories` 按 `index` 升序;`RemoveQuestCategory` 按对象删除; + `RequestQuestCategoriesStatistic` 在 `isHiddenUpdate=false` 时把 `statisticLoadedTime` 推到 `Long.MAX_VALUE`; + `SetQuestCategoriesStatistic` 按 `questCategoryId` 建索引并写 `Date.now()`。 + - `mainQuest` → `WZ` / `ih.bsk`(`MainQuestReducer.kt`):5 字段,7 个实例臂。 + `SetQuests` 把任务按 `completedCount` 展开(`completed && count != 0` 的原始项丢弃, + 再为 `1..count` 生成 `completed=true` 的副本),最后按 `completed` 降序稳定排序,并清空已领奖; + `UpdateQuests` 按 `id` 就地替换/追加;`AddRewardedQuest` 里的 `questId` 实际是任务对象; + `VZ`→`ResetQuestsLoaded`(无 `toString`,只 `isLoaded=false`),`KZ`→`QuestsLoaded` 只置 `isLoaded=true`。 + - `securitySettings` → `V2` / `pc.qsv`(`SecuritySettingsReducer.kt`):1 字段,缺省 `isProfileBlocked=true`, + `SetProfileBlockStatus` 直接写布尔。 + - `referralEvent` → `pQ` / `lh.osk`(`ReferralEventReducer.kt`):14 字段,4 个实例臂。 + `SetReferrer` 写前 2 字段、`SetReferrals` 写人数与明细、`SetPromoCode` 写邀请码; + `SetReferralEventData` 一次写后 9 个字段(面板标题 / 弹窗图与背景 / 两个分类 id / 任务数 / 两个隐藏标志 / 公告时间)。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 27 个有 reducer 的切片, - `launchParams` 与其余 **61 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 28/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **419 项全绿**,含 89 槽位完整性、27 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 32 个有 reducer 的切片, + `launchParams` 与其余 **56 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 33/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **457 项全绿**,含 89 槽位完整性、32 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -258,7 +274,7 @@ entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、 language 的 1 个、announcement 的 3 个、news 的 3 个(含两个单例)、notifications 的 2 个、 quests 的 1 个、lobbyLoader 的 2 个、purchaseDialogState 的 1 个、supportUrl 的 1 个、 - notificationByCountry 的 1 个匿名类 / 单例同样按效果定名并登记。 + notificationByCountry 的 1 个、mainQuest 的 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 bfb8745..6319c22 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T05:17:45.346Z", + "generatedAt": "2026-10-24T06:01:37.203Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -34,7 +34,12 @@ "supportUrl", "rulesUpdated", "notificationByCountry", - "availabilityByRank" + "availabilityByRank", + "icons", + "questCategories", + "mainQuest", + "securitySettings", + "referralEvent" ], "AnonymousActions": [ { @@ -697,6 +702,12 @@ "Name": "MarkHomeScreenDialogShown", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)" + }, + { + "Local": "VZ", + "Name": "ResetQuestsLoaded", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isLoaded=false(`n.qsj(!1)`;动作类 rsj_1 在 reducer 里未被读取)" } ], "Markers": [ @@ -1929,7 +1940,7 @@ "SetProfileBlockStatus" ], "Default": "new V2()", - "Migrated": false + "Migrated": true }, { "Field": "purchaseDialogState", @@ -1999,16 +2010,16 @@ "Reducer": "ih.bsk", "Delegate": "ih.bsk(t, n.fpx_1)", "Actions": [ - "?VZ", "AddRewardedQuest", "QuestsLoaded", + "ResetQuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests" ], "Default": "new WZ()", - "Migrated": false + "Migrated": true }, { "Field": "language", @@ -2270,7 +2281,7 @@ "SetIcons" ], "Default": "new oH()", - "Migrated": false + "Migrated": true }, { "Field": "itemNotifications", @@ -2331,7 +2342,7 @@ "SetQuestCategoriesStatistic" ], "Default": "new zZ()", - "Migrated": false + "Migrated": true }, { "Field": "proBattle", @@ -2434,7 +2445,7 @@ "SetReferrer" ], "Default": "new pQ()", - "Migrated": false + "Migrated": true }, { "Field": "supportUrl", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 8830681..e032bbd 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -179,6 +179,8 @@ const AnonymousActionNames = new Map([ ['R4', 'ClearSupportUrl'], // notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 效果定名。 ['qJ', 'MarkHomeScreenDialogShown'], + // mainQuest 切片(WZ / ih.bsk):VZ 没有 toString,按 reducer 效果定名。 + ['VZ', 'ResetQuestsLoaded'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -592,6 +594,11 @@ const Migrated = { Reduce: 'ReduceNotificationByCountry', }, availabilityByRank: { Type: 'AvailabilityByRankStateSlice', Reduce: 'ReduceAvailabilityByRank' }, + icons: { Type: 'IconsStateSlice', Reduce: 'ReduceIcons' }, + questCategories: { Type: 'QuestCategoriesStateSlice', Reduce: 'ReduceQuestCategories' }, + mainQuest: { Type: 'MainQuestStateSlice', Reduce: 'ReduceMainQuest' }, + securitySettings: { Type: 'SecuritySettingsStateSlice', Reduce: 'ReduceSecuritySettings' }, + referralEvent: { Type: 'ReferralEventStateSlice', Reduce: 'ReduceReferralEvent' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -799,6 +806,11 @@ const NotificationByCountryAnonymousEffects = { qJ: 'homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)', }; +// mainQuest 切片(WZ / ih.bsk):VZ 没有 toString / 不在 action_inventory,按 reducer 分支效果定名。 +const MainQuestAnonymousEffects = { + VZ: 'isLoaded=false(`n.qsj(!1)`;动作类 rsj_1 在 reducer 里未被读取)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -923,6 +935,12 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(MainQuestAnonymousEffects).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/IconsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/IconsReducer.kt new file mode 100644 index 0000000..312a5dd --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/IconsReducer.kt @@ -0,0 +1,29 @@ +// IconsReducer.kt —— 图标资源表切片(对应 module_10032 的 oH 状态 + hH.kru) +// +// 证据链: +// 状态类 oH:1 个字段,toString 名 "Icons(icons=…)";copy 是 iru(1 字段)。 +// 默认:icons = ag() = emptyMap()。 +// 根委派:`vs.kru(t, n.wpx_1)`。 +// reducer:`t instanceof _H ? n.iru(u) : n`,其中 +// `u` 是 HashMap:对 `action.icons` 每一项取 `Pair(icon, resource)` 再 put。 +// 生成物 `IconData(icon: String, resource: Any?)` 与之一致。 +package App.State.Reducers + +import App.State.SetIcons + +/* ------------------------------------------------------------------ 状态 */ + +data class IconsStateSlice( + val Icons: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceIcons( + State0: IconsStateSlice, + Action: Any, +): IconsStateSlice = when (Action) { + is SetIcons -> + State0.copy(Icons = Action.icons.associate { it.icon to it.resource }) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MainQuestReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MainQuestReducer.kt new file mode 100644 index 0000000..99599b4 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MainQuestReducer.kt @@ -0,0 +1,88 @@ +// MainQuestReducer.kt —— 主线任务切片(对应 module_10032 的 WZ 状态 + nQ.bsk) +// +// 证据链: +// 状态类 WZ:5 个字段,toString 名 "MainQuest(...)";copy 是 qsj(5 字段 + 回调)。 +// 默认:isLoaded=false、quests=emptyList()、rewardedQuests=emptyList()、 +// eventPassShopItemId=null、lastBattleUpdatedQuests=emptyList()。 +// 根委派:`(null == ih && new nQ(), ih).bsk(t, n.fpx_1)` → `ih.bsk(t, n.fpx_1)`。 +// reducer: +// VZ → isLoaded=false(动作类无 toString,字段 rsj_1 在 reducer 里不读) +// KZ → isLoaded=true(`QuestsLoaded(quests)` 里的 quests 在这一支里没有被读取) +// YZ → quests = Expand(action.quests)、rewardedQuests = emptyList() +// XZ → quests = 按 id 就地替换,找不到则追加(`Qd(c.me7_1, _.me7_1)`) +// ZZ → rewardedQuests = rewardedQuests + action.questId +// QZ → eventPassShopItemId = action.shopItemId +// tQ → lastBattleUpdatedQuests = action.updatedQuestsData +// `Expand`(原 `uQ`): +// 每个任务若 `completed && completedCount != 0` 则不保留原项; +// 再对 1..completedCount 生成一份 `completed=true、completedCount=该值` 的副本; +// 最后按 completed 降序稳定排序(`_Q(aQ)`,aQ = compareValues(n.completed, t.completed))。 +package App.State.Reducers + +import App.State.AddRewardedQuest +import App.State.MainQuestInfo +import App.State.QuestsLoaded +import App.State.SetBattleUpdatedQuests +import App.State.SetEventPassShopItemId +import App.State.SetQuests +import App.State.UpdateQuests + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `VZ`:把 isLoaded 置 false(没有 toString / 不在 action_inventory 里)。 */ +object ResetQuestsLoaded + +/* ------------------------------------------------------------------ 状态 */ + +data class MainQuestStateSlice( + val IsLoaded: Boolean = false, + val Quests: List = emptyList(), + val RewardedQuests: List = emptyList(), + val EventPassShopItemId: Long? = null, + val LastBattleUpdatedQuests: List = emptyList(), +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `hQ(c)`:把一份任务副本标记为「已完成第 c 次」。 */ +private fun MarkCompleted(Quest: MainQuestInfo, Count: Int): MainQuestInfo = + Quest.copy(completed = true, completedCount = Count) + +/** 原 `uQ`:把任务按完成次数展开,并按 completed 降序排序。 */ +private fun Expand(Quests: List): List { + val Result = mutableListOf() + for (Quest in Quests) { + val Count = Quest.completedCount + if (!(Quest.completed && Count != 0)) Result += Quest + for (Index in 1..Count) Result += MarkCompleted(Quest, Index) + } + return Result.sortedWith(compareByDescending { it.completed }) +} + +fun ReduceMainQuest( + State0: MainQuestStateSlice, + Action: Any, +): MainQuestStateSlice = when (Action) { + is ResetQuestsLoaded -> State0.copy(IsLoaded = false) + is QuestsLoaded -> State0.copy(IsLoaded = true) + is SetQuests -> State0.copy( + Quests = Expand(Action.quests as List), + RewardedQuests = emptyList(), + ) + is UpdateQuests -> { + val Result = State0.Quests.toMutableList() + for (Quest in Action.quests) { + val Index = Result.indexOfFirst { it.id == Quest.id } + if (Index >= 0) Result[Index] = Quest else Result += Quest + } + State0.copy(Quests = Result) + } + is AddRewardedQuest -> State0.copy( + RewardedQuests = State0.RewardedQuests + (Action.questId as MainQuestInfo), + ) + is SetEventPassShopItemId -> State0.copy(EventPassShopItemId = Action.shopItemId) + is SetBattleUpdatedQuests -> State0.copy( + LastBattleUpdatedQuests = Action.updatedQuestsData as List, + ) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/QuestCategoriesReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/QuestCategoriesReducer.kt new file mode 100644 index 0000000..8c2a76c --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/QuestCategoriesReducer.kt @@ -0,0 +1,50 @@ +// QuestCategoriesReducer.kt —— 任务分类切片(对应 module_10032 的 zZ 状态 + SZ.lsh) +// +// 证据链: +// 状态类 zZ:3 个字段,toString 名 "QuestCategories(...)";copy 是 fsh(3 字段 + 回调)。 +// 默认:categories=emptyList()、statistic=emptyMap()、statisticLoadedTime=0L(`new Dd(0, 0)` 是 Long)。 +// 根委派:`th.lsh(t, n.apy_1)`。 +// reducer: +// bZ → categories = action.categories 按 `index` 升序(`xZ(jZ)`,jZ 比较 ze9_1 = index) +// qZ → categories = categories - action.category(`Ym` 是 minus 元素) +// yZ → isHiddenUpdate ? 不变 : statisticLoadedTime = Long.MAX_VALUE +// (`new Dd(-1, 2147483647)` 是 Long 的高低位,即 Long.MAX_VALUE) +// $Z → statistic = action.statistic.associateBy { it.questCategoryId }、statisticLoadedTime = Date.now() +// `Md()` = 当前毫秒(与 LootBoxesReducer 里的 CurrentTimeMillis 同源)。 +package App.State.Reducers + +import App.State.QuestCategoryData +import App.State.RemoveQuestCategory +import App.State.RequestQuestCategoriesStatistic +import App.State.SetQuestCategories +import App.State.SetQuestCategoriesStatistic + +/* ------------------------------------------------------------------ 状态 */ + +data class QuestCategoriesStateSlice( + val Categories: List = emptyList(), + val Statistic: Map = emptyMap(), + val StatisticLoadedTime: Long = 0L, +) + +/* ------------------------------------------------------------------ reducer */ + +private fun CurrentTimeMillis(): Long = kotlin.js.Date.now().toLong() + +fun ReduceQuestCategories( + State0: QuestCategoriesStateSlice, + Action: Any, +): QuestCategoriesStateSlice = when (Action) { + is SetQuestCategories -> + State0.copy(Categories = Action.categories.sortedBy { it.index }) + is RemoveQuestCategory -> + State0.copy(Categories = State0.Categories - (Action.category as QuestCategoryData)) + is RequestQuestCategoriesStatistic -> + if (Action.isHiddenUpdate) State0 + else State0.copy(StatisticLoadedTime = Long.MAX_VALUE) + is SetQuestCategoriesStatistic -> State0.copy( + Statistic = Action.statistic.associateBy { it.questCategoryId }, + StatisticLoadedTime = CurrentTimeMillis(), + ) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ReferralEventReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ReferralEventReducer.kt new file mode 100644 index 0000000..a570b95 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ReferralEventReducer.kt @@ -0,0 +1,75 @@ +// ReferralEventReducer.kt —— 推荐活动切片(对应 module_10032 的 pQ 状态 + bQ.osk) +// +// 证据链: +// 状态类 pQ:14 个字段,toString 名 "ReferralEvent(...)";copy 是 hsk(14 字段 + 回调)。 +// 默认:referrer=null、referrerUid=null、referralsCount=0、referralsData=emptyMap()、 +// code=null、referrerPanelTitle=""、promoCodeDialogImage=null、promoCodeDialogBackGround=null、 +// referrerCategoryId=null、referralCategoryId=null、referralQuestsSize=0、 +// hideReferrerCategory=true、hideReferralCategory=true、hidedAnnounce=null。 +// 根委派:`lh.osk(t, n.hpy_1)`。 +// reducer: +// zQ(SetReferrer) → referrer、referrerUid +// kQ(SetReferrals) → referralsCount、referralsData +// mQ(SetPromoCode) → code +// wQ(SetReferralEventData) → 后 9 个字段,来自 ReferralEventUserModelCC +// (字段顺序与模型 toString 一致:referrerPanelTitle、promoCodeDialogImage、 +// promoCodeDialogBackGround、referrerCategory、referralCategory、referralQuestsSize、 +// hideReferrerCategory、hideReferralCategory、hidedAnnounce) +package App.State.Reducers + +import App.State.ReferralEventUserModelCC +import App.State.SetPromoCode +import App.State.SetReferralEventData +import App.State.SetReferrals +import App.State.SetReferrer + +/* ------------------------------------------------------------------ 状态 */ + +data class ReferralEventStateSlice( + val Referrer: Any? = null, + val ReferrerUid: Any? = null, + val ReferralsCount: Int = 0, + val ReferralsData: Map = emptyMap(), + val Code: String? = null, + val ReferrerPanelTitle: String = "", + val PromoCodeDialogImage: Any? = null, + val PromoCodeDialogBackGround: Any? = null, + val ReferrerCategoryId: Long? = null, + val ReferralCategoryId: Long? = null, + val ReferralQuestsSize: Int = 0, + val HideReferrerCategory: Boolean = true, + val HideReferralCategory: Boolean = true, + val HidedAnnounce: Long? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceReferralEvent( + State0: ReferralEventStateSlice, + Action: Any, +): ReferralEventStateSlice = when (Action) { + is SetReferrer -> State0.copy( + Referrer = Action.referrerId, + ReferrerUid = Action.referrerUid, + ) + is SetReferrals -> State0.copy( + ReferralsCount = Action.referralsCount, + ReferralsData = Action.referralsData as Map, + ) + is SetPromoCode -> State0.copy(Code = Action.code) + is SetReferralEventData -> { + val Data: ReferralEventUserModelCC = Action.eventData + State0.copy( + ReferrerPanelTitle = Data.referrerPanelTitle, + PromoCodeDialogImage = Data.promoCodeDialogImage, + PromoCodeDialogBackGround = Data.promoCodeDialogBackGround, + ReferrerCategoryId = Data.referrerCategory, + ReferralCategoryId = Data.referralCategory, + ReferralQuestsSize = Data.referralQuestsSize, + HideReferrerCategory = Data.hideReferrerCategory, + HideReferralCategory = Data.hideReferralCategory, + HidedAnnounce = Data.hidedAnnounce, + ) + } + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SecuritySettingsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SecuritySettingsReducer.kt new file mode 100644 index 0000000..3006889 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SecuritySettingsReducer.kt @@ -0,0 +1,26 @@ +// SecuritySettingsReducer.kt —— 账号安全设置切片(对应 module_10032 的 V2 状态 + X2.qsv) +// +// 证据链: +// 状态类 V2:1 个字段,toString 名 "SecuritySettingsState(isProfileBlocked=…)";copy 是 f7j(1 字段)。 +// 默认:`function V2(t) { t = t === cd || t }` → isProfileBlocked=true。 +// 根委派:`pc.qsv(t, n.vpx_1)`。 +// reducer:`t instanceof K2 ? n.f7j(t.osv_1) : n`,`K2` 是 SetProfileBlockStatus。 +package App.State.Reducers + +import App.State.SetProfileBlockStatus + +/* ------------------------------------------------------------------ 状态 */ + +data class SecuritySettingsStateSlice( + val IsProfileBlocked: Boolean = true, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceSecuritySettings( + State0: SecuritySettingsStateSlice, + Action: Any, +): SecuritySettingsStateSlice = when (Action) { + is SetProfileBlockStatus -> State0.copy(IsProfileBlocked = Action.isProfileBlocked) + 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 83f5f74..9aac6f3 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 出新状态)。 -// 这里采用同一形状。已迁移的 28 个切片里,27 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 33 个切片里,32 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -30,9 +30,14 @@ // 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)`) +// icons → ReduceIcons(oH / vs.kru,根委派 `vs.kru(t, n.wpx_1)`) +// questCategories → ReduceQuestCategories(zZ / th.lsh,根委派 `th.lsh(t, n.apy_1)`) +// mainQuest → ReduceMainQuest(WZ / ih.bsk,根委派 `(null == ih && new nQ(), ih).bsk(t, n.fpx_1)`) +// securitySettings → ReduceSecuritySettings(V2 / pc.qsv,根委派 `pc.qsv(t, n.apx_1)`) +// referralEvent → ReduceReferralEvent(pQ / lh.osk,根委派 `lh.osk(t, n.hpy_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 61 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 56 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -43,11 +48,13 @@ import App.State.Reducers.ReduceEntrance import App.State.Reducers.ReduceGarage import App.State.Reducers.ReduceGaragePreview import App.State.Reducers.ReduceHome +import App.State.Reducers.ReduceIcons 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.ReduceMainQuest import App.State.Reducers.ReduceAvailabilityByRank import App.State.Reducers.ReduceLobbyLoader import App.State.Reducers.ReduceNews @@ -56,9 +63,12 @@ import App.State.Reducers.ReduceNotifications import App.State.Reducers.ReduceProBattle import App.State.Reducers.ReducePromoCode import App.State.Reducers.ReducePurchaseDialog +import App.State.Reducers.ReduceQuestCategories import App.State.Reducers.ReduceQuests +import App.State.Reducers.ReduceReferralEvent import App.State.Reducers.ReduceRulesUpdated import App.State.Reducers.ReduceScreen +import App.State.Reducers.ReduceSecuritySettings import App.State.Reducers.ReduceSupportUrl import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceShop @@ -105,6 +115,11 @@ object RootReducer { RulesUpdated = ReduceRulesUpdated(State0.RulesUpdated, Action), NotificationByCountry = ReduceNotificationByCountry(State0.NotificationByCountry, Action), AvailabilityByRank = ReduceAvailabilityByRank(State0.AvailabilityByRank, Action), + Icons = ReduceIcons(State0.Icons, Action), + QuestCategories = ReduceQuestCategories(State0.QuestCategories, Action), + MainQuest = ReduceMainQuest(State0.MainQuest, Action), + SecuritySettings = ReduceSecuritySettings(State0.SecuritySettings, Action), + ReferralEvent = ReduceReferralEvent(State0.ReferralEvent, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index a2bd0d3..a7f58a2 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -11,20 +11,25 @@ import App.State.Reducers.EntranceStateSlice import App.State.Reducers.GarageStateSlice import App.State.Reducers.GaragePreviewStateSlice import App.State.Reducers.HomeStateSlice +import App.State.Reducers.IconsStateSlice 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.MainQuestStateSlice 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.QuestCategoriesStateSlice import App.State.Reducers.QuestsStateSlice +import App.State.Reducers.ReferralEventStateSlice import App.State.Reducers.RulesUpdatedStateSlice import App.State.Reducers.ScreenStateSlice +import App.State.Reducers.SecuritySettingsStateSlice import App.State.Reducers.SettingsStateSlice import App.State.Reducers.ShopStateSlice import App.State.Reducers.SupportUrlStateSlice @@ -144,8 +149,8 @@ data class RootState( val GraphicsSettings: Any? = null, /** 原 controlSettings(i2 / _2().ess)。未迁移:原样透传。 */ val ControlSettings: Any? = null, - /** 原 securitySettings(V2 / pc.qsv)。未迁移:原样透传。 */ - val SecuritySettings: Any? = null, + /** 原 securitySettings(V2 / pc.qsv)。已迁移。 */ + val SecuritySettings: SecuritySettingsStateSlice = SecuritySettingsStateSlice(), /** 原 purchaseDialogState(CO / je.ur2)。已迁移。 */ val PurchaseDialogState: PurchaseDialogStateSlice = PurchaseDialogStateSlice(), /** 原 battleInviteState(att / Ttt().nth)。未迁移:原样透传。 */ @@ -154,8 +159,8 @@ data class RootState( val KeyboardSettings: Any? = null, /** 原 touchscreenHudSettings(j7 / N7().xsv)。未迁移:原样透传。 */ val TouchscreenHudSettings: Any? = null, - /** 原 mainQuest(WZ / ih.bsk)。未迁移:原样透传。 */ - val MainQuest: Any? = null, + /** 原 mainQuest(WZ / ih.bsk)。已迁移。 */ + val MainQuest: MainQuestStateSlice = MainQuestStateSlice(), /** 原 language(? / m6().jtg)。已迁移。 */ val Language: Language = App.State.Language.EN, /** 原 launchParams(? / (pass-through))。已迁移(透传,强类型)。 */ @@ -188,16 +193,16 @@ data class RootState( val RewardedVideosState: Any? = null, /** 原 twitchState(g9 / uf.gta)。已迁移。 */ val TwitchState: TwitchStateSlice = TwitchStateSlice(), - /** 原 icons(oH / vs.kru)。未迁移:原样透传。 */ - val Icons: Any? = null, + /** 原 icons(oH / vs.kru)。已迁移。 */ + val Icons: IconsStateSlice = IconsStateSlice(), /** 原 itemNotifications(_1 / A1().dso)。未迁移:原样透传。 */ val ItemNotifications: Any? = null, /** 原 contextTutorial(Mst / Jst().tth)。未迁移:原样透传。 */ val ContextTutorial: Any? = null, /** 原 suppliesTutorial(? / Uw.uth)。未迁移:原样透传。 */ val SuppliesTutorial: Any? = null, - /** 原 questCategories(zZ / th.lsh)。未迁移:原样透传。 */ - val QuestCategories: Any? = null, + /** 原 questCategories(zZ / th.lsh)。已迁移。 */ + val QuestCategories: QuestCategoriesStateSlice = QuestCategoriesStateSlice(), /** 原 proBattle(Wtt / uv.wth)。已迁移。 */ val ProBattle: ProBattleStateSlice = ProBattleStateSlice(), /** 原 availabilityByRank(tet / $p.xth)。已迁移。 */ @@ -210,8 +215,8 @@ data class RootState( val ContractState: Any? = null, /** 原 promoCode(cZ / Q_.ysg)。已迁移。 */ val PromoCode: PromoCodeStateSlice = PromoCodeStateSlice(), - /** 原 referralEvent(pQ / lh.osk)。未迁移:原样透传。 */ - val ReferralEvent: Any? = null, + /** 原 referralEvent(pQ / lh.osk)。已迁移。 */ + val ReferralEvent: ReferralEventStateSlice = ReferralEventStateSlice(), /** 原 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 69742bd..2f060c1 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -73,7 +73,7 @@ object RootWiring { 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")), - SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("?VZ", "AddRewardedQuest", "QuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")), + SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("AddRewardedQuest", "QuestsLoaded", "ResetQuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")), 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")), @@ -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", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank") + 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", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 e9b32dc..a8f45a6 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -9,10 +9,11 @@ // ⑥ 动作目录: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 / -// LobbyLoader / PurchaseDialogState / PromoCode / SupportUrl / RulesUpdated / NotificationByCountry / AvailabilityByRank; +// LobbyLoader / PurchaseDialogState / PromoCode / SupportUrl / RulesUpdated / NotificationByCountry / AvailabilityByRank / +// Icons / QuestCategories / MainQuest / SecuritySettings / ReferralEvent; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -355,6 +356,38 @@ import App.State.Reducers.ResetPurchaseDialog import App.State.Reducers.RulesUpdatedStateSlice import App.State.Reducers.ShowLoader import App.State.Reducers.SupportUrlStateSlice +import App.State.IconData +import App.State.SetIcons +import App.State.Reducers.IconsStateSlice +import App.State.Reducers.ReduceIcons +import App.State.QuestCategoryData +import App.State.QuestCategoryStatistic +import App.State.RemoveQuestCategory +import App.State.RequestQuestCategoriesStatistic +import App.State.SetQuestCategories +import App.State.SetQuestCategoriesStatistic +import App.State.Reducers.QuestCategoriesStateSlice +import App.State.Reducers.ReduceQuestCategories +import App.State.AddRewardedQuest +import App.State.MainQuestInfo +import App.State.QuestsLoaded +import App.State.SetBattleUpdatedQuests +import App.State.SetEventPassShopItemId +import App.State.SetQuests +import App.State.UpdateQuests +import App.State.Reducers.MainQuestStateSlice +import App.State.Reducers.ReduceMainQuest +import App.State.Reducers.ResetQuestsLoaded +import App.State.SetProfileBlockStatus +import App.State.Reducers.ReduceSecuritySettings +import App.State.Reducers.SecuritySettingsStateSlice +import App.State.ReferralEventUserModelCC +import App.State.SetPromoCode +import App.State.SetReferralEventData +import App.State.SetReferrals +import App.State.SetReferrer +import App.State.Reducers.ReduceReferralEvent +import App.State.Reducers.ReferralEventStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -782,7 +815,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 28 个已迁移切片(27 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 33 个已迁移切片(32 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -846,7 +879,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 28 个已迁移字段,其余 61 个待迁", + "根:迁移表 = 33 个已迁移字段,其余 56 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -856,8 +889,9 @@ object StateCheck { "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", + "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", ) && - RootWiring.Unmigrated.size == 61 + RootWiring.Unmigrated.size == 56 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -913,7 +947,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 28/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 33/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -3367,6 +3401,269 @@ object StateCheck { ) } + // ㊴ 切片 reducer:Icons(oH / vs.kru,1 字段;_H → SetIcons) + run { + val S0 = IconsStateSlice() + Check("切片 Icons:默认空表", S0.Icons.isEmpty()) + val S1 = ReduceIcons( + S0, + SetIcons(listOf(IconData("rank", "r1"), IconData("gold", "r2"))), + ) + Check( + "切片 Icons:SetIcons 建成 icon→resource 表", + S1.Icons == mapOf("rank" to "r1", "gold" to "r2") + ) + Check("切片 Icons:无关动作不改状态", ReduceIcons(S1, AddFrags(1)) == S1) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetIcons(listOf(IconData("a", null)))) + Check( + "根:SetIcons 只动 icons", + Root1.Icons.Icons.keys == setOf("a") && + Root1.QuestCategories == Root0.QuestCategories + ) + val W = RootWiring.ByField("icons")!! + Check( + "根:icons 委派到 oH / vs.kru 且 1 个动作", + W.SliceClass == "oH" && W.Reducer == "vs.kru" && W.Actions == listOf("SetIcons") + ) + } + + // ㊵ 切片 reducer:QuestCategories(zZ / th.lsh,3 字段;4 个实例臂) + run { + fun Cat(Id: Long, Index: Int): QuestCategoryData = QuestCategoryData( + color = "", description = "", endDate = null, hideIfEmpty = false, id = Id, + image = null, index = Index, name = "c$Id", rankRange = "", min = "", + startDate = null, type = "", + ) + + val A = Cat(1L, 2) + val B = Cat(2L, 1) + val S0 = QuestCategoriesStateSlice() + Check( + "切片 QuestCategories:默认空表、加载时间 0", + S0.Categories.isEmpty() && S0.Statistic.isEmpty() && S0.StatisticLoadedTime == 0L + ) + val S1 = ReduceQuestCategories(S0, SetQuestCategories(listOf(A, B))) + Check( + "切片 QuestCategories:SetQuestCategories 按 index 升序", + S1.Categories == listOf(B, A) + ) + Check( + "切片 QuestCategories:RemoveQuestCategory 按对象删除", + ReduceQuestCategories(S1, RemoveQuestCategory(B)).Categories == listOf(A) + ) + Check( + "切片 QuestCategories:隐藏刷新不改加载时间", + ReduceQuestCategories(S1, RequestQuestCategoriesStatistic(true)) == S1 + ) + Check( + "切片 QuestCategories:强制刷新把加载时间推到 Long.MAX_VALUE", + ReduceQuestCategories(S1, RequestQuestCategoriesStatistic(false)) + .StatisticLoadedTime == Long.MAX_VALUE + ) + val S2 = ReduceQuestCategories( + S1, + SetQuestCategoriesStatistic( + listOf( + QuestCategoryStatistic(nearEndTime = null, questCategoryId = 1L, quests = 3), + QuestCategoryStatistic(nearEndTime = 5L, questCategoryId = 2L, quests = 4), + ) + ), + ) + Check( + "切片 QuestCategories:统计按 questCategoryId 建索引并写当前时间", + S2.Statistic.keys == setOf(1L, 2L) && S2.StatisticLoadedTime > 0L + ) + Check("切片 QuestCategories:无关动作不改状态", ReduceQuestCategories(S2, AddFrags(1)) == S2) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetQuestCategories(listOf(A))) + Check( + "根:SetQuestCategories 只动 questCategories", + Root1.QuestCategories.Categories == listOf(A) && Root1.MainQuest == Root0.MainQuest + ) + val W = RootWiring.ByField("questCategories")!! + Check( + "根:questCategories 委派到 zZ / th.lsh 且 4 个动作", + W.SliceClass == "zZ" && W.Reducer == "th.lsh" && + W.Actions.toSet() == setOf( + "SetQuestCategories", "RemoveQuestCategory", + "RequestQuestCategoriesStatistic", "SetQuestCategoriesStatistic", + ) + ) + } + + // ㊶ 切片 reducer:MainQuest(WZ / ih.bsk,5 字段;7 个动作,含 VZ→ResetQuestsLoaded) + run { + fun Quest( + Id: Long, + Name: String = "q$Id", + Completed: Boolean = false, + Count: Int = 0, + ): MainQuestInfo = MainQuestInfo( + action = "", actionParams = null, activateTime = null, active = true, + battlePass = false, categoryId = 1L, completed = Completed, completedCount = Count, + conditions = null, battleMode = null, contextItemsInfo = null, id = Id, name = Name, + quantity = 1, skipForShowProgress = false, type = "", depends = emptyList(), + description = "", durationInMinutes = 0, eventPass = false, + extendedPeriodicalEndTime = null, extendedPeriodicalStartTime = null, icon = null, + minRank = 0, oneTime = false, pinned = false, premium = false, progress = null, + rewardExpireTime = null, rewards = "", count = 1, preview = null, + skipNewNotification = false, sortedIndex = 0, startTime = 0L, COMPLETE_QUEST = "", + ) + + val Q1 = Quest(1L) + val Q2 = Quest(2L, "q2", Completed = true, Count = 2) + val S0 = MainQuestStateSlice() + Check( + "切片 MainQuest:默认未加载、三个列表为空、活动商品 id 为 null", + !S0.IsLoaded && S0.Quests.isEmpty() && S0.RewardedQuests.isEmpty() && + S0.EventPassShopItemId == null && S0.LastBattleUpdatedQuests.isEmpty() + ) + val L1 = ReduceMainQuest(S0, QuestsLoaded(listOf(Q1))) + Check("切片 MainQuest:QuestsLoaded 只置 isLoaded", L1.IsLoaded && L1.Quests.isEmpty()) + Check( + "切片 MainQuest:ResetQuestsLoaded 复位 isLoaded", + !ReduceMainQuest(L1, ResetQuestsLoaded).IsLoaded + ) + + val S1 = ReduceMainQuest(S0, SetQuests(listOf(Q1, Q2))) + Check( + "切片 MainQuest:SetQuests 按完成次数展开并清空已领奖", + S1.Quests.size == 3 && + S1.Quests[0].completed && S1.Quests[0].completedCount == 1 && + S1.Quests[1].completed && S1.Quests[1].completedCount == 2 && + S1.Quests[2].id == 1L && !S1.Quests[2].completed && + S1.RewardedQuests.isEmpty() + ) + + val S2 = ReduceMainQuest(S1, UpdateQuests(listOf(Quest(1L, "q1-new"), Quest(3L)))) + Check( + "切片 MainQuest:UpdateQuests 按 id 就地替换、缺失追加", + S2.Quests.size == 4 && S2.Quests[2].name == "q1-new" && S2.Quests[3].id == 3L + ) + + val S3 = ReduceMainQuest(S2, AddRewardedQuest(Q1)) + Check( + "切片 MainQuest:AddRewardedQuest 追加到已领奖列表", + S3.RewardedQuests == listOf(Q1) + ) + Check( + "切片 MainQuest:SetEventPassShopItemId 写商品 id", + ReduceMainQuest(S3, SetEventPassShopItemId(77L)).EventPassShopItemId == 77L + ) + Check( + "切片 MainQuest:SetBattleUpdatedQuests 写战斗增量", + ReduceMainQuest(S3, SetBattleUpdatedQuests(listOf("x"))) + .LastBattleUpdatedQuests == listOf("x") + ) + Check("切片 MainQuest:无关动作不改状态", ReduceMainQuest(S3, AddFrags(1)) == S3) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetEventPassShopItemId(9L)) + Check( + "根:SetEventPassShopItemId 只动 mainQuest", + Root1.MainQuest.EventPassShopItemId == 9L && Root1.Icons == Root0.Icons + ) + val W = RootWiring.ByField("mainQuest")!! + Check( + "根:mainQuest 委派到 WZ / ih.bsk 且 7 个动作(含 ResetQuestsLoaded)", + W.SliceClass == "WZ" && W.Reducer == "ih.bsk" && + W.Actions.toSet() == setOf( + "ResetQuestsLoaded", "QuestsLoaded", "SetQuests", "UpdateQuests", + "AddRewardedQuest", "SetEventPassShopItemId", "SetBattleUpdatedQuests", + ) + ) + } + + // ㊷ 切片 reducer:SecuritySettings(V2 / pc.qsv,1 字段;K2 → SetProfileBlockStatus) + run { + val S0 = SecuritySettingsStateSlice() + Check("切片 SecuritySettings:默认隐藏个人资料", S0.IsProfileBlocked) + Check( + "切片 SecuritySettings:SetProfileBlockStatus 写布尔", + !ReduceSecuritySettings(S0, SetProfileBlockStatus(false)).IsProfileBlocked + ) + Check( + "切片 SecuritySettings:无关动作不改状态", + ReduceSecuritySettings(S0, AddFrags(1)) == S0 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetProfileBlockStatus(false)) + Check( + "根:SetProfileBlockStatus 只动 securitySettings", + !Root1.SecuritySettings.IsProfileBlocked && Root1.ReferralEvent == Root0.ReferralEvent + ) + val W = RootWiring.ByField("securitySettings")!! + Check( + "根:securitySettings 委派到 V2 / pc.qsv", + W.SliceClass == "V2" && W.Reducer == "pc.qsv" && + W.Actions == listOf("SetProfileBlockStatus") + ) + } + + // ㊸ 切片 reducer:ReferralEvent(pQ / lh.osk,14 字段;4 个实例臂) + run { + val S0 = ReferralEventStateSlice() + Check( + "切片 ReferralEvent:默认值完整", + S0.Referrer == null && S0.ReferrerUid == null && S0.ReferralsCount == 0 && + S0.ReferralsData.isEmpty() && S0.Code == null && S0.ReferrerPanelTitle == "" && + S0.ReferralQuestsSize == 0 && S0.HideReferrerCategory && + S0.HideReferralCategory && S0.HidedAnnounce == null + ) + val S1 = ReduceReferralEvent(S0, SetReferrer("rid", "ruid")) + Check( + "切片 ReferralEvent:SetReferrer 写前两字段", + S1.Referrer == "rid" && S1.ReferrerUid == "ruid" && S1.ReferralsCount == 0 + ) + val S2 = ReduceReferralEvent(S1, SetReferrals(5, mapOf("a" to 1))) + Check( + "切片 ReferralEvent:SetReferrals 写人数与明细", + S2.ReferralsCount == 5 && S2.ReferralsData == mapOf("a" to 1) + ) + Check( + "切片 ReferralEvent:SetPromoCode 写邀请码", + ReduceReferralEvent(S2, SetPromoCode("CODE")).Code == "CODE" + ) + val Data = ReferralEventUserModelCC( + hideReferralCategory = false, hideReferrerCategory = false, hidedAnnounce = 3L, + promoCodeDialogBackGround = "bg", promoCodeDialogImage = "img", + referralCategory = 7L, referralQuestsSize = 4, referrerCategory = 8L, + referrerPanelTitle = "title", + ) + val S3 = ReduceReferralEvent(S2, SetReferralEventData(Data)) + Check( + "切片 ReferralEvent:SetReferralEventData 写后 9 个字段", + S3.ReferrerPanelTitle == "title" && S3.PromoCodeDialogImage == "img" && + S3.PromoCodeDialogBackGround == "bg" && S3.ReferrerCategoryId == 8L && + S3.ReferralCategoryId == 7L && S3.ReferralQuestsSize == 4 && + !S3.HideReferrerCategory && !S3.HideReferralCategory && + S3.HidedAnnounce == 3L && S3.Referrer == "rid" && S3.ReferralsCount == 5 + ) + Check( + "切片 ReferralEvent:无关动作不改状态", + ReduceReferralEvent(S3, AddFrags(1)) == S3 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetPromoCode("X")) + Check( + "根:SetPromoCode 只动 referralEvent", + Root1.ReferralEvent.Code == "X" && Root1.SecuritySettings == Root0.SecuritySettings + ) + val W = RootWiring.ByField("referralEvent")!! + Check( + "根:referralEvent 委派到 pQ / lh.osk 且 4 个动作", + W.SliceClass == "pQ" && W.Reducer == "lh.osk" && + W.Actions.toSet() == setOf( + "SetReferrer", "SetReferrals", "SetPromoCode", "SetReferralEventData", + ) + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)