feat(state): 迁移 news/notifications/quests 切片
This commit is contained in:
1 parent
e66636c737
commit
792973ef5b
11 files changed
+556
-37
No files matched your search
@@ -143,7 +143,7 @@
|
||||
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
|
||||
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
|
||||
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
|
||||
- 已迁移切片 **18/89**:
|
||||
- 已迁移切片 **21/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)
|
||||
@@ -210,12 +210,25 @@
|
||||
- `announcement` → `lC` / `PC().rq5`(`AnnouncementReducer.kt`):5 字段,4 个 `instanceof` 臂。
|
||||
`isEnabled` 缺省 true,其余 false/空列表;`bC`→`DisableAnnouncement`、`qC`→`SetAnnouncementModelLoaded`、
|
||||
`CC`→`SetAnnouncementItems`(生成物 `Init` 属于另一个切片,已改名)、`$C`→`SetSkipNextPromotedAnnounce`。
|
||||
- `news` → `ZX` / `E_.asd`(`NewsReducer.kt`):4 字段,7 个分支(4 个 `instanceof` + 2 个单例 + `hJ`)。
|
||||
`SetNewsItemAction` / `AddNewsItemAction` 都按 `compareBy { -dateInSeconds }` 排序(与原包 32 位取负一致),
|
||||
后者是「追加再排序」;`RemoveNewsItemAction` 按 `id` 过滤,`SetUnreadNewsCounter` 写计数。
|
||||
单例 `x_`/`j_` 定名 `OpenNews` / `CloseNews`;`hJ` 无字段、只把语义未定的 `p` 置 true,暂名 `SetNewsFlag`。
|
||||
- `notifications` → `jJ` / `B_.ise`(`NotificationsReducer.kt`):2 字段,4 个 `instanceof` 臂。
|
||||
缺省 `notifications=emptyList()`、`notificationSound=null`(根状态是 `new jJ()`,生成器认不出类名,迁移表里显式补 `jJ`)。
|
||||
`AddNotification` 追加、`PJ`→`RemoveNotifications(predicate)` 按谓词过滤、`RJ`→`RemoveFirstNotification` 丢第一条。
|
||||
- `quests` → `Frt` / `Yrt.otg`(`QuestsReducer.kt` + `QuestModels.kt`):3 字段,4 个分支。
|
||||
缺省 `currentSection=SPECIAL`、`currentCategory=null`、`isNotCompletedQuests=false`。
|
||||
`GoTo` 到 `tX`(`QuestCategoryScreen(section, categoryId)`)时切到目标分区,到 `nX`(`ChallengeScreen`)时切 `CHALLENGE`;
|
||||
原包那条分支还会发 `Wrt(QuestsChangeCategory)` 命令,属于副作用,留给中间件层。
|
||||
`Hrt` 类体为空、reducer 读的 `t.iu5_1` 是 `undefined`,Kotlin 侧按 `SetNotCompletedQuests` 收口为 false。
|
||||
`tX` / `nX` 是屏幕标识类,已从动作表剔除并记入 `Markers`(`screen-id-class`)。
|
||||
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
|
||||
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
|
||||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 17 个有 reducer 的切片,
|
||||
`launchParams` 与其余 **71 个字段原样透传**;
|
||||
迁移进度用 `RootReducer.Coverage`(`已迁移 18/89`)与 `RootWiring.Unmigrated` 暴露。
|
||||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **350 项全绿**,含 89 槽位完整性、17 个归约切片一次归约互不串片、未迁移字段透传,
|
||||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 20 个有 reducer 的切片,
|
||||
`launchParams` 与其余 **68 个字段原样透传**;
|
||||
迁移进度用 `RootReducer.Coverage`(`已迁移 21/89`)与 `RootWiring.Unmigrated` 暴露。
|
||||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **376 项全绿**,含 89 槽位完整性、20 个归约切片一次归约互不串片、未迁移字段透传,
|
||||
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
|
||||
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
|
||||
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
|
||||
@@ -226,7 +239,8 @@
|
||||
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等);
|
||||
screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、
|
||||
entrance 的 6 个、user 的 3 个、settings 的 2 个、accountSettings 的 11 个、
|
||||
language 的 1 个、announcement 的 3 个匿名类同样按效果定名并登记。
|
||||
language 的 1 个、announcement 的 3 个、news 的 3 个(含两个单例)、notifications 的 2 个、
|
||||
quests 的 1 个匿名类 / 单例同样按效果定名并登记。
|
||||
|
||||
### S4 引擎核心
|
||||
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-10-23T09:52:15.365Z",
|
||||
"generatedAt": "2026-10-24T04:49:01.300Z",
|
||||
"module": "module_10032.js",
|
||||
"RootState": {
|
||||
"Class": "z6",
|
||||
@@ -24,7 +24,10 @@
|
||||
"proBattle",
|
||||
"language",
|
||||
"announcement",
|
||||
"launchParams"
|
||||
"launchParams",
|
||||
"news",
|
||||
"notifications",
|
||||
"quests"
|
||||
],
|
||||
"AnonymousActions": [
|
||||
{
|
||||
@@ -621,6 +624,42 @@
|
||||
"Name": "SetAnnouncementItems",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "items = action.items"
|
||||
},
|
||||
{
|
||||
"Local": "x_",
|
||||
"Name": "OpenNews",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "isOpen = true(`yd(t, x_)` → `n.psc(!0)`;x_ = new QX() 单例)"
|
||||
},
|
||||
{
|
||||
"Local": "j_",
|
||||
"Name": "CloseNews",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "isOpen = false(`yd(t, j_)` → `n.psc(!1)`;j_ = new tJ() 单例)"
|
||||
},
|
||||
{
|
||||
"Local": "hJ",
|
||||
"Name": "SetNewsFlag",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "p = true(`n.psc(cd, cd, cd, !0)`;动作类无字段,字段语义未定)"
|
||||
},
|
||||
{
|
||||
"Local": "PJ",
|
||||
"Name": "RemoveNotifications",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "notifications = notifications.filter {!action.predicate(it)}(`t.hse_1(s) || e.y(s)`)"
|
||||
},
|
||||
{
|
||||
"Local": "RJ",
|
||||
"Name": "RemoveFirstNotification",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "notifications = notifications.drop(1)(`w$(n.tsd_1, 1)`)"
|
||||
},
|
||||
{
|
||||
"Local": "Hrt",
|
||||
"Name": "SetNotCompletedQuests",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "isNotCompletedQuests = action.isNotCompletedQuests(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,原包写入 undefined,Kotlin 侧收口为 false)"
|
||||
}
|
||||
],
|
||||
"Markers": [
|
||||
@@ -653,6 +692,16 @@
|
||||
"Local": "UB",
|
||||
"Kind": "state-class",
|
||||
"Evidence": "garage 切片状态类:`function UB(…31 字段)`,toString 名 \"Garage(...)\";InitState 分支用 `h instanceof UB` 校验灌入状态,不是动作。"
|
||||
},
|
||||
{
|
||||
"Local": "tX",
|
||||
"Kind": "screen-id-class",
|
||||
"Evidence": "屏幕标识类 Lobby.Quests.QuestCategory:`function tX(categoryType, id){ … this.gs8_1 = \"Lobby.Quests.QuestCategory.\" + … }`;quests 的 GoTo 分支用它判断 section,不是 dispatch 的动作。"
|
||||
},
|
||||
{
|
||||
"Local": "nX",
|
||||
"Kind": "screen-id-class",
|
||||
"Evidence": "屏幕标识类 Lobby.Quests.Challenge:`function nX(t){ … this.js8_1 = \"Lobby.Quests.Challenge\" }`;quests 的 GoTo 分支命中它时把 section 置 CHALLENGE,不是 dispatch 的动作。"
|
||||
}
|
||||
],
|
||||
"Rows": [
|
||||
@@ -1040,13 +1089,12 @@
|
||||
"Reducer": "Yrt.otg",
|
||||
"Delegate": "(null == zp && new Yrt(), zp).otg(t, n.npv_1)",
|
||||
"Actions": [
|
||||
"?Hrt",
|
||||
"GoTo",
|
||||
"QuestCategory",
|
||||
"SetCurrentCategory"
|
||||
"SetCurrentCategory",
|
||||
"SetNotCompletedQuests"
|
||||
],
|
||||
"Default": "new Frt()",
|
||||
"Migrated": false
|
||||
"Migrated": true
|
||||
},
|
||||
{
|
||||
"Field": "chatState",
|
||||
@@ -1196,14 +1244,14 @@
|
||||
"Reducer": "E_.asd",
|
||||
"Delegate": "E_.asd(t, n.tpv_1)",
|
||||
"Actions": [
|
||||
"?hJ",
|
||||
"AddNewsItemAction",
|
||||
"RemoveNewsItemAction",
|
||||
"SetNewsFlag",
|
||||
"SetNewsItemAction",
|
||||
"SetUnreadNewsCounter"
|
||||
],
|
||||
"Default": "new ZX()",
|
||||
"Migrated": false
|
||||
"Migrated": true
|
||||
},
|
||||
{
|
||||
"Field": "settings",
|
||||
@@ -1274,17 +1322,17 @@
|
||||
{
|
||||
"Field": "notifications",
|
||||
"Slot": "xpv_1",
|
||||
"SliceClass": null,
|
||||
"SliceClass": "jJ",
|
||||
"Reducer": "B_.ise",
|
||||
"Delegate": "B_.ise(t, n.xpv_1)",
|
||||
"Actions": [
|
||||
"?PJ",
|
||||
"?RJ",
|
||||
"AddNotification",
|
||||
"InitNotificationSound"
|
||||
"InitNotificationSound",
|
||||
"RemoveFirstNotification",
|
||||
"RemoveNotifications"
|
||||
],
|
||||
"Default": "",
|
||||
"Migrated": false
|
||||
"Migrated": true
|
||||
},
|
||||
{
|
||||
"Field": "friends",
|
||||
|
||||
@@ -158,6 +158,18 @@ const AnonymousActionNames = new Map([
|
||||
['bC', 'DisableAnnouncement'],
|
||||
['qC', 'SetAnnouncementModelLoaded'],
|
||||
['CC', 'SetAnnouncementItems'],
|
||||
// notifications 切片(jJ / B_.ise):2 个没有 toString / 不在 action_inventory 的类。
|
||||
// PJ 是带谓词的移除,RJ 是丢掉第一条;实现见 NotificationsReducer.kt。
|
||||
['PJ', 'RemoveNotifications'],
|
||||
['RJ', 'RemoveFirstNotification'],
|
||||
// quests 切片(Frt / Yrt.otg):1 个没有 toString / 不在 action_inventory 的类。
|
||||
// 实现见 QuestsReducer.kt。
|
||||
['Hrt', 'SetNotCompletedQuests'],
|
||||
// news 切片(ZX / E_.asd):hJ 没有 toString;两个单例 x_/j_ 按效果定名。
|
||||
// 实现见 NewsReducer.kt。
|
||||
['hJ', 'SetNewsFlag'],
|
||||
['x_', 'OpenNews'],
|
||||
['j_', 'CloseNews'],
|
||||
]);
|
||||
|
||||
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
|
||||
@@ -167,6 +179,8 @@ const NonActionInstanceof = new Map([
|
||||
['RX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 System.Loader:`function RX(){ this.ysb_1="System.Loader" }`,`PX()` 返回该单例,也是 WK.current 的默认值;GoTo 到它时不发 screen_show。' }],
|
||||
['FX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Garage:`function FX(t){ … this.irm_1 = t; this.jrm_1 = "Garage" }`,默认分类 `BB().sq5_1`;garage 的 GoTo 分支用它把 currentCategory 复位。' }],
|
||||
['UB', { Kind: 'state-class', Evidence: 'garage 切片状态类:`function UB(…31 字段)`,toString 名 "Garage(...)";InitState 分支用 `h instanceof UB` 校验灌入状态,不是动作。' }],
|
||||
['tX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Lobby.Quests.QuestCategory:`function tX(categoryType, id){ … this.gs8_1 = "Lobby.Quests.QuestCategory." + … }`;quests 的 GoTo 分支用它判断 section,不是 dispatch 的动作。' }],
|
||||
['nX', { Kind: 'screen-id-class', Evidence: '屏幕标识类 Lobby.Quests.Challenge:`function nX(t){ … this.js8_1 = "Lobby.Quests.Challenge" }`;quests 的 GoTo 分支命中它时把 section 置 CHALLENGE,不是 dispatch 的动作。' }],
|
||||
]);
|
||||
|
||||
/** 动作局部类名 → 动作名;未收录的返回 null。 */
|
||||
@@ -550,9 +564,24 @@ const Migrated = {
|
||||
Import: false,
|
||||
PassThrough: true,
|
||||
},
|
||||
news: { Type: 'NewsStateSlice', Reduce: 'ReduceNews' },
|
||||
// 原根状态的这一片是 `new jJ()`,生成器没认出切片类,默认值在这里显式补。
|
||||
notifications: {
|
||||
Type: 'NotificationsStateSlice',
|
||||
Default: 'NotificationsStateSlice()',
|
||||
SliceClass: 'jJ',
|
||||
Reduce: 'ReduceNotifications',
|
||||
},
|
||||
quests: { Type: 'QuestsStateSlice', Reduce: 'ReduceQuests' },
|
||||
};
|
||||
|
||||
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
|
||||
// 少数切片的类名生成器认不出来(例如根构造里是 `new jJ()` 但字段默认值挡住了推断),
|
||||
// 迁移表里可以显式补一个 SliceClass。
|
||||
for (const R of Rows) {
|
||||
const M = Migrated[R.Field];
|
||||
if (M && M.SliceClass) R.SliceClass = M.SliceClass;
|
||||
}
|
||||
|
||||
// 字段名 → Kotlin 属性名(首字母大写,保留原有大小写与数字)
|
||||
function KotlinName(Field) { return Field.charAt(0).toUpperCase() + Field.slice(1); }
|
||||
@@ -712,6 +741,24 @@ const AnnouncementAnonymousEffects = {
|
||||
CC: 'items = action.items',
|
||||
};
|
||||
|
||||
// news 切片(ZX / E_.asd):没有 toString / 不在 action_inventory 的类与单例,按 reducer 分支效果定名。
|
||||
const NewsAnonymousEffects = {
|
||||
x_: 'isOpen = true(`yd(t, x_)` → `n.psc(!0)`;x_ = new QX() 单例)',
|
||||
j_: 'isOpen = false(`yd(t, j_)` → `n.psc(!1)`;j_ = new tJ() 单例)',
|
||||
hJ: 'p = true(`n.psc(cd, cd, cd, !0)`;动作类无字段,字段语义未定)',
|
||||
};
|
||||
|
||||
// notifications 切片(jJ / NJ.ise):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
|
||||
const NotificationsAnonymousEffects = {
|
||||
PJ: 'notifications = notifications.filter {!action.predicate(it)}(`t.hse_1(s) || e.y(s)`)',
|
||||
RJ: 'notifications = notifications.drop(1)(`w$(n.tsd_1, 1)`)',
|
||||
};
|
||||
|
||||
// quests 切片(Frt / Yrt.otg):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
|
||||
const QuestsAnonymousEffects = {
|
||||
Hrt: 'isNotCompletedQuests = action.isNotCompletedQuests(`n.au5(cd, cd, t.iu5_1)`;动作类无字段,原包写入 undefined,Kotlin 侧收口为 false)',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(P, 'names', 'root_wiring.json'),
|
||||
JSON.stringify(
|
||||
@@ -794,6 +841,24 @@ fs.writeFileSync(
|
||||
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
|
||||
Effect,
|
||||
})),
|
||||
...Object.entries(NewsAnonymousEffects).map(([Local, Effect]) => ({
|
||||
Local,
|
||||
Name: AnonymousActionNames.get(Local),
|
||||
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
|
||||
Effect,
|
||||
})),
|
||||
...Object.entries(NotificationsAnonymousEffects).map(([Local, Effect]) => ({
|
||||
Local,
|
||||
Name: AnonymousActionNames.get(Local),
|
||||
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
|
||||
Effect,
|
||||
})),
|
||||
...Object.entries(QuestsAnonymousEffects).map(([Local, Effect]) => ({
|
||||
Local,
|
||||
Name: AnonymousActionNames.get(Local),
|
||||
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
|
||||
Effect,
|
||||
})),
|
||||
],
|
||||
Markers: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// QuestModels.kt —— 任务切片的枚举与屏幕标识
|
||||
//
|
||||
// 证据链:
|
||||
// · 分区枚举 `kk`:`(Nn = new kk("SPECIAL", 0), Mn = new kk("CHALLENGE", 1), On = new kk("CONTRACT", 2))`,
|
||||
// `vg()` = SPECIAL、`bk()` = CHALLENGE、`qk()` = CONTRACT(module_47243)。
|
||||
// · 原 `tX` 是屏幕标识(不是动作):`m()` 返回 "Lobby.Quests.QuestCategory.<section>[.<id>]",
|
||||
// toString 名 "QuestCategory(categoryType=…, id=…)";quests reducer 用它的 `es8_1` 当新分区。
|
||||
// · 原 `nX`:`m()` 返回 "Lobby.Quests.Challenge",toString 名 "Challenge(battlePassFlavor=…)"。
|
||||
// Sealed interface 要求实现与声明同包,所以这两个屏幕类放在 App.State 而不是 Reducers 子包。
|
||||
package App.State
|
||||
|
||||
/** 原 `kk` 的三个枚举值。 */
|
||||
enum class QuestSection { SPECIAL, CHALLENGE, CONTRACT }
|
||||
|
||||
/** 原 `tX`:`Lobby.Quests.QuestCategory.<section>[.<id>]`。 */
|
||||
data class QuestCategoryScreen(
|
||||
val section: QuestSection,
|
||||
val categoryId: Any? = null,
|
||||
) : ScreenId {
|
||||
override val name: String =
|
||||
"Lobby.Quests.QuestCategory." + section.name + (categoryId?.let { ".$it" } ?: "")
|
||||
}
|
||||
|
||||
/** 原 `nX`:`Lobby.Quests.Challenge`。 */
|
||||
data class ChallengeScreen(
|
||||
val battlePassFlavor: Any?,
|
||||
) : ScreenId {
|
||||
override val name: String = "Lobby.Quests.Challenge"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// NewsReducer.kt —— 新闻切片(对应 module_10032 的 ZX 状态 + E_.asd)
|
||||
//
|
||||
// 证据链:
|
||||
// 状态类 ZX:4 个字段,toString 名 "NewsState(...)";copy 是 psc(4 字段 + 回调)。
|
||||
// 默认:isOpen=false、news=emptyList()、unreadNewsCounter=0、p=false。
|
||||
// 根委派:`E_.asd(t, n.tpv_1)`。
|
||||
// reducer 七个分支:
|
||||
// yd(t, x_) → isOpen=true(x_ = new QX() 单例,无字段)
|
||||
// yd(t, j_) → isOpen=false(j_ = new tJ() 单例,无字段)
|
||||
// t instanceof sJ → news = action.news.sortedWith(compareBy { -dateInSeconds })
|
||||
// t instanceof oJ → news = (news + action.news).sortedWith(同上)
|
||||
// t instanceof _J → news = news.filterNot { it.id == action.newsId }
|
||||
// t instanceof lJ → unreadNewsCounter = action.count
|
||||
// t instanceof hJ → p=true
|
||||
// 排序证据:`wm(r, new pJ((t, n) -> bk(fJ(0, t), fJ(0, n))))`,
|
||||
// fJ(0, t) 是 `0 | -t.li5_1`(li5_1 = NewsItemData.dateInSeconds),bk 是 compareValues。
|
||||
// `p` 的语义不明(只有一支写 true,从不复位),按字段名保留、动作名先叫 SetNewsFlag。
|
||||
package App.State.Reducers
|
||||
|
||||
import App.State.AddNewsItemAction
|
||||
import App.State.NewsItemData
|
||||
import App.State.RemoveNewsItemAction
|
||||
import App.State.SetNewsItemAction
|
||||
import App.State.SetUnreadNewsCounter
|
||||
|
||||
/* ------------------------------------------------------------------ 匿名动作 */
|
||||
|
||||
/** 原 `x_`(`new QX()`):打开新闻面板。 */
|
||||
object OpenNews
|
||||
|
||||
/** 原 `j_`(`new tJ()`):关闭新闻面板。 */
|
||||
object CloseNews
|
||||
|
||||
/** 原 `hJ`:把 `p` 置 true;字段语义未定。 */
|
||||
object SetNewsFlag
|
||||
|
||||
/* ------------------------------------------------------------------ 状态 */
|
||||
|
||||
data class NewsStateSlice(
|
||||
val IsOpen: Boolean = false,
|
||||
val News: List<NewsItemData> = emptyList(),
|
||||
val UnreadNewsCounter: Int = 0,
|
||||
val P: Boolean = false,
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ reducer */
|
||||
|
||||
/**
|
||||
* 原包的排序:`compareBy { -dateInSeconds }`。
|
||||
* 用取负而不是 `compareByDescending`,是为了和原包 32 位取负(`0 | -x`)的溢出行为一致。
|
||||
*/
|
||||
private fun SortNews(News: List<NewsItemData>): List<NewsItemData> =
|
||||
News.sortedWith(compareBy { -it.dateInSeconds })
|
||||
|
||||
fun ReduceNews(
|
||||
State0: NewsStateSlice,
|
||||
Action: Any,
|
||||
): NewsStateSlice = when (Action) {
|
||||
is OpenNews -> State0.copy(IsOpen = true)
|
||||
is CloseNews -> State0.copy(IsOpen = false)
|
||||
is SetNewsItemAction -> State0.copy(News = SortNews(Action.news))
|
||||
is AddNewsItemAction ->
|
||||
State0.copy(News = SortNews(State0.News + (Action.news as NewsItemData)))
|
||||
is RemoveNewsItemAction -> {
|
||||
val NewsId = Action.newsId as Long
|
||||
State0.copy(News = State0.News.filterNot { it.id == NewsId })
|
||||
}
|
||||
is SetUnreadNewsCounter -> State0.copy(UnreadNewsCounter = Action.count)
|
||||
is SetNewsFlag -> State0.copy(P = true)
|
||||
else -> State0
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// NotificationsReducer.kt —— 通知切片(对应 module_10032 的 jJ 状态 + NJ.ise)
|
||||
//
|
||||
// 证据链:
|
||||
// 状态类 jJ:2 个字段,toString 名 "Notifications(...)";copy 是 wsd(2 字段 + 回调)。
|
||||
// 默认:notifications=emptyList()、notificationSound=null。根状态里就是 `new jJ()`。
|
||||
// 根委派:`B_.ise(t, n.xpv_1)`。
|
||||
// reducer 四个 instanceof 臂:
|
||||
// IJ → notificationSound = action.sound
|
||||
// AJ → notifications = notifications + action.notificationItem(`Hg(list, item)`)
|
||||
// RJ → notifications = notifications.drop(1)(`w$(list, 1)`)
|
||||
// PJ → notifications = notifications.filterNot { action.predicate(it) }(`t.hse_1(s) || e.y(s)`)
|
||||
// 通知项的类型在协议层是 `CJ`(带存活时长的通知包装),还没迁移,先用 Any? 收口。
|
||||
package App.State.Reducers
|
||||
|
||||
import App.State.AddNotification
|
||||
import App.State.InitNotificationSound
|
||||
|
||||
/* ------------------------------------------------------------------ 匿名动作 */
|
||||
|
||||
/** 原 `PJ`:带谓词地移除通知;谓词来自调用点。 */
|
||||
class RemoveNotifications(val predicate: (Any?) -> Boolean)
|
||||
|
||||
/** 原 `RJ`:丢掉第一条通知。 */
|
||||
object RemoveFirstNotification
|
||||
|
||||
/* ------------------------------------------------------------------ 状态 */
|
||||
|
||||
data class NotificationsStateSlice(
|
||||
val Notifications: List<Any?> = emptyList(),
|
||||
val NotificationSound: Any? = null,
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ reducer */
|
||||
|
||||
fun ReduceNotifications(
|
||||
State0: NotificationsStateSlice,
|
||||
Action: Any,
|
||||
): NotificationsStateSlice = when (Action) {
|
||||
is InitNotificationSound -> State0.copy(NotificationSound = Action.sound)
|
||||
is AddNotification -> State0.copy(Notifications = State0.Notifications + Action.notificationItem)
|
||||
is RemoveNotifications ->
|
||||
State0.copy(Notifications = State0.Notifications.filterNot(Action.predicate))
|
||||
is RemoveFirstNotification -> State0.copy(Notifications = State0.Notifications.drop(1))
|
||||
else -> State0
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// QuestsReducer.kt —— 任务切片(对应 module_10032 的 Frt 状态 + Yrt.otg)
|
||||
//
|
||||
// 证据链:
|
||||
// 状态类 Frt:3 个字段,toString 名 "Quests(...)";copy 是 au5(3 字段 + 回调)。
|
||||
// 默认:currentSection=vg()=SPECIAL、currentCategory=null、isNotCompletedQuests=false。
|
||||
// 根委派:`(null == zp && new Yrt(), zp).otg(t, n.npv_1)`。
|
||||
// reducer:
|
||||
// t instanceof Vrt → currentCategory = action.currentCategory
|
||||
// t instanceof Hrt → isNotCompletedQuests = action.isNotCompletedQuests
|
||||
// (Hrt 类体为空、没有字段,`t.iu5_1` 是 undefined;Kotlin 侧收口为 false)
|
||||
// t instanceof QK(GoTo)
|
||||
// · 目标是 tX(Lobby.Quests.QuestCategory.<section>[.<id>])→ currentSection = 目标里的 section
|
||||
// 并额外 dispatch `Wrt(previousScreen, previousCategoryId, section)`(见下方注释)
|
||||
// · 目标是 nX(Lobby.Quests.Challenge)→ currentSection = CHALLENGE(`pg()`)
|
||||
// · 其他目标 → 状态不变
|
||||
// `currentCategory` 的真实类型是任务分类对象(QuestCategoryData),切片里只当不透明值保存。
|
||||
package App.State.Reducers
|
||||
|
||||
import App.State.ChallengeScreen
|
||||
import App.State.GoTo
|
||||
import App.State.QuestCategoryScreen
|
||||
import App.State.QuestSection
|
||||
import App.State.SetCurrentCategory
|
||||
|
||||
/* ------------------------------------------------------------------ 匿名动作 */
|
||||
|
||||
/**
|
||||
* 原 `Hrt`:类体为空,reducer 读的 `t.iu5_1` 恒为 undefined。
|
||||
* 原包的实际效果是把 isNotCompletedQuests 置成假值,这里按 false 收口。
|
||||
*/
|
||||
object SetNotCompletedQuests
|
||||
|
||||
/* ------------------------------------------------------------------ 状态 */
|
||||
|
||||
data class QuestsStateSlice(
|
||||
val CurrentSection: QuestSection = QuestSection.SPECIAL,
|
||||
val CurrentCategory: Any? = null,
|
||||
val IsNotCompletedQuests: Boolean = false,
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ reducer */
|
||||
|
||||
fun ReduceQuests(
|
||||
State0: QuestsStateSlice,
|
||||
Action: Any,
|
||||
): QuestsStateSlice = when (Action) {
|
||||
is SetCurrentCategory -> State0.copy(CurrentCategory = Action.currentCategory)
|
||||
is SetNotCompletedQuests -> State0.copy(IsNotCompletedQuests = false)
|
||||
// 原包的 QuestCategory 分支还会 `u.uar(new Wrt(currentScreen, currentCategoryId, section))`
|
||||
// 发一条 QuestsChangeCategory 命令;那是副作用,不属于纯状态归约,等中间件层再补。
|
||||
is GoTo -> when (val Screen = Action.screen) {
|
||||
is QuestCategoryScreen -> State0.copy(CurrentSection = Screen.section)
|
||||
is ChallengeScreen -> State0.copy(CurrentSection = QuestSection.CHALLENGE)
|
||||
else -> State0
|
||||
}
|
||||
else -> State0
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
|
||||
//
|
||||
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
|
||||
// 这里采用同一形状。已迁移的 18 个切片里,17 个真正调用 reducer,launchParams 只做类型收口:
|
||||
// 这里采用同一形状。已迁移的 21 个切片里,20 个真正调用 reducer,launchParams 只做类型收口:
|
||||
//
|
||||
// inviteState → ReduceInvite(aD / pD.er5)
|
||||
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
|
||||
@@ -20,9 +20,12 @@
|
||||
// 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)`)
|
||||
// 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)`)
|
||||
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
|
||||
//
|
||||
// 其余 71 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
|
||||
// 其余 68 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
|
||||
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
|
||||
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
|
||||
package App.State
|
||||
@@ -38,7 +41,10 @@ import App.State.Reducers.ReduceLanguage
|
||||
import App.State.Reducers.ReduceLocalBattleUser
|
||||
import App.State.Reducers.ReduceLoginByPassword
|
||||
import App.State.Reducers.ReduceLootBoxes
|
||||
import App.State.Reducers.ReduceNews
|
||||
import App.State.Reducers.ReduceNotifications
|
||||
import App.State.Reducers.ReduceProBattle
|
||||
import App.State.Reducers.ReduceQuests
|
||||
import App.State.Reducers.ReduceScreen
|
||||
import App.State.Reducers.ReduceSettings
|
||||
import App.State.Reducers.ReduceShop
|
||||
@@ -75,6 +81,9 @@ object RootReducer {
|
||||
ProBattle = ReduceProBattle(State0.ProBattle, Action),
|
||||
Language = ReduceLanguage(State0.Language, Action),
|
||||
Announcement = ReduceAnnouncement(State0.Announcement, Action),
|
||||
News = ReduceNews(State0.News, Action),
|
||||
Notifications = ReduceNotifications(State0.Notifications, Action),
|
||||
Quests = ReduceQuests(State0.Quests, Action),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ import App.State.Reducers.InviteStateSlice
|
||||
import App.State.Reducers.LocalBattleUserStateSlice
|
||||
import App.State.Reducers.LoginByPasswordStateSlice
|
||||
import App.State.Reducers.LootBoxesStateSlice
|
||||
import App.State.Reducers.NewsStateSlice
|
||||
import App.State.Reducers.NotificationsStateSlice
|
||||
import App.State.Reducers.ProBattleStateSlice
|
||||
import App.State.Reducers.QuestsStateSlice
|
||||
import App.State.Reducers.ScreenStateSlice
|
||||
import App.State.Reducers.SettingsStateSlice
|
||||
import App.State.Reducers.ShopStateSlice
|
||||
@@ -56,8 +59,8 @@ data class RootState(
|
||||
val PasswordChangeState: Any? = null,
|
||||
/** 原 system(_9 / w9().xt9)。未迁移:原样透传。 */
|
||||
val System: Any? = null,
|
||||
/** 原 quests(Frt / Yrt.otg)。未迁移:原样透传。 */
|
||||
val Quests: Any? = null,
|
||||
/** 原 quests(Frt / Yrt.otg)。已迁移。 */
|
||||
val Quests: QuestsStateSlice = QuestsStateSlice(),
|
||||
/** 原 chatState(LP / $r.wqv)。未迁移:原样透传。 */
|
||||
val ChatState: Any? = null,
|
||||
/** 原 clanChatState(qnt / yv.qtg)。未迁移:原样透传。 */
|
||||
@@ -68,16 +71,16 @@ data class RootState(
|
||||
val ClansStorage: Any? = null,
|
||||
/** 原 tipsState(dst / Hw.vth)。未迁移:原样透传。 */
|
||||
val TipsState: Any? = null,
|
||||
/** 原 news(ZX / E_.asd)。未迁移:原样透传。 */
|
||||
val News: Any? = null,
|
||||
/** 原 news(ZX / E_.asd)。已迁移。 */
|
||||
val News: NewsStateSlice = NewsStateSlice(),
|
||||
/** 原 settings(Uut / xw.ltg)。已迁移。 */
|
||||
val Settings: SettingsStateSlice = SettingsStateSlice(),
|
||||
/** 原 accountSettings(oet / Lp.mtg)。已迁移。 */
|
||||
val AccountSettings: AccountSettingsStateSlice = AccountSettingsStateSlice(),
|
||||
/** 原 lootBoxes(aH / ws.wrv)。已迁移。 */
|
||||
val LootBoxes: LootBoxesStateSlice = LootBoxesStateSlice(),
|
||||
/** 原 notifications(? / B_.ise)。未迁移:原样透传。 */
|
||||
val Notifications: Any? = null,
|
||||
/** 原 notifications(jJ / B_.ise)。已迁移。 */
|
||||
val Notifications: NotificationsStateSlice = NotificationsStateSlice(),
|
||||
/** 原 friends(KD / NL().cr9)。未迁移:原样透传。 */
|
||||
val Friends: Any? = null,
|
||||
/** 原 remoteUserDataStorage($Q / vh.isl)。未迁移:原样透传。 */
|
||||
|
||||
@@ -29,17 +29,17 @@ object RootWiring {
|
||||
SliceWiring("captchaState", "kpv_1", "jP", "ar.iqt", "ar.iqt(t, n.kpv_1)", listOf("?AP", "?IP", "?PP", "?RP", "?SP", "LocationsUpdated", "SetLocations")),
|
||||
SliceWiring("passwordChangeState", "lpv_1", "VD", "uu.pr6", "uu.pr6(t, n.lpv_1)", listOf("SetPasswordChangeHash", "ShowEmailAndPasswordChangeDialog")),
|
||||
SliceWiring("system", "mpv_1", "_9", "w9().xt9", "w9().xt9(t, n.mpv_1)", listOf("DoShowMessage", "SetFatalError")),
|
||||
SliceWiring("quests", "npv_1", "Frt", "Yrt.otg", "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", listOf("?Hrt", "GoTo", "QuestCategory", "SetCurrentCategory")),
|
||||
SliceWiring("quests", "npv_1", "Frt", "Yrt.otg", "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", listOf("GoTo", "SetCurrentCategory", "SetNotCompletedQuests")),
|
||||
SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("?QP", "?XP", "?_C", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")),
|
||||
SliceWiring("clanChatState", "ppv_1", "qnt", "yv.qtg", "yv.qtg(t, n.ppv_1)", listOf("?\$nt", "?Tnt", "?xnt", "Configure", "DoAddMessages")),
|
||||
SliceWiring("clan", "qpv_1", "jN", "we.hr0", "we.hr0(t, n.qpv_1)", listOf("?FM", "?IM", "?KM", "?PM", "?RM", "?VM", "?YM", "?iM", "AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", "AddNewIncomingClan", "AddOutgoingClan", "AddUsersToAccepted", "AddUsersToNewAccepted", "ClanBanned", "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", "NewIncomingUserNotification", "OutgoingAdded", "OutgoingRemoved", "RemoveIncomingClan", "RemoveNewIncoming", "RemoveNewIncomingClan", "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", "SetClanEditPrice", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", "UpdateTimeToJoinRestriction", "UpdateValidation")),
|
||||
SliceWiring("clansStorage", "rpv_1", "lO", "me.vr1", "me.vr1(t, n.rpv_1)", listOf("?mO", "AddUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", "ClanLoaded", "ClanUnloaded", "ForeignClanLoaded", "MinimalRankChanged", "RecruitmentChanged", "RemoveUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan")),
|
||||
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("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("AddNewsItemAction", "RemoveNewsItemAction", "SetNewsFlag", "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("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("notifications", "xpv_1", "jJ", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("AddNotification", "InitNotificationSound", "RemoveFirstNotification", "RemoveNotifications")),
|
||||
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")),
|
||||
SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")),
|
||||
SliceWiring("battlesSelect", "apw_1", "fA", "Mi.vqk", "Mi.vqk(t, n.apw_1)", listOf("?dA", "?gA", "AddBattle", "AddUser", "AddUsers", "ChangeName", "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", "RemoveBattle", "RemoveUser", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", "SwapTeam", "UpdateFund", "UpdateSuspicion", "UpdateTeamScore", "UpdateUserScore", "UpdateUserSuspicion")),
|
||||
@@ -106,7 +106,7 @@ object RootWiring {
|
||||
)
|
||||
|
||||
/** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */
|
||||
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams")
|
||||
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 Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
// ⑥ 动作目录: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;
|
||||
// AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests;
|
||||
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
|
||||
//
|
||||
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
|
||||
@@ -165,6 +165,14 @@ import App.State.DisplayMainEntranceScreen
|
||||
import App.State.DisplayReauthorizationMessage
|
||||
import App.State.EntranceScreen
|
||||
import App.State.GoTo
|
||||
import App.State.AddNewsItemAction
|
||||
import App.State.AddNotification
|
||||
import App.State.InitNotificationSound
|
||||
import App.State.NewsItemData
|
||||
import App.State.RemoveNewsItemAction
|
||||
import App.State.SetCurrentCategory
|
||||
import App.State.SetNewsItemAction
|
||||
import App.State.SetUnreadNewsCounter
|
||||
import App.State.LoaderScreen
|
||||
import App.State.NamedScreen
|
||||
import App.State.PressedAnyButton
|
||||
@@ -302,6 +310,21 @@ import App.State.Reducers.SetRepeatNewPassword
|
||||
import App.State.Reducers.SetSettingsCategory
|
||||
import App.State.Reducers.SetUnlockButtonForRenameUid
|
||||
import App.State.Reducers.SettingsStateSlice
|
||||
import App.State.ChallengeScreen
|
||||
import App.State.Reducers.CloseNews
|
||||
import App.State.Reducers.NewsStateSlice
|
||||
import App.State.Reducers.NotificationsStateSlice
|
||||
import App.State.Reducers.OpenNews
|
||||
import App.State.QuestCategoryScreen
|
||||
import App.State.QuestSection
|
||||
import App.State.Reducers.QuestsStateSlice
|
||||
import App.State.Reducers.ReduceNews
|
||||
import App.State.Reducers.ReduceNotifications
|
||||
import App.State.Reducers.ReduceQuests
|
||||
import App.State.Reducers.RemoveFirstNotification
|
||||
import App.State.Reducers.RemoveNotifications
|
||||
import App.State.Reducers.SetNewsFlag
|
||||
import App.State.Reducers.SetNotCompletedQuests
|
||||
import App.State.Reducers.UserRankBounds
|
||||
import App.State.Reducers.UserStateSlice
|
||||
import App.State.Reducers.ClearHullData
|
||||
@@ -729,7 +752,7 @@ object StateCheck {
|
||||
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
|
||||
}
|
||||
|
||||
// ⑭ 根 reducer:89 个槽位的委派表 + 18 个已迁移切片(17 归约 + launchParams 透传)
|
||||
// ⑭ 根 reducer:89 个槽位的委派表 + 21 个已迁移切片(20 归约 + launchParams 透传)
|
||||
run {
|
||||
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
|
||||
Check(
|
||||
@@ -793,15 +816,16 @@ object StateCheck {
|
||||
LocalBattleW.Reducer == "(inline@localBattleUserState)"
|
||||
)
|
||||
Check(
|
||||
"根:迁移表 = 18 个已迁移字段,其余 71 个待迁",
|
||||
"根:迁移表 = 21 个已迁移字段,其余 68 个待迁",
|
||||
RootReducer.Migrated ==
|
||||
setOf(
|
||||
"inviteState", "loginByPassword", "twitchState", "home",
|
||||
"localBattleUserState", "shop", "screen", "lootBoxes", "garage",
|
||||
"garagePreview", "entrance", "user", "settings", "accountSettings",
|
||||
"proBattle", "language", "announcement", "launchParams",
|
||||
"news", "notifications", "quests",
|
||||
) &&
|
||||
RootWiring.Unmigrated.size == 71
|
||||
RootWiring.Unmigrated.size == 68
|
||||
)
|
||||
|
||||
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
|
||||
@@ -857,7 +881,7 @@ object StateCheck {
|
||||
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
|
||||
)
|
||||
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
|
||||
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 18/89")
|
||||
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 21/89")
|
||||
|
||||
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
|
||||
Store.Dispatch(SetStatus("LOGGING_IN"))
|
||||
@@ -2925,6 +2949,160 @@ object StateCheck {
|
||||
)
|
||||
}
|
||||
|
||||
// ㉙ 切片 reducer:News(ZX / E_.asd,4 字段;7 个分支)
|
||||
run {
|
||||
val S0 = NewsStateSlice()
|
||||
Check(
|
||||
"切片 News:默认值完整",
|
||||
!S0.IsOpen && S0.News.isEmpty() && S0.UnreadNewsCounter == 0 && !S0.P
|
||||
)
|
||||
val N1 = NewsItemData(10, "d1", 0, "h1", 1L, "")
|
||||
val N2 = NewsItemData(30, "d2", 0, "h2", 2L, "")
|
||||
val N3 = NewsItemData(20, "d3", 0, "h3", 3L, "")
|
||||
Check(
|
||||
"切片 News:SetNewsItemAction 按 dateInSeconds 降序",
|
||||
ReduceNews(S0, SetNewsItemAction(listOf(N1, N2, N3))).News.map { it.id } ==
|
||||
listOf(2L, 3L, 1L)
|
||||
)
|
||||
val S1 = ReduceNews(S0, SetNewsItemAction(listOf(N1, N2)))
|
||||
Check(
|
||||
"切片 News:AddNewsItemAction 追加后重新排序",
|
||||
ReduceNews(S1, AddNewsItemAction(N3)).News.map { it.id } == listOf(2L, 3L, 1L)
|
||||
)
|
||||
Check(
|
||||
"切片 News:RemoveNewsItemAction 按 id 删除",
|
||||
ReduceNews(S1, RemoveNewsItemAction(2L)).News.map { it.id } == listOf(1L)
|
||||
)
|
||||
Check(
|
||||
"切片 News:SetUnreadNewsCounter 写计数",
|
||||
ReduceNews(S0, SetUnreadNewsCounter(5)).UnreadNewsCounter == 5
|
||||
)
|
||||
Check(
|
||||
"切片 News:OpenNews/CloseNews 开关面板、SetNewsFlag 置 p",
|
||||
ReduceNews(ReduceNews(ReduceNews(S0, OpenNews), CloseNews), SetNewsFlag) ==
|
||||
NewsStateSlice(IsOpen = false, P = true)
|
||||
)
|
||||
Check("切片 News:无关动作不改状态", ReduceNews(S0, AddFrags(1)) == S0)
|
||||
|
||||
val Root0 = RootState()
|
||||
val Root1 = ReduceRoot(Root0, OpenNews)
|
||||
Check(
|
||||
"根:OpenNews 只动 news",
|
||||
Root1.News.IsOpen && Root1.Notifications == Root0.Notifications
|
||||
)
|
||||
val W = RootWiring.ByField("news")!!
|
||||
Check(
|
||||
"根:news 委派到 ZX / E_.asd 且 5 个动作全部具名",
|
||||
W.SliceClass == "ZX" && W.Reducer == "E_.asd" &&
|
||||
W.Actions.toSet() == setOf(
|
||||
"AddNewsItemAction", "RemoveNewsItemAction", "SetNewsFlag",
|
||||
"SetNewsItemAction", "SetUnreadNewsCounter",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ㉚ 切片 reducer:Notifications(jJ / B_.ise,2 字段;4 个 instanceof 臂)
|
||||
run {
|
||||
val S0 = NotificationsStateSlice()
|
||||
Check(
|
||||
"切片 Notifications:默认值完整",
|
||||
S0.Notifications.isEmpty() && S0.NotificationSound == null
|
||||
)
|
||||
val S1 = ReduceNotifications(S0, InitNotificationSound("ding"))
|
||||
Check(
|
||||
"切片 Notifications:InitNotificationSound 写音效",
|
||||
S1.NotificationSound == "ding" && S1.Notifications.isEmpty()
|
||||
)
|
||||
val S2 = ReduceNotifications(
|
||||
ReduceNotifications(S1, AddNotification("n1")),
|
||||
AddNotification("n2"),
|
||||
)
|
||||
Check(
|
||||
"切片 Notifications:AddNotification 追加到末尾",
|
||||
S2.Notifications == listOf<Any?>("n1", "n2")
|
||||
)
|
||||
Check(
|
||||
"切片 Notifications:RemoveNotifications 按谓词过滤",
|
||||
ReduceNotifications(S2, RemoveNotifications { it == "n1" }).Notifications ==
|
||||
listOf<Any?>("n2")
|
||||
)
|
||||
Check(
|
||||
"切片 Notifications:RemoveFirstNotification 丢掉第一条",
|
||||
ReduceNotifications(S2, RemoveFirstNotification).Notifications ==
|
||||
listOf<Any?>("n2")
|
||||
)
|
||||
Check("切片 Notifications:无关动作不改状态", ReduceNotifications(S2, AddFrags(1)) == S2)
|
||||
|
||||
val Root0 = RootState()
|
||||
val Root1 = ReduceRoot(Root0, AddNotification("x"))
|
||||
Check(
|
||||
"根:AddNotification 只动 notifications",
|
||||
Root1.Notifications.Notifications == listOf<Any?>("x") && Root1.News == Root0.News
|
||||
)
|
||||
val W = RootWiring.ByField("notifications")!!
|
||||
Check(
|
||||
"根:notifications 委派到 jJ / B_.ise 且 4 个动作全部具名",
|
||||
W.SliceClass == "jJ" && W.Reducer == "B_.ise" &&
|
||||
W.Actions.toSet() == setOf(
|
||||
"AddNotification", "InitNotificationSound",
|
||||
"RemoveFirstNotification", "RemoveNotifications",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ㉛ 切片 reducer:Quests(Frt / Yrt.otg,3 字段;GoTo 按目标屏幕切分区)
|
||||
run {
|
||||
val S0 = QuestsStateSlice()
|
||||
Check(
|
||||
"切片 Quests:默认值完整",
|
||||
S0.CurrentSection == QuestSection.SPECIAL && S0.CurrentCategory == null &&
|
||||
!S0.IsNotCompletedQuests
|
||||
)
|
||||
Check(
|
||||
"切片 Quests:SetCurrentCategory 写分类",
|
||||
ReduceQuests(S0, SetCurrentCategory("cat-1")).CurrentCategory == "cat-1"
|
||||
)
|
||||
Check(
|
||||
"切片 Quests:SetNotCompletedQuests 收口为 false",
|
||||
!ReduceQuests(QuestsStateSlice(IsNotCompletedQuests = true), SetNotCompletedQuests)
|
||||
.IsNotCompletedQuests
|
||||
)
|
||||
Check(
|
||||
"切片 Quests:屏幕名按原包拼接",
|
||||
QuestCategoryScreen(QuestSection.SPECIAL, 7L).name ==
|
||||
"Lobby.Quests.QuestCategory.SPECIAL.7" &&
|
||||
ChallengeScreen(null).name == "Lobby.Quests.Challenge"
|
||||
)
|
||||
val S1 = ReduceQuests(S0, GoTo(QuestCategoryScreen(QuestSection.CONTRACT, 42L), false))
|
||||
Check(
|
||||
"切片 Quests:GoTo(QuestCategory) 切到目标分区",
|
||||
S1.CurrentSection == QuestSection.CONTRACT
|
||||
)
|
||||
val S2 = ReduceQuests(S1, GoTo(ChallengeScreen(null), false))
|
||||
Check(
|
||||
"切片 Quests:GoTo(Challenge) 切到 CHALLENGE",
|
||||
S2.CurrentSection == QuestSection.CHALLENGE
|
||||
)
|
||||
Check(
|
||||
"切片 Quests:其他屏幕的 GoTo 不改状态",
|
||||
ReduceQuests(S2, GoTo(NamedScreen("Lobby.Home"), true)) == S2
|
||||
)
|
||||
|
||||
val Root0 = RootState()
|
||||
val Root1 = ReduceRoot(Root0, GoTo(QuestCategoryScreen(QuestSection.CHALLENGE, null), false))
|
||||
Check(
|
||||
"根:GoTo 只动 quests 的分区",
|
||||
Root1.Quests.CurrentSection == QuestSection.CHALLENGE &&
|
||||
Root1.Notifications == Root0.Notifications
|
||||
)
|
||||
val W = RootWiring.ByField("quests")!!
|
||||
Check(
|
||||
"根:quests 委派到 Frt / Yrt.otg,屏幕标识类不算动作",
|
||||
W.SliceClass == "Frt" && W.Reducer == "Yrt.otg" &&
|
||||
W.Actions == listOf("GoTo", "SetCurrentCategory", "SetNotCompletedQuests")
|
||||
)
|
||||
}
|
||||
|
||||
val Failed = Results.filter { !it.second }
|
||||
println("TankiOffline 状态层自检:${Results.size} 项")
|
||||
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)
|
||||
|
||||
Reference in new issue
Block a user