feat(state): 迁移补给箱切片(18 个动作 + ContainerType)

This commit is contained in:
TermiNexus committed 2026-10-23 12:25:43 +08:00
1 parent 974a9f49ad
commit f66f431670
13 files changed
+606 -35

No files matched your search

+12 -5
View File
@@ -143,7 +143,7 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **7/89**:
- 已迁移切片 **8/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)
@@ -158,9 +158,15 @@
`current` 换成目标屏并清 `isResetBackStackOnNextGoTo` / `isDelayedGoto`;`screen_show` analytics 是副作用,纯 reducer 只保留状态。
屏幕身份用 `App.State.ScreenId`(`EntranceScreen` / `LoaderScreen` / `NamedScreen(name)`);
`LX` / `RX` 是屏幕标识类,已从动作表剔除并记入 `root_wiring.json` 的 `Markers`(`screen-id-class`)。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述七片,其余 **82 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 7/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **141 项全绿**,含 89 槽位完整性、七片一次归约互不串片、未迁移字段透传,
- `lootBoxes` → `aH` / `EH.wrv`(`LootBoxesReducer.kt`):18 字段,18 个 `instanceof` 臂全量翻译。
`timeToGiveLootBox` 是 Long 毫秒,`SetTimeToGiveLootBox` 按 `Date.now() + seconds * 1000` 写入;
`AddKeys` 沿用原 `Map.getValue` 语义(缺键抛错),`SetRewards` 同时把 `isReopening` 置 false;
`ClearRewards` 清空 `rewards` / `shownRewards`,`ShownRewards` 是追加(原 `List.plus`)。
补给箱类型用 `App.State.ContainerType`(`STANDARD/RARE/EPIC/LEGENDARY`,默认 `STANDARD`);
无 `toString` 的 `mH/kH/zH/gH/CH/SH` 按效果定名(`SetTimeToGiveLootBox`/`SetPeriodOfGiveLootBox`/…)。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述八片,其余 **81 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 8/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **163 项全绿**,含 89 槽位完整性、八片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
@@ -168,7 +174,8 @@
- 无 `toString` 的默认动作按证据定名(`root_wiring.json` 的 `AnonymousActions`):
`o1` → `ShowShopDiscountNotification`(`$_$.gl` 单例,由 module_18334 的 `Qx.qge` 派发;
效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示);
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等)。
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等);
screen 的 9 个、lootBoxes 的 6 个匿名类同样按效果定名并登记。
### S4 引擎核心
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
+46 -9
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-22T19:22:01.390Z",
"generatedAt": "2026-10-23T04:19:26.216Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -13,7 +13,8 @@
"home",
"localBattleUserState",
"shop",
"screen"
"screen",
"lootBoxes"
],
"AnonymousActions": [
{
@@ -178,6 +179,42 @@
"Name": "PopScreen",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)"
},
{
"Local": "mH",
"Name": "SetTimeToGiveLootBox",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "timeToGiveLootBox = Date.now() + seconds * 1000(第 9 个构造参数 `Fg(Md(), Qk.y2o(Wg(t.zrv_1)))`)"
},
{
"Local": "kH",
"Name": "SetPeriodOfGiveLootBox",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "periodOfGiveLootBoxInSec = action.yrv_1(第 10 个构造参数)"
},
{
"Local": "zH",
"Name": "ClearRewards",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "rewards = emptyList()、shownRewards = emptyList()(第 6、7 个构造参数 `md(), md()`)"
},
{
"Local": "gH",
"Name": "LootBoxOpeningFailed",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "openingFailed = true(第 11 个构造参数 !0)"
},
{
"Local": "CH",
"Name": "SetIsReopening",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isReopening = true(第 17 个构造参数 !0)"
},
{
"Local": "SH",
"Name": "SetIsBlockOpenButtons",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "isBlockOpenButtons = action.xrv_1(第 18 个构造参数)"
}
],
"Markers": [
@@ -798,27 +835,27 @@
"Reducer": "ws.wrv",
"Delegate": "ws.wrv(t, n.wpv_1)",
"Actions": [
"?CH",
"?SH",
"?gH",
"?kH",
"?mH",
"?zH",
"AddKeys",
"ClearRewards",
"LootBoxOpeningFailed",
"SelectCategory",
"SetContainerData",
"SetCurrentContainer",
"SetGoToGarage",
"SetIsAccelerated",
"SetIsBlockOpenButtons",
"SetIsOpening",
"SetIsReopening",
"SetIsShopVisited",
"SetKeys",
"SetPeriodOfGiveLootBox",
"SetPossibleRewards",
"SetRewards",
"SetTimeToGiveLootBox",
"ShownRewards"
],
"Default": "new aH()",
"Migrated": false
"Migrated": true
},
{
"Field": "notifications",
+155
View File
@@ -0,0 +1,155 @@
{
"generatedAt": "2026-10-23T03:54:25.170Z",
"module": "module_10032.js",
"Field": "lootBoxes",
"Slot": "wpv_1",
"Class": "aH",
"ClassSource": "new aH()",
"Params": [
"t",
"n",
"i",
"r",
"e",
"u",
"s",
"o",
"_",
"h",
"a",
"c",
"l",
"f",
"v",
"p",
"w",
"d"
],
"Fields": [
{
"Field": "rewardsForKeys",
"Slot": "lru_1",
"Kotlin": "RewardsForKeys",
"Default": "md()"
},
{
"Field": "currentContainerType",
"Slot": "mru_1",
"Kotlin": "CurrentContainerType",
"Default": "Ly()"
},
{
"Field": "keys",
"Slot": "nru_1",
"Kotlin": "Keys",
"Default": "ag()"
},
{
"Field": "containerNames",
"Slot": "oru_1",
"Kotlin": "ContainerNames",
"Default": "ag()"
},
{
"Field": "containerDescriptions",
"Slot": "pru_1",
"Kotlin": "ContainerDescriptions",
"Default": "ag()"
},
{
"Field": "rewards",
"Slot": "qru_1",
"Kotlin": "Rewards",
"Default": "md()"
},
{
"Field": "shownRewards",
"Slot": "rru_1",
"Kotlin": "ShownRewards",
"Default": "md()"
},
{
"Field": "gotoGarage",
"Slot": "sru_1",
"Kotlin": "GotoGarage",
"Default": "false"
},
{
"Field": "timeToGiveLootBox",
"Slot": "tru_1",
"Kotlin": "TimeToGiveLootBox",
"Default": "new Dd(0, 0)"
},
{
"Field": "periodOfGiveLootBoxInSec",
"Slot": "uru_1",
"Kotlin": "PeriodOfGiveLootBoxInSec",
"Default": "0"
},
{
"Field": "openingFailed",
"Slot": "vru_1",
"Kotlin": "OpeningFailed",
"Default": "false"
},
{
"Field": "adsAvailable",
"Slot": "wru_1",
"Kotlin": "AdsAvailable",
"Default": "false"
},
{
"Field": "currentCategory",
"Slot": "xru_1",
"Kotlin": "CurrentCategory",
"Default": "null"
},
{
"Field": "isOpening",
"Slot": "yru_1",
"Kotlin": "IsOpening",
"Default": "false"
},
{
"Field": "isAccelerated",
"Slot": "zru_1",
"Kotlin": "IsAccelerated",
"Default": "false"
},
{
"Field": "isShopVisited",
"Slot": "arv_1",
"Kotlin": "IsShopVisited",
"Default": "false"
},
{
"Field": "isReopening",
"Slot": "brv_1",
"Kotlin": "IsReopening",
"Default": "false"
},
{
"Field": "isBlockOpenButtons",
"Slot": "crv_1",
"Kotlin": "IsBlockOpenButtons",
"Default": "false"
}
],
"CopyMethod": null,
"Branches": [
{
"Guard": "(具名 reducer)",
"Locals": [],
"Kind": "named",
"Expr": "ws.wrv(t, n.wpv_1)"
}
],
"SubReducers": [],
"Counts": {
"TotalInstanceof": null,
"TopLevelBranches": 1,
"SubReducerArms": 0
},
"Locals": {},
"Delegate": "ws.wrv(t, n.wpv_1)"
}
+5
View File
@@ -569,6 +569,11 @@ function InferDeep(Expr, Mod, Off, Depth) {
// 类型定义见 TankiOffline/.../App/State/ScreenId.kt。
const FieldTypeOverrides = new Map([
['GoTo.screen', 'ScreenId'],
// lootBoxes 切片:lH/qH/yH 的字段在别处的调用点只能给出 Boolean / Any?,但 reducer 里分别是
// ContainerType(默认 Ly()=STANDARD)、可空 ContainerType、及补给箱奖励列表。
['SetCurrentContainer.type', 'ContainerType'],
['SelectCategory.containerCategory', 'ContainerType?'],
['ShownRewards.rewards', 'List<ContainerGivenItem>'],
]);
const Emitted = [];
+24
View File
@@ -68,6 +68,13 @@ const AnonymousActionNames = new Map([
['lY', 'EnableBack'],
['cY', 'DisableBack'],
['fY', 'PopScreen'],
// 补给箱切片(aH / EH.wrv):没有 toString / 不在 action_inventory 的 6 个类。
['mH', 'SetTimeToGiveLootBox'],
['kH', 'SetPeriodOfGiveLootBox'],
['zH', 'ClearRewards'],
['gH', 'LootBoxOpeningFailed'],
['CH', 'SetIsReopening'],
['SH', 'SetIsBlockOpenButtons'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -433,6 +440,7 @@ const Migrated = {
localBattleUserState: { Type: 'LocalBattleUserStateSlice', Reduce: 'ReduceLocalBattleUser' },
shop: { Type: 'ShopStateSlice', Reduce: 'ReduceShop' },
screen: { Type: 'ScreenStateSlice', Reduce: 'ReduceScreen' },
lootBoxes: { Type: 'LootBoxesStateSlice', Reduce: 'ReduceLootBoxes' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -487,6 +495,16 @@ const ScreenAnonymousEffects = {
fY: 'screenStack = dropLast(1)(`f$(stack, 1)` 是 Kotlin dropLast)',
};
// 补给箱切片(aH / EH.wrv)的匿名动作:按 reducer 分支效果定名。
const LootBoxesAnonymousEffects = {
mH: 'timeToGiveLootBox = Date.now() + seconds * 1000(第 9 个构造参数 `Fg(Md(), Qk.y2o(Wg(t.zrv_1)))`)',
kH: 'periodOfGiveLootBoxInSec = action.yrv_1(第 10 个构造参数)',
zH: 'rewards = emptyList()、shownRewards = emptyList()(第 6、7 个构造参数 `md(), md()`)',
gH: 'openingFailed = true(第 11 个构造参数 !0)',
CH: 'isReopening = true(第 17 个构造参数 !0)',
SH: 'isBlockOpenButtons = action.xrv_1(第 18 个构造参数)',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
JSON.stringify(
@@ -515,6 +533,12 @@ fs.writeFileSync(
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
...Object.entries(LootBoxesAnonymousEffects).map(([Local, Effect]) => ({
Local,
Name: AnonymousActionNames.get(Local),
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
Effect,
})),
],
Markers: [
{
@@ -346,14 +346,14 @@ object ActionCatalog {
ActionMeta("SetIcons", SetIcons::class, listOf(ActionFieldMeta("icons", "List<IconData>"))),
ActionMeta("LootBoxes", LootBoxes::class, listOf(ActionFieldMeta("rewardsForKeys", "Any?"), ActionFieldMeta("currentContainerType", "Any?"), ActionFieldMeta("keys", "Any?"), ActionFieldMeta("containerNames", "Any?"), ActionFieldMeta("containerDescriptions", "Any?"), ActionFieldMeta("rewards", "Any?"), ActionFieldMeta("shownRewards", "Any?"), ActionFieldMeta("gotoGarage", "Any?"), ActionFieldMeta("timeToGiveLootBox", "Any?"), ActionFieldMeta("periodOfGiveLootBoxInSec", "Any?"), ActionFieldMeta("openingFailed", "Any?"), ActionFieldMeta("adsAvailable", "Any?"), ActionFieldMeta("currentCategory", "Any?"), ActionFieldMeta("isOpening", "Any?"), ActionFieldMeta("isAccelerated", "Any?"), ActionFieldMeta("isShopVisited", "Any?"), ActionFieldMeta("isReopening", "Any?"), ActionFieldMeta("isBlockOpenButtons", "Any?"))),
ActionMeta("SetPossibleRewards", SetPossibleRewards::class, listOf(ActionFieldMeta("rewards", "Any?"))),
ActionMeta("SetCurrentContainer", SetCurrentContainer::class, listOf(ActionFieldMeta("type", "Boolean"))),
ActionMeta("SetCurrentContainer", SetCurrentContainer::class, listOf(ActionFieldMeta("type", "ContainerType"))),
ActionMeta("SetKeys", SetKeys::class, listOf(ActionFieldMeta("keys", "Map<String, Int>"))),
ActionMeta("AddKeys", AddKeys::class, listOf(ActionFieldMeta("keyType", "String"), ActionFieldMeta("delta", "Int"))),
ActionMeta("SetContainerData", SetContainerData::class, listOf(ActionFieldMeta("type", "String"), ActionFieldMeta("name", "String"), ActionFieldMeta("description", "String"))),
ActionMeta("SetGoToGarage", SetGoToGarage::class, listOf(ActionFieldMeta("gotoGarage", "Boolean"))),
ActionMeta("OpenContainer", OpenContainer::class, listOf(ActionFieldMeta("amount", "Any?"), ActionFieldMeta("type", "Any?"))),
ActionMeta("SelectCategory", SelectCategory::class, listOf(ActionFieldMeta("containerCategory", "Any?"))),
ActionMeta("ShownRewards", ShownRewards::class, listOf(ActionFieldMeta("rewards", "Any?"))),
ActionMeta("SelectCategory", SelectCategory::class, listOf(ActionFieldMeta("containerCategory", "ContainerType?"))),
ActionMeta("ShownRewards", ShownRewards::class, listOf(ActionFieldMeta("rewards", "List<ContainerGivenItem>"))),
ActionMeta("SetIsOpening", SetIsOpening::class, listOf(ActionFieldMeta("isOpening", "Boolean"))),
ActionMeta("SetIsAccelerated", SetIsAccelerated::class, listOf(ActionFieldMeta("isAccelerated", "Boolean"))),
ActionMeta("SetIsShopVisited", SetIsShopVisited::class, listOf(ActionFieldMeta("isShopVisited", "Boolean"))),
@@ -5,7 +5,7 @@ package App.State
import Core.Vector3
// 动作/状态类共 816 个,字段 2150 个(已定类型 509 个)
// 动作/状态类共 816 个,字段 2150 个(已定类型 511 个)
data class AnnouncementState(
val isEnabled: Any?, // 仅 copy 转发(无类型信息)
val modelLoaded: Any?, // 仅 copy 转发(无类型信息)
@@ -2019,7 +2019,7 @@ data class SetPossibleRewards(
)
data class SetCurrentContainer(
val type: Boolean, // callsite
val type: ContainerType, // override
)
data class SetKeys(
@@ -2047,11 +2047,11 @@ data class OpenContainer(
)
data class SelectCategory(
val containerCategory: Any?, // 调用点: i | null
val containerCategory: ContainerType?, // override
)
data class ShownRewards(
val rewards: Any?, // 调用点: t.state.rewards | HT(n)
val rewards: List<ContainerGivenItem>, // override
)
data class SetIsOpening(
@@ -0,0 +1,21 @@
// LootBoxesModels.kt —— 补给箱切片用到的枚举(App.State 包内,供 Inventory.kt 的字段类型引用)
//
// 证据链:module_88693 的 `Vt()` 初始化
// `(u = new Kt("STANDARD", 0)), (s = new Kt("RARE", 1)), (o = new Kt("EPIC", 2)), (_ = new Kt("LEGENDARY", 3))`
// `Yt()` 返回 `u`(STANDARD)——module_10032 的 `Ly()` 正是 `Yt`,即 aH.currentContainerType 的默认值。
package App.State
/** 原 `Kt`:补给箱类型,顺序与枚举 ordinal 一致(STANDARD=0 … LEGENDARY=3)。 */
enum class ContainerType {
STANDARD,
RARE,
EPIC,
LEGENDARY,
;
companion object {
/** 运行期是 JS 枚举对象,toString 就是成员名;认不出时与原包默认值一致落到 STANDARD。 */
fun FromText(Text: Any?): ContainerType =
entries.firstOrNull { it.name == Text?.toString() } ?: STANDARD
}
}
@@ -0,0 +1,135 @@
// LootBoxesReducer.kt —— 补给箱切片(对应 module_10032 的 aH 状态 + EH.wrv reducer)
//
// 证据链:
// 状态类 aH:`function aH(t, n, i, r, e, u, s, o, _, h, a, c, l, f, v, p, w, d)`,18 个字段,
// toString 名为 `LootBoxes(rewardsForKeys=…, currentContainerType=…, …)`。默认值取自构造:
// rewardsForKeys = md()(emptyList)、currentContainerType = Ly()(ContainerType.STANDARD,
// 见 module_88693 的 `Vt()` / `Yt()`)、keys / containerNames / containerDescriptions = ag()(emptyMap)、
// rewards / shownRewards = md()、gotoGarage = false、timeToGiveLootBox = new Dd(0, 0)(Long 0)、
// periodOfGiveLootBoxInSec = 0、openingFailed / adsAvailable = false、currentCategory = null、
// 其余布尔 = false。
// reducer:`ld(EH).wrv`(module_10032 第 33150 行)18 个 instanceof 臂:
//
// cH SetPossibleRewards → rewardsForKeys = action.rewards
// lH SetCurrentContainer → currentContainerType = action.type
// fH SetKeys → keys = action.keys
// vH AddKeys → keys[action.keyType] += action.delta(原 `nm` 是 Map.getValue,
// 缺键抛 "Key … is missing in the map.";加法按 32 位 Int 截断)
// pH SetContainerData → containerNames[type] = name、containerDescriptions[type] = description
// (原 `cg(map, Vd(k, v))` 是 Map.plus(Pair))
// wH SetGoToGarage → gotoGarage = action.gotoGarage
// bH SetRewards → rewards = action.rewards,同时 isReopening = false(第 17 个构造参数 !1)
// mH SetTimeToGiveLootBox → timeToGiveLootBox = Date.now() + action.seconds * 1000
// (原 `Fg(Md(), Qk.y2o(Wg(sec)))`:Md()=Date.now()、Wg=Int→Long、
// Qk.y2o=秒→毫秒、Fg=Long 加法)
// kH SetPeriodOfGiveLootBox → periodOfGiveLootBoxInSec = action.periodOfGiveLootBoxInSec
// zH ClearRewards → rewards = emptyList()、shownRewards = emptyList()
// gH LootBoxOpeningFailed → openingFailed = true
// qH SelectCategory → currentCategory = action.containerCategory
// yH ShownRewards → shownRewards += action.rewards(原 `Im(list, list)` 是 List.plus)
// $H SetIsOpening → isOpening = action.isOpening
// xH SetIsAccelerated → isAccelerated = action.isAccelerated
// jH SetIsShopVisited → isShopVisited = action.isShopVisited
// CH SetIsReopening → isReopening = true
// SH SetIsBlockOpenButtons → isBlockOpenButtons = action.isBlockOpenButtons
//
// 无 toString / 不在 action_inventory 的 6 个类按 reducer 效果定名,原名写在注释里;名字同时登记在
// 3DTank/tools/gen_root_wiring.js 的 AnonymousActionNames,重新生成 RootWiring 后显示。
package App.State.Reducers
import App.State.AddKeys
import App.State.ContainerGivenItem
import App.State.ContainerType
import App.State.SelectCategory
import App.State.SetContainerData
import App.State.SetCurrentContainer
import App.State.SetGoToGarage
import App.State.SetIsAccelerated
import App.State.SetIsOpening
import App.State.SetIsShopVisited
import App.State.SetKeys
import App.State.SetPossibleRewards
import App.State.SetRewards
import App.State.ShownRewards
/** 原 `mH`:补给箱倒计时(秒);归约后写到 timeToGiveLootBox = now + seconds * 1000。 */
data class SetTimeToGiveLootBox(val seconds: Int)
/** 原 `kH`:补给箱发放周期(秒)。 */
data class SetPeriodOfGiveLootBox(val periodOfGiveLootBoxInSec: Int)
/** 原 `zH`:无字段;清空 rewards 与 shownRewards。 */
object ClearRewards
/** 原 `gH`:无字段;openingFailed = true。 */
object LootBoxOpeningFailed
/** 原 `CH`:无字段;isReopening = true。 */
object SetIsReopening
/** 原 `SH`:isBlockOpenButtons = action.xrv_1。 */
data class SetIsBlockOpenButtons(val isBlockOpenButtons: Boolean)
/**
* 原 `aH` / `LootBoxes`:`LootBoxes(rewardsForKeys, currentContainerType, keys, containerNames,
* containerDescriptions, rewards, shownRewards, gotoGarage, timeToGiveLootBox, periodOfGiveLootBoxInSec,
* openingFailed, adsAvailable, currentCategory, isOpening, isAccelerated, isShopVisited, isReopening,
* isBlockOpenButtons)`。
*/
data class LootBoxesStateSlice(
/** 本 reducer 只 copy 转发、从不读;原构造默认 md()(emptyList),元素类型没有证据,保留 Any?。 */
val RewardsForKeys: Any? = emptyList<ContainerGivenItem>(),
val CurrentContainerType: ContainerType = ContainerType.STANDARD,
val Keys: Map<String, Int> = emptyMap(),
val ContainerNames: Map<String, String> = emptyMap(),
val ContainerDescriptions: Map<String, String> = emptyMap(),
val Rewards: List<ContainerGivenItem> = emptyList(),
val ShownRewards: List<ContainerGivenItem> = emptyList(),
val GotoGarage: Boolean = false,
val TimeToGiveLootBox: Long = 0L,
val PeriodOfGiveLootBoxInSec: Int = 0,
val OpeningFailed: Boolean = false,
val AdsAvailable: Boolean = false,
val CurrentCategory: ContainerType? = null,
val IsOpening: Boolean = false,
val IsAccelerated: Boolean = false,
val IsShopVisited: Boolean = false,
val IsReopening: Boolean = false,
val IsBlockOpenButtons: Boolean = false,
)
/** 原 `Md()`:`ve(Date.now())`,即当前毫秒(Long)。 */
private fun CurrentTimeMillis(): Long = kotlin.js.Date.now().toLong()
/** 根 reducer 的 lootBoxes 分支(原 `ws.wrv(t, n.wpv_1)`)。 */
fun ReduceLootBoxes(State0: LootBoxesStateSlice, A: Any): LootBoxesStateSlice = when (A) {
is SetPossibleRewards -> State0.copy(RewardsForKeys = A.rewards)
is SetCurrentContainer -> State0.copy(CurrentContainerType = A.type)
is SetKeys -> State0.copy(Keys = A.keys)
is AddKeys -> State0.copy(
Keys = State0.Keys + (A.keyType to (State0.Keys.getValue(A.keyType) + A.delta))
)
is SetContainerData -> State0.copy(
ContainerNames = State0.ContainerNames + (A.type to A.name),
ContainerDescriptions = State0.ContainerDescriptions + (A.type to A.description),
)
is SetGoToGarage -> State0.copy(GotoGarage = A.gotoGarage)
is SetRewards -> State0.copy(Rewards = A.rewards, IsReopening = false)
is SetTimeToGiveLootBox -> State0.copy(
TimeToGiveLootBox = CurrentTimeMillis() + A.seconds.toLong() * 1000L
)
is SetPeriodOfGiveLootBox -> State0.copy(PeriodOfGiveLootBoxInSec = A.periodOfGiveLootBoxInSec)
is ClearRewards -> State0.copy(Rewards = emptyList(), ShownRewards = emptyList())
is LootBoxOpeningFailed -> State0.copy(OpeningFailed = true)
is SelectCategory -> State0.copy(CurrentCategory = A.containerCategory)
is ShownRewards -> State0.copy(ShownRewards = State0.ShownRewards + A.rewards)
is SetIsOpening -> State0.copy(IsOpening = A.isOpening)
is SetIsAccelerated -> State0.copy(IsAccelerated = A.isAccelerated)
is SetIsShopVisited -> State0.copy(IsShopVisited = A.isShopVisited)
is SetIsReopening -> State0.copy(IsReopening = true)
is SetIsBlockOpenButtons -> State0.copy(IsBlockOpenButtons = A.isBlockOpenButtons)
else -> State0
}
@@ -10,8 +10,9 @@
// localBattleUserState → ReduceLocalBattleUser(lE / `(inline@localBattleUserState)` 分支)
// shop → ReduceShop(j4 / `(inline@shop)` 分支)
// screen → ReduceScreen(WK / dY().ks5)
// lootBoxes → ReduceLootBoxes(aH / EH.wrv)
//
// 其余 82 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 81 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -20,6 +21,7 @@ import App.State.Reducers.ReduceHome
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceLocalBattleUser
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceLootBoxes
import App.State.Reducers.ReduceScreen
import App.State.Reducers.ReduceShop
import App.State.Reducers.ReduceTwitch
@@ -34,7 +36,7 @@ object RootReducer {
val Coverage: String get() = "已迁移 ${Migrated.size}/${RootWiring.All.size}"
/**
* 归约一次。未迁移字段原样带过去(`copy` 只改已迁移的三个)。
* 归约一次。未迁移字段原样带过去(`copy` 只改已迁移的字段)。
*/
fun Reduce(State0: RootState, Action: Any): RootState = State0.copy(
InviteState = ReduceInvite(State0.InviteState, Action),
@@ -44,6 +46,7 @@ object RootReducer {
LocalBattleUserState = ReduceLocalBattleUser(State0.LocalBattleUserState, Action),
Shop = ReduceShop(State0.Shop, Action),
Screen = ReduceScreen(State0.Screen, Action),
LootBoxes = ReduceLootBoxes(State0.LootBoxes, Action),
)
}
@@ -8,6 +8,7 @@ import App.State.Reducers.HomeStateSlice
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.LocalBattleUserStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.LootBoxesStateSlice
import App.State.Reducers.ScreenStateSlice
import App.State.Reducers.ShopStateSlice
import App.State.Reducers.TwitchStateSlice
@@ -65,8 +66,8 @@ data class RootState(
val Settings: Any? = null,
/** 原 accountSettings(oet / Lp.mtg)。未迁移:原样透传。 */
val AccountSettings: Any? = null,
/** 原 lootBoxes(aH / ws.wrv)。未迁移:原样透传。 */
val LootBoxes: Any? = null,
/** 原 lootBoxes(aH / ws.wrv)。已迁移。 */
val LootBoxes: LootBoxesStateSlice = LootBoxesStateSlice(),
/** 原 notifications(? / B_.ise)。未迁移:原样透传。 */
val Notifications: Any? = null,
/** 原 friends(KD / NL().cr9)。未迁移:原样透传。 */
@@ -38,7 +38,7 @@ object RootWiring {
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("?Fut", "?Gut")),
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("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("?CH", "?SH", "?gH", "?kH", "?mH", "?zH", "AddKeys", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsOpening", "SetIsShopVisited", "SetKeys", "SetPossibleRewards", "SetRewards", "ShownRewards")),
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")),
SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")),
@@ -106,7 +106,7 @@ object RootWiring {
)
/** 已迁移(由 RootReducer 真正归约)的字段。 */
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen")
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -9,8 +9,8 @@
// ⑥ 动作目录:816 个类与 138 个真实派发名对齐
// ⑦ 回放:真实 toString 样例的类/字段/类型校验
// ⑧ 全目录动作类都能按 KClass 路由
// ⑨~⑰ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen;⑭ 根 reducer 的 89 槽位委派
// ⑨~⑱ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
package App
@@ -144,6 +144,28 @@ import App.State.Reducers.PopScreen
import App.State.Reducers.ReduceScreen
import App.State.Reducers.ResetScreenStackOnNextGoTo
import App.State.Reducers.ScreenStateSlice
import App.State.AddKeys
import App.State.ContainerGivenItem
import App.State.ContainerType
import App.State.SelectCategory
import App.State.SetContainerData
import App.State.SetCurrentContainer
import App.State.SetGoToGarage
import App.State.SetIsAccelerated
import App.State.SetIsOpening
import App.State.SetIsShopVisited
import App.State.SetKeys
import App.State.SetPossibleRewards
import App.State.SetRewards
import App.State.ShownRewards
import App.State.Reducers.ClearRewards
import App.State.Reducers.LootBoxOpeningFailed
import App.State.Reducers.LootBoxesStateSlice
import App.State.Reducers.ReduceLootBoxes
import App.State.Reducers.SetIsBlockOpenButtons
import App.State.Reducers.SetIsReopening
import App.State.Reducers.SetPeriodOfGiveLootBox
import App.State.Reducers.SetTimeToGiveLootBox
private data class AddFrags(val fragsCount: Int)
private data class SetScreen(val screen: String)
@@ -517,7 +539,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 六个已迁移切片一次归约
// ⑭ 根 reducer:89 个槽位的委派表 + 八个已迁移切片一次归约
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -581,13 +603,16 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 七个已迁移字段,其余 82 个待迁",
"根:迁移表 = 八个已迁移字段,其余 81 个待迁",
RootReducer.Migrated ==
setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen") &&
RootWiring.Unmigrated.size == 82
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
"localBattleUserState", "shop", "screen", "lootBoxes",
) &&
RootWiring.Unmigrated.size == 81
)
// 一次 ReduceRoot 里六个切片各归约自己的动作,互不串片
// 一次 ReduceRoot 里八个切片各归约自己的动作,互不串片
val S0 = RootState(User = "user-1", Screen = ScreenStateSlice(Current = NamedScreen("Preloader")))
val S1 = ReduceRoot(S0, ApplyInvite("uid-1"))
Check(
@@ -640,7 +665,7 @@ object StateCheck {
S5.User == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 7/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 8/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -1067,6 +1092,164 @@ object StateCheck {
).all { it in W.Actions }
)
}
// ⑱ 切片 reducer:LootBoxes(原 module_10032 的 aH / EH.wrv,18 个 instanceof 臂)
run {
val Item = { Name: String ->
ContainerGivenItem("INVENTORY", 1, "INVENTORY", 10007277, null, Name, "mine")
}
val S0 = LootBoxesStateSlice()
Check(
"切片 LootBoxes:默认值与原构造一致",
S0.RewardsForKeys == emptyList<ContainerGivenItem>() &&
S0.CurrentContainerType == ContainerType.STANDARD &&
S0.Keys.isEmpty() && S0.ContainerNames.isEmpty() &&
S0.ContainerDescriptions.isEmpty() && S0.Rewards.isEmpty() &&
S0.ShownRewards.isEmpty() && !S0.GotoGarage && S0.TimeToGiveLootBox == 0L &&
S0.PeriodOfGiveLootBoxInSec == 0 && !S0.OpeningFailed && !S0.AdsAvailable &&
S0.CurrentCategory == null && !S0.IsOpening && !S0.IsAccelerated &&
!S0.IsShopVisited && !S0.IsReopening && !S0.IsBlockOpenButtons
)
val Rewards = listOf(Item("mine"))
Check(
"切片 LootBoxes:SetPossibleRewards / SetCurrentContainer 各写本字段",
ReduceLootBoxes(S0, SetPossibleRewards(Rewards)).RewardsForKeys == Rewards &&
ReduceLootBoxes(S0, SetCurrentContainer(ContainerType.EPIC))
.CurrentContainerType == ContainerType.EPIC &&
ReduceLootBoxes(S0, SetCurrentContainer(ContainerType.EPIC))
.CurrentContainerType != ContainerType.RARE
)
val Keyed = ReduceLootBoxes(S0, SetKeys(mapOf("STANDARD" to 3)))
Check(
"切片 LootBoxes:SetKeys 覆盖整张表",
Keyed.Keys == mapOf("STANDARD" to 3) &&
ReduceLootBoxes(S0, SetKeys(emptyMap())).Keys.isEmpty()
)
Check(
"切片 LootBoxes:AddKeys 在原值上加 delta",
ReduceLootBoxes(Keyed, AddKeys("STANDARD", 2)).Keys == mapOf("STANDARD" to 5) &&
ReduceLootBoxes(Keyed, AddKeys("STANDARD", -3)).Keys == mapOf("STANDARD" to 0)
)
Check(
"切片 LootBoxes:AddKeys 缺键抛错(原 nm 是 Map.getValue)",
runCatching { ReduceLootBoxes(Keyed, AddKeys("MISSING", 1)) }.isFailure
)
val Described = ReduceLootBoxes(S0, SetContainerData("STANDARD", "标准箱", "普通补给箱"))
Check(
"切片 LootBoxes:SetContainerData 同时写名字与描述两张表",
Described.ContainerNames == mapOf("STANDARD" to "标准箱") &&
Described.ContainerDescriptions == mapOf("STANDARD" to "普通补给箱") &&
Described.Keys.isEmpty() && Described.Rewards.isEmpty()
)
Check(
"切片 LootBoxes:SetGoToGarage 写 gotoGarage",
ReduceLootBoxes(S0, SetGoToGarage(true)).GotoGarage &&
!ReduceLootBoxes(S0.copy(GotoGarage = true), SetGoToGarage(false)).GotoGarage
)
val Rewarded = ReduceLootBoxes(
S0.copy(IsReopening = true, ShownRewards = listOf(Item("old"))),
SetRewards(Rewards)
)
Check(
"切片 LootBoxes:SetRewards 换奖励并把 isReopening 归 false",
Rewarded.Rewards == Rewards && !Rewarded.IsReopening &&
Rewarded.ShownRewards == listOf(Item("old"))
)
val Before = kotlin.js.Date.now().toLong()
val Timed = ReduceLootBoxes(S0, SetTimeToGiveLootBox(3))
val After = kotlin.js.Date.now().toLong()
Check(
"切片 LootBoxes:SetTimeToGiveLootBox = 当前毫秒 + 秒 * 1000",
Timed.TimeToGiveLootBox >= Before + 3000L && Timed.TimeToGiveLootBox <= After + 3000L
)
Check(
"切片 LootBoxes:SetPeriodOfGiveLootBox 写周期(秒)",
ReduceLootBoxes(S0, SetPeriodOfGiveLootBox(3600)).PeriodOfGiveLootBoxInSec == 3600
)
val Cleared = ReduceLootBoxes(
S0.copy(Rewards = Rewards, ShownRewards = Rewards, OpeningFailed = true),
ClearRewards
)
Check(
"切片 LootBoxes:ClearRewards 只清两个奖励表",
Cleared.Rewards.isEmpty() && Cleared.ShownRewards.isEmpty() && Cleared.OpeningFailed
)
Check(
"切片 LootBoxes:LootBoxOpeningFailed 置 openingFailed",
ReduceLootBoxes(S0, LootBoxOpeningFailed).OpeningFailed
)
Check(
"切片 LootBoxes:SelectCategory 写可空 currentCategory",
ReduceLootBoxes(S0, SelectCategory(ContainerType.RARE)).CurrentCategory == ContainerType.RARE &&
ReduceLootBoxes(S0.copy(CurrentCategory = ContainerType.RARE), SelectCategory(null))
.CurrentCategory == null
)
val Shown = ReduceLootBoxes(S0.copy(ShownRewards = listOf(Item("old"))), ShownRewards(Rewards))
Check(
"切片 LootBoxes:ShownRewards 追加到 shownRewards(原 Im 是 List.plus)",
Shown.ShownRewards == listOf(Item("old"), Item("mine")) && Shown.Rewards.isEmpty()
)
Check(
"切片 LootBoxes:四个布尔动作各写本字段",
!ReduceLootBoxes(S0.copy(IsOpening = true), SetIsOpening(false)).IsOpening &&
ReduceLootBoxes(S0, SetIsOpening(true)).IsOpening &&
ReduceLootBoxes(S0, SetIsAccelerated(true)).IsAccelerated &&
ReduceLootBoxes(S0, SetIsShopVisited(true)).IsShopVisited
)
Check(
"切片 LootBoxes:SetIsReopening 置 true;SetIsBlockOpenButtons 写值",
ReduceLootBoxes(S0, SetIsReopening).IsReopening &&
ReduceLootBoxes(S0, SetIsBlockOpenButtons(true)).IsBlockOpenButtons &&
!ReduceLootBoxes(S0.copy(IsBlockOpenButtons = true), SetIsBlockOpenButtons(false))
.IsBlockOpenButtons
)
Check("切片 LootBoxes:无关动作不改状态", ReduceLootBoxes(S0, AddFrags(1)) == S0)
// 根:lootBoxes 动作只动 lootBoxes
val R0 = RootState()
val R1 = ReduceRoot(R0, SetCurrentContainer(ContainerType.LEGENDARY))
Check(
"根:SetCurrentContainer 只动 lootBoxes",
R1.LootBoxes.CurrentContainerType == ContainerType.LEGENDARY &&
R1.Shop == R0.Shop && R1.Screen == R0.Screen &&
R1.InviteState == R0.InviteState && R1.Home == R0.Home
)
val R2 = ReduceRoot(R1, SetRewards(listOf(Item("root"))))
Check(
"根:SetRewards 只动 lootBoxes,且保留前一次归约",
R2.LootBoxes.Rewards == listOf(Item("root")) &&
R2.LootBoxes.CurrentContainerType == ContainerType.LEGENDARY &&
R2.Shop == R1.Shop
)
Check("根:lootBoxes 迁移后无关动作仍返回等值状态", ReduceRoot(R2, AddFrags(1)) == R2)
val W = RootWiring.ByField("lootBoxes")!!
Check(
"根:lootBoxes 委派到 aH / ws.wrv 且 18 个动作全部具名",
W.SliceClass == "aH" && W.Reducer == "ws.wrv" && W.Actions.size == 18 &&
W.Actions.none { it.startsWith("?") }
)
Check(
"根:lootBoxes 的动作名覆盖 18 个 instanceof 臂",
listOf(
"SetPossibleRewards", "SetCurrentContainer", "SetKeys", "AddKeys",
"SetContainerData", "SetGoToGarage", "SetRewards", "SetTimeToGiveLootBox",
"SetPeriodOfGiveLootBox", "ClearRewards", "LootBoxOpeningFailed",
"SelectCategory", "ShownRewards", "SetIsOpening", "SetIsAccelerated",
"SetIsShopVisited", "SetIsReopening", "SetIsBlockOpenButtons",
).all { it in W.Actions }
)
}
}
val Failed = Results.filter { !it.second }