feat(state): 迁移 matchmakingGroup

This commit is contained in:
TermiNexus committed 2026-10-24 22:18:57 +08:00
1 parent 6f68afa678
commit 8b124388ec
8 files changed
+304 -21

No files matched your search

+9 -5
View File
@@ -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<String, String>`,不新增 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@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
+18 -5
View File
@@ -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",
+6
View File
@@ -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'),
@@ -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<Any?> = emptyList(),
val AllowedFromFriends: Set<Any?> = emptySet(),
val PendingInvitationsUsers: Set<Any?> = emptySet(),
val SelectedPlayersSendInvite: Set<Any?> = emptySet(),
)
/* ------------------------------------------------------------------ reducer */
private fun AsMember(Value: Any?): MatchmakingGroupMember? = Value as? MatchmakingGroupMember
@Suppress("UNCHECKED_CAST")
private fun AsSet(Value: Any?): Set<Any?> = when (Value) {
is Iterable<*> -> Value.toSet()
is Array<*> -> Value.toSet()
else -> emptySet()
}
private fun MapMember(
Members: List<Any?>,
UserId: Any?,
Transform: (MatchmakingGroupMember) -> MatchmakingGroupMember,
): List<Any?> = Members.map {
val Member = AsMember(it)
if (Member != null && Member.UserId == UserId) Transform(Member) else it
}
private fun AddMember(Members: List<Any?>, UserId: Any?): List<Any?> =
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<Any?>) ?: 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<Any?, Any?> ?: 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
}
@@ -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),
)
}
@@ -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)。已迁移。 */
@@ -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<String> = 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<String> = 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<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -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<Any?>(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<Any?, Any?>(1L to "mount")))
.Members.first() as MatchmakingGroupMember).MountItems == "mount"
)
Check(
"切片 MatchmakingGroup:选中列表三臂",
ReduceMatchmakingGroup(S2, AddUserToSelected(5L))
.SelectedPlayersSendInvite == setOf<Any?>(5L) &&
ReduceMatchmakingGroup(
ReduceMatchmakingGroup(S2, AddUserToSelected(5L)),
RemoveUserToSelected(5L),
).SelectedPlayersSendInvite.isEmpty() &&
ReduceMatchmakingGroup(S2, SetSelectedUsers(listOf(6L, 7L)))
.SelectedPlayersSendInvite == setOf<Any?>(6L, 7L)
)
Check(
"切片 MatchmakingGroup:SetAllowedFromFriends / Reset",
ReduceMatchmakingGroup(
ReduceMatchmakingGroup(S2, SetAllowedFromFriends(listOf(1L))),
ResetMatchmakingGroup,
).let { it.AllowedFromFriends == setOf<Any?>(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<Any?>(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)