feat(state): 迁移 lobbyLoader/purchaseDialogState/promoCode/supportUrl/rulesUpdated/notificationByCountry/availabilityByRank

This commit is contained in:
TermiNexus committed 2026-10-24 13:29:41 +08:00
1 parent df179dea51
commit 49e8c1efc4
14 files changed
+716 -48

No files matched your search

+23 -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` 动作名。
- 已迁移切片 **21/89**:
- 已迁移切片 **28/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)
@@ -224,12 +224,28 @@
`Hrt` 类体为空、reducer 读的 `t.iu5_1` 是 `undefined`(`cd` 也是 `undefined`),三个 copy 参数全走默认值,
这一支实际是 no-op;Kotlin 侧保留 `SetNotCompletedQuests` 动作但归约时原样返回。
`tX` / `nX` 是屏幕标识类,已从动作表剔除并记入 `Markers`(`screen-id-class`)。
- `lobbyLoader` → `KW` / `Bs.es1`(`LobbyLoaderReducer.kt`):2 字段,3 个分支。
缺省 `isShowLoader=false`、`activeLoadersCount=0`;`FW`→`ShowLoader` 计数 +1 并显示,
`GW`→`HideLoader` 计数 `max(count-1,0)`,显示取 `count != 0`(计数夹到 0 后自动隐藏)。
- `purchaseDialogState` → `CO` / `je.ur2`(`PurchaseDialogReducer.kt`):4 字段,4 个分支。
`NO`→`ResetPurchaseDialog` 复位四字段;`ChangeEmailStatus` 只写 `emailStatus`(`PurchaseEmailStatus`:
`BUSY/FORBIDDEN/FREE`);`SuccessfullyPurchase` 写 withEmail/time,`goods` 是**追加**(`Im` = List.plus)。
- `promoCode` → `cZ` / `Q_.ysg`(`PromoCodeReducer.kt`):1 字段,2 个实例臂。
`ActivatePromoCode` 置 `WAIT`,`SetCodeValidateStatus` 写入服务端状态;
`PromoCodeStatus` = `NONE/INVALID/WAIT/NOT_ACTIVATED/ACTIVATED`,缺省 `NONE`。
- `supportUrl` → `I4` / `Yl.ct7`(`SupportUrlReducer.kt`):1 字段,2 个分支。
`SetUrl` 写链接,`R4`→`ClearSupportUrl` 清成空串。
- `rulesUpdated` → `i1` / `Ch.dsm`(`RulesUpdatedReducer.kt`):1 字段,`UpdateRules` 写正文,缺省 null。
- `notificationByCountry` → `gJ` / `R_.ssd`(`NotificationByCountryReducer.kt`):4 字段,2 个分支。
`Init` 写三条国家提示消息,`qJ`→`MarkHomeScreenDialogShown` 只把 `homeScreenDialogShown` 置 true。
- `availabilityByRank` → `tet` / `$p.xth`(`AvailabilityByRankReducer.kt`):1 字段,
`SetLoadedData` 整体替换等级可购买表,缺省空 Map。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 20 个有 reducer 的切片,
`launchParams` 与其余 **68 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 21/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **376 项全绿**,含 89 槽位完整性、20 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 27 个有 reducer 的切片,
`launchParams` 与其余 **61 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 28/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **419 项全绿**,含 89 槽位完整性、27 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
@@ -241,7 +257,8 @@
screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、
entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、
language 的 1 个、announcement 的 3 个、news 的 3 个(含两个单例)、notifications 的 2 个、
quests 的 1 个匿名类 / 单例同样按效果定名并登记。
quests 的 1 个、lobbyLoader 的 2 个、purchaseDialogState 的 1 个、supportUrl 的 1 个、
notificationByCountry 的 1 个匿名类 / 单例同样按效果定名并登记。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+52 -15
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-24T05:13:42.080Z",
"generatedAt": "2026-10-24T05:17:45.346Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -27,7 +27,14 @@
"launchParams",
"news",
"notifications",
"quests"
"quests",
"lobbyLoader",
"purchaseDialogState",
"promoCode",
"supportUrl",
"rulesUpdated",
"notificationByCountry",
"availabilityByRank"
],
"AnonymousActions": [
{
@@ -660,6 +667,36 @@
"Name": "SetNotCompletedQuests",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "状态不变(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,`t.iu5_1` 与 `cd` 都是 undefined,三个参数全走默认值 → no-op)"
},
{
"Local": "FW",
"Name": "ShowLoader",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isShowLoader=true、activeLoadersCount = count + 1(`n.fs1(!0, (n.ss0_1 + 1) | 0)`)"
},
{
"Local": "GW",
"Name": "HideLoader",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "activeLoadersCount = max(count - 1, 0)、isShowLoader = count != 0(`n.fs1(!(0 === e), e)`)"
},
{
"Local": "NO",
"Name": "ResetPurchaseDialog",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "withEmail=false、goods=emptyList()、emailStatus=null、time=null(`n.jr2(!1, md(), null, null)`)"
},
{
"Local": "R4",
"Name": "ClearSupportUrl",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "url=\"\"(`n.at7()`)"
},
{
"Local": "qJ",
"Name": "MarkHomeScreenDialogShown",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)"
}
],
"Markers": [
@@ -1495,11 +1532,11 @@
"Reducer": "Bs.es1",
"Delegate": "Bs.es1(t, n.fpw_1)",
"Actions": [
"?FW",
"?GW"
"HideLoader",
"ShowLoader"
],
"Default": "new KW()",
"Migrated": false
"Migrated": true
},
{
"Field": "battleResult",
@@ -1901,12 +1938,12 @@
"Reducer": "je.ur2",
"Delegate": "je.ur2(t, n.bpx_1)",
"Actions": [
"?NO",
"ChangeEmailStatus",
"ResetPurchaseDialog",
"SuccessfullyPurchase"
],
"Default": "new CO()",
"Migrated": false
"Migrated": true
},
{
"Field": "battleInviteState",
@@ -2102,7 +2139,7 @@
"UpdateRules"
],
"Default": "new i1()",
"Migrated": false
"Migrated": true
},
{
"Field": "announcement",
@@ -2126,11 +2163,11 @@
"Reducer": "R_.ssd",
"Delegate": "R_.ssd(t, n.ppx_1)",
"Actions": [
"?qJ",
"Init"
"Init",
"MarkHomeScreenDialogShown"
],
"Default": "new gJ()",
"Migrated": false
"Migrated": true
},
{
"Field": "userRenameState",
@@ -2319,7 +2356,7 @@
"SetLoadedData"
],
"Default": "new tet()",
"Migrated": false
"Migrated": true
},
{
"Field": "userReportsState",
@@ -2382,7 +2419,7 @@
"SetCodeValidateStatus"
],
"Default": "new cZ()",
"Migrated": false
"Migrated": true
},
{
"Field": "referralEvent",
@@ -2406,11 +2443,11 @@
"Reducer": "Yl.ct7",
"Delegate": "Yl.ct7(t, n.ipy_1)",
"Actions": [
"?R4",
"ClearSupportUrl",
"SetUrl"
],
"Default": "new I4()",
"Migrated": false
"Migrated": true
}
]
}
+64
View File
@@ -170,6 +170,15 @@ const AnonymousActionNames = new Map([
['hJ', 'SetNewsFlag'],
['x_', 'OpenNews'],
['j_', 'CloseNews'],
// lobbyLoader 切片(KW / VW.es1):FW/GW 没有 toString,按 reducer 效果定名。
['FW', 'ShowLoader'],
['GW', 'HideLoader'],
// purchaseDialogState 切片(CO / DO.ur2):NO 没有 toString,按 reducer 效果定名。
['NO', 'ResetPurchaseDialog'],
// supportUrl 切片(I4 / P4.ct7):R4 没有 toString,按 reducer 效果定名。
['R4', 'ClearSupportUrl'],
// notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 效果定名。
['qJ', 'MarkHomeScreenDialogShown'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -573,6 +582,16 @@ const Migrated = {
Reduce: 'ReduceNotifications',
},
quests: { Type: 'QuestsStateSlice', Reduce: 'ReduceQuests' },
lobbyLoader: { Type: 'LobbyLoaderStateSlice', Reduce: 'ReduceLobbyLoader' },
purchaseDialogState: { Type: 'PurchaseDialogStateSlice', Reduce: 'ReducePurchaseDialog' },
promoCode: { Type: 'PromoCodeStateSlice', Reduce: 'ReducePromoCode' },
supportUrl: { Type: 'SupportUrlStateSlice', Reduce: 'ReduceSupportUrl' },
rulesUpdated: { Type: 'RulesUpdatedStateSlice', Reduce: 'ReduceRulesUpdated' },
notificationByCountry: {
Type: 'NotificationByCountryStateSlice',
Reduce: 'ReduceNotificationByCountry',
},
availabilityByRank: { Type: 'AvailabilityByRankStateSlice', Reduce: 'ReduceAvailabilityByRank' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -759,6 +778,27 @@ const QuestsAnonymousEffects = {
Hrt: '状态不变(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,`t.iu5_1` 与 `cd` 都是 undefined,三个参数全走默认值 → no-op)',
};
// lobbyLoader 切片(KW / VW.es1):FW/GW 没有 toString,按 reducer 分支效果定名。
const LobbyLoaderAnonymousEffects = {
FW: 'isShowLoader=true、activeLoadersCount = count + 1(`n.fs1(!0, (n.ss0_1 + 1) | 0)`)',
GW: 'activeLoadersCount = max(count - 1, 0)、isShowLoader = count != 0(`n.fs1(!(0 === e), e)`)',
};
// purchaseDialogState 切片(CO / DO.ur2):NO 没有 toString,按 reducer 分支效果定名。
const PurchaseDialogAnonymousEffects = {
NO: 'withEmail=false、goods=emptyList()、emailStatus=null、time=null(`n.jr2(!1, md(), null, null)`)',
};
// supportUrl 切片(I4 / P4.ct7):R4 没有 toString,按 reducer 分支效果定名。
const SupportUrlAnonymousEffects = {
R4: 'url=""(`n.at7()`)',
};
// notificationByCountry 切片(gJ / $J.ssd):qJ 没有 toString,按 reducer 分支效果定名。
const NotificationByCountryAnonymousEffects = {
qJ: 'homeScreenDialogShown=true(`n.isd(cd, cd, cd, !0)`)',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
JSON.stringify(
@@ -859,6 +899,30 @@ fs.writeFileSync(
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(LobbyLoaderAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(PurchaseDialogAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(SupportUrlAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(NotificationByCountryAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
],
Markers: [
{
@@ -0,0 +1,26 @@
// AvailabilityByRankReducer.kt —— 按等级可购买项切片(对应 module_10032 的 tet 状态 + set.xth)
//
// 证据链:
// 状态类 tet:1 个字段,toString 名 "AvailabilityByRankState(data=…)";copy 是 pu5(1 字段)。
// 默认:data = ag() = emptyMap()。
// 根委派:`$p.xth(t, n.cpy_1)`。
// reducer:`t instanceof eet ? n.pu5(t.wu5_1) : n`,`eet` 是 SetLoadedData。
package App.State.Reducers
import App.State.SetLoadedData
/* ------------------------------------------------------------------ 状态 */
data class AvailabilityByRankStateSlice(
val Data: Map<Any?, Any?> = emptyMap(),
)
/* ------------------------------------------------------------------ reducer */
fun ReduceAvailabilityByRank(
State0: AvailabilityByRankStateSlice,
Action: Any,
): AvailabilityByRankStateSlice = when (Action) {
is SetLoadedData -> State0.copy(Data = Action.loadedData as Map<Any?, Any?>)
else -> State0
}
@@ -0,0 +1,42 @@
// LobbyLoaderReducer.kt —— 大厅加载遮罩切片(对应 module_10032 的 KW 状态 + VW.es1)
//
// 证据链:
// 状态类 KW:2 个字段,toString 名 "LobbyLoader(isShowLoader=…, activeLoadersCount=…)";copy 是 fs1(2 字段)。
// 默认:isShowLoader=false、activeLoadersCount=0。
// 根委派:`Bs.es1(t, n.fpw_1)`。
// reducer:
// FW → `n.fs1(!0, (n.ss0_1 + 1) | 0)` 显示并计数 +1
// GW → `n.fs1(!(0 === e), e)`,e = max(count - 1, 0) 计数 -1(夹到 0),显示 = 计数非 0
package App.State.Reducers
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `FW`:有东西开始加载。 */
object ShowLoader
/** 原 `GW`:有东西结束加载。 */
object HideLoader
/* ------------------------------------------------------------------ 状态 */
data class LobbyLoaderStateSlice(
val IsShowLoader: Boolean = false,
val ActiveLoadersCount: Int = 0,
)
/* ------------------------------------------------------------------ reducer */
fun ReduceLobbyLoader(
State0: LobbyLoaderStateSlice,
Action: Any,
): LobbyLoaderStateSlice = when (Action) {
is ShowLoader -> State0.copy(
IsShowLoader = true,
ActiveLoadersCount = State0.ActiveLoadersCount + 1,
)
is HideLoader -> {
val Count = maxOf(State0.ActiveLoadersCount - 1, 0)
State0.copy(IsShowLoader = Count != 0, ActiveLoadersCount = Count)
}
else -> State0
}
@@ -0,0 +1,41 @@
// NotificationByCountryReducer.kt —— 按国家屏蔽提示切片(对应 module_10032 的 gJ 状态 + $J.ssd)
//
// 证据链:
// 状态类 gJ:4 个字段,toString 名 "NotificationByCountry(...)";copy 是 isd(4 字段 + 回调)。
// 默认:三条消息都是 null、homeScreenDialogShown=false。
// 根委派:`R_.ssd(t, n.ppx_1)`。
// reducer:
// bJ(Init)→ `n.isd(t.psd_1, t.qsd_1, t.rsd_1)`,写三条消息
// qJ → `n.isd(cd, cd, cd, !0)`,只把 homeScreenDialogShown 置 true
package App.State.Reducers
import App.State.Init
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `qJ`:首页的国家提示弹窗已经展示过。 */
object MarkHomeScreenDialogShown
/* ------------------------------------------------------------------ 状态 */
data class NotificationByCountryStateSlice(
val DisableRegistrationMessage: String? = null,
val DisableShopMessage: String? = null,
val HomeScreenMessage: String? = null,
val HomeScreenDialogShown: Boolean = false,
)
/* ------------------------------------------------------------------ reducer */
fun ReduceNotificationByCountry(
State0: NotificationByCountryStateSlice,
Action: Any,
): NotificationByCountryStateSlice = when (Action) {
is Init -> State0.copy(
DisableRegistrationMessage = Action.disableRegistrationMessage,
DisableShopMessage = Action.disableShopMessage,
HomeScreenMessage = Action.homeScreenMessage,
)
is MarkHomeScreenDialogShown -> State0.copy(HomeScreenDialogShown = true)
else -> State0
}
@@ -0,0 +1,36 @@
// PromoCodeReducer.kt —— 促销码切片(对应 module_10032 的 cZ 状态 + wZ.ysg)
//
// 证据链:
// 状态类 cZ:1 个字段,toString 名 "PromoCodeState(...)";copy 是 vsg(1 字段)。
// 默认:validateStatus = dZ() = NONE。
// 根委派:`Q_.ysg(t, n.gpy_1)`。
// reducer:
// vZ → `n.vsg((lZ(), Y_))`,状态置 WAIT(提交校验中)
// pZ → `n.vsg(t.xsg_1)`,写入服务端返回的状态
// `dZ()` 是 `fZ` 枚举:NONE / INVALID / WAIT / NOT_ACTIVATED / ACTIVATED。
package App.State.Reducers
import App.State.ActivatePromoCode
import App.State.SetCodeValidateStatus
/* ------------------------------------------------------------------ 枚举 */
/** 原 `fZ`:促销码校验状态。 */
enum class PromoCodeStatus { NONE, INVALID, WAIT, NOT_ACTIVATED, ACTIVATED }
/* ------------------------------------------------------------------ 状态 */
data class PromoCodeStateSlice(
val ValidateStatus: PromoCodeStatus = PromoCodeStatus.NONE,
)
/* ------------------------------------------------------------------ reducer */
fun ReducePromoCode(
State0: PromoCodeStateSlice,
Action: Any,
): PromoCodeStateSlice = when (Action) {
is ActivatePromoCode -> State0.copy(ValidateStatus = PromoCodeStatus.WAIT)
is SetCodeValidateStatus -> State0.copy(ValidateStatus = Action.promoCodeStatus as PromoCodeStatus)
else -> State0
}
@@ -0,0 +1,53 @@
// PurchaseDialogReducer.kt —— 购买对话框切片(对应 module_10032 的 CO 状态 + DO.ur2)
//
// 证据链:
// 状态类 CO:4 个字段,toString 名 "PurchaseDialogState(...)";copy 是 kr2(4 字段 + 回调)。
// 默认:withEmail=false、goods=emptyList()、emailStatus=null、time=null。
// 根委派:`je.ur2(t, n.bpx_1)`。
// reducer:
// NO → `n.jr2(!1, md(), null, null)`,四个字段全部复位
// MO → `n.kr2(cd, cd, t.pr2_1)`,只写 emailStatus
// OO → `n.kr2(u, Im(n.gr2_1, t.rr2_1), cd, s)`,withEmail/goods/time 三个字段;
// `Im` 是 List.plus,goods 是「追加」而不是替换
// emailStatus 是 `TO` 枚举(BUSY / FORBIDDEN / FREE)。
package App.State.Reducers
import App.State.ChangeEmailStatus
import App.State.SuccessfullyPurchase
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `NO`:关闭购买对话框并清空。 */
object ResetPurchaseDialog
/* ------------------------------------------------------------------ 枚举 */
/** 原 `TO`:邮箱状态。 */
enum class PurchaseEmailStatus { BUSY, FORBIDDEN, FREE }
/* ------------------------------------------------------------------ 状态 */
data class PurchaseDialogStateSlice(
val WithEmail: Boolean = false,
val Goods: List<Any?> = emptyList(),
val EmailStatus: PurchaseEmailStatus? = null,
val Time: Long? = null,
)
/* ------------------------------------------------------------------ reducer */
fun ReducePurchaseDialog(
State0: PurchaseDialogStateSlice,
Action: Any,
): PurchaseDialogStateSlice = when (Action) {
is ResetPurchaseDialog ->
PurchaseDialogStateSlice()
is ChangeEmailStatus ->
State0.copy(EmailStatus = Action.status as PurchaseEmailStatus?)
is SuccessfullyPurchase -> State0.copy(
WithEmail = Action.withEmail,
Goods = State0.Goods + (Action.goods as List<Any?>),
Time = Action.time,
)
else -> State0
}
@@ -0,0 +1,26 @@
// RulesUpdatedReducer.kt —— 规则更新提示切片(对应 module_10032 的 i1 状态 + u1.dsm)
//
// 证据链:
// 状态类 i1:1 个字段,toString 名 "RulesUpdatedState(text=…)";copy 是 bsm(1 字段)。
// 默认:text=null。
// 根委派:`Ch.dsm(t, n.npx_1)`。
// reducer:`t instanceof e1 ? n.bsm(t.csm_1) : n`,`e1` 是 UpdateRules。
package App.State.Reducers
import App.State.UpdateRules
/* ------------------------------------------------------------------ 状态 */
data class RulesUpdatedStateSlice(
val Text: String? = null,
)
/* ------------------------------------------------------------------ reducer */
fun ReduceRulesUpdated(
State0: RulesUpdatedStateSlice,
Action: Any,
): RulesUpdatedStateSlice = when (Action) {
is UpdateRules -> State0.copy(Text = Action.text as String?)
else -> State0
}
@@ -0,0 +1,34 @@
// SupportUrlReducer.kt —— 客服链接切片(对应 module_10032 的 I4 状态 + P4.ct7)
//
// 证据链:
// 状态类 I4:1 个字段,toString 名 "SupportUrlState(url=…)";copy 是 at7(1 字段)。
// 默认:url=""。
// 根委派:`Yl.ct7(t, n.ipy_1)`。
// reducer:
// A4 → `n.zt6(t.bt7_1)`,写入链接
// R4 → `n.at7()`,清成空串
package App.State.Reducers
import App.State.SetUrl
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `R4`:清空客服链接。 */
object ClearSupportUrl
/* ------------------------------------------------------------------ 状态 */
data class SupportUrlStateSlice(
val Url: String = "",
)
/* ------------------------------------------------------------------ reducer */
fun ReduceSupportUrl(
State0: SupportUrlStateSlice,
Action: Any,
): SupportUrlStateSlice = when (Action) {
is SetUrl -> State0.copy(Url = Action.url as String)
is ClearSupportUrl -> State0.copy(Url = "")
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 21 个切片里,20 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 28 个切片里,27 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -23,9 +23,16 @@
// news → ReduceNews(ZX / E_.asd,根委派 `E_.asd(t, n.tpv_1)`)
// notifications → ReduceNotifications(jJ / B_.ise,根委派 `B_.ise(t, n.xpv_1)`)
// quests → ReduceQuests(Frt / Yrt.otg,根委派 `(null == zp && new Yrt(), zp).otg(t, n.npv_1)`)
// lobbyLoader → ReduceLobbyLoader(KW / VW.es1,根委派 `Bs.es1(t, n.fpw_1)`)
// purchaseDialogState → ReducePurchaseDialog(CO / DO.ur2,根委派 `je.ur2(t, n.bpx_1)`)
// promoCode → ReducePromoCode(cZ / wZ.ysg,根委派 `Q_.ysg(t, n.gpy_1)`)
// supportUrl → ReduceSupportUrl(I4 / P4.ct7,根委派 `Yl.ct7(t, n.ipy_1)`)
// rulesUpdated → ReduceRulesUpdated(i1 / u1.dsm,根委派 `Ch.dsm(t, n.npx_1)`)
// notificationByCountry → ReduceNotificationByCountry(gJ / $J.ssd,根委派 `R_.ssd(t, n.ppx_1)`)
// availabilityByRank → ReduceAvailabilityByRank(tet / set.xth,根委派 `$p.xth(t, n.cpy_1)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 68 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 61 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -41,11 +48,18 @@ import App.State.Reducers.ReduceLanguage
import App.State.Reducers.ReduceLocalBattleUser
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceLootBoxes
import App.State.Reducers.ReduceAvailabilityByRank
import App.State.Reducers.ReduceLobbyLoader
import App.State.Reducers.ReduceNews
import App.State.Reducers.ReduceNotificationByCountry
import App.State.Reducers.ReduceNotifications
import App.State.Reducers.ReduceProBattle
import App.State.Reducers.ReducePromoCode
import App.State.Reducers.ReducePurchaseDialog
import App.State.Reducers.ReduceQuests
import App.State.Reducers.ReduceRulesUpdated
import App.State.Reducers.ReduceScreen
import App.State.Reducers.ReduceSupportUrl
import App.State.Reducers.ReduceSettings
import App.State.Reducers.ReduceShop
import App.State.Reducers.ReduceTwitch
@@ -84,6 +98,13 @@ object RootReducer {
News = ReduceNews(State0.News, Action),
Notifications = ReduceNotifications(State0.Notifications, Action),
Quests = ReduceQuests(State0.Quests, Action),
LobbyLoader = ReduceLobbyLoader(State0.LobbyLoader, Action),
PurchaseDialogState = ReducePurchaseDialog(State0.PurchaseDialogState, Action),
PromoCode = ReducePromoCode(State0.PromoCode, Action),
SupportUrl = ReduceSupportUrl(State0.SupportUrl, Action),
RulesUpdated = ReduceRulesUpdated(State0.RulesUpdated, Action),
NotificationByCountry = ReduceNotificationByCountry(State0.NotificationByCountry, Action),
AvailabilityByRank = ReduceAvailabilityByRank(State0.AvailabilityByRank, Action),
)
}
@@ -6,21 +6,28 @@ package App.State
import App.State.Reducers.AccountSettingsStateSlice
import App.State.Reducers.AnnouncementStateSlice
import App.State.Reducers.AvailabilityByRankStateSlice
import App.State.Reducers.EntranceStateSlice
import App.State.Reducers.GarageStateSlice
import App.State.Reducers.GaragePreviewStateSlice
import App.State.Reducers.HomeStateSlice
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.LobbyLoaderStateSlice
import App.State.Reducers.LocalBattleUserStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.LootBoxesStateSlice
import App.State.Reducers.NewsStateSlice
import App.State.Reducers.NotificationByCountryStateSlice
import App.State.Reducers.NotificationsStateSlice
import App.State.Reducers.ProBattleStateSlice
import App.State.Reducers.PromoCodeStateSlice
import App.State.Reducers.PurchaseDialogStateSlice
import App.State.Reducers.QuestsStateSlice
import App.State.Reducers.RulesUpdatedStateSlice
import App.State.Reducers.ScreenStateSlice
import App.State.Reducers.SettingsStateSlice
import App.State.Reducers.ShopStateSlice
import App.State.Reducers.SupportUrlStateSlice
import App.State.Reducers.TwitchStateSlice
import App.State.Reducers.UserStateSlice
@@ -95,8 +102,8 @@ data class RootState(
val BattleCreateState: Any? = null,
/** 原 battleSearchState(Itt / Htt().vtg)。未迁移:原样透传。 */
val BattleSearchState: Any? = null,
/** 原 lobbyLoader(KW / Bs.es1)。未迁移:原样透传。 */
val LobbyLoader: Any? = null,
/** 原 lobbyLoader(KW / Bs.es1)。已迁移。 */
val LobbyLoader: LobbyLoaderStateSlice = LobbyLoaderStateSlice(),
/** 原 battleResult(sR / Fi.aqp)。未迁移:原样透传。 */
val BattleResult: Any? = null,
/** 原 battleUsers(uP / sr.hqs)。未迁移:原样透传。 */
@@ -139,8 +146,8 @@ data class RootState(
val ControlSettings: Any? = null,
/** 原 securitySettings(V2 / pc.qsv)。未迁移:原样透传。 */
val SecuritySettings: Any? = null,
/** 原 purchaseDialogState(CO / je.ur2)。未迁移:原样透传。 */
val PurchaseDialogState: Any? = null,
/** 原 purchaseDialogState(CO / je.ur2)。已迁移。 */
val PurchaseDialogState: PurchaseDialogStateSlice = PurchaseDialogStateSlice(),
/** 原 battleInviteState(att / Ttt().nth)。未迁移:原样透传。 */
val BattleInviteState: Any? = null,
/** 原 keyboardSettings(Sut / bw.oth)。未迁移:原样透传。 */
@@ -163,12 +170,12 @@ data class RootState(
val Spectator: Any? = null,
/** 原 moderation(NI / Ei.oqh)。未迁移:原样透传。 */
val Moderation: Any? = null,
/** 原 rulesUpdated(i1 / Ch.dsm)。未迁移:原样透传。 */
val RulesUpdated: Any? = null,
/** 原 rulesUpdated(i1 / Ch.dsm)。已迁移。 */
val RulesUpdated: RulesUpdatedStateSlice = RulesUpdatedStateSlice(),
/** 原 announcement(lC / PC().rq5)。已迁移。 */
val Announcement: AnnouncementStateSlice = AnnouncementStateSlice(),
/** 原 notificationByCountry(gJ / R_.ssd)。未迁移:原样透传。 */
val NotificationByCountry: Any? = null,
/** 原 notificationByCountry(gJ / R_.ssd)。已迁移。 */
val NotificationByCountry: NotificationByCountryStateSlice = NotificationByCountryStateSlice(),
/** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */
val UserRenameState: Any? = null,
/** 原 eventGoods(W7 / Wc.esx)。未迁移:原样透传。 */
@@ -193,18 +200,18 @@ data class RootState(
val QuestCategories: Any? = null,
/** 原 proBattle(Wtt / uv.wth)。已迁移。 */
val ProBattle: ProBattleStateSlice = ProBattleStateSlice(),
/** 原 availabilityByRank(tet / $p.xth)。未迁移:原样透传。 */
val AvailabilityByRank: Any? = null,
/** 原 availabilityByRank(tet / $p.xth)。已迁移。 */
val AvailabilityByRank: AvailabilityByRankStateSlice = AvailabilityByRankStateSlice(),
/** 原 userReportsState(Zst / qot.zth)。未迁移:原样透传。 */
val UserReportsState: Any? = null,
/** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */
val TankBuilderState: Any? = null,
/** 原 contractState(Ont / xv.bti)。未迁移:原样透传。 */
val ContractState: Any? = null,
/** 原 promoCode(cZ / Q_.ysg)。未迁移:原样透传。 */
val PromoCode: Any? = null,
/** 原 promoCode(cZ / Q_.ysg)。已迁移。 */
val PromoCode: PromoCodeStateSlice = PromoCodeStateSlice(),
/** 原 referralEvent(pQ / lh.osk)。未迁移:原样透传。 */
val ReferralEvent: Any? = null,
/** 原 supportUrl(I4 / Yl.ct7)。未迁移:原样透传。 */
val SupportUrl: Any? = null,
/** 原 supportUrl(I4 / Yl.ct7)。已迁移。 */
val SupportUrl: SupportUrlStateSlice = SupportUrlStateSlice(),
)
@@ -47,7 +47,7 @@ object RootWiring {
SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("?WH", "?nW", "?tW", "Configure", "EnterToBattleApply", "Play", "SelectMatchmakingMode", "SetRankIndexToMinGS", "WasRegistered")),
SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")),
SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("?Dtt", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")),
SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("?FW", "?GW")),
SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("HideLoader", "ShowLoader")),
SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("?cR", "?fR", "?lR", "DoInit", "SetHasBattleResult", "SetRewards")),
SliceWiring("battleUsers", "hpw_1", "uP", "sr.hqs", "sr.hqs(t, n.hpw_1)", listOf("?_P", "?hP", "?lP", "?oP", "?qP", "?sP", "InitSuspiciousState", "InitUserLevels", "RemoveTankInfo", "SetGearScore", "SetTankInfo", "SetTankResistance", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")),
SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("?qE")),
@@ -69,7 +69,7 @@ object RootWiring {
SliceWiring("graphicsSettings", "ypw_1", "?", "lc.isu", "lc.isu(t, n.ypw_1)", listOf("UpdateSettings")),
SliceWiring("controlSettings", "zpw_1", "i2", "_2().ess", "_2().ess(t, n.zpw_1)", listOf("?_C", "UpdateSettings")),
SliceWiring("securitySettings", "apx_1", "V2", "pc.qsv", "pc.qsv(t, n.apx_1)", listOf("SetProfileBlockStatus")),
SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("?NO", "ChangeEmailStatus", "SuccessfullyPurchase")),
SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase")),
SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("?dtt", "EnableSetBattleInviteNotifications", "InvitationResponse", "InviteToBattle", "RemoveUserFromInvited", "SelectUserToInvite")),
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")),
@@ -83,7 +83,7 @@ object RootWiring {
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("DisableAnnouncement", "SetAnnouncementItems", "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce")),
SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("?qJ", "Init")),
SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("Init", "MarkHomeScreenDialogShown")),
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")),
SliceWiring("creatorTagState", "spx_1", "qO", "ze.er2", "ze.er2(t, n.spx_1)", listOf("?jO", "SetAttachedCreatorTag", "SetCreatorTag", "SetCreatorTags")),
@@ -102,11 +102,11 @@ object RootWiring {
SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "Init", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")),
SliceWiring("promoCode", "gpy_1", "cZ", "Q_.ysg", "Q_.ysg(t, n.gpy_1)", listOf("ActivatePromoCode", "SetCodeValidateStatus")),
SliceWiring("referralEvent", "hpy_1", "pQ", "lh.osk", "lh.osk(t, n.hpy_1)", listOf("SetPromoCode", "SetReferralEventData", "SetReferrals", "SetReferrer")),
SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("?R4", "SetUrl")),
SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("ClearSupportUrl", "SetUrl")),
)
/** 已迁移(强类型;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")
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")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -9,9 +9,10 @@
// ⑥ 动作目录:816 个类与 138 个真实派发名对齐
// ⑦ 回放:真实 toString 样例的类/字段/类型校验
// ⑧ 全目录动作类都能按 KClass 路由
// ⑨~㉛ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// ⑨~㊳ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings /
// AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests;
// AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests /
// LobbyLoader / PurchaseDialogState / PromoCode / SupportUrl / RulesUpdated / NotificationByCountry / AvailabilityByRank;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -165,14 +166,22 @@ import App.State.DisplayMainEntranceScreen
import App.State.DisplayReauthorizationMessage
import App.State.EntranceScreen
import App.State.GoTo
import App.State.ActivatePromoCode
import App.State.AddNewsItemAction
import App.State.AddNotification
import App.State.ChangeEmailStatus
import App.State.Init as InitNotificationByCountry
import App.State.InitNotificationSound
import App.State.NewsItemData
import App.State.RemoveNewsItemAction
import App.State.SetCodeValidateStatus
import App.State.SetCurrentCategory
import App.State.SetLoadedData
import App.State.SetNewsItemAction
import App.State.SetUnreadNewsCounter
import App.State.SetUrl
import App.State.SuccessfullyPurchase
import App.State.UpdateRules
import App.State.LoaderScreen
import App.State.NamedScreen
import App.State.PressedAnyButton
@@ -325,6 +334,27 @@ import App.State.Reducers.RemoveFirstNotification
import App.State.Reducers.RemoveNotifications
import App.State.Reducers.SetNewsFlag
import App.State.Reducers.SetNotCompletedQuests
import App.State.Reducers.AvailabilityByRankStateSlice
import App.State.Reducers.ClearSupportUrl
import App.State.Reducers.HideLoader
import App.State.Reducers.LobbyLoaderStateSlice
import App.State.Reducers.MarkHomeScreenDialogShown
import App.State.Reducers.NotificationByCountryStateSlice
import App.State.Reducers.PromoCodeStateSlice
import App.State.Reducers.PromoCodeStatus
import App.State.Reducers.PurchaseDialogStateSlice
import App.State.Reducers.PurchaseEmailStatus
import App.State.Reducers.ReduceAvailabilityByRank
import App.State.Reducers.ReduceLobbyLoader
import App.State.Reducers.ReduceNotificationByCountry
import App.State.Reducers.ReducePromoCode
import App.State.Reducers.ReducePurchaseDialog
import App.State.Reducers.ReduceRulesUpdated
import App.State.Reducers.ReduceSupportUrl
import App.State.Reducers.ResetPurchaseDialog
import App.State.Reducers.RulesUpdatedStateSlice
import App.State.Reducers.ShowLoader
import App.State.Reducers.SupportUrlStateSlice
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
@@ -752,7 +782,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 21 个已迁移切片(20 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 28 个已迁移切片(27 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -816,7 +846,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 21 个已迁移字段,其余 68 个待迁",
"根:迁移表 = 28 个已迁移字段,其余 61 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
@@ -824,8 +854,10 @@ object StateCheck {
"garagePreview", "entrance", "user", "settings", "accountSettings",
"proBattle", "language", "announcement", "launchParams",
"news", "notifications", "quests",
"lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl",
"rulesUpdated", "notificationByCountry", "availabilityByRank",
) &&
RootWiring.Unmigrated.size == 68
RootWiring.Unmigrated.size == 61
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -881,7 +913,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 21/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 28/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -3103,6 +3135,238 @@ object StateCheck {
)
}
// ㉜ 切片 reducer:LobbyLoader(KW / VW.es1,2 字段;FW/GW 两个匿名类)
run {
val S0 = LobbyLoaderStateSlice()
Check("切片 LobbyLoader:默认不显示、计数 0", !S0.IsShowLoader && S0.ActiveLoadersCount == 0)
val S1 = ReduceLobbyLoader(ReduceLobbyLoader(S0, ShowLoader), ShowLoader)
Check(
"切片 LobbyLoader:ShowLoader 计数 +1 并显示",
S1.IsShowLoader && S1.ActiveLoadersCount == 2
)
val S2 = ReduceLobbyLoader(S1, HideLoader)
Check(
"切片 LobbyLoader:HideLoader 计数 -1,仍大于 0 时继续显示",
S2.IsShowLoader && S2.ActiveLoadersCount == 1
)
val S3 = ReduceLobbyLoader(ReduceLobbyLoader(S2, HideLoader), HideLoader)
Check(
"切片 LobbyLoader:计数夹到 0 并隐藏",
!S3.IsShowLoader && S3.ActiveLoadersCount == 0
)
Check("切片 LobbyLoader:无关动作不改状态", ReduceLobbyLoader(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ShowLoader)
Check(
"根:ShowLoader 只动 lobbyLoader",
Root1.LobbyLoader.IsShowLoader && Root1.PurchaseDialogState == Root0.PurchaseDialogState
)
val W = RootWiring.ByField("lobbyLoader")!!
Check(
"根:lobbyLoader 委派到 KW / Bs.es1 且 2 个动作全部具名",
W.SliceClass == "KW" && W.Reducer == "Bs.es1" &&
W.Actions.toSet() == setOf("ShowLoader", "HideLoader")
)
}
// ㉝ 切片 reducer:PurchaseDialogState(CO / DO.ur2,4 字段;NO + 2 个实例臂)
run {
val S0 = PurchaseDialogStateSlice()
Check(
"切片 PurchaseDialog:默认值完整",
!S0.WithEmail && S0.Goods.isEmpty() && S0.EmailStatus == null && S0.Time == null
)
val S1 = ReducePurchaseDialog(S0, ChangeEmailStatus(PurchaseEmailStatus.BUSY))
Check(
"切片 PurchaseDialog:ChangeEmailStatus 只写邮箱状态",
S1.EmailStatus == PurchaseEmailStatus.BUSY && S1.Goods.isEmpty() && S1.Time == null
)
val S2 = ReducePurchaseDialog(S1, SuccessfullyPurchase(listOf("g1"), true, 123L))
Check(
"切片 PurchaseDialog:SuccessfullyPurchase 追加商品并写时间",
S2.WithEmail && S2.Goods == listOf<Any?>("g1") && S2.Time == 123L &&
S2.EmailStatus == PurchaseEmailStatus.BUSY
)
val S3 = ReducePurchaseDialog(S2, SuccessfullyPurchase(listOf("g2"), false, 456L))
Check(
"切片 PurchaseDialog:商品是追加而不是替换",
S3.Goods == listOf<Any?>("g1", "g2") && !S3.WithEmail && S3.Time == 456L
)
Check(
"切片 PurchaseDialog:ResetPurchaseDialog 复位四个字段",
ReducePurchaseDialog(S3, ResetPurchaseDialog) == PurchaseDialogStateSlice()
)
Check("切片 PurchaseDialog:无关动作不改状态", ReducePurchaseDialog(S3, AddFrags(1)) == S3)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ChangeEmailStatus(PurchaseEmailStatus.FREE))
Check(
"根:ChangeEmailStatus 只动 purchaseDialogState",
Root1.PurchaseDialogState.EmailStatus == PurchaseEmailStatus.FREE &&
Root1.LobbyLoader == Root0.LobbyLoader
)
val W = RootWiring.ByField("purchaseDialogState")!!
Check(
"根:purchaseDialogState 委派到 CO / je.ur2 且 3 个动作全部具名",
W.SliceClass == "CO" && W.Reducer == "je.ur2" &&
W.Actions.toSet() == setOf(
"ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase",
)
)
}
// ㉞ 切片 reducer:PromoCode(cZ / wZ.ysg,1 字段;2 个实例臂)
run {
val S0 = PromoCodeStateSlice()
Check("切片 PromoCode:默认 NONE", S0.ValidateStatus == PromoCodeStatus.NONE)
Check(
"切片 PromoCode:ActivatePromoCode 进入 WAIT",
ReducePromoCode(S0, ActivatePromoCode("CODE-1")).ValidateStatus == PromoCodeStatus.WAIT
)
Check(
"切片 PromoCode:SetCodeValidateStatus 写服务端状态",
ReducePromoCode(
PromoCodeStateSlice(PromoCodeStatus.WAIT),
SetCodeValidateStatus(PromoCodeStatus.ACTIVATED),
).ValidateStatus == PromoCodeStatus.ACTIVATED
)
Check("切片 PromoCode:无关动作不改状态", ReducePromoCode(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ActivatePromoCode("X"))
Check(
"根:ActivatePromoCode 只动 promoCode",
Root1.PromoCode.ValidateStatus == PromoCodeStatus.WAIT &&
Root1.SupportUrl == Root0.SupportUrl
)
val W = RootWiring.ByField("promoCode")!!
Check(
"根:promoCode 委派到 cZ / Q_.ysg 且 2 个动作",
W.SliceClass == "cZ" && W.Reducer == "Q_.ysg" &&
W.Actions == listOf("ActivatePromoCode", "SetCodeValidateStatus")
)
}
// ㉟ 切片 reducer:SupportUrl(I4 / P4.ct7,1 字段;A4 + R4)
run {
val S0 = SupportUrlStateSlice()
Check("切片 SupportUrl:默认为空串", S0.Url == "")
Check(
"切片 SupportUrl:SetUrl 写入链接",
ReduceSupportUrl(S0, SetUrl("https://support")).Url == "https://support"
)
Check(
"切片 SupportUrl:ClearSupportUrl 清空",
ReduceSupportUrl(SupportUrlStateSlice("https://support"), ClearSupportUrl).Url == ""
)
Check("切片 SupportUrl:无关动作不改状态", ReduceSupportUrl(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetUrl("u"))
Check(
"根:SetUrl 只动 supportUrl",
Root1.SupportUrl.Url == "u" && Root1.RulesUpdated == Root0.RulesUpdated
)
val W = RootWiring.ByField("supportUrl")!!
Check(
"根:supportUrl 委派到 I4 / Yl.ct7 且 2 个动作",
W.SliceClass == "I4" && W.Reducer == "Yl.ct7" &&
W.Actions.toSet() == setOf("ClearSupportUrl", "SetUrl")
)
}
// ㊱ 切片 reducer:RulesUpdated(i1 / u1.dsm,1 字段;1 个实例臂)
run {
val S0 = RulesUpdatedStateSlice()
Check("切片 RulesUpdated:默认 text = null", S0.Text == null)
Check(
"切片 RulesUpdated:UpdateRules 写入正文",
ReduceRulesUpdated(S0, UpdateRules("新规则")).Text == "新规则"
)
Check("切片 RulesUpdated:无关动作不改状态", ReduceRulesUpdated(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateRules("t"))
Check(
"根:UpdateRules 只动 rulesUpdated",
Root1.RulesUpdated.Text == "t" && Root1.NotificationByCountry == Root0.NotificationByCountry
)
val W = RootWiring.ByField("rulesUpdated")!!
Check(
"根:rulesUpdated 委派到 i1 / Ch.dsm",
W.SliceClass == "i1" && W.Reducer == "Ch.dsm" && W.Actions == listOf("UpdateRules")
)
}
// ㊲ 切片 reducer:NotificationByCountry(gJ / $J.ssd,4 字段;Init + qJ)
run {
val S0 = NotificationByCountryStateSlice()
Check(
"切片 NotificationByCountry:默认三条消息为 null、弹窗未展示",
S0.DisableRegistrationMessage == null && S0.DisableShopMessage == null &&
S0.HomeScreenMessage == null && !S0.HomeScreenDialogShown
)
val S1 = ReduceNotificationByCountry(
S0,
InitNotificationByCountry("r", "s", "h"),
)
Check(
"切片 NotificationByCountry:Init 写三条消息且不动弹窗标志",
S1.DisableRegistrationMessage == "r" && S1.DisableShopMessage == "s" &&
S1.HomeScreenMessage == "h" && !S1.HomeScreenDialogShown
)
Check(
"切片 NotificationByCountry:qJ 只把弹窗标志置 true",
ReduceNotificationByCountry(S1, MarkHomeScreenDialogShown) ==
S1.copy(HomeScreenDialogShown = true)
)
Check(
"切片 NotificationByCountry:无关动作不改状态",
ReduceNotificationByCountry(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, MarkHomeScreenDialogShown)
Check(
"根:qJ 只动 notificationByCountry",
Root1.NotificationByCountry.HomeScreenDialogShown &&
Root1.RulesUpdated == Root0.RulesUpdated
)
val W = RootWiring.ByField("notificationByCountry")!!
Check(
"根:notificationByCountry 委派到 gJ / R_.ssd 且 2 个动作",
W.SliceClass == "gJ" && W.Reducer == "R_.ssd" &&
W.Actions.toSet() == setOf("Init", "MarkHomeScreenDialogShown")
)
}
// ㊳ 切片 reducer:AvailabilityByRank(tet / set.xth,1 字段;1 个实例臂)
run {
val S0 = AvailabilityByRankStateSlice()
Check("切片 AvailabilityByRank:默认空表", S0.Data.isEmpty())
val S1 = ReduceAvailabilityByRank(S0, SetLoadedData(mapOf(1 to "a", 2 to "b")))
Check(
"切片 AvailabilityByRank:SetLoadedData 写整张表",
S1.Data == mapOf<Any?, Any?>(1 to "a", 2 to "b")
)
Check("切片 AvailabilityByRank:无关动作不改状态", ReduceAvailabilityByRank(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetLoadedData(mapOf(3 to "c")))
Check(
"根:SetLoadedData 只动 availabilityByRank",
Root1.AvailabilityByRank.Data == mapOf<Any?, Any?>(3 to "c") &&
Root1.PromoCode == Root0.PromoCode
)
val W = RootWiring.ByField("availabilityByRank")!!
Check(
"根:availabilityByRank 委派到 tet / \$p.xth",
W.SliceClass == "tet" && W.Reducer == "\$p.xth" &&
W.Actions == listOf("SetLoadedData")
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)