feat(state): 迁移 icons/questCategories/mainQuest/securitySettings/referralEvent

This commit is contained in:
TermiNexus committed 2026-10-24 14:12:19 +08:00
1 parent 49e8c1efc4
commit 6d2487d9ea
12 files changed
+664 -34

No files matched your search

+22 -6
View File
@@ -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<IconData>` 收成 `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<String, String>`,不新增 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@<field>)` 并从函数体抓到顶层动作名(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(输入)
+19 -8
View File
@@ -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",
+18
View File
@@ -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: [
{
@@ -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<String, Any?> = 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
}
@@ -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<MainQuestInfo> = emptyList(),
val RewardedQuests: List<MainQuestInfo> = emptyList(),
val EventPassShopItemId: Long? = null,
val LastBattleUpdatedQuests: List<Any?> = 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<MainQuestInfo>): List<MainQuestInfo> {
val Result = mutableListOf<MainQuestInfo>()
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<MainQuestInfo>),
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<Any?>,
)
else -> State0
}
@@ -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<QuestCategoryData> = emptyList(),
val Statistic: Map<Long, Any?> = 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
}
@@ -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<Any?, Any?> = 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<Any?, Any?>,
)
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
}
@@ -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
}
@@ -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),
)
}
@@ -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(),
)
@@ -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<String> = 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<String> = 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<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -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<String, Any?>("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<Any?>("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<Any?, Any?>("a" to 1)))
Check(
"切片 ReferralEvent:SetReferrals 写人数与明细",
S2.ReferralsCount == 5 && S2.ReferralsData == mapOf<Any?, Any?>("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)