From c49d847dd64f2b898da41a898aa74026440f45ef Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 20:23:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20remoteUserD?= =?UTF-8?q?ataStorage=EF=BC=88=E5=A4=8D=E7=94=A8=20RemoteUserData=20?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E6=A8=A1=E5=9E=8B=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 | 7 +- 3DTank/tools/gen_root_wiring.js | 4 + .../Reducers/RemoteUserDataStorageReducer.kt | 125 ++++++++++++++++++ .../jsMain/kotlin/App/State/RootReducer.kt | 7 +- .../src/jsMain/kotlin/App/State/RootState.kt | 5 +- .../src/jsMain/kotlin/App/State/RootWiring.kt | 2 +- .../app/src/jsMain/kotlin/App/StateCheck.kt | 87 +++++++++++- 8 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RemoteUserDataStorageReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 3145e24..1354da9 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` 动作名。 - - 已迁移切片 **78/89**: + - 已迁移切片 **79/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) @@ -392,12 +392,18 @@ `ChatLoaded` 写两个频道开关并置 `loaded`;`AddMessage` 只保留最后 120 条再追加; `ett`/`utt` 开关输入框、`stt` 取反历史面板、`rtt`→`ResetBattleChat` 整片复位。 消息类型直接复用生成物里的协议模型 `ChatMessage`(13 字段)。 + - `remoteUserDataStorage` → `$Q` / `vh.isl`(`RemoteUserDataStorageReducer.kt`):1 字段,8 个分支。 + 按 userId 就地更新缓存里的用户数据:uid / rank / hasPremium / clanTag / onlineStatus / battle; + `UserLeaveBattle` 在「仍在组里」时保留 `inGroup` 标记,`UserLeaveGroup` 清空 battle; + 没有缓存条目的 userId 不新建(原 reducer 直接返回原状态)。 + 用户数据模型直接复用生成物 `RemoteUserData`,战斗 / 在线 / 会员状态用 + `BattleNotifierData` / `OnlineNotifierData` / `PremiumNotifierData`。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 77 个有 reducer 的切片, - `launchParams` 与其余 **11 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 78/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **848 项全绿**,含 89 槽位完整性、77 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 78 个有 reducer 的切片, + `launchParams` 与其余 **10 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 79/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **859 项全绿**,含 89 槽位完整性、78 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `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 1114727..c270776 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T12:02:07.703Z", + "generatedAt": "2026-10-24T12:12:10.770Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -84,7 +84,8 @@ "tankBuilderState", "battleMessage", "maps", - "battleChat" + "battleChat", + "remoteUserDataStorage" ], "AnonymousActions": [ { @@ -2127,7 +2128,7 @@ "UserLeaveGroup" ], "Default": "new $Q()", - "Migrated": false + "Migrated": true }, { "Field": "battlesSelect", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 6cadd04..b3c2cfc 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -802,6 +802,10 @@ const Migrated = { battleMessage: { Type: 'BattleMessageStateSlice', Reduce: 'ReduceBattleMessage' }, maps: { Type: 'MapsStateSlice', Reduce: 'ReduceMaps' }, battleChat: { Type: 'BattleChatStateSlice', Reduce: 'ReduceBattleChat' }, + remoteUserDataStorage: { + Type: 'RemoteUserDataStorageStateSlice', + Reduce: 'ReduceRemoteUserDataStorage', + }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RemoteUserDataStorageReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RemoteUserDataStorageReducer.kt new file mode 100644 index 0000000..38ec61d --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/RemoteUserDataStorageReducer.kt @@ -0,0 +1,125 @@ +// RemoteUserDataStorageReducer.kt —— 远端用户数据缓存切片 +// (对应 module_10032 的 $Q 状态 + RQ.isl) +// +// 证据链: +// 状态类 $Q:1 个字段,toString 名 "RemoteUserDataStorage(userIdToData=…)",缺省 emptyMap()。 +// 根委派:`vh.isl(t, n.zpv_1)`。 +// reducer(8 个实例臂,全部是「按 userId 更新缓存里的 RemoteUserData」): +// xQ(UpdateUids) → uid +// jQ(UpdateRanks) → rank +// SQ(UpdatePremiumStatus)→ hasPremium +// CQ(UpdateClanTags) → clanTag +// EQ(UpdateOnlineStatus) → onlineStatus +// TQ(UserEnterBattle) → battle(inBattle / inGroup / proBattle) +// IQ(UserLeaveBattle) → 仍在组里就保留组标记,否则清空 battle +// AQ(UserLeaveGroup) → 清空 battle +// +// 用户数据模型直接复用生成物里的协议模型 `RemoteUserData` +// (userId / uid / rank / hasPremium / clanTag / onlineStatus / battle)。 +package App.State.Reducers + +import App.State.BattleNotifierData +import App.State.OnlineNotifierData +import App.State.PremiumNotifierData +import App.State.RemoteUserData +import App.State.UpdateClanTags +import App.State.UpdateOnlineStatus +import App.State.UpdatePremiumStatus +import App.State.UpdateRanks +import App.State.UpdateUids +import App.State.UserEnterBattle +import App.State.UserLeaveBattle +import App.State.UserLeaveGroup + +/* ------------------------------------------------------------------ 状态 */ + +data class RemoteUserDataStorageStateSlice( + val UserIdToData: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +private fun Upsert( + Data: Map, + UserId: Any?, + Transform: (RemoteUserData?) -> RemoteUserData, +): Map = Data + (UserId to Transform(Data[UserId] as? RemoteUserData)) + +/** 缓存里缺这条用户时用空壳补上(原 `new qQ(userId, …)`)。 */ +private fun Empty(UserId: Any?): RemoteUserData = RemoteUserData( + userId = UserId, + uid = null, + rank = null, + hasPremium = false, + clanTag = null, + onlineStatus = null, + battle = null, +) + +private fun MergeInto( + Data: Map, + Updates: Map<*, *>?, + Apply: (RemoteUserData, Any?) -> RemoteUserData, +): Map { + var Result = Data + for ((Key, Value) in Updates ?: emptyMap()) { + Result = Upsert(Result, Key) { Existing -> Apply(Existing ?: Empty(Key), Value) } + } + return Result +} + +fun ReduceRemoteUserDataStorage( + State0: RemoteUserDataStorageStateSlice, + Action: Any, +): RemoteUserDataStorageStateSlice = when (Action) { + is UpdateUids -> State0.copy( + UserIdToData = MergeInto(State0.UserIdToData, Action.uids as? Map<*, *>) { User, Value -> + User.copy(uid = Value) + }, + ) + is UpdateRanks -> State0.copy( + UserIdToData = MergeInto(State0.UserIdToData, Action.ranks) { User, Value -> + User.copy(rank = Value) + }, + ) + is UpdatePremiumStatus -> State0.copy( + UserIdToData = MergeInto(State0.UserIdToData, Action.premiums) { User, Value -> + val HasPremium = (Value as? PremiumNotifierData)?.hasPremium?.toBoolean() ?: false + User.copy(hasPremium = HasPremium) + }, + ) + is UpdateClanTags -> State0.copy( + UserIdToData = MergeInto(State0.UserIdToData, Action.clans as? Map<*, *>) { User, Value -> + User.copy(clanTag = Value) + }, + ) + is UpdateOnlineStatus -> State0.copy( + UserIdToData = MergeInto(State0.UserIdToData, Action.status) { User, Value -> + User.copy(onlineStatus = (Value as? OnlineNotifierData)?.online) + }, + ) + is UserEnterBattle -> State0.copy( + UserIdToData = MergeInto(State0.UserIdToData, Action.battles) { User, Value -> + User.copy(battle = Value) + }, + ) + is UserLeaveBattle -> State0.copy( + UserIdToData = + if (!State0.UserIdToData.containsKey(Action.userId)) State0.UserIdToData + else Upsert(State0.UserIdToData, Action.userId) { Existing -> + val User = Existing ?: Empty(Action.userId) + val Battle = User.battle as? BattleNotifierData + if (Battle?.inGroup == true) { + User.copy(battle = BattleNotifierData(inBattle = false, inGroup = true, proBattle = Battle.proBattle)) + } else User.copy(battle = null) + }, + ) + is UserLeaveGroup -> State0.copy( + UserIdToData = + if (!State0.UserIdToData.containsKey(Action.userId)) State0.UserIdToData + else Upsert(State0.UserIdToData, Action.userId) { Existing -> + (Existing ?: Empty(Action.userId)).copy(battle = null) + }, + ) + 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 34332d0..13cf80a 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 出新状态)。 -// 这里采用同一形状。已迁移的 78 个切片里,77 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 79 个切片里,78 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -80,9 +80,10 @@ // battleMessage → ReduceBattleMessage(FE / vT().rqb,根委派 `vT().rqb(t, n.qpw_1)`) // maps → ReduceMaps(OA / Oi.sql,根委派 `Oi.sql(t, n.bpw_1)`) // battleChat → ReduceBattleChat(Q6 / Hf.wtg,根委派 `Hf.wtg(t, n.rpw_1)`) +// remoteUserDataStorage → ReduceRemoteUserDataStorage($Q / vh.isl,根委派 `vh.isl(t, n.zpv_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 11 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 10 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -142,6 +143,7 @@ import App.State.Reducers.ReduceQuestCategories import App.State.Reducers.ReduceQuests import App.State.Reducers.ReduceRankUpDialog import App.State.Reducers.ReduceReferralEvent +import App.State.Reducers.ReduceRemoteUserDataStorage import App.State.Reducers.ReduceRegister import App.State.Reducers.ReduceRewardedVideos import App.State.Reducers.ReduceRulesUpdated @@ -255,6 +257,7 @@ object RootReducer { BattleMessage = ReduceBattleMessage(State0.BattleMessage, Action), Maps = ReduceMaps(State0.Maps, Action), BattleChat = ReduceBattleChat(State0.BattleChat, Action), + RemoteUserDataStorage = ReduceRemoteUserDataStorage(State0.RemoteUserDataStorage, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index cccce04..2995e3f 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -59,6 +59,7 @@ import App.State.Reducers.QuestsStateSlice import App.State.Reducers.RankUpDialogStateSlice import App.State.Reducers.ReferralEventStateSlice import App.State.Reducers.RegisterStateSlice +import App.State.Reducers.RemoteUserDataStorageStateSlice import App.State.Reducers.RewardedVideosStateSlice import App.State.Reducers.RulesUpdatedStateSlice import App.State.Reducers.ScreenStateSlice @@ -140,8 +141,8 @@ data class RootState( val Notifications: NotificationsStateSlice = NotificationsStateSlice(), /** 原 friends(KD / NL().cr9)。已迁移。 */ val Friends: FriendsStateSlice = FriendsStateSlice(), - /** 原 remoteUserDataStorage($Q / vh.isl)。未迁移:原样透传。 */ - val RemoteUserDataStorage: Any? = null, + /** 原 remoteUserDataStorage($Q / vh.isl)。已迁移。 */ + val RemoteUserDataStorage: RemoteUserDataStorageStateSlice = RemoteUserDataStorageStateSlice(), /** 原 battlesSelect(fA / Mi.vqk)。未迁移:原样透传。 */ val BattlesSelect: Any? = null, /** 原 maps(OA / Oi.sql)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 56ae4c4..59cd757 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 8752265..eae9093 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; +// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -745,6 +745,17 @@ import App.State.Reducers.OpenBattleChatInput import App.State.Reducers.ReduceBattleChat import App.State.Reducers.ResetBattleChat import App.State.Reducers.ToggleBattleChatHistory +import App.State.BattleNotifierData +import App.State.RemoteUserData +import App.State.Reducers.ReduceRemoteUserDataStorage +import App.State.Reducers.RemoteUserDataStorageStateSlice +import App.State.UpdateClanTags +import App.State.UpdateOnlineStatus +import App.State.UpdateRanks +import App.State.UpdateUids +import App.State.UserEnterBattle +import App.State.UserLeaveBattle +import App.State.UserLeaveGroup import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -1172,7 +1183,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 78 个已迁移切片(77 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 79 个已迁移切片(78 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1236,7 +1247,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 78 个已迁移字段,其余 11 个待迁", + "根:迁移表 = 79 个已迁移字段,其余 10 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1260,8 +1271,9 @@ object StateCheck { "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", + "remoteUserDataStorage", ) && - RootWiring.Unmigrated.size == 11 + RootWiring.Unmigrated.size == 10 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1317,7 +1329,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 78/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 79/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -6469,6 +6481,71 @@ object StateCheck { ) } + // 89 切片 reducer:RemoteUserDataStorage($Q / vh.isl,1 字段;8 个动作) + run { + val S0 = RemoteUserDataStorageStateSlice() + Check("切片 RemoteUserDataStorage:默认空缓存", S0.UserIdToData.isEmpty()) + val S1 = ReduceRemoteUserDataStorage(S0, UpdateUids(mapOf("1" to "uid1"))) + Check( + "切片 RemoteUserDataStorage:UpdateUids 建条目并写 uid", + (S1.UserIdToData["1"] as RemoteUserData).uid == "uid1" && + (S1.UserIdToData["1"] as RemoteUserData).userId == "1" + ) + val S2 = ReduceRemoteUserDataStorage(S1, UpdateRanks(mapOf("1" to 5))) + Check( + "切片 RemoteUserDataStorage:UpdateRanks 就地更新", + (S2.UserIdToData["1"] as RemoteUserData).rank == 5 && + (S2.UserIdToData["1"] as RemoteUserData).uid == "uid1" + ) + Check( + "切片 RemoteUserDataStorage:UpdateClanTags / UpdateOnlineStatus", + (ReduceRemoteUserDataStorage(S2, UpdateClanTags(mapOf("1" to "TAG"))) + .UserIdToData["1"] as RemoteUserData).clanTag == "TAG" && + (ReduceRemoteUserDataStorage(S2, UpdateOnlineStatus(mapOf("1" to App.State.OnlineNotifierData("online")))) + .UserIdToData["1"] as RemoteUserData).onlineStatus == "online" + ) + val S3 = ReduceRemoteUserDataStorage( + S2, + UserEnterBattle(mapOf("1" to BattleNotifierData(true, true, ""))), + ) + Check( + "切片 RemoteUserDataStorage:UserEnterBattle 写 battle", + (S3.UserIdToData["1"] as RemoteUserData).battle is BattleNotifierData + ) + val S4 = ReduceRemoteUserDataStorage(S3, UserLeaveBattle("1")) + Check( + "切片 RemoteUserDataStorage:UserLeaveBattle 保留组标记、清战斗位", + (S4.UserIdToData["1"] as RemoteUserData).battle == + BattleNotifierData(false, true, "") + ) + Check( + "切片 RemoteUserDataStorage:UserLeaveGroup 清空 battle", + (ReduceRemoteUserDataStorage(S4, UserLeaveGroup("1")) + .UserIdToData["1"] as RemoteUserData).battle == null + ) + Check( + "切片 RemoteUserDataStorage:未知用户不新建条目", + ReduceRemoteUserDataStorage(S0, UserLeaveBattle("9")).UserIdToData.isEmpty() + ) + Check( + "切片 RemoteUserDataStorage:无关动作不改状态", + ReduceRemoteUserDataStorage(S4, AddFrags(1)) == S4 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, UpdateUids(mapOf("2" to "u2"))) + Check( + "根:UpdateUids 只动 remoteUserDataStorage", + (Root1.RemoteUserDataStorage.UserIdToData["2"] as RemoteUserData).uid == "u2" && + Root1.BattleChat == Root0.BattleChat + ) + val W = RootWiring.ByField("remoteUserDataStorage")!! + Check( + "根:remoteUserDataStorage 委派到 \$Q / vh.isl 且 8 个动作", + W.SliceClass == "\$Q" && W.Reducer == "vh.isl" && W.Actions.size == 8 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)