From c11702296992d7eb6a9edebc514f9beac3dd9776 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 24 Oct 2026 16:00:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(state):=20=E8=BF=81=E7=A7=BB=20friends/sup?= =?UTF-8?q?pliesState/clanUserInfo/battleInviteState/partner/userRenameSta?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/KtRebuild_Plan.md | 33 +- 3DTank/names/root_wiring.json | 136 +++++- 3DTank/tools/gen_root_wiring.js | 90 ++++ .../Reducers/BattleInviteStateReducer.kt | 56 +++ .../App/State/Reducers/ClanUserInfoReducer.kt | 31 ++ .../App/State/Reducers/FriendsReducer.kt | 106 +++++ .../App/State/Reducers/PartnerReducer.kt | 101 ++++ .../State/Reducers/SuppliesStateReducer.kt | 31 ++ .../App/State/Reducers/UserRenameReducer.kt | 125 +++++ .../jsMain/kotlin/App/State/RootReducer.kt | 22 +- .../src/jsMain/kotlin/App/State/RootState.kt | 30 +- .../src/jsMain/kotlin/App/State/RootWiring.kt | 12 +- .../app/src/jsMain/kotlin/App/StateCheck.kt | 430 +++++++++++++++++- 13 files changed, 1149 insertions(+), 54 deletions(-) create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleInviteStateReducer.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanUserInfoReducer.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/FriendsReducer.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PartnerReducer.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesStateReducer.kt create mode 100644 TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/UserRenameReducer.kt diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 07d38ec..40001c5 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -143,7 +143,7 @@ `names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。 - 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。 - 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。 - - 已迁移切片 **53/89**: + - 已迁移切片 **59/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) @@ -309,12 +309,33 @@ 新增 `ReportKind { BATTLE_NAME, LOBBY_CHAT_MESSAGE, BATTLE_CHAT_MESSAGE }` 与 `ReportEntry(kind, targetId, expireTime)`; 三类举报都追加一条记录、有效期 +1 天。原 reducer 还把列表写回 localStorage 的 `SENT_REPORTS`, 属 storage port 职责,纯 reducer 不做 IO(Init 保留现有列表)。 + - `friends` → `KD` / `NL().cr9`(`FriendsReducer.kt`):8 字段,13 个分支。 + 新增 `FriendSearchState { HIDDEN, SEARCHING, FOUND, NOT_FOUND }`(对应 `YD()`); + 四个集合分别做集合 / 单值增删;`SetValidationStatus` 原判据是 `status.equals(pk())`, + `pk()` 是校验通过单例,离线重建按「非空即 FOUND」收口。 + `AddUsersToNewAccepted` 与 clan 切片的同名动作合并成一个类(`clanId, userIds`),friends 只读 `userIds`。 + - `suppliesState` → `wE` / `$t.fq9`(`SuppliesStateReducer.kt`):1 字段 `Map`, + `qE`→`SetSupplyCount(supplyId, count)` 写一个键值。 + - `clanUserInfo` → `hO` / `de.gr1`(`ClanUserInfoReducer.kt`):1 字段 `Map`, + `Loaded(users)` 是**合并**(`jm` = copy + `putAll`),不是覆盖。 + - `battleInviteState` → `att` / `Ttt().nth`(`BattleInviteStateReducer.kt`):3 字段,6 个分支。 + `mtt` 的 `toString` 名 `InviteToBattle` 与另一个只有 `battleId` 的同名动作冲突, + 改用 `AddInvitedUser(receiverId)`;`dtt`→`ClearSelectedForInvite`。 + - `partner` → `UJ` / `(inline@partner)`(`PartnerReducer.kt`):15 字段,11 个内联分支。 + `_Z`→`UpdatePartnerInviteAction`、`tZ`/`JJ`→`MarkAccountLinked`/`MarkAccountNotLinked`、 + `hZ`→`SetPartnerItemEnabled`(`Km` = Pair,写 `itemsEnabledForPartner`); + `loginForBindingStatus` 复用登录切片的 `ByPasswordStatus`。 + - `userRenameState` → `O8` / `Rf.pte`(`UserRenameReducer.kt`):10 字段,11 个分支。 + 新增 `RenameUidState { EMPTY, SHORT, RESERVED, FORBIDDEN, NOT_VALID, FREE }`(对应 `D8()`); + `SetValidationStatus` 按 ordinal 映射输入状态(EMPTY→SHORT、SHORT→NOT_VALID、RESERVED→RESERVED、 + FORBIDDEN/NOT_VALID→FORBIDDEN、FREE→FREE)并把 `isResponseDelivered` 置 true; + `G8`→`ResetRenameDialog` 复位弹窗字段但保留 rubies / hash / email。 - `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map; `RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map`,不新增 reducer/动作。 - - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 52 个有 reducer 的切片, - `launchParams` 与其余 **36 个字段原样透传**; - 迁移进度用 `RootReducer.Coverage`(`已迁移 53/89`)与 `RootWiring.Unmigrated` 暴露。 - - 自检:`TankiOffline/tools/RunStateTest.ps1` **627 项全绿**,含 89 槽位完整性、52 个归约切片一次归约互不串片、未迁移字段透传, + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 58 个有 reducer 的切片, + `launchParams` 与其余 **30 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 59/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **685 项全绿**,含 89 槽位完整性、58 个归约切片一次归约互不串片、未迁移字段透传, 以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。 - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE, 已按字段具名为 `(inline@)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个); @@ -331,6 +352,8 @@ captchaState 的 5 个、modalScreen 的 1 个、droppableGold 的 2 个、creatorTagState 的 1 个、 battleTutorial 的 2 个、spectator 的 4 个、antiAddiction 的 3 个、moderation 的 13 个、 迁移 / 注册共用的 10 个(`Mit`…`_rt`)匿名类 / 单例同样按效果定名并登记。 + friends 的 2 个、suppliesState 的 1 个、battleInviteState 的 2 个、partner 的 4 个、 + userRenameState 的 5 个也已登记(其中 `mtt` 因同名类冲突改名为 `AddInvitedUser`)。 ### S4 引擎核心 - 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入) diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json index 3f1ce13..86aba2f 100644 --- a/3DTank/names/root_wiring.json +++ b/3DTank/names/root_wiring.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-24T07:23:39.141Z", + "generatedAt": "2026-10-24T07:48:13.464Z", "module": "module_10032.js", "RootState": { "Class": "z6", @@ -59,7 +59,13 @@ "moderation", "migrationState", "register", - "userReportsState" + "userReportsState", + "friends", + "suppliesState", + "clanUserInfo", + "battleInviteState", + "partner", + "userRenameState" ], "AnonymousActions": [ { @@ -998,6 +1004,90 @@ "Name": "ResetAuthenticationForm", "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", "Effect": "login=\"\"、两个状态复位(两个切片都读)" + }, + { + "Local": "_L", + "Name": "SetContextMenuUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "contextMenuUser=参数(`n.er7(cd, t.er9_1)`)" + }, + { + "Local": "hL", + "Name": "ClearContextMenuUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "contextMenuUser=null(`n.er7(cd, null)`)" + }, + { + "Local": "qE", + "Name": "SetSupplyCount", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "count 写键值(`n.bq9(cg(n.aq9_1, Vd(t.gq9_1, t.hq9_1)))`)" + }, + { + "Local": "mtt", + "Name": "AddInvitedUser", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "invitedUsers += receiverId(原名 InviteToBattle,与另一个只有 battleId 的同名动作冲突,改名 AddInvitedUser)" + }, + { + "Local": "dtt", + "Name": "ClearSelectedForInvite", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "selectedForInvite = emptySet()(`Yg()`)" + }, + { + "Local": "_Z", + "Name": "UpdatePartnerInviteAction", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "inviteAction=参数(`n.osf(…, t.osg_1)`)" + }, + { + "Local": "tZ", + "Name": "MarkAccountLinked", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isAccountAlreadyLinked=true(`n.osf(…, !0)`)" + }, + { + "Local": "JJ", + "Name": "MarkAccountNotLinked", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isAccountAlreadyLinked=false(`n.osf(…, !1)`)" + }, + { + "Local": "hZ", + "Name": "SetPartnerItemEnabled", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "itemsEnabledForPartner 写键值(`cg(n.ksf_1, new Km(t.msg_1, t.nsg_1))`)" + }, + { + "Local": "B8", + "Name": "MarkFreeRenameAvailable", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isFreeRenameAvailable=true(`n.cte(!0)`)" + }, + { + "Local": "F8", + "Name": "MarkRenameSucceeded", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "isFreeRenameAvailable=false、renameSuccessful=true(`n.cte(!1, cd, cd, !0)`)" + }, + { + "Local": "U8", + "Name": "MarkRenameFailed", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "renameSuccessful=false(`n.cte(cd, cd, cd, !1)`)" + }, + { + "Local": "G8", + "Name": "ResetRenameDialog", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "复位 shopItemId / uidInputState / renameSuccessful / 两个 response 位(`n.cte(cd, null, n6(), null, cd, !1, !1)`)" + }, + { + "Local": "Q8", + "Name": "HideLinkErrorWarning", + "DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名", + "Effect": "showLinkErrorWarning=false(`n.cte(…, !1)`)" } ], "Markers": [ @@ -1679,22 +1769,22 @@ "Reducer": "NL().cr9", "Delegate": "NL().cr9(t, n.ypv_1)", "Actions": [ - "?_L", - "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", + "ClearContextMenuUser", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", + "SetContextMenuUser", "SetLimits", "SetValidationStatus" ], "Default": "new KD()", - "Migrated": false + "Migrated": true }, { "Field": "remoteUserDataStorage", @@ -1892,10 +1982,10 @@ "Reducer": "$t.fq9", "Delegate": "$t.fq9(t, n.ipw_1)", "Actions": [ - "?qE" + "SetSupplyCount" ], "Default": "new wE()", - "Migrated": false + "Migrated": true }, { "Field": "matchmakingGroup", @@ -2064,7 +2154,7 @@ "Loaded" ], "Default": "new hO()", - "Migrated": false + "Migrated": true }, { "Field": "tankActionsLogState", @@ -2253,15 +2343,15 @@ "Reducer": "Ttt().nth", "Delegate": "Ttt().nth(t, n.cpx_1)", "Actions": [ - "?dtt", + "AddInvitedUser", + "ClearSelectedForInvite", "EnableSetBattleInviteNotifications", "InvitationResponse", - "InviteToBattle", "RemoveUserFromInvited", "SelectUserToInvite" ], "Default": "new att()", - "Migrated": false + "Migrated": true }, { "Field": "keyboardSettings", @@ -2340,20 +2430,20 @@ "Reducer": "(inline@partner)", "Delegate": "(function (t, n) { var i; if (t instanceof VJ) i = n.osf(t.rsf_1, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ssf_1); else if (t instanceof nZ) { var r = t.bsg_1, e = t.csg_1, u = t.dsg_1, s = t.esg_1, o = t.fsg_1, _ = t.gsg_1; i = n.osf(cd, cd, cd, r, e, u, cd, cd, o, _, s); } else i = t instanceof iZ ? n.osf(cd, t.hsg_1) : t instanceof uZ ? n.osf(cd, cd, t.ksg_1) : t instanceof eZ ? n.osf(cd, cd, cd, cd, cd, cd, cd, t.jsg_1) : t instanceof _Z ? n.osf(cd, cd, cd, cd, cd, cd, t.osg_1) : t instanceof X …", "Actions": [ - "?JJ", - "?_Z", - "?hZ", - "?tZ", "InitPartnerInviteAction", "InitSocialNetworkParameters", + "MarkAccountLinked", + "MarkAccountNotLinked", "SetLoginParams", "SetLoginStatusForBinding", "SetPartnerId", + "SetPartnerItemEnabled", "SetPartnerParameters", - "SetPartnerReady" + "SetPartnerReady", + "UpdatePartnerInviteAction" ], "Default": "new UJ()", - "Migrated": false + "Migrated": true }, { "Field": "entrance", @@ -2477,12 +2567,12 @@ "Reducer": "Rf.pte", "Delegate": "Rf.pte(t, n.qpx_1)", "Actions": [ - "?B8", - "?F8", - "?G8", - "?Q8", - "?U8", "ChangeUidInputState", + "HideLinkErrorWarning", + "MarkFreeRenameAvailable", + "MarkRenameFailed", + "MarkRenameSucceeded", + "ResetRenameDialog", "ResponseActual", "ResponseDelivered", "SetChangeUidParams", @@ -2490,7 +2580,7 @@ "SetValidationStatus" ], "Default": "new O8()", - "Migrated": false + "Migrated": true }, { "Field": "eventGoods", diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js index 957d184..77fa661 100644 --- a/3DTank/tools/gen_root_wiring.js +++ b/3DTank/tools/gen_root_wiring.js @@ -237,6 +237,26 @@ const AnonymousActionNames = new Map([ ['Git', 'RegisterFailed'], ['hrt', 'ResetRegisterStatus'], ['_rt', 'ResetAuthenticationForm'], + // friends 切片(KD / PL.cr9):_L/hL 没有 toString。 + ['_L', 'SetContextMenuUser'], + ['hL', 'ClearContextMenuUser'], + // suppliesState 切片(wE / yE.fq9):qE 没有 toString。 + ['qE', 'SetSupplyCount'], + // battleInviteState 切片(att / Ett.nth):dtt 没有 toString; + // mtt 的 toString 名 InviteToBattle 与另一个只有 battleId 的同名动作冲突,这里改用不冲突的类名。 + ['mtt', 'AddInvitedUser'], + ['dtt', 'ClearSelectedForInvite'], + // partner 切片(UJ / 内联 reducer):_Z/tZ/JJ/hZ 没有 toString。 + ['_Z', 'UpdatePartnerInviteAction'], + ['tZ', 'MarkAccountLinked'], + ['JJ', 'MarkAccountNotLinked'], + ['hZ', 'SetPartnerItemEnabled'], + // userRenameState 切片(O8 / Rf.pte):B8/F8/U8/G8/Q8 没有 toString。 + ['B8', 'MarkFreeRenameAvailable'], + ['F8', 'MarkRenameSucceeded'], + ['U8', 'MarkRenameFailed'], + ['G8', 'ResetRenameDialog'], + ['Q8', 'HideLinkErrorWarning'], ]); // 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。 @@ -678,6 +698,12 @@ const Migrated = { migrationState: { Type: 'MigrationStateSlice', Reduce: 'ReduceMigrationState' }, register: { Type: 'RegisterStateSlice', Reduce: 'ReduceRegister' }, userReportsState: { Type: 'UserReportsStateSlice', Reduce: 'ReduceUserReports' }, + friends: { Type: 'FriendsStateSlice', Reduce: 'ReduceFriends' }, + suppliesState: { Type: 'SuppliesStateSlice', Reduce: 'ReduceSupplies' }, + clanUserInfo: { Type: 'ClanUserInfoStateSlice', Reduce: 'ReduceClanUserInfo' }, + battleInviteState: { Type: 'BattleInviteStateSlice', Reduce: 'ReduceBattleInvite' }, + partner: { Type: 'PartnerStateSlice', Reduce: 'ReducePartner' }, + userRenameState: { Type: 'UserRenameStateSlice', Reduce: 'ReduceUserRename' }, }; for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); @@ -979,6 +1005,40 @@ const AuthAnonymousEffects = { _rt: 'login=""、两个状态复位(两个切片都读)', }; +// friends 切片(KD / PL.cr9):_L/hL 没有 toString。 +const FriendsAnonymousEffects = { + _L: 'contextMenuUser=参数(`n.er7(cd, t.er9_1)`)', + hL: 'contextMenuUser=null(`n.er7(cd, null)`)', +}; + +// suppliesState 切片(wE / yE.fq9):qE 没有 toString。 +const SuppliesAnonymousEffects = { + qE: 'count 写键值(`n.bq9(cg(n.aq9_1, Vd(t.gq9_1, t.hq9_1)))`)', +}; + +// battleInviteState 切片(att / Ett.nth):dtt 没有 toString;mtt 名字冲突另收口。 +const BattleInviteAnonymousEffects = { + mtt: 'invitedUsers += receiverId(原名 InviteToBattle,与另一个只有 battleId 的同名动作冲突,改名 AddInvitedUser)', + dtt: 'selectedForInvite = emptySet()(`Yg()`)', +}; + +// partner 切片(UJ / 内联 reducer):_Z/tZ/JJ/hZ 没有 toString。 +const PartnerAnonymousEffects = { + _Z: 'inviteAction=参数(`n.osf(…, t.osg_1)`)', + tZ: 'isAccountAlreadyLinked=true(`n.osf(…, !0)`)', + JJ: 'isAccountAlreadyLinked=false(`n.osf(…, !1)`)', + hZ: 'itemsEnabledForPartner 写键值(`cg(n.ksf_1, new Km(t.msg_1, t.nsg_1))`)', +}; + +// userRenameState 切片(O8 / Rf.pte):B8/F8/U8/G8/Q8 没有 toString。 +const UserRenameAnonymousEffects = { + B8: 'isFreeRenameAvailable=true(`n.cte(!0)`)', + F8: 'isFreeRenameAvailable=false、renameSuccessful=true(`n.cte(!1, cd, cd, !0)`)', + U8: 'renameSuccessful=false(`n.cte(cd, cd, cd, !1)`)', + G8: '复位 shopItemId / uidInputState / renameSuccessful / 两个 response 位(`n.cte(cd, null, n6(), null, cd, !1, !1)`)', + Q8: 'showLinkErrorWarning=false(`n.cte(…, !1)`)', +}; + fs.writeFileSync( path.join(P, 'names', 'root_wiring.json'), JSON.stringify( @@ -1175,6 +1235,36 @@ fs.writeFileSync( DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', Effect, })), + ...Object.entries(FriendsAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(SuppliesAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(BattleInviteAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(PartnerAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), + ...Object.entries(UserRenameAnonymousEffects).map(([Local, Effect]) => ({ + Local, + Name: AnonymousActionNames.get(Local), + DispatchedAt: '未在 action_trace_merged.json 中出现;按 reducer 分支效果定名', + Effect, + })), ], Markers: [ { diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleInviteStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleInviteStateReducer.kt new file mode 100644 index 0000000..e7803c4 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/BattleInviteStateReducer.kt @@ -0,0 +1,56 @@ +// BattleInviteStateReducer.kt —— 战斗邀请切片(对应 module_10032 的 att 状态 + Ett.nth) +// +// 证据链: +// 状态类 att:3 个字段,toString 名 "BattleInviteState(…)"。 +// 默认:isEnabled=true、invitedUsers=emptySet()、selectedForInvite=emptySet()。 +// 根委派:`Ttt().nth(t, n.cpx_1)`。 +// reducer(6 个实例臂): +// mtt(InviteToBattle) → invitedUsers += receiverId +// ytt(InvitationResponse) → invitedUsers -= userId +// wtt(SelectUserToInvite) → selectedForInvite += userId +// ptt(RemoveUserFromInvited) → selectedForInvite -= userId +// dtt(无 toString) → selectedForInvite = emptySet() +// ftt(EnableSetBattleInviteNotifications)→ isEnabled +package App.State.Reducers + +import App.State.EnableSetBattleInviteNotifications +import App.State.InvitationResponse +import App.State.RemoveUserFromInvited +import App.State.SelectUserToInvite + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `mtt`(`InviteToBattle`):把被邀请者加入已邀请集合。 + * 生成物里的 `InviteToBattle` 只保留了另一个同名的 `battleId` 动作,缺 `receiverId`, + * 因此这里单独收口成 `AddInvitedUser(userId)`。 */ +data class AddInvitedUser(val userId: Any?) + +/** 原 `dtt`:清空待邀请列表。 */ +object ClearSelectedForInvite + +/* ------------------------------------------------------------------ 状态 */ + +data class BattleInviteStateSlice( + val IsEnabled: Boolean = true, + val InvitedUsers: Set = emptySet(), + val SelectedForInvite: Set = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceBattleInvite( + State0: BattleInviteStateSlice, + Action: Any, +): BattleInviteStateSlice = when (Action) { + is AddInvitedUser -> + State0.copy(InvitedUsers = State0.InvitedUsers + Action.userId) + is InvitationResponse -> + State0.copy(InvitedUsers = State0.InvitedUsers - Action.userId) + is SelectUserToInvite -> + State0.copy(SelectedForInvite = State0.SelectedForInvite + Action.userId) + is RemoveUserFromInvited -> + State0.copy(SelectedForInvite = State0.SelectedForInvite - Action.userId) + is ClearSelectedForInvite -> State0.copy(SelectedForInvite = emptySet()) + is EnableSetBattleInviteNotifications -> State0.copy(IsEnabled = Action.isEnabled) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanUserInfoReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanUserInfoReducer.kt new file mode 100644 index 0000000..ebc7d2d --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/ClanUserInfoReducer.kt @@ -0,0 +1,31 @@ +// ClanUserInfoReducer.kt —— 战队成员信息切片(对应 module_10032 的 hO 状态 + cO.gr1) +// +// 证据链: +// 状态类 hO:1 个字段,toString 名 "ClanUserInfo(users=…)"。 +// 默认:users = ag() = emptyMap()。 +// 根委派:`de.gr1(t, n.opw_1)`。 +// reducer:`t instanceof aO ? n.dr1(jm(n.cr1_1, t.fr1_1)) : n` +// —— `jm` = Map.copy + putAll(`i3` 是 putAll),即把动作里的用户表合并进来。 +// 动作 `aO` = Loaded(users)。 +package App.State.Reducers + +import App.State.Loaded + +/* ------------------------------------------------------------------ 状态 */ + +data class ClanUserInfoStateSlice( + val Users: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceClanUserInfo( + State0: ClanUserInfoStateSlice, + Action: Any, +): ClanUserInfoStateSlice = when (Action) { + is Loaded -> { + val Incoming = Action.users as? Map ?: emptyMap() + State0.copy(Users = State0.Users + Incoming) + } + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/FriendsReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/FriendsReducer.kt new file mode 100644 index 0000000..40c936d --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/FriendsReducer.kt @@ -0,0 +1,106 @@ +// FriendsReducer.kt —— 好友切片(对应 module_10032 的 KD 状态 + PL.cr9) +// +// 证据链: +// 状态类 KD:8 个字段,toString 名 "FriendsState(…)"。 +// 默认:userSearchState=HIDDEN、contextMenuUser=null、两个 limit=0、四个集合 emptySet()。 +// 根委派:`NL().cr9(t, n.ypv_1)`。 +// reducer(13 个实例臂): +// uL(ChangeFriendAddFindState)→ userSearchState +// _L(无 toString) → contextMenuUser = 参数 +// hL(无 toString) → contextMenuUser = null +// aL(SetLimits) → acceptedLimit / incomingLimit +// cL / pL → accepted ∓ +// lL / vL → newAccepted ∓ +// dL / zL → incoming ∓ +// gL / kL → newIncoming ∓ +// U9(SetValidationStatus) → userSearchState = FOUND / NOT_FOUND +// +// 枚举直接对应源码 `YD()`:HIDDEN / SEARCHING / FOUND / NOT_FOUND。 +// 原判据是 `status.equals(pk())`,`pk()` 是校验通过的单例(`new tJ()`); +// 离线重建没有这个单例的 Kotlin 值,这里按「传了非空状态即视为找到」收口。 +package App.State.Reducers + +import App.State.AddUsersToAccepted +import App.State.AddUsersToIncoming +import App.State.AddUsersToNewAccepted +import App.State.AddUsersToNewIncoming +import App.State.ChangeFriendAddFindState +import App.State.RemoveUserFromAccepted +import App.State.RemoveUserFromIncoming +import App.State.RemoveUserFromNewAccepted +import App.State.RemoveUsersFromNewIncoming +import App.State.SetLimits +import App.State.SetValidationStatus + +/* ------------------------------------------------------------------ 枚举 */ + +enum class FriendSearchState { + HIDDEN, + SEARCHING, + FOUND, + NOT_FOUND, +} + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `_L`:打开某个好友的右键菜单。 */ +data class SetContextMenuUser(val user: Any?) + +/** 原 `hL`:关闭右键菜单。 */ +object ClearContextMenuUser + +/* ------------------------------------------------------------------ 状态 */ + +data class FriendsStateSlice( + val UserSearchState: FriendSearchState = FriendSearchState.HIDDEN, + val ContextMenuUser: Any? = null, + val AcceptedLimit: Int = 0, + val IncomingLimit: Int = 0, + val Accepted: Set = emptySet(), + val NewAccepted: Set = emptySet(), + val Incoming: Set = emptySet(), + val NewIncoming: Set = emptySet(), +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `Zg`:集合 += 集合 / 单值。 */ +private fun Plus(Target: Set, Value: Any?): Set = + if (Value is Iterable<*>) Target + Value.toSet() else Target + Value + +/** 原 `Qg`:集合 -= 集合 / 单值。 */ +private fun Minus(Target: Set, Value: Any?): Set = + if (Value is Iterable<*>) Target - Value.toSet() else Target - Value + +fun ReduceFriends( + State0: FriendsStateSlice, + Action: Any, +): FriendsStateSlice = when (Action) { + is ChangeFriendAddFindState -> + State0.copy(UserSearchState = Action.state as FriendSearchState) + is SetContextMenuUser -> State0.copy(ContextMenuUser = Action.user) + is ClearContextMenuUser -> State0.copy(ContextMenuUser = null) + is SetLimits -> State0.copy( + AcceptedLimit = Action.acceptedLimit, + IncomingLimit = Action.incomingLimit, + ) + is AddUsersToAccepted -> State0.copy(Accepted = Plus(State0.Accepted, Action.usersId)) + is RemoveUserFromAccepted -> State0.copy(Accepted = Minus(State0.Accepted, Action.userId)) + // 生成物里 `AddUsersToNewAccepted` 与 clan 切片的同名动作合并成了一个类 + // (`clanId, userIds`),friends 只读 `userIds`。 + is AddUsersToNewAccepted -> + State0.copy(NewAccepted = Plus(State0.NewAccepted, Action.userIds)) + is RemoveUserFromNewAccepted -> + State0.copy(NewAccepted = Minus(State0.NewAccepted, Action.userId)) + is AddUsersToIncoming -> State0.copy(Incoming = Plus(State0.Incoming, Action.usersId)) + is RemoveUserFromIncoming -> State0.copy(Incoming = Minus(State0.Incoming, Action.userId)) + is AddUsersToNewIncoming -> + State0.copy(NewIncoming = Plus(State0.NewIncoming, Action.usersId)) + is RemoveUsersFromNewIncoming -> + State0.copy(NewIncoming = Minus(State0.NewIncoming, Action.usersId)) + is SetValidationStatus -> State0.copy( + UserSearchState = + if (Action.status == null) FriendSearchState.NOT_FOUND else FriendSearchState.FOUND, + ) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PartnerReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PartnerReducer.kt new file mode 100644 index 0000000..c4c97f7 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/PartnerReducer.kt @@ -0,0 +1,101 @@ +// PartnerReducer.kt —— 合作方(partner)切片(对应 module_10032 的 UJ 状态 + 内联 reducer) +// +// 证据链: +// 状态类 UJ:15 个字段,toString 名 "PartnerState(…)";copy 是 osf(15 字段 + 回调)。 +// 默认:partnerId=null、loginParams=emptyMap()、params=emptyMap()、三个能力位 false、 +// paymentAction=null、inviteAction=null、canOpenExternalLinks=false、 +// hasAccountBinding=false、failedRedirectUrl=null、loginForBindingStatus=OD()(NONE)、 +// isAccountAlreadyLinked=false、itemsEnabledForPartner=emptyMap()、isPartnerReady=false。 +// 根委派:内联 IIFE,字段 `fpy_1`;委派表记作 `(inline@partner)`,11 个动作。 +// reducer: +// VJ(SetPartnerId) → partnerId + failedRedirectUrl +// nZ(InitSocialNetworkParameters)→ 3 个能力位 + canOpenExternalLinks + hasAccountBinding + failedRedirectUrl +// iZ(SetLoginParams) → loginParams +// uZ(SetPartnerParameters) → params +// eZ(InitPartnerInviteAction) → inviteAction +// _Z(无 toString) → inviteAction(另一个派发点) +// XJ(SetLoginStatusForBinding) → loginForBindingStatus(复用登录切片的 ByPasswordStatus) +// tZ / JJ(无 toString) → isAccountAlreadyLinked = true / false +// hZ(无 toString) → itemsEnabledForPartner 写一个键值(`Km` = Pair) +// rZ(SetPartnerReady) → isPartnerReady +package App.State.Reducers + +import App.State.InitPartnerInviteAction +import App.State.InitSocialNetworkParameters +import App.State.SetLoginParams +import App.State.SetLoginStatusForBinding +import App.State.SetPartnerId +import App.State.SetPartnerParameters +import App.State.SetPartnerReady + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `_Z`:更新邀请动作(与 InitPartnerInviteAction 同字段,另一个派发点)。 */ +data class UpdatePartnerInviteAction(val action: Any?) + +/** 原 `tZ`:标记账号已绑定。 */ +object MarkAccountLinked + +/** 原 `JJ`:标记账号未绑定。 */ +object MarkAccountNotLinked + +/** 原 `hZ`:写某个合作方物品的启用状态。 */ +data class SetPartnerItemEnabled(val itemId: Any?, val enabled: Boolean) + +/* ------------------------------------------------------------------ 状态 */ + +data class PartnerStateSlice( + val PartnerId: Any? = null, + val LoginParams: Map = emptyMap(), + val Params: Map = emptyMap(), + val HasOwnPaymentSystem: Boolean = false, + val HasSocialFunction: Boolean = false, + val HasClientPaymentAction: Boolean = false, + val PaymentAction: Any? = null, + val InviteAction: Any? = null, + val CanOpenExternalLinks: Boolean = false, + val HasAccountBinding: Boolean = false, + val FailedRedirectUrl: Any? = null, + val LoginForBindingStatus: ByPasswordStatus = ByPasswordStatus.NONE, + val IsAccountAlreadyLinked: Boolean = false, + val ItemsEnabledForPartner: Map = emptyMap(), + val IsPartnerReady: Boolean = false, +) + +/* ------------------------------------------------------------------ reducer */ + +@Suppress("UNCHECKED_CAST") +private fun AsMap(Value: Any?): Map = + Value as? Map ?: emptyMap() + +fun ReducePartner( + State0: PartnerStateSlice, + Action: Any, +): PartnerStateSlice = when (Action) { + is SetPartnerId -> State0.copy( + PartnerId = Action.partnerId, + FailedRedirectUrl = Action.failedRedirectUrl, + ) + is InitSocialNetworkParameters -> State0.copy( + HasOwnPaymentSystem = Action.hasOwnPaymentSystem, + HasSocialFunction = Action.hasSocialFunction, + HasClientPaymentAction = Action.hasClientPaymentAction, + CanOpenExternalLinks = Action.canOpenExternalLinks, + HasAccountBinding = Action.hasAccountBinding, + FailedRedirectUrl = Action.failedRedirectUrl, + ) + is SetLoginParams -> State0.copy(LoginParams = AsMap(Action.params)) + is SetPartnerParameters -> State0.copy(Params = AsMap(Action.params)) + is InitPartnerInviteAction -> State0.copy(InviteAction = Action.action) + is UpdatePartnerInviteAction -> State0.copy(InviteAction = Action.action) + is SetLoginStatusForBinding -> + State0.copy(LoginForBindingStatus = Action.status as ByPasswordStatus) + is MarkAccountLinked -> State0.copy(IsAccountAlreadyLinked = true) + is MarkAccountNotLinked -> State0.copy(IsAccountAlreadyLinked = false) + is SetPartnerItemEnabled -> State0.copy( + ItemsEnabledForPartner = + State0.ItemsEnabledForPartner + (Action.itemId to Action.enabled), + ) + is SetPartnerReady -> State0.copy(IsPartnerReady = Action.isReady) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesStateReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesStateReducer.kt new file mode 100644 index 0000000..0e3ae2c --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/SuppliesStateReducer.kt @@ -0,0 +1,31 @@ +// SuppliesStateReducer.kt —— 补给计数切片(对应 module_10032 的 wE 状态 + yE.fq9) +// +// 证据链: +// 状态类 wE:1 个字段,toString 名 "SuppliesState(count=…)"。 +// 默认:count = ag() = emptyMap()。 +// 根委派:`$t.fq9(t, n.ipw_1)`。 +// reducer:`t instanceof qE ? n.bq9(cg(n.aq9_1, Vd(t.gq9_1, t.hq9_1))) : n` +// —— `qE(key, value)` 往 Map 里写一个键值(`cg` = put,`Vd` = Pair)。 +package App.State.Reducers + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `qE`:写一个补给 id 的剩余次数。 */ +data class SetSupplyCount(val supplyId: Any?, val count: Any?) + +/* ------------------------------------------------------------------ 状态 */ + +data class SuppliesStateSlice( + val Count: Map = emptyMap(), +) + +/* ------------------------------------------------------------------ reducer */ + +fun ReduceSupplies( + State0: SuppliesStateSlice, + Action: Any, +): SuppliesStateSlice = when (Action) { + is SetSupplyCount -> + State0.copy(Count = State0.Count + (Action.supplyId to Action.count)) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/UserRenameReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/UserRenameReducer.kt new file mode 100644 index 0000000..245e2a4 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/UserRenameReducer.kt @@ -0,0 +1,125 @@ +// UserRenameReducer.kt —— 改名切片(对应 module_10032 的 O8 状态 + t6.pte) +// +// 证据链: +// 状态类 O8:10 个字段,toString 名 "UserRenameState(…)";copy 是 cte(10 字段 + 回调)。 +// 默认:isFreeRenameAvailable=false、userRenameShopItemId=null、uidInputState=EMPTY、 +// renameSuccessful=null、rubiesForRename=Int.MAX_VALUE、两个 response 位 false、 +// changeUidHash=null、email=null、showLinkErrorWarning=true。 +// 根委派:`Rf.pte(t, n.qpx_1)`。 +// reducer(11 个实例臂): +// H8(ChangeUidInputState)→ uidInputState +// B8(无 toString) → isFreeRenameAvailable=true +// F8(无 toString) → isFreeRenameAvailable=false、renameSuccessful=true +// U8(无 toString) → renameSuccessful=false +// U9(SetValidationStatus)→ isResponseDelivered=true; +// uidInputState 按校验状态映射 +// (源码 `switch (status.k3_1)`:EMPTY→SHORT、SHORT→NOT_VALID、 +// RESERVED→RESERVED、FORBIDDEN/NOT_VALID→FORBIDDEN、FREE→FREE) +// V8(SetRubiesForRename) → rubiesForRename +// G8(无 toString) → 整片复位(shopItemId=null、uidInputState=EMPTY、renameSuccessful=null、 +// 两个 response 位 false) +// K8(ResponseDelivered) → isResponseDelivered +// Y8(ResponseActual) → isResponseActual +// X8(SetChangeUidParams) → changeUidHash、email +// Q8(无 toString) → showLinkErrorWarning=false +// +// 枚举 `UidInputState` 直接对应源码 `D8()`:EMPTY / SHORT / RESERVED / FORBIDDEN / NOT_VALID / FREE。 +// `SetValidationStatus.status` 在原包里是同一枚举;离线重建按该枚举的 ordinal 映射。 +package App.State.Reducers + +import App.State.ChangeUidInputState +import App.State.ResponseActual +import App.State.ResponseDelivered +import App.State.SetChangeUidParams +import App.State.SetRubiesForRename +import App.State.SetValidationStatus + +/* ------------------------------------------------------------------ 枚举 */ + +enum class RenameUidState { + EMPTY, + SHORT, + RESERVED, + FORBIDDEN, + NOT_VALID, + FREE, +} + +/* ------------------------------------------------------------------ 匿名动作 */ + +/** 原 `B8`:免费改名可用。 */ +object MarkFreeRenameAvailable + +/** 原 `F8`:改名成功(同时把免费名额用掉)。 */ +object MarkRenameSucceeded + +/** 原 `U8`:改名失败。 */ +object MarkRenameFailed + +/** 原 `G8`:复位改名弹窗(保留 rubiesForRename / email 等外层字段)。 */ +object ResetRenameDialog + +/** 原 `Q8`:隐藏链接错误提示。 */ +object HideLinkErrorWarning + +/* ------------------------------------------------------------------ 状态 */ + +data class UserRenameStateSlice( + val IsFreeRenameAvailable: Boolean = false, + val UserRenameShopItemId: Any? = null, + val UidInputState: RenameUidState = RenameUidState.EMPTY, + val RenameSuccessful: Any? = null, + val RubiesForRename: Int = Int.MAX_VALUE, + val IsResponseDelivered: Boolean = false, + val IsResponseActual: Boolean = false, + val ChangeUidHash: Any? = null, + val Email: Any? = null, + val ShowLinkErrorWarning: Boolean = true, +) + +/* ------------------------------------------------------------------ reducer */ + +/** 原 `switch (status.k3_1)`:校验状态 → 输入框状态。 */ +private fun MapValidationStatus(Status: Any?): RenameUidState? = + when ((Status as? RenameUidState)?.ordinal) { + 0 -> RenameUidState.SHORT + 1 -> RenameUidState.NOT_VALID + 2 -> RenameUidState.RESERVED + 3, 4 -> RenameUidState.FORBIDDEN + 5 -> RenameUidState.FREE + else -> null + } + +fun ReduceUserRename( + State0: UserRenameStateSlice, + Action: Any, +): UserRenameStateSlice = when (Action) { + is ChangeUidInputState -> + State0.copy(UidInputState = Action.uidInputState as RenameUidState) + is MarkFreeRenameAvailable -> State0.copy(IsFreeRenameAvailable = true) + is MarkRenameSucceeded -> State0.copy( + IsFreeRenameAvailable = false, + RenameSuccessful = true, + ) + is MarkRenameFailed -> State0.copy(RenameSuccessful = false) + is SetValidationStatus -> State0.copy( + UidInputState = MapValidationStatus(Action.status) ?: State0.UidInputState, + IsResponseDelivered = true, + ) + is SetRubiesForRename -> State0.copy(RubiesForRename = Action.rubiesForRename) + is ResetRenameDialog -> State0.copy( + UserRenameShopItemId = null, + UidInputState = RenameUidState.EMPTY, + RenameSuccessful = null, + IsResponseDelivered = false, + IsResponseActual = false, + ) + is ResponseDelivered -> State0.copy(IsResponseDelivered = Action.isDelivered) + is ResponseActual -> State0.copy(IsResponseActual = Action.isResponseActual) + is SetChangeUidParams -> State0.copy( + ChangeUidHash = Action.changeUidHash, + Email = Action.email, + ) + is HideLinkErrorWarning -> State0.copy(ShowLinkErrorWarning = false) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt index b4923a9..a48a6fc 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -1,7 +1,7 @@ // RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6) // // 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。 -// 这里采用同一形状。已迁移的 53 个切片里,52 个真正调用 reducer,launchParams 只做类型收口: +// 这里采用同一形状。已迁移的 59 个切片里,58 个真正调用 reducer,launchParams 只做类型收口: // // inviteState → ReduceInvite(aD / pD.er5) // loginByPassword → ReduceLoginByPassword(gD / MD.cr6) @@ -55,9 +55,15 @@ // migrationState → ReduceMigrationState($it / Uv.rtg,根委派 `Uv.rtg(t, n.ipv_1)`) // register → ReduceRegister(Cit / cp.stg,根委派 `cp.stg(t, n.hpv_1)`) // userReportsState → ReduceUserReports(Zst / qot.zth,根委派 `(null == sd && new qot(), sd).zth(t, n.dpy_1)`) +// friends → ReduceFriends(KD / NL().cr9,根委派 `NL().cr9(t, n.ypv_1)`) +// suppliesState → ReduceSupplies(wE / $t.fq9,根委派 `$t.fq9(t, n.ipw_1)`) +// clanUserInfo → ReduceClanUserInfo(hO / de.gr1,根委派 `de.gr1(t, n.opw_1)`) +// battleInviteState → ReduceBattleInvite(att / Ttt().nth,根委派 `Ttt().nth(t, n.cpx_1)`) +// partner → ReducePartner(UJ / `(inline@partner)`,根委派内联 IIFE) +// userRenameState → ReduceUserRename(O8 / Rf.pte,根委派 `Rf.pte(t, n.qpx_1)`) // launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap()) // -// 其余 36 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 其余 30 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, // 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 // 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 package App.State @@ -67,7 +73,9 @@ import App.State.Reducers.ReduceAnnouncement import App.State.Reducers.ReduceAntiAddiction import App.State.Reducers.ReduceBattleResult import App.State.Reducers.ReduceBattleTutorial +import App.State.Reducers.ReduceBattleInvite import App.State.Reducers.ReduceCaptcha +import App.State.Reducers.ReduceClanUserInfo import App.State.Reducers.ReduceContextTutorial import App.State.Reducers.ReduceCreatorTag import App.State.Reducers.ReduceDroppableGold @@ -75,6 +83,7 @@ import App.State.Reducers.ReduceEntrance import App.State.Reducers.ReduceEventGoods import App.State.Reducers.ReduceGarage import App.State.Reducers.ReduceGaragePreview +import App.State.Reducers.ReduceFriends import App.State.Reducers.ReduceHome import App.State.Reducers.ReduceIcons import App.State.Reducers.ReduceInvite @@ -92,6 +101,7 @@ import App.State.Reducers.ReduceNews import App.State.Reducers.ReduceNotificationByCountry import App.State.Reducers.ReduceNotifications import App.State.Reducers.ReducePasswordChange +import App.State.Reducers.ReducePartner import App.State.Reducers.ReduceProBattle import App.State.Reducers.ReducePromoCode import App.State.Reducers.ReducePurchaseDialog @@ -108,11 +118,13 @@ import App.State.Reducers.ReduceSpectator import App.State.Reducers.ReduceSupportUrl import App.State.Reducers.ReduceSettings import App.State.Reducers.ReduceShop +import App.State.Reducers.ReduceSupplies import App.State.Reducers.ReduceSystem import App.State.Reducers.ReduceTips import App.State.Reducers.ReduceTwitch import App.State.Reducers.ReduceTwoStepVerification import App.State.Reducers.ReduceUser +import App.State.Reducers.ReduceUserRename import App.State.Reducers.ReduceUserReports /** 根 reducer:`k6`。 */ @@ -180,6 +192,12 @@ object RootReducer { MigrationState = ReduceMigrationState(State0.MigrationState, Action), Register = ReduceRegister(State0.Register, Action), UserReportsState = ReduceUserReports(State0.UserReportsState, Action), + Friends = ReduceFriends(State0.Friends, Action), + SuppliesState = ReduceSupplies(State0.SuppliesState, Action), + ClanUserInfo = ReduceClanUserInfo(State0.ClanUserInfo, Action), + BattleInviteState = ReduceBattleInvite(State0.BattleInviteState, Action), + Partner = ReducePartner(State0.Partner, Action), + UserRenameState = ReduceUserRename(State0.UserRenameState, Action), ) } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt index 807bfe6..27f4e94 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -8,14 +8,17 @@ import App.State.Reducers.AccountSettingsStateSlice import App.State.Reducers.AnnouncementStateSlice import App.State.Reducers.AntiAddictionStateSlice import App.State.Reducers.AvailabilityByRankStateSlice +import App.State.Reducers.BattleInviteStateSlice import App.State.Reducers.BattleResultStateSlice import App.State.Reducers.BattleTutorialStateSlice import App.State.Reducers.CaptchaStateSlice +import App.State.Reducers.ClanUserInfoStateSlice import App.State.Reducers.ContextTutorialStateSlice import App.State.Reducers.CreatorTagStateSlice import App.State.Reducers.DroppableGoldStateSlice import App.State.Reducers.EntranceStateSlice import App.State.Reducers.EventGoodsStateSlice +import App.State.Reducers.FriendsStateSlice import App.State.Reducers.GarageStateSlice import App.State.Reducers.GaragePreviewStateSlice import App.State.Reducers.HomeStateSlice @@ -32,6 +35,7 @@ import App.State.Reducers.ModerationStateSlice import App.State.Reducers.NewsStateSlice import App.State.Reducers.NotificationByCountryStateSlice import App.State.Reducers.NotificationsStateSlice +import App.State.Reducers.PartnerStateSlice import App.State.Reducers.PasswordChangeStateSlice import App.State.Reducers.ProBattleStateSlice import App.State.Reducers.PromoCodeStateSlice @@ -48,12 +52,14 @@ import App.State.Reducers.SecuritySettingsStateSlice import App.State.Reducers.SettingsStateSlice import App.State.Reducers.ShopStateSlice import App.State.Reducers.SpectatorStateSlice +import App.State.Reducers.SuppliesStateSlice import App.State.Reducers.SupportUrlStateSlice import App.State.Reducers.SystemStateSlice import App.State.Reducers.TipsStateSlice import App.State.Reducers.TwitchStateSlice import App.State.Reducers.TwoStepVerificationStateSlice import App.State.Reducers.UserStateSlice +import App.State.Reducers.UserRenameStateSlice import App.State.Reducers.UserReportsStateSlice /** @@ -113,8 +119,8 @@ data class RootState( val LootBoxes: LootBoxesStateSlice = LootBoxesStateSlice(), /** 原 notifications(jJ / B_.ise)。已迁移。 */ val Notifications: NotificationsStateSlice = NotificationsStateSlice(), - /** 原 friends(KD / NL().cr9)。未迁移:原样透传。 */ - val Friends: Any? = null, + /** 原 friends(KD / NL().cr9)。已迁移。 */ + val Friends: FriendsStateSlice = FriendsStateSlice(), /** 原 remoteUserDataStorage($Q / vh.isl)。未迁移:原样透传。 */ val RemoteUserDataStorage: Any? = null, /** 原 battlesSelect(fA / Mi.vqk)。未迁移:原样透传。 */ @@ -133,8 +139,8 @@ data class RootState( val BattleResult: BattleResultStateSlice = BattleResultStateSlice(), /** 原 battleUsers(uP / sr.hqs)。未迁移:原样透传。 */ val BattleUsers: Any? = null, - /** 原 suppliesState(wE / $t.fq9)。未迁移:原样透传。 */ - val SuppliesState: Any? = null, + /** 原 suppliesState(wE / $t.fq9)。已迁移。 */ + val SuppliesState: SuppliesStateSlice = SuppliesStateSlice(), /** 原 matchmakingGroup(rW / NW().is0)。未迁移:原样透传。 */ val MatchmakingGroup: Any? = null, /** 原 shop(j4 / (inline@shop))。已迁移。 */ @@ -145,8 +151,8 @@ data class RootState( val BattleViewState: Any? = null, /** 原 rankUpDialogState(LO / Se.br3)。已迁移。 */ val RankUpDialogState: RankUpDialogStateSlice = RankUpDialogStateSlice(), - /** 原 clanUserInfo(hO / de.gr1)。未迁移:原样透传。 */ - val ClanUserInfo: Any? = null, + /** 原 clanUserInfo(hO / de.gr1)。已迁移。 */ + val ClanUserInfo: ClanUserInfoStateSlice = ClanUserInfoStateSlice(), /** 原 tankActionsLogState(vI / wi.gqe)。未迁移:原样透传。 */ val TankActionsLogState: Any? = null, /** 原 battleMessage(FE / vT().rqb)。未迁移:原样透传。 */ @@ -173,8 +179,8 @@ data class RootState( val SecuritySettings: SecuritySettingsStateSlice = SecuritySettingsStateSlice(), /** 原 purchaseDialogState(CO / je.ur2)。已迁移。 */ val PurchaseDialogState: PurchaseDialogStateSlice = PurchaseDialogStateSlice(), - /** 原 battleInviteState(att / Ttt().nth)。未迁移:原样透传。 */ - val BattleInviteState: Any? = null, + /** 原 battleInviteState(att / Ttt().nth)。已迁移。 */ + val BattleInviteState: BattleInviteStateSlice = BattleInviteStateSlice(), /** 原 keyboardSettings(Sut / bw.oth)。未迁移:原样透传。 */ val KeyboardSettings: Any? = null, /** 原 touchscreenHudSettings(j7 / N7().xsv)。未迁移:原样透传。 */ @@ -185,8 +191,8 @@ data class RootState( val Language: Language = App.State.Language.EN, /** 原 launchParams(? / (pass-through))。已迁移(透传,强类型)。 */ val LaunchParams: LaunchParamsStateSlice = emptyMap(), - /** 原 partner(UJ / (inline@partner))。未迁移:原样透传。 */ - val Partner: Any? = null, + /** 原 partner(UJ / (inline@partner))。已迁移。 */ + val Partner: PartnerStateSlice = PartnerStateSlice(), /** 原 entrance(hD / Bv.pth)。已迁移。 */ val Entrance: EntranceStateSlice = EntranceStateSlice(), /** 原 antiAddiction(UC / W.iq8)。已迁移。 */ @@ -201,8 +207,8 @@ data class RootState( val Announcement: AnnouncementStateSlice = AnnouncementStateSlice(), /** 原 notificationByCountry(gJ / R_.ssd)。已迁移。 */ val NotificationByCountry: NotificationByCountryStateSlice = NotificationByCountryStateSlice(), - /** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */ - val UserRenameState: Any? = null, + /** 原 userRenameState(O8 / Rf.pte)。已迁移。 */ + val UserRenameState: UserRenameStateSlice = UserRenameStateSlice(), /** 原 eventGoods(W7 / Wc.esx)。已迁移。 */ val EventGoods: EventGoodsStateSlice = EventGoodsStateSlice(), /** 原 creatorTagState(qO / ze.er2)。已迁移。 */ diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt index 7e19c25..e1779e3 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -40,7 +40,7 @@ object RootWiring { SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("ClearAccountRemovalTimer", "ClearInputUid", "ResetPasswordFields", "SetAccountRemovalDays", "SetCountry", "SetCurrentPassword", "SetCurrentPasswordMandatory", "SetEmail", "SetEmailConfirmed", "SetNewPassword", "SetPasswordStatus", "SetRepeatNewPassword", "SetUnlockButtonForRenameUid")), SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("AddKeys", "ClearRewards", "LootBoxOpeningFailed", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsBlockOpenButtons", "SetIsOpening", "SetIsReopening", "SetIsShopVisited", "SetKeys", "SetPeriodOfGiveLootBox", "SetPossibleRewards", "SetRewards", "SetTimeToGiveLootBox", "ShownRewards")), SliceWiring("notifications", "xpv_1", "jJ", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("AddNotification", "InitNotificationSound", "RemoveFirstNotification", "RemoveNotifications")), - SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("?_L", "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetLimits", "SetValidationStatus")), + SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "ClearContextMenuUser", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetContextMenuUser", "SetLimits", "SetValidationStatus")), SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")), SliceWiring("battlesSelect", "apw_1", "fA", "Mi.vqk", "Mi.vqk(t, n.apw_1)", listOf("?dA", "?gA", "AddBattle", "AddUser", "AddUsers", "ChangeName", "EnterToBattleInfo", "InitProBattleRewardLimit", "LimitReached", "RemoveBattle", "RemoveUser", "RoundFinish", "RoundStarted", "SetReEnterProtectBattleId", "SwapTeam", "UpdateFund", "UpdateSuspicion", "UpdateTeamScore", "UpdateUserScore", "UpdateUserSuspicion")), SliceWiring("maps", "bpw_1", "OA", "Oi.sql", "Oi.sql(t, n.bpw_1)", listOf("AddMap")), @@ -50,7 +50,7 @@ object RootWiring { SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("HideLoader", "ShowLoader")), SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("DoInit", "ResetBattleRewards", "SetHasBattleResult", "SetIncreasedReward", "SetRewards", "SetSimpleReward")), SliceWiring("battleUsers", "hpw_1", "uP", "sr.hqs", "sr.hqs(t, n.hpw_1)", listOf("?_P", "?hP", "?lP", "?oP", "?qP", "?sP", "InitSuspiciousState", "InitUserLevels", "RemoveTankInfo", "SetGearScore", "SetTankInfo", "SetTankResistance", "UpdateUserLevel", "UpdateUserRank", "UpdateUserScoreKillsAndDeaths", "UpdateUserSuspiciousState", "UpdateUsersClanTag", "UpdateUsersScoreKillsAndDeaths")), - SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("?qE")), + SliceWiring("suppliesState", "ipw_1", "wE", "\$t.fq9", "\$t.fq9(t, n.ipw_1)", listOf("SetSupplyCount")), SliceWiring("matchmakingGroup", "jpw_1", "rW", "NW().is0", "NW().is0(t, n.jpw_1)", listOf("?zW", "AcceptedInviteToGroup", "AddUser", "AddUserToSelected", "InitMatchmakingGroup", "Invite", "RejectedInviteToGroup", "RemoveUserToSelected", "SetAllowedFromFriends", "SetSelectedUsers", "UpdateMountItems", "UpdateUserReadiness", "UserRemoved")), SliceWiring("shop", "kpw_1", "j4", "(inline@shop)", "(function (t, n) { var i; if (t instanceof w4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, t.lt6_1); else if (t instanceof Yut) i = n.qt6(cd, cd, cd, cd, !0); else if (t instanceof F5) i = new j4(cd, cd, cd, cd, cd, cd, cd, n.qq3_1); else if (t instanceof h4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.st5_1); else if (t instanceof c4) i = n.qt6(cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ut5_1); else if (t instanceof u4) i = n.qt6( cd, cd, cd, cd, cd, cd, …", listOf("?x4", "AddCategoriesWithBonus", "AddItemToOrderProcessing", "BlockBackButton", "BraintreeLoadingInterrupt", "ClearPreviousScreen", "ClearTargetCategory", "ClearTargetProductId", "CurrentPayMode", "DeleteOneTimePurchaseItem", "InitAdditionalDescription", "InitAmountString", "InitCategory", "InitCoinPackage", "InitCompensation", "InitCrystalKitPackage", "InitFeatureItem", "InitGroup", "InitItemViewType", "InitKeysPackage", "InitKitBundle", "InitKitPackage", "InitLootBoxAndPaint", "InitOneTimePurchaseItem", "InitPayModeAction", "InitPaymentModeAsShopItem", "InitPaymentProviderItems", "InitPaymentViewType", "InitRestrictionByPayMode", "InitRewardedVideosBlocks", "InitShopCard", "InitShopCardDiscount", "InitShopCardView", "InitShopItem", "InitShopItemName", "InitSingleItemKit", "InitSpecialKitPackage", "MobilePaymentStateChanged", "PaymentProviderFinishFail", "PaymentProviderFinishSuccess", "PaymentProviderInProgress", "PaymentUrlReceiveFailed", "PaymentUrlReceived", "RemoveDiscountItemForItem", "RemoveItemFromOrderProcessing", "ResetPaymentState", "ResetShop", "SavePreviousScreen", "SaveTargetProductId", "SaveTargetVisitedCategory", "SelectCategory", "SelectCategoryId", "SelectItem", "SetBraintreeToken", "SetCoinPaymentProviderUrl", "SetGoToHomeAfterBuy", "SetInitShopItemId", "SetPurchaseCompleted", "SetStripeIntentParams", "SetupDiscountCategoryByItem", "SetupDiscountItem", "SetupDiscountItemForItem", "ShopCardExpired", "ShopCardRemoved", "ShopCardSold", "ShopLoaded", "ToggleShopActivity", "TryToBuyOneTimePurchase", "UnblockBackButton")), SliceWiring("battlePauseState", "lpw_1", "dT", "NT().mqc", "NT().mqc(t, n.lpw_1)", listOf("?TT", "?xT", "OnPauseMode", "SetIdleKickPeriod")), @@ -70,13 +70,13 @@ object RootWiring { SliceWiring("controlSettings", "zpw_1", "i2", "_2().ess", "_2().ess(t, n.zpw_1)", listOf("?_C", "UpdateSettings")), SliceWiring("securitySettings", "apx_1", "V2", "pc.qsv", "pc.qsv(t, n.apx_1)", listOf("SetProfileBlockStatus")), SliceWiring("purchaseDialogState", "bpx_1", "CO", "je.ur2", "je.ur2(t, n.bpx_1)", listOf("ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase")), - SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("?dtt", "EnableSetBattleInviteNotifications", "InvitationResponse", "InviteToBattle", "RemoveUserFromInvited", "SelectUserToInvite")), + SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("AddInvitedUser", "ClearSelectedForInvite", "EnableSetBattleInviteNotifications", "InvitationResponse", "RemoveUserFromInvited", "SelectUserToInvite")), SliceWiring("keyboardSettings", "dpx_1", "Sut", "bw.oth", "bw.oth(t, n.dpx_1)", listOf("?Iut", "?_C", "ChangeAction", "RemoveAction", "ResetAction")), SliceWiring("touchscreenHudSettings", "epx_1", "j7", "N7().xsv", "N7().xsv(t, n.epx_1)", listOf("?E7", "?_C", "UpdateSettings")), SliceWiring("mainQuest", "fpx_1", "WZ", "ih.bsk", "ih.bsk(t, n.fpx_1)", listOf("AddRewardedQuest", "QuestsLoaded", "ResetQuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")), SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("SetLanguage")), SliceWiring("launchParams", "hpx_1", "?", "(pass-through)", "n.hpx_1", listOf()), - SliceWiring("partner", "ipx_1", "UJ", "(inline@partner)", "(function (t, n) { var i; if (t instanceof VJ) i = n.osf(t.rsf_1, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ssf_1); else if (t instanceof nZ) { var r = t.bsg_1, e = t.csg_1, u = t.dsg_1, s = t.esg_1, o = t.fsg_1, _ = t.gsg_1; i = n.osf(cd, cd, cd, r, e, u, cd, cd, o, _, s); } else i = t instanceof iZ ? n.osf(cd, t.hsg_1) : t instanceof uZ ? n.osf(cd, cd, t.ksg_1) : t instanceof eZ ? n.osf(cd, cd, cd, cd, cd, cd, cd, t.jsg_1) : t instanceof _Z ? n.osf(cd, cd, cd, cd, cd, cd, t.osg_1) : t instanceof X …", listOf("?JJ", "?_Z", "?hZ", "?tZ", "InitPartnerInviteAction", "InitSocialNetworkParameters", "SetLoginParams", "SetLoginStatusForBinding", "SetPartnerId", "SetPartnerParameters", "SetPartnerReady")), + SliceWiring("partner", "ipx_1", "UJ", "(inline@partner)", "(function (t, n) { var i; if (t instanceof VJ) i = n.osf(t.rsf_1, cd, cd, cd, cd, cd, cd, cd, cd, cd, t.ssf_1); else if (t instanceof nZ) { var r = t.bsg_1, e = t.csg_1, u = t.dsg_1, s = t.esg_1, o = t.fsg_1, _ = t.gsg_1; i = n.osf(cd, cd, cd, r, e, u, cd, cd, o, _, s); } else i = t instanceof iZ ? n.osf(cd, t.hsg_1) : t instanceof uZ ? n.osf(cd, cd, t.ksg_1) : t instanceof eZ ? n.osf(cd, cd, cd, cd, cd, cd, cd, t.jsg_1) : t instanceof _Z ? n.osf(cd, cd, cd, cd, cd, cd, t.osg_1) : t instanceof X …", listOf("InitPartnerInviteAction", "InitSocialNetworkParameters", "MarkAccountLinked", "MarkAccountNotLinked", "SetLoginParams", "SetLoginStatusForBinding", "SetPartnerId", "SetPartnerItemEnabled", "SetPartnerParameters", "SetPartnerReady", "UpdatePartnerInviteAction")), SliceWiring("entrance", "jpx_1", "hD", "Bv.pth", "Bv.pth(t, n.jpx_1)", listOf("HideSocialNetworks", "SetAgeWarnShow", "SetAuthorizedByChinaSdkToken", "SetClassic", "SetControlsForChina", "SetRunningInChinaDataCenter", "SetRunningInChinaPartners")), SliceWiring("antiAddiction", "kpx_1", "UC", "W.iq8", "W.iq8(t, n.kpx_1)", listOf("EnableRealNameCheck", "RequireUserIdVerification", "SetRealNameAndIdNumber", "SetValidateAntiAddictionUserDataStatus", "UpdatePlayingTime")), SliceWiring("spectator", "lpx_1", "HA", "Li.jqm", "Li.jqm(t, n.lpx_1)", listOf("AddSpectator", "RemoveSpectator", "SetSpectator", "SetSpectators", "SetSpectatorsListVisible", "UpdateHudLayout")), @@ -84,7 +84,7 @@ object RootWiring { SliceWiring("rulesUpdated", "npx_1", "i1", "Ch.dsm", "Ch.dsm(t, n.npx_1)", listOf("UpdateRules")), SliceWiring("announcement", "opx_1", "lC", "PC().rq5", "PC().rq5(t, n.opx_1)", listOf("DisableAnnouncement", "SetAnnouncementItems", "SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce")), SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("Init", "MarkHomeScreenDialogShown")), - SliceWiring("userRenameState", "qpx_1", "O8", "Rf.pte", "Rf.pte(t, n.qpx_1)", listOf("?B8", "?F8", "?G8", "?Q8", "?U8", "ChangeUidInputState", "ResponseActual", "ResponseDelivered", "SetChangeUidParams", "SetRubiesForRename", "SetValidationStatus")), + SliceWiring("userRenameState", "qpx_1", "O8", "Rf.pte", "Rf.pte(t, n.qpx_1)", listOf("ChangeUidInputState", "HideLinkErrorWarning", "MarkFreeRenameAvailable", "MarkRenameFailed", "MarkRenameSucceeded", "ResetRenameDialog", "ResponseActual", "ResponseDelivered", "SetChangeUidParams", "SetRubiesForRename", "SetValidationStatus")), SliceWiring("eventGoods", "rpx_1", "W7", "Wc.esx", "Wc.esx(t, n.rpx_1)", listOf("InitEventGoods")), SliceWiring("creatorTagState", "spx_1", "qO", "ze.er2", "ze.er2(t, n.spx_1)", listOf("ClearCreatorTag", "SetAttachedCreatorTag", "SetCreatorTag", "SetCreatorTags")), SliceWiring("twoStepVerificationState", "tpx_1", "x9", "_f.atb", "_f.atb(t, n.tpx_1)", listOf("ActivationStatus2FA", "Enable2FaRequest", "SetSecretAndQRCode", "SetUse2FA", "VerificationStatus")), @@ -106,7 +106,7 @@ object RootWiring { ) /** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */ - val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState") + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState") /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ val Unmigrated: List get() = All.filter { it.Field !in Migrated } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index 95b1b51..5cc5edf 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -9,7 +9,7 @@ // ⑥ 动作目录:816 个类与 138 个真实派发名对齐 // ⑦ 回放:真实 toString 样例的类/字段/类型校验 // ⑧ 全目录动作类都能按 KClass 路由 -// ⑨~㊿/51-63 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / +// ⑨~㊿/51-69 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState / // Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings / // AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests / // LobbyLoader / PurchaseDialogState / PromoCode / SupportUrl / RulesUpdated / NotificationByCountry / AvailabilityByRank / @@ -17,7 +17,8 @@ // TipsState / RankUpDialogState / BattleResult / CaptchaState / ModalScreen / DroppableGold / EventGoods / // CreatorTagState / RewardedVideosState / PasswordChangeState / // System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction / -// Moderation / MigrationState / Register / UserReports; +// Moderation / MigrationState / Register / UserReports / +// Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename; // ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) @@ -534,6 +535,63 @@ import App.State.Reducers.ReduceUserReports import App.State.Reducers.ReportEntry import App.State.Reducers.ReportKind import App.State.Reducers.UserReportsStateSlice +import App.State.ChangeFriendAddFindState +import App.State.AddUsersToAccepted +import App.State.AddUsersToIncoming +import App.State.AddUsersToNewAccepted +import App.State.AddUsersToNewIncoming +import App.State.RemoveUserFromAccepted +import App.State.RemoveUserFromIncoming +import App.State.RemoveUserFromNewAccepted +import App.State.RemoveUsersFromNewIncoming +import App.State.SetLimits +import App.State.SetValidationStatus +import App.State.Reducers.ClearContextMenuUser +import App.State.Reducers.FriendSearchState +import App.State.Reducers.FriendsStateSlice +import App.State.Reducers.ReduceFriends +import App.State.Reducers.SetContextMenuUser +import App.State.Reducers.ReduceSupplies +import App.State.Reducers.SetSupplyCount +import App.State.Reducers.SuppliesStateSlice +import App.State.ClanNotifierData +import App.State.Loaded +import App.State.Reducers.ClanUserInfoStateSlice +import App.State.Reducers.ReduceClanUserInfo +import App.State.EnableSetBattleInviteNotifications +import App.State.InvitationResponse +import App.State.RemoveUserFromInvited +import App.State.SelectUserToInvite +import App.State.Reducers.AddInvitedUser +import App.State.Reducers.BattleInviteStateSlice +import App.State.Reducers.ClearSelectedForInvite +import App.State.Reducers.ReduceBattleInvite +import App.State.InitPartnerInviteAction +import App.State.InitSocialNetworkParameters +import App.State.SetLoginParams +import App.State.SetLoginStatusForBinding +import App.State.SetPartnerId +import App.State.SetPartnerParameters +import App.State.SetPartnerReady +import App.State.Reducers.MarkAccountLinked +import App.State.Reducers.MarkAccountNotLinked +import App.State.Reducers.PartnerStateSlice +import App.State.Reducers.ReducePartner +import App.State.Reducers.SetPartnerItemEnabled +import App.State.Reducers.UpdatePartnerInviteAction +import App.State.ChangeUidInputState +import App.State.ResponseActual +import App.State.ResponseDelivered +import App.State.SetChangeUidParams +import App.State.SetRubiesForRename +import App.State.Reducers.HideLinkErrorWarning +import App.State.Reducers.MarkFreeRenameAvailable +import App.State.Reducers.MarkRenameFailed +import App.State.Reducers.MarkRenameSucceeded +import App.State.Reducers.ReduceUserRename +import App.State.Reducers.ResetRenameDialog +import App.State.Reducers.RenameUidState +import App.State.Reducers.UserRenameStateSlice import App.State.Reducers.UserRankBounds import App.State.Reducers.UserStateSlice import App.State.Reducers.ClearHullData @@ -961,7 +1019,7 @@ object StateCheck { Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled) } - // ⑭ 根 reducer:89 个槽位的委派表 + 53 个已迁移切片(52 归约 + launchParams 透传) + // ⑭ 根 reducer:89 个槽位的委派表 + 59 个已迁移切片(58 归约 + launchParams 透传) run { Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) Check( @@ -1025,7 +1083,7 @@ object StateCheck { LocalBattleW.Reducer == "(inline@localBattleUserState)" ) Check( - "根:迁移表 = 53 个已迁移字段,其余 36 个待迁", + "根:迁移表 = 59 个已迁移字段,其余 30 个待迁", RootReducer.Migrated == setOf( "inviteState", "loginByPassword", "twitchState", "home", @@ -1042,8 +1100,10 @@ object StateCheck { "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", + "friends", "suppliesState", "clanUserInfo", "battleInviteState", + "partner", "userRenameState", ) && - RootWiring.Unmigrated.size == 36 + RootWiring.Unmigrated.size == 30 ) // 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片 @@ -1099,7 +1159,7 @@ object StateCheck { S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader") ) Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5) - Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 53/89") + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 59/89") val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) Store.Dispatch(SetStatus("LOGGING_IN")) @@ -4815,6 +4875,364 @@ object StateCheck { ) } + // 64 切片 reducer:Friends(KD / NL().cr9,8 字段;13 个动作) + run { + val S0 = FriendsStateSlice() + Check( + "切片 Friends:默认值完整", + S0.UserSearchState == FriendSearchState.HIDDEN && S0.ContextMenuUser == null && + S0.AcceptedLimit == 0 && S0.IncomingLimit == 0 && + S0.Accepted.isEmpty() && S0.NewAccepted.isEmpty() && + S0.Incoming.isEmpty() && S0.NewIncoming.isEmpty() + ) + val S1 = ReduceFriends(S0, ChangeFriendAddFindState(FriendSearchState.SEARCHING)) + Check( + "切片 Friends:ChangeFriendAddFindState 写搜索状态", + S1.UserSearchState == FriendSearchState.SEARCHING + ) + Check( + "切片 Friends:SetValidationStatus 命中即 FOUND,null 即 NOT_FOUND", + ReduceFriends(S1, SetValidationStatus("ok")).UserSearchState == + FriendSearchState.FOUND && + ReduceFriends(S1, SetValidationStatus(null)).UserSearchState == + FriendSearchState.NOT_FOUND + ) + Check( + "切片 Friends:SetLimits 写两个上限", + ReduceFriends(S1, SetLimits(50, 30)).let { + it.AcceptedLimit == 50 && it.IncomingLimit == 30 + } + ) + Check( + "切片 Friends:右键菜单设置 / 清除", + ReduceFriends(S1, SetContextMenuUser("u1")).ContextMenuUser == "u1" && + ReduceFriends(S1, ClearContextMenuUser).ContextMenuUser == null + ) + val S2 = ReduceFriends(S1, AddUsersToAccepted(listOf(1L, 2L))) + Check( + "切片 Friends:AddUsersToAccepted 整批入集合", + S2.Accepted == setOf(1L, 2L) + ) + Check( + "切片 Friends:RemoveUserFromAccepted 删单值", + ReduceFriends(S2, RemoveUserFromAccepted(1L)).Accepted == setOf(2L) + ) + val S3 = ReduceFriends(S2, AddUsersToNewAccepted(clanId = 0, userIds = listOf(3L, 4L))) + Check( + "切片 Friends:AddUsersToNewAccepted 读 userIds", + S3.NewAccepted == setOf(3L, 4L) + ) + Check( + "切片 Friends:RemoveUserFromNewAccepted / AddUsersToIncoming", + ReduceFriends(S3, RemoveUserFromNewAccepted(3L)).NewAccepted == setOf(4L) && + ReduceFriends(S3, AddUsersToIncoming(listOf(5L))).Incoming == setOf(5L) + ) + Check( + "切片 Friends:RemoveUsersFromNewIncoming 按集合减", + ReduceFriends( + ReduceFriends(S3, AddUsersToNewIncoming(listOf(6L, 7L))), + RemoveUsersFromNewIncoming(listOf(6L)), + ).NewIncoming == setOf(7L) + ) + Check( + "切片 Friends:无关动作不改状态", + ReduceFriends(S3, AddFrags(1)) == S3 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, ChangeFriendAddFindState(FriendSearchState.FOUND)) + Check( + "根:ChangeFriendAddFindState 只动 friends", + Root1.Friends.UserSearchState == FriendSearchState.FOUND && + Root1.Partner == Root0.Partner + ) + val W = RootWiring.ByField("friends")!! + Check( + "根:friends 委派到 KD / NL().cr9 且 13 个动作", + W.SliceClass == "KD" && W.Reducer == "NL().cr9" && W.Actions.size == 13 + ) + } + + // 65 切片 reducer:Supplies(wE / $t.fq9,1 字段;SetSupplyCount) + run { + val S0 = SuppliesStateSlice() + Check("切片 Supplies:默认空 Map", S0.Count.isEmpty()) + val S1 = ReduceSupplies(S0, SetSupplyCount("mine", 3)) + Check( + "切片 Supplies:SetSupplyCount 写键值", + S1.Count == mapOf("mine" to 3) + ) + Check( + "切片 Supplies:再次写入覆盖旧值", + ReduceSupplies(S1, SetSupplyCount("mine", 1)).Count == + mapOf("mine" to 1) + ) + Check("切片 Supplies:无关动作不改状态", ReduceSupplies(S1, AddFrags(1)) == S1) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetSupplyCount("k", 9)) + Check( + "根:SetSupplyCount 只动 suppliesState", + Root1.SuppliesState.Count == mapOf("k" to 9) && + Root1.ClanUserInfo == Root0.ClanUserInfo + ) + val W = RootWiring.ByField("suppliesState")!! + Check( + "根:suppliesState 委派到 wE / \$t.fq9", + W.SliceClass == "wE" && W.Reducer == "\$t.fq9" && + W.Actions == listOf("SetSupplyCount") + ) + } + + // 66 切片 reducer:ClanUserInfo(hO / de.gr1,1 字段;Loaded 合并) + run { + val S0 = ClanUserInfoStateSlice() + Check("切片 ClanUserInfo:默认空 Map", S0.Users.isEmpty()) + val S1 = ReduceClanUserInfo(S0, Loaded(mapOf("u1" to ClanNotifierData(1L, null, 11L)))) + Check( + "切片 ClanUserInfo:Loaded 写入用户表", + S1.Users.keys == setOf("u1") && + (S1.Users["u1"] as ClanNotifierData).userId == 11L + ) + Check( + "切片 ClanUserInfo:第二次 Loaded 是合并不是覆盖", + ReduceClanUserInfo(S1, Loaded(mapOf("u2" to ClanNotifierData(2L, null, 22L)))).Users.keys == + setOf("u1", "u2") + ) + Check( + "切片 ClanUserInfo:无关动作不改状态", + ReduceClanUserInfo(S1, AddFrags(1)) == S1 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, Loaded(mapOf("x" to ClanNotifierData(3L, null, 33L)))) + Check( + "根:Loaded 只动 clanUserInfo", + Root1.ClanUserInfo.Users.keys == setOf("x") && + Root1.SuppliesState == Root0.SuppliesState + ) + val W = RootWiring.ByField("clanUserInfo")!! + Check( + "根:clanUserInfo 委派到 hO / de.gr1", + W.SliceClass == "hO" && W.Reducer == "de.gr1" && + W.Actions == listOf("Loaded") + ) + } + + // 67 切片 reducer:BattleInvite(att / Ttt().nth,3 字段;6 个动作) + run { + val S0 = BattleInviteStateSlice() + Check( + "切片 BattleInvite:默认开启、两个集合为空", + S0.IsEnabled && S0.InvitedUsers.isEmpty() && S0.SelectedForInvite.isEmpty() + ) + val S1 = ReduceBattleInvite(S0, AddInvitedUser(1L)) + Check("切片 BattleInvite:AddInvitedUser 入已邀请集合", S1.InvitedUsers == setOf(1L)) + Check( + "切片 BattleInvite:InvitationResponse 从已邀请集合删", + ReduceBattleInvite(S1, InvitationResponse(1L, "ok")).InvitedUsers.isEmpty() + ) + val S2 = ReduceBattleInvite(S1, SelectUserToInvite(2L)) + Check( + "切片 BattleInvite:SelectUserToInvite 入待邀请集合", + S2.SelectedForInvite == setOf(2L) + ) + Check( + "切片 BattleInvite:RemoveUserFromInvited 从待邀请集合删", + ReduceBattleInvite(S2, RemoveUserFromInvited(2L)).SelectedForInvite.isEmpty() + ) + Check( + "切片 BattleInvite:ClearSelectedForInvite 清空", + ReduceBattleInvite(S2, ClearSelectedForInvite).SelectedForInvite.isEmpty() + ) + Check( + "切片 BattleInvite:EnableSetBattleInviteNotifications 写开关", + !ReduceBattleInvite(S2, EnableSetBattleInviteNotifications(false)).IsEnabled + ) + Check( + "切片 BattleInvite:无关动作不改状态", + ReduceBattleInvite(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, AddInvitedUser(9L)) + Check( + "根:AddInvitedUser 只动 battleInviteState", + Root1.BattleInviteState.InvitedUsers == setOf(9L) && + Root1.UserRenameState == Root0.UserRenameState + ) + val W = RootWiring.ByField("battleInviteState")!! + Check( + "根:battleInviteState 委派到 att / Ttt().nth 且 6 个动作", + W.SliceClass == "att" && W.Reducer == "Ttt().nth" && + W.Actions.toSet() == setOf( + "AddInvitedUser", "InvitationResponse", "SelectUserToInvite", + "RemoveUserFromInvited", "ClearSelectedForInvite", + "EnableSetBattleInviteNotifications", + ) + ) + } + + // 68 切片 reducer:Partner(UJ / (inline@partner),15 字段;11 个动作) + run { + val S0 = PartnerStateSlice() + Check( + "切片 Partner:默认值完整", + S0.PartnerId == null && S0.LoginParams.isEmpty() && S0.Params.isEmpty() && + !S0.HasOwnPaymentSystem && !S0.HasSocialFunction && + !S0.HasClientPaymentAction && S0.PaymentAction == null && + S0.InviteAction == null && !S0.CanOpenExternalLinks && + !S0.HasAccountBinding && S0.FailedRedirectUrl == null && + S0.LoginForBindingStatus == ByPasswordStatus.NONE && + !S0.IsAccountAlreadyLinked && S0.ItemsEnabledForPartner.isEmpty() && + !S0.IsPartnerReady + ) + val S1 = ReducePartner(S0, SetPartnerId("p1", "https://fail")) + Check( + "切片 Partner:SetPartnerId 同时写失败跳转", + S1.PartnerId == "p1" && S1.FailedRedirectUrl == "https://fail" + ) + val S2 = ReducePartner( + S1, + InitSocialNetworkParameters( + hasOwnPaymentSystem = true, hasSocialFunction = true, + hasClientPaymentAction = false, failedRedirectUrl = null, + canOpenExternalLinks = true, hasAccountBinding = true, + ), + ) + Check( + "切片 Partner:InitSocialNetworkParameters 写 6 个字段", + S2.HasOwnPaymentSystem && S2.HasSocialFunction && !S2.HasClientPaymentAction && + S2.CanOpenExternalLinks && S2.HasAccountBinding + ) + Check( + "切片 Partner:loginParams / params 分别写入", + ReducePartner(S2, SetLoginParams(mapOf("a" to 1))).LoginParams == + mapOf("a" to 1) && + ReducePartner(S2, SetPartnerParameters(mapOf("b" to 2))).Params == + mapOf("b" to 2) + ) + Check( + "切片 Partner:两种邀请动作都写 inviteAction", + ReducePartner(S2, InitPartnerInviteAction("i1")).InviteAction == "i1" && + ReducePartner(S2, UpdatePartnerInviteAction("i2")).InviteAction == "i2" + ) + Check( + "切片 Partner:SetLoginStatusForBinding 写绑定状态", + ReducePartner(S2, SetLoginStatusForBinding(ByPasswordStatus.WRONG_PASSWORD)) + .LoginForBindingStatus == ByPasswordStatus.WRONG_PASSWORD + ) + Check( + "切片 Partner:已绑定标记两态", + ReducePartner(S2, MarkAccountLinked).IsAccountAlreadyLinked && + !ReducePartner(S2, MarkAccountNotLinked).IsAccountAlreadyLinked + ) + Check( + "切片 Partner:SetPartnerItemEnabled 写物品开关 Map", + ReducePartner(S2, SetPartnerItemEnabled("item", true)) + .ItemsEnabledForPartner == mapOf("item" to true) + ) + Check( + "切片 Partner:SetPartnerReady 写就绪位", + ReducePartner(S2, SetPartnerReady(true)).IsPartnerReady + ) + Check("切片 Partner:无关动作不改状态", ReducePartner(S2, AddFrags(1)) == S2) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetPartnerReady(true)) + Check( + "根:SetPartnerReady 只动 partner", + Root1.Partner.IsPartnerReady && Root1.Friends == Root0.Friends + ) + val W = RootWiring.ByField("partner")!! + Check( + "根:partner 是内联 reducer 且 11 个动作", + W.SliceClass == "UJ" && W.Reducer == "(inline@partner)" && W.Actions.size == 11 + ) + } + + // 69 切片 reducer:UserRename(O8 / Rf.pte,10 字段;11 个动作) + run { + val S0 = UserRenameStateSlice() + Check( + "切片 UserRename:默认值完整", + !S0.IsFreeRenameAvailable && S0.UserRenameShopItemId == null && + S0.UidInputState == RenameUidState.EMPTY && S0.RenameSuccessful == null && + S0.RubiesForRename == Int.MAX_VALUE && !S0.IsResponseDelivered && + !S0.IsResponseActual && S0.ChangeUidHash == null && S0.Email == null && + S0.ShowLinkErrorWarning + ) + val S1 = ReduceUserRename(S0, ChangeUidInputState(RenameUidState.SHORT)) + Check( + "切片 UserRename:ChangeUidInputState 写输入状态", + S1.UidInputState == RenameUidState.SHORT + ) + Check( + "切片 UserRename:免费改名 / 改名成功 / 改名失败", + ReduceUserRename(S1, MarkFreeRenameAvailable).IsFreeRenameAvailable && + ReduceUserRename(S1, MarkRenameSucceeded).let { + !it.IsFreeRenameAvailable && it.RenameSuccessful == true + } && + ReduceUserRename(S1, MarkRenameFailed).RenameSuccessful == false + ) + Check( + "切片 UserRename:SetValidationStatus 按 ordinal 映射输入状态", + ReduceUserRename(S1, SetValidationStatus(RenameUidState.FREE)).let { + it.UidInputState == RenameUidState.FREE && it.IsResponseDelivered + } && + ReduceUserRename(S1, SetValidationStatus(RenameUidState.SHORT)).UidInputState == + RenameUidState.NOT_VALID && + ReduceUserRename(S1, SetValidationStatus(RenameUidState.NOT_VALID)) + .UidInputState == RenameUidState.FORBIDDEN + ) + Check( + "切片 UserRename:SetRubiesForRename 写价格", + ReduceUserRename(S1, SetRubiesForRename(99)).RubiesForRename == 99 + ) + val S2 = ReduceUserRename( + ReduceUserRename( + ReduceUserRename(S1, SetChangeUidParams("hash", "mail")), + ResponseDelivered(true), + ), + ResponseActual(true), + ) + Check( + "切片 UserRename:改绑参数与两个 response 位", + S2.ChangeUidHash == "hash" && S2.Email == "mail" && + S2.IsResponseDelivered && S2.IsResponseActual + ) + Check( + "切片 UserRename:ResetRenameDialog 复位弹窗字段", + ReduceUserRename(S2, ResetRenameDialog).let { + it.UserRenameShopItemId == null && + it.UidInputState == RenameUidState.EMPTY && + it.RenameSuccessful == null && !it.IsResponseDelivered && + !it.IsResponseActual && it.ChangeUidHash == "hash" + } + ) + Check( + "切片 UserRename:HideLinkErrorWarning 关提示", + !ReduceUserRename(S2, HideLinkErrorWarning).ShowLinkErrorWarning + ) + Check( + "切片 UserRename:无关动作不改状态", + ReduceUserRename(S2, AddFrags(1)) == S2 + ) + + val Root0 = RootState() + val Root1 = ReduceRoot(Root0, SetRubiesForRename(5)) + Check( + "根:SetRubiesForRename 只动 userRenameState", + Root1.UserRenameState.RubiesForRename == 5 && + Root1.Partner == Root0.Partner + ) + val W = RootWiring.ByField("userRenameState")!! + Check( + "根:userRenameState 委派到 O8 / Rf.pte 且 11 个动作", + W.SliceClass == "O8" && W.Reducer == "Rf.pte" && W.Actions.size == 11 + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)