diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index c81adeb..c271ff7 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` 动作名。 - - 已迁移切片 **84/89**: + - 已迁移切片 **85/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) @@ -425,12 +425,16 @@ `vO`(原名 `ClanLoaded`,与 clan 切片的 `isLoaded` 同名)改名 `LoadClanIntoStorage`; `dO`/`gO`(原名 `AddUser`/`RemoveUser`,与战斗列表同名)改名 `AddClanUser`/`RemoveClanUser`; 查找时兼容 Int/Long 两种 clanId。 + - `clan` → `jN` / `we.hr0`(`ClanReducer.kt`):36 字段,43 个分支。 + 成员 / 待审用户 / 人员 / 外战队 / 新申请各有增删两臂;校验状态 4 个 setter + 2 个复位; + `ClanInfoLoaded` 写 6 个身份字段(复用 `ClanInfo`);`UpdateHasClan(false)` 整片复位; + 通知计数 3 个直接写 + 2 个已读 + 计数 ±1;其余为中间态三元组、封禁、弹窗与改名价格。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 83 个有 reducer 的切片, - `launchParams` 与其余 **5 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 84/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **924 项全绿**,含 89 槽位完整性、83 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 84 个有 reducer 的切片, + `launchParams` 与其余 **4 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 85/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **939 项全绿**,含 89 槽位完整性、84 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 c9862b8..412997c 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T13:56:08.668Z", + "generatedAt": "2026-10-24T14:00:39.799Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -90,7 +90,8 @@ "battlesSelect", "battleUsers", "challenges", - "clansStorage" + "clansStorage", + "clan" ], "AnonymousActions": [ { @@ -1514,6 +1515,54 @@ "Name": "RemoveClanUser", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "clan.users -= userId(原名 RemoveUser,同上改名 RemoveClanUser)" + }, + { + "Local": "iM", + "Name": "ResetClanUserValidation", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "clanUserValidation 复位(`n.iqx(…, sO())`)" + }, + { + "Local": "FM", + "Name": "ResetClanNameTagValidation", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "clanNameNameValidation / clanTagNameValidation 复位(`oO()`)" + }, + { + "Local": "IM", + "Name": "UpdateIntermediateMinRank", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "intermediateMinRankIndex(`n.iqx(cd, cd, cd, cd, cd, cd, cd, t.lr0_1)`)" + }, + { + "Local": "RM", + "Name": "UpdateIntermediateRequests", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "intermediateRequestsEnabled(`n.iqx(cd, cd, cd, cd, cd, cd, t.kr0_1)`)" + }, + { + "Local": "PM", + "Name": "UpdateIntermediateDescription", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "intermediateDescription(`n.iqx(cd, cd, cd, cd, cd, cd, cd, cd, t.jr0_1)`)" + }, + { + "Local": "VM", + "Name": "SetClanNotificationsCount", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "notificationsCount(`t.ir0_1`)" + }, + { + "Local": "KM", + "Name": "IncrementClanNotifications", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "notificationsCount + 1(`(n.bqx_1 + 1) | 0`)" + }, + { + "Local": "YM", + "Name": "DecrementClanNotifications", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "notificationsCount - 1,下限 0" } ], "Markers": [ @@ -2008,14 +2057,6 @@ "Reducer": "we.hr0", "Delegate": "we.hr0(t, n.qpv_1)", "Actions": [ - "?FM", - "?IM", - "?KM", - "?PM", - "?RM", - "?VM", - "?YM", - "?iM", "AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", @@ -2027,9 +2068,11 @@ "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", + "DecrementClanNotifications", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", + "IncrementClanNotifications", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", @@ -2042,18 +2085,24 @@ "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", + "ResetClanNameTagValidation", + "ResetClanUserValidation", "SetClanEditPrice", + "SetClanNotificationsCount", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", + "UpdateIntermediateDescription", + "UpdateIntermediateMinRank", + "UpdateIntermediateRequests", "UpdateTimeToJoinRestriction", "UpdateValidation" ], "Default": "new jN()", - "Migrated": false + "Migrated": true }, { "Field": "clansStorage", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index ca06fb2..2e9a57d 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -345,6 +345,15 @@ const AnonymousActionNames = new Map([ ['gO', 'RemoveClanUser'], // vO 的 toString 名 ClanLoaded 与 clan 切片的 isLoaded 动作同名冲突,改名避免字段错配。 ['vO', 'LoadClanIntoStorage'], + // clan 切片(jN / we.hr0):8 个匿名类。 + ['iM', 'ResetClanUserValidation'], + ['FM', 'ResetClanNameTagValidation'], + ['IM', 'UpdateIntermediateMinRank'], + ['RM', 'UpdateIntermediateRequests'], + ['PM', 'UpdateIntermediateDescription'], + ['VM', 'SetClanNotificationsCount'], + ['KM', 'IncrementClanNotifications'], + ['YM', 'DecrementClanNotifications'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -833,6 +842,7 @@ const Migrated = { battleUsers: { Type: 'BattleUsersStateSlice', Reduce: 'ReduceBattleUsers' }, challenges: { Type: 'ChallengesStateSlice', Reduce: 'ReduceChallenges' }, clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' }, + clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1261,6 +1271,14 @@ const AppSettingsAnonymousEffects = { vO: 'clans[clan.id]=clan(原名 ClanLoaded,与 clan 切片同名类冲突,改名 LoadClanIntoStorage)', dO: 'clan.users += userId(原名 AddUser,与战斗列表同名类冲突,改名 AddClanUser)', gO: 'clan.users -= userId(原名 RemoveUser,同上改名 RemoveClanUser)', + iM: 'clanUserValidation 复位(`n.iqx(…, sO())`)', + FM: 'clanNameNameValidation / clanTagNameValidation 复位(`oO()`)', + IM: 'intermediateMinRankIndex(`n.iqx(cd, cd, cd, cd, cd, cd, cd, t.lr0_1)`)', + RM: 'intermediateRequestsEnabled(`n.iqx(cd, cd, cd, cd, cd, cd, t.kr0_1)`)', + PM: 'intermediateDescription(`n.iqx(cd, cd, cd, cd, cd, cd, cd, cd, t.jr0_1)`)', + VM: 'notificationsCount(`t.ir0_1`)', + KM: 'notificationsCount + 1(`(n.bqx_1 + 1) | 0`)', + YM: 'notificationsCount - 1,下限 0', }; fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanReducer.kt new file mode 100644 index 0000000..56736bd --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanReducer.kt @@ -0,0 +1,199 @@ +// ClanReducer.kt —— 战队切片(对应 module_10032 的 jN 状态 + we.hr0) +// +// 证据链: +// 状态类 jN:36 个字段,toString 名 "Clan(…)";copy 是 iqx(36 字段 + 回调)。 +// 根委派:`we.hr0(t, n.qpv_1)`。 +// reducer(43 个实例臂)——绝大多数是 `iqx` 的单个位置参数: +// 成员/待审:DN/LN(accepted)BN/FN(acceptedNew) +// 校验:iM 复位、rM/uM/sM/oM 直接写、FM 复位 name/tag +// 载入:HN(ClanInfoLoaded 写 id/creatorId/name/createTime/description/tag)、fM(isLoaded) +// 人员:WN/VN/KN/YN(incoming/outgoing) +// 外战队:vM/pM/wM/dM/gM/mM +// 中间态:IM/RM/PM(minRank / requests / description) +// 通知计数:VM/KM/YM/UN/WM/XM/JM/ZM +// 其他:GN 最小创建等级、ON 成员统计合并、CM 入队限制时间、 +// lM hasClan(false 时整片复位)、NM 封禁、GM/HM 新申请、QM 弹窗、tO 改名价格 +// +// `ClanInfoLoaded.info` 复用生成物 `ClanInfo`;`ON` 的 `jm` 是 putAll(合并)。 +package App.State.Reducers + +import App.State.AddIncomingClan +import App.State.AddNewIncoming +import App.State.AddNewIncomingClan +import App.State.AddOutgoingClan +import App.State.AddUsersToAccepted +import App.State.AddUsersToNewAccepted +import App.State.AcceptedUserViewed +import App.State.ClanBanned +import App.State.ClanInfo +import App.State.ClanInfoLoaded +import App.State.ClanLoaded +import App.State.ClanModalVisible +import App.State.IncomingAdded +import App.State.IncomingRemoved +import App.State.IncomingUserViewed +import App.State.MembersFetched +import App.State.NewAcceptedUserNotification +import App.State.NewClanInviteNotification +import App.State.NewIncomingUserNotification +import App.State.OutgoingAdded +import App.State.OutgoingRemoved +import App.State.RemoveIncomingClan +import App.State.RemoveNewIncoming +import App.State.RemoveNewIncomingClan +import App.State.RemoveOutgoingClan +import App.State.RemoveUserFromAccepted +import App.State.RemoveUsersFromNewAccepted +import App.State.SetClanEditPrice +import App.State.SetMinRankForCreate +import App.State.UpdateClanLicense +import App.State.UpdateClanNameValidation +import App.State.UpdateClanSearchValidation +import App.State.UpdateClanTagValidation +import App.State.UpdateHasClan +import App.State.UpdateTimeToJoinRestriction +import App.State.UpdateValidation + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `iM`:复位用户名校验状态。 */ +object ResetClanUserValidation + +/** 原 `FM`:复位战队名 / 标签校验状态。 */ +object ResetClanNameTagValidation + +/** 原 `IM`:写中间态最小等级。 */ +data class UpdateIntermediateMinRank(val rankIndex: Any?) + +/** 原 `RM`:写「可自由加入」开关。 */ +data class UpdateIntermediateRequests(val enabled: Any?) + +/** 原 `PM`:写中间态说明。 */ +data class UpdateIntermediateDescription(val description: Any?) + +/** 原 `VM`:直接写通知总数。 */ +data class SetClanNotificationsCount(val count: Int) + +/** 原 `KM` / `YM`:通知总数 +1 / -1。 */ +object IncrementClanNotifications +object DecrementClanNotifications + +/* ------------------------------------------------------------------ 状态 */ + +data class ClanStateSlice( + val CurrentClanId: Any? = null, + val CreatorId: Any? = null, + val Name: Any? = null, + val CreateTime: Any? = null, + val Description: Any? = null, + val Tag: Any? = null, + val IntermediateRequestsEnabled: Any? = null, + val IntermediateMinRankIndex: Any? = null, + val IntermediateDescription: Any? = null, + val RulesConfirmed: Boolean = false, + val HasLicense: Boolean = false, + val HasClan: Boolean = false, + val IsLoaded: Any? = null, + val IncomingUsers: Set = emptySet(), + val OutgoingUsers: Set = emptySet(), + val ClanUserValidation: Any? = null, + val ClanNameNameValidation: Any? = null, + val ClanTagNameValidation: Any? = null, + val ClanSearchValidation: Any? = null, + val ClanAcceptedMembers: Set = emptySet(), + val ClanAcceptedMembersNew: Set = emptySet(), + val MinRankForCreateClan: Int = 0, + val ClanMembersStatistics: Map = emptyMap(), + val OutgoingClans: Set = emptySet(), + val IncomingClans: Set = emptySet(), + val RestrictionToJoinInClan: Any? = null, + val Banned: Boolean = false, + val BanReason: Any? = null, + val NewIncomingClans: Set = emptySet(), + val NewIncoming: Set = emptySet(), + val NotificationsCount: Int = 0, + val IncomingUserNotificationCount: Int = 0, + val AcceptedUserNotificationCount: Int = 0, + val ClanInviteNotificationCount: Int = 0, + val ClanModalVisible: Boolean = false, + val ClanEditPrice: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +private fun Plus(Target: Set, Value: Any?): Set = + if (Value is Iterable<*>) Target + Value.toSet() else Target + Value + +private fun Minus(Target: Set, Value: Any?): Set = + if (Value is Iterable<*>) Target - Value.toSet() else Target - Value + +fun ReduceClan( + State0: ClanStateSlice, + Action: Any, +): ClanStateSlice = when (Action) { + is AddUsersToAccepted -> State0.copy(ClanAcceptedMembers = Plus(State0.ClanAcceptedMembers, Action.usersId)) + is RemoveUserFromAccepted -> State0.copy(ClanAcceptedMembers = Minus(State0.ClanAcceptedMembers, Action.userId)) + is AddUsersToNewAccepted -> State0.copy(ClanAcceptedMembersNew = Plus(State0.ClanAcceptedMembersNew, Action.userIds)) + is RemoveUsersFromNewAccepted -> State0.copy(ClanAcceptedMembersNew = Minus(State0.ClanAcceptedMembersNew, Action.userId)) + is SetMinRankForCreate -> State0.copy(MinRankForCreateClan = (Action.rankIndex as? Number)?.toInt() ?: State0.MinRankForCreateClan) + is MembersFetched -> State0.copy( + ClanMembersStatistics = + State0.ClanMembersStatistics + ((Action.users as? Map) ?: emptyMap()), + ) + is ClanInfoLoaded -> { + val Info = Action.info as? ClanInfo + if (Info == null) State0 + else State0.copy( + CurrentClanId = Info.id, + CreatorId = Info.creatorId, + Name = Info.name, + CreateTime = Info.createTime, + Description = Info.description, + Tag = Info.tag, + ) + } + is IncomingAdded -> State0.copy(IncomingUsers = Plus(State0.IncomingUsers, Action.incomingUsers)) + is IncomingRemoved -> State0.copy(IncomingUsers = Minus(State0.IncomingUsers, Action.incomingUsers)) + is OutgoingAdded -> State0.copy(OutgoingUsers = Plus(State0.OutgoingUsers, Action.outgoingUsers)) + is OutgoingRemoved -> State0.copy(OutgoingUsers = Minus(State0.OutgoingUsers, Action.outgoingUser)) + is ResetClanUserValidation -> State0.copy(ClanUserValidation = null) + is UpdateValidation -> State0.copy(ClanUserValidation = Action.state) + is UpdateClanNameValidation -> State0.copy(ClanNameNameValidation = Action.state) + is UpdateClanTagValidation -> State0.copy(ClanTagNameValidation = Action.state) + is UpdateClanSearchValidation -> State0.copy(ClanSearchValidation = Action.state) + is UpdateClanLicense -> State0.copy(HasLicense = Action.hasLicense) + is UpdateHasClan -> if (Action.hasClan) State0.copy(HasClan = true) else ClanStateSlice() + is ClanLoaded -> State0.copy(IsLoaded = Action.isLoaded) + is AddOutgoingClan -> State0.copy(OutgoingClans = Plus(State0.OutgoingClans, Action.clanId)) + is RemoveOutgoingClan -> State0.copy(OutgoingClans = Minus(State0.OutgoingClans, Action.clanId)) + is AddIncomingClan -> State0.copy(IncomingClans = Plus(State0.IncomingClans, Action.clanIds)) + is RemoveIncomingClan -> State0.copy(IncomingClans = Minus(State0.IncomingClans, Action.clanId)) + is AddNewIncomingClan -> State0.copy(NewIncomingClans = Plus(State0.NewIncomingClans, Action.clanIds)) + is RemoveNewIncomingClan -> State0.copy(NewIncomingClans = Minus(State0.NewIncomingClans, Action.clanId)) + is UpdateTimeToJoinRestriction -> + State0.copy(RestrictionToJoinInClan = Action.restrictionTimeJoinClanInSec) + is UpdateIntermediateMinRank -> State0.copy(IntermediateMinRankIndex = Action.rankIndex) + is UpdateIntermediateRequests -> State0.copy(IntermediateRequestsEnabled = Action.enabled) + is UpdateIntermediateDescription -> State0.copy(IntermediateDescription = Action.description) + is ClanBanned -> State0.copy(Banned = true, BanReason = Action.reason) + is ResetClanNameTagValidation -> State0.copy( + ClanNameNameValidation = null, + ClanTagNameValidation = null, + ) + is AddNewIncoming -> State0.copy(NewIncoming = Plus(State0.NewIncoming, Action.newIncoming)) + is RemoveNewIncoming -> State0.copy(NewIncoming = Minus(State0.NewIncoming, Action.newIncoming)) + is SetClanNotificationsCount -> State0.copy(NotificationsCount = Action.count) + is IncrementClanNotifications -> State0.copy(NotificationsCount = State0.NotificationsCount + 1) + is DecrementClanNotifications -> + State0.copy(NotificationsCount = (State0.NotificationsCount - 1).coerceAtLeast(0)) + is AcceptedUserViewed -> + State0.copy(AcceptedUserNotificationCount = State0.AcceptedUserNotificationCount - 1) + is IncomingUserViewed -> + State0.copy(IncomingUserNotificationCount = State0.IncomingUserNotificationCount - 1) + is NewIncomingUserNotification -> State0.copy(IncomingUserNotificationCount = Action.count) + is NewAcceptedUserNotification -> State0.copy(AcceptedUserNotificationCount = Action.count) + is NewClanInviteNotification -> State0.copy(ClanInviteNotificationCount = Action.count) + is ClanModalVisible -> State0.copy(ClanModalVisible = Action.visible) + is SetClanEditPrice -> State0.copy(ClanEditPrice = Action.clanEditPrice) + 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 97b63cb..3451caa 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 出新状态)。 -// 这里采用同一形状。已迁移的 84 个切片里,83 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 85 个切片里,84 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -86,9 +86,10 @@ // 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)`) +// clan → ReduceClan(jN / we.hr0,根委派 `we.hr0(t, n.qpv_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 5 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 4 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -110,6 +111,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.ReduceClan import App.State.Reducers.ReduceClanStorage import App.State.Reducers.ReduceClanUserInfo import App.State.Reducers.ReduceClanChat @@ -273,6 +275,7 @@ object RootReducer { BattleUsers = ReduceBattleUsers(State0.BattleUsers, Action), Challenges = ReduceChallenges(State0.Challenges, Action), ClansStorage = ReduceClanStorage(State0.ClansStorage, Action), + Clan = ReduceClan(State0.Clan, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 72d0cde..02317d7 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.ClanStateSlice import App.State.Reducers.ClanChatStateSlice import App.State.Reducers.ClanUserInfoStateSlice import App.State.Reducers.ClanStorageStateSlice @@ -128,8 +129,8 @@ data class RootState( val ChatState: Any? = null, /** 原 clanChatState(qnt / yv.qtg)。已迁移。 */ val ClanChatState: ClanChatStateSlice = ClanChatStateSlice(), - /** 原 clan(jN / we.hr0)。未迁移:原样透传。 */ - val Clan: Any? = null, + /** 原 clan(jN / we.hr0)。已迁移。 */ + val Clan: ClanStateSlice = ClanStateSlice(), /** 原 clansStorage(lO / me.vr1)。已迁移。 */ val ClansStorage: ClanStorageStateSlice = ClanStorageStateSlice(), /** 原 tipsState(dst / Hw.vth)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 958ac53..810da70 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -32,7 +32,7 @@ object RootWiring { 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("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("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")), 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")), @@ -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") + 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 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 b6270af..0596e0e 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; +// 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; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -826,6 +826,7 @@ import App.State.UpdateStars import App.State.Reducers.ChallengesStateSlice import App.State.Reducers.ReduceChallenges import App.State.Reducers.RefreshBattlePasses +import App.State.ClanEditPriceCC import App.State.ClanInfo import App.State.ClanUnloaded import App.State.BuyAdditionalPlace @@ -840,6 +841,44 @@ import App.State.Reducers.ClanStorageStateSlice import App.State.Reducers.ClearForeignClan import App.State.Reducers.ReduceClanStorage import App.State.Reducers.RemoveClanUser +import App.State.AddIncomingClan +import App.State.AddNewIncoming +import App.State.AddNewIncomingClan +import App.State.AddOutgoingClan +import App.State.AcceptedUserViewed +import App.State.ClanBanned +import App.State.ClanModalVisible +import App.State.IncomingAdded +import App.State.IncomingRemoved +import App.State.IncomingUserViewed +import App.State.MembersFetched +import App.State.NewAcceptedUserNotification +import App.State.NewClanInviteNotification +import App.State.NewIncomingUserNotification +import App.State.OutgoingAdded +import App.State.OutgoingRemoved +import App.State.ClanInfoLoaded +import App.State.RemoveIncomingClan +import App.State.RemoveNewIncoming +import App.State.RemoveNewIncomingClan +import App.State.RemoveOutgoingClan +import App.State.SetClanEditPrice +import App.State.SetMinRankForCreate +import App.State.UpdateClanLicense +import App.State.UpdateClanNameValidation +import App.State.UpdateHasClan +import App.State.UpdateTimeToJoinRestriction +import App.State.UpdateValidation +import App.State.Reducers.ClanStateSlice +import App.State.Reducers.DecrementClanNotifications +import App.State.Reducers.IncrementClanNotifications +import App.State.Reducers.ReduceClan +import App.State.Reducers.ResetClanNameTagValidation +import App.State.Reducers.ResetClanUserValidation +import App.State.Reducers.SetClanNotificationsCount +import App.State.Reducers.UpdateIntermediateDescription +import App.State.Reducers.UpdateIntermediateMinRank +import App.State.Reducers.UpdateIntermediateRequests import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1267,7 +1306,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 84 个已迁移切片(83 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 85 个已迁移切片(84 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1331,7 +1370,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 84 个已迁移字段,其余 5 个待迁", + "根:迁移表 = 85 个已迁移字段,其余 4 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1355,9 +1394,9 @@ object StateCheck { "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", - "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", + "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", ) && - RootWiring.Unmigrated.size == 5 + RootWiring.Unmigrated.size == 4 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1413,7 +1452,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 84/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 85/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -7082,6 +7121,113 @@ object StateCheck { ) } + // 95 切片 reducer:Clan(jN / we.hr0,36 字段;43 个动作) + run { + val S0 = ClanStateSlice() + Check( + "切片 Clan:默认值完整", + S0.CurrentClanId == null && !S0.HasClan && !S0.HasLicense && + S0.ClanAcceptedMembers.isEmpty() + ) + val S1 = ReduceClan(S0, SetMinRankForCreate(5)) + Check("切片 Clan:SetMinRankForCreate 写最小等级", S1.MinRankForCreateClan == 5) + val S2 = ReduceClan(S1, AddUsersToAccepted(listOf(1L, 2L))) + Check( + "切片 Clan:AddUsersToAccepted 批量入集合", + S2.ClanAcceptedMembers == setOf(1L, 2L) + ) + Check( + "切片 Clan:RemoveUserFromAccepted 删单值", + ReduceClan(S2, RemoveUserFromAccepted(1L)) + .ClanAcceptedMembers == setOf(2L) + ) + val Info = ClanInfo( + id = 7L, 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 S3 = ReduceClan(S2, ClanInfoLoaded(Info)) + Check( + "切片 Clan:ClanInfoLoaded 写 6 个身份字段", + S3.CurrentClanId == 7L && S3.CreatorId == 9L && S3.Name == "n" && + S3.Tag == "t" && S3.Description == "d" + ) + Check( + "切片 Clan:人员 / 战队列表四臂", + ReduceClan(S3, IncomingAdded(listOf(3L))).IncomingUsers == setOf(3L) && + ReduceClan(S3, OutgoingAdded(listOf(4L))).OutgoingUsers == setOf(4L) && + ReduceClan(S3, AddOutgoingClan(11L)).OutgoingClans == setOf(11L) && + ReduceClan(S3, AddNewIncomingClan(listOf(12L))) + .NewIncomingClans == setOf(12L) + ) + Check( + "切片 Clan:中间态三臂", + ReduceClan(S3, UpdateIntermediateMinRank(2)).IntermediateMinRankIndex == 2 && + ReduceClan(S3, UpdateIntermediateRequests(true)) + .IntermediateRequestsEnabled == true && + ReduceClan(S3, UpdateIntermediateDescription("x")) + .IntermediateDescription == "x" + ) + Check( + "切片 Clan:校验状态写 / 复位", + ReduceClan(S3, UpdateValidation("bad")).ClanUserValidation == "bad" && + ReduceClan(ReduceClan(S3, UpdateValidation("bad")), ResetClanUserValidation) + .ClanUserValidation == null && + ReduceClan(ReduceClan(S3, UpdateClanNameValidation("bad")), + ResetClanNameTagValidation).ClanNameNameValidation == null + ) + Check( + "切片 Clan:HasClan=false 整片复位、true 只置位", + ReduceClan( + ReduceClan(S3, UpdateClanLicense(true)), + UpdateHasClan(false), + ) == ClanStateSlice() && + ReduceClan(S3, UpdateHasClan(true)).HasClan + ) + Check( + "切片 Clan:封禁 / 弹窗 / 改名价格 / 入队限制", + ReduceClan(S3, ClanBanned("why")).let { it.Banned && it.BanReason == "why" } && + ReduceClan(S3, ClanModalVisible(true)).ClanModalVisible && + ReduceClan(S3, SetClanEditPrice(ClanEditPriceCC(1, 2, null, 3, 4, 5))) + .ClanEditPrice == ClanEditPriceCC(1, 2, null, 3, 4, 5) && + ReduceClan(S3, UpdateTimeToJoinRestriction(60)) + .RestrictionToJoinInClan == 60 + ) + Check( + "切片 Clan:通知计数 +1 / -1 / 直接写", + ReduceClan(S3, IncrementClanNotifications).NotificationsCount == 1 && + ReduceClan(ReduceClan(S3, IncrementClanNotifications), + DecrementClanNotifications).NotificationsCount == 0 && + ReduceClan(S3, SetClanNotificationsCount(4)).NotificationsCount == 4 + ) + Check( + "切片 Clan:三个通知徽标与两个已读动作", + ReduceClan(S3, NewIncomingUserNotification(2)) + .IncomingUserNotificationCount == 2 && + ReduceClan(ReduceClan(S3, NewAcceptedUserNotification(3)), + AcceptedUserViewed(1L)).AcceptedUserNotificationCount == 2 && + ReduceClan(ReduceClan(S3, NewIncomingUserNotification(2)), + IncomingUserViewed(1L)).IncomingUserNotificationCount == 1 && + ReduceClan(S3, NewClanInviteNotification(1)).ClanInviteNotificationCount == 1 + ) + Check("切片 Clan:无关动作不改状态", ReduceClan(S3, AddFrags(1)) == S3) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetMinRankForCreate(3)) + Check( + "根:SetMinRankForCreate 只动 clan", + Root1.Clan.MinRankForCreateClan == 3 && Root1.ClansStorage == Root0.ClansStorage + ) + val W = RootWiring.ByField("clan")!! + Check( + "根:clan 委派到 jN / we.hr0 且 43 个动作", + W.SliceClass == "jN" && W.Reducer == "we.hr0" && W.Actions.size == 43 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)