feat(state): 迁移 itemNotifications(复用 ItemNotificationData)

This commit is contained in:
TermiNexus committed 2026-10-24 22:14:50 +08:00
1 parent bb544643b5
commit 6f68afa678
8 files changed
+251 -21

No files matched your search

+9 -5
View File
@@ -143,7 +143,7 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。 `names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。 - 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。 - 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **86/89**: - 已迁移切片 **87/89**:
- `inviteState` → `aD` / `pD.er5`(`InviteReducer.kt`) - `inviteState` → `aD` / `pD.er5`(`InviteReducer.kt`)
- `loginByPassword` → `gD` / `MD.cr6`(`LoginReducer.kt`,含 `SetLogin`/`SetPassword` 把 status 打回 `NONE` 的原语义) - `loginByPassword` → `gD` / `MD.cr6`(`LoginReducer.kt`,含 `SetLogin`/`SetPassword` 把 status 打回 `NONE` 的原语义)
- `twitchState` → `g9` / `y9.gta`(`TwitchReducer.kt`;`SetAccount` 是 Twitch 账号,不是 UID) - `twitchState` → `g9` / `y9.gta`(`TwitchReducer.kt`;`SetAccount` 是 Twitch 账号,不是 UID)
@@ -435,12 +435,16 @@
`IgnoreUserMessages`/`ShowUserMessages` 增删屏蔽名单(`_C` 重载时清空); `IgnoreUserMessages`/`ShowUserMessages` 增删屏蔽名单(`_C` 重载时清空);
`rN`(用户资料刷新)依赖消息内的用户对象,离线重建按 no-op;`SendMessageToServer` 的时间戳留给中间件。 `rN`(用户资料刷新)依赖消息内的用户对象,离线重建按 no-op;`SendMessageToServer` 的时间戳留给中间件。
消息复用生成物 `ChatMessage`。 消息复用生成物 `ChatMessage`。
- `itemNotifications` → `_1` / `A1().dso`(`ItemNotificationsReducer.kt`):1 字段,6 个分支。
`AddItemNotification(s)` 加入(构造 `ItemNotificationData`);`RemoveItemNotification` 先按有效期过滤,
再删掉匹配谓词的通知;`UpdateItemNotification` 对匹配项应用 handler;
`S1` 只做持久化(no-op);`_C` 重载时只保留有效期内的通知。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。 `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 85 个有 reducer 的切片, - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 86 个有 reducer 的切片,
`launchParams` 与其余 **3 个字段原样透传**; `launchParams` 与其余 **2 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 86/89`)与 `RootWiring.Unmigrated` 暴露。 迁移进度用 `RootReducer.Coverage`(`已迁移 87/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **953 项全绿**,含 89 槽位完整性、85 个归约切片一次归约互不串片、未迁移字段透传, - 自检:`TankiOffline/tools/RunStateTest.ps1` **964 项全绿**,含 89 槽位完整性、86 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); 已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
+11 -4
View File
@@ -1,5 +1,5 @@
{ {
"generatedAt": "2026-10-24T14:08:05.684Z", "generatedAt": "2026-10-24T14:12:51.294Z",
"module": "module_10032.js", "module": "module_10032.js",
"RootState": { "RootState": {
"Class": "z6", "Class": "z6",
@@ -92,7 +92,8 @@
"challenges", "challenges",
"clansStorage", "clansStorage",
"clan", "clan",
"chatState" "chatState",
"itemNotifications"
], ],
"AnonymousActions": [ "AnonymousActions": [
{ {
@@ -1636,6 +1637,12 @@
"Name": "SetChatConfig", "Name": "SetChatConfig",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)" "Effect": "config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)"
},
{
"Local": "S1",
"Name": "PersistItemNotifications",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "通知列表变化后写回存储(纯 reducer 里 no-op)"
} }
], ],
"Markers": [ "Markers": [
@@ -3218,15 +3225,15 @@
"Reducer": "A1().dso", "Reducer": "A1().dso",
"Delegate": "A1().dso(t, n.xpx_1)", "Delegate": "A1().dso(t, n.xpx_1)",
"Actions": [ "Actions": [
"?S1",
"AddItemNotification", "AddItemNotification",
"AddItemNotifications", "AddItemNotifications",
"PersistItemNotifications",
"RemoveItemNotification", "RemoveItemNotification",
"ResetSettingsFromStorage", "ResetSettingsFromStorage",
"UpdateItemNotification" "UpdateItemNotification"
], ],
"Default": "new _1()", "Default": "new _1()",
"Migrated": false "Migrated": true
}, },
{ {
"Field": "contextTutorial", "Field": "contextTutorial",
+4
View File
@@ -368,6 +368,8 @@ const AnonymousActionNames = new Map([
['sN', 'MarkFirstChannelMessagesLoaded'], ['sN', 'MarkFirstChannelMessagesLoaded'],
// YP 的 toString 名 Configure 与匹配大厅同名类冲突,改名避免字段错配。 // YP 的 toString 名 Configure 与匹配大厅同名类冲突,改名避免字段错配。
['YP', 'SetChatConfig'], ['YP', 'SetChatConfig'],
// itemNotifications 切片(_1 / I1.dso):S1 没有 toString。
['S1', 'PersistItemNotifications'],
]); ]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -858,6 +860,7 @@ const Migrated = {
clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' }, clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' },
clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' }, clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' },
chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' }, chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' },
itemNotifications: { Type: 'ItemNotificationsStateSlice', Reduce: 'ReduceItemNotifications' },
}; };
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -1306,6 +1309,7 @@ const AppSettingsAnonymousEffects = {
qN: 'isNeedScroll=true', qN: 'isNeedScroll=true',
sN: 'isFirstChannelMessagesLoad=false', sN: 'isFirstChannelMessagesLoad=false',
YP: 'config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)', YP: 'config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)',
S1: '通知列表变化后写回存储(纯 reducer 里 no-op)',
}; };
fs.writeFileSync( fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'), path.join(P, 'names', 'root_wiring.json'),
@@ -0,0 +1,126 @@
// ItemNotificationsReducer.kt —— 物品「新」标记切片(对应 module_10032 的 _1 状态 + I1.dso)
//
// 证据链:
// 状态类 _1:1 个字段,toString 名 "ItemNotificationsState(notifications=…)"。
// 默认:notifications = emptySet()。
// 根委派:`A1().dso(t, n.xpx_1)`。
// reducer:
// q1(AddItemNotifications) → 批量加入(逐条去重)
// y1(AddItemNotification) → 用 6 个字段构造 `ItemNotificationData` 后加入
// $1(RemoveItemNotification) → 先按有效期过滤,再删掉匹配谓词的通知
// (section/itemId/type/parentId/excludeType;ssn_1 = 跳过有效期检查)
// j1(UpdateItemNotification) → 保留不匹配项,对匹配项应用 updateHandler
// S1(无 toString) → 只做持久化标记(纯 reducer 里 no-op)
// _C(ResetSettingsFromStorage)→ 从存储重载并过滤过期(Kotlin 侧按「只保留有效期内的」收口)
//
// 有效期判定 `msm`:`(startTime == null || startTime <= now) && (endTime == null || endTime > now)`。
// 通知模型直接复用生成物 `ItemNotificationData`(section / itemId / type / parentId / startTime / endTime)。
package App.State.Reducers
import App.State.AddItemNotification
import App.State.AddItemNotifications
import App.State.ItemNotificationData
import App.State.RemoveItemNotification
import App.State.UpdateItemNotification
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `S1`:通知列表变化后写回存储(纯 reducer 里 no-op)。 */
object PersistItemNotifications
/* ------------------------------------------------------------------ 状态 */
data class ItemNotificationsStateSlice(
val Notifications: Set<Any?> = emptySet(),
)
/* ------------------------------------------------------------------ reducer */
private fun Now(): Double = (js("Date.now()") as Number).toDouble()
private fun TimeOf(Value: Any?): Double? = (Value as? Number)?.toDouble()
/** 原 `msm`:有效期判定。 */
private fun InPeriod(Item: ItemNotificationData): Boolean {
val Now = Now()
val Start = TimeOf(Item.startTime)
val End = TimeOf(Item.endTime)
return (Start == null || Start <= Now) && (End == null || End > Now)
}
/** 原 `tsm`:删除 / 更新谓词(忽略最后一个 `skipPeriodCheck` 参数)。 */
private fun Matches(
Item: ItemNotificationData,
Section: Any?,
ItemId: Any?,
Type: Any?,
ParentId: Any?,
ExcludeType: Any?,
): Boolean {
val WantedId = (ItemId as? Number)?.toLong() ?: ItemId
return (Section == null || Item.section == Section) &&
(ItemId == null || Item.itemId == WantedId) &&
(Type == null || Item.type == Type) &&
(ParentId == null || Item.parentId == ParentId) &&
(ExcludeType == null || Item.type != ExcludeType)
}
private fun AsItem(Value: Any?): ItemNotificationData? = Value as? ItemNotificationData
fun ReduceItemNotifications(
State0: ItemNotificationsStateSlice,
Action: Any,
): ItemNotificationsStateSlice = when (Action) {
is AddItemNotifications ->
State0.copy(Notifications = State0.Notifications + Action.data)
is AddItemNotification -> State0.copy(
Notifications = State0.Notifications + ItemNotificationData(
section = Action.section,
itemId = Action.itemId,
type = Action.type,
parentId = Action.parentId,
startTime = Action.startTime,
endTime = Action.endTime,
),
)
is RemoveItemNotification -> State0.copy(
Notifications = State0.Notifications.filterNot {
val Item = AsItem(it) ?: return@filterNot false
val Skipped = Action.skipPeriodCheck == true
(Skipped || InPeriod(Item)) &&
Matches(
Item,
Action.section,
Action.itemId,
Action.type,
Action.parentId,
Action.excludeType,
)
}.toSet(),
)
is UpdateItemNotification -> State0.copy(
Notifications = State0.Notifications.map {
val Item = AsItem(it) ?: return@map it
if (!Matches(
Item,
Action.section,
Action.itemId,
Action.type,
Action.parentId,
Action.excludeType,
)
) {
it
} else {
@Suppress("UNCHECKED_CAST")
val Transform = Action.updateHandler as? Function1<ItemNotificationData, ItemNotificationData>
Transform?.invoke(Item) ?: it
}
}.toSet(),
)
is PersistItemNotifications -> State0
is ResetSettingsFromStorage -> State0.copy(
Notifications = State0.Notifications.filter { AsItem(it)?.let(::InPeriod) ?: true }.toSet(),
)
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6) // RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
// //
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。 // 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 86 个切片里,85 个真正调用 reducer,launchParams 只做类型收口: // 这里采用同一形状。已迁移的 87 个切片里,86 个真正调用 reducer,launchParams 只做类型收口:
// //
// inviteState → ReduceInvite(aD / pD.er5) // inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -88,9 +88,10 @@
// clansStorage → ReduceClanStorage(lO / me.vr1,根委派 `me.vr1(t, n.rpv_1)`) // clansStorage → ReduceClanStorage(lO / me.vr1,根委派 `me.vr1(t, n.rpv_1)`)
// clan → ReduceClan(jN / we.hr0,根委派 `we.hr0(t, n.qpv_1)`) // clan → ReduceClan(jN / we.hr0,根委派 `we.hr0(t, n.qpv_1)`)
// chatState → ReduceChat(LP / $r.wqv,根委派 `$r.wqv(t, n.opv_1)`) // chatState → ReduceChat(LP / $r.wqv,根委派 `$r.wqv(t, n.opv_1)`)
// itemNotifications → ReduceItemNotifications(_1 / A1().dso,根委派 `A1().dso(t, n.xpx_1)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
// //
// 其余 3 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 其余 2 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State package App.State
@@ -132,6 +133,7 @@ import App.State.Reducers.ReduceGraphicsSettings
import App.State.Reducers.ReduceHome import App.State.Reducers.ReduceHome
import App.State.Reducers.ReduceIcons import App.State.Reducers.ReduceIcons
import App.State.Reducers.ReduceInvite import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceItemNotifications
import App.State.Reducers.ReduceKeyboardSettings import App.State.Reducers.ReduceKeyboardSettings
import App.State.Reducers.ReduceLanguage import App.State.Reducers.ReduceLanguage
import App.State.Reducers.ReduceLocalBattleUser import App.State.Reducers.ReduceLocalBattleUser
@@ -279,6 +281,7 @@ object RootReducer {
ClansStorage = ReduceClanStorage(State0.ClansStorage, Action), ClansStorage = ReduceClanStorage(State0.ClansStorage, Action),
Clan = ReduceClan(State0.Clan, Action), Clan = ReduceClan(State0.Clan, Action),
ChatState = ReduceChat(State0.ChatState, Action), ChatState = ReduceChat(State0.ChatState, Action),
ItemNotifications = ReduceItemNotifications(State0.ItemNotifications, Action),
) )
} }
@@ -42,6 +42,7 @@ import App.State.Reducers.GraphicsSettingsStateSlice
import App.State.Reducers.HomeStateSlice import App.State.Reducers.HomeStateSlice
import App.State.Reducers.IconsStateSlice import App.State.Reducers.IconsStateSlice
import App.State.Reducers.InviteStateSlice import App.State.Reducers.InviteStateSlice
import App.State.Reducers.ItemNotificationsStateSlice
import App.State.Reducers.KeyboardSettingsStateSlice import App.State.Reducers.KeyboardSettingsStateSlice
import App.State.Reducers.LobbyLoaderStateSlice import App.State.Reducers.LobbyLoaderStateSlice
import App.State.Reducers.LocalBattleUserStateSlice import App.State.Reducers.LocalBattleUserStateSlice
@@ -248,8 +249,8 @@ data class RootState(
val TwitchState: TwitchStateSlice = TwitchStateSlice(), val TwitchState: TwitchStateSlice = TwitchStateSlice(),
/** 原 icons(oH / vs.kru)。已迁移。 */ /** 原 icons(oH / vs.kru)。已迁移。 */
val Icons: IconsStateSlice = IconsStateSlice(), val Icons: IconsStateSlice = IconsStateSlice(),
/** 原 itemNotifications(_1 / A1().dso)。未迁移:原样透传。 */ /** 原 itemNotifications(_1 / A1().dso)。已迁移。 */
val ItemNotifications: Any? = null, val ItemNotifications: ItemNotificationsStateSlice = ItemNotificationsStateSlice(),
/** 原 contextTutorial(Mst / Jst().tth)。已迁移。 */ /** 原 contextTutorial(Mst / Jst().tth)。已迁移。 */
val ContextTutorial: ContextTutorialStateSlice = ContextTutorialStateSlice(), val ContextTutorial: ContextTutorialStateSlice = ContextTutorialStateSlice(),
/** 原 suppliesTutorial(ust / Uw.uth)。已迁移。 */ /** 原 suppliesTutorial(ust / Uw.uth)。已迁移。 */
@@ -91,7 +91,7 @@ object RootWiring {
SliceWiring("rewardedVideosState", "upx_1", "JQ", "xh.zsl", "xh.zsl(t, n.upx_1)", listOf("SetDisplayState")), SliceWiring("rewardedVideosState", "upx_1", "JQ", "xh.zsl", "xh.zsl(t, n.upx_1)", listOf("SetDisplayState")),
SliceWiring("twitchState", "vpx_1", "g9", "uf.gta", "uf.gta(t, n.vpx_1)", listOf("SetAccount", "SetDaysForNextLinking", "SetTwitchEnabled")), SliceWiring("twitchState", "vpx_1", "g9", "uf.gta", "uf.gta(t, n.vpx_1)", listOf("SetAccount", "SetDaysForNextLinking", "SetTwitchEnabled")),
SliceWiring("icons", "wpx_1", "oH", "vs.kru", "vs.kru(t, n.wpx_1)", listOf("SetIcons")), SliceWiring("icons", "wpx_1", "oH", "vs.kru", "vs.kru(t, n.wpx_1)", listOf("SetIcons")),
SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("?S1", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", "ResetSettingsFromStorage", "UpdateItemNotification")), SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("AddItemNotification", "AddItemNotifications", "PersistItemNotifications", "RemoveItemNotification", "ResetSettingsFromStorage", "UpdateItemNotification")),
SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")), SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")),
SliceWiring("suppliesTutorial", "zpx_1", "ust", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")), SliceWiring("suppliesTutorial", "zpx_1", "ust", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")),
SliceWiring("questCategories", "apy_1", "zZ", "th.lsh", "th.lsh(t, n.apy_1)", listOf("RemoveQuestCategory", "RequestQuestCategoriesStatistic", "SetQuestCategories", "SetQuestCategoriesStatistic")), SliceWiring("questCategories", "apy_1", "zZ", "th.lsh", "th.lsh(t, n.apy_1)", listOf("RemoveQuestCategory", "RequestQuestCategoriesStatistic", "SetQuestCategories", "SetQuestCategoriesStatistic")),
@@ -106,7 +106,7 @@ object RootWiring {
) )
/** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ /** 已迁移(强类型;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", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState") 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", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated } val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -19,7 +19,7 @@
// System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction / // System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction /
// Moderation / MigrationState / Register / UserReports / // Moderation / MigrationState / Register / UserReports /
// Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename / // Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename /
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState; // BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
// //
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -904,6 +904,14 @@ import App.State.Reducers.RequireHideChatKeyboard
import App.State.Reducers.ResetChat import App.State.Reducers.ResetChat
import App.State.Reducers.SetChatConfig import App.State.Reducers.SetChatConfig
import App.State.Reducers.ShowChatKeyboard import App.State.Reducers.ShowChatKeyboard
import App.State.AddItemNotification
import App.State.AddItemNotifications
import App.State.ItemNotificationData
import App.State.RemoveItemNotification
import App.State.UpdateItemNotification
import App.State.Reducers.ItemNotificationsStateSlice
import App.State.Reducers.PersistItemNotifications
import App.State.Reducers.ReduceItemNotifications
import App.State.Reducers.UserRankBounds import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData import App.State.Reducers.ClearHullData
@@ -1331,7 +1339,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
} }
// ⑭ 根 reducer:89 个槽位的委派表 + 86 个已迁移切片(85 归约 + launchParams 透传) // ⑭ 根 reducer:89 个槽位的委派表 + 87 个已迁移切片(86 归约 + launchParams 透传)
run { run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check( Check(
@@ -1395,7 +1403,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)" LocalBattleW.Reducer == "(inline@localBattleUserState)"
) )
Check( Check(
"根:迁移表 = 86 个已迁移字段,其余 3 个待迁", "根:迁移表 = 87 个已迁移字段,其余 2 个待迁",
RootReducer.Migrated == RootReducer.Migrated ==
setOf( setOf(
"inviteState", "loginByPassword", "twitchState", "home", "inviteState", "loginByPassword", "twitchState", "home",
@@ -1419,9 +1427,9 @@ object StateCheck {
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
"graphicsSettings", "controlSettings", "battleSettings", "contractState", "graphicsSettings", "controlSettings", "battleSettings", "contractState",
"tankBuilderState", "battleMessage", "maps", "battleChat", "tankBuilderState", "battleMessage", "maps", "battleChat",
"remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications",
) && ) &&
RootWiring.Unmigrated.size == 3 RootWiring.Unmigrated.size == 2
) )
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -1477,7 +1485,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
) )
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 86/89") Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 87/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN")) Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -7346,6 +7354,83 @@ object StateCheck {
) )
} }
// 97 切片 reducer:ItemNotifications(_1 / A1().dso,1 字段;6 个动作)
run {
fun Item(Id: Long, End: Any? = null): ItemNotificationData = ItemNotificationData(
section = "shop", itemId = Id, type = "new", parentId = null,
startTime = null, endTime = End,
)
val S0 = ItemNotificationsStateSlice()
Check("切片 ItemNotifications:默认空集合", S0.Notifications.isEmpty())
val S1 = ReduceItemNotifications(S0, AddItemNotification("shop", 1L, "new", null, null, null))
Check(
"切片 ItemNotifications:AddItemNotification 构造并入集合",
S1.Notifications.size == 1 &&
(S1.Notifications.first() as ItemNotificationData).itemId == 1L
)
val S2 = ReduceItemNotifications(S1, AddItemNotifications(listOf(Item(2L), Item(3L))))
Check(
"切片 ItemNotifications:AddItemNotifications 批量加入",
(S2.Notifications.map { (it as ItemNotificationData).itemId }).toSet() ==
setOf(1L, 2L, 3L)
)
Check(
"切片 ItemNotifications:RemoveItemNotification 按 section+itemId 删",
ReduceItemNotifications(
S2,
RemoveItemNotification("shop", 2L, null, null, null, true),
).Notifications.size == 2
)
Check(
"切片 ItemNotifications:excludeType 命中时保留",
ReduceItemNotifications(
S2,
RemoveItemNotification("shop", null, null, null, "new", true),
).Notifications.size == 3
)
Check(
"切片 ItemNotifications:UpdateItemNotification 对匹配项应用 handler",
ReduceItemNotifications(
S2,
UpdateItemNotification("shop", 1L, null, null, null, { Item: ItemNotificationData ->
Item.copy(section = "updated")
}),
).let { Updated ->
(Updated.Notifications.first { (it as ItemNotificationData).itemId == 1L }
as ItemNotificationData).section == "updated"
}
)
Check(
"切片 ItemNotifications:PersistItemNotifications 是 no-op",
ReduceItemNotifications(S2, PersistItemNotifications) == S2
)
Check(
"切片 ItemNotifications:过期项在重载时被过滤",
ReduceItemNotifications(
ReduceItemNotifications(S0, AddItemNotification("shop", 9L, "new", null, null, 1L)),
ResetSettingsFromStorage,
).Notifications.isEmpty()
)
Check(
"切片 ItemNotifications:无关动作不改状态",
ReduceItemNotifications(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddItemNotification("shop", 1L, "new", null, null, null))
Check(
"根:AddItemNotification 只动 itemNotifications",
Root1.ItemNotifications.Notifications.size == 1 &&
Root1.ChatState == Root0.ChatState
)
val W = RootWiring.ByField("itemNotifications")!!
Check(
"根:itemNotifications 委派到 _1 / A1().dso 且 6 个动作",
W.SliceClass == "_1" && W.Reducer == "A1().dso" && W.Actions.size == 6
)
}
val Failed = Results.filter { !it.second } val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项") println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name) for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)