feat(state): 迁移 remoteUserDataStorage(复用 RemoteUserData 协议模型)

This commit is contained in:
TermiNexus committed 2026-10-24 20:23:50 +08:00
1 parent 5327312b13
commit 9e51b695aa
8 files changed
+235 -18

No files matched your search

+11 -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` 动作名。
- 已迁移切片 **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<String, String>`,不新增 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@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
+4 -3
View File
@@ -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",
+4
View File
@@ -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);
@@ -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<Any?, Any?> = emptyMap(),
)
/* ------------------------------------------------------------------ reducer */
private fun Upsert(
Data: Map<Any?, Any?>,
UserId: Any?,
Transform: (RemoteUserData?) -> RemoteUserData,
): Map<Any?, Any?> = 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<Any?, Any?>,
Updates: Map<*, *>?,
Apply: (RemoteUserData, Any?) -> RemoteUserData,
): Map<Any?, Any?> {
var Result = Data
for ((Key, Value) in Updates ?: emptyMap<Any?, Any?>()) {
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
}
@@ -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),
)
}
@@ -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)。已迁移。 */
@@ -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")
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")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
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;
// 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)