From 0ae39eebcd91d5fa2c1d2ef0bc39ffa224e494ba Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 21:21:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20battleUsers?= =?UTF-8?q?=EF=BC=88=E5=A4=8D=E7=94=A8=20RemoteUserData/UserStat/TankInfoC?= =?UTF-8?q?C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 14 +- 3DTank/names/root_wiring.json | 55 +++++- 3DTank/tools/gen_root_wiring.js | 14 ++ .../App/State/Reducers/BattleUsersReducer.kt | 164 ++++++++++++++++++ .../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 | 129 +++++++++++++- 8 files changed, 366 insertions(+), 26 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleUsersReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 8aee773..a4ecddc 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` 动作名。 - - 已迁移切片 **81/89**: + - 已迁移切片 **82/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) @@ -409,12 +409,16 @@ `SwapTeam` 换队并把该战斗内用户分数归零;`RemoveBattle` / `RemoveUser` 清对应的四张子表; 其余为名字 / 可疑度 / 队伍分 / 个人分 / 资金 / 奖励上限 / 观战标记。 模型全部复用生成物:`BattleParams` / `BattleInfoUser` / `BattleCreateParameters` / `ProBattleRewardLimitData`。 + - `battleUsers` → `uP` / `sr.hqs`(`BattleUsersReducer.kt`):13 字段,18 个分支。 + 用户加入时同时写 onlineUsers / uids / ranks / clanTags / usersPremium / teams / stats; + stats 值是生成物 `UserStat`(deaths / killScore / kills / score / user); + 坦克信息与抗性复用 `TankInfoCC` / `TankResistance`;`sP`(旧队伍枚举迁移)按 no-op 收口。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 80 个有 reducer 的切片, - `launchParams` 与其余 **8 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 81/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **888 项全绿**,含 89 槽位完整性、80 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 81 个有 reducer 的切片, + `launchParams` 与其余 **7 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 82/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **902 项全绿**,含 89 槽位完整性、81 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 15bafdd..8f20e6e 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T12:57:54.249Z", + "generatedAt": "2026-10-24T13:07:20.497Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -87,7 +87,8 @@ "battleChat", "remoteUserDataStorage", "matchmaking", - "battlesSelect" + "battlesSelect", + "battleUsers" ], "AnonymousActions": [ { @@ -1445,6 +1446,42 @@ "Name": "ResetBattlesSelect", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "new fA() 整片复位" + }, + { + "Local": "oP", + "Name": "ResetBattleUsers", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "new uP() 整片复位" + }, + { + "Local": "_P", + "Name": "AddBattleUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "单个用户写 onlineUsers / uids / ranks / clanTags / usersPremium / teams / stats" + }, + { + "Local": "hP", + "Name": "AddBattleUsers", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "批量用户同上(循环调用 `_P` 的逻辑)" + }, + { + "Local": "qP", + "Name": "RemoveOnlineUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "onlineUsers -= userId(`n.hqr(Jg(n.iqa_1, t.gqs_1))`)" + }, + { + "Local": "lP", + "Name": "SetUserPremium", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "usersPremium[userId](`cg(n.mqa_1, Vd(t.iqs_1, t.jqs_1))`)" + }, + { + "Local": "sP", + "Name": "MigrateBattleUserTeams", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "旧队伍枚举迁移(`qm/bm/em` 映射),离线重建按 no-op 收口" } ], "Markers": [ @@ -2310,18 +2347,18 @@ "Reducer": "sr.hqs", "Delegate": "sr.hqs(t, n.hpw_1)", "Actions": [ - "?_P", - "?hP", - "?lP", - "?oP", - "?qP", - "?sP", + "AddBattleUser", + "AddBattleUsers", "InitSuspiciousState", "InitUserLevels", + "MigrateBattleUserTeams", + "RemoveOnlineUser", "RemoveTankInfo", + "ResetBattleUsers", "SetGearScore", "SetTankInfo", "SetTankResistance", + "SetUserPremium", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", @@ -2330,7 +2367,7 @@ "UpdateUsersScoreKillsAndDeaths" ], "Default": "new uP()", - "Migrated": false + "Migrated": true }, { "Field": "suppliesState", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 3ca799f..a905e03 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -330,6 +330,13 @@ const AnonymousActionNames = new Map([ // battlesSelect 切片(fA / MA.vqk):gA/dA 没有 toString。 ['gA', 'MarkBattlesLoaded'], ['dA', 'ResetBattlesSelect'], + // battleUsers 切片(uP / yP.hqs):oP/_P/hP/qP/lP/sP 没有 toString。 + ['oP', 'ResetBattleUsers'], + ['_P', 'AddBattleUser'], + ['hP', 'AddBattleUsers'], + ['qP', 'RemoveOnlineUser'], + ['lP', 'SetUserPremium'], + ['sP', 'MigrateBattleUserTeams'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -815,6 +822,7 @@ const Migrated = { }, matchmaking: { Type: 'MatchmakingStateSlice', Reduce: 'ReduceMatchmaking' }, battlesSelect: { Type: 'BattlesSelectStateSlice', Reduce: 'ReduceBattlesSelect' }, + battleUsers: { Type: 'BattleUsersStateSlice', Reduce: 'ReduceBattleUsers' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -1232,6 +1240,12 @@ const AppSettingsAnonymousEffects = { nW: 'startLoadingBattle=false(`n.brw(cd, cd, cd, cd, !1)`)', gA: 'loaded=true(`n.hqj(!0)`)', dA: 'new fA() 整片复位', + oP: 'new uP() 整片复位', + _P: '单个用户写 onlineUsers / uids / ranks / clanTags / usersPremium / teams / stats', + hP: '批量用户同上(循环调用 `_P` 的逻辑)', + qP: 'onlineUsers -= userId(`n.hqr(Jg(n.iqa_1, t.gqs_1))`)', + lP: 'usersPremium[userId](`cg(n.mqa_1, Vd(t.iqs_1, t.jqs_1))`)', + sP: '旧队伍枚举迁移(`qm/bm/em` 映射),离线重建按 no-op 收口', }; fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleUsersReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleUsersReducer.kt new file mode 100644 index 0000000..4e936a2 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleUsersReducer.kt @@ -0,0 +1,164 @@ +// BattleUsersReducer.kt —— 战斗内用户表切片(对应 module_10032 的 uP 状态 + yP.hqs) +// +// 证据链: +// 状态类 uP:13 个字段,toString 名 "BattleUsers(…)";copy 是 hqr(13 字段 + 回调)。 +// 根委派:`sr.hqs(t, n.hpw_1)`。 +// reducer(18 个实例臂): +// oP(无 toString) → `new uP()` 整片复位 +// _P / hP(无 toString) → 单个 / 批量把用户写进 onlineUsers / uids / ranks / +// clanTags / usersPremium / teams / stats +// aP(UpdateUserRank) → ranks[id] +// cP(UpdateUsersClanTag) → clanTags 合并 +// dP / gP(UpdateUserScoreKillsAndDeaths / …Users…)→ stats[id] = userStat +// mP(SetGearScore) → gearScores[user] +// bP(SetTankResistance) → tankResistance[user] +// qP(无 toString) → onlineUsers -= userId +// lP(无 toString) → usersPremium[userId] +// fP(UpdateUserSuspiciousState)→ suspiciousUsers / suspiciousByIpUsers 按开关增删 +// vP(InitSuspiciousState) → 两个集合整体替换 +// sP(无 toString) → 队伍枚举迁移(原 `qm/bm/em` 映射):离线重建按 no-op 收口 +// pP(InitUserLevels) → tankLevels 合并 +// wP(UpdateUserLevel) → tankLevels[userId] +// kP(SetTankInfo) → tankInfo[user] +// zP(RemoveTankInfo) → tankInfo -= user +// +// 用户模型复用生成物 `RemoteUserData`(userId / uid / rank / hasPremium / clanTag / …); +// 坦克信息与抗性复用 `TankInfoCC` / `TankResistance`。 +package App.State.Reducers + +import App.State.InitSuspiciousState +import App.State.InitUserLevels +import App.State.RemoteUserData +import App.State.SetGearScore +import App.State.RemoveTankInfo +import App.State.SetTankInfo +import App.State.SetTankResistance +import App.State.TankInfoCC +import App.State.TankResistance +import App.State.UpdateUserLevel +import App.State.UpdateUserRank +import App.State.UpdateUserScoreKillsAndDeaths +import App.State.UpdateUserSuspiciousState +import App.State.UpdateUsersClanTag +import App.State.UpdateUsersScoreKillsAndDeaths +import App.State.UserStat + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `oP`:整片复位(`new uP()`)。 */ +object ResetBattleUsers + +/** 原 `_P`:加入一个战斗用户。 */ +data class AddBattleUser(val user: RemoteUserData, val team: Any?) + +/** 原 `hP`:批量加入战斗用户。 */ +data class AddBattleUsers(val users: List, val team: Any?) + +/** 原 `qP`:把用户从在线列表移除。 */ +data class RemoveOnlineUser(val userId: Any?) + +/** 原 `lP`:写用户的会员标记。 */ +data class SetUserPremium(val userId: Any?, val premium: Any?) + +/** 原 `sP`:旧队伍枚举迁移(离线重建按 no-op)。 */ +object MigrateBattleUserTeams + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleUsersStateSlice( + val OnlineUsers: Set = emptySet(), + val Uids: Map = emptyMap(), + val Ranks: Map = emptyMap(), + val ClanTags: Map = emptyMap(), + val UsersPremium: Map = emptyMap(), + val Teams: Map = emptyMap(), + val Stats: Map = emptyMap(), + val GearScores: Map = emptyMap(), + val TankInfo: Map = emptyMap(), + val TankResistance: Map = emptyMap(), + val TankLevels: Map = emptyMap(), + val SuspiciousUsers: Set = emptySet(), + val SuspiciousByIpUsers: Set = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +private fun ApplyUser( + State0: BattleUsersStateSlice, + User: RemoteUserData, + Team: Any?, +): BattleUsersStateSlice { + val Id = User.userId + return State0.copy( + OnlineUsers = State0.OnlineUsers + Id, + Uids = State0.Uids + (Id to User.uid), + Ranks = State0.Ranks + (Id to User.rank), + ClanTags = State0.ClanTags + (Id to User.clanTag), + UsersPremium = State0.UsersPremium + (Id to User.hasPremium), + Teams = State0.Teams + (Id to Team), + Stats = State0.Stats + (Id to User), + ) +} + +fun ReduceBattleUsers( + State0: BattleUsersStateSlice, + Action: Any, +): BattleUsersStateSlice = when (Action) { + is ResetBattleUsers -> BattleUsersStateSlice() + is AddBattleUser -> ApplyUser(State0, Action.user, Action.team) + is AddBattleUsers -> { + var Result = State0 + for (Item in Action.users) { + val User = Item as? RemoteUserData ?: continue + Result = ApplyUser(Result, User, Action.team) + } + Result + } + is UpdateUserRank -> State0.copy(Ranks = State0.Ranks + (Action.id to Action.rank)) + is UpdateUsersClanTag -> { + val Incoming = Action.clans as? Map ?: emptyMap() + State0.copy(ClanTags = State0.ClanTags + Incoming) + } + is UpdateUserScoreKillsAndDeaths -> + State0.copy(Stats = State0.Stats + (Action.userStat.user to Action.userStat)) + is UpdateUsersScoreKillsAndDeaths -> { + var Stats = State0.Stats + for (Item in (Action.userScores as? Iterable<*> ?: emptyList())) { + val Stat = Item as? UserStat ?: continue + Stats = Stats + (Stat.user to Stat) + } + State0.copy(Stats = Stats) + } + is SetGearScore -> State0.copy(GearScores = State0.GearScores + (Action.user to Action.score)) + is SetTankResistance -> State0.copy( + TankResistance = State0.TankResistance + (Action.user to Action.tankResistance), + ) + is RemoveOnlineUser -> State0.copy(OnlineUsers = State0.OnlineUsers - Action.userId) + is SetUserPremium -> State0.copy( + UsersPremium = State0.UsersPremium + (Action.userId to Action.premium), + ) + is UpdateUserSuspiciousState -> State0.copy( + SuspiciousUsers = + if (Action.suspicious == true) State0.SuspiciousUsers + Action.id + else State0.SuspiciousUsers - Action.id, + SuspiciousByIpUsers = + if (Action.suspiciousByIp == true) State0.SuspiciousByIpUsers + Action.id + else State0.SuspiciousByIpUsers - Action.id, + ) + is InitSuspiciousState -> State0.copy( + SuspiciousUsers = (Action.suspiciousUserIds as? Iterable<*>)?.toSet() ?: emptySet(), + SuspiciousByIpUsers = + (Action.suspiciousByIpUserIds as? Iterable<*>)?.toSet() ?: emptySet(), + ) + is MigrateBattleUserTeams -> State0 + is InitUserLevels -> { + val Incoming = (Action.levels as? Map) ?: emptyMap() + State0.copy(TankLevels = State0.TankLevels + Incoming) + } + is UpdateUserLevel -> State0.copy( + TankLevels = State0.TankLevels + (Action.userId to Action.level), + ) + is SetTankInfo -> State0.copy(TankInfo = State0.TankInfo + (Action.user to Action.info)) + is RemoveTankInfo -> State0.copy(TankInfo = State0.TankInfo - Action.user) + 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 cd8bc61..7aefebf 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 出新状态)。 -// 这里采用同一形状。已迁移的 81 个切片里,80 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 82 个切片里,81 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -83,9 +83,10 @@ // 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)`) +// battleUsers → ReduceBattleUsers(uP / sr.hqs,根委派 `sr.hqs(t, n.hpw_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 8 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 7 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -102,6 +103,7 @@ import App.State.Reducers.ReduceBattlesSelect import App.State.Reducers.ReduceBattleSettings import App.State.Reducers.ReduceBattleStatistics import App.State.Reducers.ReduceBattleTutorial +import App.State.Reducers.ReduceBattleUsers import App.State.Reducers.ReduceBattleView import App.State.Reducers.ReduceBattleInvite import App.State.Reducers.ReduceCaptcha @@ -264,6 +266,7 @@ object RootReducer { RemoteUserDataStorage = ReduceRemoteUserDataStorage(State0.RemoteUserDataStorage, Action), Matchmaking = ReduceMatchmaking(State0.Matchmaking, Action), BattlesSelect = ReduceBattlesSelect(State0.BattlesSelect, Action), + BattleUsers = ReduceBattleUsers(State0.BattleUsers, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 4b3bac0..fabd217 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -17,6 +17,7 @@ import App.State.Reducers.BattleSearchStateSlice import App.State.Reducers.BattleSettingsStateSlice import App.State.Reducers.BattleStatisticsStateSlice import App.State.Reducers.BattleTutorialStateSlice +import App.State.Reducers.BattleUsersStateSlice import App.State.Reducers.BattleViewStateSlice import App.State.Reducers.BattlesSelectStateSlice import App.State.Reducers.CaptchaStateSlice @@ -159,8 +160,8 @@ data class RootState( val LobbyLoader: LobbyLoaderStateSlice = LobbyLoaderStateSlice(), /** 原 battleResult(sR / Fi.aqp)。已迁移。 */ val BattleResult: BattleResultStateSlice = BattleResultStateSlice(), - /** 原 battleUsers(uP / sr.hqs)。未迁移:原样透传。 */ - val BattleUsers: Any? = null, + /** 原 battleUsers(uP / sr.hqs)。已迁移。 */ + val BattleUsers: BattleUsersStateSlice = BattleUsersStateSlice(), /** 原 suppliesState(wE / $t.fq9)。已迁移。 */ val SuppliesState: SuppliesStateSlice = SuppliesStateSlice(), /** 原 matchmakingGroup(rW / NW().is0)。未迁移:原样透传。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 6eba8b2..d2e4e2d 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -49,7 +49,7 @@ object RootWiring { SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("ClearSelectedBattle", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")), SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("HideLoader", "ShowLoader")), 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("?_P", "?hP", "?lP", "?oP", "?qP", "?sP", "InitSuspiciousState", "InitUserLevels", "RemoveTankInfo", "SetGearScore", "SetTankInfo", "SetTankResistance", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")), + 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("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")), @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 a476f0a..12e6bd9 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -791,6 +791,29 @@ import App.State.Reducers.BattlesSelectStateSlice import App.State.Reducers.MarkBattlesLoaded import App.State.Reducers.ReduceBattlesSelect import App.State.Reducers.ResetBattlesSelect +import App.State.InitSuspiciousState +import App.State.InitUserLevels +import App.State.SetGearScore +import App.State.SetTankInfo +import App.State.SetTankResistance +import App.State.TankInfoCC +import App.State.TankResistance +import App.State.UpdateUserLevel +import App.State.UpdateUserRank +import App.State.UpdateUserScoreKillsAndDeaths +import App.State.UpdateUserSuspiciousState +import App.State.UpdateUsersClanTag +import App.State.UpdateUsersScoreKillsAndDeaths +import App.State.Reducers.AddBattleUser +import App.State.Reducers.AddBattleUsers +import App.State.Reducers.BattleUsersStateSlice +import App.State.Reducers.MigrateBattleUserTeams +import App.State.Reducers.ReduceBattleUsers +import App.State.Reducers.RemoveOnlineUser +import App.State.Reducers.ResetBattleUsers +import App.State.Reducers.SetUserPremium +import App.State.RemoveTankInfo +import App.State.UserStat import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1218,7 +1241,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 81 个已迁移切片(80 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 82 个已迁移切片(81 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1282,7 +1305,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 81 个已迁移字段,其余 8 个待迁", + "根:迁移表 = 82 个已迁移字段,其余 7 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1306,9 +1329,9 @@ object StateCheck { "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", - "remoteUserDataStorage", "matchmaking", "battlesSelect", + "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", ) && - RootWiring.Unmigrated.size == 8 + RootWiring.Unmigrated.size == 7 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1364,7 +1387,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 81/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 82/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6797,6 +6820,100 @@ object StateCheck { ) } + // 92 切片 reducer:BattleUsers(uP / sr.hqs,13 字段;18 个动作) + run { + fun User(Id: Long): RemoteUserData = RemoteUserData( + userId = Id, uid = "u$Id", rank = 5, hasPremium = true, + clanTag = "T", onlineStatus = "online", battle = null, + ) + val TankInfo = TankInfoCC(1L, null, 2L, 3L, null, 4L) + val Resistance = TankResistance(10, "fire") + + val S0 = BattleUsersStateSlice() + Check("切片 BattleUsers:默认值完整", S0.OnlineUsers.isEmpty() && S0.Stats.isEmpty()) + val S1 = ReduceBattleUsers(S0, AddBattleUser(User(1L), "RED")) + Check( + "切片 BattleUsers:AddBattleUser 写七张表", + S1.OnlineUsers == setOf(1L) && S1.Uids[1L] == "u1" && + S1.Ranks[1L] == 5 && S1.ClanTags[1L] == "T" && + S1.UsersPremium[1L] == true && S1.Teams[1L] == "RED" && + S1.Stats[1L] == User(1L) + ) + Check( + "切片 BattleUsers:AddBattleUsers 批量加入", + ReduceBattleUsers(S1, AddBattleUsers(listOf(User(2L), User(3L)), "BLUE")) + .OnlineUsers == setOf(1L, 2L, 3L) + ) + Check( + "切片 BattleUsers:UpdateUserRank / UpdateUsersClanTag", + ReduceBattleUsers(S1, UpdateUserRank(1L, 9)).Ranks[1L] == 9 && + ReduceBattleUsers(S1, UpdateUsersClanTag(mapOf(1L to "X"))) + .ClanTags[1L] == "X" + ) + Check( + "切片 BattleUsers:两种 stats 写入", + ReduceBattleUsers(S1, UpdateUserScoreKillsAndDeaths(UserStat(0, 0, 0, 0, 4L))) + .Stats[4L] == UserStat(0, 0, 0, 0, 4L) && + ReduceBattleUsers(S1, UpdateUsersScoreKillsAndDeaths(listOf(UserStat(0, 0, 0, 0, 5L)))) + .Stats[5L] == UserStat(0, 0, 0, 0, 5L) + ) + Check( + "切片 BattleUsers:SetGearScore / SetTankResistance", + ReduceBattleUsers(S1, SetGearScore(1L, 100)).GearScores[1L] == 100 && + ReduceBattleUsers(S1, SetTankResistance(1L, listOf(Resistance))) + .TankResistance[1L] == listOf(Resistance) + ) + Check( + "切片 BattleUsers:RemoveOnlineUser / SetUserPremium", + ReduceBattleUsers(S1, RemoveOnlineUser(1L)).OnlineUsers.isEmpty() && + ReduceBattleUsers(S1, SetUserPremium(1L, false)).UsersPremium[1L] == false + ) + Check( + "切片 BattleUsers:可疑用户开关与整体替换", + ReduceBattleUsers(S1, UpdateUserSuspiciousState(1L, true, false)) + .let { it.SuspiciousUsers == setOf(1L) && it.SuspiciousByIpUsers.isEmpty() } && + ReduceBattleUsers(S1, InitSuspiciousState(listOf(2L), listOf(3L))) + .let { + it.SuspiciousUsers == setOf(2L) && + it.SuspiciousByIpUsers == setOf(3L) + } + ) + Check( + "切片 BattleUsers:等级两臂", + ReduceBattleUsers(S1, InitUserLevels(mapOf(1L to 3))).TankLevels[1L] == 3 && + ReduceBattleUsers(S1, UpdateUserLevel(1L, 4)).TankLevels[1L] == 4 + ) + Check( + "切片 BattleUsers:坦克信息写入与移除", + ReduceBattleUsers(S1, SetTankInfo(1L, TankInfo)).TankInfo[1L] == TankInfo && + ReduceBattleUsers(S1, SetTankInfo(1L, TankInfo)).let { + ReduceBattleUsers(it, RemoveTankInfo(1L)).TankInfo.isEmpty() + } + ) + Check( + "切片 BattleUsers:枚举迁移是 no-op、复位清空", + ReduceBattleUsers(S1, MigrateBattleUserTeams) == S1 && + ReduceBattleUsers(S1, ResetBattleUsers) == BattleUsersStateSlice() + ) + Check( + "切片 BattleUsers:无关动作不改状态", + ReduceBattleUsers(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, AddBattleUser(User(7L), "RED")) + Check( + "根:AddBattleUser 只动 battleUsers", + Root1.BattleUsers.OnlineUsers == setOf(7L) && + Root1.BattlesSelect == Root0.BattlesSelect + ) + val W = RootWiring.ByField("battleUsers")!! + Check( + "根:battleUsers 委派到 uP / sr.hqs 且 18 个动作", + W.SliceClass == "uP" && W.Reducer == "sr.hqs" && W.Actions.size == 18 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)