From bb544643b5a8c260448f094d1e0fa163efad273d Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 22:09:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20chatState?= =?UTF-8?q?=EF=BC=88=E5=A4=8D=E7=94=A8=20ChatMessage=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 16 +- 3DTank/names/root_wiring.json | 103 +++++++++++-- 3DTank/tools/gen_root_wiring.js | 27 ++++ .../App/State/Reducers/ChatStateReducer.kt | 139 ++++++++++++++++++ .../jsMain/kotlin/App/State/RootReducer.kt | 7 +- .../src/jsMain/kotlin/App/State/RootState.kt | 5 +- .../src/jsMain/kotlin/App/State/RootWiring.kt | 4 +- .../app/src/jsMain/kotlin/App/StateCheck.kt | 130 +++++++++++++++- 8 files changed, 399 insertions(+), 32 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChatStateReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index c271ff7..df25b18 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -143,7 +143,7 @@ `names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。 - 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。 - 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。 - - 已迁移切片 **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`,不新增 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@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 412997c..403849a 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -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", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 2e9a57d..6e84d00 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -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'), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChatStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChatStateReducer.kt new file mode 100644 index 0000000..edd7c7d --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ChatStateReducer.kt @@ -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 = 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 = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `YP`(原名 `Configure`,与匹配大厅的同名类冲突):写聊天配置。 */ +data class SetChatConfig(val config: Any?) + +/** 原 `iN`:按 uid 从每个频道里过滤消息(sourceUser / targetUser 命中即删)。 */ +private fun CleanByUid( + Channels: Map, + Uid: Any?, +): Map = Channels.mapValues { (_, Messages) -> + (Messages as? List)?.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) ?: 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 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt index 3451caa..50c0a96 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -1,7 +1,7 @@ // RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6) // // 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。 -// 这里采用同一形状。已迁移的 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), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 02317d7..4fde8ab 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -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)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 810da70..923d982 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -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 = 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 = 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 get() = All.filter { it.Field !in Migrated } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index 0596e0e..601b63b 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -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).size == 1 + ) + Check( + "切片 ChatState:CleanUsersMessage 按 sourceUser 过滤", + (ReduceChat(S2, CleanUsersMessage("u1")) + .MessagesByChannel["general"] as List).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("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)