From cd597b4693e9a756b1834ac784041d49e195d148 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 21:03:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20battlesSele?= =?UTF-8?q?ct=EF=BC=88=E5=A4=8D=E7=94=A8=20BattleParams/BattleInfoUser?= =?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 | 16 +- 3DTank/names/root_wiring.json | 23 +- 3DTank/tools/gen_root_wiring.js | 7 +- .../State/Reducers/BattlesSelectReducer.kt | 238 ++++++++++++++++++ .../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 | 157 +++++++++++- 8 files changed, 434 insertions(+), 23 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlesSelectReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 1366dce..8aee773 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` 动作名。 - - 已迁移切片 **80/89**: + - 已迁移切片 **81/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) @@ -403,12 +403,18 @@ `WasRegistered` 写等待时间与模式并清 `requestSent`;`EnterToBattleApply` 同时置两个进行中位; `WH`→`ResetMatchmakingProgress`、`tW`/`nW`→`StartLoadingBattle`/`CancelLoadingBattle`; 模式表复用生成物 `MatchmakingModeRank`,`rankIndexToMinGS` 收口为 `Map`。 + - `battlesSelect` → `fA` / `Mi.vqk`(`BattlesSelectReducer.kt`):15 字段,20 个实例臂。 + `AddBattle` 把 `BattleCreateParameters`(+ suspicionLevel)转成 `BattleParams` 并建 users / score 空表; + `AddUsers` 批量写 users / usersTeam 并同步个人分与可疑标记(`AddUser` 复用它); + `SwapTeam` 换队并把该战斗内用户分数归零;`RemoveBattle` / `RemoveUser` 清对应的四张子表; + 其余为名字 / 可疑度 / 队伍分 / 个人分 / 资金 / 奖励上限 / 观战标记。 + 模型全部复用生成物:`BattleParams` / `BattleInfoUser` / `BattleCreateParameters` / `ProBattleRewardLimitData`。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 79 个有 reducer 的切片, - `launchParams` 与其余 **9 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 80/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **871 项全绿**,含 89 槽位完整性、79 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 80 个有 reducer 的切片, + `launchParams` 与其余 **8 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 81/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **888 项全绿**,含 89 槽位完整性、80 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 8a0055c..15bafdd 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T12:37:28.603Z", + "generatedAt": "2026-10-24T12:57:54.249Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -86,7 +86,8 @@ "maps", "battleChat", "remoteUserDataStorage", - "matchmaking" + "matchmaking", + "battlesSelect" ], "AnonymousActions": [ { @@ -1432,6 +1433,18 @@ "Name": "CancelLoadingBattle", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "startLoadingBattle=false(`n.brw(cd, cd, cd, cd, !1)`)" + }, + { + "Local": "gA", + "Name": "MarkBattlesLoaded", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "loaded=true(`n.hqj(!0)`)" + }, + { + "Local": "dA", + "Name": "ResetBattlesSelect", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new fA() 整片复位" } ], "Markers": [ @@ -2156,8 +2169,6 @@ "Reducer": "Mi.vqk", "Delegate": "Mi.vqk(t, n.apw_1)", "Actions": [ - "?dA", - "?gA", "AddBattle", "AddUser", "AddUsers", @@ -2165,8 +2176,10 @@ "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", + "MarkBattlesLoaded", "RemoveBattle", "RemoveUser", + "ResetBattlesSelect", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", @@ -2178,7 +2191,7 @@ "UpdateUserSuspicion" ], "Default": "new fA()", - "Migrated": false + "Migrated": true }, { "Field": "maps", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index b7dc8de..3ca799f 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -327,6 +327,9 @@ const AnonymousActionNames = new Map([ ['WH', 'ResetMatchmakingProgress'], ['tW', 'StartLoadingBattle'], ['nW', 'CancelLoadingBattle'], + // battlesSelect 切片(fA / MA.vqk):gA/dA 没有 toString。 + ['gA', 'MarkBattlesLoaded'], + ['dA', 'ResetBattlesSelect'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -811,6 +814,7 @@ const Migrated = { Reduce: 'ReduceRemoteUserDataStorage', }, matchmaking: { Type: 'MatchmakingStateSlice', Reduce: 'ReduceMatchmaking' }, + battlesSelect: { Type: 'BattlesSelectStateSlice', Reduce: 'ReduceBattlesSelect' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1226,8 +1230,9 @@ const AppSettingsAnonymousEffects = { WH: 'matchmakingAndLoadingBattleInProgress=false、startLoadingBattle=false、requestSent=false', tW: 'startLoadingBattle=true(`n.brw(cd, cd, cd, cd, !0)`)', nW: 'startLoadingBattle=false(`n.brw(cd, cd, cd, cd, !1)`)', + gA: 'loaded=true(`n.hqj(!0)`)', + dA: 'new fA() 整片复位', }; - fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlesSelectReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlesSelectReducer.kt new file mode 100644 index 0000000..7d8acc8 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattlesSelectReducer.kt @@ -0,0 +1,238 @@ +// BattlesSelectReducer.kt —— 战斗列表切片(对应 module_10032 的 fA 状态 + MA.vqk) +// +// 证据链: +// 状态类 fA:15 个字段,toString 名 "BattlesSelect(…)";copy 是 hqj(15 字段 + 回调)。 +// 根委派:`Mi.vqk(t, n.apw_1)`。 +// reducer(20 个实例臂): +// TA(RoundStarted) → roundEndTimes[battleId] = 当前时间 + 剩余秒;score[battleId] 清空 +// IA(SwapTeam) → 该战斗内所有用户交换队伍,并把他们的个人分数归零 +// AA(RoundFinish) → roundEndTimes / fund 删掉该战斗 +// gA(无 toString) → loaded=true +// mA(AddBattle) → battles[battleId]=BattleParams;users / score 建空表;creators 写创建者 +// kA(RemoveBattle) → battles / users / creators / alreadySeeBattleInfo 删该战斗 +// zA(ChangeName) → battles[battleId].name +// bA(UpdateSuspicion) → battles[battleId].suspicionLevel +// yA(AddUsers) → 批量写 users / usersTeam(`$A` AddUser 复用它) +// xA(RemoveUser) → users / usersTeam / userScore / userSuspicion / userSuspicionByIp 删该用户 +// jA(UpdateUserScore) → userScore[userId] +// CA(UpdateUserSuspicion)→ userSuspicion / userSuspicionByIp +// qA(UpdateTeamScore) → score[battleId][team] +// dA(无 toString) → `new fA()` 整片复位 +// RA(EnterToBattleInfo) → alreadySeeBattleInfo += battleId +// SA(UpdateFund) → fund[battleId] +// EA(InitProBattleRewardLimit)→ proLimit +// PA(LimitReached) → isLimitReached +// NA(SetReEnterProtectBattleId)→ reEnterProtectBattleId +// +// 模型直接复用生成物:`BattleParams`(战斗描述)、`BattleInfoUser`(列表里的用户)、 +// `BattleCreateParameters`(AddBattle 载荷)、`ProBattleRewardLimitData`。 +package App.State.Reducers + +import App.State.AddBattle +import App.State.AddUser +import App.State.AddUsers +import App.State.BattleCreateParameters +import App.State.BattleInfoUser +import App.State.BattleParams +import App.State.ChangeName +import App.State.EnterToBattleInfo +import App.State.InitProBattleRewardLimit +import App.State.LimitReached +import App.State.ProBattleRewardLimitData +import App.State.RemoveBattle +import App.State.RemoveUser +import App.State.RoundFinish +import App.State.RoundStarted +import App.State.SetReEnterProtectBattleId +import App.State.SwapTeam +import App.State.UpdateFund +import App.State.UpdateSuspicion +import App.State.UpdateTeamScore +import App.State.UpdateUserScore +import App.State.UpdateUserSuspicion + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `gA`:战斗列表已加载。 */ +object MarkBattlesLoaded + +/** 原 `dA`:整片复位(`new fA()`)。 */ +object ResetBattlesSelect + +/* ------------------------------------------------------------------ 状态 */ + +data class BattlesSelectStateSlice( + val Loaded: Boolean = false, + val Battles: Map = emptyMap(), + val Score: Map = emptyMap(), + val Users: Map = emptyMap(), + val Creators: Map = emptyMap(), + val UsersTeam: Map = emptyMap(), + val UserScore: Map = emptyMap(), + val UserSuspicion: Map = emptyMap(), + val UserSuspicionByIp: Map = emptyMap(), + val RoundEndTimes: Map = emptyMap(), + val AlreadySeeBattleInfo: Set = emptySet(), + val Fund: Map = emptyMap(), + val ProLimit: ProBattleRewardLimitData? = null, + val IsLimitReached: Boolean = false, + val ReEnterProtectBattleId: Any? = null, +) + +/* ------------------------------------------------------------------ reducer */ + +@Suppress("UNCHECKED_CAST") +private fun Inner(Map: Map, Key: Any?): Map = + Map[Key] as? Map ?: emptyMap() + +/** `BattleCreateParameters` → 列表用的 `BattleParams`(原 `new vA(...)`)。 */ +private fun ParamsOf( + Params: BattleCreateParameters, + SuspicionLevel: Any?, +): BattleParams = BattleParams( + map = Params.mapId, + battleMode = 0, + format = Params.formatMode, + limits = Params.limits, + mapId = Params.mapId, + maxPeopleCount = Params.maxPeopleCount, + name = Params.name?.toString() ?: "", + parkourMode = Params.battleMode == "PARKOUR", + valuableBattle = Params.battleOptions.contains("VALUABLE"), + privateBattle = Params.battleOptions.contains("PRIVATE"), + proBattle = Params.proBattle, + rankRange = Params.rankRange, + theme = Params.theme.toIntOrNull() ?: 0, + suspicionLevel = SuspicionLevel, + options = Params.battleOptions, +) + +/** 原 `eP`:队伍互换(字符串队伍名)。 */ +private fun Swap(Team: Any?): Any? = when (Team) { + "RED", "red" -> "BLUE" + "BLUE", "blue" -> "RED" + else -> Team +} + +fun ReduceBattlesSelect( + State0: BattlesSelectStateSlice, + Action: Any, +): BattlesSelectStateSlice = when (Action) { + is RoundStarted -> State0.copy( + RoundEndTimes = State0.RoundEndTimes + (Action.battleId to Action.timeLeftInSec), + Score = State0.Score + (Action.battleId to emptyMap()), + ) + is SwapTeam -> { + val Teams = Inner(State0.UsersTeam, Action.battleId) + val Swapped = Teams.mapValues { (_, Team) -> Swap(Team) } + val Users = Inner(State0.Users, Action.battleId) + val Zeroed = State0.UserScore.mapValues { (UserId, _) -> + if (Users.containsKey(UserId)) 0 else State0.UserScore[UserId] + } + State0.copy( + UsersTeam = State0.UsersTeam + (Action.battleId to Swapped), + UserScore = Zeroed, + ) + } + is RoundFinish -> State0.copy( + RoundEndTimes = State0.RoundEndTimes - Action.battleId, + Fund = State0.Fund - Action.battleId, + ) + is MarkBattlesLoaded -> State0.copy(Loaded = true) + is AddBattle -> State0.copy( + Battles = State0.Battles + ( + Action.battleId to ParamsOf(Action.params, Action.suspicionLevel) + ), + Users = State0.Users + (Action.battleId to emptyMap()), + Score = State0.Score + (Action.battleId to emptyMap()), + Creators = State0.Creators + (Action.battleId to Action.creatorId), + ) + is RemoveBattle -> State0.copy( + Battles = State0.Battles - Action.battleId, + Users = State0.Users - Action.battleId, + Creators = State0.Creators - Action.battleId, + AlreadySeeBattleInfo = State0.AlreadySeeBattleInfo - Action.battleId, + ) + is ChangeName -> { + val Battle = State0.Battles[Action.battleId] as? BattleParams + if (Battle == null) State0 + else State0.copy( + Battles = State0.Battles + ( + Action.battleId to Battle.copy(name = Action.name?.toString() ?: Battle.name) + ), + ) + } + is UpdateSuspicion -> { + val Battle = State0.Battles[Action.battleId] as? BattleParams + if (Battle == null) State0 + else State0.copy( + Battles = State0.Battles + ( + Action.battleId to Battle.copy(suspicionLevel = Action.suspicionLevel) + ), + ) + } + is AddUsers -> { + var Users = State0.Users + var Teams = State0.UsersTeam + var Scores = State0.UserScore + var Suspicion = State0.UserSuspicion + var SuspicionByIp = State0.UserSuspicionByIp + val CurrentUsers = LinkedHashMap(Inner(State0.Users, Action.battleId)) + val CurrentTeams = LinkedHashMap(Inner(State0.UsersTeam, Action.battleId)) + for (User in Action.users) { + CurrentUsers[User.user] = User + CurrentTeams[User.user] = Action.team + Scores = Scores + (User.user to User.score) + Suspicion = Suspicion + (User.user to User.suspicious) + SuspicionByIp = SuspicionByIp + (User.user to User.suspiciousByIp) + } + Users = Users + (Action.battleId to CurrentUsers) + Teams = Teams + (Action.battleId to CurrentTeams) + State0.copy( + Users = Users, + UsersTeam = Teams, + UserScore = Scores, + UserSuspicion = Suspicion, + UserSuspicionByIp = SuspicionByIp, + ) + } + is AddUser -> ReduceBattlesSelect( + State0, + AddUsers( + battleId = Action.battleId, + users = listOf(Action.infoUser as BattleInfoUser), + team = Action.team, + ), + ) + is RemoveUser -> State0.copy( + Users = State0.Users + ( + Action.battleId to Inner(State0.Users, Action.battleId) - Action.userId + ), + UsersTeam = State0.UsersTeam + ( + Action.battleId to Inner(State0.UsersTeam, Action.battleId) - Action.userId + ), + UserScore = State0.UserScore - Action.userId, + UserSuspicion = State0.UserSuspicion - Action.userId, + UserSuspicionByIp = State0.UserSuspicionByIp - Action.userId, + ) + is UpdateUserScore -> State0.copy( + UserScore = State0.UserScore + (Action.userId to Action.score), + ) + is UpdateUserSuspicion -> State0.copy( + UserSuspicion = State0.UserSuspicion + (Action.userId to Action.suspicious), + UserSuspicionByIp = State0.UserSuspicionByIp + (Action.userId to Action.suspiciousByIp), + ) + is UpdateTeamScore -> { + val Teams = LinkedHashMap(Inner(State0.Score, Action.battleId)) + Teams[Action.team] = Action.score + State0.copy(Score = State0.Score + (Action.battleId to Teams)) + } + is ResetBattlesSelect -> BattlesSelectStateSlice() + is EnterToBattleInfo -> + State0.copy(AlreadySeeBattleInfo = State0.AlreadySeeBattleInfo + Action.battleId) + is UpdateFund -> State0.copy(Fund = State0.Fund + (Action.battleId to Action.fund)) + is InitProBattleRewardLimit -> State0.copy(ProLimit = Action.proLimit) + is LimitReached -> State0.copy(IsLimitReached = Action.isLimitReached) + is SetReEnterProtectBattleId -> State0.copy(ReEnterProtectBattleId = Action.battleId) + 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 18c107f..cd8bc61 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 出新状态)。 -// 这里采用同一形状。已迁移的 80 个切片里,79 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 81 个切片里,80 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -82,9 +82,10 @@ // battleChat → ReduceBattleChat(Q6 / Hf.wtg,根委派 `Hf.wtg(t, n.rpw_1)`) // remoteUserDataStorage → ReduceRemoteUserDataStorage($Q / vh.isl,根委派 `vh.isl(t, n.zpv_1)`) // matchmaking → ReduceMatchmaking(TH / JH().qrx,根委派 `JH().qrx(t, n.cpw_1)`) +// battlesSelect → ReduceBattlesSelect(fA / Mi.vqk,根委派 `Mi.vqk(t, n.apw_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 9 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 8 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -97,6 +98,7 @@ import App.State.Reducers.ReduceBattleChat import App.State.Reducers.ReduceBattleMessage import App.State.Reducers.ReduceBattlePause import App.State.Reducers.ReduceBattleSearch +import App.State.Reducers.ReduceBattlesSelect import App.State.Reducers.ReduceBattleSettings import App.State.Reducers.ReduceBattleStatistics import App.State.Reducers.ReduceBattleTutorial @@ -261,6 +263,7 @@ object RootReducer { BattleChat = ReduceBattleChat(State0.BattleChat, Action), RemoteUserDataStorage = ReduceRemoteUserDataStorage(State0.RemoteUserDataStorage, Action), Matchmaking = ReduceMatchmaking(State0.Matchmaking, Action), + BattlesSelect = ReduceBattlesSelect(State0.BattlesSelect, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index b8919af..4b3bac0 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -18,6 +18,7 @@ import App.State.Reducers.BattleSettingsStateSlice import App.State.Reducers.BattleStatisticsStateSlice import App.State.Reducers.BattleTutorialStateSlice import App.State.Reducers.BattleViewStateSlice +import App.State.Reducers.BattlesSelectStateSlice import App.State.Reducers.CaptchaStateSlice import App.State.Reducers.ClanChatStateSlice import App.State.Reducers.ClanUserInfoStateSlice @@ -144,8 +145,8 @@ data class RootState( val Friends: FriendsStateSlice = FriendsStateSlice(), /** 原 remoteUserDataStorage($Q / vh.isl)。已迁移。 */ val RemoteUserDataStorage: RemoteUserDataStorageStateSlice = RemoteUserDataStorageStateSlice(), - /** 原 battlesSelect(fA / Mi.vqk)。未迁移:原样透传。 */ - val BattlesSelect: Any? = null, + /** 原 battlesSelect(fA / Mi.vqk)。已迁移。 */ + val BattlesSelect: BattlesSelectStateSlice = BattlesSelectStateSlice(), /** 原 maps(OA / Oi.sql)。已迁移。 */ val Maps: MapsStateSlice = MapsStateSlice(), /** 原 matchmaking(TH / JH().qrx)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index bfadce5..6eba8b2 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -42,7 +42,7 @@ object RootWiring { SliceWiring("notifications", "xpv_1", "jJ", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("AddNotification", "InitNotificationSound", "RemoveFirstNotification", "RemoveNotifications")), SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "ClearContextMenuUser", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetContextMenuUser", "SetLimits", "SetValidationStatus")), SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")), - SliceWiring("battlesSelect", "apw_1", "fA", "Mi.vqk", "Mi.vqk(t, n.apw_1)", listOf("?dA", "?gA", "AddBattle", "AddUser", "AddUsers", "ChangeName", "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", "RemoveBattle", "RemoveUser", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", "SwapTeam", "UpdateFund", "UpdateSuspicion", "UpdateTeamScore", "UpdateUserScore", "UpdateUserSuspicion")), + SliceWiring("battlesSelect", "apw_1", "fA", "Mi.vqk", "Mi.vqk(t, n.apw_1)", listOf("AddBattle", "AddUser", "AddUsers", "ChangeName", "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", "MarkBattlesLoaded", "RemoveBattle", "RemoveUser", "ResetBattlesSelect", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", "SwapTeam", "UpdateFund", "UpdateSuspicion", "UpdateTeamScore", "UpdateUserScore", "UpdateUserSuspicion")), SliceWiring("maps", "bpw_1", "OA", "Oi.sql", "Oi.sql(t, n.bpw_1)", listOf("AddMap")), SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("CancelLoadingBattle", "Configure", "EnterToBattleApply", "Play", "ResetMatchmakingProgress", "SelectMatchmakingMode", "SetRankIndexToMinGS", "StartLoadingBattle", "WasRegistered")), SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 6a78699..a476f0a 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -767,6 +767,30 @@ import App.State.Reducers.MatchmakingStateSlice import App.State.Reducers.ReduceMatchmaking import App.State.Reducers.ResetMatchmakingProgress import App.State.Reducers.StartLoadingBattle +import App.State.AddBattle +import App.State.AddUser +import App.State.AddUsers +import App.State.BattleCreateParameters +import App.State.BattleInfoUser +import App.State.BattleParams +import App.State.ChangeName +import App.State.EnterToBattleInfo +import App.State.InitProBattleRewardLimit +import App.State.LimitReached +import App.State.ProBattleRewardLimitData +import App.State.RemoveUser +import App.State.RoundFinish +import App.State.RoundStarted +import App.State.SetReEnterProtectBattleId +import App.State.SwapTeam +import App.State.UpdateSuspicion +import App.State.UpdateTeamScore +import App.State.UpdateUserScore +import App.State.UpdateUserSuspicion +import App.State.Reducers.BattlesSelectStateSlice +import App.State.Reducers.MarkBattlesLoaded +import App.State.Reducers.ReduceBattlesSelect +import App.State.Reducers.ResetBattlesSelect import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1194,7 +1218,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 80 个已迁移切片(79 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 81 个已迁移切片(80 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1258,7 +1282,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 80 个已迁移字段,其余 9 个待迁", + "根:迁移表 = 81 个已迁移字段,其余 8 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1282,9 +1306,9 @@ object StateCheck { "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", - "remoteUserDataStorage", "matchmaking", + "remoteUserDataStorage", "matchmaking", "battlesSelect", ) && - RootWiring.Unmigrated.size == 9 + RootWiring.Unmigrated.size == 8 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1340,7 +1364,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 80/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 81/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6652,6 +6676,127 @@ object StateCheck { ) } + // 91 切片 reducer:BattlesSelect(fA / Mi.vqk,15 字段;20 个动作) + run { + fun Params(): BattleCreateParameters = BattleCreateParameters( + battleMode = "DM", battleModeForMatchmaking = null, + battleOptions = listOf("VALUABLE"), crossPlayMode = "", formatMode = "fmt", + limits = "10", timeLimitInSec = "600", mapId = 1L, maxPeopleCount = 8, + name = "b1", proBattle = true, rankRange = "1-5", min = "1", theme = "2", + ) + + val S0 = BattlesSelectStateSlice() + Check("切片 BattlesSelect:默认值完整", !S0.Loaded && S0.Battles.isEmpty()) + val S1 = ReduceBattlesSelect(S0, AddBattle(1L, 99L, Params(), "LOW")) + val Battle = S1.Battles[1L] as BattleParams + Check( + "切片 BattlesSelect:AddBattle 建列表项与四个子表", + Battle.name == "b1" && Battle.mapId == 1L && Battle.proBattle && + Battle.parkourMode == false && S1.Users[1L] == emptyMap() && + S1.Score[1L] == emptyMap() && S1.Creators[1L] == 99L + ) + val S2 = ReduceBattlesSelect(S1, RoundStarted(1L, 60)) + Check( + "切片 BattlesSelect:RoundStarted 写回合计时并清分数", + S2.RoundEndTimes[1L] == 60 && S2.Score[1L] == emptyMap() + ) + val S3 = ReduceBattlesSelect(S2, UpdateTeamScore(1L, "RED", 3)) + Check( + "切片 BattlesSelect:UpdateTeamScore 写队伍分", + (S3.Score[1L] as Map)["RED"] == 3 + ) + val S4 = ReduceBattlesSelect( + S3, + AddUsers(1L, listOf(BattleInfoUser(0L, 0, false, false, 7L)), "RED"), + ) + Check( + "切片 BattlesSelect:AddUsers 写用户 / 队伍 / 个人分", + (S4.Users[1L] as Map).keys == setOf(7L) && + (S4.UsersTeam[1L] as Map)[7L] == "RED" && + S4.UserScore[7L] == 0 + ) + val S5 = ReduceBattlesSelect( + ReduceBattlesSelect(S4, UpdateUserScore(1L, 7L, 9)), + UpdateUserSuspicion(1L, 7L, true, false), + ) + Check( + "切片 BattlesSelect:个人分与可疑标记", + S5.UserScore[7L] == 9 && S5.UserSuspicion[7L] == true && + S5.UserSuspicionByIp[7L] == false + ) + val S6 = ReduceBattlesSelect(S5, SwapTeam(1L)) + Check( + "切片 BattlesSelect:SwapTeam 换队并把该用户分数归零", + (S6.UsersTeam[1L] as Map)[7L] == "BLUE" && S6.UserScore[7L] == 0 + ) + Check( + "切片 BattlesSelect:ChangeName / UpdateSuspicion", + (ReduceBattlesSelect(S1, ChangeName(1L, "renamed")) + .Battles[1L] as BattleParams).name == "renamed" && + (ReduceBattlesSelect(S1, UpdateSuspicion(1L, "HIGH")) + .Battles[1L] as BattleParams).suspicionLevel == "HIGH" + ) + Check( + "切片 BattlesSelect:EnterToBattleInfo / UpdateFund / LimitReached", + ReduceBattlesSelect(S1, EnterToBattleInfo(1L)) + .AlreadySeeBattleInfo == setOf(1L) && + ReduceBattlesSelect(S1, UpdateFund(1L, 20)).Fund[1L] == 20 && + ReduceBattlesSelect(S1, LimitReached(true)).IsLimitReached + ) + Check( + "切片 BattlesSelect:InitProBattleRewardLimit / SetReEnterProtectBattleId", + ReduceBattlesSelect( + S1, + InitProBattleRewardLimit(ProBattleRewardLimitData(1, 2, 3, 4)), + ).ProLimit == ProBattleRewardLimitData(1, 2, 3, 4) && + ReduceBattlesSelect(S1, SetReEnterProtectBattleId(5L)) + .ReEnterProtectBattleId == 5L + ) + Check( + "切片 BattlesSelect:RemoveUser 清四个个人表", + ReduceBattlesSelect(S5, RemoveUser(1L, 7L)).let { + (it.Users[1L] as Map).isEmpty() && + it.UserScore[7L] == null && it.UserSuspicion[7L] == null + } + ) + Check( + "切片 BattlesSelect:RemoveBattle 清四个战斗表", + ReduceBattlesSelect( + ReduceBattlesSelect(S5, EnterToBattleInfo(1L)), + RemoveBattle(1L), + ).let { + it.Battles.isEmpty() && it.Users.isEmpty() && it.Creators.isEmpty() && + it.AlreadySeeBattleInfo.isEmpty() + } + ) + Check( + "切片 BattlesSelect:RoundFinish 删计时与资金", + ReduceBattlesSelect(ReduceBattlesSelect(S2, UpdateFund(1L, 5)), RoundFinish(1L)) + .let { it.RoundEndTimes[1L] == null && it.Fund[1L] == null } + ) + Check( + "切片 BattlesSelect:MarkBattlesLoaded / ResetBattlesSelect", + ReduceBattlesSelect(S0, MarkBattlesLoaded).Loaded && + ReduceBattlesSelect(S5, ResetBattlesSelect) == BattlesSelectStateSlice() + ) + Check( + "切片 BattlesSelect:无关动作不改状态", + ReduceBattlesSelect(S5, AddFrags(1)) == S5 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, MarkBattlesLoaded) + Check( + "根:MarkBattlesLoaded 只动 battlesSelect", + Root1.BattlesSelect.Loaded && Root1.Matchmaking == Root0.Matchmaking + ) + val W = RootWiring.ByField("battlesSelect")!! + Check( + "根:battlesSelect 委派到 fA / Mi.vqk 且 20 个动作", + W.SliceClass == "fA" && W.Reducer == "Mi.vqk" && W.Actions.size == 20 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)