From ad5686241d1b6b823d7b289ae8db46441839f531 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 21:58:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20clansStorag?= =?UTF-8?q?e=EF=BC=88=E5=A4=8D=E7=94=A8=20ClanInfo=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 | 17 +- 3DTank/names/root_wiring.json | 39 ++++- 3DTank/tools/gen_root_wiring.js | 11 ++ .../App/State/Reducers/ClanStorageReducer.kt | 148 ++++++++++++++++++ .../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 | 103 +++++++++++- 8 files changed, 310 insertions(+), 24 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanStorageReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 3f5d6f4..c81adeb 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` 动作名。 - - 已迁移切片 **83/89**: + - 已迁移切片 **84/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) @@ -418,12 +418,19 @@ `SetBattlePass(flavor)` 追加一个 `Challenge` 战令条目,两个「已领取索引」动作按 flavor 命中后 copy; `SetRewardForDialog` / `LZ` 依赖 `FZ`/`GZ` 构造与外部刷新,纯 reducer 里按 no-op 收口。 `Challenge` 直接复用生成物模型(9 字段)。 + - `clansStorage` → `lO` / `me.vr1`(`ClanStorageReducer.kt`):3 字段,13 个分支。 + 列表项复用生成物 `ClanInfo`(16 字段);各分支都是对 `ClanInfo` 的 copy: + `MinimalRankChanged`(第 5 参) / `RecruitmentChanged`(第 6 参) / 成员与待审用户(第 9/16 参) / + `UpdateClan`(name/tag/description) / `BuyAdditionalPlace`(maxMembers+1)。 + `vO`(原名 `ClanLoaded`,与 clan 切片的 `isLoaded` 同名)改名 `LoadClanIntoStorage`; + `dO`/`gO`(原名 `AddUser`/`RemoveUser`,与战斗列表同名)改名 `AddClanUser`/`RemoveClanUser`; + 查找时兼容 Int/Long 两种 clanId。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 82 个有 reducer 的切片, - `launchParams` 与其余 **6 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 83/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **913 项全绿**,含 89 槽位完整性、82 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 83 个有 reducer 的切片, + `launchParams` 与其余 **5 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 84/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **924 项全绿**,含 89 槽位完整性、83 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 108765e..c9862b8 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T13:23:52.900Z", + "generatedAt": "2026-10-24T13:56:08.668Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -89,7 +89,8 @@ "matchmaking", "battlesSelect", "battleUsers", - "challenges" + "challenges", + "clansStorage" ], "AnonymousActions": [ { @@ -1489,6 +1490,30 @@ "Name": "RefreshBattlePasses", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "战令条目批量刷新(`GZ`),离线重建按 no-op 收口" + }, + { + "Local": "mO", + "Name": "ClearForeignClan", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "foreignClan=null(`n.lr1(cd, cd, null)`)" + }, + { + "Local": "vO", + "Name": "LoadClanIntoStorage", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "clans[clan.id]=clan(原名 ClanLoaded,与 clan 切片同名类冲突,改名 LoadClanIntoStorage)" + }, + { + "Local": "dO", + "Name": "AddClanUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "clan.users += userId(原名 AddUser,与战斗列表同名类冲突,改名 AddClanUser)" + }, + { + "Local": "gO", + "Name": "RemoveClanUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "clan.users -= userId(原名 RemoveUser,同上改名 RemoveClanUser)" } ], "Markers": [ @@ -2037,22 +2062,22 @@ "Reducer": "me.vr1", "Delegate": "me.vr1(t, n.rpv_1)", "Actions": [ - "?mO", - "AddUser", + "AddClanUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", - "ClanLoaded", "ClanUnloaded", + "ClearForeignClan", "ForeignClanLoaded", + "LoadClanIntoStorage", "MinimalRankChanged", "RecruitmentChanged", - "RemoveUser", + "RemoveClanUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan" ], "Default": "new lO()", - "Migrated": false + "Migrated": true }, { "Field": "tipsState", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 38f6be3..ca06fb2 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -339,6 +339,12 @@ const AnonymousActionNames = new Map([ ['sP', 'MigrateBattleUserTeams'], // challenges 切片(CZ / nh.hsj):LZ 没有 toString。 ['LZ', 'RefreshBattlePasses'], + // clansStorage 切片(lO / zO.vr1):mO 没有 toString;dO/gO 与战斗列表的同名类冲突,改名避免字段错配。 + ['mO', 'ClearForeignClan'], + ['dO', 'AddClanUser'], + ['gO', 'RemoveClanUser'], + // vO 的 toString 名 ClanLoaded 与 clan 切片的 isLoaded 动作同名冲突,改名避免字段错配。 + ['vO', 'LoadClanIntoStorage'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -826,6 +832,7 @@ const Migrated = { battlesSelect: { Type: 'BattlesSelectStateSlice', Reduce: 'ReduceBattlesSelect' }, battleUsers: { Type: 'BattleUsersStateSlice', Reduce: 'ReduceBattleUsers' }, challenges: { Type: 'ChallengesStateSlice', Reduce: 'ReduceChallenges' }, + clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1250,6 +1257,10 @@ const AppSettingsAnonymousEffects = { lP: 'usersPremium[userId](`cg(n.mqa_1, Vd(t.iqs_1, t.jqs_1))`)', sP: '旧队伍枚举迁移(`qm/bm/em` 映射),离线重建按 no-op 收口', LZ: '战令条目批量刷新(`GZ`),离线重建按 no-op 收口', + mO: 'foreignClan=null(`n.lr1(cd, cd, null)`)', + vO: 'clans[clan.id]=clan(原名 ClanLoaded,与 clan 切片同名类冲突,改名 LoadClanIntoStorage)', + dO: 'clan.users += userId(原名 AddUser,与战斗列表同名类冲突,改名 AddClanUser)', + gO: 'clan.users -= userId(原名 RemoveUser,同上改名 RemoveClanUser)', }; fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanStorageReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanStorageReducer.kt new file mode 100644 index 0000000..b0956b8 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanStorageReducer.kt @@ -0,0 +1,148 @@ +// ClanStorageReducer.kt —— 战队列表缓存切片(对应 module_10032 的 lO 状态 + zO.vr1) +// +// 证据链: +// 状态类 lO:3 个字段,toString 名 "ClansStorage(clans=…, foreignClanId=…, foreignClan=…)"。 +// 默认:clans=emptyMap()、foreignClanId=null、foreignClan=null。 +// 根委派:`me.vr1(t, n.rpv_1)`。 +// reducer(13 个实例臂,全部围绕 ClanInfo 的 copy `nr1`): +// vO(ClanLoaded) → clans[clan.id] = clan +// pO(ClanUnloaded) → clans -= clanId +// TM(MinimalRankChanged) → copy(minRankIndexForAddClan) // `nr1` 第 5 参 +// AM(RecruitmentChanged) → copy(incomingRequestEnabled) // 第 6 参 +// BN(AddUsersToNewAccepted)→ copy(newUsers += userIds) // 第 16 参 +// FN(RemoveUsersFromNewAccepted)→ copy(newUsers -= userId) // 第 16 参 +// dO(AddUser) → copy(users += userId) // 第 9 参 +// gO(RemoveUser) → copy(users -= userId) // 第 9 参 +// wO(ForeignClanLoaded) → foreignClan +// mO(无 toString) → foreignClan = null +// OM(ShowForeignClan) → foreignClanId +// nO(UpdateClan) → copy(name / tag / description,null 时保留原值) +// iO(BuyAdditionalPlace) → copy(maxMembers + 1) // 第 15 参 +// +// 列表里的战队摘要直接复用生成物 `ClanInfo`(16 字段)。 +package App.State.Reducers + +import App.State.AddUsersToNewAccepted +import App.State.BuyAdditionalPlace +import App.State.ClanInfo +import App.State.ClanUnloaded +import App.State.ForeignClanLoaded +import App.State.MinimalRankChanged +import App.State.RecruitmentChanged +import App.State.RemoveUsersFromNewAccepted +import App.State.ShowForeignClan +import App.State.UpdateClan + +/* ------------------------------------------------------------------ 动作 */ + +/** + * 原 `vO`(原名 `ClanLoaded`,与 clan 切片的 `isLoaded` 动作同名冲突):把战队摘要写进缓存。 + */ +data class LoadClanIntoStorage(val clan: ClanInfo) + +/** 原 `dO`(原名 `AddUser`,与战斗列表的同名类冲突):把用户加入战队成员表。 */ +data class AddClanUser(val clanId: Any?, val userId: Any?) + +/** 原 `gO`(原名 `RemoveUser`,与战斗列表的同名类冲突):把用户移出战队成员表。 */ +data class RemoveClanUser(val clanId: Any?, val userId: Any?) + +/** 原 `mO`:清空外战队详情。 */ +object ClearForeignClan + +/* ------------------------------------------------------------------ 状态 */ + +data class ClanStorageStateSlice( + val Clans: Map = emptyMap(), + val ForeignClanId: Any? = null, + val ForeignClan: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +private fun Plus(Target: Any?, Value: Any?): Any? = when (Target) { + is Set<*> -> if (Value is Iterable<*>) Target + Value.toSet() else Target + Value + is List<*> -> if (Value is Iterable<*>) Target + Value.toList() else Target + Value + else -> if (Value is Iterable<*>) Value.toSet() else setOf(Value) +} + +private fun Minus(Target: Any?, Value: Any?): Any? = when (Target) { + is Set<*> -> if (Value is Iterable<*>) Target - Value.toSet() else Target - Value + is List<*> -> if (Value is Iterable<*>) Target - Value.toList() else Target - Value + else -> emptySet() +} + +/** 原 key 是 Long,部分动作字段是 Int;按数值兼容查一次。 */ +private fun Lookup(Clans: Map, ClanId: Any?): Pair? { + (Clans[ClanId] as? ClanInfo)?.let { return ClanId to it } + val Number = ClanId as? Number ?: return null + (Clans[Number.toLong()] as? ClanInfo)?.let { return Number.toLong() to it } + (Clans[Number.toInt()] as? ClanInfo)?.let { return Number.toInt() to it } + return null +} + +private fun Update( + Clans: Map, + ClanId: Any?, + Transform: (ClanInfo) -> ClanInfo, +): Map { + val (Key, Clan) = Lookup(Clans, ClanId) ?: return Clans + return Clans + (Key to Transform(Clan)) +} + +fun ReduceClanStorage( + State0: ClanStorageStateSlice, + Action: Any, +): ClanStorageStateSlice = when (Action) { + is LoadClanIntoStorage -> State0.copy( + Clans = State0.Clans + (Action.clan.id to Action.clan), + ) + is ClanUnloaded -> State0.copy(Clans = State0.Clans - Action.clanId) + is MinimalRankChanged -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(minRankIndexForAddClan = (Action.rankIndex as? Int) ?: it.minRankIndexForAddClan) + }, + ) + is RecruitmentChanged -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(incomingRequestEnabled = Action.enabled == true) + }, + ) + is AddUsersToNewAccepted -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(newUsers = Plus(it.newUsers, Action.userIds)) + }, + ) + is RemoveUsersFromNewAccepted -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(newUsers = Minus(it.newUsers, Action.userId)) + }, + ) + is AddClanUser -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(users = Plus(it.users, Action.userId)) + }, + ) + is RemoveClanUser -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(users = Minus(it.users, Action.userId)) + }, + ) + is ForeignClanLoaded -> State0.copy(ForeignClan = Action.clan) + is ClearForeignClan -> State0.copy(ForeignClan = null) + is ShowForeignClan -> State0.copy(ForeignClanId = Action.clanId) + is UpdateClan -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy( + name = Action.name ?: it.name, + tag = Action.tag ?: it.tag, + description = (Action.description as? String) ?: it.description, + ) + }, + ) + is BuyAdditionalPlace -> State0.copy( + Clans = Update(State0.Clans, Action.clanId) { + it.copy(maxMembers = it.maxMembers + 1) + }, + ) + 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 13a557a..97b63cb 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 出新状态)。 -// 这里采用同一形状。已迁移的 83 个切片里,82 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 84 个切片里,83 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -85,9 +85,10 @@ // battlesSelect → ReduceBattlesSelect(fA / Mi.vqk,根委派 `Mi.vqk(t, n.apw_1)`) // battleUsers → ReduceBattleUsers(uP / sr.hqs,根委派 `sr.hqs(t, n.hpw_1)`) // challenges → ReduceChallenges(CZ / nh.hsj,根委派 `nh.hsj(t, n.fpv_1)`) +// clansStorage → ReduceClanStorage(lO / me.vr1,根委派 `me.vr1(t, n.rpv_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 6 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 5 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -109,6 +110,7 @@ import App.State.Reducers.ReduceBattleView import App.State.Reducers.ReduceBattleInvite import App.State.Reducers.ReduceCaptcha import App.State.Reducers.ReduceChallenges +import App.State.Reducers.ReduceClanStorage import App.State.Reducers.ReduceClanUserInfo import App.State.Reducers.ReduceClanChat import App.State.Reducers.ReduceContextTutorial @@ -270,6 +272,7 @@ object RootReducer { BattlesSelect = ReduceBattlesSelect(State0.BattlesSelect, Action), BattleUsers = ReduceBattleUsers(State0.BattleUsers, Action), Challenges = ReduceChallenges(State0.Challenges, Action), + ClansStorage = ReduceClanStorage(State0.ClansStorage, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index f1f4a82..72d0cde 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -24,6 +24,7 @@ import App.State.Reducers.CaptchaStateSlice import App.State.Reducers.ChallengesStateSlice import App.State.Reducers.ClanChatStateSlice import App.State.Reducers.ClanUserInfoStateSlice +import App.State.Reducers.ClanStorageStateSlice import App.State.Reducers.ContextTutorialStateSlice import App.State.Reducers.ContractStateSlice import App.State.Reducers.ControlSettingsStateSlice @@ -129,8 +130,8 @@ data class RootState( val ClanChatState: ClanChatStateSlice = ClanChatStateSlice(), /** 原 clan(jN / we.hr0)。未迁移:原样透传。 */ val Clan: Any? = null, - /** 原 clansStorage(lO / me.vr1)。未迁移:原样透传。 */ - val ClansStorage: Any? = null, + /** 原 clansStorage(lO / me.vr1)。已迁移。 */ + val ClansStorage: ClanStorageStateSlice = ClanStorageStateSlice(), /** 原 tipsState(dst / Hw.vth)。已迁移。 */ val TipsState: TipsStateSlice = TipsStateSlice(), /** 原 news(ZX / E_.asd)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 16bcd29..958ac53 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -33,7 +33,7 @@ object RootWiring { 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("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("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")), SliceWiring("tipsState", "spv_1", "dst", "Hw.vth", "Hw.vth(t, n.spv_1)", listOf("UpdateTips")), SliceWiring("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("AddNewsItemAction", "RemoveNewsItemAction", "SetNewsFlag", "SetNewsItemAction", "SetUnreadNewsCounter")), SliceWiring("settings", "upv_1", "Uut", "xw.ltg", "xw.ltg(t, n.upv_1)", listOf("DisableAccountSettings", "SetSettingsCategory")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 b2bab8f..b6270af 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; +// 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; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -826,6 +826,20 @@ import App.State.UpdateStars import App.State.Reducers.ChallengesStateSlice import App.State.Reducers.ReduceChallenges import App.State.Reducers.RefreshBattlePasses +import App.State.ClanInfo +import App.State.ClanUnloaded +import App.State.BuyAdditionalPlace +import App.State.ForeignClanLoaded +import App.State.MinimalRankChanged +import App.State.RecruitmentChanged +import App.State.ShowForeignClan +import App.State.UpdateClan +import App.State.Reducers.AddClanUser +import App.State.Reducers.LoadClanIntoStorage +import App.State.Reducers.ClanStorageStateSlice +import App.State.Reducers.ClearForeignClan +import App.State.Reducers.ReduceClanStorage +import App.State.Reducers.RemoveClanUser import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1253,7 +1267,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 83 个已迁移切片(82 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 84 个已迁移切片(83 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1317,7 +1331,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 83 个已迁移字段,其余 6 个待迁", + "根:迁移表 = 84 个已迁移字段,其余 5 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1341,9 +1355,9 @@ object StateCheck { "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", - "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", + "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", ) && - RootWiring.Unmigrated.size == 6 + RootWiring.Unmigrated.size == 5 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1399,7 +1413,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 83/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 84/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6991,6 +7005,83 @@ object StateCheck { ) } + // 94 切片 reducer:ClansStorage(lO / me.vr1,3 字段;13 个动作) + run { + fun Clan(Id: Any? = 1L): ClanInfo = ClanInfo( + id = Id, name = "n", tag = "t", description = "d", + minRankIndexForAddClan = 1, incomingRequestEnabled = true, + creatorId = 9L, createTime = 0L, users = emptySet(), + gameObject = null, maxCharactersDescription = 100, blocked = false, + reasonForBlocking = "", self = false, maxMembers = 10, + newUsers = emptySet(), + ) + + val S0 = ClanStorageStateSlice() + Check("切片 ClansStorage:默认值完整", S0.Clans.isEmpty() && S0.ForeignClan == null) + val S1 = ReduceClanStorage(S0, LoadClanIntoStorage(Clan())) + Check( + "切片 ClansStorage:ClanLoaded 按 id 入表", + (S1.Clans[1L] as ClanInfo).name == "n" + ) + Check( + "切片 ClansStorage:MinimalRankChanged / RecruitmentChanged 就地 copy", + (ReduceClanStorage(S1, MinimalRankChanged(1L, 5)).Clans[1L] as ClanInfo) + .minRankIndexForAddClan == 5 && + !(ReduceClanStorage(S1, RecruitmentChanged(1L, false)).Clans[1L] as ClanInfo) + .incomingRequestEnabled + ) + Check( + "切片 ClansStorage:成员与待审用户的增删", + (ReduceClanStorage( + ReduceClanStorage(S1, AddClanUser(1L, 7L)), + AddUsersToNewAccepted(1, listOf(8L)), + ).Clans[1L] as ClanInfo).let { + it.users == setOf(7L) && it.newUsers == setOf(8L) + } && + (ReduceClanStorage( + ReduceClanStorage(S1, AddClanUser(1L, 7L)), + RemoveClanUser(1L, 7L), + ).Clans[1L] as ClanInfo).users == emptySet() + ) + Check( + "切片 ClansStorage:UpdateClan 用 null 保留原值", + (ReduceClanStorage(S1, UpdateClan(1L, "n2", null, null)) + .Clans[1L] as ClanInfo).let { it.name == "n2" && it.tag == "t" } + ) + Check( + "切片 ClansStorage:BuyAdditionalPlace 让 maxMembers +1", + (ReduceClanStorage(S1, BuyAdditionalPlace(1L)).Clans[1L] as ClanInfo) + .maxMembers == 11 + ) + Check( + "切片 ClansStorage:外战队三臂", + ReduceClanStorage(S1, ForeignClanLoaded(Clan(2L))).ForeignClan != null && + ReduceClanStorage(S1, ClearForeignClan).ForeignClan == null && + ReduceClanStorage(S1, ShowForeignClan(2L)).ForeignClanId == 2L + ) + Check( + "切片 ClansStorage:ClanUnloaded 删表", + ReduceClanStorage(S1, ClanUnloaded(1L)).Clans.isEmpty() + ) + Check( + "切片 ClansStorage:无关动作不改状态", + ReduceClanStorage(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, LoadClanIntoStorage(Clan())) + Check( + "根:ClanLoaded 只动 clansStorage", + Root1.ClansStorage.Clans.keys == setOf(1L) && + Root1.Challenges == Root0.Challenges + ) + val W = RootWiring.ByField("clansStorage")!! + Check( + "根:clansStorage 委派到 lO / me.vr1 且 13 个动作", + W.SliceClass == "lO" && W.Reducer == "me.vr1" && W.Actions.size == 13 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)