diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 0803522..4b10bb6 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` 动作名。 - - 已迁移切片 **63/89**: + - 已迁移切片 **65/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) @@ -341,12 +341,18 @@ `OnUserJoinTheBattle` / `OnUserLeaveTheBattle` 增删 `noticed`;`dI`→`ResetTankActionsLog` 整片复位。 - `suppliesTutorial` → `ust` / `Uw.uth`(`SuppliesTutorialReducer.kt`):2 字段,2 个分支。 生成器认不出切片类(根构造默认值不是 `new ust()`),在迁移表里显式补 `SliceClass: 'ust'`。 + - `clanChatState` → `qnt` / `yv.qtg`(`ClanChatStateReducer.kt`):3 字段,5 个分支。 + `$nt`/`xnt` 开关面板,`Configure` 写配置(`ynt` 的 `config` 与 clan 切片同名动作冲突, + 整条动作对象按配置载荷收口),`DoAddMessages` 追加后只保留最后 100 条,`Tnt` 整片复位。 + - `battleSearchState` → `Itt` / `Htt().vtg`(`BattleSearchStateReducer.kt`):5 字段,9 个分支。 + `Fight` 只发副作用、状态不变;`RemoveBattle` 仅当选中 id 匹配时清空; + `selectedFormat` 默认值来自服务定位器,按 `null` 收口。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 62 个有 reducer 的切片, - `launchParams` 与其余 **26 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 63/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **719 项全绿**,含 89 槽位完整性、62 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 64 个有 reducer 的切片, + `launchParams` 与其余 **24 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 65/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **741 项全绿**,含 89 槽位完整性、64 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -366,6 +372,7 @@ friends 的 2 个、suppliesState 的 1 个、battleInviteState 的 2 个、partner 的 4 个、 userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`); battlePauseState 的 2 个、battleViewState 的 12 个、tankActionsLogState 的 1 个同样按效果定名。 + clanChatState 的 3 个(`$nt`/`xnt`/`Tnt`)、battleSearchState 的 1 个也已登记。 ### S4 引擎核心 - 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入) diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 2a8b046..2022765 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T08:27:50.811Z", + "generatedAt": "2026-10-24T08:38:53.378Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -69,7 +69,9 @@ "battlePauseState", "battleViewState", "tankActionsLogState", - "suppliesTutorial" + "suppliesTutorial", + "clanChatState", + "battleSearchState" ], "AnonymousActions": [ { @@ -1182,6 +1184,30 @@ "Name": "ResetTankActionsLog", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "new vI() 整片复位" + }, + { + "Local": "$nt", + "Name": "OpenClanChat", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "open=true(`n.qtp(!0)`)" + }, + { + "Local": "xnt", + "Name": "CloseClanChat", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "open=false(`n.qtp(!1)`)" + }, + { + "Local": "Tnt", + "Name": "ResetClanChat", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new qnt() 整片复位" + }, + { + "Local": "Dtt", + "Name": "ClearSelectedBattle", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "selectedBattleId=null(`n.vtn(cd, cd, cd, cd, null)`)" } ], "Markers": [ @@ -1660,14 +1686,14 @@ "Reducer": "yv.qtg", "Delegate": "yv.qtg(t, n.ppv_1)", "Actions": [ - "?$nt", - "?Tnt", - "?xnt", + "CloseClanChat", "Configure", - "DoAddMessages" + "DoAddMessages", + "OpenClanChat", + "ResetClanChat" ], "Default": "new qnt()", - "Migrated": false + "Migrated": true }, { "Field": "clan", @@ -1997,7 +2023,7 @@ "Reducer": "Htt().vtg", "Delegate": "Htt().vtg(t, n.epw_1)", "Actions": [ - "?Dtt", + "ClearSelectedBattle", "EnterToBattle", "Fight", "RemoveBattle", @@ -2008,7 +2034,7 @@ "SetSuspiciousFilter" ], "Default": "new Itt()", - "Migrated": false + "Migrated": true }, { "Field": "lobbyLoader", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 5180751..5ed91a4 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -275,6 +275,12 @@ const AnonymousActionNames = new Map([ ['eI', 'LeaveBackground'], // tankActionsLogState 切片(vI / yI.gqe):dI 没有 toString。 ['dI', 'ResetTankActionsLog'], + // clanChatState 切片(qnt / Int.qtg):$nt/xnt/Tnt 没有 toString。 + ['$nt', 'OpenClanChat'], + ['xnt', 'CloseClanChat'], + ['Tnt', 'ResetClanChat'], + // battleSearchState 切片(Itt / Gtt.vtg):Dtt 没有 toString。 + ['Dtt', 'ClearSelectedBattle'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -731,6 +737,8 @@ const Migrated = { SliceClass: 'ust', Reduce: 'ReduceSuppliesTutorial', }, + clanChatState: { Type: 'ClanChatStateSlice', Reduce: 'ReduceClanChat' }, + battleSearchState: { Type: 'BattleSearchStateSlice', Reduce: 'ReduceBattleSearch' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1093,6 +1101,18 @@ const TankActionsLogAnonymousEffects = { dI: 'new vI() 整片复位', }; +// clanChatState 切片(qnt / Int.qtg):$nt/xnt/Tnt 没有 toString。 +const ClanChatAnonymousEffects = { + $nt: 'open=true(`n.qtp(!0)`)', + xnt: 'open=false(`n.qtp(!1)`)', + Tnt: 'new qnt() 整片复位', +}; + +// battleSearchState 切片(Itt / Gtt.vtg):Dtt 没有 toString。 +const BattleSearchAnonymousEffects = { + Dtt: 'selectedBattleId=null(`n.vtn(cd, cd, cd, cd, null)`)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -1337,6 +1357,18 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(ClanChatAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(BattleSearchAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), ], Markers: [ { diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleSearchStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleSearchStateReducer.kt new file mode 100644 index 0000000..afe4cc3 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleSearchStateReducer.kt @@ -0,0 +1,67 @@ +// BattleSearchStateReducer.kt —— 战斗搜索切片(对应 module_10032 的 Itt 状态 + Gtt.vtg) +// +// 证据链: +// 状态类 Itt:5 个字段,toString 名 "BattleSearchState(…)"。 +// 默认:filter=null、selectedFormat=cA().xqh()、search=null、suspiciousBattlesOnly=false、 +// selectedBattleId=null。 +// 根委派:`Htt().vtg(t, n.epw_1)`。 +// reducer(9 个实例臂): +// Ntt(SetFilter) → filter +// Mtt(SetSearch) → search +// Ptt(SetSuspiciousFilter)→ suspiciousBattlesOnly +// Vtt(Fight) → 状态不变(原分支只发 `Fy().ven()` 副作用) +// FH(EnterToBattle) → search=null +// Ott(SelectBattle) → selectedBattleId +// Ltt(SelectBattleFormat)→ selectedFormat +// kA(RemoveBattle) → 选中的正是该战斗时清空 selectedBattleId +// Dtt(无 toString) → selectedBattleId=null +// +// `selectedFormat` 的默认值 `cA().xqh()` 来自服务定位器,离线重建按 `null` 收口。 +package App.State.Reducers + +import App.State.BattleFormat +import App.State.EnterToBattle +import App.State.Fight +import App.State.RemoveBattle +import App.State.SelectBattle +import App.State.SelectBattleFormat +import App.State.SetFilter +import App.State.SetSearch +import App.State.SetSuspiciousFilter + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `Dtt`:清空当前选中的战斗。 */ +object ClearSelectedBattle + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleSearchStateSlice( + val Filter: Boolean? = null, + val SelectedFormat: BattleFormat? = null, + val Search: Any? = null, + val SuspiciousBattlesOnly: Boolean = false, + val SelectedBattleId: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceBattleSearch( + State0: BattleSearchStateSlice, + Action: Any, +): BattleSearchStateSlice = when (Action) { + is SetFilter -> State0.copy(Filter = Action.filter) + is SetSearch -> State0.copy(Search = Action.search) + is SetSuspiciousFilter -> + State0.copy(SuspiciousBattlesOnly = Action.suspiciousBattlesOnly) + // 原分支只发 `Fy().ven()`(特效/音效副作用),不改状态。 + is Fight -> State0 + is EnterToBattle -> State0.copy(Search = null) + is SelectBattle -> State0.copy(SelectedBattleId = Action.battleId) + is SelectBattleFormat -> State0.copy(SelectedFormat = Action.format) + is RemoveBattle -> + if (State0.SelectedBattleId == Action.battleId) State0.copy(SelectedBattleId = null) + else State0 + is ClearSelectedBattle -> State0.copy(SelectedBattleId = null) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanChatStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanChatStateReducer.kt new file mode 100644 index 0000000..e99508b --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanChatStateReducer.kt @@ -0,0 +1,58 @@ +// ClanChatStateReducer.kt —— 战队聊天切片(对应 module_10032 的 qnt 状态 + Int.qtg) +// +// 证据链: +// 状态类 qnt:3 个字段,toString 名 "ClanChatState(…)"。 +// 默认:open=false、messages=emptyList()、config=null。 +// 根委派:`yv.qtg(t, n.ppv_1)`。 +// reducer(5 个实例臂): +// $nt(无 toString) → open=true +// xnt(无 toString) → open=false +// ynt(Configure) → config +// Cnt(DoAddMessages) → messages 追加后只保留最后 100 条 +// Tnt(无 toString) → `new qnt()` 整片复位 +package App.State.Reducers + +import App.State.Configure +import App.State.DoAddMessages + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `$nt`:展开战队聊天。 */ +object OpenClanChat + +/** 原 `xnt`:收起战队聊天。 */ +object CloseClanChat + +/** 原 `Tnt`:整片复位(`new qnt()`)。 */ +object ResetClanChat + +/* ------------------------------------------------------------------ 状态 */ + +data class ClanChatStateSlice( + val Open: Boolean = false, + val Messages: List = emptyList(), + val Config: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `e.q1(max(0, size - 100), size)`:只保留最后 100 条。 */ +private const val MaxMessages = 100 + +private fun AppendMessages(Current: List, Incoming: Any?): List { + val Added = if (Incoming is Iterable<*>) Current + Incoming else Current + Incoming + return if (Added.size <= MaxMessages) Added + else Added.subList(Added.size - MaxMessages, Added.size) +} + +fun ReduceClanChat( + State0: ClanChatStateSlice, + Action: Any, +): ClanChatStateSlice = when (Action) { + is OpenClanChat -> State0.copy(Open = true) + is CloseClanChat -> State0.copy(Open = false) + is Configure -> State0.copy(Config = Action) + is DoAddMessages -> State0.copy(Messages = AppendMessages(State0.Messages, Action.messages)) + is ResetClanChat -> ClanChatStateSlice() + 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 4c95c6e..8cc0e08 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 出新状态)。 -// 这里采用同一形状。已迁移的 63 个切片里,62 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 65 个切片里,64 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -65,9 +65,11 @@ // battleViewState → ReduceBattleView(DT / sI.jqd,根委派 `(null == $n && new sI(), $n).jqd(t, n.mpw_1)`) // tankActionsLogState → ReduceTankActionsLog(vI / wi.gqe,根委派 `wi.gqe(t, n.ppw_1)`) // suppliesTutorial → ReduceSuppliesTutorial(ust / Uw.uth,根委派 `Uw.uth(t, n.zpx_1)`) +// clanChatState → ReduceClanChat(qnt / yv.qtg,根委派 `yv.qtg(t, n.ppv_1)`) +// battleSearchState → ReduceBattleSearch(Itt / Htt().vtg,根委派 `Htt().vtg(t, n.epw_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 26 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 24 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -77,11 +79,13 @@ import App.State.Reducers.ReduceAnnouncement import App.State.Reducers.ReduceAntiAddiction import App.State.Reducers.ReduceBattleResult import App.State.Reducers.ReduceBattlePause +import App.State.Reducers.ReduceBattleSearch import App.State.Reducers.ReduceBattleTutorial import App.State.Reducers.ReduceBattleView import App.State.Reducers.ReduceBattleInvite import App.State.Reducers.ReduceCaptcha import App.State.Reducers.ReduceClanUserInfo +import App.State.Reducers.ReduceClanChat import App.State.Reducers.ReduceContextTutorial import App.State.Reducers.ReduceCreatorTag import App.State.Reducers.ReduceDroppableGold @@ -210,6 +214,8 @@ object RootReducer { BattleViewState = ReduceBattleView(State0.BattleViewState, Action), TankActionsLogState = ReduceTankActionsLog(State0.TankActionsLogState, Action), SuppliesTutorial = ReduceSuppliesTutorial(State0.SuppliesTutorial, Action), + ClanChatState = ReduceClanChat(State0.ClanChatState, Action), + BattleSearchState = ReduceBattleSearch(State0.BattleSearchState, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 5f9195a..21ff834 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -11,9 +11,11 @@ import App.State.Reducers.AvailabilityByRankStateSlice import App.State.Reducers.BattleInviteStateSlice import App.State.Reducers.BattlePauseStateSlice import App.State.Reducers.BattleResultStateSlice +import App.State.Reducers.BattleSearchStateSlice import App.State.Reducers.BattleTutorialStateSlice import App.State.Reducers.BattleViewStateSlice import App.State.Reducers.CaptchaStateSlice +import App.State.Reducers.ClanChatStateSlice import App.State.Reducers.ClanUserInfoStateSlice import App.State.Reducers.ContextTutorialStateSlice import App.State.Reducers.CreatorTagStateSlice @@ -105,8 +107,8 @@ data class RootState( val Quests: QuestsStateSlice = QuestsStateSlice(), /** 原 chatState(LP / $r.wqv)。未迁移:原样透传。 */ val ChatState: Any? = null, - /** 原 clanChatState(qnt / yv.qtg)。未迁移:原样透传。 */ - val ClanChatState: Any? = null, + /** 原 clanChatState(qnt / yv.qtg)。已迁移。 */ + val ClanChatState: ClanChatStateSlice = ClanChatStateSlice(), /** 原 clan(jN / we.hr0)。未迁移:原样透传。 */ val Clan: Any? = null, /** 原 clansStorage(lO / me.vr1)。未迁移:原样透传。 */ @@ -135,8 +137,8 @@ data class RootState( val Matchmaking: Any? = null, /** 原 battleCreateState($6 / Df.ttg)。未迁移:原样透传。 */ val BattleCreateState: Any? = null, - /** 原 battleSearchState(Itt / Htt().vtg)。未迁移:原样透传。 */ - val BattleSearchState: Any? = null, + /** 原 battleSearchState(Itt / Htt().vtg)。已迁移。 */ + val BattleSearchState: BattleSearchStateSlice = BattleSearchStateSlice(), /** 原 lobbyLoader(KW / Bs.es1)。已迁移。 */ val LobbyLoader: LobbyLoaderStateSlice = LobbyLoaderStateSlice(), /** 原 battleResult(sR / Fi.aqp)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 0e6ec93..455f409 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -31,7 +31,7 @@ object RootWiring { 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", "?_C", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")), - SliceWiring("clanChatState", "ppv_1", "qnt", "yv.qtg", "yv.qtg(t, n.ppv_1)", listOf("?\$nt", "?Tnt", "?xnt", "Configure", "DoAddMessages")), + 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("?FM", "?IM", "?KM", "?PM", "?RM", "?VM", "?YM", "?iM", "AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", "AddNewIncomingClan", "AddOutgoingClan", "AddUsersToAccepted", "AddUsersToNewAccepted", "ClanBanned", "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", "NewIncomingUserNotification", "OutgoingAdded", "OutgoingRemoved", "RemoveIncomingClan", "RemoveNewIncoming", "RemoveNewIncomingClan", "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", "SetClanEditPrice", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", "UpdateTimeToJoinRestriction", "UpdateValidation")), SliceWiring("clansStorage", "rpv_1", "lO", "me.vr1", "me.vr1(t, n.rpv_1)", listOf("?mO", "AddUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", "ClanLoaded", "ClanUnloaded", "ForeignClanLoaded", "MinimalRankChanged", "RecruitmentChanged", "RemoveUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan")), SliceWiring("tipsState", "spv_1", "dst", "Hw.vth", "Hw.vth(t, n.spv_1)", listOf("UpdateTips")), @@ -46,7 +46,7 @@ object RootWiring { SliceWiring("maps", "bpw_1", "OA", "Oi.sql", "Oi.sql(t, n.bpw_1)", listOf("AddMap")), SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("?WH", "?nW", "?tW", "Configure", "EnterToBattleApply", "Play", "SelectMatchmakingMode", "SetRankIndexToMinGS", "WasRegistered")), SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")), - SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("?Dtt", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")), + SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("ClearSelectedBattle", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")), SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("HideLoader", "ShowLoader")), SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("DoInit", "ResetBattleRewards", "SetHasBattleResult", "SetIncreasedReward", "SetRewards", "SetSimpleReward")), SliceWiring("battleUsers", "hpw_1", "uP", "sr.hqs", "sr.hqs(t, n.hpw_1)", listOf("?_P", "?hP", "?lP", "?oP", "?qP", "?sP", "InitSuspiciousState", "InitUserLevels", "RemoveTankInfo", "SetGearScore", "SetTankInfo", "SetTankResistance", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 3ebe7cc..cc24e15 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -625,6 +625,24 @@ import App.State.ActivatedInCurrentBattle import App.State.SetLoadedElement import App.State.Reducers.ReduceSuppliesTutorial import App.State.Reducers.SuppliesTutorialStateSlice +import App.State.Configure +import App.State.DoAddMessages +import App.State.Reducers.ClanChatStateSlice +import App.State.Reducers.CloseClanChat +import App.State.Reducers.OpenClanChat +import App.State.Reducers.ReduceClanChat +import App.State.Reducers.ResetClanChat +import App.State.BattleFormat +import App.State.EnterToBattle +import App.State.RemoveBattle +import App.State.SelectBattle +import App.State.SelectBattleFormat +import App.State.SetFilter +import App.State.SetSearch +import App.State.SetSuspiciousFilter +import App.State.Reducers.BattleSearchStateSlice +import App.State.Reducers.ClearSelectedBattle +import App.State.Reducers.ReduceBattleSearch import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1052,7 +1070,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 63 个已迁移切片(62 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 65 个已迁移切片(64 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1116,7 +1134,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 63 个已迁移字段,其余 26 个待迁", + "根:迁移表 = 65 个已迁移字段,其余 24 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1136,8 +1154,9 @@ object StateCheck { "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", + "clanChatState", "battleSearchState", ) && - RootWiring.Unmigrated.size == 26 + RootWiring.Unmigrated.size == 24 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1193,7 +1212,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 63/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 65/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -5476,6 +5495,121 @@ object StateCheck { ) } + // 74 切片 reducer:ClanChat(qnt / yv.qtg,3 字段;5 个动作) + run { + val S0 = ClanChatStateSlice() + Check( + "切片 ClanChat:默认关闭、无消息、无配置", + !S0.Open && S0.Messages.isEmpty() && S0.Config == null + ) + val S1 = ReduceClanChat(S0, OpenClanChat) + Check("切片 ClanChat:OpenClanChat 打开", S1.Open) + Check( + "切片 ClanChat:CloseClanChat 关闭", + !ReduceClanChat(S1, CloseClanChat).Open + ) + val S2 = ReduceClanChat(S1, DoAddMessages(listOf("m1", "m2"))) + Check( + "切片 ClanChat:DoAddMessages 追加消息", + S2.Messages == listOf("m1", "m2") + ) + var S3 = S2 + for (I in 1..120) S3 = ReduceClanChat(S3, DoAddMessages(listOf("x$I"))) + Check( + "切片 ClanChat:超过 100 条只保留最后 100", + S3.Messages.size == 100 && S3.Messages.last() == "x120" + ) + Check( + "切片 ClanChat:Configure 写配置", + ReduceClanChat(S1, Configure(emptyList(), emptyList(), false, "", null, null, "", 0, 0, false, null, "")) + .Config != null + ) + Check( + "切片 ClanChat:ResetClanChat 整片复位", + ReduceClanChat(S3, ResetClanChat) == ClanChatStateSlice() + ) + Check( + "切片 ClanChat:无关动作不改状态", + ReduceClanChat(S3, AddFrags(1)) == S3 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, OpenClanChat) + Check( + "根:OpenClanChat 只动 clanChatState", + Root1.ClanChatState.Open && + Root1.BattleSearchState == Root0.BattleSearchState + ) + val W = RootWiring.ByField("clanChatState")!! + Check( + "根:clanChatState 委派到 qnt / yv.qtg 且 5 个动作", + W.SliceClass == "qnt" && W.Reducer == "yv.qtg" && + W.Actions.toSet() == setOf( + "OpenClanChat", "CloseClanChat", "Configure", "DoAddMessages", + "ResetClanChat", + ) + ) + } + + // 75 切片 reducer:BattleSearch(Itt / Htt().vtg,5 字段;9 个动作) + run { + val Format = BattleFormat("fmt", "name", "desc", null, null) + val S0 = BattleSearchStateSlice() + Check( + "切片 BattleSearch:默认值完整", + S0.Filter == null && S0.SelectedFormat == null && S0.Search == null && + !S0.SuspiciousBattlesOnly && S0.SelectedBattleId == null + ) + val S1 = ReduceBattleSearch(S0, SetFilter(true)) + Check("切片 BattleSearch:SetFilter 写过滤位", S1.Filter == true) + val S2 = ReduceBattleSearch(S1, SetSearch("abc")) + Check("切片 BattleSearch:SetSearch 写搜索串", S2.Search == "abc") + Check( + "切片 BattleSearch:SetSuspiciousFilter 写布尔", + ReduceBattleSearch(S2, SetSuspiciousFilter(true)).SuspiciousBattlesOnly + ) + Check( + "切片 BattleSearch:EnterToBattle 清空搜索串", + ReduceBattleSearch(S2, EnterToBattle(7L)).Search == null + ) + Check( + "切片 BattleSearch:SelectBattle / SelectBattleFormat", + ReduceBattleSearch(S2, SelectBattle(7L)).SelectedBattleId == 7L && + ReduceBattleSearch(S2, SelectBattleFormat(Format)).SelectedFormat == Format + ) + Check( + "切片 BattleSearch:RemoveBattle 只清匹配的那条", + ReduceBattleSearch(ReduceBattleSearch(S2, SelectBattle(7L)), RemoveBattle(7L)) + .SelectedBattleId == null && + ReduceBattleSearch( + ReduceBattleSearch(S2, SelectBattle(7L)), + RemoveBattle(8L), + ).SelectedBattleId == 7L + ) + Check( + "切片 BattleSearch:ClearSelectedBattle 清空选中", + ReduceBattleSearch(S2, ClearSelectedBattle).SelectedBattleId == null + ) + Check("切片 BattleSearch:Fight 不改状态", ReduceBattleSearch(S2, Fight(1L, BattleTeam.NONE)) == S2) + Check( + "切片 BattleSearch:无关动作不改状态", + ReduceBattleSearch(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetSearch("q")) + Check( + "根:SetSearch 只动 battleSearchState", + Root1.BattleSearchState.Search == "q" && + Root1.ClanChatState == Root0.ClanChatState + ) + val W = RootWiring.ByField("battleSearchState")!! + Check( + "根:battleSearchState 委派到 Itt / Htt().vtg 且 9 个动作", + W.SliceClass == "Itt" && W.Reducer == "Htt().vtg" && W.Actions.size == 9 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)