feat(state): 批量迁移 accountSettings/proBattle/language/announcement 切片

This commit is contained in:
TermiNexus committed 2026-10-24 12:19:13 +08:00
1 parent 6947fcb567
commit 006e6a7ebd
17 files changed
+749 -53

No files matched your search

+20 -6
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` 动作名。
- 已迁移切片 **14/89**:
- 已迁移切片 **18/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)
@@ -197,12 +197,25 @@
- `settings` → `Uut` / `xw.ltg`(`SettingsReducer.kt`):2 个字段,2 个 `instanceof` 臂。
`currentCategory` 是设置页分类枚举(`ACCOUNT`…`SUPPORT`,默认 `GAME`),
`disableAccountSettings` 默认 false;`Fut`→`SetSettingsCategory`、`Gut`→`DisableAccountSettings`。
- `accountSettings` → `oet` / `Lp.mtg`(`AccountSettingsReducer.kt`):13 字段,13 个 `instanceof` 臂。
`newPasswordStatus` 用 `PasswordStatus`(`NONE/OK/SIMPLE/NOT_EQUALS`);
`ResetPasswordFields` 只清三个密码输入、状态和解锁按钮;`removalTimer` 存时间戳 `Long?`;
`locationCheckEnabled` 没有写入臂;11 个匿名动作按效果定名并登记。
- `proBattle` → `Wtt` / `uv.wth`(`ProBattleReducer.kt`):3 字段,2 个 `instanceof` 臂。
默认 `lastFightBattleId=-1`、`lastFightBattleTeam=NONE`、`battlePasswords=emptyMap()`;
`Fight` 写 id/team,`SetPassword` 空串删键、非空覆盖。登录切片的 `SetPassword` 是不同类,
这里用 `App.State.Reducers.SetPassword(battleId, password)`,避免丢 `battleId`。
- `language` → 枚举值 / `m6().jtg`(`LanguageReducer.kt`):`language` 在根状态里是枚举对象本身,
默认 `EN`;`d6`→`SetLanguage` 只写回语言,不包 StateSlice。
- `announcement` → `lC` / `PC().rq5`(`AnnouncementReducer.kt`):5 字段,4 个 `instanceof` 臂。
`isEnabled` 缺省 true,其余 false/空列表;`bC`→`DisableAnnouncement`、`qC`→`SetAnnouncementModelLoaded`、
`CC`→`SetAnnouncementItems`(生成物 `Init` 属于另一个切片,已改名)、`$C`→`SetSkipNextPromotedAnnounce`。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 13 个有 reducer 的切片,
`launchParams` 与其余 **75 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 14/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **325 项全绿**,含 89 槽位完整性、13 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 17 个有 reducer 的切片,
`launchParams` 与其余 **71 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 18/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **350 项全绿**,含 89 槽位完整性、17 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
@@ -212,7 +225,8 @@
效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示);
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等);
screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、
entrance 的 6 个、user 的 3 个、settings 的 2 个匿名类同样按效果定名并登记。
entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、
language 的 1 个、announcement 的 3 个匿名类同样按效果定名并登记。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+115 -21
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-23T09:11:41.133Z",
"generatedAt": "2026-10-23T09:52:15.365Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -20,6 +20,10 @@
"entrance",
"user",
"settings",
"accountSettings",
"proBattle",
"language",
"announcement",
"launchParams"
],
"AnonymousActions": [
@@ -527,6 +531,96 @@
"Name": "DisableAccountSettings",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "disableAccountSettings = true(`n.bu8(cd, !0)`)"
},
{
"Local": "aet",
"Name": "ResetPasswordFields",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "currentPassword/newPassword/repeatNewPassword = \"\",newPasswordStatus = NONE,isUnlockButtonForRenameUid = false"
},
{
"Local": "fet",
"Name": "SetCurrentPassword",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "currentPassword = action.currentPassword"
},
{
"Local": "vet",
"Name": "SetNewPassword",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "newPassword = action.newPassword"
},
{
"Local": "pet",
"Name": "SetRepeatNewPassword",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "repeatNewPassword = action.repeatNewPassword"
},
{
"Local": "wet",
"Name": "SetPasswordStatus",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "newPasswordStatus = action.status"
},
{
"Local": "det",
"Name": "SetCurrentPasswordMandatory",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isCurrentPasswordMandatory = action.isMandatory"
},
{
"Local": "get",
"Name": "SetEmail",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "email = action.email"
},
{
"Local": "met",
"Name": "SetEmailConfirmed",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "emailConfirmed = action.emailConfirmed"
},
{
"Local": "cet",
"Name": "SetCountry",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "countryCode / countryName = action 的两个字段"
},
{
"Local": "ket",
"Name": "SetUnlockButtonForRenameUid",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isUnlockButtonForRenameUid = action.isUnlockButtonForRenameUid"
},
{
"Local": "xet",
"Name": "ClearAccountRemovalTimer",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "removalTimer = null"
},
{
"Local": "d6",
"Name": "SetLanguage",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "language = action.language(`t instanceof d6 ? t.itg_1 : n`)"
},
{
"Local": "bC",
"Name": "DisableAnnouncement",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isEnabled = false"
},
{
"Local": "qC",
"Name": "SetAnnouncementModelLoaded",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "modelLoaded = true"
},
{
"Local": "CC",
"Name": "SetAnnouncementItems",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "items = action.items"
}
],
"Markers": [
@@ -1131,22 +1225,22 @@
"Reducer": "Lp.mtg",
"Delegate": "Lp.mtg(t, n.vpv_1)",
"Actions": [
"?aet",
"?cet",
"?det",
"?fet",
"?get",
"?ket",
"?met",
"?pet",
"?vet",
"?wet",
"?xet",
"ClearAccountRemovalTimer",
"ClearInputUid",
"SetAccountRemovalDays"
"ResetPasswordFields",
"SetAccountRemovalDays",
"SetCountry",
"SetCurrentPassword",
"SetCurrentPasswordMandatory",
"SetEmail",
"SetEmailConfirmed",
"SetNewPassword",
"SetPasswordStatus",
"SetRepeatNewPassword",
"SetUnlockButtonForRenameUid"
],
"Default": "new oet()",
"Migrated": false
"Migrated": true
},
{
"Field": "lootBoxes",
@@ -1838,10 +1932,10 @@
"Reducer": "m6().jtg",
"Delegate": "m6().jtg(t, n.gpx_1)",
"Actions": [
"?d6"
"SetLanguage"
],
"Default": "tj()",
"Migrated": false
"Migrated": true
},
{
"Field": "launchParams",
@@ -1969,13 +2063,13 @@
"Reducer": "PC().rq5",
"Delegate": "PC().rq5(t, n.opx_1)",
"Actions": [
"?bC",
"?qC",
"Init",
"DisableAnnouncement",
"SetAnnouncementItems",
"SetAnnouncementModelLoaded",
"SetSkipNextPromotedAnnounce"
],
"Default": "new lC()",
"Migrated": false
"Migrated": true
},
{
"Field": "notificationByCountry",
@@ -2165,7 +2259,7 @@
"SetPassword"
],
"Default": "new Wtt()",
"Migrated": false
"Migrated": true
},
{
"Field": "availabilityByRank",
+4
View File
@@ -569,6 +569,10 @@ function InferDeep(Expr, Mod, Off, Depth) {
// 类型定义见 TankiOffline/.../App/State/ScreenId.kt。
const FieldTypeOverrides = new Map([
['GoTo.screen', 'ScreenId'],
// proBattle 切片:Fight 的字段在别处调用点只能推成 Any?,reducer 需要真实类型。
// battleId → Long,battleTeam → BattleTeam(module_31908 的 TEAM_A/TEAM_B/NONE)。
['Fight.battleId', 'Long'],
['Fight.battleTeam', 'BattleTeam'],
// lootBoxes 切片:lH/qH/yH 的字段在别处的调用点只能给出 Boolean / Any?,但 reducer 里分别是
// ContainerType(默认 Ly()=STANDARD)、可空 ContainerType、及补给箱奖励列表。
['SetCurrentContainer.type', 'ContainerType'],
+80 -1
View File
@@ -136,6 +136,28 @@ const AnonymousActionNames = new Map([
// 括号里是原 reducer 的效果;实现见 SettingsReducer.kt。
['Fut', 'SetSettingsCategory'],
['Gut', 'DisableAccountSettings'],
// accountSettings 切片(oet / Lp.mtg):11 个没有 toString / 不在 action_inventory 的类。
// 括号里是原 reducer 的效果;实现见 AccountSettingsReducer.kt。
['aet', 'ResetPasswordFields'],
['fet', 'SetCurrentPassword'],
['vet', 'SetNewPassword'],
['pet', 'SetRepeatNewPassword'],
['wet', 'SetPasswordStatus'],
['det', 'SetCurrentPasswordMandatory'],
['get', 'SetEmail'],
['met', 'SetEmailConfirmed'],
['cet', 'SetCountry'],
['ket', 'SetUnlockButtonForRenameUid'],
['xet', 'ClearAccountRemovalTimer'],
// language 切片(m6().jtg):1 个没有 toString / 不在 action_inventory 的类。
// 实现见 LanguageReducer.kt。
['d6', 'SetLanguage'],
// announcement 切片(lC / PC().rq5):3 个没有 toString / 不在 action_inventory 的类。
// CC 在 action_inventory 里叫 Init,但生成物 App.State.Init 是另一个切片的同名类,这里改名。
// 实现见 AnnouncementReducer.kt。
['bC', 'DisableAnnouncement'],
['qC', 'SetAnnouncementModelLoaded'],
['CC', 'SetAnnouncementItems'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -149,7 +171,9 @@ const NonActionInstanceof = new Map([
/** 动作局部类名 → 动作名;未收录的返回 null。 */
function ActionNameOf(Local) {
return LocalToName.get(Local) || AnonymousActionNames.get(Local) || null;
// 匿名表优先:个别同名类(如 announcement 的 CC=Init)会用生成物里的名字,
// 但该生成物属于另一个切片,必须在匿名表里显式改名。
return AnonymousActionNames.get(Local) || LocalToName.get(Local) || null;
}
// ---------------------------------------------------------------- 源码扫描基础
@@ -509,6 +533,16 @@ const Migrated = {
entrance: { Type: 'EntranceStateSlice', Reduce: 'ReduceEntrance' },
user: { Type: 'UserStateSlice', Reduce: 'ReduceUser' },
settings: { Type: 'SettingsStateSlice', Reduce: 'ReduceSettings' },
accountSettings: { Type: 'AccountSettingsStateSlice', Reduce: 'ReduceAccountSettings' },
proBattle: { Type: 'ProBattleStateSlice', Reduce: 'ReduceProBattle' },
// language 在原根状态里是枚举对象本身,不是对象切片;Default/Import 显式指定。
language: {
Type: 'Language',
Default: 'App.State.Language.EN',
Reduce: 'ReduceLanguage',
Import: false,
},
announcement: { Type: 'AnnouncementStateSlice', Reduce: 'ReduceAnnouncement' },
// 原根 reducer 对这一片没有调用,只有默认值 `ag()`(空 Map);这里仅做类型收口。
launchParams: {
Type: 'LaunchParamsStateSlice',
@@ -651,6 +685,33 @@ const SettingsAnonymousEffects = {
Gut: 'disableAccountSettings = true(`n.bu8(cd, !0)`)',
};
// accountSettings 切片(oet / Lp.mtg):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const AccountSettingsAnonymousEffects = {
aet: 'currentPassword/newPassword/repeatNewPassword = "",newPasswordStatus = NONE,isUnlockButtonForRenameUid = false',
fet: 'currentPassword = action.currentPassword',
vet: 'newPassword = action.newPassword',
pet: 'repeatNewPassword = action.repeatNewPassword',
wet: 'newPasswordStatus = action.status',
det: 'isCurrentPasswordMandatory = action.isMandatory',
get: 'email = action.email',
met: 'emailConfirmed = action.emailConfirmed',
cet: 'countryCode / countryName = action 的两个字段',
ket: 'isUnlockButtonForRenameUid = action.isUnlockButtonForRenameUid',
xet: 'removalTimer = null',
};
// language 切片(m6().jtg):只把动作里的语言枚举写回。
const LanguageAnonymousEffects = {
d6: 'language = action.language(`t instanceof d6 ? t.itg_1 : n`)',
};
// announcement 切片(lC / PC().rq5):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
const AnnouncementAnonymousEffects = {
bC: 'isEnabled = false',
qC: 'modelLoaded = true',
CC: 'items = action.items',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
JSON.stringify(
@@ -715,6 +776,24 @@ fs.writeFileSync(
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(AccountSettingsAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(LanguageAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(AnnouncementAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
],
Markers: [
{
@@ -0,0 +1,14 @@
// AccountSettingsModels.kt —— accountSettings 切片用到的枚举(App.State 包内)
//
// 证据链:module_10032 的 `_et()` 初始化
// `NONE=0, OK=1, SIMPLE=2, NOT_EQUALS=3`;`Ret()` 返回 NONE,
// 即 `oet.newPasswordStatus` 的默认值。
package App.State
/** 原 `het`:新密码校验状态,顺序与枚举 ordinal 一致。 */
enum class PasswordStatus {
NONE,
OK,
SIMPLE,
NOT_EQUALS,
}
@@ -721,7 +721,7 @@ object ActionCatalog {
ActionMeta("SelectBattle", SelectBattle::class, listOf(ActionFieldMeta("battleId", "Long"))),
ActionMeta("SelectBattleFormat", SelectBattleFormat::class, listOf(ActionFieldMeta("format", "BattleFormat"))),
ActionMeta("ProBattleState", ProBattleState::class, listOf(ActionFieldMeta("lastFightBattleId", "Any?"), ActionFieldMeta("lastFightBattleTeam", "Any?"), ActionFieldMeta("battlePasswords", "Any?"))),
ActionMeta("Fight", Fight::class, listOf(ActionFieldMeta("battleId", "Any?"), ActionFieldMeta("battleTeam", "Any?"))),
ActionMeta("Fight", Fight::class, listOf(ActionFieldMeta("battleId", "Long"), ActionFieldMeta("battleTeam", "BattleTeam"))),
ActionMeta("Spectate", Spectate::class, listOf(ActionFieldMeta("battleId", "Any?"))),
ActionMeta("CheckPassword", CheckPassword::class, listOf(ActionFieldMeta("battleId", "Any?"), ActionFieldMeta("password", "Any?"))),
ActionMeta("GoToBattleInfo", GoToBattleInfo::class, listOf(ActionFieldMeta("battleId", "Any?"))),
@@ -5,7 +5,7 @@ package App.State
import Core.Vector3
// 动作/状态类共 816 个,字段 2150 个(已定类型 584 个)
// 动作/状态类共 816 个,字段 2150 个(已定类型 586 个)
data class AnnouncementState(
val isEnabled: Any?, // 仅 copy 转发(无类型信息)
val modelLoaded: Any?, // 仅 copy 转发(无类型信息)
@@ -4077,8 +4077,8 @@ data class ProBattleState(
)
data class Fight(
val battleId: Any?, // 调用点: P2t().kar().paq_1.bpy_1.fto_1 | t.props.battleId
val battleTeam: Any?, // 调用点: P2t().kar().paq_1.bpy_1.gto_1 | t.props.battleTeam
val battleId: Long, // override
val battleTeam: BattleTeam, // override
)
data class Spectate(
@@ -0,0 +1,23 @@
// LanguageModels.kt —— language 切片的值类型(App.State 包内)
//
// 证据链:module_52943 的 `lo()` 初始化
// `EN=0, RU=1, DE=2, ES=3, PL=4, PT=5, ZH=6, FR=7, JA=8, TR=9, CS=10, HI=11`;
// `tj()` 返回 `lt`,即根状态 language 的默认值 EN。
// language 在根状态里是枚举对象本身,不是对象切片,因此不额外包一层 StateSlice。
package App.State
/** 原 `fo`:客户端语言,顺序与枚举 ordinal 一致。 */
enum class Language {
EN,
RU,
DE,
ES,
PL,
PT,
ZH,
FR,
JA,
TR,
CS,
HI,
}
@@ -0,0 +1,13 @@
// ProBattleModels.kt —— proBattle 切片用到的枚举(App.State 包内)
//
// 证据链:module_31908 的 `Ir()` 初始化
// `TEAM_A=0, TEAM_B=1, NONE=2`;module_10032 的 `em()` 返回 NONE,
// 即 `Wtt.lastFightBattleTeam` 的默认值。
package App.State
/** 原 `Ar`:上一场战斗的队伍,顺序与枚举 ordinal 一致。 */
enum class BattleTeam {
TEAM_A,
TEAM_B,
NONE,
}
@@ -0,0 +1,88 @@
// AccountSettingsReducer.kt —— 账户设置切片(对应 module_10032 的 oet 状态 + Lp.mtg reducer)
//
// 证据链:
// 状态类 oet:13 个字段,toString 名 "AccountSettings(...)";copy 是 nu6(13 字段 + 回调)。
// 默认:isCurrentPasswordMandatory=false、三个密码字段空串、newPasswordStatus=NONE、
// countryCode/countryName/email=null、emailConfirmed/locationCheckEnabled=false、
// isUnlockButtonForRenameUid/isClearInputUid=false、removalTimer=null。
// 根委派:`Lp.mtg(t, n.vpv_1)`。
// 13 个 instanceof 臂见下方 reducer;`locationCheckEnabled` 没有写入臂。
// `removalTimer` 原值由 `cm(t.au7_1)` 生成 Kotlin Date,这里存时间戳 Long?,语义仍是同一时刻。
package App.State.Reducers
import App.State.ClearInputUid
import App.State.PasswordStatus
import App.State.SetAccountRemovalDays
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `aet`:清空三个密码输入,把校验状态打回 NONE,并复位改名解锁按钮。 */
object ResetPasswordFields
class SetCurrentPassword(val currentPassword: String)
class SetNewPassword(val newPassword: String)
class SetRepeatNewPassword(val repeatNewPassword: String)
class SetPasswordStatus(val status: PasswordStatus)
class SetCurrentPasswordMandatory(val isMandatory: Boolean)
class SetEmail(val email: String?)
class SetEmailConfirmed(val emailConfirmed: Boolean)
class SetCountry(val countryCode: String?, val countryName: String?)
class SetUnlockButtonForRenameUid(val isUnlockButtonForRenameUid: Boolean)
/** 原 `xet`:只把 removalTimer 置 null。 */
object ClearAccountRemovalTimer
/* ------------------------------------------------------------------ 状态 */
data class AccountSettingsStateSlice(
val IsCurrentPasswordMandatory: Boolean = false,
val CurrentPassword: String = "",
val NewPassword: String = "",
val RepeatNewPassword: String = "",
val NewPasswordStatus: PasswordStatus = PasswordStatus.NONE,
val CountryCode: String? = null,
val CountryName: String? = null,
val Email: String? = null,
val EmailConfirmed: Boolean = false,
val LocationCheckEnabled: Boolean = false,
val IsUnlockButtonForRenameUid: Boolean = false,
val IsClearInputUid: Boolean = false,
val RemovalTimer: Long? = null,
)
/* ------------------------------------------------------------------ reducer */
fun ReduceAccountSettings(
State0: AccountSettingsStateSlice,
Action: Any,
): AccountSettingsStateSlice = when (Action) {
is ResetPasswordFields -> State0.copy(
CurrentPassword = "",
NewPassword = "",
RepeatNewPassword = "",
NewPasswordStatus = PasswordStatus.NONE,
IsUnlockButtonForRenameUid = false,
)
is SetCurrentPassword -> State0.copy(CurrentPassword = Action.currentPassword)
is SetNewPassword -> State0.copy(NewPassword = Action.newPassword)
is SetRepeatNewPassword -> State0.copy(RepeatNewPassword = Action.repeatNewPassword)
is SetPasswordStatus -> State0.copy(NewPasswordStatus = Action.status)
is SetCurrentPasswordMandatory -> State0.copy(IsCurrentPasswordMandatory = Action.isMandatory)
is SetEmail -> State0.copy(Email = Action.email)
is SetEmailConfirmed -> State0.copy(EmailConfirmed = Action.emailConfirmed)
is SetCountry -> State0.copy(CountryCode = Action.countryCode, CountryName = Action.countryName)
is SetUnlockButtonForRenameUid ->
State0.copy(IsUnlockButtonForRenameUid = Action.isUnlockButtonForRenameUid)
is ClearInputUid -> State0.copy(IsClearInputUid = Action.isClearInputUid)
is SetAccountRemovalDays -> State0.copy(RemovalTimer = Action.removalTimer)
is ClearAccountRemovalTimer -> State0.copy(RemovalTimer = null)
else -> State0
}
@@ -0,0 +1,51 @@
// AnnouncementReducer.kt —— 公告切片(对应 module_10032 的 lC 状态 + RC.rq5)
//
// 证据链:
// 状态类 lC:5 个字段,toString 名 "AnnouncementState(...)";copy 是 cq0(5 字段 + 回调)。
// 默认:isEnabled=true、modelLoaded=false、items=emptyList()、wasInBattle=false、
// skipNextPromotedAnnounce=false。
// 根委派:`PC().rq5(t, n.opx_1)`。
// reducer 四个 instanceof 臂:
// bC → isEnabled=false
// qC → modelLoaded=true
// CC → items=action.announcementItems(生成物里的 Init 是另一个切片的同名类,故单独建类)
// $C → skipNextPromotedAnnounce=action.skipShowNextAnnounce
// `wasInBattle` 没有写入臂。
package App.State.Reducers
import App.State.SetSkipNextPromotedAnnounce
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `bC`:关闭公告。 */
object DisableAnnouncement
/** 原 `qC`:公告模型已加载。 */
object SetAnnouncementModelLoaded
/** 原 `CC`:写入公告列表。 */
class SetAnnouncementItems(val items: List<Any?>)
/* ------------------------------------------------------------------ 状态 */
data class AnnouncementStateSlice(
val IsEnabled: Boolean = true,
val ModelLoaded: Boolean = false,
val Items: List<Any?> = emptyList(),
val WasInBattle: Boolean = false,
val SkipNextPromotedAnnounce: Boolean = false,
)
/* ------------------------------------------------------------------ reducer */
fun ReduceAnnouncement(
State0: AnnouncementStateSlice,
Action: Any,
): AnnouncementStateSlice = when (Action) {
is DisableAnnouncement -> State0.copy(IsEnabled = false)
is SetAnnouncementModelLoaded -> State0.copy(ModelLoaded = true)
is SetAnnouncementItems -> State0.copy(Items = Action.items)
is SetSkipNextPromotedAnnounce ->
State0.copy(SkipNextPromotedAnnounce = Action.skipShowNextAnnounce)
else -> State0
}
@@ -0,0 +1,20 @@
// LanguageReducer.kt —— 语言切片(对应 module_10032 的 language 值 + g6.jtg)
//
// 证据链:
// 根状态 language 的默认值是 `tj()` → module_52943 的 `vo()` → EN。
// reducer:`t instanceof d6 ? t.itg_1 : n`,即只把动作里的语言枚举写回。
// language 在原根状态里就是枚举对象本身,因此 reducer 直接 (Language, Any) → Language。
package App.State.Reducers
import App.State.Language
/* ------------------------------------------------------------------ 动作 */
class SetLanguage(val language: Language)
/* ------------------------------------------------------------------ reducer */
fun ReduceLanguage(State0: Language, Action: Any): Language = when (Action) {
is SetLanguage -> Action.language
else -> State0
}
@@ -0,0 +1,48 @@
// ProBattleReducer.kt —— 上一场战斗 / 战斗密码切片(对应 module_10032 的 Wtt 状态 + Jtt.wth)
//
// 证据链:
// 状态类 Wtt:3 个字段,toString 名 "ProBattleState(...)";copy 是 jto(3 字段 + 回调)。
// 默认:lastFightBattleId=-1、lastFightBattleTeam=NONE、battlePasswords=emptyMap()。
// 根委派:`uv.wth(t, n.bpy_1)`。
// reducer:
// `t instanceof Vtt ? n.jto(t.lto_1, t.mto_1)`(Fight 只写 id + team)
// `t instanceof Xtt ? password 为空 ? n.jto(cd, cd, am(map, id)) : n.jto(cd, cd, cg(map, Vd(id, password)))`
// `SetPassword` 在登录切片也有同名类(qD,只有 password),这里按 proBattle 的 Xtt 建独立类,
// 避免复用生成物 App.State.SetPassword 导致丢 battleId。
package App.State.Reducers
import App.State.BattleTeam
import App.State.Fight
/* ------------------------------------------------------------------ 动作 */
/**
* 原 `Xtt`:i = battleId,password = 新密码。
* 密码为空时从 map 删除,否则覆盖写入。
*/
class SetPassword(val battleId: Long, val password: String)
/* ------------------------------------------------------------------ 状态 */
data class ProBattleStateSlice(
val LastFightBattleId: Long = -1L,
val LastFightBattleTeam: BattleTeam = BattleTeam.NONE,
val BattlePasswords: Map<Long, String> = emptyMap(),
)
/* ------------------------------------------------------------------ reducer */
fun ReduceProBattle(State0: ProBattleStateSlice, Action: Any): ProBattleStateSlice = when (Action) {
is Fight -> State0.copy(
LastFightBattleId = Action.battleId,
LastFightBattleTeam = Action.battleTeam,
)
is SetPassword -> State0.copy(
BattlePasswords = if (Action.password.isEmpty()) {
State0.BattlePasswords - Action.battleId
} else {
State0.BattlePasswords + (Action.battleId to Action.password)
},
)
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 14 个切片里,13 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 18 个切片里,17 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -16,21 +16,29 @@
// entrance → ReduceEntrance(hD / Bv.pth,根委派 `Bv.pth(t, n.jpx_1)`)
// user → ReduceUser(t8 / M8().qtd,根委派 `M8().qtd(t, n.ypu_1)`)
// settings → ReduceSettings(Uut / xw.ltg,根委派 `xw.ltg(t, n.upv_1)`)
// accountSettings → ReduceAccountSettings(oet / Lp.mtg,根委派 `Lp.mtg(t, n.vpv_1)`)
// proBattle → ReduceProBattle(Wtt / uv.wth,根委派 `uv.wth(t, n.bpy_1)`)
// language → ReduceLanguage(枚举值 / m6().jtg,根委派 `m6().jtg(t, n.gpx_1)`)
// announcement → ReduceAnnouncement(lC / PC().rq5,根委派 `PC().rq5(t, n.opx_1)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 75 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 71 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
import App.State.Reducers.ReduceAccountSettings
import App.State.Reducers.ReduceAnnouncement
import App.State.Reducers.ReduceEntrance
import App.State.Reducers.ReduceGarage
import App.State.Reducers.ReduceGaragePreview
import App.State.Reducers.ReduceHome
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceLanguage
import App.State.Reducers.ReduceLocalBattleUser
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceLootBoxes
import App.State.Reducers.ReduceProBattle
import App.State.Reducers.ReduceScreen
import App.State.Reducers.ReduceSettings
import App.State.Reducers.ReduceShop
@@ -63,6 +71,10 @@ object RootReducer {
Entrance = ReduceEntrance(State0.Entrance, Action),
User = ReduceUser(State0.User, Action),
Settings = ReduceSettings(State0.Settings, Action),
AccountSettings = ReduceAccountSettings(State0.AccountSettings, Action),
ProBattle = ReduceProBattle(State0.ProBattle, Action),
Language = ReduceLanguage(State0.Language, Action),
Announcement = ReduceAnnouncement(State0.Announcement, Action),
)
}
@@ -4,6 +4,8 @@
// 已迁移的切片用强类型;其余保持 Any?(原样透传),迁移一个改一个再重新生成。
package App.State
import App.State.Reducers.AccountSettingsStateSlice
import App.State.Reducers.AnnouncementStateSlice
import App.State.Reducers.EntranceStateSlice
import App.State.Reducers.GarageStateSlice
import App.State.Reducers.GaragePreviewStateSlice
@@ -12,6 +14,7 @@ import App.State.Reducers.InviteStateSlice
import App.State.Reducers.LocalBattleUserStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.LootBoxesStateSlice
import App.State.Reducers.ProBattleStateSlice
import App.State.Reducers.ScreenStateSlice
import App.State.Reducers.SettingsStateSlice
import App.State.Reducers.ShopStateSlice
@@ -69,8 +72,8 @@ data class RootState(
val News: Any? = null,
/** 原 settings(Uut / xw.ltg)。已迁移。 */
val Settings: SettingsStateSlice = SettingsStateSlice(),
/** 原 accountSettings(oet / Lp.mtg)。未迁移:原样透传。 */
val AccountSettings: Any? = null,
/** 原 accountSettings(oet / Lp.mtg)。已迁移。 */
val AccountSettings: AccountSettingsStateSlice = AccountSettingsStateSlice(),
/** 原 lootBoxes(aH / ws.wrv)。已迁移。 */
val LootBoxes: LootBoxesStateSlice = LootBoxesStateSlice(),
/** 原 notifications(? / B_.ise)。未迁移:原样透传。 */
@@ -143,8 +146,8 @@ data class RootState(
val TouchscreenHudSettings: Any? = null,
/** 原 mainQuest(WZ / ih.bsk)。未迁移:原样透传。 */
val MainQuest: Any? = null,
/** 原 language(? / m6().jtg)。未迁移:原样透传。 */
val Language: Any? = null,
/** 原 language(? / m6().jtg)。已迁移。 */
val Language: Language = App.State.Language.EN,
/** 原 launchParams(? / (pass-through))。已迁移(透传,强类型)。 */
val LaunchParams: LaunchParamsStateSlice = emptyMap(),
/** 原 partner(UJ / (inline@partner))。未迁移:原样透传。 */
@@ -159,8 +162,8 @@ data class RootState(
val Moderation: Any? = null,
/** 原 rulesUpdated(i1 / Ch.dsm)。未迁移:原样透传。 */
val RulesUpdated: Any? = null,
/** 原 announcement(lC / PC().rq5)。未迁移:原样透传。 */
val Announcement: Any? = null,
/** 原 announcement(lC / PC().rq5)。已迁移。 */
val Announcement: AnnouncementStateSlice = AnnouncementStateSlice(),
/** 原 notificationByCountry(gJ / R_.ssd)。未迁移:原样透传。 */
val NotificationByCountry: Any? = null,
/** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */
@@ -185,8 +188,8 @@ data class RootState(
val SuppliesTutorial: Any? = null,
/** 原 questCategories(zZ / th.lsh)。未迁移:原样透传。 */
val QuestCategories: Any? = null,
/** 原 proBattle(Wtt / uv.wth)。未迁移:原样透传。 */
val ProBattle: Any? = null,
/** 原 proBattle(Wtt / uv.wth)。已迁移。 */
val ProBattle: ProBattleStateSlice = ProBattleStateSlice(),
/** 原 availabilityByRank(tet / $p.xth)。未迁移:原样透传。 */
val AvailabilityByRank: Any? = null,
/** 原 userReportsState(Zst / qot.zth)。未迁移:原样透传。 */
@@ -37,7 +37,7 @@ object RootWiring {
SliceWiring("tipsState", "spv_1", "dst", "Hw.vth", "Hw.vth(t, n.spv_1)", listOf("UpdateTips")),
SliceWiring("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("?hJ", "AddNewsItemAction", "RemoveNewsItemAction", "SetNewsItemAction", "SetUnreadNewsCounter")),
SliceWiring("settings", "upv_1", "Uut", "xw.ltg", "xw.ltg(t, n.upv_1)", listOf("DisableAccountSettings", "SetSettingsCategory")),
SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("?aet", "?cet", "?det", "?fet", "?get", "?ket", "?met", "?pet", "?vet", "?wet", "?xet", "ClearInputUid", "SetAccountRemovalDays")),
SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("ClearAccountRemovalTimer", "ClearInputUid", "ResetPasswordFields", "SetAccountRemovalDays", "SetCountry", "SetCurrentPassword", "SetCurrentPasswordMandatory", "SetEmail", "SetEmailConfirmed", "SetNewPassword", "SetPasswordStatus", "SetRepeatNewPassword", "SetUnlockButtonForRenameUid")),
SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("AddKeys", "ClearRewards", "LootBoxOpeningFailed", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsBlockOpenButtons", "SetIsOpening", "SetIsReopening", "SetIsShopVisited", "SetKeys", "SetPeriodOfGiveLootBox", "SetPossibleRewards", "SetRewards", "SetTimeToGiveLootBox", "ShownRewards")),
SliceWiring("notifications", "xpv_1", "?", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("?PJ", "?RJ", "AddNotification", "InitNotificationSound")),
SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("?_L", "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetLimits", "SetValidationStatus")),
@@ -74,7 +74,7 @@ object RootWiring {
SliceWiring("keyboardSettings", "dpx_1", "Sut", "bw.oth", "bw.oth(t, n.dpx_1)", listOf("?Iut", "?_C", "ChangeAction", "RemoveAction", "ResetAction")),
SliceWiring("touchscreenHudSettings", "epx_1", "j7", "N7().xsv", "N7().xsv(t, n.epx_1)", listOf("?E7", "?_C", "UpdateSettings")),
SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("?VZ", "AddRewardedQuest", "QuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")),
SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("?d6")),
SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("SetLanguage")),
SliceWiring("launchParams", "hpx_1", "?", "(pass-through)", "n.hpx_1", listOf()),
SliceWiring("partner", "ipx_1", "UJ", "(inline@partner)", "(function (t, n) { var i; if (t instanceof VJ) i = n.osf(t.rsf_1, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ssf_1); else if (t instanceof nZ) { var r = t.bsg_1, e = t.csg_1, u = t.dsg_1, s = t.esg_1, o = t.fsg_1, _ = t.gsg_1; i = n.osf(cd, cd, cd, r, e, u, cd, cd, o, _, s); } else i = t instanceof iZ ? n.osf(cd, t.hsg_1) : t instanceof uZ ? n.osf(cd, cd, t.ksg_1) : t instanceof eZ ? n.osf(cd, cd, cd, cd, cd, cd, cd, t.jsg_1) : t instanceof _Z ? n.osf(cd, cd, cd, cd, cd, cd, t.osg_1) : t instanceof X …", listOf("?JJ", "?_Z", "?hZ", "?tZ", "InitPartnerInviteAction", "InitSocialNetworkParameters", "SetLoginParams", "SetLoginStatusForBinding", "SetPartnerId", "SetPartnerParameters", "SetPartnerReady")),
SliceWiring("entrance", "jpx_1", "hD", "Bv.pth", "Bv.pth(t, n.jpx_1)", listOf("HideSocialNetworks", "SetAgeWarnShow", "SetAuthorizedByChinaSdkToken", "SetClassic", "SetControlsForChina", "SetRunningInChinaDataCenter", "SetRunningInChinaPartners")),
@@ -82,7 +82,7 @@ object RootWiring {
SliceWiring("spectator", "lpx_1", "HA", "Li.jqm", "Li.jqm(t, n.lpx_1)", listOf("?JA", "?XA", "?YA", "?ZA", "SetSpectator", "UpdateHudLayout")),
SliceWiring("moderation", "mpx_1", "NI", "Ei.oqh", "Ei.oqh(t, n.mpx_1)", listOf("?FI", "?GI", "?JI", "?KI", "?QI", "?UI", "?WI", "?XI", "?YI", "?ZI", "?nA", "?tA", "?uA")),
SliceWiring("rulesUpdated", "npx_1", "i1", "Ch.dsm", "Ch.dsm(t, n.npx_1)", listOf("UpdateRules")),
SliceWiring("announcement", "opx_1", "lC", "PC().rq5", "PC().rq5(t, n.opx_1)", listOf("?bC", "?qC", "Init", "SetSkipNextPromotedAnnounce")),
SliceWiring("announcement", "opx_1", "lC", "PC().rq5", "PC().rq5(t, n.opx_1)", listOf("DisableAnnouncement", "SetAnnouncementItems", "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce")),
SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("?qJ", "Init")),
SliceWiring("userRenameState", "qpx_1", "O8", "Rf.pte", "Rf.pte(t, n.qpx_1)", listOf("?B8", "?F8", "?G8", "?Q8", "?U8", "ChangeUidInputState", "ResponseActual", "ResponseDelivered", "SetChangeUidParams", "SetRubiesForRename", "SetValidationStatus")),
SliceWiring("eventGoods", "rpx_1", "W7", "Wc.esx", "Wc.esx(t, n.rpx_1)", listOf("InitEventGoods")),
@@ -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", "launchParams")
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -9,8 +9,9 @@
// ⑥ 动作目录:816 个类与 138 个真实派发名对齐
// ⑦ 回放:真实 toString 样例的类/字段/类型校验
// ⑧ 全目录动作类都能按 KClass 路由
// ⑨~㉔ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings;
// ⑨~㉘ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings /
// AccountSettings / ProBattle / Language / Announcement;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -100,6 +101,13 @@ import App.State.SetRankNames
import App.State.SetRubies
import App.State.SetUserPropertiesAction
import App.State.SetUserId
import App.State.BattleTeam
import App.State.ClearInputUid
import App.State.Fight
import App.State.Language
import App.State.PasswordStatus
import App.State.SetAccountRemovalDays
import App.State.SetSkipNextPromotedAnnounce
import App.State.SettingsCategory
import App.State.SpendCoins
import App.State.SpendCrystals
@@ -268,8 +276,31 @@ import App.State.Reducers.DisableAccountSettings
import App.State.Reducers.DisableLobbyTutorial
import App.State.Reducers.ReduceSettings
import App.State.Reducers.ReduceUser
import App.State.Reducers.AccountSettingsStateSlice
import App.State.Reducers.AnnouncementStateSlice
import App.State.Reducers.ClearAccountRemovalTimer
import App.State.Reducers.DisableAnnouncement
import App.State.Reducers.ProBattleStateSlice
import App.State.Reducers.ReduceAccountSettings
import App.State.Reducers.ReduceAnnouncement
import App.State.Reducers.ReduceLanguage
import App.State.Reducers.ReduceProBattle
import App.State.Reducers.ResetPasswordFields
import App.State.Reducers.SetAnnouncementItems
import App.State.Reducers.SetAnnouncementModelLoaded
import App.State.Reducers.SetCountry
import App.State.Reducers.SetCurrentPassword
import App.State.Reducers.SetCurrentPasswordMandatory
import App.State.Reducers.SetEmail
import App.State.Reducers.SetEmailConfirmed
import App.State.Reducers.SetInDebt
import App.State.Reducers.SetLanguage
import App.State.Reducers.SetNewPassword
import App.State.Reducers.SetPassword as SetBattlePassword
import App.State.Reducers.SetPasswordStatus
import App.State.Reducers.SetRepeatNewPassword
import App.State.Reducers.SetSettingsCategory
import App.State.Reducers.SetUnlockButtonForRenameUid
import App.State.Reducers.SettingsStateSlice
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
@@ -698,7 +729,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 14 个已迁移切片(13 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 18 个已迁移切片(17 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -762,14 +793,15 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 14 个已迁移字段,其余 75 个待迁",
"根:迁移表 = 18 个已迁移字段,其余 71 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
"localBattleUserState", "shop", "screen", "lootBoxes", "garage",
"garagePreview", "entrance", "user", "settings", "launchParams",
"garagePreview", "entrance", "user", "settings", "accountSettings",
"proBattle", "language", "announcement", "launchParams",
) &&
RootWiring.Unmigrated.size == 75
RootWiring.Unmigrated.size == 71
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -825,7 +857,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 14/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 18/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -2692,6 +2724,207 @@ object StateCheck {
)
}
// ㉕ 切片 reducer:AccountSettings(oet / Lp.mtg,13 字段;13 个 instanceof 臂)
run {
val S0 = AccountSettingsStateSlice()
Check(
"切片 AccountSettings:默认值完整",
!S0.IsCurrentPasswordMandatory && S0.CurrentPassword == "" &&
S0.NewPassword == "" && S0.RepeatNewPassword == "" &&
S0.NewPasswordStatus == PasswordStatus.NONE && S0.CountryCode == null &&
S0.CountryName == null && S0.Email == null && !S0.EmailConfirmed &&
!S0.LocationCheckEnabled && !S0.IsUnlockButtonForRenameUid &&
!S0.IsClearInputUid && S0.RemovalTimer == null
)
val Reset = ReduceAccountSettings(
S0.copy(
CurrentPassword = "old",
NewPassword = "new",
RepeatNewPassword = "new",
NewPasswordStatus = PasswordStatus.OK,
IsUnlockButtonForRenameUid = true,
Email = "keep@example.com",
RemovalTimer = 42L,
),
ResetPasswordFields,
)
Check(
"切片 AccountSettings:ResetPasswordFields 只清密码三件套/状态/解锁按钮",
Reset.CurrentPassword == "" && Reset.NewPassword == "" &&
Reset.RepeatNewPassword == "" && Reset.NewPasswordStatus == PasswordStatus.NONE &&
!Reset.IsUnlockButtonForRenameUid && Reset.Email == "keep@example.com" &&
Reset.RemovalTimer == 42L
)
val S1 = listOf<Any>(
SetCurrentPassword("cur"),
SetNewPassword("next"),
SetRepeatNewPassword("next"),
SetPasswordStatus(PasswordStatus.SIMPLE),
SetCurrentPasswordMandatory(true),
SetEmail("user@example.com"),
SetEmailConfirmed(true),
SetCountry("RU", "Russia"),
SetUnlockButtonForRenameUid(true),
ClearInputUid(true),
SetAccountRemovalDays(1_700_000_000_000L),
).fold(S0) { S, A -> ReduceAccountSettings(S, A) }
Check(
"切片 AccountSettings:11 个单字段动作按顺序写入",
S1.CurrentPassword == "cur" && S1.NewPassword == "next" &&
S1.RepeatNewPassword == "next" && S1.NewPasswordStatus == PasswordStatus.SIMPLE &&
S1.IsCurrentPasswordMandatory && S1.Email == "user@example.com" &&
S1.EmailConfirmed && S1.CountryCode == "RU" && S1.CountryName == "Russia" &&
S1.IsUnlockButtonForRenameUid && S1.IsClearInputUid &&
S1.RemovalTimer == 1_700_000_000_000L
)
Check(
"切片 AccountSettings:ClearAccountRemovalTimer 只清 removalTimer",
ReduceAccountSettings(S1, ClearAccountRemovalTimer).let {
it.RemovalTimer == null && it.Email == "user@example.com"
}
)
Check(
"切片 AccountSettings:无关动作不改状态",
ReduceAccountSettings(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetCurrentPassword("root-pass"))
Check(
"根:SetCurrentPassword 只动 accountSettings",
Root1.AccountSettings.CurrentPassword == "root-pass" &&
Root1.Language == Root0.Language && Root1.ProBattle == Root0.ProBattle
)
Check(
"根:accountSettings 无关动作保留前一次归约",
ReduceRoot(Root1, AddFrags(1)).AccountSettings == Root1.AccountSettings
)
val W = RootWiring.ByField("accountSettings")!!
Check(
"根:accountSettings 委派到 oet / Lp.mtg 且 13 个动作全部具名",
W.SliceClass == "oet" && W.Reducer == "Lp.mtg" && W.Actions.size == 13 &&
W.Actions.containsAll(
setOf(
"ClearAccountRemovalTimer", "ClearInputUid", "ResetPasswordFields",
"SetAccountRemovalDays", "SetCountry", "SetCurrentPassword",
"SetCurrentPasswordMandatory", "SetEmail", "SetEmailConfirmed",
"SetNewPassword", "SetPasswordStatus", "SetRepeatNewPassword",
"SetUnlockButtonForRenameUid",
)
)
)
}
// ㉖ 切片 reducer:ProBattle(Wtt / uv.wth,3 字段;2 个 instanceof 臂)
run {
val S0 = ProBattleStateSlice()
Check(
"切片 ProBattle:默认 -1 / NONE / 空密码表",
S0.LastFightBattleId == -1L && S0.LastFightBattleTeam == BattleTeam.NONE &&
S0.BattlePasswords.isEmpty()
)
val Fight1 = ReduceProBattle(S0, Fight(10L, BattleTeam.TEAM_A))
Check(
"切片 ProBattle:Fight 写上一场 id + team",
Fight1.LastFightBattleId == 10L && Fight1.LastFightBattleTeam == BattleTeam.TEAM_A
)
val WithPass = ReduceProBattle(Fight1, SetBattlePassword(10L, "secret"))
Check(
"切片 ProBattle:SetPassword 非空时写入密码表",
WithPass.BattlePasswords == mapOf(10L to "secret")
)
Check(
"切片 ProBattle:SetPassword 空串时删除密码表项",
ReduceProBattle(WithPass, SetBattlePassword(10L, "")).BattlePasswords.isEmpty()
)
Check("切片 ProBattle:无关动作不改状态", ReduceProBattle(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, Fight(77L, BattleTeam.TEAM_B))
Check(
"根:Fight 只动 proBattle",
Root1.ProBattle.LastFightBattleId == 77L &&
Root1.ProBattle.LastFightBattleTeam == BattleTeam.TEAM_B &&
Root1.AccountSettings == Root0.AccountSettings
)
val W = RootWiring.ByField("proBattle")!!
Check(
"根:proBattle 委派到 Wtt / uv.wth 且 2 个动作",
W.SliceClass == "Wtt" && W.Reducer == "uv.wth" &&
W.Actions.toSet() == setOf("Fight", "SetPassword")
)
}
// ㉗ 切片 reducer:Language(枚举值 / m6().jtg,1 个 instanceof 臂)
run {
Check("切片 Language:根状态默认 EN", RootState().Language == Language.EN)
Check(
"切片 Language:SetLanguage 写回枚举",
ReduceLanguage(Language.EN, SetLanguage(Language.ZH)) == Language.ZH
)
Check(
"切片 Language:无关动作保留当前语言",
ReduceLanguage(Language.ZH, AddFrags(1)) == Language.ZH
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetLanguage(Language.DE))
Check(
"根:SetLanguage 只动 language",
Root1.Language == Language.DE && Root1.Announcement == Root0.Announcement
)
val W = RootWiring.ByField("language")!!
Check(
"根:language 委派到 m6().jtg 且动作具名",
W.SliceClass == "?" && W.Reducer == "m6().jtg" &&
W.Actions == listOf("SetLanguage")
)
}
// ㉘ 切片 reducer:Announcement(lC / PC().rq5,5 字段;4 个 instanceof 臂)
run {
val S0 = AnnouncementStateSlice()
Check(
"切片 Announcement:默认值完整",
S0.IsEnabled && !S0.ModelLoaded && S0.Items.isEmpty() &&
!S0.WasInBattle && !S0.SkipNextPromotedAnnounce
)
val S1 = ReduceAnnouncement(
ReduceAnnouncement(
ReduceAnnouncement(
ReduceAnnouncement(S0, DisableAnnouncement),
SetAnnouncementModelLoaded,
),
SetAnnouncementItems(listOf("a", "b")),
),
SetSkipNextPromotedAnnounce(true),
)
Check(
"切片 Announcement:4 个臂分别写 enabled/modelLoaded/items/skip",
!S1.IsEnabled && S1.ModelLoaded && S1.Items == listOf<Any?>("a", "b") &&
S1.SkipNextPromotedAnnounce
)
Check(
"切片 Announcement:无关动作不改状态",
ReduceAnnouncement(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, DisableAnnouncement)
Check(
"根:DisableAnnouncement 只动 announcement",
!Root1.Announcement.IsEnabled && Root1.AccountSettings == Root0.AccountSettings
)
val W = RootWiring.ByField("announcement")!!
Check(
"根:announcement 委派到 lC / PC().rq5 且 4 个动作全部具名",
W.SliceClass == "lC" && W.Reducer == "PC().rq5" &&
W.Actions.toSet() == setOf(
"DisableAnnouncement", "SetAnnouncementItems",
"SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce",
)
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)