feat(state): 迁移 maps 并补地图描述协议模型

This commit is contained in:
TermiNexus committed 2026-10-24 19:56:41 +08:00
1 parent 9cf760b354
commit 8dac8d96e8
8 files changed
+164 -19

No files matched your search

+9 -5
View File
@@ -143,7 +143,7 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **76/89**:
- 已迁移切片 **77/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)
@@ -384,12 +384,16 @@
`LocalizedMessage(…, content, arguments, isHidden)`、`LocalizedUserMessage(…, content, userTeamRelation, userUid)`、
`LocalizedTeamMessage(…, content, teamRelation)`;`HideMessage` 只把 `hideTimeMs` 匹配的消息标隐藏,
`RemoveMessage` 删掉匹配项,`sT`→`ResetBattleMessages` 整片复位。
- `maps` → `OA` / `Oi.sql`(`MapsReducer.kt`):1 字段,1 个分支。
`AddMap` 的动作载荷就是生成物里的 `MapInfoCC`(11 个字段,与源码 properties 一致),
直接复用;reducer 以 `info.mapId` 为键新建 / 复用 `MapsDescriptionEntry`,
再把 `MapsThemeEntry` 写进 `themes[info.theme]`(同地图多主题是合并)。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 75 个有 reducer 的切片,
`launchParams` 与其余 **13 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 76/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **832 项全绿**,含 89 槽位完整性、75 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 76 个有 reducer 的切片,
`launchParams` 与其余 **12 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 77/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **838 项全绿**,含 89 槽位完整性、76 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
+9 -3
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-24T11:41:22.721Z",
"generatedAt": "2026-10-24T11:49:38.715Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -82,7 +82,8 @@
"battleSettings",
"contractState",
"tankBuilderState",
"battleMessage"
"battleMessage",
"maps"
],
"AnonymousActions": [
{
@@ -1381,6 +1382,11 @@
"Name": "ResetBattleMessages",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "new FE() 整片复位"
},
{
"Local": "FA",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "按 info.mapId 新建/复用 MapDescription,并把 MapInfo 写进 themes[themeId](原名 AddMap)"
}
],
"Markers": [
@@ -2139,7 +2145,7 @@
"AddMap"
],
"Default": "new OA()",
"Migrated": false
"Migrated": true
},
{
"Field": "matchmaking",
+2
View File
@@ -795,6 +795,7 @@ const Migrated = {
contractState: { Type: 'ContractStateSlice', Reduce: 'ReduceContractState' },
tankBuilderState: { Type: 'TankBuilderStateSlice', Reduce: 'ReduceTankBuilder' },
battleMessage: { Type: 'BattleMessageStateSlice', Reduce: 'ReduceBattleMessage' },
maps: { Type: 'MapsStateSlice', Reduce: 'ReduceMaps' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -1202,6 +1203,7 @@ const AppSettingsAnonymousEffects = {
Hnt: '只保留 rewards 非空的合同',
Trt: 'new brt() 整片复位',
sT: 'new FE() 整片复位',
FA: '按 info.mapId 新建/复用 MapDescription,并把 MapInfo 写进 themes[themeId](原名 AddMap)',
};
fs.writeFileSync(
@@ -0,0 +1,81 @@
// MapsReducer.kt —— 地图描述切片(对应 module_10032 的 OA 状态 + GA.sql)
//
// 证据链:
// 状态类 OA:1 个字段,toString 名 "Maps(mapDescriptionsById=…)",缺省 emptyMap()。
// 根委派:`Oi.sql(t, n.bpw_1)`。
// 动作 `FA`(AddMap)的 toString 名 "AddMap(id=…, info=…)",载荷类型就是生成物里的 `MapInfoCC`
// (`defaultTheme / enabled / mapId / mapName / matchmakingMark / maxPeople / preview /
// rankLimit / min / supportedModes / theme`,与源码 `properties` 一一对应)。
// reducer:
// FA → 以 `info.mapId` 为键:已有 `LA`(MapDescription)就复用,否则新建
// `MapDescription(mapId, mapName, defaultTheme, emptyMap())`;
// 再把本次主题写进 `themes[info.theme]`(`DA` = MapInfo)。
//
// 生成物里另有同名的 `MapDescription` / `MapInfo`(字段是 Int 版),为避免歧义,
// 这里用 `MapsDescriptionEntry` / `MapsThemeEntry` 两个包内模型。
package App.State.Reducers
import App.State.AddMap
/* ------------------------------------------------------------------ 模型 */
data class MapsThemeEntry(
val Theme: String,
val MapName: Any?,
val Enabled: Boolean,
val DefaultTheme: String,
val MatchmakingMark: Boolean,
val MaxPeople: Int,
val Preview: Any?,
val RankLimit: String,
val Min: String,
val SupportedModes: List<String> = emptyList(),
)
data class MapsDescriptionEntry(
val MapId: Long,
val Name: Any?,
val DefaultTheme: String,
val Themes: Map<Any?, Any?> = emptyMap(),
)
/* ------------------------------------------------------------------ 状态 */
data class MapsStateSlice(
val MapDescriptionsById: Map<Any?, Any?> = emptyMap(),
)
/* ------------------------------------------------------------------ reducer */
fun ReduceMaps(
State0: MapsStateSlice,
Action: Any,
): MapsStateSlice = when (Action) {
is AddMap -> {
val Info = Action.info
val Existing = State0.MapDescriptionsById[Info.mapId] as? MapsDescriptionEntry
val Base = Existing ?: MapsDescriptionEntry(
MapId = Info.mapId,
Name = Info.mapName,
DefaultTheme = Info.defaultTheme,
)
val Entry = MapsThemeEntry(
Theme = Info.theme,
MapName = Info.mapName,
Enabled = Info.enabled,
DefaultTheme = Info.defaultTheme,
MatchmakingMark = Info.matchmakingMark,
MaxPeople = Info.maxPeople,
Preview = Info.preview,
RankLimit = Info.rankLimit,
Min = Info.min,
SupportedModes = Info.supportedModes,
)
State0.copy(
MapDescriptionsById = State0.MapDescriptionsById + (
Info.mapId to Base.copy(Themes = Base.Themes + (Info.theme to Entry))
),
)
}
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 76 个切片里,75 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 77 个切片里,76 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -78,9 +78,10 @@
// contractState → ReduceContractState(Ont / xv.bti,根委派 `xv.bti(t, n.fpy_1)`)
// tankBuilderState → ReduceTankBuilder(brt / wp.ati,根委派 `wp.ati(t, n.epy_1)`)
// battleMessage → ReduceBattleMessage(FE / vT().rqb,根委派 `vT().rqb(t, n.qpw_1)`)
// maps → ReduceMaps(OA / Oi.sql,根委派 `Oi.sql(t, n.bpw_1)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 13 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 12 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -121,6 +122,7 @@ import App.State.Reducers.ReduceLocalBattleUser
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceLootBoxes
import App.State.Reducers.ReduceMainQuest
import App.State.Reducers.ReduceMaps
import App.State.Reducers.ReduceAvailabilityByRank
import App.State.Reducers.ReduceLobbyLoader
import App.State.Reducers.ReduceMigrationState
@@ -249,6 +251,7 @@ object RootReducer {
ContractState = ReduceContractState(State0.ContractState, Action),
TankBuilderState = ReduceTankBuilder(State0.TankBuilderState, Action),
BattleMessage = ReduceBattleMessage(State0.BattleMessage, Action),
Maps = ReduceMaps(State0.Maps, Action),
)
}
@@ -41,6 +41,7 @@ import App.State.Reducers.LocalBattleUserStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.LootBoxesStateSlice
import App.State.Reducers.MainQuestStateSlice
import App.State.Reducers.MapsStateSlice
import App.State.Reducers.MigrationStateSlice
import App.State.Reducers.ModalScreenStateSlice
import App.State.Reducers.ModerationStateSlice
@@ -142,8 +143,8 @@ data class RootState(
val RemoteUserDataStorage: Any? = null,
/** 原 battlesSelect(fA / Mi.vqk)。未迁移:原样透传。 */
val BattlesSelect: Any? = null,
/** 原 maps(OA / Oi.sql)。未迁移:原样透传。 */
val Maps: Any? = null,
/** 原 maps(OA / Oi.sql)。已迁移。 */
val Maps: MapsStateSlice = MapsStateSlice(),
/** 原 matchmaking(TH / JH().qrx)。未迁移:原样透传。 */
val Matchmaking: Any? = null,
/** 原 battleCreateState($6 / Df.ttg)。未迁移:原样透传。 */
@@ -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")
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")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
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;
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -730,6 +730,12 @@ import App.State.Reducers.LocalizedUserMessage
import App.State.Reducers.ReduceBattleMessage
import App.State.Reducers.ResetBattleMessages
import App.State.Reducers.TextMessage
import App.State.AddMap
import App.State.MapInfoCC
import App.State.Reducers.MapsDescriptionEntry
import App.State.Reducers.MapsStateSlice
import App.State.Reducers.MapsThemeEntry
import App.State.Reducers.ReduceMaps
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
@@ -1157,7 +1163,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 76 个已迁移切片(75 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 77 个已迁移切片(76 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -1221,7 +1227,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 76 个已迁移字段,其余 13 个待迁",
"根:迁移表 = 77 个已迁移字段,其余 12 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
@@ -1244,9 +1250,9 @@ object StateCheck {
"clanChatState", "battleSearchState", "battleStatistics",
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
"graphicsSettings", "controlSettings", "battleSettings", "contractState",
"tankBuilderState", "battleMessage",
"tankBuilderState", "battleMessage", "maps",
) &&
RootWiring.Unmigrated.size == 13
RootWiring.Unmigrated.size == 12
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -1302,7 +1308,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 76/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 77/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -6349,6 +6355,48 @@ object StateCheck {
)
}
// 87 切片 reducer:Maps(OA / Oi.sql,1 字段;AddMap)
run {
fun Info(Theme: String): MapInfoCC = MapInfoCC(
defaultTheme = "summer", enabled = true, mapId = 1L, mapName = "Desert",
matchmakingMark = true, maxPeople = 16, preview = null, rankLimit = "1-5",
min = "1", supportedModes = listOf("DM"), theme = Theme,
)
val S0 = MapsStateSlice()
Check("切片 Maps:默认空表", S0.MapDescriptionsById.isEmpty())
val S1 = ReduceMaps(S0, AddMap(0L, Info("summer")))
val Desc1 = S1.MapDescriptionsById[1L] as MapsDescriptionEntry
Check(
"切片 Maps:AddMap 新建地图描述并写入模式条目",
Desc1.Name == "Desert" && Desc1.DefaultTheme == "summer" &&
Desc1.Themes.keys == setOf<Any?>("summer") &&
(Desc1.Themes["summer"] as MapsThemeEntry).MapName == "Desert"
)
val S2 = ReduceMaps(S1, AddMap(1L, Info("winter")))
val Desc2 = S2.MapDescriptionsById[1L] as MapsDescriptionEntry
Check(
"切片 Maps:同地图新增主题是合并而不是覆盖",
Desc2.Themes.keys == setOf<Any?>("summer", "winter") &&
(Desc2.Themes["winter"] as MapsThemeEntry).Theme == "winter"
)
Check("切片 Maps:无关动作不改状态", ReduceMaps(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddMap(0L, Info("summer")))
Check(
"根:AddMap 只动 maps",
Root1.Maps.MapDescriptionsById.keys == setOf<Any?>(1L) &&
Root1.BattleMessage == Root0.BattleMessage
)
val W = RootWiring.ByField("maps")!!
Check(
"根:maps 委派到 OA / Oi.sql",
W.SliceClass == "OA" && W.Reducer == "Oi.sql" &&
W.Actions == listOf("AddMap")
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)