feat(state): 迁移 itemNotifications(复用 ItemNotificationData)
This commit is contained in:
1 parent
bb544643b5
commit
6f68afa678
8 files changed
+251
-21
No files matched your search
@@ -143,7 +143,7 @@
|
||||
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
|
||||
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
|
||||
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
|
||||
- 已迁移切片 **86/89**:
|
||||
- 已迁移切片 **87/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)
|
||||
@@ -435,12 +435,16 @@
|
||||
`IgnoreUserMessages`/`ShowUserMessages` 增删屏蔽名单(`_C` 重载时清空);
|
||||
`rN`(用户资料刷新)依赖消息内的用户对象,离线重建按 no-op;`SendMessageToServer` 的时间戳留给中间件。
|
||||
消息复用生成物 `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;
|
||||
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
|
||||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 85 个有 reducer 的切片,
|
||||
`launchParams` 与其余 **3 个字段原样透传**;
|
||||
迁移进度用 `RootReducer.Coverage`(`已迁移 86/89`)与 `RootWiring.Unmigrated` 暴露。
|
||||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **953 项全绿**,含 89 槽位完整性、85 个归约切片一次归约互不串片、未迁移字段透传,
|
||||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 86 个有 reducer 的切片,
|
||||
`launchParams` 与其余 **2 个字段原样透传**;
|
||||
迁移进度用 `RootReducer.Coverage`(`已迁移 87/89`)与 `RootWiring.Unmigrated` 暴露。
|
||||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **964 项全绿**,含 89 槽位完整性、86 个归约切片一次归约互不串片、未迁移字段透传,
|
||||
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
|
||||
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
|
||||
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-10-24T14:08:05.684Z",
|
||||
"generatedAt": "2026-10-24T14:12:51.294Z",
|
||||
"module": "module_10032.js",
|
||||
"RootState": {
|
||||
"Class": "z6",
|
||||
@@ -92,7 +92,8 @@
|
||||
"challenges",
|
||||
"clansStorage",
|
||||
"clan",
|
||||
"chatState"
|
||||
"chatState",
|
||||
"itemNotifications"
|
||||
],
|
||||
"AnonymousActions": [
|
||||
{
|
||||
@@ -1636,6 +1637,12 @@
|
||||
"Name": "SetChatConfig",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)"
|
||||
},
|
||||
{
|
||||
"Local": "S1",
|
||||
"Name": "PersistItemNotifications",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "通知列表变化后写回存储(纯 reducer 里 no-op)"
|
||||
}
|
||||
],
|
||||
"Markers": [
|
||||
@@ -3218,15 +3225,15 @@
|
||||
"Reducer": "A1().dso",
|
||||
"Delegate": "A1().dso(t, n.xpx_1)",
|
||||
"Actions": [
|
||||
"?S1",
|
||||
"AddItemNotification",
|
||||
"AddItemNotifications",
|
||||
"PersistItemNotifications",
|
||||
"RemoveItemNotification",
|
||||
"ResetSettingsFromStorage",
|
||||
"UpdateItemNotification"
|
||||
],
|
||||
"Default": "new _1()",
|
||||
"Migrated": false
|
||||
"Migrated": true
|
||||
},
|
||||
{
|
||||
"Field": "contextTutorial",
|
||||
|
||||
@@ -368,6 +368,8 @@ const AnonymousActionNames = new Map([
|
||||
['sN', 'MarkFirstChannelMessagesLoaded'],
|
||||
// YP 的 toString 名 Configure 与匹配大厅同名类冲突,改名避免字段错配。
|
||||
['YP', 'SetChatConfig'],
|
||||
// itemNotifications 切片(_1 / I1.dso):S1 没有 toString。
|
||||
['S1', 'PersistItemNotifications'],
|
||||
]);
|
||||
|
||||
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
|
||||
@@ -858,6 +860,7 @@ const Migrated = {
|
||||
clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' },
|
||||
clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' },
|
||||
chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' },
|
||||
itemNotifications: { Type: 'ItemNotificationsStateSlice', Reduce: 'ReduceItemNotifications' },
|
||||
};
|
||||
|
||||
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
|
||||
@@ -1306,6 +1309,7 @@ const AppSettingsAnonymousEffects = {
|
||||
qN: 'isNeedScroll=true',
|
||||
sN: 'isFirstChannelMessagesLoad=false',
|
||||
YP: 'config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)',
|
||||
S1: '通知列表变化后写回存储(纯 reducer 里 no-op)',
|
||||
};
|
||||
fs.writeFileSync(
|
||||
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)
|
||||
//
|
||||
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
|
||||
// 这里采用同一形状。已迁移的 86 个切片里,85 个真正调用 reducer,launchParams 只做类型收口:
|
||||
// 这里采用同一形状。已迁移的 87 个切片里,86 个真正调用 reducer,launchParams 只做类型收口:
|
||||
//
|
||||
// inviteState → ReduceInvite(aD / pD.er5)
|
||||
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
|
||||
@@ -88,9 +88,10 @@
|
||||
// clansStorage → ReduceClanStorage(lO / me.vr1,根委派 `me.vr1(t, n.rpv_1)`)
|
||||
// clan → ReduceClan(jN / we.hr0,根委派 `we.hr0(t, n.qpv_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())
|
||||
//
|
||||
// 其余 3 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
|
||||
// 其余 2 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
|
||||
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
|
||||
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
|
||||
package App.State
|
||||
@@ -132,6 +133,7 @@ import App.State.Reducers.ReduceGraphicsSettings
|
||||
import App.State.Reducers.ReduceHome
|
||||
import App.State.Reducers.ReduceIcons
|
||||
import App.State.Reducers.ReduceInvite
|
||||
import App.State.Reducers.ReduceItemNotifications
|
||||
import App.State.Reducers.ReduceKeyboardSettings
|
||||
import App.State.Reducers.ReduceLanguage
|
||||
import App.State.Reducers.ReduceLocalBattleUser
|
||||
@@ -279,6 +281,7 @@ object RootReducer {
|
||||
ClansStorage = ReduceClanStorage(State0.ClansStorage, Action),
|
||||
Clan = ReduceClan(State0.Clan, 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.IconsStateSlice
|
||||
import App.State.Reducers.InviteStateSlice
|
||||
import App.State.Reducers.ItemNotificationsStateSlice
|
||||
import App.State.Reducers.KeyboardSettingsStateSlice
|
||||
import App.State.Reducers.LobbyLoaderStateSlice
|
||||
import App.State.Reducers.LocalBattleUserStateSlice
|
||||
@@ -248,8 +249,8 @@ data class RootState(
|
||||
val TwitchState: TwitchStateSlice = TwitchStateSlice(),
|
||||
/** 原 icons(oH / vs.kru)。已迁移。 */
|
||||
val Icons: IconsStateSlice = IconsStateSlice(),
|
||||
/** 原 itemNotifications(_1 / A1().dso)。未迁移:原样透传。 */
|
||||
val ItemNotifications: Any? = null,
|
||||
/** 原 itemNotifications(_1 / A1().dso)。已迁移。 */
|
||||
val ItemNotifications: ItemNotificationsStateSlice = ItemNotificationsStateSlice(),
|
||||
/** 原 contextTutorial(Mst / Jst().tth)。已迁移。 */
|
||||
val ContextTutorial: ContextTutorialStateSlice = ContextTutorialStateSlice(),
|
||||
/** 原 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("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("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("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")),
|
||||
@@ -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", "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 }
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction /
|
||||
// Moderation / MigrationState / Register / UserReports /
|
||||
// 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 槽位委派
|
||||
//
|
||||
// 用法:$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.SetChatConfig
|
||||
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.UserStateSlice
|
||||
import App.State.Reducers.ClearHullData
|
||||
@@ -1331,7 +1339,7 @@ object StateCheck {
|
||||
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
|
||||
}
|
||||
|
||||
// ⑭ 根 reducer:89 个槽位的委派表 + 86 个已迁移切片(85 归约 + launchParams 透传)
|
||||
// ⑭ 根 reducer:89 个槽位的委派表 + 87 个已迁移切片(86 归约 + launchParams 透传)
|
||||
run {
|
||||
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
|
||||
Check(
|
||||
@@ -1395,7 +1403,7 @@ object StateCheck {
|
||||
LocalBattleW.Reducer == "(inline@localBattleUserState)"
|
||||
)
|
||||
Check(
|
||||
"根:迁移表 = 86 个已迁移字段,其余 3 个待迁",
|
||||
"根:迁移表 = 87 个已迁移字段,其余 2 个待迁",
|
||||
RootReducer.Migrated ==
|
||||
setOf(
|
||||
"inviteState", "loginByPassword", "twitchState", "home",
|
||||
@@ -1419,9 +1427,9 @@ object StateCheck {
|
||||
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
|
||||
"graphicsSettings", "controlSettings", "battleSettings", "contractState",
|
||||
"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 里多个切片各归约自己的动作,互不串片
|
||||
@@ -1477,7 +1485,7 @@ object StateCheck {
|
||||
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
|
||||
)
|
||||
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) })
|
||||
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 }
|
||||
println("TankiOffline 状态层自检:${Results.size} 项")
|
||||
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)
|
||||
|
||||
Reference in new issue
Block a user