feat(state): 迁移 user 切片(32 字段 / 28 臂)
This commit is contained in:
1 parent
77a90fdfff
commit
9b3e76d42d
11 files changed
+529
-56
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` 动作名。
|
||||
- 已迁移切片 **12/89**:
|
||||
- 已迁移切片 **13/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)
|
||||
@@ -186,12 +186,20 @@
|
||||
`SetControlsForChina` / `SetRunningInChinaDataCenter` / `SetRunningInChinaPartners` /
|
||||
`SetAuthorizedByChinaSdkToken` / `SetClassic` 各置一个 true,`SetAgeWarnShow` 写入 `isAgeWarnShow`。
|
||||
原 reducer 用 `ur4` 的 `cd` 参数保留其余字段;`authorizedByClassicEarlyAccessToken` 没有写入臂。
|
||||
- `user` → `t8` / `M8().qtd`(`UserReducer.kt`):32 字段,28 个 `instanceof` 臂。
|
||||
根委派是 `M8().qtd(t, n.ypu_1)`,全部通过 `qtb`(`t8.copy`,33 参数 = 32 字段 + 回调)写回,`cd` 表示保留原字段。
|
||||
`id` / `premiumExpirationTime` / `eventPassEndTime` 是 Kotlin/JS `Long`(原构造 `Dd`),
|
||||
`rankBounds` 保留原 `RankBounds` 形状(默认 `UserRankBounds(0,0)`);`disabledWithoutPremiumLocations` 收口为 `Set<Int>`。
|
||||
`SetUserPropertiesAction` 只复制 id/货币/rank/分数/rating/uid/观战权限/normalizedRank/profileUrl,不清空未触及字段;
|
||||
`AddCoins`/`AddRubies` 把 `spent` 收回主余额后清零,`SpendCoins`/`SpendRubies` 同步累加 `spent`。
|
||||
`UpdateRank` 用 `min(rankNames.size, rank)` 重算 `normalizedRank`;`SetUserId(null)` 归零。
|
||||
3 个匿名动作按效果定名:`R8`→`DisableLobbyTutorial`、`v8`→`SetInDebt`、`p8`→`ClearInDebt`。
|
||||
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
|
||||
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
|
||||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 11 个有 reducer 的切片,
|
||||
`launchParams` 与其余 **77 个字段原样透传**;
|
||||
迁移进度用 `RootReducer.Coverage`(`已迁移 12/89`)与 `RootWiring.Unmigrated` 暴露。
|
||||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **294 项全绿**,含 89 槽位完整性、11 个归约切片一次归约互不串片、未迁移字段透传,
|
||||
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 12 个有 reducer 的切片,
|
||||
`launchParams` 与其余 **76 个字段原样透传**;
|
||||
迁移进度用 `RootReducer.Coverage`(`已迁移 13/89`)与 `RootWiring.Unmigrated` 暴露。
|
||||
- 自检:`TankiOffline/tools/RunStateTest.ps1` **318 项全绿**,含 89 槽位完整性、12 个归约切片一次归约互不串片、未迁移字段透传,
|
||||
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
|
||||
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
|
||||
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
|
||||
@@ -201,7 +209,7 @@
|
||||
效果是把 `HomeScreen.shopDiscountNotification` 置 true,对应主菜单商店项的折扣提示);
|
||||
shop 的 17 个匿名类按 reducer 效果定名并登记(`Yut`→`ShopLoaded`、`F5`→`ResetShop` 等);
|
||||
screen 的 9 个、lootBoxes 的 6 个、garage 的 25 个、garagePreview 的 15 个、
|
||||
entrance 的 6 个匿名类同样按效果定名并登记。
|
||||
entrance 的 6 个、user 的 3 个匿名类同样按效果定名并登记。
|
||||
|
||||
### S4 引擎核心
|
||||
- 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-10-23T07:37:58.358Z",
|
||||
"generatedAt": "2026-10-23T08:14:22.248Z",
|
||||
"module": "module_10032.js",
|
||||
"RootState": {
|
||||
"Class": "z6",
|
||||
@@ -18,6 +18,7 @@
|
||||
"garage",
|
||||
"garagePreview",
|
||||
"entrance",
|
||||
"user",
|
||||
"launchParams"
|
||||
],
|
||||
"AnonymousActions": [
|
||||
@@ -495,6 +496,24 @@
|
||||
"Name": "SetClassic",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "isClassic = true(`n.ur4(cd, cd, cd, cd, cd, cd, !0)`)"
|
||||
},
|
||||
{
|
||||
"Local": "R8",
|
||||
"Name": "DisableLobbyTutorial",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "lobbyTutorialEnabled = false(`n.qtb(cd × 29, !1)`)"
|
||||
},
|
||||
{
|
||||
"Local": "v8",
|
||||
"Name": "SetInDebt",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "inDebt = true(`n.qtb(cd × 30, !0)`)"
|
||||
},
|
||||
{
|
||||
"Local": "p8",
|
||||
"Name": "ClearInDebt",
|
||||
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
|
||||
"Effect": "inDebt = false(`n.qtb(cd × 30, !1)`)"
|
||||
}
|
||||
],
|
||||
"Markers": [
|
||||
@@ -537,16 +556,16 @@
|
||||
"Reducer": "M8().qtd",
|
||||
"Delegate": "M8().qtd(t, n.ypu_1)",
|
||||
"Actions": [
|
||||
"?R8",
|
||||
"?p8",
|
||||
"?v8",
|
||||
"AddCoins",
|
||||
"AddRubies",
|
||||
"ClearInDebt",
|
||||
"DisableLobbyTutorial",
|
||||
"SetBatteryLevel",
|
||||
"SetClanInfo",
|
||||
"SetCoins",
|
||||
"SetDisabledWithoutPremiumLocations",
|
||||
"SetHasTemporaryCash",
|
||||
"SetInDebt",
|
||||
"SetPremiumExpirationTime",
|
||||
"SetRankNames",
|
||||
"SetRubies",
|
||||
@@ -566,7 +585,7 @@
|
||||
"UpdateUserRating"
|
||||
],
|
||||
"Default": "new t8()",
|
||||
"Migrated": false
|
||||
"Migrated": true
|
||||
},
|
||||
{
|
||||
"Field": "screen",
|
||||
|
||||
@@ -641,6 +641,21 @@ const FieldTypeOverrides = new Map([
|
||||
['ResetItem3dModelActionCache.itemId', 'Long'],
|
||||
['SetCaptureActions.screenshotAction', '(() -> Unit)?'],
|
||||
['SetCaptureActions.videoAction', '(() -> Unit)?'],
|
||||
// user 切片:按状态字段/copy 调用点收口;y8 在源码里有 `new y8(null)`,userId 必须可空。
|
||||
['SetUserId.userId', 'Long?'],
|
||||
['UpdateRank.rank', 'Int'],
|
||||
['UpdateRank.score', 'Int'],
|
||||
['UpdateUserRating.userRating', 'Int'],
|
||||
['SpendCrystals.crystal', 'Int'],
|
||||
['SpendCoins.coins', 'Int'],
|
||||
['AddCoins.coins', 'Int'],
|
||||
['SpendRubies.rubies', 'Int'],
|
||||
['AddRubies.rubies', 'Int'],
|
||||
['SetHasTemporaryCash.hasTemporaryCash', 'Boolean'],
|
||||
['UpdateUid.uid', 'String'],
|
||||
['UpdateEventPass.eventPassEndTime', 'Long'],
|
||||
['SetPremiumExpirationTime.expirationTime', 'Long'],
|
||||
['SetDisabledWithoutPremiumLocations.locations', 'Collection<Int>'],
|
||||
]);
|
||||
|
||||
const Emitted = [];
|
||||
|
||||
@@ -127,6 +127,11 @@ const AnonymousActionNames = new Map([
|
||||
['oit', 'SetRunningInChinaPartners'],
|
||||
['lit', 'SetAuthorizedByChinaSdkToken'],
|
||||
['fit', 'SetClassic'],
|
||||
// user 切片(t8 / M8().qtd):3 个没有 toString / 不在 action_inventory 的类。
|
||||
// 括号里是原 reducer 的效果;实现见 UserReducer.kt。
|
||||
['R8', 'DisableLobbyTutorial'],
|
||||
['v8', 'SetInDebt'],
|
||||
['p8', 'ClearInDebt'],
|
||||
]);
|
||||
|
||||
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
|
||||
@@ -498,6 +503,7 @@ const Migrated = {
|
||||
garage: { Type: 'GarageStateSlice', Reduce: 'ReduceGarage' },
|
||||
garagePreview: { Type: 'GaragePreviewStateSlice', Reduce: 'ReduceGaragePreview' },
|
||||
entrance: { Type: 'EntranceStateSlice', Reduce: 'ReduceEntrance' },
|
||||
user: { Type: 'UserStateSlice', Reduce: 'ReduceUser' },
|
||||
// 原根 reducer 对这一片没有调用,只有默认值 `ag()`(空 Map);这里仅做类型收口。
|
||||
launchParams: {
|
||||
Type: 'LaunchParamsStateSlice',
|
||||
@@ -627,6 +633,13 @@ const EntranceAnonymousEffects = {
|
||||
fit: 'isClassic = true(`n.ur4(cd, cd, cd, cd, cd, cd, !0)`)',
|
||||
};
|
||||
|
||||
// user 切片(t8 / M8().qtd):没有 toString / 不在 action_inventory 的类,按 reducer 分支效果定名。
|
||||
const UserAnonymousEffects = {
|
||||
R8: 'lobbyTutorialEnabled = false(`n.qtb(cd × 29, !1)`)',
|
||||
v8: 'inDebt = true(`n.qtb(cd × 30, !0)`)',
|
||||
p8: 'inDebt = false(`n.qtb(cd × 30, !1)`)',
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(P, 'names', 'root_wiring.json'),
|
||||
JSON.stringify(
|
||||
@@ -679,6 +692,12 @@ fs.writeFileSync(
|
||||
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
|
||||
Effect,
|
||||
})),
|
||||
...Object.entries(UserAnonymousEffects).map(([Local, Effect]) => ({
|
||||
Local,
|
||||
Name: AnonymousActionNames.get(Local),
|
||||
DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名',
|
||||
Effect,
|
||||
})),
|
||||
],
|
||||
Markers: [
|
||||
{
|
||||
|
||||
@@ -660,27 +660,27 @@ object ActionCatalog {
|
||||
ActionMeta("TemporaryCashSlot", TemporaryCashSlot::class, listOf(ActionFieldMeta("amount", "Any?"), ActionFieldMeta("daysToExpire", "Any?"))),
|
||||
ActionMeta("User", User::class, listOf(ActionFieldMeta("isUserPropertiesLoaded", "Boolean"), ActionFieldMeta("id", "Any?"), ActionFieldMeta("clan", "Any?"), ActionFieldMeta("crystals", "Any?"), ActionFieldMeta("coins", "Any?"), ActionFieldMeta("spentCoins", "Int"), ActionFieldMeta("rubies", "Any?"), ActionFieldMeta("spentRubies", "Int"), ActionFieldMeta("hasTemporaryCash", "Any?"), ActionFieldMeta("rank", "Any?"), ActionFieldMeta("rankBounds", "Any?"), ActionFieldMeta("score", "Any?"), ActionFieldMeta("gearScore", "Any?"), ActionFieldMeta("weaponGS", "Any?"), ActionFieldMeta("hullGS", "Any?"), ActionFieldMeta("droneGS", "Any?"), ActionFieldMeta("bazookaGS", "Any?"), ActionFieldMeta("resistanceGS", "Any?"), ActionFieldMeta("rating", "Any?"), ActionFieldMeta("uid", "Any?"), ActionFieldMeta("clanTag", "Any?"), ActionFieldMeta("premiumExpirationTime", "Any?"), ActionFieldMeta("disabledWithoutPremiumLocations", "Any?"), ActionFieldMeta("hasPremium", "Any?"), ActionFieldMeta("hasSpectatorPermissions", "Any?"), ActionFieldMeta("rankNames", "Any?"), ActionFieldMeta("normalizedRank", "Any?"), ActionFieldMeta("profileUrl", "Any?"), ActionFieldMeta("eventPassEndTime", "Any?"), ActionFieldMeta("lobbyTutorialEnabled", "Boolean"), ActionFieldMeta("inDebt", "Boolean"), ActionFieldMeta("batteryLevel", "Any?"))),
|
||||
ActionMeta("UpdateCrystals", UpdateCrystals::class, listOf(ActionFieldMeta("crystal", "Int"))),
|
||||
ActionMeta("SpendCrystals", SpendCrystals::class, listOf(ActionFieldMeta("crystal", "Any?"))),
|
||||
ActionMeta("AddCoins", AddCoins::class, listOf(ActionFieldMeta("coins", "Any?"))),
|
||||
ActionMeta("SpendCoins", SpendCoins::class, listOf(ActionFieldMeta("coins", "Any?"))),
|
||||
ActionMeta("SpendCrystals", SpendCrystals::class, listOf(ActionFieldMeta("crystal", "Int"))),
|
||||
ActionMeta("AddCoins", AddCoins::class, listOf(ActionFieldMeta("coins", "Int"))),
|
||||
ActionMeta("SpendCoins", SpendCoins::class, listOf(ActionFieldMeta("coins", "Int"))),
|
||||
ActionMeta("SetCoins", SetCoins::class, listOf(ActionFieldMeta("coins", "Int"))),
|
||||
ActionMeta("AddRubies", AddRubies::class, listOf(ActionFieldMeta("rubies", "Any?"))),
|
||||
ActionMeta("SpendRubies", SpendRubies::class, listOf(ActionFieldMeta("rubies", "Any?"))),
|
||||
ActionMeta("AddRubies", AddRubies::class, listOf(ActionFieldMeta("rubies", "Int"))),
|
||||
ActionMeta("SpendRubies", SpendRubies::class, listOf(ActionFieldMeta("rubies", "Int"))),
|
||||
ActionMeta("SetRubies", SetRubies::class, listOf(ActionFieldMeta("rubies", "Int"))),
|
||||
ActionMeta("SetHasTemporaryCash", SetHasTemporaryCash::class, listOf(ActionFieldMeta("hasTemporaryCash", "Any?"))),
|
||||
ActionMeta("SetHasTemporaryCash", SetHasTemporaryCash::class, listOf(ActionFieldMeta("hasTemporaryCash", "Boolean"))),
|
||||
ActionMeta("UpdateGearScore", UpdateGearScore::class, listOf(ActionFieldMeta("gearScore", "Int"), ActionFieldMeta("weaponGS", "Int"), ActionFieldMeta("hullGS", "Int"), ActionFieldMeta("droneGS", "Int"), ActionFieldMeta("bazookaGS", "Int"), ActionFieldMeta("resistanceGS", "Int"))),
|
||||
ActionMeta("UpdateUserRating", UpdateUserRating::class, listOf(ActionFieldMeta("userRating", "Any?"))),
|
||||
ActionMeta("UpdateUid", UpdateUid::class, listOf(ActionFieldMeta("uid", "Any?"))),
|
||||
ActionMeta("UpdateUserRating", UpdateUserRating::class, listOf(ActionFieldMeta("userRating", "Int"))),
|
||||
ActionMeta("UpdateUid", UpdateUid::class, listOf(ActionFieldMeta("uid", "String"))),
|
||||
ActionMeta("UpdateClanTag", UpdateClanTag::class, listOf(ActionFieldMeta("clanTag", "String"))),
|
||||
ActionMeta("UpdateRank", UpdateRank::class, listOf(ActionFieldMeta("rank", "Any?"), ActionFieldMeta("score", "Any?"), ActionFieldMeta("bounds", "Any?"))),
|
||||
ActionMeta("UpdateRank", UpdateRank::class, listOf(ActionFieldMeta("rank", "Int"), ActionFieldMeta("score", "Int"), ActionFieldMeta("bounds", "Any?"))),
|
||||
ActionMeta("SetRankNames", SetRankNames::class, listOf(ActionFieldMeta("rankNames", "List<String>"))),
|
||||
ActionMeta("SetUserId", SetUserId::class, listOf(ActionFieldMeta("userId", "Long"))),
|
||||
ActionMeta("SetUserId", SetUserId::class, listOf(ActionFieldMeta("userId", "Long?"))),
|
||||
ActionMeta("SetUserPropertiesAction", SetUserPropertiesAction::class, listOf(ActionFieldMeta("user", "UserPropertiesCC"))),
|
||||
ActionMeta("UpdatePremium", UpdatePremium::class, listOf(ActionFieldMeta("hasPremium", "Boolean"))),
|
||||
ActionMeta("UpdateEventPass", UpdateEventPass::class, listOf(ActionFieldMeta("eventPassEndTime", "Int"))),
|
||||
ActionMeta("SetPremiumExpirationTime", SetPremiumExpirationTime::class, listOf(ActionFieldMeta("expirationTime", "Any?"))),
|
||||
ActionMeta("UpdateEventPass", UpdateEventPass::class, listOf(ActionFieldMeta("eventPassEndTime", "Long"))),
|
||||
ActionMeta("SetPremiumExpirationTime", SetPremiumExpirationTime::class, listOf(ActionFieldMeta("expirationTime", "Long"))),
|
||||
ActionMeta("SetPremiumRemainingSeconds", SetPremiumRemainingSeconds::class, listOf(ActionFieldMeta("seconds", "Int"))),
|
||||
ActionMeta("SetDisabledWithoutPremiumLocations", SetDisabledWithoutPremiumLocations::class, listOf(ActionFieldMeta("locations", "Any?"))),
|
||||
ActionMeta("SetDisabledWithoutPremiumLocations", SetDisabledWithoutPremiumLocations::class, listOf(ActionFieldMeta("locations", "Collection<Int>"))),
|
||||
ActionMeta("SetClanInfo", SetClanInfo::class, listOf(ActionFieldMeta("clan", "Any?"))),
|
||||
ActionMeta("SetAppAccountToken", SetAppAccountToken::class, listOf(ActionFieldMeta("appAccountToken", "Any?"))),
|
||||
ActionMeta("SetBatteryLevel", SetBatteryLevel::class, listOf(ActionFieldMeta("level", "Any?"))),
|
||||
|
||||
@@ -5,7 +5,7 @@ package App.State
|
||||
|
||||
import Core.Vector3
|
||||
|
||||
// 动作/状态类共 816 个,字段 2150 个(已定类型 572 个)
|
||||
// 动作/状态类共 816 个,字段 2150 个(已定类型 584 个)
|
||||
data class AnnouncementState(
|
||||
val isEnabled: Any?, // 仅 copy 转发(无类型信息)
|
||||
val modelLoaded: Any?, // 仅 copy 转发(无类型信息)
|
||||
@@ -3762,15 +3762,15 @@ data class UpdateCrystals(
|
||||
)
|
||||
|
||||
data class SpendCrystals(
|
||||
val crystal: Any?, // 调用点: u
|
||||
val crystal: Int, // override
|
||||
)
|
||||
|
||||
data class AddCoins(
|
||||
val coins: Any?, // 仅 copy 转发(无类型信息)
|
||||
val coins: Int, // override
|
||||
)
|
||||
|
||||
data class SpendCoins(
|
||||
val coins: Any?, // 调用点: u
|
||||
val coins: Int, // override
|
||||
)
|
||||
|
||||
data class SetCoins(
|
||||
@@ -3778,11 +3778,11 @@ data class SetCoins(
|
||||
)
|
||||
|
||||
data class AddRubies(
|
||||
val rubies: Any?, // 仅 copy 转发(无类型信息)
|
||||
val rubies: Int, // override
|
||||
)
|
||||
|
||||
data class SpendRubies(
|
||||
val rubies: Any?, // 调用点: u
|
||||
val rubies: Int, // override
|
||||
)
|
||||
|
||||
data class SetRubies(
|
||||
@@ -3790,7 +3790,7 @@ data class SetRubies(
|
||||
)
|
||||
|
||||
data class SetHasTemporaryCash(
|
||||
val hasTemporaryCash: Any?, // 仅 copy 转发(无类型信息)
|
||||
val hasTemporaryCash: Boolean, // override
|
||||
)
|
||||
|
||||
data class UpdateGearScore(
|
||||
@@ -3803,11 +3803,11 @@ data class UpdateGearScore(
|
||||
)
|
||||
|
||||
data class UpdateUserRating(
|
||||
val userRating: Any?, // 仅 copy 转发(无类型信息)
|
||||
val userRating: Int, // override
|
||||
)
|
||||
|
||||
data class UpdateUid(
|
||||
val uid: Any?, // 仅 copy 转发(无类型信息)
|
||||
val uid: String, // override
|
||||
)
|
||||
|
||||
data class UpdateClanTag(
|
||||
@@ -3815,8 +3815,8 @@ data class UpdateClanTag(
|
||||
)
|
||||
|
||||
data class UpdateRank(
|
||||
val rank: Any?, // 调用点: t
|
||||
val score: Any?, // 调用点: n
|
||||
val rank: Int, // override
|
||||
val score: Int, // override
|
||||
val bounds: Any?, // 调用点: i
|
||||
)
|
||||
|
||||
@@ -3825,7 +3825,7 @@ data class SetRankNames(
|
||||
)
|
||||
|
||||
data class SetUserId(
|
||||
val userId: Long, // runtime
|
||||
val userId: Long?, // override
|
||||
)
|
||||
|
||||
data class SetUserPropertiesAction(
|
||||
@@ -3837,11 +3837,11 @@ data class UpdatePremium(
|
||||
)
|
||||
|
||||
data class UpdateEventPass(
|
||||
val eventPassEndTime: Int, // runtime
|
||||
val eventPassEndTime: Long, // override
|
||||
)
|
||||
|
||||
data class SetPremiumExpirationTime(
|
||||
val expirationTime: Any?, // 调用点: u
|
||||
val expirationTime: Long, // override
|
||||
)
|
||||
|
||||
data class SetPremiumRemainingSeconds(
|
||||
@@ -3849,7 +3849,7 @@ data class SetPremiumRemainingSeconds(
|
||||
)
|
||||
|
||||
data class SetDisabledWithoutPremiumLocations(
|
||||
val locations: Any?, // 调用点: this.q7k().pdh()
|
||||
val locations: Collection<Int>, // override
|
||||
)
|
||||
|
||||
data class SetClanInfo(
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
// UserReducer.kt —— 用户切片(对应 module_10032 的 t8 状态 + N8.qtd reducer)
|
||||
//
|
||||
// 证据链:
|
||||
// 状态类 t8:`function t8(t, n, …, T)`,32 个字段,toString 名为
|
||||
// `User(isUserPropertiesLoaded=…, …, batteryLevel=…)`。
|
||||
// 根委派:`M8().qtd(t, n.ypu_1)`;qtd 有 28 个 instanceof 臂 + 默认透传,
|
||||
// 全部通过 `qtb`(t8.copy,33 参数 = 32 字段 + 回调)写回,`cd` 表示保留原字段。
|
||||
//
|
||||
// 字段默认值(构造):
|
||||
// isUserPropertiesLoaded=false,id=Long(0,0),clan=null,
|
||||
// crystals/coins/spentCoins/rubies/spentRubies=0,hasTemporaryCash=false,
|
||||
// rank=1,rankBounds=RankBounds(0,0),score/gearScore/weaponGS/hullGS/droneGS/
|
||||
// bazookaGS/resistanceGS=0,rating=0,uid="",clanTag=null,
|
||||
// premiumExpirationTime=Long(0,0),disabledWithoutPremiumLocations=HashSet(),
|
||||
// hasPremium=false,hasSpectatorPermissions=false,rankNames=[],normalizedRank=1,
|
||||
// profileUrl="",eventPassEndTime=Long(0,0),lobbyTutorialEnabled=true,
|
||||
// inDebt=false,batteryLevel=null。
|
||||
//
|
||||
// 3 个匿名动作没有 toString / 不在 action_inventory,按 reducer 效果定名:
|
||||
// R8 → DisableLobbyTutorial(lobbyTutorialEnabled = false)
|
||||
// v8 → SetInDebt(inDebt = true)
|
||||
// p8 → ClearInDebt(inDebt = false)
|
||||
package App.State.Reducers
|
||||
|
||||
import App.State.AddCoins
|
||||
import App.State.AddRubies
|
||||
import App.State.SetBatteryLevel
|
||||
import App.State.SetClanInfo
|
||||
import App.State.SetCoins
|
||||
import App.State.SetDisabledWithoutPremiumLocations
|
||||
import App.State.SetHasTemporaryCash
|
||||
import App.State.SetPremiumExpirationTime
|
||||
import App.State.SetRankNames
|
||||
import App.State.SetRubies
|
||||
import App.State.SetUserPropertiesAction
|
||||
import App.State.SetUserId
|
||||
import App.State.SpendCoins
|
||||
import App.State.SpendCrystals
|
||||
import App.State.SpendRubies
|
||||
import App.State.UpdateClanTag
|
||||
import App.State.UpdateCrystals
|
||||
import App.State.UpdateEventPass
|
||||
import App.State.UpdateGearScore
|
||||
import App.State.UpdatePremium
|
||||
import App.State.UpdateRank
|
||||
import App.State.UpdateScore
|
||||
import App.State.UpdateUid
|
||||
import App.State.UpdateUserRating
|
||||
|
||||
/* ------------------------------------------------------------------ 匿名动作 */
|
||||
|
||||
class DisableLobbyTutorial
|
||||
class SetInDebt
|
||||
class ClearInDebt
|
||||
|
||||
/* ------------------------------------------------------------------ 状态 */
|
||||
|
||||
/** 原 `Nx(0,0)`:`RankBounds [lowBound = 0 topBound = 0]`。 */
|
||||
data class UserRankBounds(val LowBound: Int = 0, val TopBound: Int = 0)
|
||||
|
||||
data class UserStateSlice(
|
||||
val IsUserPropertiesLoaded: Boolean = false,
|
||||
val Id: Long = 0L,
|
||||
val Clan: Any? = null,
|
||||
val Crystals: Int = 0,
|
||||
val Coins: Int = 0,
|
||||
val SpentCoins: Int = 0,
|
||||
val Rubies: Int = 0,
|
||||
val SpentRubies: Int = 0,
|
||||
val HasTemporaryCash: Boolean = false,
|
||||
val Rank: Int = 1,
|
||||
/** 原 Nx(0,0),具体模型未在 10032 内展开。 */
|
||||
val RankBounds: Any? = UserRankBounds(),
|
||||
val Score: Int = 0,
|
||||
val GearScore: Int = 0,
|
||||
val WeaponGS: Int = 0,
|
||||
val HullGS: Int = 0,
|
||||
val DroneGS: Int = 0,
|
||||
val BazookaGS: Int = 0,
|
||||
val ResistanceGS: Int = 0,
|
||||
val Rating: Int = 0,
|
||||
val Uid: String = "",
|
||||
val ClanTag: String? = null,
|
||||
/** 原 Dd(0,0),Dd 是 Kotlin/JS 的 Long。 */
|
||||
val PremiumExpirationTime: Long = 0L,
|
||||
/** 原 Yg() = HashSet,元素为地点 id。 */
|
||||
val DisabledWithoutPremiumLocations: Set<Int> = emptySet(),
|
||||
val HasPremium: Boolean = false,
|
||||
val HasSpectatorPermissions: Boolean = false,
|
||||
val RankNames: List<String> = emptyList(),
|
||||
val NormalizedRank: Int = 1,
|
||||
val ProfileUrl: String = "",
|
||||
/** 原 Dd(0,0)。 */
|
||||
val EventPassEndTime: Long = 0L,
|
||||
val LobbyTutorialEnabled: Boolean = true,
|
||||
val InDebt: Boolean = false,
|
||||
val BatteryLevel: Any? = null,
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------ reducer */
|
||||
|
||||
fun ReduceUser(State0: UserStateSlice, Action: Any): UserStateSlice = when (Action) {
|
||||
is SetClanInfo -> State0.copy(Clan = Action.clan)
|
||||
|
||||
is UpdateCrystals -> State0.copy(Crystals = Action.crystal)
|
||||
is SpendCrystals -> State0.copy(Crystals = State0.Crystals - Action.crystal)
|
||||
|
||||
is AddCoins -> State0.copy(
|
||||
Coins = State0.Coins + State0.SpentCoins + Action.coins,
|
||||
SpentCoins = 0,
|
||||
)
|
||||
is SetCoins -> State0.copy(Coins = Action.coins)
|
||||
is SpendCoins -> State0.copy(
|
||||
Coins = State0.Coins - Action.coins,
|
||||
SpentCoins = State0.SpentCoins + Action.coins,
|
||||
)
|
||||
|
||||
is SetHasTemporaryCash -> State0.copy(HasTemporaryCash = Action.hasTemporaryCash)
|
||||
|
||||
is AddRubies -> State0.copy(
|
||||
Rubies = State0.Rubies + State0.SpentRubies + Action.rubies,
|
||||
SpentRubies = 0,
|
||||
)
|
||||
is SetRubies -> State0.copy(Rubies = Action.rubies)
|
||||
is SpendRubies -> State0.copy(
|
||||
Rubies = State0.Rubies - Action.rubies,
|
||||
SpentRubies = State0.SpentRubies + Action.rubies,
|
||||
)
|
||||
|
||||
is UpdateUserRating -> State0.copy(Rating = Action.userRating)
|
||||
is UpdateUid -> State0.copy(Uid = Action.uid)
|
||||
is UpdateClanTag -> State0.copy(ClanTag = Action.clanTag)
|
||||
is UpdateScore -> State0.copy(Score = Action.score)
|
||||
|
||||
is UpdateGearScore -> State0.copy(
|
||||
GearScore = Action.gearScore,
|
||||
WeaponGS = Action.weaponGS,
|
||||
HullGS = Action.hullGS,
|
||||
DroneGS = Action.droneGS,
|
||||
BazookaGS = Action.bazookaGS,
|
||||
ResistanceGS = Action.resistanceGS,
|
||||
)
|
||||
|
||||
is UpdateRank -> State0.copy(
|
||||
Rank = Action.rank,
|
||||
RankBounds = Action.bounds,
|
||||
Score = Action.score,
|
||||
NormalizedRank = minOf(State0.RankNames.size, Action.rank),
|
||||
)
|
||||
|
||||
// SetUserPropertiesAction:只覆盖 id/货币/rank/分数/rating/uid/观战权限/normalizedRank/profileUrl。
|
||||
is SetUserPropertiesAction -> {
|
||||
val User = Action.user
|
||||
State0.copy(
|
||||
IsUserPropertiesLoaded = true,
|
||||
Id = User.id,
|
||||
Crystals = User.crystals,
|
||||
Rank = User.rank,
|
||||
RankBounds = User.rankBounds,
|
||||
Score = User.score,
|
||||
GearScore = User.gearScore,
|
||||
WeaponGS = User.weaponGearScore,
|
||||
HullGS = User.hullGearScore,
|
||||
DroneGS = User.droneGearScore,
|
||||
BazookaGS = User.bazookaGearScore,
|
||||
ResistanceGS = User.resistanceGearScore,
|
||||
Rating = User.userRating,
|
||||
Uid = User.uid,
|
||||
HasSpectatorPermissions = User.hasSpectatorPermissions,
|
||||
NormalizedRank = minOf(State0.RankNames.size, User.rank),
|
||||
ProfileUrl = User.userProfileUrl,
|
||||
)
|
||||
}
|
||||
|
||||
is SetPremiumExpirationTime -> State0.copy(PremiumExpirationTime = Action.expirationTime)
|
||||
is SetDisabledWithoutPremiumLocations ->
|
||||
State0.copy(DisabledWithoutPremiumLocations = Action.locations.toSet())
|
||||
|
||||
is UpdatePremium -> State0.copy(HasPremium = Action.hasPremium)
|
||||
is UpdateEventPass -> State0.copy(EventPassEndTime = Action.eventPassEndTime)
|
||||
is SetRankNames -> State0.copy(RankNames = Action.rankNames)
|
||||
is SetUserId -> State0.copy(Id = Action.userId ?: 0L)
|
||||
|
||||
is DisableLobbyTutorial -> State0.copy(LobbyTutorialEnabled = false)
|
||||
is SetBatteryLevel -> State0.copy(BatteryLevel = Action.level)
|
||||
is SetInDebt -> State0.copy(InDebt = true)
|
||||
is ClearInDebt -> State0.copy(InDebt = false)
|
||||
|
||||
else -> State0
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
|
||||
//
|
||||
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
|
||||
// 这里采用同一形状。已迁移的 12 个切片里,11 个真正调用 reducer,launchParams 只做类型收口:
|
||||
// 这里采用同一形状。已迁移的 13 个切片里,12 个真正调用 reducer,launchParams 只做类型收口:
|
||||
//
|
||||
// inviteState → ReduceInvite(aD / pD.er5)
|
||||
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
|
||||
@@ -14,9 +14,10 @@
|
||||
// garage → ReduceGarage(UB / hF,根委派 `i = hF(t, n)`)
|
||||
// garagePreview → ReduceGaragePreview(IG / fs.eru,根委派 `fs.eru(t, n.epv_1)`)
|
||||
// entrance → ReduceEntrance(hD / Bv.pth,根委派 `Bv.pth(t, n.jpx_1)`)
|
||||
// user → ReduceUser(t8 / M8().qtd,根委派 `M8().qtd(t, n.ypu_1)`)
|
||||
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
|
||||
//
|
||||
// 其余 77 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
|
||||
// 其余 76 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
|
||||
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
|
||||
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
|
||||
package App.State
|
||||
@@ -32,6 +33,7 @@ import App.State.Reducers.ReduceLootBoxes
|
||||
import App.State.Reducers.ReduceScreen
|
||||
import App.State.Reducers.ReduceShop
|
||||
import App.State.Reducers.ReduceTwitch
|
||||
import App.State.Reducers.ReduceUser
|
||||
|
||||
/** 根 reducer:`k6`。 */
|
||||
object RootReducer {
|
||||
@@ -57,6 +59,7 @@ object RootReducer {
|
||||
Garage = ReduceGarage(State0.Garage, Action),
|
||||
GaragePreview = ReduceGaragePreview(State0.GaragePreview, Action),
|
||||
Entrance = ReduceEntrance(State0.Entrance, Action),
|
||||
User = ReduceUser(State0.User, Action),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,15 @@ import App.State.Reducers.LootBoxesStateSlice
|
||||
import App.State.Reducers.ScreenStateSlice
|
||||
import App.State.Reducers.ShopStateSlice
|
||||
import App.State.Reducers.TwitchStateSlice
|
||||
import App.State.Reducers.UserStateSlice
|
||||
|
||||
/**
|
||||
* 原 `z6` / `TOState`:89 个切片字段,顺序与 toString 一致。
|
||||
* 未迁移字段默认 null —— 原包构造时会给它们 `new <Slice>()`,这里等对应切片迁移后再补默认值。
|
||||
*/
|
||||
data class RootState(
|
||||
/** 原 user(t8 / M8().qtd)。未迁移:原样透传。 */
|
||||
val User: Any? = null,
|
||||
/** 原 user(t8 / M8().qtd)。已迁移。 */
|
||||
val User: UserStateSlice = UserStateSlice(),
|
||||
/** 原 screen(WK / dY().ks5)。已迁移。 */
|
||||
val Screen: ScreenStateSlice = ScreenStateSlice(),
|
||||
/** 原 modalScreen(nV / Fs.js1)。未迁移:原样透传。 */
|
||||
|
||||
@@ -14,7 +14,7 @@ data class SliceWiring(
|
||||
|
||||
object RootWiring {
|
||||
val All: List<SliceWiring> = listOf(
|
||||
SliceWiring("user", "ypu_1", "t8", "M8().qtd", "M8().qtd(t, n.ypu_1)", listOf("?R8", "?p8", "?v8", "AddCoins", "AddRubies", "SetBatteryLevel", "SetClanInfo", "SetCoins", "SetDisabledWithoutPremiumLocations", "SetHasTemporaryCash", "SetPremiumExpirationTime", "SetRankNames", "SetRubies", "SetUserId", "SetUserPropertiesAction", "SpendCoins", "SpendCrystals", "SpendRubies", "UpdateClanTag", "UpdateCrystals", "UpdateEventPass", "UpdateGearScore", "UpdatePremium", "UpdateRank", "UpdateScore", "UpdateUid", "UpdateUserRating")),
|
||||
SliceWiring("user", "ypu_1", "t8", "M8().qtd", "M8().qtd(t, n.ypu_1)", listOf("AddCoins", "AddRubies", "ClearInDebt", "DisableLobbyTutorial", "SetBatteryLevel", "SetClanInfo", "SetCoins", "SetDisabledWithoutPremiumLocations", "SetHasTemporaryCash", "SetInDebt", "SetPremiumExpirationTime", "SetRankNames", "SetRubies", "SetUserId", "SetUserPropertiesAction", "SpendCoins", "SpendCrystals", "SpendRubies", "UpdateClanTag", "UpdateCrystals", "UpdateEventPass", "UpdateGearScore", "UpdatePremium", "UpdateRank", "UpdateScore", "UpdateUid", "UpdateUserRating")),
|
||||
SliceWiring("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("ClearScreenStack", "DelayNextGoTo", "DisableBack", "DisableForeground", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "EnableBack", "EnableForeground", "FilterScreenStack", "GoTo", "PopScreen", "PressedAnyButton", "ResetScreenStackOnNextGoTo")),
|
||||
SliceWiring("modalScreen", "apv_1", "nV", "Fs.js1", "Fs.js1(t, n.apv_1)", listOf("?XW", "AddModal", "RemoveCurrentByName", "RemoveModal")),
|
||||
SliceWiring("battleStatistics", "bpv_1", "jR", "rr.fqr", "rr.fqr(t, n.bpv_1)", listOf("?DR", "?HR", "?KR", "?PR", "?QR", "?UR", "?XR", "?YR", "?ZR", "?nP", "?tP", "Init", "InitBattleModeWithLevels", "SetBattleMode", "SetDisplayClientInfo", "SetFlagOrBallOwner", "SetRankRange", "SetReArmor", "SetRemainingTime", "SetRoundRestartTime", "UpdateFund", "UpdateScore")),
|
||||
@@ -106,7 +106,7 @@ object RootWiring {
|
||||
)
|
||||
|
||||
/** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */
|
||||
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "launchParams")
|
||||
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "launchParams")
|
||||
|
||||
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
|
||||
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 / LootBoxes / Garage / GaragePreview / Entrance;
|
||||
// ⑨~㉓ 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
|
||||
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User;
|
||||
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
|
||||
//
|
||||
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
|
||||
@@ -88,6 +88,31 @@ import App.State.ShopCardSold
|
||||
import App.State.ShopItem
|
||||
import App.State.ToggleShopActivity
|
||||
import App.State.TryToBuyOneTimePurchase
|
||||
import App.State.AddCoins
|
||||
import App.State.AddRubies
|
||||
import App.State.SetBatteryLevel
|
||||
import App.State.SetClanInfo
|
||||
import App.State.SetCoins
|
||||
import App.State.SetDisabledWithoutPremiumLocations
|
||||
import App.State.SetHasTemporaryCash
|
||||
import App.State.SetPremiumExpirationTime
|
||||
import App.State.SetRankNames
|
||||
import App.State.SetRubies
|
||||
import App.State.SetUserPropertiesAction
|
||||
import App.State.SetUserId
|
||||
import App.State.SpendCoins
|
||||
import App.State.SpendCrystals
|
||||
import App.State.SpendRubies
|
||||
import App.State.UpdateClanTag
|
||||
import App.State.UpdateCrystals
|
||||
import App.State.UpdateEventPass
|
||||
import App.State.UpdateGearScore
|
||||
import App.State.UpdatePremium
|
||||
import App.State.UpdateRank
|
||||
import App.State.UpdateScore
|
||||
import App.State.UpdateUid
|
||||
import App.State.UpdateUserRating
|
||||
import App.State.UserPropertiesCC
|
||||
import App.State.Reducers.AddCategoriesWithBonus
|
||||
import App.State.Reducers.BlockBackButton
|
||||
import App.State.Reducers.CategoryType
|
||||
@@ -237,6 +262,12 @@ import App.State.Reducers.ClearDroneData
|
||||
import App.State.Reducers.ClearGaragePreviewParams
|
||||
import App.State.Reducers.ClearGrenadeData
|
||||
import App.State.Reducers.ClearHasGrenades
|
||||
import App.State.Reducers.ClearInDebt
|
||||
import App.State.Reducers.DisableLobbyTutorial
|
||||
import App.State.Reducers.ReduceUser
|
||||
import App.State.Reducers.SetInDebt
|
||||
import App.State.Reducers.UserRankBounds
|
||||
import App.State.Reducers.UserStateSlice
|
||||
import App.State.Reducers.ClearHullData
|
||||
import App.State.Reducers.ClearUpgradeItemInProcessing
|
||||
import App.State.Reducers.ClearWaitServerItemMount
|
||||
@@ -662,7 +693,7 @@ object StateCheck {
|
||||
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
|
||||
}
|
||||
|
||||
// ⑭ 根 reducer:89 个槽位的委派表 + 12 个已迁移切片(11 归约 + launchParams 透传)
|
||||
// ⑭ 根 reducer:89 个槽位的委派表 + 13 个已迁移切片(12 归约 + launchParams 透传)
|
||||
run {
|
||||
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
|
||||
Check(
|
||||
@@ -726,18 +757,18 @@ object StateCheck {
|
||||
LocalBattleW.Reducer == "(inline@localBattleUserState)"
|
||||
)
|
||||
Check(
|
||||
"根:迁移表 = 12 个已迁移字段,其余 77 个待迁",
|
||||
"根:迁移表 = 13 个已迁移字段,其余 76 个待迁",
|
||||
RootReducer.Migrated ==
|
||||
setOf(
|
||||
"inviteState", "loginByPassword", "twitchState", "home",
|
||||
"localBattleUserState", "shop", "screen", "lootBoxes", "garage",
|
||||
"garagePreview", "entrance", "launchParams",
|
||||
"garagePreview", "entrance", "user", "launchParams",
|
||||
) &&
|
||||
RootWiring.Unmigrated.size == 77
|
||||
RootWiring.Unmigrated.size == 76
|
||||
)
|
||||
|
||||
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
|
||||
val S0 = RootState(User = "user-1", Screen = ScreenStateSlice(Current = NamedScreen("Preloader")))
|
||||
val S0 = RootState(User = UserStateSlice(Uid = "user-1"), Screen = ScreenStateSlice(Current = NamedScreen("Preloader")))
|
||||
val S1 = ReduceRoot(S0, ApplyInvite("uid-1"))
|
||||
Check(
|
||||
"根:ApplyInvite 只动 inviteState",
|
||||
@@ -786,10 +817,10 @@ object StateCheck {
|
||||
// 未迁移字段原样透传;无关动作不改变任何字段
|
||||
Check(
|
||||
"根:未迁移字段原样透传",
|
||||
S5.User == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
|
||||
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
|
||||
)
|
||||
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
|
||||
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 12/89")
|
||||
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 13/89")
|
||||
|
||||
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
|
||||
Store.Dispatch(SetStatus("LOGGING_IN"))
|
||||
@@ -1042,7 +1073,7 @@ object StateCheck {
|
||||
val R1 = ReduceRoot(R0, InitShopItem(1L, Item))
|
||||
Check(
|
||||
"根:InitShopItem 只动 shop",
|
||||
R1.Shop.ShopItems.Items[1L] == Item && R1.User == null && R1.Home == R0.Home
|
||||
R1.Shop.ShopItems.Items[1L] == Item && R1.User == R0.User && R1.Home == R0.Home
|
||||
)
|
||||
val R2 = ReduceRoot(R1, InitCategory(C))
|
||||
Check(
|
||||
@@ -1189,7 +1220,7 @@ object StateCheck {
|
||||
val R1 = ReduceRoot(R0, GoTo(Shop, false))
|
||||
Check(
|
||||
"根:GoTo 只动 screen",
|
||||
R1.Screen.Current == Shop && R1.User == null && R1.Shop == R0.Shop &&
|
||||
R1.Screen.Current == Shop && R1.User == R0.User && R1.Shop == R0.Shop &&
|
||||
R1.InviteState == R0.InviteState && R1.Home == R0.Home
|
||||
)
|
||||
val R2 = ReduceRoot(R1, EnableForeground)
|
||||
@@ -2425,6 +2456,193 @@ object StateCheck {
|
||||
)
|
||||
}
|
||||
|
||||
// ㉓ 切片 reducer:User(原 module_10032 的 t8 / M8().qtd,32 字段;28 个 instanceof 臂)
|
||||
run {
|
||||
val S0 = UserStateSlice()
|
||||
Check(
|
||||
"切片 User:默认值与原构造一致",
|
||||
!S0.IsUserPropertiesLoaded && S0.Id == 0L && S0.Clan == null &&
|
||||
S0.Crystals == 0 && S0.Coins == 0 && S0.SpentCoins == 0 &&
|
||||
S0.Rubies == 0 && S0.SpentRubies == 0 && !S0.HasTemporaryCash &&
|
||||
S0.Rank == 1 && S0.RankBounds == UserRankBounds(0, 0) &&
|
||||
S0.Score == 0 && S0.GearScore == 0 && S0.WeaponGS == 0 &&
|
||||
S0.HullGS == 0 && S0.DroneGS == 0 && S0.BazookaGS == 0 &&
|
||||
S0.ResistanceGS == 0 && S0.Rating == 0 && S0.Uid == "" &&
|
||||
S0.ClanTag == null && S0.PremiumExpirationTime == 0L &&
|
||||
S0.DisabledWithoutPremiumLocations.isEmpty() && !S0.HasPremium &&
|
||||
!S0.HasSpectatorPermissions && S0.RankNames.isEmpty() &&
|
||||
S0.NormalizedRank == 1 && S0.ProfileUrl == "" &&
|
||||
S0.EventPassEndTime == 0L && S0.LobbyTutorialEnabled &&
|
||||
!S0.InDebt && S0.BatteryLevel == null
|
||||
)
|
||||
|
||||
Check(
|
||||
"切片 User:SetClanInfo 写 clan",
|
||||
ReduceUser(S0, SetClanInfo("clan-1")).Clan == "clan-1"
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdateCrystals / SpendCrystals 写 crystal",
|
||||
ReduceUser(S0, UpdateCrystals(50)).Crystals == 50 &&
|
||||
ReduceUser(S0.copy(Crystals = 50), SpendCrystals(20)).Crystals == 30
|
||||
)
|
||||
|
||||
val Coins = ReduceUser(
|
||||
ReduceUser(ReduceUser(S0, SetCoins(100)), SpendCoins(30)),
|
||||
AddCoins(10),
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetCoins / SpendCoins / AddCoins 维护 coins 与 spentCoins",
|
||||
Coins.Coins == 110 && Coins.SpentCoins == 0
|
||||
)
|
||||
|
||||
val Rubies = ReduceUser(
|
||||
ReduceUser(ReduceUser(S0, SetRubies(80)), SpendRubies(20)),
|
||||
AddRubies(5),
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetRubies / SpendRubies / AddRubies 维护 rubies 与 spentRubies",
|
||||
Rubies.Rubies == 85 && Rubies.SpentRubies == 0
|
||||
)
|
||||
|
||||
Check(
|
||||
"切片 User:SetHasTemporaryCash 写 hasTemporaryCash",
|
||||
ReduceUser(S0, SetHasTemporaryCash(true)).HasTemporaryCash
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdateUserRating 写 rating",
|
||||
ReduceUser(S0, UpdateUserRating(12)).Rating == 12
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdateUid / UpdateClanTag 写 uid / clanTag",
|
||||
ReduceUser(S0, UpdateUid("uid-42")).Uid == "uid-42" &&
|
||||
ReduceUser(S0, UpdateClanTag("TAG")).ClanTag == "TAG"
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdateScore 写 score",
|
||||
ReduceUser(S0, UpdateScore(team = "red", score = 123)).Score == 123
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdateGearScore 写六个装备分",
|
||||
ReduceUser(S0, UpdateGearScore(1, 2, 3, 4, 5, 6)).let {
|
||||
it.GearScore == 1 && it.WeaponGS == 2 && it.HullGS == 3 &&
|
||||
it.DroneGS == 4 && it.BazookaGS == 5 && it.ResistanceGS == 6
|
||||
}
|
||||
)
|
||||
|
||||
val Rank = ReduceUser(
|
||||
S0.copy(RankNames = listOf("A", "B", "C")),
|
||||
UpdateRank(rank = 5, score = 500, bounds = UserRankBounds(1, 10)),
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdateRank 写 rank/rankBounds/score 并重算 normalizedRank",
|
||||
Rank.Rank == 5 && Rank.Score == 500 &&
|
||||
Rank.RankBounds == UserRankBounds(1, 10) && Rank.NormalizedRank == 3
|
||||
)
|
||||
|
||||
val Props = UserPropertiesCC(
|
||||
bazookaGearScore = 6,
|
||||
canUseGroup = true,
|
||||
crystals = 321,
|
||||
crystalsRating = 0,
|
||||
droneGearScore = 5,
|
||||
eventPassEndTime = 0,
|
||||
gearScore = 21,
|
||||
goldsTakenRating = 0,
|
||||
hasSpectatorPermissions = true,
|
||||
hullGearScore = 4,
|
||||
id = 99L,
|
||||
partnerUid = null,
|
||||
rank = 7,
|
||||
rankBounds = "1-10",
|
||||
topBound = "10",
|
||||
registrationTimestamp = 0,
|
||||
resistanceGearScore = 3,
|
||||
score = 777,
|
||||
scoreRating = 0,
|
||||
uid = "uid-99",
|
||||
userProfileUrl = "https://example.test/u/99",
|
||||
userRating = 11,
|
||||
weaponGearScore = 2,
|
||||
)
|
||||
val SP = ReduceUser(
|
||||
S0.copy(RankNames = listOf("A", "B", "C")),
|
||||
SetUserPropertiesAction(Props),
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetUserPropertiesAction 复制服务端字段并置 isUserPropertiesLoaded",
|
||||
SP.IsUserPropertiesLoaded && SP.Id == 99L && SP.Crystals == 321 &&
|
||||
SP.Rank == 7 && SP.RankBounds == "1-10" && SP.Score == 777 &&
|
||||
SP.GearScore == 21 && SP.WeaponGS == 2 && SP.HullGS == 4 &&
|
||||
SP.DroneGS == 5 && SP.BazookaGS == 6 && SP.ResistanceGS == 3 &&
|
||||
SP.Rating == 11 && SP.Uid == "uid-99" &&
|
||||
SP.HasSpectatorPermissions && SP.NormalizedRank == 3 &&
|
||||
SP.ProfileUrl == "https://example.test/u/99"
|
||||
)
|
||||
|
||||
Check(
|
||||
"切片 User:SetPremiumExpirationTime / UpdateEventPass 写时间戳",
|
||||
ReduceUser(S0, SetPremiumExpirationTime(1_700_000_000_000L))
|
||||
.PremiumExpirationTime == 1_700_000_000_000L &&
|
||||
ReduceUser(S0, UpdateEventPass(1_700_000_000_001L))
|
||||
.EventPassEndTime == 1_700_000_000_001L
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetDisabledWithoutPremiumLocations 去重成 Set",
|
||||
ReduceUser(S0, SetDisabledWithoutPremiumLocations(listOf(1, 2, 2)))
|
||||
.DisabledWithoutPremiumLocations == setOf(1, 2)
|
||||
)
|
||||
Check(
|
||||
"切片 User:UpdatePremium / UpdateEventPass 写对应字段",
|
||||
ReduceUser(S0, UpdatePremium(true)).HasPremium
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetRankNames 只写 rankNames",
|
||||
ReduceUser(S0.copy(NormalizedRank = 9), SetRankNames(listOf("A", "B")))
|
||||
.let { it.RankNames == listOf("A", "B") && it.NormalizedRank == 9 }
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetUserId 写 id,null 归零",
|
||||
ReduceUser(S0, SetUserId(42L)).Id == 42L &&
|
||||
ReduceUser(S0, SetUserId(null)).Id == 0L
|
||||
)
|
||||
Check(
|
||||
"切片 User:DisableLobbyTutorial / SetBatteryLevel",
|
||||
!ReduceUser(S0, DisableLobbyTutorial()).LobbyTutorialEnabled &&
|
||||
ReduceUser(S0, SetBatteryLevel(0.75)).BatteryLevel == 0.75
|
||||
)
|
||||
Check(
|
||||
"切片 User:SetInDebt / ClearInDebt 切换 inDebt",
|
||||
ReduceUser(S0, SetInDebt()).InDebt &&
|
||||
!ReduceUser(S0.copy(InDebt = true), ClearInDebt()).InDebt
|
||||
)
|
||||
Check("切片 User:无关动作不改状态", ReduceUser(S0, AddFrags(1)) == S0)
|
||||
|
||||
val Root0 = RootState()
|
||||
val Root1 = ReduceRoot(Root0, UpdateCrystals(7))
|
||||
Check(
|
||||
"根:UpdateCrystals 只动 user",
|
||||
Root1.User.Crystals == 7 &&
|
||||
Root1.LoginByPassword == Root0.LoginByPassword &&
|
||||
Root1.Entrance == Root0.Entrance
|
||||
)
|
||||
Check(
|
||||
"根:user 无关动作保留前一次归约",
|
||||
ReduceRoot(Root1, AddFrags(1)).User == Root1.User
|
||||
)
|
||||
|
||||
val W = RootWiring.ByField("user")!!
|
||||
Check(
|
||||
"根:user 委派到 t8 / M8().qtd 且 27 个动作全部具名",
|
||||
W.SliceClass == "t8" && W.Reducer == "M8().qtd" &&
|
||||
W.Actions.size == 27 && W.Actions.none { it.startsWith("?") }
|
||||
)
|
||||
Check(
|
||||
"根:user 的匿名动作名覆盖三个分支",
|
||||
listOf("DisableLobbyTutorial", "SetInDebt", "ClearInDebt")
|
||||
.all { it in W.Actions }
|
||||
)
|
||||
}
|
||||
|
||||
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