diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 1e124bc..6d42487 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` 动作名。 - - 已迁移切片 **87/89**: + - 已迁移切片 **88/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) @@ -439,12 +439,16 @@ `AddItemNotification(s)` 加入(构造 `ItemNotificationData`);`RemoveItemNotification` 先按有效期过滤, 再删掉匹配谓词的通知;`UpdateItemNotification` 对匹配项应用 handler; `S1` 只做持久化(no-op);`_C` 重载时只保留有效期内的通知。 + - `matchmakingGroup` → `rW` / `NW().is0`(`MatchmakingGroupReducer.kt`):4 字段,13 个分支。 + 组员模型 `MatchmakingGroupMember(userId, isReady, mountItems)`(原包由 DI 服务构造,这里按 userId + 默认值收口); + `AddUser`(与战斗列表同名)改名 `AddGroupMember`;邀请 / 接受 / 拒绝维护 pending 与 members; + `UpdateUserReadiness` / `UpdateMountItems` 按 userId 命中后 copy;选中列表三臂 + `zW` 清空。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 86 个有 reducer 的切片, - `launchParams` 与其余 **2 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 87/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **964 项全绿**,含 89 槽位完整性、86 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 87 个有 reducer 的切片, + `launchParams` 与其余 **1 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 88/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **977 项全绿**,含 89 槽位完整性、87 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 e8fd75c..f83b989 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T14:12:51.294Z", + "generatedAt": "2026-10-24T14:15:45.383Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -93,7 +93,8 @@ "clansStorage", "clan", "chatState", - "itemNotifications" + "itemNotifications", + "matchmakingGroup" ], "AnonymousActions": [ { @@ -1643,6 +1644,18 @@ "Name": "PersistItemNotifications", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "通知列表变化后写回存储(纯 reducer 里 no-op)" + }, + { + "Local": "zW", + "Name": "ResetMatchmakingGroup", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "members / pendingInvitationsUsers 清空" + }, + { + "Local": "lW", + "Name": "AddGroupMember", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "按 userId 去重后追加组员(原名 AddUser,与战斗列表同名类冲突,改名 AddGroupMember)" } ], "Markers": [ @@ -2549,14 +2562,14 @@ "Reducer": "NW().is0", "Delegate": "NW().is0(t, n.jpw_1)", "Actions": [ - "?zW", "AcceptedInviteToGroup", - "AddUser", + "AddGroupMember", "AddUserToSelected", "InitMatchmakingGroup", "Invite", "RejectedInviteToGroup", "RemoveUserToSelected", + "ResetMatchmakingGroup", "SetAllowedFromFriends", "SetSelectedUsers", "UpdateMountItems", @@ -2564,7 +2577,7 @@ "UserRemoved" ], "Default": "new rW()", - "Migrated": false + "Migrated": true }, { "Field": "shop", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 7bcd173..45aa3f5 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -370,6 +370,9 @@ const AnonymousActionNames = new Map([ ['YP', 'SetChatConfig'], // itemNotifications 切片(_1 / I1.dso):S1 没有 toString。 ['S1', 'PersistItemNotifications'], + // matchmakingGroup 切片(rW / PW.is0):zW 没有 toString;lW 的 AddUser 与战斗列表同名类冲突。 + ['zW', 'ResetMatchmakingGroup'], + ['lW', 'AddGroupMember'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -861,6 +864,7 @@ const Migrated = { clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' }, chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' }, itemNotifications: { Type: 'ItemNotificationsStateSlice', Reduce: 'ReduceItemNotifications' }, + matchmakingGroup: { Type: 'MatchmakingGroupStateSlice', Reduce: 'ReduceMatchmakingGroup' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1310,6 +1314,8 @@ const AppSettingsAnonymousEffects = { sN: 'isFirstChannelMessagesLoad=false', YP: 'config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)', S1: '通知列表变化后写回存储(纯 reducer 里 no-op)', + zW: 'members / pendingInvitationsUsers 清空', + lW: '按 userId 去重后追加组员(原名 AddUser,与战斗列表同名类冲突,改名 AddGroupMember)', }; fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MatchmakingGroupReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MatchmakingGroupReducer.kt new file mode 100644 index 0000000..fa5d5d0 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MatchmakingGroupReducer.kt @@ -0,0 +1,146 @@ +// MatchmakingGroupReducer.kt —— 匹配组队切片(对应 module_10032 的 rW 状态 + PW.is0) +// +// 证据链: +// 状态类 rW:4 个字段,toString 名 "MatchmakingGroup(…)"。 +// 默认:members=emptyList()、allowedFromFriends=emptySet()、pendingInvitationsUsers=emptySet()、 +// selectedPlayersSendInvite=emptySet()。 +// 根委派:`NW().is0(t, n.jpw_1)`。 +// reducer(13 个实例臂): +// AW(InitMatchmakingGroup) → members(整体替换) +// cW(SetAllowedFromFriends) → allowedFromFriends +// lW(AddUser) → 按 userId 去重后追加组员 +// fW(UserRemoved) → 按 userId 移除 +// zW(无 toString) → members / pending 清空 +// dW(Invite) → pending += userId +// TW(AcceptedInviteToGroup) → 加入组员、pending -= userId +// EW(RejectedInviteToGroup) → pending -= userId +// bW(UpdateMountItems) → 命中 userId 的组员写 mountItems +// qW(UpdateUserReadiness) → 命中 userId 的组员写 isReady +// yW / $W(AddUserToSelected / RemoveUserToSelected)→ selected += / -= +// xW(SetSelectedUsers) → selected(整体替换) +// +// 组员模型 `MatchmakingGroupMember(userId, isReady, mountItems)`:原包由 DI 服务构造 +// (`hW(r).t2e(...)`),离线重建按「userId + 默认值」收口。 +package App.State.Reducers + +import App.State.AcceptedInviteToGroup +import App.State.AddUserToSelected +import App.State.InitMatchmakingGroup +import App.State.Invite +import App.State.RejectedInviteToGroup +import App.State.RemoveUserToSelected +import App.State.SetAllowedFromFriends +import App.State.SetSelectedUsers +import App.State.UpdateMountItems +import App.State.UpdateUserReadiness +import App.State.UserRemoved + +/* ------------------------------------------------------------------ 模型 */ + +data class MatchmakingGroupMember( + val UserId: Any?, + val IsReady: Boolean = false, + val MountItems: Any? = null, +) + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `zW`:清空组员与待处理邀请。 */ +object ResetMatchmakingGroup + +/* ------------------------------------------------------------------ 动作(原名冲突) */ + +/** 原 `lW`(原名 `AddUser`,与战斗列表的同名类冲突)。 */ +data class AddGroupMember(val user: Any?) + +/* ------------------------------------------------------------------ 状态 */ + +data class MatchmakingGroupStateSlice( + val Members: List = emptyList(), + val AllowedFromFriends: Set = emptySet(), + val PendingInvitationsUsers: Set = emptySet(), + val SelectedPlayersSendInvite: Set = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +private fun AsMember(Value: Any?): MatchmakingGroupMember? = Value as? MatchmakingGroupMember + +@Suppress("UNCHECKED_CAST") +private fun AsSet(Value: Any?): Set = when (Value) { + is Iterable<*> -> Value.toSet() + is Array<*> -> Value.toSet() + else -> emptySet() +} + +private fun MapMember( + Members: List, + UserId: Any?, + Transform: (MatchmakingGroupMember) -> MatchmakingGroupMember, +): List = Members.map { + val Member = AsMember(it) + if (Member != null && Member.UserId == UserId) Transform(Member) else it +} + +private fun AddMember(Members: List, UserId: Any?): List = + Members.filterNot { AsMember(it)?.UserId == UserId } + MatchmakingGroupMember(UserId) + +fun ReduceMatchmakingGroup( + State0: MatchmakingGroupStateSlice, + Action: Any, +): MatchmakingGroupStateSlice = when (Action) { + is InitMatchmakingGroup -> + State0.copy(Members = (Action.members as? List) ?: emptyList()) + is SetAllowedFromFriends -> + State0.copy(AllowedFromFriends = AsSet(Action.allowedFromFriends)) + is AddGroupMember -> { + val Member = AsMember(Action.user) + if (Member == null) State0 + else State0.copy( + Members = State0.Members.filterNot { AsMember(it)?.UserId == Member.UserId } + Member, + ) + } + is UserRemoved -> State0.copy( + Members = State0.Members.filterNot { AsMember(it)?.UserId == Action.userId }, + ) + is ResetMatchmakingGroup -> State0.copy( + Members = emptyList(), + PendingInvitationsUsers = emptySet(), + ) + is Invite -> State0.copy( + PendingInvitationsUsers = State0.PendingInvitationsUsers + Action.userId, + ) + is AcceptedInviteToGroup -> State0.copy( + Members = AddMember(State0.Members, Action.userId), + PendingInvitationsUsers = State0.PendingInvitationsUsers - Action.userId, + ) + is RejectedInviteToGroup -> State0.copy( + PendingInvitationsUsers = State0.PendingInvitationsUsers - Action.userId, + ) + is UpdateMountItems -> { + val Data = Action.mountItemsUserData as? Map ?: emptyMap() + State0.copy( + Members = State0.Members.map { + val Member = AsMember(it) + if (Member != null && Data.containsKey(Member.UserId)) { + Member.copy(MountItems = Data[Member.UserId]) + } else it + }, + ) + } + is UpdateUserReadiness -> State0.copy( + Members = MapMember(State0.Members, Action.userId) { + it.copy(IsReady = Action.isReady == true) + }, + ) + is AddUserToSelected -> State0.copy( + SelectedPlayersSendInvite = State0.SelectedPlayersSendInvite + Action.userId, + ) + is RemoveUserToSelected -> State0.copy( + SelectedPlayersSendInvite = State0.SelectedPlayersSendInvite - Action.usersId, + ) + is SetSelectedUsers -> State0.copy( + SelectedPlayersSendInvite = AsSet(Action.userIds), + ) + 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 26e59bf..b11c7e7 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 出新状态)。 -// 这里采用同一形状。已迁移的 87 个切片里,86 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 88 个切片里,87 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -89,9 +89,10 @@ // clan → ReduceClan(jN / we.hr0,根委派 `we.hr0(t, n.qpv_1)`) // chatState → ReduceChat(LP / $r.wqv,根委派 `$r.wqv(t, n.opv_1)`) // itemNotifications → ReduceItemNotifications(_1 / A1().dso,根委派 `A1().dso(t, n.xpx_1)`) +// matchmakingGroup → ReduceMatchmakingGroup(rW / NW().is0,根委派 `NW().is0(t, n.jpw_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 2 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 1 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -141,6 +142,7 @@ import App.State.Reducers.ReduceLoginByPassword import App.State.Reducers.ReduceLootBoxes import App.State.Reducers.ReduceMainQuest import App.State.Reducers.ReduceMatchmaking +import App.State.Reducers.ReduceMatchmakingGroup import App.State.Reducers.ReduceMaps import App.State.Reducers.ReduceAvailabilityByRank import App.State.Reducers.ReduceLobbyLoader @@ -282,6 +284,7 @@ object RootReducer { Clan = ReduceClan(State0.Clan, Action), ChatState = ReduceChat(State0.ChatState, Action), ItemNotifications = ReduceItemNotifications(State0.ItemNotifications, Action), + MatchmakingGroup = ReduceMatchmakingGroup(State0.MatchmakingGroup, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 1e7ed7d..e3f464e 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -51,6 +51,7 @@ import App.State.Reducers.LootBoxesStateSlice import App.State.Reducers.MainQuestStateSlice import App.State.Reducers.MapsStateSlice import App.State.Reducers.MatchmakingStateSlice +import App.State.Reducers.MatchmakingGroupStateSlice import App.State.Reducers.MigrationStateSlice import App.State.Reducers.ModalScreenStateSlice import App.State.Reducers.ModerationStateSlice @@ -169,8 +170,8 @@ data class RootState( val BattleUsers: BattleUsersStateSlice = BattleUsersStateSlice(), /** 原 suppliesState(wE / $t.fq9)。已迁移。 */ val SuppliesState: SuppliesStateSlice = SuppliesStateSlice(), - /** 原 matchmakingGroup(rW / NW().is0)。未迁移:原样透传。 */ - val MatchmakingGroup: Any? = null, + /** 原 matchmakingGroup(rW / NW().is0)。已迁移。 */ + val MatchmakingGroup: MatchmakingGroupStateSlice = MatchmakingGroupStateSlice(), /** 原 shop(j4 / (inline@shop))。已迁移。 */ val Shop: ShopStateSlice = ShopStateSlice(), /** 原 battlePauseState(dT / NT().mqc)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index d39f428..a192d73 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -51,7 +51,7 @@ object RootWiring { 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("AddBattleUser", "AddBattleUsers", "InitSuspiciousState", "InitUserLevels", "MigrateBattleUserTeams", "RemoveOnlineUser", "RemoveTankInfo", "ResetBattleUsers", "SetGearScore", "SetTankInfo", "SetTankResistance", "SetUserPremium", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")), SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("SetSupplyCount")), - SliceWiring("matchmakingGroup", "jpw_1", "rW", "NW().is0", "NW().is0(t, n.jpw_1)", listOf("?zW", "AcceptedInviteToGroup", "AddUser", "AddUserToSelected", "InitMatchmakingGroup", "Invite", "RejectedInviteToGroup", "RemoveUserToSelected", "SetAllowedFromFriends", "SetSelectedUsers", "UpdateMountItems", "UpdateUserReadiness", "UserRemoved")), + SliceWiring("matchmakingGroup", "jpw_1", "rW", "NW().is0", "NW().is0(t, n.jpw_1)", listOf("AcceptedInviteToGroup", "AddGroupMember", "AddUserToSelected", "InitMatchmakingGroup", "Invite", "RejectedInviteToGroup", "RemoveUserToSelected", "ResetMatchmakingGroup", "SetAllowedFromFriends", "SetSelectedUsers", "UpdateMountItems", "UpdateUserReadiness", "UserRemoved")), SliceWiring("shop", "kpw_1", "j4", "(inline@shop)", "(function (t, n) { var i; if (t instanceof w4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, t.lt6_1); else if (t instanceof Yut) i = n.qt6(cd, cd, cd, cd, !0); else if (t instanceof F5) i = new j4(cd, cd, cd, cd, cd, cd, cd, n.qq3_1); else if (t instanceof h4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.st5_1); else if (t instanceof c4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ut5_1); else if (t instanceof u4) i = n.qt6( cd, cd, cd, cd, cd, cd, …", listOf("?x4", "AddCategoriesWithBonus", "AddItemToOrderProcessing", "BlockBackButton", "BraintreeLoadingInterrupt", "ClearPreviousScreen", "ClearTargetCategory", "ClearTargetProductId", "CurrentPayMode", "DeleteOneTimePurchaseItem", "InitAdditionalDescription", "InitAmountString", "InitCategory", "InitCoinPackage", "InitCompensation", "InitCrystalKitPackage", "InitFeatureItem", "InitGroup", "InitItemViewType", "InitKeysPackage", "InitKitBundle", "InitKitPackage", "InitLootBoxAndPaint", "InitOneTimePurchaseItem", "InitPayModeAction", "InitPaymentModeAsShopItem", "InitPaymentProviderItems", "InitPaymentViewType", "InitRestrictionByPayMode", "InitRewardedVideosBlocks", "InitShopCard", "InitShopCardDiscount", "InitShopCardView", "InitShopItem", "InitShopItemName", "InitSingleItemKit", "InitSpecialKitPackage", "MobilePaymentStateChanged", "PaymentProviderFinishFail", "PaymentProviderFinishSuccess", "PaymentProviderInProgress", "PaymentUrlReceiveFailed", "PaymentUrlReceived", "RemoveDiscountItemForItem", "RemoveItemFromOrderProcessing", "ResetPaymentState", "ResetShop", "SavePreviousScreen", "SaveTargetProductId", "SaveTargetVisitedCategory", "SelectCategory", "SelectCategoryId", "SelectItem", "SetBraintreeToken", "SetCoinPaymentProviderUrl", "SetGoToHomeAfterBuy", "SetInitShopItemId", "SetPurchaseCompleted", "SetStripeIntentParams", "SetupDiscountCategoryByItem", "SetupDiscountItem", "SetupDiscountItemForItem", "ShopCardExpired", "ShopCardRemoved", "ShopCardSold", "ShopLoaded", "ToggleShopActivity", "TryToBuyOneTimePurchase", "UnblockBackButton")), SliceWiring("battlePauseState", "lpw_1", "dT", "NT().mqc", "NT().mqc(t, n.lpw_1)", listOf("CancelAutoPause", "OnPauseMode", "ResetBattlePause", "SetIdleKickPeriod")), SliceWiring("battleViewState", "mpw_1", "DT", "sI.jqd", "(null == \$n && new sI(), \$n).jqd(t, n.mpw_1)", listOf("ClearInvisibleElements", "DisableBattleInput", "DisableBattleMenu", "DisableBattleStatistics", "EnableBattleInput", "EnableBattleMenu", "EnableBattleStatistics", "EnterBackground", "HideBattleView", "LeaveBackground", "ResetBattleView", "SetElementsVisibility", "SetVisibility", "ShowBattleView")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications", "matchmakingGroup") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 c08fc87..abb43ce 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -19,7 +19,7 @@ // System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction / // Moderation / MigrationState / Register / UserReports / // Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename / -// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications / MatchmakingGroup; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -912,6 +912,22 @@ import App.State.UpdateItemNotification import App.State.Reducers.ItemNotificationsStateSlice import App.State.Reducers.PersistItemNotifications import App.State.Reducers.ReduceItemNotifications +import App.State.AcceptedInviteToGroup +import App.State.AddUserToSelected +import App.State.InitMatchmakingGroup +import App.State.Invite +import App.State.RejectedInviteToGroup +import App.State.RemoveUserToSelected +import App.State.SetAllowedFromFriends +import App.State.SetSelectedUsers +import App.State.UpdateMountItems +import App.State.UpdateUserReadiness +import App.State.UserRemoved +import App.State.Reducers.AddGroupMember +import App.State.Reducers.MatchmakingGroupMember +import App.State.Reducers.MatchmakingGroupStateSlice +import App.State.Reducers.ReduceMatchmakingGroup +import App.State.Reducers.ResetMatchmakingGroup import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1339,7 +1355,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 87 个已迁移切片(86 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 88 个已迁移切片(87 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1403,7 +1419,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 87 个已迁移字段,其余 2 个待迁", + "根:迁移表 = 88 个已迁移字段,其余 1 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1428,8 +1444,9 @@ object StateCheck { "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications", + "matchmakingGroup", ) && - RootWiring.Unmigrated.size == 2 + RootWiring.Unmigrated.size == 1 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1485,7 +1502,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 87/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 88/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -7431,6 +7448,99 @@ object StateCheck { ) } + // 98 切片 reducer:MatchmakingGroup(rW / NW().is0,4 字段;13 个动作) + run { + fun Member(Id: Any?, Ready: Boolean = false): MatchmakingGroupMember = + MatchmakingGroupMember(UserId = Id, IsReady = Ready) + + val S0 = MatchmakingGroupStateSlice() + Check( + "切片 MatchmakingGroup:默认值完整", + S0.Members.isEmpty() && S0.AllowedFromFriends.isEmpty() && + S0.PendingInvitationsUsers.isEmpty() && + S0.SelectedPlayersSendInvite.isEmpty() + ) + val S1 = ReduceMatchmakingGroup(S0, InitMatchmakingGroup(listOf(Member(1L)))) + Check("切片 MatchmakingGroup:InitMatchmakingGroup 整体替换", S1.Members.size == 1) + val S2 = ReduceMatchmakingGroup(S1, AddGroupMember(Member(2L))) + Check( + "切片 MatchmakingGroup:AddGroupMember 追加组员", + S2.Members.size == 2 + ) + Check( + "切片 MatchmakingGroup:AddGroupMember 按 userId 去重", + ReduceMatchmakingGroup(S2, AddGroupMember(Member(2L, true))).let { + it.Members.size == 2 && + (it.Members.last() as MatchmakingGroupMember).IsReady + } + ) + Check( + "切片 MatchmakingGroup:UserRemoved 删组员", + ReduceMatchmakingGroup(S2, UserRemoved(1L)).Members.size == 1 + ) + Check( + "切片 MatchmakingGroup:邀请 / 接受 / 拒绝", + ReduceMatchmakingGroup(S2, Invite(3L)) + .PendingInvitationsUsers == setOf(3L) && + ReduceMatchmakingGroup( + ReduceMatchmakingGroup(S2, Invite(3L)), + AcceptedInviteToGroup(3L), + ).let { + it.PendingInvitationsUsers.isEmpty() && + it.Members.any { M -> (M as MatchmakingGroupMember).UserId == 3L } + } && + ReduceMatchmakingGroup( + ReduceMatchmakingGroup(S2, Invite(4L)), + RejectedInviteToGroup(4L, "no"), + ).PendingInvitationsUsers.isEmpty() + ) + Check( + "切片 MatchmakingGroup:UpdateUserReadiness 命中 copy", + (ReduceMatchmakingGroup(S2, UpdateUserReadiness(1L, true)) + .Members.first() as MatchmakingGroupMember).IsReady + ) + Check( + "切片 MatchmakingGroup:UpdateMountItems 按 userId 写挂件", + (ReduceMatchmakingGroup(S2, UpdateMountItems(mapOf(1L to "mount"))) + .Members.first() as MatchmakingGroupMember).MountItems == "mount" + ) + Check( + "切片 MatchmakingGroup:选中列表三臂", + ReduceMatchmakingGroup(S2, AddUserToSelected(5L)) + .SelectedPlayersSendInvite == setOf(5L) && + ReduceMatchmakingGroup( + ReduceMatchmakingGroup(S2, AddUserToSelected(5L)), + RemoveUserToSelected(5L), + ).SelectedPlayersSendInvite.isEmpty() && + ReduceMatchmakingGroup(S2, SetSelectedUsers(listOf(6L, 7L))) + .SelectedPlayersSendInvite == setOf(6L, 7L) + ) + Check( + "切片 MatchmakingGroup:SetAllowedFromFriends / Reset", + ReduceMatchmakingGroup( + ReduceMatchmakingGroup(S2, SetAllowedFromFriends(listOf(1L))), + ResetMatchmakingGroup, + ).let { it.AllowedFromFriends == setOf(1L) && it.Members.isEmpty() } + ) + Check( + "切片 MatchmakingGroup:无关动作不改状态", + ReduceMatchmakingGroup(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, Invite(9L)) + Check( + "根:Invite 只动 matchmakingGroup", + Root1.MatchmakingGroup.PendingInvitationsUsers == setOf(9L) && + Root1.ItemNotifications == Root0.ItemNotifications + ) + val W = RootWiring.ByField("matchmakingGroup")!! + Check( + "根:matchmakingGroup 委派到 rW / NW().is0 且 13 个动作", + W.SliceClass == "rW" && W.Reducer == "NW().is0" && W.Actions.size == 13 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)