diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 1ded789..4201602 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -138,6 +138,20 @@ - 模块:10032(Redux 核心)、18334(DI 装配)、61860(连接与对象模型)、13431(启动)、52943(工具库)、26482(SW 注册) - **目标**:新工程能完成"启动 → 连接 → 进入大厅"。 - **验收**:L2 的一半(登录 + 大厅)。 +- **进度(Kotlin/JS,2026-10)**: + - 根状态 / 委派表由 `3DTank/tools/gen_root_wiring.js` 从当前 `module_10032.js` 现场解析: + `names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。 + - 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。 + - 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。 + - 已迁移切片 **3/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) + - `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 只归约上述三片,其余 **86 个字段原样透传**; + 迁移进度用 `RootReducer.Coverage`(`已迁移 3/89`)与 `RootWiring.Unmigrated` 暴露。 + - 自检:`TankiOffline/tools/RunStateTest.ps1` **61 项全绿**,含 89 槽位完整性、三片一次归约互不串片、未迁移字段透传。 + - 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE(记为 `(inline)`,动作名暂缺); + `launchParams` 是纯透传(`(pass-through)`,无 reducer)。 ### S4 引擎核心 - 模块:32088(3D 数学)、92980(渲染)、61378(ECS)、55088(物理)、36659 + 71105(资源)、36734(音频)、3569(输入) diff --git a/3DTank/docs/StateMap.md b/3DTank/docs/StateMap.md index 53a203b..cddc259 100644 --- a/3DTank/docs/StateMap.md +++ b/3DTank/docs/StateMap.md @@ -1,5 +1,10 @@ # TOState 字段 ↔ reducer 映射表 +> **版本说明(2026-10)**:本文依据旧版 `names/tostate-map.json`(根状态 `w8` / 根 reducer `v8`、 +> 槽位 `app_1` / `bpp_1`)写成,与当前 `src/module_10032.js`(根状态 `z6` / `TOState`、根 reducer `k6`、 +> 槽位 `<名>_1`)符号名不同,**只用于交叉验证动作归属**。当前源码口径以 `3DTank/tools/gen_root_wiring.js` +> 现场解析出的 `names/root_wiring.json` 与 Kotlin 侧 `App/State/RootWiring.kt` 为准。 + 本文是 `names/tostate-map.json` 的人类可读版,用途:**重写客户端时对照根状态的每个分片该由哪个 reducer / 哪些 action 驱动**。 ## 1. TOState 是什么 diff --git a/3DTank/names/root_wiring.json b/3DTank/names/root_wiring.json new file mode 100644 index 0000000..96d75e4 --- /dev/null +++ b/3DTank/names/root_wiring.json @@ -0,0 +1,1641 @@ +{ + "generatedAt": "2026-10-22T16:35:51.231Z", + "module": "module_10032.js", + "RootState": { + "Class": "z6", + "Reducer": "k6", + "FieldCount": 89 + }, + "Migrated": [ + "inviteState", + "loginByPassword", + "twitchState" + ], + "Rows": [ + { + "Field": "user", + "Slot": "ypu_1", + "SliceClass": "t8", + "Reducer": "M8().qtd", + "Delegate": "M8().qtd(t, n.ypu_1)", + "Actions": [ + "?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" + ], + "Default": "new t8()", + "Migrated": false + }, + { + "Field": "screen", + "Slot": "zpu_1", + "SliceClass": "WK", + "Reducer": "dY().ks5", + "Delegate": "dY().ks5(t, n.zpu_1)", + "Actions": [ + "?LX", + "?RX", + "?cY", + "?eY", + "?fY", + "?lY", + "?oC", + "?oY", + "?sC", + "?sY", + "?uY", + "DisplayMainEntranceScreen", + "DisplayReauthorizationMessage", + "GoTo", + "PressedAnyButton" + ], + "Default": "new WK(PX())", + "Migrated": false + }, + { + "Field": "modalScreen", + "Slot": "apv_1", + "SliceClass": "nV", + "Reducer": "Fs.js1", + "Delegate": "Fs.js1(t, n.apv_1)", + "Actions": [ + "?XW", + "AddModal", + "RemoveCurrentByName", + "RemoveModal" + ], + "Default": "new nV()", + "Migrated": false + }, + { + "Field": "battleStatistics", + "Slot": "bpv_1", + "SliceClass": "jR", + "Reducer": "rr.fqr", + "Delegate": "rr.fqr(t, n.bpv_1)", + "Actions": [ + "?DR", + "?HR", + "?KR", + "?PR", + "?QR", + "?UR", + "?XR", + "?YR", + "?ZR", + "?nP", + "?tP", + "Init", + "InitBattleModeWithLevels", + "SetBattleMode", + "SetDisplayClientInfo", + "SetFlagOrBallOwner", + "SetRankRange", + "SetReArmor", + "SetRemainingTime", + "SetRoundRestartTime", + "UpdateFund", + "UpdateScore" + ], + "Default": "new jR()", + "Migrated": false + }, + { + "Field": "battleTutorial", + "Slot": "cpv_1", + "SliceClass": "rE", + "Reducer": "Q.sq8", + "Delegate": "Q.sq8(t, n.cpv_1)", + "Actions": [ + "?oE", + "?sE", + "SetFragsCount", + "ShowMessage" + ], + "Default": "new rE()", + "Migrated": false + }, + { + "Field": "garage", + "Slot": "dpv_1", + "SliceClass": "UB", + "Reducer": "hF", + "Delegate": "hF(t, n)", + "Actions": [ + "?EU", + "?FB", + "?FX", + "?JB", + "?MU", + "?NU", + "?OU", + "?PU", + "?QU", + "?RU", + "?SU", + "?VB", + "?WB", + "?aF", + "?bU", + "?cF", + "?dU", + "?eF", + "?fF", + "?iU", + "?lF", + "?pF", + "?rF", + "?vF", + "?wU", + "?yU", + "AddOverdriveDescription", + "AddRelatedBazooka", + "AddRelatedResistanceModule", + "AddToFavoriteItem", + "BuyProtectionSlot", + "Garage", + "GarageAddItemToDepot", + "GarageApplyResistanceMount", + "GarageBuyDevice", + "GarageDeviceTrySetInfinityLifetime", + "GarageDevicesLoadedAction", + "GarageRemoveDevice", + "GarageRemoveDiscountForDevice", + "GarageRemoveItemFromDepot", + "GarageRemoveItemFromStore", + "GarageResistanceUnMount", + "GarageSelectAlteration", + "GarageSelectItem", + "GarageSelectShotSkin", + "GarageSelectSkin", + "GarageUpdateItemCount", + "GarageUpgradeItemAction", + "GarageUpgradeItemConfirm", + "GarageUpgradeItemCoolDownFinished", + "GarageUpgradeItemCoolDownStarted", + "GoTo", + "InitState", + "RemoveFromFavoriteItem", + "RemoveFromSale", + "SetGarageSkinsAction", + "SetItemRarity", + "SetRestrictionMount", + "SwitchingRowsScroll", + "UpdateDiscount" + ], + "Default": "new UB()", + "Migrated": false + }, + { + "Field": "garagePreview", + "Slot": "epv_1", + "SliceClass": "IG", + "Reducer": "fs.eru", + "Delegate": "fs.eru(t, n.epv_1)", + "Actions": [ + "?BG", + "?JG", + "?KG", + "?OG", + "?QG", + "?UG", + "?VG", + "?XG", + "?YG", + "?ZG", + "?nH", + "?tH", + "AddRequestedItem", + "InitGarageResources", + "ResetItem3dModelActionCache", + "SetCaptureActions", + "SetDrone", + "SetExpectedItemId", + "SetMountedItemsProperties", + "SetPaint", + "ShowRequest", + "UpdatePreviewSettings" + ], + "Default": "new IG()", + "Migrated": false + }, + { + "Field": "challenges", + "Slot": "fpv_1", + "SliceClass": "CZ", + "Reducer": "nh.hsj", + "Delegate": "nh.hsj(t, n.fpv_1)", + "Actions": [ + "?LZ", + "RemoveChallenge", + "SetBattlePass", + "SetBattlePassPackages", + "SetChallenge", + "SetRewardForDialog", + "SetTakenBattlePassRewardIndex", + "SetTakenFreeRewardIndex", + "UpdateStars" + ], + "Default": "new CZ()", + "Migrated": false + }, + { + "Field": "loginByPassword", + "Slot": "gpv_1", + "SliceClass": "gD", + "Reducer": "nu.cr6", + "Delegate": "nu.cr6(t, n.gpv_1)", + "Actions": [ + "?ND", + "?PD", + "?RD", + "IncorrectLogin2FA", + "RequestsNumber2FA", + "SetLogin", + "SetPassword", + "SetRememberMe", + "SetStatus" + ], + "Default": "new gD()", + "Migrated": true + }, + { + "Field": "register", + "Slot": "hpv_1", + "SliceClass": "Cit", + "Reducer": "cp.stg", + "Delegate": "cp.stg(t, n.hpv_1)", + "Actions": [ + "?Bit", + "?Dit", + "?Fit", + "?Git", + "?Hit", + "?Lit", + "?Mit", + "?Uit", + "?_rt", + "?hrt", + "EnteredUidIsBusy", + "ForceSetUid", + "ReceiveOAuthUri", + "SetOpenedPassword", + "SetPassword", + "SetPasswordRestrictions", + "SetUid", + "SocialNetworkAvailability", + "SocialNetworkClientIds" + ], + "Default": "new Cit()", + "Migrated": false + }, + { + "Field": "migrationState", + "Slot": "ipv_1", + "SliceClass": "$it", + "Reducer": "Uv.rtg", + "Delegate": "Uv.rtg(t, n.ipv_1)", + "Actions": [ + "?Bit", + "?Dit", + "?Fit", + "?Git", + "?Hit", + "?Lit", + "?_rt", + "EnteredUidIsBusy", + "ForceSetUid", + "SetPassword", + "SetUid" + ], + "Default": "new $it()", + "Migrated": false + }, + { + "Field": "inviteState", + "Slot": "jpv_1", + "SliceClass": "aD", + "Reducer": "Ke.er5", + "Delegate": "Ke.er5(t, n.jpv_1)", + "Actions": [ + "ApplyInvite", + "SetInviteNeed", + "SetInviteStatus" + ], + "Default": "new aD()", + "Migrated": true + }, + { + "Field": "captchaState", + "Slot": "kpv_1", + "SliceClass": "jP", + "Reducer": "ar.iqt", + "Delegate": "ar.iqt(t, n.kpv_1)", + "Actions": [ + "?AP", + "?IP", + "?PP", + "?RP", + "?SP", + "LocationsUpdated", + "SetLocations" + ], + "Default": "new jP()", + "Migrated": false + }, + { + "Field": "passwordChangeState", + "Slot": "lpv_1", + "SliceClass": "VD", + "Reducer": "uu.pr6", + "Delegate": "uu.pr6(t, n.lpv_1)", + "Actions": [ + "SetPasswordChangeHash", + "ShowEmailAndPasswordChangeDialog" + ], + "Default": "new VD()", + "Migrated": false + }, + { + "Field": "system", + "Slot": "mpv_1", + "SliceClass": "_9", + "Reducer": "w9().xt9", + "Delegate": "w9().xt9(t, n.mpv_1)", + "Actions": [ + "DoShowMessage", + "SetFatalError" + ], + "Default": "new _9()", + "Migrated": false + }, + { + "Field": "quests", + "Slot": "npv_1", + "SliceClass": "Frt", + "Reducer": "Yrt.otg", + "Delegate": "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", + "Actions": [ + "?Hrt", + "GoTo", + "QuestCategory", + "SetCurrentCategory" + ], + "Default": "new Frt()", + "Migrated": false + }, + { + "Field": "chatState", + "Slot": "opv_1", + "SliceClass": "LP", + "Reducer": "$r.wqv", + "Delegate": "$r.wqv(t, n.opv_1)", + "Actions": [ + "?QP", + "?XP", + "?_C", + "?bN", + "?fN", + "?lN", + "?nN", + "?pN", + "?qN", + "?sN", + "?vN", + "?wN", + "ChangeChannel", + "CleanUsersMessage", + "Configure", + "IgnoreUserMessages", + "SaveChatMessage", + "SendMessageToServer", + "SetMessages", + "SetSelectedUid", + "ShowContextMenu", + "ShowUserMessages", + "UpdateTypingSpeedAntifloodParams", + "UpdateUsers" + ], + "Default": "new LP()", + "Migrated": false + }, + { + "Field": "clanChatState", + "Slot": "ppv_1", + "SliceClass": "qnt", + "Reducer": "yv.qtg", + "Delegate": "yv.qtg(t, n.ppv_1)", + "Actions": [ + "?$nt", + "?Tnt", + "?xnt", + "Configure", + "DoAddMessages" + ], + "Default": "new qnt()", + "Migrated": false + }, + { + "Field": "clan", + "Slot": "qpv_1", + "SliceClass": "jN", + "Reducer": "we.hr0", + "Delegate": "we.hr0(t, n.qpv_1)", + "Actions": [ + "?FM", + "?IM", + "?KM", + "?PM", + "?RM", + "?VM", + "?YM", + "?iM", + "AcceptedUserViewed", + "AddIncomingClan", + "AddNewIncoming", + "AddNewIncomingClan", + "AddOutgoingClan", + "AddUsersToAccepted", + "AddUsersToNewAccepted", + "ClanBanned", + "ClanInfoLoaded", + "ClanLoaded", + "ClanModalVisible", + "IncomingAdded", + "IncomingRemoved", + "IncomingUserViewed", + "MembersFetched", + "NewAcceptedUserNotification", + "NewClanInviteNotification", + "NewIncomingUserNotification", + "OutgoingAdded", + "OutgoingRemoved", + "RemoveIncomingClan", + "RemoveNewIncoming", + "RemoveNewIncomingClan", + "RemoveOutgoingClan", + "RemoveUserFromAccepted", + "RemoveUsersFromNewAccepted", + "SetClanEditPrice", + "SetMinRankForCreate", + "UpdateClanLicense", + "UpdateClanNameValidation", + "UpdateClanSearchValidation", + "UpdateClanTagValidation", + "UpdateHasClan", + "UpdateTimeToJoinRestriction", + "UpdateValidation" + ], + "Default": "new jN()", + "Migrated": false + }, + { + "Field": "clansStorage", + "Slot": "rpv_1", + "SliceClass": "lO", + "Reducer": "me.vr1", + "Delegate": "me.vr1(t, n.rpv_1)", + "Actions": [ + "?mO", + "AddUser", + "AddUsersToNewAccepted", + "BuyAdditionalPlace", + "ClanLoaded", + "ClanUnloaded", + "ForeignClanLoaded", + "MinimalRankChanged", + "RecruitmentChanged", + "RemoveUser", + "RemoveUsersFromNewAccepted", + "ShowForeignClan", + "UpdateClan" + ], + "Default": "new lO()", + "Migrated": false + }, + { + "Field": "tipsState", + "Slot": "spv_1", + "SliceClass": "dst", + "Reducer": "Hw.vth", + "Delegate": "Hw.vth(t, n.spv_1)", + "Actions": [ + "UpdateTips" + ], + "Default": "new dst()", + "Migrated": false + }, + { + "Field": "news", + "Slot": "tpv_1", + "SliceClass": "ZX", + "Reducer": "E_.asd", + "Delegate": "E_.asd(t, n.tpv_1)", + "Actions": [ + "?hJ", + "AddNewsItemAction", + "RemoveNewsItemAction", + "SetNewsItemAction", + "SetUnreadNewsCounter" + ], + "Default": "new ZX()", + "Migrated": false + }, + { + "Field": "settings", + "Slot": "upv_1", + "SliceClass": "Uut", + "Reducer": "xw.ltg", + "Delegate": "xw.ltg(t, n.upv_1)", + "Actions": [ + "?Fut", + "?Gut" + ], + "Default": "new Uut()", + "Migrated": false + }, + { + "Field": "accountSettings", + "Slot": "vpv_1", + "SliceClass": "oet", + "Reducer": "Lp.mtg", + "Delegate": "Lp.mtg(t, n.vpv_1)", + "Actions": [ + "?aet", + "?cet", + "?det", + "?fet", + "?get", + "?ket", + "?met", + "?pet", + "?vet", + "?wet", + "?xet", + "ClearInputUid", + "SetAccountRemovalDays" + ], + "Default": "new oet()", + "Migrated": false + }, + { + "Field": "lootBoxes", + "Slot": "wpv_1", + "SliceClass": "aH", + "Reducer": "ws.wrv", + "Delegate": "ws.wrv(t, n.wpv_1)", + "Actions": [ + "?CH", + "?SH", + "?gH", + "?kH", + "?mH", + "?zH", + "AddKeys", + "SelectCategory", + "SetContainerData", + "SetCurrentContainer", + "SetGoToGarage", + "SetIsAccelerated", + "SetIsOpening", + "SetIsShopVisited", + "SetKeys", + "SetPossibleRewards", + "SetRewards", + "ShownRewards" + ], + "Default": "new aH()", + "Migrated": false + }, + { + "Field": "notifications", + "Slot": "xpv_1", + "SliceClass": null, + "Reducer": "B_.ise", + "Delegate": "B_.ise(t, n.xpv_1)", + "Actions": [ + "?PJ", + "?RJ", + "AddNotification", + "InitNotificationSound" + ], + "Default": "", + "Migrated": false + }, + { + "Field": "friends", + "Slot": "ypv_1", + "SliceClass": "KD", + "Reducer": "NL().cr9", + "Delegate": "NL().cr9(t, n.ypv_1)", + "Actions": [ + "?_L", + "?hL", + "AddUsersToAccepted", + "AddUsersToIncoming", + "AddUsersToNewAccepted", + "AddUsersToNewIncoming", + "ChangeFriendAddFindState", + "RemoveUserFromAccepted", + "RemoveUserFromIncoming", + "RemoveUserFromNewAccepted", + "RemoveUsersFromNewIncoming", + "SetLimits", + "SetValidationStatus" + ], + "Default": "new KD()", + "Migrated": false + }, + { + "Field": "remoteUserDataStorage", + "Slot": "zpv_1", + "SliceClass": "$Q", + "Reducer": "vh.isl", + "Delegate": "vh.isl(t, n.zpv_1)", + "Actions": [ + "UpdateClanTags", + "UpdateOnlineStatus", + "UpdatePremiumStatus", + "UpdateRanks", + "UpdateUids", + "UserEnterBattle", + "UserLeaveBattle", + "UserLeaveGroup" + ], + "Default": "new $Q()", + "Migrated": false + }, + { + "Field": "battlesSelect", + "Slot": "apw_1", + "SliceClass": "fA", + "Reducer": "Mi.vqk", + "Delegate": "Mi.vqk(t, n.apw_1)", + "Actions": [ + "?dA", + "?gA", + "AddBattle", + "AddUser", + "AddUsers", + "ChangeName", + "EnterToBattleInfo", + "InitProBattleRewardLimit", + "LimitReached", + "RemoveBattle", + "RemoveUser", + "RoundFinish", + "RoundStarted", + "SetReEnterProtectBattleId", + "SwapTeam", + "UpdateFund", + "UpdateSuspicion", + "UpdateTeamScore", + "UpdateUserScore", + "UpdateUserSuspicion" + ], + "Default": "new fA()", + "Migrated": false + }, + { + "Field": "maps", + "Slot": "bpw_1", + "SliceClass": "OA", + "Reducer": "Oi.sql", + "Delegate": "Oi.sql(t, n.bpw_1)", + "Actions": [ + "AddMap" + ], + "Default": "new OA()", + "Migrated": false + }, + { + "Field": "matchmaking", + "Slot": "cpw_1", + "SliceClass": "TH", + "Reducer": "JH().qrx", + "Delegate": "JH().qrx(t, n.cpw_1)", + "Actions": [ + "?WH", + "?nW", + "?tW", + "Configure", + "EnterToBattleApply", + "Play", + "SelectMatchmakingMode", + "SetRankIndexToMinGS", + "WasRegistered" + ], + "Default": "new TH()", + "Migrated": false + }, + { + "Field": "battleCreateState", + "Slot": "dpw_1", + "SliceClass": "$6", + "Reducer": "Df.ttg", + "Delegate": "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", + "Actions": [ + "?A6", + "?B6", + "?C6", + "?D6", + "?E6", + "?F6", + "?I6", + "?L6", + "?M6", + "?N6", + "?P6", + "?R6", + "?T6", + "?U6", + "Configure", + "CreateBattle", + "SetRestrictions" + ], + "Default": "new $6()", + "Migrated": false + }, + { + "Field": "battleSearchState", + "Slot": "epw_1", + "SliceClass": "Itt", + "Reducer": "Htt().vtg", + "Delegate": "Htt().vtg(t, n.epw_1)", + "Actions": [ + "?Dtt", + "EnterToBattle", + "Fight", + "RemoveBattle", + "SelectBattle", + "SelectBattleFormat", + "SetFilter", + "SetSearch", + "SetSuspiciousFilter" + ], + "Default": "new Itt()", + "Migrated": false + }, + { + "Field": "lobbyLoader", + "Slot": "fpw_1", + "SliceClass": "KW", + "Reducer": "Bs.es1", + "Delegate": "Bs.es1(t, n.fpw_1)", + "Actions": [ + "?FW", + "?GW" + ], + "Default": "new KW()", + "Migrated": false + }, + { + "Field": "battleResult", + "Slot": "gpw_1", + "SliceClass": "sR", + "Reducer": "Fi.aqp", + "Delegate": "Fi.aqp(t, n.gpw_1)", + "Actions": [ + "?cR", + "?fR", + "?lR", + "DoInit", + "SetHasBattleResult", + "SetRewards" + ], + "Default": "new sR()", + "Migrated": false + }, + { + "Field": "battleUsers", + "Slot": "hpw_1", + "SliceClass": "uP", + "Reducer": "sr.hqs", + "Delegate": "sr.hqs(t, n.hpw_1)", + "Actions": [ + "?_P", + "?hP", + "?lP", + "?oP", + "?qP", + "?sP", + "InitSuspiciousState", + "InitUserLevels", + "RemoveTankInfo", + "SetGearScore", + "SetTankInfo", + "SetTankResistance", + "UpdateUserLevel", + "UpdateUserRank", + "UpdateUserScoreKillsAndDeaths", + "UpdateUserSuspiciousState", + "UpdateUsersClanTag", + "UpdateUsersScoreKillsAndDeaths" + ], + "Default": "new uP()", + "Migrated": false + }, + { + "Field": "suppliesState", + "Slot": "ipw_1", + "SliceClass": "wE", + "Reducer": "$t.fq9", + "Delegate": "$t.fq9(t, n.ipw_1)", + "Actions": [ + "?qE" + ], + "Default": "new wE()", + "Migrated": false + }, + { + "Field": "matchmakingGroup", + "Slot": "jpw_1", + "SliceClass": "rW", + "Reducer": "NW().is0", + "Delegate": "NW().is0(t, n.jpw_1)", + "Actions": [ + "?zW", + "AcceptedInviteToGroup", + "AddUser", + "AddUserToSelected", + "InitMatchmakingGroup", + "Invite", + "RejectedInviteToGroup", + "RemoveUserToSelected", + "SetAllowedFromFriends", + "SetSelectedUsers", + "UpdateMountItems", + "UpdateUserReadiness", + "UserRemoved" + ], + "Default": "new rW()", + "Migrated": false + }, + { + "Field": "shop", + "Slot": "kpw_1", + "SliceClass": "j4", + "Reducer": "(inline)", + "Delegate": "(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,", + "Actions": [], + "Default": "new j4()", + "Migrated": false + }, + { + "Field": "battlePauseState", + "Slot": "lpw_1", + "SliceClass": "dT", + "Reducer": "NT().mqc", + "Delegate": "NT().mqc(t, n.lpw_1)", + "Actions": [ + "?TT", + "?xT", + "OnPauseMode", + "SetIdleKickPeriod" + ], + "Default": "new dT()", + "Migrated": false + }, + { + "Field": "battleViewState", + "Slot": "mpw_1", + "SliceClass": "DT", + "Reducer": "sI.jqd", + "Delegate": "(null == $n && new sI(), $n).jqd(t, n.mpw_1)", + "Actions": [ + "?JT", + "?QT", + "?WT", + "?YT", + "?ZT", + "?eI", + "?iI", + "?nE", + "?nI", + "?rI", + "?tE", + "?tI", + "SetElementsVisibility", + "SetVisibility" + ], + "Default": "new DT()", + "Migrated": false + }, + { + "Field": "rankUpDialogState", + "Slot": "npw_1", + "SliceClass": "LO", + "Reducer": "Se.br3", + "Delegate": "Se.br3(t, n.npw_1)", + "Actions": [ + "?FO", + "ShowAvailableItems" + ], + "Default": "new LO()", + "Migrated": false + }, + { + "Field": "clanUserInfo", + "Slot": "opw_1", + "SliceClass": "hO", + "Reducer": "de.gr1", + "Delegate": "de.gr1(t, n.opw_1)", + "Actions": [ + "Loaded" + ], + "Default": "new hO()", + "Migrated": false + }, + { + "Field": "tankActionsLogState", + "Slot": "ppw_1", + "SliceClass": "vI", + "Reducer": "wi.gqe", + "Delegate": "wi.gqe(t, n.ppw_1)", + "Actions": [ + "?dI", + "OnUserJoinTheBattle", + "OnUserLeaveTheBattle", + "UpdateLog" + ], + "Default": "new vI()", + "Migrated": false + }, + { + "Field": "battleMessage", + "Slot": "qpw_1", + "SliceClass": "FE", + "Reducer": "vT().rqb", + "Delegate": "vT().rqb(t, n.qpw_1)", + "Actions": [ + "?sT", + "DoAddLocalizedMessage", + "DoAddLocalizedTeamMessage", + "DoAddLocalizedUserMessage", + "DoAddTextMessage", + "HideMessage", + "RemoveMessage" + ], + "Default": "new FE()", + "Migrated": false + }, + { + "Field": "battleChat", + "Slot": "rpw_1", + "SliceClass": "Q6", + "Reducer": "Hf.wtg", + "Delegate": "Hf.wtg(t, n.rpw_1)", + "Actions": [ + "?ett", + "?rtt", + "?stt", + "?utt", + "AddMessage", + "ChatLoaded" + ], + "Default": "new Q6()", + "Migrated": false + }, + { + "Field": "droppableGold", + "Slot": "spw_1", + "SliceClass": "Vet", + "Reducer": "Wp.kth", + "Delegate": "Wp.kth(t, n.spw_1)", + "Actions": [ + "?Ket", + "?Yet" + ], + "Default": "new Vet()", + "Migrated": false + }, + { + "Field": "home", + "Slot": "tpw_1", + "SliceClass": "s1", + "Reducer": "(inline)", + "Delegate": "(function (t, n) { return t instanceof o1 ? n.f7j(!0) : n; })(t, n.tpw_1)", + "Actions": [], + "Default": "new s1()", + "Migrated": false + }, + { + "Field": "localBattleUserState", + "Slot": "upw_1", + "SliceClass": "lE", + "Reducer": "(inline)", + "Delegate": "(function (t, n) { return t instanceof fE ? n.xq8(t.yq8_1) : t instanceof vE ? n.xq8(cd, t.zq8_1) : n; })(t, n.upw_1)", + "Actions": [], + "Default": "new lE()", + "Migrated": false + }, + { + "Field": "soundSettings", + "Slot": "vpw_1", + "SliceClass": "Det", + "Reducer": "Wet().jth", + "Delegate": "Wet().jth(t, n.vpw_1)", + "Actions": [ + "?Uet", + "?_C", + "SetEntranceSoundEnabled", + "UpdateSettings" + ], + "Default": "new Det()", + "Migrated": false + }, + { + "Field": "gameSettings", + "Slot": "wpw_1", + "SliceClass": "c2", + "Reducer": "g2().zss", + "Delegate": "g2().zss(t, n.wpw_1)", + "Actions": [ + "?_C", + "UpdateSettings" + ], + "Default": "new c2()", + "Migrated": false + }, + { + "Field": "battleSettings", + "Slot": "xpw_1", + "SliceClass": "W0", + "Reducer": "X0().esq", + "Delegate": "X0().esq(t, n.xpw_1)", + "Actions": [ + "?_C", + "UpdateSettings" + ], + "Default": "new W0()", + "Migrated": false + }, + { + "Field": "graphicsSettings", + "Slot": "ypw_1", + "SliceClass": null, + "Reducer": "lc.isu", + "Delegate": "lc.isu(t, n.ypw_1)", + "Actions": [ + "UpdateSettings" + ], + "Default": "vc.dsv()", + "Migrated": false + }, + { + "Field": "controlSettings", + "Slot": "zpw_1", + "SliceClass": "i2", + "Reducer": "_2().ess", + "Delegate": "_2().ess(t, n.zpw_1)", + "Actions": [ + "?_C", + "UpdateSettings" + ], + "Default": "new i2()", + "Migrated": false + }, + { + "Field": "securitySettings", + "Slot": "apx_1", + "SliceClass": "V2", + "Reducer": "pc.qsv", + "Delegate": "pc.qsv(t, n.apx_1)", + "Actions": [ + "SetProfileBlockStatus" + ], + "Default": "new V2()", + "Migrated": false + }, + { + "Field": "purchaseDialogState", + "Slot": "bpx_1", + "SliceClass": "CO", + "Reducer": "je.ur2", + "Delegate": "je.ur2(t, n.bpx_1)", + "Actions": [ + "?NO", + "ChangeEmailStatus", + "SuccessfullyPurchase" + ], + "Default": "new CO()", + "Migrated": false + }, + { + "Field": "battleInviteState", + "Slot": "cpx_1", + "SliceClass": "att", + "Reducer": "Ttt().nth", + "Delegate": "Ttt().nth(t, n.cpx_1)", + "Actions": [ + "?dtt", + "EnableSetBattleInviteNotifications", + "InvitationResponse", + "InviteToBattle", + "RemoveUserFromInvited", + "SelectUserToInvite" + ], + "Default": "new att()", + "Migrated": false + }, + { + "Field": "keyboardSettings", + "Slot": "dpx_1", + "SliceClass": "Sut", + "Reducer": "bw.oth", + "Delegate": "bw.oth(t, n.dpx_1)", + "Actions": [ + "?Iut", + "?_C", + "ChangeAction", + "RemoveAction", + "ResetAction" + ], + "Default": "new Sut()", + "Migrated": false + }, + { + "Field": "touchscreenHudSettings", + "Slot": "epx_1", + "SliceClass": "j7", + "Reducer": "N7().xsv", + "Delegate": "N7().xsv(t, n.epx_1)", + "Actions": [ + "?E7", + "?_C", + "UpdateSettings" + ], + "Default": "new j7()", + "Migrated": false + }, + { + "Field": "mainQuest", + "Slot": "fpx_1", + "SliceClass": "WZ", + "Reducer": "ih.bsk", + "Delegate": "ih.bsk(t, n.fpx_1)", + "Actions": [ + "?VZ", + "AddRewardedQuest", + "QuestsLoaded", + "SetBattleUpdatedQuests", + "SetEventPassShopItemId", + "SetQuests", + "UpdateQuests" + ], + "Default": "new WZ()", + "Migrated": false + }, + { + "Field": "language", + "Slot": "gpx_1", + "SliceClass": null, + "Reducer": "m6().jtg", + "Delegate": "m6().jtg(t, n.gpx_1)", + "Actions": [ + "?d6" + ], + "Default": "tj()", + "Migrated": false + }, + { + "Field": "launchParams", + "Slot": "hpx_1", + "SliceClass": null, + "Reducer": "(pass-through)", + "Delegate": "n.hpx_1", + "Actions": [], + "Default": "ag()", + "Migrated": false + }, + { + "Field": "partner", + "Slot": "ipx_1", + "SliceClass": "UJ", + "Reducer": "(inline)", + "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": [], + "Default": "new UJ()", + "Migrated": false + }, + { + "Field": "entrance", + "Slot": "jpx_1", + "SliceClass": "hD", + "Reducer": "Bv.pth", + "Delegate": "Bv.pth(t, n.jpx_1)", + "Actions": [ + "?eit", + "?fit", + "?lit", + "?oit", + "?sit", + "?uit", + "SetAgeWarnShow" + ], + "Default": "new hD()", + "Migrated": false + }, + { + "Field": "antiAddiction", + "Slot": "kpx_1", + "SliceClass": "UC", + "Reducer": "W.iq8", + "Delegate": "W.iq8(t, n.kpx_1)", + "Actions": [ + "?FC", + "?XC", + "?YC", + "SetRealNameAndIdNumber", + "SetValidateAntiAddictionUserDataStatus" + ], + "Default": "new UC()", + "Migrated": false + }, + { + "Field": "spectator", + "Slot": "lpx_1", + "SliceClass": "HA", + "Reducer": "Li.jqm", + "Delegate": "Li.jqm(t, n.lpx_1)", + "Actions": [ + "?JA", + "?XA", + "?YA", + "?ZA", + "SetSpectator", + "UpdateHudLayout" + ], + "Default": "new HA()", + "Migrated": false + }, + { + "Field": "moderation", + "Slot": "mpx_1", + "SliceClass": "NI", + "Reducer": "Ei.oqh", + "Delegate": "Ei.oqh(t, n.mpx_1)", + "Actions": [ + "?FI", + "?GI", + "?JI", + "?KI", + "?QI", + "?UI", + "?WI", + "?XI", + "?YI", + "?ZI", + "?nA", + "?tA", + "?uA" + ], + "Default": "new NI()", + "Migrated": false + }, + { + "Field": "rulesUpdated", + "Slot": "npx_1", + "SliceClass": "i1", + "Reducer": "Ch.dsm", + "Delegate": "Ch.dsm(t, n.npx_1)", + "Actions": [ + "UpdateRules" + ], + "Default": "new i1()", + "Migrated": false + }, + { + "Field": "announcement", + "Slot": "opx_1", + "SliceClass": "lC", + "Reducer": "PC().rq5", + "Delegate": "PC().rq5(t, n.opx_1)", + "Actions": [ + "?bC", + "?qC", + "Init", + "SetSkipNextPromotedAnnounce" + ], + "Default": "new lC()", + "Migrated": false + }, + { + "Field": "notificationByCountry", + "Slot": "ppx_1", + "SliceClass": "gJ", + "Reducer": "R_.ssd", + "Delegate": "R_.ssd(t, n.ppx_1)", + "Actions": [ + "?qJ", + "Init" + ], + "Default": "new gJ()", + "Migrated": false + }, + { + "Field": "userRenameState", + "Slot": "qpx_1", + "SliceClass": "O8", + "Reducer": "Rf.pte", + "Delegate": "Rf.pte(t, n.qpx_1)", + "Actions": [ + "?B8", + "?F8", + "?G8", + "?Q8", + "?U8", + "ChangeUidInputState", + "ResponseActual", + "ResponseDelivered", + "SetChangeUidParams", + "SetRubiesForRename", + "SetValidationStatus" + ], + "Default": "new O8()", + "Migrated": false + }, + { + "Field": "eventGoods", + "Slot": "rpx_1", + "SliceClass": "W7", + "Reducer": "Wc.esx", + "Delegate": "Wc.esx(t, n.rpx_1)", + "Actions": [ + "InitEventGoods" + ], + "Default": "new W7()", + "Migrated": false + }, + { + "Field": "creatorTagState", + "Slot": "spx_1", + "SliceClass": "qO", + "Reducer": "ze.er2", + "Delegate": "ze.er2(t, n.spx_1)", + "Actions": [ + "?jO", + "SetAttachedCreatorTag", + "SetCreatorTag", + "SetCreatorTags" + ], + "Default": "new qO()", + "Migrated": false + }, + { + "Field": "twoStepVerificationState", + "Slot": "tpx_1", + "SliceClass": "x9", + "Reducer": "_f.atb", + "Delegate": "_f.atb(t, n.tpx_1)", + "Actions": [ + "ActivationStatus2FA", + "Enable2FaRequest", + "SetSecretAndQRCode", + "SetUse2FA", + "VerificationStatus" + ], + "Default": "new x9()", + "Migrated": false + }, + { + "Field": "rewardedVideosState", + "Slot": "upx_1", + "SliceClass": "JQ", + "Reducer": "xh.zsl", + "Delegate": "xh.zsl(t, n.upx_1)", + "Actions": [ + "SetDisplayState" + ], + "Default": "new JQ()", + "Migrated": false + }, + { + "Field": "twitchState", + "Slot": "vpx_1", + "SliceClass": "g9", + "Reducer": "uf.gta", + "Delegate": "uf.gta(t, n.vpx_1)", + "Actions": [ + "SetAccount", + "SetDaysForNextLinking", + "SetTwitchEnabled" + ], + "Default": "new g9()", + "Migrated": true + }, + { + "Field": "icons", + "Slot": "wpx_1", + "SliceClass": "oH", + "Reducer": "vs.kru", + "Delegate": "vs.kru(t, n.wpx_1)", + "Actions": [ + "SetIcons" + ], + "Default": "new oH()", + "Migrated": false + }, + { + "Field": "itemNotifications", + "Slot": "xpx_1", + "SliceClass": "_1", + "Reducer": "A1().dso", + "Delegate": "A1().dso(t, n.xpx_1)", + "Actions": [ + "?S1", + "?_C", + "AddItemNotification", + "AddItemNotifications", + "RemoveItemNotification", + "UpdateItemNotification" + ], + "Default": "new _1()", + "Migrated": false + }, + { + "Field": "contextTutorial", + "Slot": "ypx_1", + "SliceClass": "Mst", + "Reducer": "Jst().tth", + "Delegate": "Jst().tth(t, n.ypx_1)", + "Actions": [ + "LoadCurrentElement", + "MarkElementAsViewed", + "SetLoadedElements", + "SetMissionToElement", + "SetUnViewedElements" + ], + "Default": "new Mst()", + "Migrated": false + }, + { + "Field": "suppliesTutorial", + "Slot": "zpx_1", + "SliceClass": null, + "Reducer": "Uw.uth", + "Delegate": "Uw.uth(t, n.zpx_1)", + "Actions": [ + "ActivatedInCurrentBattle", + "SetLoadedElement" + ], + "Default": "", + "Migrated": false + }, + { + "Field": "questCategories", + "Slot": "apy_1", + "SliceClass": "zZ", + "Reducer": "th.lsh", + "Delegate": "th.lsh(t, n.apy_1)", + "Actions": [ + "RemoveQuestCategory", + "RequestQuestCategoriesStatistic", + "SetQuestCategories", + "SetQuestCategoriesStatistic" + ], + "Default": "new zZ()", + "Migrated": false + }, + { + "Field": "proBattle", + "Slot": "bpy_1", + "SliceClass": "Wtt", + "Reducer": "uv.wth", + "Delegate": "uv.wth(t, n.bpy_1)", + "Actions": [ + "Fight", + "SetPassword" + ], + "Default": "new Wtt()", + "Migrated": false + }, + { + "Field": "availabilityByRank", + "Slot": "cpy_1", + "SliceClass": "tet", + "Reducer": "$p.xth", + "Delegate": "$p.xth(t, n.cpy_1)", + "Actions": [ + "SetLoadedData" + ], + "Default": "new tet()", + "Migrated": false + }, + { + "Field": "userReportsState", + "Slot": "dpy_1", + "SliceClass": "Zst", + "Reducer": "qot.zth", + "Delegate": "(null == sd && new qot(), sd).zth(t, n.dpy_1)", + "Actions": [ + "InitUserReports", + "ReportBattleChatMessage", + "ReportLobbyChatMessage", + "ReportProBattleName", + "SetAvailableGameReports", + "SetAvailableReports" + ], + "Default": "new Zst()", + "Migrated": false + }, + { + "Field": "tankBuilderState", + "Slot": "epy_1", + "SliceClass": "brt", + "Reducer": "wp.ati", + "Delegate": "wp.ati(t, n.epy_1)", + "Actions": [ + "?Trt", + "SetKode", + "SetParts", + "SetPartsInfo" + ], + "Default": "new brt()", + "Migrated": false + }, + { + "Field": "contractState", + "Slot": "fpy_1", + "SliceClass": "Ont", + "Reducer": "xv.bti", + "Delegate": "xv.bti(t, n.fpy_1)", + "Actions": [ + "?Hnt", + "GetReward", + "Init", + "SetActiveContract", + "SetBattleProgress", + "SetContract", + "UpdateProgress" + ], + "Default": "new Ont()", + "Migrated": false + }, + { + "Field": "promoCode", + "Slot": "gpy_1", + "SliceClass": "cZ", + "Reducer": "Q_.ysg", + "Delegate": "Q_.ysg(t, n.gpy_1)", + "Actions": [ + "ActivatePromoCode", + "SetCodeValidateStatus" + ], + "Default": "new cZ()", + "Migrated": false + }, + { + "Field": "referralEvent", + "Slot": "hpy_1", + "SliceClass": "pQ", + "Reducer": "lh.osk", + "Delegate": "lh.osk(t, n.hpy_1)", + "Actions": [ + "SetPromoCode", + "SetReferralEventData", + "SetReferrals", + "SetReferrer" + ], + "Default": "new pQ()", + "Migrated": false + }, + { + "Field": "supportUrl", + "Slot": "ipy_1", + "SliceClass": "I4", + "Reducer": "Yl.ct7", + "Delegate": "Yl.ct7(t, n.ipy_1)", + "Actions": [ + "?R4", + "SetUrl" + ], + "Default": "new I4()", + "Migrated": false + } + ] +} diff --git a/3DTank/tools/gen_root_wiring.js b/3DTank/tools/gen_root_wiring.js new file mode 100644 index 0000000..269b443 --- /dev/null +++ b/3DTank/tools/gen_root_wiring.js @@ -0,0 +1,444 @@ +// gen_root_wiring.js —— 从 module_10032 抽取「根状态 → 切片 reducer」委派表 +// +// 产出: +// 3DTank/names/root_wiring.json 证据表(字段 / 槽位 / 切片类 / reducer / 动作名) +// TankiOffline/app/.../App/State/RootState.kt 原 z6 的 89 个切片字段(已迁移的用强类型,其余保留 Any?) +// TankiOffline/app/.../App/State/RootWiring.kt 委派表 + 迁移覆盖统计 +// +// 解析口径(全部取自源码,不猜): +// · 根 reducer:body 含 `return n.cti(` 的 function(当前是 k6) +// · 根状态类:定义 .cti 的类(当前是 z6),字段顺序 = 构造函数参数顺序 +// · 槽位:构造函数里的 `(this._1 = )` +// · 字段名:toString 里的 `"=" + this._1` +// · 委派:根 reducer 顶层赋值 `x = (t, n.…)`;hF(t, n) 这种传整个状态的, +// 到被调函数体里找它实际读的 n. +// · 未具名形态:内联 IIFE 记 `(inline)`,纯 `n.` 透传记 `(pass-through)` +// · 动作:被调 reducer 顶层 `instanceof ` → action_inventory 里的类名 +// +// 用法:cd 3DTank/tools && node gen_root_wiring.js +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const P = path.resolve(__dirname, '..'); +const T = fs.readFileSync(path.join(P, 'src', 'module_10032.js'), 'utf8'); +const Inv = JSON.parse(fs.readFileSync(path.join(P, 'names', 'action_inventory_10032.json'), 'utf8')); +const LocalToName = new Map(); +for (const R of Inv.Rows) if (!LocalToName.has(R.Local)) LocalToName.set(R.Local, R.Name); +// module_10849 的 Redux 初始化动作(本模块只 import,不在 action_inventory_10032 里) +LocalToName.set('iz', 'InitState'); + +// ---------------------------------------------------------------- 源码扫描基础 + +/** 字符串/注释感知的花括号配对。 */ +function BracePairs(Text) { + const Pairs = new Map(); + const Stack = []; + let I = 0; + while (I < Text.length) { + const C = Text[I]; + if (C === '"' || C === "'" || C === '`') { + const Q = C; + I++; + while (I < Text.length) { + if (Text[I] === '\\') { I += 2; continue; } + if (Text[I] === Q) { I++; break; } + I++; + } + continue; + } + if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); I = N < 0 ? Text.length : N; continue; } + if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); I = N < 0 ? Text.length : N + 2; continue; } + if (C === '{') Stack.push(I); + else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); } + I++; + } + return Pairs; +} +const Pairs = BracePairs(T); + +/** 收集所有 function 与 `X.m = function` 的体区间。 */ +const Fns = []; +for (const M of T.matchAll(/(? F.Name === Name) || null; } +function FindMethod(Ref) { + const Dot = Ref.lastIndexOf('.'); + if (Dot < 0) return null; + const Obj = Ref.slice(0, Dot); + const Method = Ref.slice(Dot + 1); + return Methods.find((M) => M.Obj === Obj && M.Method === Method) || null; +} + +/** 把 [From, To) 内的嵌套 function 体挖空,只留顶层分支(避免把 IIFE 里的 instanceof 算进来)。 */ +function MaskNested(Text, From, To) { + const Out = Text.split(''); + for (const F of Fns) { + if (F.Start > From && F.End < To) { + for (let I = F.Start; I <= F.End; I++) Out[I] = ' '; + } + } + for (const M of Methods) { + if (M.Start > From && M.End < To) { + for (let I = M.Start; I <= M.End; I++) Out[I] = ' '; + } + } + return Out.slice(From, To).join(''); +} + +/** 顶层 `name = expr` 赋值;返回 [{Name, Expr}](expr 已 trim)。 */ +function TopLevelAssignments(Body) { + const Out = []; + let I = 0; + let Depth = 0; + const Strings = []; + while (I < Body.length) { + const C = Body[I]; + if (C === '"' || C === "'" || C === '`') { + const Q = C; + I++; + while (I < Body.length) { + if (Body[I] === '\\') { I += 2; continue; } + if (Body[I] === Q) { I++; break; } + I++; + } + Strings.push(I); + continue; + } + if (C === '(' || C === '[' || C === '{') { Depth++; I++; continue; } + if (C === ')' || C === ']' || C === '}') { Depth--; I++; continue; } + if (Depth === 0) { + const M = /^([A-Za-z_$][\w$]*)\s*=\s*/.exec(Body.slice(I)); + if (M && (I === 0 || /[;{}\s]/.test(Body[I - 1]))) { + const Name = M[1]; + let J = I + M[0].length; + let D = 0; + while (J < Body.length) { + const D0 = Body[J]; + if (D0 === '"' || D0 === "'" || D0 === '`') { + const Q = D0; + J++; + while (J < Body.length) { + if (Body[J] === '\\') { J += 2; continue; } + if (Body[J] === Q) { J++; break; } + J++; + } + continue; + } + if (D0 === '(' || D0 === '[' || D0 === '{') D++; + else if (D0 === ')' || D0 === ']' || D0 === '}') D--; + else if (D0 === ',' && D === 0) break; + else if (D0 === ';' && D === 0) break; + J++; + } + Out.push({ Name, Expr: Body.slice(I + M[0].length, J).trim() }); + I = J + 1; + continue; + } + } + I++; + } + return Out; +} + +/** 定位根 reducer / 根状态类。 */ +const RootReducerFn = Fns.find((F) => T.slice(F.Body, F.End).includes('return n.cti(')); +if (!RootReducerFn) throw new Error('找不到根 reducer(body 含 return n.cti()'); +const CopyMethod = Methods.find((M) => M.Method === 'cti'); +if (!CopyMethod) throw new Error('找不到根状态类的 cti'); +const RootClass = CopyMethod.Obj; +const RootClassFn = FindFn(RootClass); +if (!RootClassFn) throw new Error('找不到根状态类构造函数 ' + RootClass); + +// ---------------------------------------------------------------- 根状态:参数 / 槽位 / 字段 + +const Params = RootClassFn.Params.split(',').map((S) => S.trim()).filter(Boolean); +const CtorBody = T.slice(RootClassFn.Body, RootClassFn.End); + +const SlotOfParam = new Map(); +for (const M of CtorBody.matchAll(/\(this\.(\w+) = (\w+)\)/g)) SlotOfParam.set(M[2], M[1]); + +const DefaultOfParam = new Map(); +for (const M of CtorBody.matchAll(/\((\w+) = \1 === cd \? ([\s\S]*?) : \1\)/g)) { + DefaultOfParam.set(M[1], M[2].replace(/\s+/g, ' ').trim()); +} +for (const M of CtorBody.matchAll(/\((\w+) = \1 !== cd && \1\)/g)) DefaultOfParam.set(M[1], 'false'); + +const ToStringStart = T.indexOf(`(ld(${RootClass}).toString = function`); +if (ToStringStart < 0) throw new Error('找不到根状态类 toString'); +const ToStringEnd = T.indexOf(`(ld(${RootClass}).hashCode`, ToStringStart); +const ToStringBody = T.slice(ToStringStart, ToStringEnd); + +const Fields = []; +for (const M of ToStringBody.matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) { + Fields.push({ Field: M[2], Slot: M[3] }); +} +if (Fields.length !== Params.length) { + throw new Error(`字段数 ${Fields.length} 与构造参数数 ${Params.length} 不一致`); +} + +// ---------------------------------------------------------------- 委派表达式 + +const ReduceBody = T.slice(RootReducerFn.Body, RootReducerFn.End); +const Assignments = TopLevelAssignments(ReduceBody); + +/** 从表达式里找出所有 n. 引用。 */ +function SlotsIn(Expr) { + return [...new Set([...Expr.matchAll(/\bn\.([A-Za-z0-9_$]+)/g)].map((M) => M[1]))]; +} + +/** 单例持有者 → 实际类,例如 `(Ke = new pD())`、`(nu = new MD())`。 */ +const SingletonClass = new Map(); +for (const M of T.matchAll(/\(([A-Za-z_$][\w$]*) = new ([A-Za-z_$][\w$]*)\(\)\)/g)) { + SingletonClass.set(M[1], M[2]); +} + +/** `X()` 形式的工厂 → 它返回的类,例如 `function M8() { return (null == X && new N8(), X); }`。 */ +function FactoryClass(Name) { + const F = FindFn(Name); + if (!F) return null; + const Body = T.slice(F.Body, F.End); + const M = /new ([A-Za-z_$][\w$]*)\(/.exec(Body); + return M ? M[1] : null; +} + +/** + * 表达式对应的 reducer 名字(`M8().qtd(t, n.ypu_1)` → `M8().qtd`)。 + * 表达式以别的调用开头、但结尾仍是 `.(t, n.)` 时取末尾调用, + * 例如 quests 的 `(null == zp && new Yrt(), zp).otg(t, n.npv_1)` → `zp.otg`;真 IIFE → `(inline)`。 + */ +function ReducerRef(Expr) { + // 懒汉单例:`(null == zp && new Yrt(), zp).otg(t, n.npv_1)` → `Yrt.otg` + const Lazy = /^\(null == [A-Za-z_$][\w$]* && new ([A-Za-z_$][\w$]*)\(\), [A-Za-z_$][\w$]*\)\.([A-Za-z_$][\w$]*)\(/.exec(Expr); + if (Lazy) return Lazy[1] + '.' + Lazy[2]; + // 纯透传:`at = n.hpx_1`(切片没有 reducer,值原样带过去) + if (/^n\.[A-Za-z0-9_$]+$/.test(Expr)) return '(pass-through)'; + const Head = /^([A-Za-z_$][\w$]*)(\(\))?(?:\.([A-Za-z_$][\w$]*))?\s*\(/.exec(Expr); + if (Head && (Head[3] || /^\(\s*t\s*,/.test(Expr.slice(Head[0].length - 1)))) { + if (Head[3]) return Head[1] + (Head[2] || '') + '.' + Head[3]; + return Head[1]; + } + const Tail = /([A-Za-z_$][\w$]*(?:\(\))?\.[A-Za-z_$][\w$]*)\(t,\s*n\.\w+\)\s*$/.exec(Expr); + if (Tail) return Tail[1]; + return Head ? Head[1] + (Head[2] || '') : '(inline)'; +} + +/** 取 reducer 体(function / X().m / X.m;hF 这种传整个状态的也能处理)。 */ +function BodyOfRef(Ref) { + if (Ref.startsWith('(')) return null; + const Dot = Ref.lastIndexOf('.'); + if (Dot < 0) { + const F = FindFn(Ref); + return F ? { From: F.Body, To: F.End } : null; + } + const ObjRaw = Ref.slice(0, Dot); + const Method = Ref.slice(Dot + 1).replace(/\(\)$/, ''); + const IsFactory = /\(\)$/.test(ObjRaw); + const Obj = ObjRaw.replace(/\(\)$/, ''); + let ClassName = null; + if (IsFactory) ClassName = FactoryClass(Obj); + if (!ClassName) ClassName = SingletonClass.get(Obj) || null; + if (!ClassName) ClassName = Obj; + const M = Methods.find((X) => X.Obj === ClassName && X.Method === Method) || + Methods.find((X) => X.Obj === Obj && X.Method === Method); + return M ? { From: M.Body, To: M.End } : null; +} + +/** reducer 顶层 instanceof 命中的动作名(没有 toString 的类记作 `?`)。 */ +function ActionsOf(Ref) { + const B = BodyOfRef(Ref); + if (!B) return []; + const Masked = MaskNested(T, B.From, B.To); + const Names = new Set(); + for (const M of Masked.matchAll(/instanceof ([A-Za-z0-9_$]+)/g)) { + const N = LocalToName.get(M[1]); + Names.add(N || '?' + M[1]); + } + return [...Names].sort(); +} + +// 槽位 → 委派 { Reducer, Expr, Slots } +const WiringBySlot = new Map(); +for (const A of Assignments) { + const Slots = SlotsIn(A.Expr); + if (!Slots.length) continue; + for (const S of Slots) { + if (!WiringBySlot.has(S)) { + WiringBySlot.set(S, { Reducer: ReducerRef(A.Expr), Expr: A.Expr.replace(/\s+/g, ' ').trim(), Local: A.Name }); + } + } +} +// hF(t, n):表达式里没有 n.,到被调体里找它读的槽位 +for (const A of Assignments) { + if (SlotsIn(A.Expr).length) continue; + const Ref = ReducerRef(A.Expr); + const B = BodyOfRef(Ref); + if (!B) continue; + const Slots = SlotsIn(T.slice(B.From, B.To)); + for (const S of Slots) { + if (!WiringBySlot.has(S)) { + WiringBySlot.set(S, { Reducer: Ref, Expr: A.Expr.replace(/\s+/g, ' ').trim(), Local: A.Name }); + } + } +} + +// ---------------------------------------------------------------- 汇总 + +const Rows = Fields.map((F) => { + const W = WiringBySlot.get(F.Slot); + const Param = Params.find((P) => SlotOfParam.get(P) === F.Slot) || null; + return { + Field: F.Field, + Slot: F.Slot, + SliceClass: null, // 由下面补(切片类名在构造函数默认值 / toString 里没有,靠 reducer 签名查) + Reducer: W ? W.Reducer : '(未识别)', + Delegate: W ? W.Expr.replace(/\s+/g, ' ').trim().slice(0, 500) : '', + Actions: W ? ActionsOf(W.Reducer) : [], + Default: Param ? DefaultOfParam.get(Param) || '' : '', + }; +}); + +// 切片类:从 reducer 所在类的 `X(new )` 或根状态构造默认 `new ()` 取 +const SliceClassOfParam = new Map(); +for (const [Param, Default] of DefaultOfParam) { + const M = /^new ([A-Za-z0-9_$]+)\(/.exec(Default); + if (M) SliceClassOfParam.set(Param, M[1]); +} +for (const R of Rows) { + const Param = Params.find((P) => SlotOfParam.get(P) === R.Slot); + R.SliceClass = Param ? SliceClassOfParam.get(Param) || null : null; +} + +// 根 reducer 里没有出现过的槽位(例如被包在更大表达式里) +for (const R of Rows) if (R.Reducer === '(未识别)') console.warn('未识别委派: ' + R.Field + ' / ' + R.Slot); + +// ---------------------------------------------------------------- 迁移标记(与 Kotlin 侧保持一致) + +const Migrated = { + inviteState: { Type: 'InviteStateSlice', Reduce: 'ReduceInvite' }, + loginByPassword: { Type: 'LoginByPasswordStateSlice', Reduce: 'ReduceLoginByPassword' }, + twitchState: { Type: 'TwitchStateSlice', Reduce: 'ReduceTwitch' }, +}; + +for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field); + +// 字段名 → Kotlin 属性名(首字母大写,保留原有大小写与数字) +function KotlinName(Field) { return Field.charAt(0).toUpperCase() + Field.slice(1); } + +/** JSON 字符串 → Kotlin 字符串字面量(`$` 要转义,否则会被当成模板起点)。 */ +function KStr(S) { return JSON.stringify(String(S)).replace(/\$/g, '\\$'); } + +const SeenProps = new Set(); +for (const R of Rows) { + const N = KotlinName(R.Field); + if (SeenProps.has(N)) throw new Error('Kotlin 属性名重复: ' + N); + SeenProps.add(N); +} + +// ---------------------------------------------------------------- 输出 + +fs.writeFileSync( + path.join(P, 'names', 'root_wiring.json'), + JSON.stringify( + { + generatedAt: new Date().toISOString(), + module: 'module_10032.js', + RootState: { Class: RootClass, Reducer: RootReducerFn.Name, FieldCount: Rows.length }, + Migrated: Object.keys(Migrated), + Rows, + }, + null, + 2 + ) + '\n' +); + +const KT = path.resolve(__dirname, '..', '..', 'TankiOffline', 'app', 'src', 'jsMain', 'kotlin', 'App', 'State'); +fs.mkdirSync(KT, { recursive: true }); + +const Lines = []; +Lines.push('// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。'); +Lines.push(`// 来源:module_10032 的根状态 ${RootClass}(toString 名 TOState,${Rows.length} 个切片)`); +Lines.push('// 与根 reducer ' + RootReducerFn.Name + '(逐切片归约后 copy 出新状态)。'); +Lines.push('// 已迁移的切片用强类型;其余保持 Any?(原样透传),迁移一个改一个再重新生成。'); +Lines.push('package App.State'); +Lines.push(''); +Lines.push('import App.State.Reducers.InviteStateSlice'); +Lines.push('import App.State.Reducers.LoginByPasswordStateSlice'); +Lines.push('import App.State.Reducers.TwitchStateSlice'); +Lines.push(''); +Lines.push('/**'); +Lines.push(' * 原 `' + RootClass + '` / `TOState`:' + Rows.length + ' 个切片字段,顺序与 toString 一致。'); +Lines.push(' * 未迁移字段默认 null —— 原包构造时会给它们 `new ()`,这里等对应切片迁移后再补默认值。'); +Lines.push(' */'); +Lines.push('data class RootState('); +for (const R of Rows) { + const Prop = KotlinName(R.Field); + const M = Migrated[R.Field]; + if (M) { + Lines.push(` /** 原 ${R.Field}(${R.SliceClass || '?'} / ${R.Reducer})。已迁移。 */`); + Lines.push(` val ${Prop}: ${M.Type} = ${M.Type}(),`); + } else { + Lines.push(` /** 原 ${R.Field}(${R.SliceClass || '?'} / ${R.Reducer})。未迁移:原样透传。 */`); + Lines.push(` val ${Prop}: Any? = null,`); + } +} +Lines.push(')'); +Lines.push(''); +fs.writeFileSync(path.join(KT, 'RootState.kt'), Lines.join('\n')); + +const WLines = []; +WLines.push('// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。'); +WLines.push('// 证据表:module_10032 根 reducer ' + RootReducerFn.Name + ' 的逐切片委派(字段 / 槽位 / 切片类 / reducer / 动作)。'); +WLines.push('package App.State'); +WLines.push(''); +WLines.push('/** 一个切片在根状态里的委派项。 */'); +WLines.push('data class SliceWiring('); +WLines.push(' val Field: String,'); +WLines.push(' val Slot: String,'); +WLines.push(' val SliceClass: String,'); +WLines.push(' val Reducer: String,'); +WLines.push(' val Delegate: String,'); +WLines.push(' val Actions: List,'); +WLines.push(')'); +WLines.push(''); +WLines.push('object RootWiring {'); +WLines.push(' val All: List = listOf('); +for (const R of Rows) { + const Acts = R.Actions.map((A) => KStr(A)).join(', '); + const Parts = [ + KStr(R.Field), + KStr(R.Slot), + KStr(R.SliceClass || '?'), + KStr(R.Reducer), + KStr(R.Delegate), + 'listOf(' + Acts + ')', + ]; + WLines.push(' SliceWiring(' + Parts.join(', ') + '),'); +} +WLines.push(' )'); +WLines.push(''); +WLines.push(' /** 已迁移(由 RootReducer 真正归约)的字段。 */'); +WLines.push(' val Migrated: Set = setOf(' + Object.keys(Migrated).map((F) => KStr(F)).join(', ') + ')'); +WLines.push(''); +WLines.push(' /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */'); +WLines.push(' val Unmigrated: List get() = All.filter { it.Field !in Migrated }'); +WLines.push(''); +WLines.push(' fun ByField(Field: String): SliceWiring? = All.firstOrNull { it.Field == Field }'); +WLines.push('}'); +WLines.push(''); +fs.writeFileSync(path.join(KT, 'RootWiring.kt'), WLines.join('\n')); + +console.log(`根状态 ${RootClass}:${Rows.length} 个切片;已迁移 ${Object.keys(Migrated).length} 个,未迁移 ${Rows.length - Object.keys(Migrated).length} 个`); +console.log('未迁移清单:' + Rows.filter((R) => !R.Migrated).map((R) => R.Field).join(', ')); diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LoginReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LoginReducer.kt new file mode 100644 index 0000000..f9d482a --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/LoginReducer.kt @@ -0,0 +1,87 @@ +// LoginReducer.kt —— 密码登录切片(对应 module_10032 的 gD 状态 + MD.cr6 reducer) +// +// 证据链: +// 状态类 gD:`function gD(t, n, i, r, e, u, s) { t = t === cd ? "" : t; n = n === cd ? "" : n; +// i = i === cd || i; r = r === cd ? OD() : r; e = e !== cd && e; u = u !== cd && u; +// s = s === cd ? 0 : s; … }` +// toString:`LoginByPasswordState(login=…, password=…, rememberMe=…, status=…, +// onlyLogin=…, incorrectLogin2FA=…, requestsNumber2FA=…)` +// 枚举 kD(由 mD() 初始化):NONE / LOGGING_IN / WRONG_PASSWORD / WRONG_CAPTCHA;OD() 返回 NONE +// reducer MD.cr6(module_10032:28051 附近): +// PD → new gD()(整个切片复位) +// RD → copy(status = NONE) // 原包导出为 $_$ff,UI 在 82347 派发 +// SetLogin → copy(login, status = NONE) // 注意:改 login 会把 status 打回 NONE +// SetPassword → copy(password, status = NONE) +// SetRememberMe → copy(rememberMe) +// SetStatus → copy(status) +// ND → copy(login = t.dr6_1, onlyLogin = true) +// IncorrectLogin2FA → copy(incorrectLogin2FA) +// RequestsNumber2FA → copy(requestsNumber2FA) +// 其它 → 原状态 +// +// 原类的 PD / RD / ND 都没有 toString(不是动作目录里的具名动作),这里按语义命名并在注释里保留原名。 +package App.State.Reducers + +import App.State.IncorrectLogin2FA +import App.State.RequestsNumber2FA +import App.State.SetLogin +import App.State.SetPassword +import App.State.SetRememberMe +import App.State.SetStatus + +/** 原 `kD`(module_10032 的密码登录状态枚举);`SetStatus` 的字段名是 `byPasswordStatus`。 */ +enum class ByPasswordStatus { + NONE, + LOGGING_IN, + WRONG_PASSWORD, + WRONG_CAPTCHA, + ; + + companion object { + /** + * 动作字段在运行期是 JS 枚举对象,toString 就是成员名(`SetStatus(byPasswordStatus=NONE)`)。 + * 与原包一致:认不出时落到默认值 NONE。 + */ + fun FromText(Text: Any?): ByPasswordStatus = + entries.firstOrNull { it.name == Text?.toString() } ?: NONE + } +} + +/** 原 `PD`:无字段、无 toString,reducer 分支里整片复位。 */ +class ResetLoginByPassword + +/** 原 `RD`:无字段、无 toString;原包以 `$_$ff` 导出,UI 派发它把状态打回 NONE。 */ +class ResetLoginByPasswordStatus + +/** 原 `ND`:只有一个字段 `dr6_1`;reducer 用它做「只登录」模式(login + onlyLogin=true)。 */ +class LoginOnly(val login: String) + +/** + * 原 `gD`:`LoginByPasswordState(login, password, rememberMe, status, onlyLogin, incorrectLogin2FA, requestsNumber2FA)`。 + * 默认值全部取自原构造函数:`"" / "" / true / NONE / false / false / 0`。 + */ +data class LoginByPasswordStateSlice( + val Login: String = "", + val Password: String = "", + val RememberMe: Boolean = true, + val Status: ByPasswordStatus = ByPasswordStatus.NONE, + val OnlyLogin: Boolean = false, + val IncorrectLogin2FA: Boolean = false, + val RequestsNumber2FA: Int = 0, +) + +/** 原 `MD.cr6`。 */ +fun ReduceLoginByPassword(State0: LoginByPasswordStateSlice, A: Any): LoginByPasswordStateSlice = when (A) { + is ResetLoginByPassword -> LoginByPasswordStateSlice() + // RD:只把 status 复位,其余字段保留 + is ResetLoginByPasswordStatus -> State0.copy(Status = ByPasswordStatus.NONE) + // SetLogin / SetPassword 都会顺带把 status 打回 NONE(原包 `pr5(…, OD())`) + is SetLogin -> State0.copy(Login = A.login?.toString() ?: "", Status = ByPasswordStatus.NONE) + is SetPassword -> State0.copy(Password = A.password?.toString() ?: "", Status = ByPasswordStatus.NONE) + is SetRememberMe -> State0.copy(RememberMe = A.rememberMe) + is SetStatus -> State0.copy(Status = ByPasswordStatus.FromText(A.byPasswordStatus)) + is LoginOnly -> State0.copy(Login = A.login, OnlyLogin = true) + is IncorrectLogin2FA -> State0.copy(IncorrectLogin2FA = A.incorrectLogin2FA) + is RequestsNumber2FA -> State0.copy(RequestsNumber2FA = A.number) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TwitchReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TwitchReducer.kt new file mode 100644 index 0000000..0a60091 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/Reducers/TwitchReducer.kt @@ -0,0 +1,36 @@ +// TwitchReducer.kt —— Twitch 绑定切片(对应 module_10032 的 g9 状态 + y9.gta reducer) +// +// 证据链: +// 状态类 g9:`function g9(t, n, i) { t = t !== cd && t; n = n === cd ? null : n; i = i === cd ? 0 : i; … }` +// toString:`TwitchState(isTwitchEnabled=…, twitchAccount=…, daysForNextLinking=…)` +// reducer y9.gta(module_10032:41442): +// SetTwitchEnabled → copy(isTwitchEnabled = t.dta_1) +// SetAccount → copy(twitchAccount = t.eta_1) // 这里的 SetAccount 是「Twitch 账号」,不是用户 UID +// SetDaysForNextLinking → copy(daysForNextLinking = t.fta_1) +// 其它 → 原状态 +// +// 注意:动作目录里另有一个同名 `SetAccount` 的候选,靠样例 `SetAccount(account=null)` + 本 reducer 的 +// `instanceof z9` 才能锁定;`SetAccountRemovalDays` 之类属于别的切片。 +package App.State.Reducers + +import App.State.SetAccount +import App.State.SetDaysForNextLinking +import App.State.SetTwitchEnabled + +/** + * 原 `g9`:`TwitchState(isTwitchEnabled, twitchAccount, daysForNextLinking)`。 + * 默认值取自原构造函数:`false / null / 0`。 + */ +data class TwitchStateSlice( + val IsTwitchEnabled: Boolean = false, + val TwitchAccount: String? = null, + val DaysForNextLinking: Int = 0, +) + +/** 原 `y9.gta`。 */ +fun ReduceTwitch(State0: TwitchStateSlice, A: Any): TwitchStateSlice = when (A) { + is SetTwitchEnabled -> State0.copy(IsTwitchEnabled = A.twitchEnabled) + is SetAccount -> State0.copy(TwitchAccount = A.account) + is SetDaysForNextLinking -> State0.copy(DaysForNextLinking = A.days) + else -> State0 +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt new file mode 100644 index 0000000..9cd66e1 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootReducer.kt @@ -0,0 +1,39 @@ +// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6) +// +// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。 +// 这里采用同一形状,但只真正归约已迁移的切片: +// +// inviteState → ReduceInvite(aD / pD.er5) +// loginByPassword → ReduceLoginByPassword(gD / MD.cr6) +// twitchState → ReduceTwitch(g9 / y9.gta) +// +// 其余 86 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态, +// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。 +// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。 +package App.State + +import App.State.Reducers.ReduceInvite +import App.State.Reducers.ReduceLoginByPassword +import App.State.Reducers.ReduceTwitch + +/** 根 reducer:`k6`。 */ +object RootReducer { + + /** 已迁移(真正归约)的字段名,来自生成物 RootWiring.Migrated。 */ + val Migrated: Set get() = RootWiring.Migrated + + /** 形如「已迁移 3/89」,给日志与自检用。 */ + val Coverage: String get() = "已迁移 ${Migrated.size}/${RootWiring.All.size}" + + /** + * 归约一次。未迁移字段原样带过去(`copy` 只改已迁移的三个)。 + */ + fun Reduce(State0: RootState, Action: Any): RootState = State0.copy( + InviteState = ReduceInvite(State0.InviteState, Action), + LoginByPassword = ReduceLoginByPassword(State0.LoginByPassword, Action), + TwitchState = ReduceTwitch(State0.TwitchState, Action), + ) +} + +/** 顶层入口,与切片 reducer 的 `ReduceXxx(State0, Action)` 命名保持一致。 */ +fun ReduceRoot(State0: RootState, Action: Any): RootState = RootReducer.Reduce(State0, Action) diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt new file mode 100644 index 0000000..3493848 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootState.kt @@ -0,0 +1,194 @@ +// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。 +// 来源:module_10032 的根状态 z6(toString 名 TOState,89 个切片) +// 与根 reducer k6(逐切片归约后 copy 出新状态)。 +// 已迁移的切片用强类型;其余保持 Any?(原样透传),迁移一个改一个再重新生成。 +package App.State + +import App.State.Reducers.InviteStateSlice +import App.State.Reducers.LoginByPasswordStateSlice +import App.State.Reducers.TwitchStateSlice + +/** + * 原 `z6` / `TOState`:89 个切片字段,顺序与 toString 一致。 + * 未迁移字段默认 null —— 原包构造时会给它们 `new ()`,这里等对应切片迁移后再补默认值。 + */ +data class RootState( + /** 原 user(t8 / M8().qtd)。未迁移:原样透传。 */ + val User: Any? = null, + /** 原 screen(WK / dY().ks5)。未迁移:原样透传。 */ + val Screen: Any? = null, + /** 原 modalScreen(nV / Fs.js1)。未迁移:原样透传。 */ + val ModalScreen: Any? = null, + /** 原 battleStatistics(jR / rr.fqr)。未迁移:原样透传。 */ + val BattleStatistics: Any? = null, + /** 原 battleTutorial(rE / Q.sq8)。未迁移:原样透传。 */ + val BattleTutorial: Any? = null, + /** 原 garage(UB / hF)。未迁移:原样透传。 */ + val Garage: Any? = null, + /** 原 garagePreview(IG / fs.eru)。未迁移:原样透传。 */ + val GaragePreview: Any? = null, + /** 原 challenges(CZ / nh.hsj)。未迁移:原样透传。 */ + val Challenges: Any? = null, + /** 原 loginByPassword(gD / nu.cr6)。已迁移。 */ + val LoginByPassword: LoginByPasswordStateSlice = LoginByPasswordStateSlice(), + /** 原 register(Cit / cp.stg)。未迁移:原样透传。 */ + val Register: Any? = null, + /** 原 migrationState($it / Uv.rtg)。未迁移:原样透传。 */ + val MigrationState: Any? = null, + /** 原 inviteState(aD / Ke.er5)。已迁移。 */ + val InviteState: InviteStateSlice = InviteStateSlice(), + /** 原 captchaState(jP / ar.iqt)。未迁移:原样透传。 */ + val CaptchaState: Any? = null, + /** 原 passwordChangeState(VD / uu.pr6)。未迁移:原样透传。 */ + val PasswordChangeState: Any? = null, + /** 原 system(_9 / w9().xt9)。未迁移:原样透传。 */ + val System: Any? = null, + /** 原 quests(Frt / Yrt.otg)。未迁移:原样透传。 */ + val Quests: Any? = null, + /** 原 chatState(LP / $r.wqv)。未迁移:原样透传。 */ + val ChatState: Any? = null, + /** 原 clanChatState(qnt / yv.qtg)。未迁移:原样透传。 */ + val ClanChatState: Any? = null, + /** 原 clan(jN / we.hr0)。未迁移:原样透传。 */ + val Clan: Any? = null, + /** 原 clansStorage(lO / me.vr1)。未迁移:原样透传。 */ + val ClansStorage: Any? = null, + /** 原 tipsState(dst / Hw.vth)。未迁移:原样透传。 */ + val TipsState: Any? = null, + /** 原 news(ZX / E_.asd)。未迁移:原样透传。 */ + val News: Any? = null, + /** 原 settings(Uut / xw.ltg)。未迁移:原样透传。 */ + val Settings: Any? = null, + /** 原 accountSettings(oet / Lp.mtg)。未迁移:原样透传。 */ + val AccountSettings: Any? = null, + /** 原 lootBoxes(aH / ws.wrv)。未迁移:原样透传。 */ + val LootBoxes: Any? = null, + /** 原 notifications(? / B_.ise)。未迁移:原样透传。 */ + val Notifications: Any? = null, + /** 原 friends(KD / NL().cr9)。未迁移:原样透传。 */ + val Friends: Any? = null, + /** 原 remoteUserDataStorage($Q / vh.isl)。未迁移:原样透传。 */ + val RemoteUserDataStorage: Any? = null, + /** 原 battlesSelect(fA / Mi.vqk)。未迁移:原样透传。 */ + val BattlesSelect: Any? = null, + /** 原 maps(OA / Oi.sql)。未迁移:原样透传。 */ + val Maps: Any? = null, + /** 原 matchmaking(TH / JH().qrx)。未迁移:原样透传。 */ + val Matchmaking: Any? = null, + /** 原 battleCreateState($6 / Df.ttg)。未迁移:原样透传。 */ + val BattleCreateState: Any? = null, + /** 原 battleSearchState(Itt / Htt().vtg)。未迁移:原样透传。 */ + val BattleSearchState: Any? = null, + /** 原 lobbyLoader(KW / Bs.es1)。未迁移:原样透传。 */ + val LobbyLoader: Any? = null, + /** 原 battleResult(sR / Fi.aqp)。未迁移:原样透传。 */ + val BattleResult: Any? = null, + /** 原 battleUsers(uP / sr.hqs)。未迁移:原样透传。 */ + val BattleUsers: Any? = null, + /** 原 suppliesState(wE / $t.fq9)。未迁移:原样透传。 */ + val SuppliesState: Any? = null, + /** 原 matchmakingGroup(rW / NW().is0)。未迁移:原样透传。 */ + val MatchmakingGroup: Any? = null, + /** 原 shop(j4 / (inline))。未迁移:原样透传。 */ + val Shop: Any? = null, + /** 原 battlePauseState(dT / NT().mqc)。未迁移:原样透传。 */ + val BattlePauseState: Any? = null, + /** 原 battleViewState(DT / sI.jqd)。未迁移:原样透传。 */ + val BattleViewState: Any? = null, + /** 原 rankUpDialogState(LO / Se.br3)。未迁移:原样透传。 */ + val RankUpDialogState: Any? = null, + /** 原 clanUserInfo(hO / de.gr1)。未迁移:原样透传。 */ + val ClanUserInfo: Any? = null, + /** 原 tankActionsLogState(vI / wi.gqe)。未迁移:原样透传。 */ + val TankActionsLogState: Any? = null, + /** 原 battleMessage(FE / vT().rqb)。未迁移:原样透传。 */ + val BattleMessage: Any? = null, + /** 原 battleChat(Q6 / Hf.wtg)。未迁移:原样透传。 */ + val BattleChat: Any? = null, + /** 原 droppableGold(Vet / Wp.kth)。未迁移:原样透传。 */ + val DroppableGold: Any? = null, + /** 原 home(s1 / (inline))。未迁移:原样透传。 */ + val Home: Any? = null, + /** 原 localBattleUserState(lE / (inline))。未迁移:原样透传。 */ + val LocalBattleUserState: Any? = null, + /** 原 soundSettings(Det / Wet().jth)。未迁移:原样透传。 */ + val SoundSettings: Any? = null, + /** 原 gameSettings(c2 / g2().zss)。未迁移:原样透传。 */ + val GameSettings: Any? = null, + /** 原 battleSettings(W0 / X0().esq)。未迁移:原样透传。 */ + val BattleSettings: Any? = null, + /** 原 graphicsSettings(? / lc.isu)。未迁移:原样透传。 */ + val GraphicsSettings: Any? = null, + /** 原 controlSettings(i2 / _2().ess)。未迁移:原样透传。 */ + val ControlSettings: Any? = null, + /** 原 securitySettings(V2 / pc.qsv)。未迁移:原样透传。 */ + val SecuritySettings: Any? = null, + /** 原 purchaseDialogState(CO / je.ur2)。未迁移:原样透传。 */ + val PurchaseDialogState: Any? = null, + /** 原 battleInviteState(att / Ttt().nth)。未迁移:原样透传。 */ + val BattleInviteState: Any? = null, + /** 原 keyboardSettings(Sut / bw.oth)。未迁移:原样透传。 */ + val KeyboardSettings: Any? = null, + /** 原 touchscreenHudSettings(j7 / N7().xsv)。未迁移:原样透传。 */ + val TouchscreenHudSettings: Any? = null, + /** 原 mainQuest(WZ / ih.bsk)。未迁移:原样透传。 */ + val MainQuest: Any? = null, + /** 原 language(? / m6().jtg)。未迁移:原样透传。 */ + val Language: Any? = null, + /** 原 launchParams(? / (pass-through))。未迁移:原样透传。 */ + val LaunchParams: Any? = null, + /** 原 partner(UJ / (inline))。未迁移:原样透传。 */ + val Partner: Any? = null, + /** 原 entrance(hD / Bv.pth)。未迁移:原样透传。 */ + val Entrance: Any? = null, + /** 原 antiAddiction(UC / W.iq8)。未迁移:原样透传。 */ + val AntiAddiction: Any? = null, + /** 原 spectator(HA / Li.jqm)。未迁移:原样透传。 */ + val Spectator: Any? = null, + /** 原 moderation(NI / Ei.oqh)。未迁移:原样透传。 */ + val Moderation: Any? = null, + /** 原 rulesUpdated(i1 / Ch.dsm)。未迁移:原样透传。 */ + val RulesUpdated: Any? = null, + /** 原 announcement(lC / PC().rq5)。未迁移:原样透传。 */ + val Announcement: Any? = null, + /** 原 notificationByCountry(gJ / R_.ssd)。未迁移:原样透传。 */ + val NotificationByCountry: Any? = null, + /** 原 userRenameState(O8 / Rf.pte)。未迁移:原样透传。 */ + val UserRenameState: Any? = null, + /** 原 eventGoods(W7 / Wc.esx)。未迁移:原样透传。 */ + val EventGoods: Any? = null, + /** 原 creatorTagState(qO / ze.er2)。未迁移:原样透传。 */ + val CreatorTagState: Any? = null, + /** 原 twoStepVerificationState(x9 / _f.atb)。未迁移:原样透传。 */ + val TwoStepVerificationState: Any? = null, + /** 原 rewardedVideosState(JQ / xh.zsl)。未迁移:原样透传。 */ + val RewardedVideosState: Any? = null, + /** 原 twitchState(g9 / uf.gta)。已迁移。 */ + val TwitchState: TwitchStateSlice = TwitchStateSlice(), + /** 原 icons(oH / vs.kru)。未迁移:原样透传。 */ + val Icons: Any? = null, + /** 原 itemNotifications(_1 / A1().dso)。未迁移:原样透传。 */ + val ItemNotifications: Any? = null, + /** 原 contextTutorial(Mst / Jst().tth)。未迁移:原样透传。 */ + val ContextTutorial: Any? = null, + /** 原 suppliesTutorial(? / Uw.uth)。未迁移:原样透传。 */ + val SuppliesTutorial: Any? = null, + /** 原 questCategories(zZ / th.lsh)。未迁移:原样透传。 */ + val QuestCategories: Any? = null, + /** 原 proBattle(Wtt / uv.wth)。未迁移:原样透传。 */ + val ProBattle: Any? = null, + /** 原 availabilityByRank(tet / $p.xth)。未迁移:原样透传。 */ + val AvailabilityByRank: Any? = null, + /** 原 userReportsState(Zst / qot.zth)。未迁移:原样透传。 */ + val UserReportsState: Any? = null, + /** 原 tankBuilderState(brt / wp.ati)。未迁移:原样透传。 */ + val TankBuilderState: Any? = null, + /** 原 contractState(Ont / xv.bti)。未迁移:原样透传。 */ + val ContractState: Any? = null, + /** 原 promoCode(cZ / Q_.ysg)。未迁移:原样透传。 */ + val PromoCode: Any? = null, + /** 原 referralEvent(pQ / lh.osk)。未迁移:原样透传。 */ + val ReferralEvent: Any? = null, + /** 原 supportUrl(I4 / Yl.ct7)。未迁移:原样透传。 */ + val SupportUrl: Any? = null, +) diff --git a/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt new file mode 100644 index 0000000..3fdc820 --- /dev/null +++ b/TankiOffline/app/src/jsMain/kotlin/App/State/RootWiring.kt @@ -0,0 +1,115 @@ +// 本文件由 3DTank/tools/gen_root_wiring.js 生成,不要手改。 +// 证据表:module_10032 根 reducer k6 的逐切片委派(字段 / 槽位 / 切片类 / reducer / 动作)。 +package App.State + +/** 一个切片在根状态里的委派项。 */ +data class SliceWiring( + val Field: String, + val Slot: String, + val SliceClass: String, + val Reducer: String, + val Delegate: String, + val Actions: List, +) + +object RootWiring { + val All: List = 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("screen", "zpu_1", "WK", "dY().ks5", "dY().ks5(t, n.zpu_1)", listOf("?LX", "?RX", "?cY", "?eY", "?fY", "?lY", "?oC", "?oY", "?sC", "?sY", "?uY", "DisplayMainEntranceScreen", "DisplayReauthorizationMessage", "GoTo", "PressedAnyButton")), + 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")), + SliceWiring("battleTutorial", "cpv_1", "rE", "Q.sq8", "Q.sq8(t, n.cpv_1)", listOf("?oE", "?sE", "SetFragsCount", "ShowMessage")), + SliceWiring("garage", "dpv_1", "UB", "hF", "hF(t, n)", listOf("?EU", "?FB", "?FX", "?JB", "?MU", "?NU", "?OU", "?PU", "?QU", "?RU", "?SU", "?VB", "?WB", "?aF", "?bU", "?cF", "?dU", "?eF", "?fF", "?iU", "?lF", "?pF", "?rF", "?vF", "?wU", "?yU", "AddOverdriveDescription", "AddRelatedBazooka", "AddRelatedResistanceModule", "AddToFavoriteItem", "BuyProtectionSlot", "Garage", "GarageAddItemToDepot", "GarageApplyResistanceMount", "GarageBuyDevice", "GarageDeviceTrySetInfinityLifetime", "GarageDevicesLoadedAction", "GarageRemoveDevice", "GarageRemoveDiscountForDevice", "GarageRemoveItemFromDepot", "GarageRemoveItemFromStore", "GarageResistanceUnMount", "GarageSelectAlteration", "GarageSelectItem", "GarageSelectShotSkin", "GarageSelectSkin", "GarageUpdateItemCount", "GarageUpgradeItemAction", "GarageUpgradeItemConfirm", "GarageUpgradeItemCoolDownFinished", "GarageUpgradeItemCoolDownStarted", "GoTo", "InitState", "RemoveFromFavoriteItem", "RemoveFromSale", "SetGarageSkinsAction", "SetItemRarity", "SetRestrictionMount", "SwitchingRowsScroll", "UpdateDiscount")), + SliceWiring("garagePreview", "epv_1", "IG", "fs.eru", "fs.eru(t, n.epv_1)", listOf("?BG", "?JG", "?KG", "?OG", "?QG", "?UG", "?VG", "?XG", "?YG", "?ZG", "?nH", "?tH", "AddRequestedItem", "InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions", "SetDrone", "SetExpectedItemId", "SetMountedItemsProperties", "SetPaint", "ShowRequest", "UpdatePreviewSettings")), + SliceWiring("challenges", "fpv_1", "CZ", "nh.hsj", "nh.hsj(t, n.fpv_1)", listOf("?LZ", "RemoveChallenge", "SetBattlePass", "SetBattlePassPackages", "SetChallenge", "SetRewardForDialog", "SetTakenBattlePassRewardIndex", "SetTakenFreeRewardIndex", "UpdateStars")), + SliceWiring("loginByPassword", "gpv_1", "gD", "nu.cr6", "nu.cr6(t, n.gpv_1)", listOf("?ND", "?PD", "?RD", "IncorrectLogin2FA", "RequestsNumber2FA", "SetLogin", "SetPassword", "SetRememberMe", "SetStatus")), + SliceWiring("register", "hpv_1", "Cit", "cp.stg", "cp.stg(t, n.hpv_1)", listOf("?Bit", "?Dit", "?Fit", "?Git", "?Hit", "?Lit", "?Mit", "?Uit", "?_rt", "?hrt", "EnteredUidIsBusy", "ForceSetUid", "ReceiveOAuthUri", "SetOpenedPassword", "SetPassword", "SetPasswordRestrictions", "SetUid", "SocialNetworkAvailability", "SocialNetworkClientIds")), + SliceWiring("migrationState", "ipv_1", "\$it", "Uv.rtg", "Uv.rtg(t, n.ipv_1)", listOf("?Bit", "?Dit", "?Fit", "?Git", "?Hit", "?Lit", "?_rt", "EnteredUidIsBusy", "ForceSetUid", "SetPassword", "SetUid")), + SliceWiring("inviteState", "jpv_1", "aD", "Ke.er5", "Ke.er5(t, n.jpv_1)", listOf("ApplyInvite", "SetInviteNeed", "SetInviteStatus")), + SliceWiring("captchaState", "kpv_1", "jP", "ar.iqt", "ar.iqt(t, n.kpv_1)", listOf("?AP", "?IP", "?PP", "?RP", "?SP", "LocationsUpdated", "SetLocations")), + SliceWiring("passwordChangeState", "lpv_1", "VD", "uu.pr6", "uu.pr6(t, n.lpv_1)", listOf("SetPasswordChangeHash", "ShowEmailAndPasswordChangeDialog")), + SliceWiring("system", "mpv_1", "_9", "w9().xt9", "w9().xt9(t, n.mpv_1)", listOf("DoShowMessage", "SetFatalError")), + SliceWiring("quests", "npv_1", "Frt", "Yrt.otg", "(null == zp && new Yrt(), zp).otg(t, n.npv_1)", listOf("?Hrt", "GoTo", "QuestCategory", "SetCurrentCategory")), + SliceWiring("chatState", "opv_1", "LP", "\$r.wqv", "\$r.wqv(t, n.opv_1)", listOf("?QP", "?XP", "?_C", "?bN", "?fN", "?lN", "?nN", "?pN", "?qN", "?sN", "?vN", "?wN", "ChangeChannel", "CleanUsersMessage", "Configure", "IgnoreUserMessages", "SaveChatMessage", "SendMessageToServer", "SetMessages", "SetSelectedUid", "ShowContextMenu", "ShowUserMessages", "UpdateTypingSpeedAntifloodParams", "UpdateUsers")), + SliceWiring("clanChatState", "ppv_1", "qnt", "yv.qtg", "yv.qtg(t, n.ppv_1)", listOf("?\$nt", "?Tnt", "?xnt", "Configure", "DoAddMessages")), + SliceWiring("clan", "qpv_1", "jN", "we.hr0", "we.hr0(t, n.qpv_1)", listOf("?FM", "?IM", "?KM", "?PM", "?RM", "?VM", "?YM", "?iM", "AcceptedUserViewed", "AddIncomingClan", "AddNewIncoming", "AddNewIncomingClan", "AddOutgoingClan", "AddUsersToAccepted", "AddUsersToNewAccepted", "ClanBanned", "ClanInfoLoaded", "ClanLoaded", "ClanModalVisible", "IncomingAdded", "IncomingRemoved", "IncomingUserViewed", "MembersFetched", "NewAcceptedUserNotification", "NewClanInviteNotification", "NewIncomingUserNotification", "OutgoingAdded", "OutgoingRemoved", "RemoveIncomingClan", "RemoveNewIncoming", "RemoveNewIncomingClan", "RemoveOutgoingClan", "RemoveUserFromAccepted", "RemoveUsersFromNewAccepted", "SetClanEditPrice", "SetMinRankForCreate", "UpdateClanLicense", "UpdateClanNameValidation", "UpdateClanSearchValidation", "UpdateClanTagValidation", "UpdateHasClan", "UpdateTimeToJoinRestriction", "UpdateValidation")), + SliceWiring("clansStorage", "rpv_1", "lO", "me.vr1", "me.vr1(t, n.rpv_1)", listOf("?mO", "AddUser", "AddUsersToNewAccepted", "BuyAdditionalPlace", "ClanLoaded", "ClanUnloaded", "ForeignClanLoaded", "MinimalRankChanged", "RecruitmentChanged", "RemoveUser", "RemoveUsersFromNewAccepted", "ShowForeignClan", "UpdateClan")), + SliceWiring("tipsState", "spv_1", "dst", "Hw.vth", "Hw.vth(t, n.spv_1)", listOf("UpdateTips")), + SliceWiring("news", "tpv_1", "ZX", "E_.asd", "E_.asd(t, n.tpv_1)", listOf("?hJ", "AddNewsItemAction", "RemoveNewsItemAction", "SetNewsItemAction", "SetUnreadNewsCounter")), + SliceWiring("settings", "upv_1", "Uut", "xw.ltg", "xw.ltg(t, n.upv_1)", listOf("?Fut", "?Gut")), + SliceWiring("accountSettings", "vpv_1", "oet", "Lp.mtg", "Lp.mtg(t, n.vpv_1)", listOf("?aet", "?cet", "?det", "?fet", "?get", "?ket", "?met", "?pet", "?vet", "?wet", "?xet", "ClearInputUid", "SetAccountRemovalDays")), + SliceWiring("lootBoxes", "wpv_1", "aH", "ws.wrv", "ws.wrv(t, n.wpv_1)", listOf("?CH", "?SH", "?gH", "?kH", "?mH", "?zH", "AddKeys", "SelectCategory", "SetContainerData", "SetCurrentContainer", "SetGoToGarage", "SetIsAccelerated", "SetIsOpening", "SetIsShopVisited", "SetKeys", "SetPossibleRewards", "SetRewards", "ShownRewards")), + SliceWiring("notifications", "xpv_1", "?", "B_.ise", "B_.ise(t, n.xpv_1)", listOf("?PJ", "?RJ", "AddNotification", "InitNotificationSound")), + SliceWiring("friends", "ypv_1", "KD", "NL().cr9", "NL().cr9(t, n.ypv_1)", listOf("?_L", "?hL", "AddUsersToAccepted", "AddUsersToIncoming", "AddUsersToNewAccepted", "AddUsersToNewIncoming", "ChangeFriendAddFindState", "RemoveUserFromAccepted", "RemoveUserFromIncoming", "RemoveUserFromNewAccepted", "RemoveUsersFromNewIncoming", "SetLimits", "SetValidationStatus")), + SliceWiring("remoteUserDataStorage", "zpv_1", "\$Q", "vh.isl", "vh.isl(t, n.zpv_1)", listOf("UpdateClanTags", "UpdateOnlineStatus", "UpdatePremiumStatus", "UpdateRanks", "UpdateUids", "UserEnterBattle", "UserLeaveBattle", "UserLeaveGroup")), + 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")), + SliceWiring("matchmaking", "cpw_1", "TH", "JH().qrx", "JH().qrx(t, n.cpw_1)", listOf("?WH", "?nW", "?tW", "Configure", "EnterToBattleApply", "Play", "SelectMatchmakingMode", "SetRankIndexToMinGS", "WasRegistered")), + SliceWiring("battleCreateState", "dpw_1", "\$6", "Df.ttg", "Df.ttg(t, n.dpw_1, n.bpw_1, n.ypu_1)", listOf("?A6", "?B6", "?C6", "?D6", "?E6", "?F6", "?I6", "?L6", "?M6", "?N6", "?P6", "?R6", "?T6", "?U6", "Configure", "CreateBattle", "SetRestrictions")), + SliceWiring("battleSearchState", "epw_1", "Itt", "Htt().vtg", "Htt().vtg(t, n.epw_1)", listOf("?Dtt", "EnterToBattle", "Fight", "RemoveBattle", "SelectBattle", "SelectBattleFormat", "SetFilter", "SetSearch", "SetSuspiciousFilter")), + SliceWiring("lobbyLoader", "fpw_1", "KW", "Bs.es1", "Bs.es1(t, n.fpw_1)", listOf("?FW", "?GW")), + SliceWiring("battleResult", "gpw_1", "sR", "Fi.aqp", "Fi.aqp(t, n.gpw_1)", listOf("?cR", "?fR", "?lR", "DoInit", "SetHasBattleResult", "SetRewards")), + 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("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)", "(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()), + SliceWiring("battlePauseState", "lpw_1", "dT", "NT().mqc", "NT().mqc(t, n.lpw_1)", listOf("?TT", "?xT", "OnPauseMode", "SetIdleKickPeriod")), + SliceWiring("battleViewState", "mpw_1", "DT", "sI.jqd", "(null == \$n && new sI(), \$n).jqd(t, n.mpw_1)", listOf("?JT", "?QT", "?WT", "?YT", "?ZT", "?eI", "?iI", "?nE", "?nI", "?rI", "?tE", "?tI", "SetElementsVisibility", "SetVisibility")), + SliceWiring("rankUpDialogState", "npw_1", "LO", "Se.br3", "Se.br3(t, n.npw_1)", listOf("?FO", "ShowAvailableItems")), + SliceWiring("clanUserInfo", "opw_1", "hO", "de.gr1", "de.gr1(t, n.opw_1)", listOf("Loaded")), + SliceWiring("tankActionsLogState", "ppw_1", "vI", "wi.gqe", "wi.gqe(t, n.ppw_1)", listOf("?dI", "OnUserJoinTheBattle", "OnUserLeaveTheBattle", "UpdateLog")), + SliceWiring("battleMessage", "qpw_1", "FE", "vT().rqb", "vT().rqb(t, n.qpw_1)", listOf("?sT", "DoAddLocalizedMessage", "DoAddLocalizedTeamMessage", "DoAddLocalizedUserMessage", "DoAddTextMessage", "HideMessage", "RemoveMessage")), + SliceWiring("battleChat", "rpw_1", "Q6", "Hf.wtg", "Hf.wtg(t, n.rpw_1)", listOf("?ett", "?rtt", "?stt", "?utt", "AddMessage", "ChatLoaded")), + SliceWiring("droppableGold", "spw_1", "Vet", "Wp.kth", "Wp.kth(t, n.spw_1)", listOf("?Ket", "?Yet")), + SliceWiring("home", "tpw_1", "s1", "(inline)", "(function (t, n) { return t instanceof o1 ? n.f7j(!0) : n; })(t, n.tpw_1)", listOf()), + SliceWiring("localBattleUserState", "upw_1", "lE", "(inline)", "(function (t, n) { return t instanceof fE ? n.xq8(t.yq8_1) : t instanceof vE ? n.xq8(cd, t.zq8_1) : n; })(t, n.upw_1)", listOf()), + SliceWiring("soundSettings", "vpw_1", "Det", "Wet().jth", "Wet().jth(t, n.vpw_1)", listOf("?Uet", "?_C", "SetEntranceSoundEnabled", "UpdateSettings")), + SliceWiring("gameSettings", "wpw_1", "c2", "g2().zss", "g2().zss(t, n.wpw_1)", listOf("?_C", "UpdateSettings")), + SliceWiring("battleSettings", "xpw_1", "W0", "X0().esq", "X0().esq(t, n.xpw_1)", listOf("?_C", "UpdateSettings")), + SliceWiring("graphicsSettings", "ypw_1", "?", "lc.isu", "lc.isu(t, n.ypw_1)", listOf("UpdateSettings")), + 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("?NO", "ChangeEmailStatus", "SuccessfullyPurchase")), + SliceWiring("battleInviteState", "cpx_1", "att", "Ttt().nth", "Ttt().nth(t, n.cpx_1)", listOf("?dtt", "EnableSetBattleInviteNotifications", "InvitationResponse", "InviteToBattle", "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("?VZ", "AddRewardedQuest", "QuestsLoaded", "SetBattleUpdatedQuests", "SetEventPassShopItemId", "SetQuests", "UpdateQuests")), + SliceWiring("language", "gpx_1", "?", "m6().jtg", "m6().jtg(t, n.gpx_1)", listOf("?d6")), + SliceWiring("launchParams", "hpx_1", "?", "(pass-through)", "n.hpx_1", listOf()), + SliceWiring("partner", "ipx_1", "UJ", "(inline)", "(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()), + SliceWiring("entrance", "jpx_1", "hD", "Bv.pth", "Bv.pth(t, n.jpx_1)", listOf("?eit", "?fit", "?lit", "?oit", "?sit", "?uit", "SetAgeWarnShow")), + SliceWiring("antiAddiction", "kpx_1", "UC", "W.iq8", "W.iq8(t, n.kpx_1)", listOf("?FC", "?XC", "?YC", "SetRealNameAndIdNumber", "SetValidateAntiAddictionUserDataStatus")), + SliceWiring("spectator", "lpx_1", "HA", "Li.jqm", "Li.jqm(t, n.lpx_1)", listOf("?JA", "?XA", "?YA", "?ZA", "SetSpectator", "UpdateHudLayout")), + SliceWiring("moderation", "mpx_1", "NI", "Ei.oqh", "Ei.oqh(t, n.mpx_1)", listOf("?FI", "?GI", "?JI", "?KI", "?QI", "?UI", "?WI", "?XI", "?YI", "?ZI", "?nA", "?tA", "?uA")), + 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("?bC", "?qC", "Init", "SetSkipNextPromotedAnnounce")), + SliceWiring("notificationByCountry", "ppx_1", "gJ", "R_.ssd", "R_.ssd(t, n.ppx_1)", listOf("?qJ", "Init")), + 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("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("?jO", "SetAttachedCreatorTag", "SetCreatorTag", "SetCreatorTags")), + SliceWiring("twoStepVerificationState", "tpx_1", "x9", "_f.atb", "_f.atb(t, n.tpx_1)", listOf("ActivationStatus2FA", "Enable2FaRequest", "SetSecretAndQRCode", "SetUse2FA", "VerificationStatus")), + SliceWiring("rewardedVideosState", "upx_1", "JQ", "xh.zsl", "xh.zsl(t, n.upx_1)", listOf("SetDisplayState")), + SliceWiring("twitchState", "vpx_1", "g9", "uf.gta", "uf.gta(t, n.vpx_1)", listOf("SetAccount", "SetDaysForNextLinking", "SetTwitchEnabled")), + SliceWiring("icons", "wpx_1", "oH", "vs.kru", "vs.kru(t, n.wpx_1)", listOf("SetIcons")), + SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("?S1", "?_C", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", "UpdateItemNotification")), + SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")), + SliceWiring("suppliesTutorial", "zpx_1", "?", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")), + SliceWiring("questCategories", "apy_1", "zZ", "th.lsh", "th.lsh(t, n.apy_1)", listOf("RemoveQuestCategory", "RequestQuestCategoriesStatistic", "SetQuestCategories", "SetQuestCategoriesStatistic")), + SliceWiring("proBattle", "bpy_1", "Wtt", "uv.wth", "uv.wth(t, n.bpy_1)", listOf("Fight", "SetPassword")), + SliceWiring("availabilityByRank", "cpy_1", "tet", "\$p.xth", "\$p.xth(t, n.cpy_1)", listOf("SetLoadedData")), + SliceWiring("userReportsState", "dpy_1", "Zst", "qot.zth", "(null == sd && new qot(), sd).zth(t, n.dpy_1)", listOf("InitUserReports", "ReportBattleChatMessage", "ReportLobbyChatMessage", "ReportProBattleName", "SetAvailableGameReports", "SetAvailableReports")), + SliceWiring("tankBuilderState", "epy_1", "brt", "wp.ati", "wp.ati(t, n.epy_1)", listOf("?Trt", "SetKode", "SetParts", "SetPartsInfo")), + SliceWiring("contractState", "fpy_1", "Ont", "xv.bti", "xv.bti(t, n.fpy_1)", listOf("?Hnt", "GetReward", "Init", "SetActiveContract", "SetBattleProgress", "SetContract", "UpdateProgress")), + SliceWiring("promoCode", "gpy_1", "cZ", "Q_.ysg", "Q_.ysg(t, n.gpy_1)", listOf("ActivatePromoCode", "SetCodeValidateStatus")), + SliceWiring("referralEvent", "hpy_1", "pQ", "lh.osk", "lh.osk(t, n.hpy_1)", listOf("SetPromoCode", "SetReferralEventData", "SetReferrals", "SetReferrer")), + SliceWiring("supportUrl", "ipy_1", "I4", "Yl.ct7", "Yl.ct7(t, n.ipy_1)", listOf("?R4", "SetUrl")), + ) + + /** 已迁移(由 RootReducer 真正归约)的字段。 */ + val Migrated: Set = setOf("inviteState", "loginByPassword", "twitchState") + + /** 未迁移字段:归约时原样透传,作为后续迁移清单。 */ + val Unmigrated: List get() = All.filter { it.Field !in Migrated } + + fun ByField(Field: String): SliceWiring? = All.firstOrNull { it.Field == Field } +} diff --git a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt index 63f0099..3019e08 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt @@ -6,6 +6,10 @@ // ③ 网关:按动作类的监听器在状态更新之前触发 // ④ 网关:模型订阅可退订;重复订阅直接失败 // ⑤ 网关:调度器负责把派发排进引擎循环(这里用同步调度器验证) +// ⑥ 动作目录:816 个类与 138 个真实派发名对齐 +// ⑦ 回放:真实 toString 样例的类/字段/类型校验 +// ⑧ 全目录动作类都能按 KClass 路由 +// ⑨~⑫ 切片 reducer:InviteState / LoginByPasswordState / TwitchState;⑫ 根 reducer 的 89 槽位委派 // // 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1) package App @@ -17,11 +21,32 @@ import State.StoreListener import App.State.ActionCatalog import App.State.ActionSamples import App.State.ApplyInvite +import App.State.Reducers.ByPasswordStatus import App.State.Reducers.InviteStateSlice import App.State.Reducers.InviteStatus +import App.State.Reducers.LoginByPasswordStateSlice +import App.State.Reducers.LoginOnly import App.State.Reducers.ReduceInvite +import App.State.Reducers.ReduceLoginByPassword +import App.State.Reducers.ReduceTwitch +import App.State.Reducers.ResetLoginByPassword +import App.State.Reducers.ResetLoginByPasswordStatus +import App.State.Reducers.TwitchStateSlice +import App.State.ReduceRoot +import App.State.RootReducer +import App.State.RootState +import App.State.RootWiring +import App.State.IncorrectLogin2FA +import App.State.RequestsNumber2FA +import App.State.SetAccount +import App.State.SetDaysForNextLinking import App.State.SetInviteNeed import App.State.SetInviteStatus +import App.State.SetLogin +import App.State.SetPassword +import App.State.SetRememberMe +import App.State.SetStatus +import App.State.SetTwitchEnabled private data class AddFrags(val fragsCount: Int) private data class SetScreen(val screen: String) @@ -259,6 +284,188 @@ object StateCheck { ) } + // ⑩ 切片 reducer:LoginByPasswordState(原 module_10032 的 gD / MD.cr6,枚举 kD) + run { + val S0 = LoginByPasswordStateSlice() + Check( + "切片 Login:默认值与原构造一致", + S0.Login == "" && S0.Password == "" && S0.RememberMe && S0.Status == ByPasswordStatus.NONE && + !S0.OnlyLogin && !S0.IncorrectLogin2FA && S0.RequestsNumber2FA == 0 + ) + + // 原包 SetLogin 分支是 `pr5(t.qr5_1, cd, cd, OD())`:写 login 的同时把 status 打回 NONE + val S1 = ReduceLoginByPassword( + S0.copy(Status = ByPasswordStatus.WRONG_PASSWORD), + SetLogin("pilot"), + ) + Check("切片 Login:SetLogin 写 login 并把 status 复位为 NONE", S1.Login == "pilot" && S1.Status == ByPasswordStatus.NONE) + + val S2 = ReduceLoginByPassword(S1.copy(Status = ByPasswordStatus.LOGGING_IN), SetPassword("secret")) + Check( + "切片 Login:SetPassword 写 password 并把 status 复位为 NONE", + S2.Password == "secret" && S2.Status == ByPasswordStatus.NONE && S2.Login == "pilot" + ) + + val S3 = ReduceLoginByPassword(S2.copy(Status = ByPasswordStatus.WRONG_CAPTCHA), SetRememberMe(false)) + Check("切片 Login:SetRememberMe 只改 rememberMe", !S3.RememberMe && S3.Status == ByPasswordStatus.WRONG_CAPTCHA) + + val S4 = ReduceLoginByPassword(S3, SetStatus("WRONG_PASSWORD")) + Check("切片 Login:SetStatus 按枚举名解析", S4.Status == ByPasswordStatus.WRONG_PASSWORD) + Check( + "切片 Login:SetStatus 认不出时落到 NONE", + ReduceLoginByPassword(S4, SetStatus("SOMETHING_ELSE")).Status == ByPasswordStatus.NONE + ) + + val S5 = ReduceLoginByPassword(S4, LoginOnly("uid-only")) + Check( + "切片 Login:LoginOnly 写 login/onlyLogin 且保留 status", + S5.Login == "uid-only" && S5.OnlyLogin && S5.Status == ByPasswordStatus.WRONG_PASSWORD + ) + + val S6 = ReduceLoginByPassword(S5, IncorrectLogin2FA(true)) + val S7 = ReduceLoginByPassword(S6, RequestsNumber2FA(3)) + Check( + "切片 Login:IncorrectLogin2FA / RequestsNumber2FA 各自只改本字段", + S7.IncorrectLogin2FA && S7.RequestsNumber2FA == 3 && S7.Login == "uid-only" + ) + + val S8 = ReduceLoginByPassword(S7, ResetLoginByPasswordStatus()) + Check( + "切片 Login:ResetLoginByPasswordStatus 只清 status", + S8.Status == ByPasswordStatus.NONE && S8.Login == "uid-only" && S8.RequestsNumber2FA == 3 + ) + + val S9 = ReduceLoginByPassword(S8, ResetLoginByPassword()) + Check("切片 Login:ResetLoginByPassword 整片复位", S9 == LoginByPasswordStateSlice()) + + val SA = ReduceLoginByPassword(S4, AddFrags(1)) + Check("切片 Login:无关动作不改状态", SA == S4) + + val Store = ReduxStore(S0, { S, A -> ReduceLoginByPassword(S, A) }) + var Notified = false + Store.Subscribe(StoreListener { Notified = true }) + Store.Dispatch(SetPassword("p")) + Store.Dispatch(SetStatus("LOGGING_IN")) + Check( + "切片 Login:接 Store 派发后状态已归约", + Store.State.Password == "p" && Store.State.Status == ByPasswordStatus.LOGGING_IN && Notified + ) + } + + // ⑪ 切片 reducer:TwitchState(原 module_10032 的 g9 / y9.gta) + run { + val S0 = TwitchStateSlice() + Check( + "切片 Twitch:默认值与原构造一致", + !S0.IsTwitchEnabled && S0.TwitchAccount == null && S0.DaysForNextLinking == 0 + ) + + val S1 = ReduceTwitch(S0, SetTwitchEnabled(true)) + Check("切片 Twitch:SetTwitchEnabled 只改开关", S1.IsTwitchEnabled && S1.TwitchAccount == null) + + // 样例 SetAccount(account=null) 就落在这一片:SetAccount 写的是 twitchAccount + val S2 = ReduceTwitch(S1, SetAccount("tank_tv")) + Check("切片 Twitch:SetAccount 写 twitchAccount", S2.TwitchAccount == "tank_tv" && S2.IsTwitchEnabled) + Check("切片 Twitch:SetAccount(null) 清空账号", ReduceTwitch(S2, SetAccount(null)).TwitchAccount == null) + + val S3 = ReduceTwitch(S2, SetDaysForNextLinking(7)) + Check("切片 Twitch:SetDaysForNextLinking 只改天数", S3.DaysForNextLinking == 7 && S3.TwitchAccount == "tank_tv") + Check("切片 Twitch:无关动作不改状态", ReduceTwitch(S3, AddFrags(1)) == S3) + + val Store = ReduxStore(S0, { S, A -> ReduceTwitch(S, A) }) + Store.Dispatch(SetDaysForNextLinking(2)) + Check("切片 Twitch:接 Store 派发后状态已归约", Store.State.DaysForNextLinking == 2) + } + + // ⑫ 根 reducer:89 个槽位的委派表 + 三个已迁移切片一次归约 + run { + Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89) + Check( + "根:字段名与槽位都唯一", + RootWiring.All.map { it.Field }.toSet().size == RootWiring.All.size && + RootWiring.All.map { it.Slot }.toSet().size == RootWiring.All.size + ) + Check( + "根:每个槽位都有 reducer 证据", + RootWiring.All.all { it.Reducer.isNotBlank() && it.Reducer != "(未识别)" } + ) + Check( + "根:只有 4 个内联 IIFE + 1 个纯透传未具名", + RootWiring.All.count { it.Reducer == "(inline)" } == 4 && + RootWiring.All.count { it.Reducer == "(pass-through)" } == 1 + ) + + // 已迁移三片的委派与原包一致:nu.cr6 / Ke.er5 / uf.gta + val LoginW = RootWiring.ByField("loginByPassword") + val InviteW = RootWiring.ByField("inviteState") + val TwitchW = RootWiring.ByField("twitchState") + Check( + "根:loginByPassword 委派到 gD / nu.cr6", + LoginW != null && LoginW.SliceClass == "gD" && LoginW.Reducer == "nu.cr6" + ) + Check( + "根:inviteState 委派到 aD / Ke.er5", + InviteW != null && InviteW.SliceClass == "aD" && InviteW.Reducer == "Ke.er5" + ) + Check( + "根:twitchState 委派到 g9 / uf.gta", + TwitchW != null && TwitchW.SliceClass == "g9" && TwitchW.Reducer == "uf.gta" + ) + Check( + "根:迁移表 = 三个已迁移字段,其余 86 个待迁", + RootReducer.Migrated == setOf("inviteState", "loginByPassword", "twitchState") && + RootWiring.Unmigrated.size == 86 + ) + + // 一次 ReduceRoot 里三个切片各归约自己的动作,互不串片 + val S0 = RootState(User = "user-1", Screen = "Preloader") + val S1 = ReduceRoot(S0, ApplyInvite("uid-1")) + Check( + "根:ApplyInvite 只动 inviteState", + S1.InviteState.Invite == "uid-1" && + S1.LoginByPassword == S0.LoginByPassword && + S1.TwitchState == S0.TwitchState + ) + + val S2 = ReduceRoot(S1, SetLogin("pilot")) + Check( + "根:SetLogin 只动 loginByPassword", + S2.LoginByPassword.Login == "pilot" && + S2.InviteState == S1.InviteState && + S2.TwitchState == S1.TwitchState + ) + + val S3 = ReduceRoot(S2, SetAccount("tank_tv")) + Check( + "根:SetAccount 只动 twitchState", + S3.TwitchState.TwitchAccount == "tank_tv" && + S3.InviteState == S2.InviteState && + S3.LoginByPassword == S2.LoginByPassword + ) + Check( + "根:三次归约后三个切片各自保留结果", + S3.InviteState.Invite == "uid-1" && + S3.LoginByPassword.Login == "pilot" && + S3.TwitchState.TwitchAccount == "tank_tv" + ) + + // 未迁移字段原样透传;无关动作不改变任何字段 + Check( + "根:未迁移字段原样透传", + S3.User == "user-1" && S3.Screen == "Preloader" + ) + Check("根:无关动作返回等值状态", ReduceRoot(S3, AddFrags(1)) == S3) + Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 3/89") + + val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) }) + Store.Dispatch(SetStatus("LOGGING_IN")) + Store.Dispatch(SetTwitchEnabled(true)) + Check( + "根:接 Store 派发后两片同时归约", + Store.State.LoginByPassword.Status == ByPasswordStatus.LOGGING_IN && Store.State.TwitchState.IsTwitchEnabled + ) + } + val Failed = Results.filter { !it.second } println("TankiOffline 状态层自检:${Results.size} 项") for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name) diff --git a/TankiOffline/tools/RunStateTest.ps1 b/TankiOffline/tools/RunStateTest.ps1 index 1793999..2a3c672 100644 --- a/TankiOffline/tools/RunStateTest.ps1 +++ b/TankiOffline/tools/RunStateTest.ps1 @@ -1,4 +1,4 @@ -# RunStateTest.ps1 —— 跑状态层内核自检(Store / 中间件 / 网关) +# RunStateTest.ps1 —— 跑状态层内核自检(Store / 中间件 / 网关) # # 覆盖:归约、监听器通知、中间件顺序、动作监听器时序、模型订阅退订、调度器。 # 用法:powershell -ExecutionPolicy Bypass -File .\tools\RunStateTest.ps1