feat(state): 迁移 chatState(复用 ChatMessage 模型)

This commit is contained in:
TermiNexus committed 2026-10-24 22:09:49 +08:00
1 parent 2f38609a98
commit bb544643b5
8 files changed
+399 -32

No files matched your search

+11 -5
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` 动作名。
- 已迁移切片 **85/89**:
- 已迁移切片 **86/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)
@@ -429,12 +429,18 @@
成员 / 待审用户 / 人员 / 外战队 / 新申请各有增删两臂;校验状态 4 个 setter + 2 个复位;
`ClanInfoLoaded` 写 6 个身份字段(复用 `ClanInfo`);`UpdateHasClan(false)` 整片复位;
通知计数 3 个直接写 + 2 个已读 + 计数 ±1;其余为中间态三元组、封禁、弹窗与改名价格。
- `chatState` → `LP` / `$r.wqv`(`ChatStateReducer.kt`):16 字段,24 个分支。
`SaveChatMessage` 写消息并优先选中消息里的目标用户;`SetMessages` 整体替换频道表;
`CleanUsersMessage` 按 uid 过滤每个频道;键盘 / 滚动 / 右键菜单各有开关;
`IgnoreUserMessages`/`ShowUserMessages` 增删屏蔽名单(`_C` 重载时清空);
`rN`(用户资料刷新)依赖消息内的用户对象,离线重建按 no-op;`SendMessageToServer` 的时间戳留给中间件。
消息复用生成物 `ChatMessage`。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 84 个有 reducer 的切片,
`launchParams` 与其余 **4 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 85/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **939 项全绿**,含 89 槽位完整性、84 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 85 个有 reducer 的切片,
`launchParams` 与其余 **3 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 86/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **953 项全绿**,含 89 槽位完整性、85 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
+88 -15
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-24T14:00:39.799Z",
"generatedAt": "2026-10-24T14:08:05.684Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -91,7 +91,8 @@
"battleUsers",
"challenges",
"clansStorage",
"clan"
"clan",
"chatState"
],
"AnonymousActions": [
{
@@ -1563,6 +1564,78 @@
"Name": "DecrementClanNotifications",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "notificationsCount - 1,下限 0"
},
{
"Local": "XP",
"Name": "OpenChat",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "open=true(`n.cqu(!0)`)"
},
{
"Local": "QP",
"Name": "CloseChat",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "open=false(`n.cqu(!1)`)"
},
{
"Local": "nN",
"Name": "ClearChatContextMenu",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "contextMenuData=null(`n.cqu(cd, cd, null)`)"
},
{
"Local": "lN",
"Name": "ResetChat",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "new LP() 整片复位"
},
{
"Local": "fN",
"Name": "ShowChatKeyboard",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isShowKeyboard=true、isNeedToHideKeyboard=false"
},
{
"Local": "vN",
"Name": "HideChatKeyboard",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isShowKeyboard=false"
},
{
"Local": "pN",
"Name": "RequireHideChatKeyboard",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isNeedToHideKeyboard=true"
},
{
"Local": "wN",
"Name": "CancelHideChatKeyboard",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isNeedToHideKeyboard=false"
},
{
"Local": "bN",
"Name": "DisableChatScroll",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isNeedScroll=false"
},
{
"Local": "qN",
"Name": "EnableChatScroll",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isNeedScroll=true"
},
{
"Local": "sN",
"Name": "MarkFirstChannelMessagesLoaded",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isFirstChannelMessagesLoad=false"
},
{
"Local": "YP",
"Name": "SetChatConfig",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)"
}
],
"Markers": [
@@ -2006,33 +2079,33 @@
"Reducer": "$r.wqv",
"Delegate": "$r.wqv(t, n.opv_1)",
"Actions": [
"?QP",
"?XP",
"?bN",
"?fN",
"?lN",
"?nN",
"?pN",
"?qN",
"?sN",
"?vN",
"?wN",
"CancelHideChatKeyboard",
"ChangeChannel",
"CleanUsersMessage",
"Configure",
"ClearChatContextMenu",
"CloseChat",
"DisableChatScroll",
"EnableChatScroll",
"HideChatKeyboard",
"IgnoreUserMessages",
"MarkFirstChannelMessagesLoaded",
"OpenChat",
"RequireHideChatKeyboard",
"ResetChat",
"ResetSettingsFromStorage",
"SaveChatMessage",
"SendMessageToServer",
"SetChatConfig",
"SetMessages",
"SetSelectedUid",
"ShowChatKeyboard",
"ShowContextMenu",
"ShowUserMessages",
"UpdateTypingSpeedAntifloodParams",
"UpdateUsers"
],
"Default": "new LP()",
"Migrated": false
"Migrated": true
},
{
"Field": "clanChatState",
+27
View File
@@ -354,6 +354,20 @@ const AnonymousActionNames = new Map([
['VM', 'SetClanNotificationsCount'],
['KM', 'IncrementClanNotifications'],
['YM', 'DecrementClanNotifications'],
// chatState 切片(LP / $r.wqv):11 个类没有 toString(hN 有名字,不需要)。
['XP', 'OpenChat'],
['QP', 'CloseChat'],
['nN', 'ClearChatContextMenu'],
['lN', 'ResetChat'],
['fN', 'ShowChatKeyboard'],
['vN', 'HideChatKeyboard'],
['pN', 'RequireHideChatKeyboard'],
['wN', 'CancelHideChatKeyboard'],
['bN', 'DisableChatScroll'],
['qN', 'EnableChatScroll'],
['sN', 'MarkFirstChannelMessagesLoaded'],
// YP 的 toString 名 Configure 与匹配大厅同名类冲突,改名避免字段错配。
['YP', 'SetChatConfig'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -843,6 +857,7 @@ const Migrated = {
challenges: { Type: 'ChallengesStateSlice', Reduce: 'ReduceChallenges' },
clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' },
clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' },
chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -1279,6 +1294,18 @@ const AppSettingsAnonymousEffects = {
VM: 'notificationsCount(`t.ir0_1`)',
KM: 'notificationsCount + 1(`(n.bqx_1 + 1) | 0`)',
YM: 'notificationsCount - 1,下限 0',
XP: 'open=true(`n.cqu(!0)`)',
QP: 'open=false(`n.cqu(!1)`)',
nN: 'contextMenuData=null(`n.cqu(cd, cd, null)`)',
lN: 'new LP() 整片复位',
fN: 'isShowKeyboard=true、isNeedToHideKeyboard=false',
vN: 'isShowKeyboard=false',
pN: 'isNeedToHideKeyboard=true',
wN: 'isNeedToHideKeyboard=false',
bN: 'isNeedScroll=false',
qN: 'isNeedScroll=true',
sN: 'isFirstChannelMessagesLoad=false',
YP: 'config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
@@ -0,0 +1,139 @@
// ChatStateReducer.kt —— 大厅聊天切片(对应 module_10032 的 LP 状态 + yN.wqv)
//
// 证据链:
// 状态类 LP:16 个字段,toString 名 "ChatState(…)";copy 是 cqu(16 字段 + 回调)。
// 根委派:`$r.wqv(t, n.opv_1)`。
// reducer(24 个实例臂):
// YP(Configure) → config
// XP / QP(无 toString) → open = true / false
// aN(ChangeChannel) → currentChannel
// oN(SaveChatMessage) → chatMessage + selectedUid(优先消息里的目标用户)
// uN(SetMessages) → messagesByChannel
// iN(CleanUsersMessage) → 从所有频道里删掉该用户的消息
// rN(UpdateUsers) → 用户资料批量刷新(依赖消息内的用户对象,离线重建按 no-op)
// tN / nN(ShowContextMenu / 无 toString)→ contextMenuData / null
// lN(无 toString) → `new LP()` 整片复位
// fN / vN(无 toString) → 显示 / 收起键盘
// pN / wN(无 toString) → 需要隐藏键盘 / 取消需要
// bN / qN(无 toString) → 滚动位 false / true
// hN(SendMessageToServer) → 需要隐藏键盘 + 需要滚动(时间戳由中间件补)
// dN(UpdateTypingSpeedAntifloodParams)→ symbolCost / enterCost
// gN(SetSelectedUid) → selectedUid(转小写)
// sN(无 toString) → isFirstChannelMessagesLoad=false
// mN / kN(IgnoreUserMessages / ShowUserMessages)→ ignoredUserIds ±
// _C(ResetSettingsFromStorage)→ 从存储重载屏蔽名单(离线重建按清空收口)
//
// 消息模型复用生成物 `ChatMessage`(channel / id / messageType / sourceUser / targetUser / text …)。
package App.State.Reducers
import App.State.ChangeChannel
import App.State.ChatMessage
import App.State.CleanUsersMessage
import App.State.IgnoreUserMessages
import App.State.SaveChatMessage
import App.State.SendMessageToServer
import App.State.SetMessages
import App.State.SetSelectedUid
import App.State.ShowContextMenu
import App.State.ShowUserMessages
import App.State.UpdateTypingSpeedAntifloodParams
import App.State.UpdateUsers
/* ------------------------------------------------------------------ 匿名动作 */
object OpenChat
object CloseChat
object ClearChatContextMenu
object ResetChat
object ShowChatKeyboard
object HideChatKeyboard
object RequireHideChatKeyboard
object CancelHideChatKeyboard
object DisableChatScroll
object EnableChatScroll
object MarkFirstChannelMessagesLoaded
/* ------------------------------------------------------------------ 状态 */
data class ChatStateSlice(
val Open: Boolean = false,
val CurrentChannel: Any? = null,
val ContextMenuData: Any? = null,
val ChatMessage: Any? = null,
val SelectedUid: String = "",
val Config: Any? = null,
val MessagesByChannel: Map<Any?, Any?> = emptyMap(),
val IsShowKeyboard: Boolean = false,
val IsNeedToHideKeyboard: Boolean = false,
val IsNeedScroll: Boolean = false,
val HaveNewDirectMessages: Boolean = false,
val SymbolCost: Any? = 0,
val EnterCost: Any? = 0,
val LastMessageTime: Any? = null,
val IsFirstChannelMessagesLoad: Boolean = true,
val IgnoredUserIds: Set<Any?> = emptySet(),
)
/* ------------------------------------------------------------------ reducer */
/** 原 `YP`(原名 `Configure`,与匹配大厅的同名类冲突):写聊天配置。 */
data class SetChatConfig(val config: Any?)
/** 原 `iN`:按 uid 从每个频道里过滤消息(sourceUser / targetUser 命中即删)。 */
private fun CleanByUid(
Channels: Map<Any?, Any?>,
Uid: Any?,
): Map<Any?, Any?> = Channels.mapValues { (_, Messages) ->
(Messages as? List<Any?>)?.filterNot {
val Message = it as? ChatMessage
Message != null && (Message.sourceUser == Uid || Message.targetUser == Uid)
} ?: Messages
}
fun ReduceChat(
State0: ChatStateSlice,
Action: Any,
): ChatStateSlice = when (Action) {
is SetChatConfig -> State0.copy(Config = Action.config)
is OpenChat -> State0.copy(Open = true)
is CloseChat -> State0.copy(Open = false)
is ChangeChannel -> State0.copy(CurrentChannel = Action.channel)
is SaveChatMessage -> {
val Message = Action.message as? ChatMessage
State0.copy(
ChatMessage = Action.message,
SelectedUid = Message?.targetUser ?: State0.SelectedUid,
)
}
is SetMessages -> State0.copy(
MessagesByChannel = (Action.messages as? Map<Any?, Any?>) ?: emptyMap(),
)
is CleanUsersMessage -> State0.copy(
MessagesByChannel = CleanByUid(State0.MessagesByChannel, Action.uid),
)
is UpdateUsers -> State0
is ShowContextMenu -> State0.copy(ContextMenuData = Action.contextMenuData)
is ClearChatContextMenu -> State0.copy(ContextMenuData = null)
is ResetChat -> ChatStateSlice()
is ShowChatKeyboard -> State0.copy(IsShowKeyboard = true, IsNeedToHideKeyboard = false)
is HideChatKeyboard -> State0.copy(IsShowKeyboard = false)
is RequireHideChatKeyboard -> State0.copy(IsNeedToHideKeyboard = true)
is CancelHideChatKeyboard -> State0.copy(IsNeedToHideKeyboard = false)
is DisableChatScroll -> State0.copy(IsNeedScroll = false)
is EnableChatScroll -> State0.copy(IsNeedScroll = true)
// 原分支把 lastMessageTime 写成「当前时间」;纯 reducer 没有时钟,时间戳留给中间件。
is SendMessageToServer -> State0.copy(
IsNeedToHideKeyboard = true,
IsNeedScroll = true,
)
is UpdateTypingSpeedAntifloodParams -> State0.copy(
SymbolCost = Action.symbolCost,
EnterCost = Action.enterCost,
)
is SetSelectedUid -> State0.copy(SelectedUid = Action.selectedUid.toString().lowercase())
is MarkFirstChannelMessagesLoaded -> State0.copy(IsFirstChannelMessagesLoad = false)
is IgnoreUserMessages -> State0.copy(IgnoredUserIds = State0.IgnoredUserIds + Action.userId)
is ShowUserMessages -> State0.copy(IgnoredUserIds = State0.IgnoredUserIds - Action.userId)
is ResetSettingsFromStorage -> State0.copy(IgnoredUserIds = emptySet())
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 85 个切片里,84 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 86 个切片里,85 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -87,9 +87,10 @@
// challenges → ReduceChallenges(CZ / nh.hsj,根委派 `nh.hsj(t, n.fpv_1)`)
// 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)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 4 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 3 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -110,6 +111,7 @@ import App.State.Reducers.ReduceBattleUsers
import App.State.Reducers.ReduceBattleView
import App.State.Reducers.ReduceBattleInvite
import App.State.Reducers.ReduceCaptcha
import App.State.Reducers.ReduceChat
import App.State.Reducers.ReduceChallenges
import App.State.Reducers.ReduceClan
import App.State.Reducers.ReduceClanStorage
@@ -276,6 +278,7 @@ object RootReducer {
Challenges = ReduceChallenges(State0.Challenges, Action),
ClansStorage = ReduceClanStorage(State0.ClansStorage, Action),
Clan = ReduceClan(State0.Clan, Action),
ChatState = ReduceChat(State0.ChatState, Action),
)
}
@@ -22,6 +22,7 @@ import App.State.Reducers.BattleViewStateSlice
import App.State.Reducers.BattlesSelectStateSlice
import App.State.Reducers.CaptchaStateSlice
import App.State.Reducers.ChallengesStateSlice
import App.State.Reducers.ChatStateSlice
import App.State.Reducers.ClanStateSlice
import App.State.Reducers.ClanChatStateSlice
import App.State.Reducers.ClanUserInfoStateSlice
@@ -125,8 +126,8 @@ data class RootState(
val System: SystemStateSlice = SystemStateSlice(),
/** 原 quests(Frt / Yrt.otg)。已迁移。 */
val Quests: QuestsStateSlice = QuestsStateSlice(),
/** 原 chatState(LP / $r.wqv)。未迁移:原样透传。 */
val ChatState: Any? = null,
/** 原 chatState(LP / $r.wqv)。已迁移。 */
val ChatState: ChatStateSlice = ChatStateSlice(),
/** 原 clanChatState(qnt / yv.qtg)。已迁移。 */
val ClanChatState: ClanChatStateSlice = ClanChatStateSlice(),
/** 原 clan(jN / we.hr0)。已迁移。 */
@@ -30,7 +30,7 @@ object RootWiring {
SliceWiring("passwordChangeState", "lpv_1", "VD", "uu.pr6", "uu.pr6(t, n.lpv_1)", listOf("SetPasswordChangeHash", "ShowEmailAndPasswordChangeDialog")),
SliceWiring("system", "mpv_1", "_9", "w9().xt9", "w9().xt9(t, n.mpv_1)", listOf("DoShowMessage", "SetFatalError")),
SliceWiring("quests", "npv_1", "Frt", "Yrt.otg", "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", listOf("GoTo", "SetCurrentCategory", "SetNotCompletedQuests")),
SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("?QP", "?XP", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "ResetSettingsFromStorage", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")),
SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("CancelHideChatKeyboard", "ChangeChannel", "CleanUsersMessage", "ClearChatContextMenu", "CloseChat", "DisableChatScroll", "EnableChatScroll", "HideChatKeyboard", "IgnoreUserMessages", "MarkFirstChannelMessagesLoaded", "OpenChat", "RequireHideChatKeyboard", "ResetChat", "ResetSettingsFromStorage", "SaveChatMessage", "SendMessageToServer", "SetChatConfig", "SetMessages", "SetSelectedUid", "ShowChatKeyboard", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")),
SliceWiring("clanChatState", "ppv_1", "qnt", "yv.qtg", "yv.qtg(t, n.ppv_1)", listOf("CloseClanChat", "Configure", "DoAddMessages", "OpenClanChat", "ResetClanChat")),
SliceWiring("clan", "qpv_1", "jN", "we.hr0", "we.hr0(t, n.qpv_1)", listOf("AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", "AddNewIncomingClan", "AddOutgoingClan", "AddUsersToAccepted", "AddUsersToNewAccepted", "ClanBanned", "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", "DecrementClanNotifications", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", "IncrementClanNotifications", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", "NewIncomingUserNotification", "OutgoingAdded", "OutgoingRemoved", "RemoveIncomingClan", "RemoveNewIncoming", "RemoveNewIncomingClan", "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", "ResetClanNameTagValidation", "ResetClanUserValidation", "SetClanEditPrice", "SetClanNotificationsCount", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", "UpdateIntermediateDescription", "UpdateIntermediateMinRank", "UpdateIntermediateRequests", "UpdateTimeToJoinRestriction", "UpdateValidation")),
SliceWiring("clansStorage", "rpv_1", "lO", "me.vr1", "me.vr1(t, n.rpv_1)", listOf("AddClanUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", "ClanUnloaded", "ClearForeignClan", "ForeignClanLoaded", "LoadClanIntoStorage", "MinimalRankChanged", "RecruitmentChanged", "RemoveClanUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan")),
@@ -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")
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 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;
// 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;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -879,6 +879,31 @@ import App.State.Reducers.SetClanNotificationsCount
import App.State.Reducers.UpdateIntermediateDescription
import App.State.Reducers.UpdateIntermediateMinRank
import App.State.Reducers.UpdateIntermediateRequests
import App.State.ChangeChannel
import App.State.CleanUsersMessage
import App.State.IgnoreUserMessages
import App.State.SaveChatMessage
import App.State.SendMessageToServer
import App.State.SetMessages
import App.State.SetSelectedUid
import App.State.ShowContextMenu
import App.State.ShowUserMessages
import App.State.UpdateTypingSpeedAntifloodParams
import App.State.UpdateUsers
import App.State.Reducers.CancelHideChatKeyboard
import App.State.Reducers.ChatStateSlice
import App.State.Reducers.ClearChatContextMenu
import App.State.Reducers.CloseChat
import App.State.Reducers.DisableChatScroll
import App.State.Reducers.EnableChatScroll
import App.State.Reducers.HideChatKeyboard
import App.State.Reducers.MarkFirstChannelMessagesLoaded
import App.State.Reducers.OpenChat
import App.State.Reducers.ReduceChat
import App.State.Reducers.RequireHideChatKeyboard
import App.State.Reducers.ResetChat
import App.State.Reducers.SetChatConfig
import App.State.Reducers.ShowChatKeyboard
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
@@ -1306,7 +1331,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 85 个已迁移切片(84 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 86 个已迁移切片(85 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -1370,7 +1395,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 85 个已迁移字段,其余 4 个待迁",
"根:迁移表 = 86 个已迁移字段,其余 3 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
@@ -1394,9 +1419,9 @@ object StateCheck {
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
"graphicsSettings", "controlSettings", "battleSettings", "contractState",
"tankBuilderState", "battleMessage", "maps", "battleChat",
"remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan",
"remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState",
) &&
RootWiring.Unmigrated.size == 4
RootWiring.Unmigrated.size == 3
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -1452,7 +1477,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 85/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 86/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -7228,6 +7253,99 @@ object StateCheck {
)
}
// 96 切片 reducer:ChatState(LP / $r.wqv,16 字段;24 个动作)
run {
fun Msg(Id: Int, Source: String, Target: String? = null): ChatMessage = ChatMessage(
addressMode = "", battleLinks = emptyList(), channel = "general", id = Id,
links = emptyList(), messageType = "text", sourceUser = Source, ip = "",
uid = null, userId = "1", targetUser = Target, text = "m", timePassedInSec = 0,
)
val S0 = ChatStateSlice()
Check(
"切片 ChatState:默认值完整",
!S0.Open && S0.CurrentChannel == null && S0.ChatMessage == null &&
S0.SelectedUid == "" && S0.MessagesByChannel.isEmpty() &&
!S0.IsShowKeyboard && !S0.IsNeedToHideKeyboard && !S0.IsNeedScroll &&
S0.IsFirstChannelMessagesLoad && S0.IgnoredUserIds.isEmpty()
)
Check(
"切片 ChatState:开关 / 频道 / 配置",
ReduceChat(S0, OpenChat).Open && !ReduceChat(S0, CloseChat).Open &&
ReduceChat(S0, ChangeChannel("general")).CurrentChannel == "general" &&
ReduceChat(S0, SetChatConfig(null)).Config == null
)
val S1 = ReduceChat(S0, SaveChatMessage(Msg(1, "u1", "u2")))
Check(
"切片 ChatState:SaveChatMessage 写消息并选中目标用户",
(S1.ChatMessage as ChatMessage).id == 1 && S1.SelectedUid == "u2"
)
val S2 = ReduceChat(S0, SetMessages(mapOf("general" to listOf(Msg(1, "u1")))))
Check(
"切片 ChatState:SetMessages 整体替换频道表",
(S2.MessagesByChannel["general"] as List<Any?>).size == 1
)
Check(
"切片 ChatState:CleanUsersMessage 按 sourceUser 过滤",
(ReduceChat(S2, CleanUsersMessage("u1"))
.MessagesByChannel["general"] as List<Any?>).isEmpty()
)
Check(
"切片 ChatState:右键菜单设置 / 清除",
ReduceChat(S0, ShowContextMenu("ctx")).ContextMenuData == "ctx" &&
ReduceChat(S0, ClearChatContextMenu).ContextMenuData == null
)
Check(
"切片 ChatState:键盘 / 滚动开关",
ReduceChat(S0, ShowChatKeyboard).let { it.IsShowKeyboard && !it.IsNeedToHideKeyboard } &&
!ReduceChat(S0, HideChatKeyboard).IsShowKeyboard &&
ReduceChat(S0, RequireHideChatKeyboard).IsNeedToHideKeyboard &&
!ReduceChat(S0, CancelHideChatKeyboard).IsNeedToHideKeyboard &&
ReduceChat(S0, EnableChatScroll).IsNeedScroll &&
!ReduceChat(S0, DisableChatScroll).IsNeedScroll
)
Check(
"切片 ChatState:SendMessageToServer 置两个待处理位",
ReduceChat(S0, SendMessageToServer("t", "mode", "m")).let {
it.IsNeedToHideKeyboard && it.IsNeedScroll
}
)
Check(
"切片 ChatState:速率参数 / 选中 uid 转小写 / 首屏标记",
ReduceChat(S0, UpdateTypingSpeedAntifloodParams(1, 2)).let {
it.SymbolCost == 1 && it.EnterCost == 2
} &&
ReduceChat(S0, SetSelectedUid("ABC")).SelectedUid == "abc" &&
!ReduceChat(S0, MarkFirstChannelMessagesLoaded).IsFirstChannelMessagesLoad
)
Check(
"切片 ChatState:屏蔽 / 取消屏蔽 / 重载清空",
ReduceChat(S0, IgnoreUserMessages("u1")).IgnoredUserIds == setOf<Any?>("u1") &&
ReduceChat(ReduceChat(S0, IgnoreUserMessages("u1")), ShowUserMessages("u1"))
.IgnoredUserIds.isEmpty() &&
ReduceChat(ReduceChat(S0, IgnoreUserMessages("u1")), ResetSettingsFromStorage)
.IgnoredUserIds.isEmpty()
)
Check(
"切片 ChatState:UpdateUsers 是 no-op、ResetChat 复位",
ReduceChat(S2, UpdateUsers(listOf("u1"))) == S2 &&
ReduceChat(S2, ResetChat) == ChatStateSlice()
)
Check("切片 ChatState:无关动作不改状态", ReduceChat(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, OpenChat)
Check(
"根:OpenChat 只动 chatState",
Root1.ChatState.Open && Root1.Clan == Root0.Clan
)
val W = RootWiring.ByField("chatState")!!
Check(
"根:chatState 委派到 LP / \$r.wqv 且 24 个动作",
W.SliceClass == "LP" && W.Reducer == "\$r.wqv" && W.Actions.size == 24
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)