diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 3913e47..b6a62dd 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` 动作名。 - - 已迁移切片 **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`,不新增 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@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 3756ae1..120ae7d 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -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", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 7cef127..c23021d 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -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( diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MapsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MapsReducer.kt new file mode 100644 index 0000000..5ac063a --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/MapsReducer.kt @@ -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 = emptyList(), +) + +data class MapsDescriptionEntry( + val MapId: Long, + val Name: Any?, + val DefaultTheme: String, + val Themes: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ 状态 */ + +data class MapsStateSlice( + val MapDescriptionsById: Map = 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 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt index 01b0401..6a9401a 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 出新状态)。 -// 这里采用同一形状。已迁移的 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), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 82bb511..c61be38 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -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)。未迁移:原样透传。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 31b3526..8683db2 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") + 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") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ 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 dd2b8e9..cdcbd96 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; +// 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("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("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(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)