feat(state): 迁移 user 切片(32 字段 / 28 臂)

This commit is contained in:
TermiNexus committed 2026-10-23 16:36:23 +08:00
1 parent 77a90fdfff
commit 9b3e76d42d
11 files changed
+529 -56

No files matched your search

+14 -6
View File
@@ -143,7 +143,7 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **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(输入)
+24 -5
View File
@@ -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",
+15
View File
@@ -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 = [];
+19
View File
@@ -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)