Files
TankIC/TankiOffline/app/src/jsMain/kotlin/App/StateCheck.kt
T

7551 lines
360 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// StateCheck.kt —— 状态层内核自检(Node 下运行,对应 S3 的 Redux 胶水)
//
// 覆盖点(都是原 module_10849 的实际语义):
// ① Store:派发 → 归约 → 通知监听器,监听器看到的是新状态
// ② 中间件:列表靠后的包在更外层(调用顺序先外后内,返回顺序反过来)
// ③ 网关:按动作类的监听器在状态更新之前触发
// ④ 网关:模型订阅可退订;重复订阅直接失败
// ⑤ 网关:调度器负责把派发排进引擎循环(这里用同步调度器验证)
// ⑥ 动作目录:816 个类与 138 个真实派发名对齐
// ⑦ 回放:真实 toString 样例的类/字段/类型校验
// ⑧ 全目录动作类都能按 KClass 路由
// ⑨~㊿/51-69 切片 reducer:InviteState / LoginByPasswordState / TwitchState / HomeScreen / LocalBattleUserState /
// Shop(含 U7 类别表与 w3 物品表两个子 reducer)/ Screen / LootBoxes / Garage / GaragePreview / Entrance / User / Settings /
// AccountSettings / ProBattle / Language / Announcement / News / Notifications / Quests /
// LobbyLoader / PurchaseDialogState / PromoCode / SupportUrl / RulesUpdated / NotificationByCountry / AvailabilityByRank /
// Icons / QuestCategories / MainQuest / SecuritySettings / ReferralEvent /
// TipsState / RankUpDialogState / BattleResult / CaptchaState / ModalScreen / DroppableGold / EventGoods /
// CreatorTagState / RewardedVideosState / PasswordChangeState /
// System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction /
// Moderation / MigrationState / Register / UserReports /
// Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename /
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications / MatchmakingGroup;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
package App
import State.Middleware
import State.ReduxGateway
import State.ReduxStore
import State.StoreListener
import App.State.ActionCatalog
import App.State.ActionSamples
import App.State.ApplyInvite
import App.State.Reducers.ByPasswordStatus
import App.State.Reducers.HomeStateSlice
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.InviteStatus
import App.State.Reducers.LocalBattleUserStateSlice
import App.State.Reducers.LoginByPasswordStateSlice
import App.State.Reducers.LoginOnly
import App.State.Reducers.ReduceHome
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceLocalBattleUser
import App.State.Reducers.ReduceLoginByPassword
import App.State.Reducers.ReduceTwitch
import App.State.Reducers.ResetLoginByPassword
import App.State.Reducers.ResetLoginByPasswordStatus
import App.State.Reducers.ShowShopDiscountNotification
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.AddItemToOrderProcessing
import App.State.AdditionalDescription
import App.State.BraintreeLoadingInterrupt
import App.State.Category
import App.State.CurrentPayMode
import App.State.DeleteOneTimePurchaseItem
import App.State.DiscountItem
import App.State.InitAdditionalDescription
import App.State.InitAmountString
import App.State.InitCategory
import App.State.InitGroup
import App.State.InitOneTimePurchaseItem
import App.State.InitPayModeAction
import App.State.InitPaymentProviderItems
import App.State.InitPaymentViewType
import App.State.InitShopCard
import App.State.InitShopCardDiscount
import App.State.InitShopCardView
import App.State.InitShopItem
import App.State.InitShopItemName
import App.State.MobilePaymentStateChanged
import App.State.OneTimePurchaseItem
import App.State.PayMode
import App.State.PaymentProviderItem
import App.State.RemoveItemFromOrderProcessing
import App.State.SavePreviousScreen
import App.State.SaveTargetProductId
import App.State.SaveTargetVisitedCategory
import App.State.SelectCategoryId
import App.State.SelectItem
import App.State.SetBraintreeToken
import App.State.SetCoinPaymentProviderUrl
import App.State.SetInitShopItemId
import App.State.SetPurchaseCompleted
import App.State.SetStripeIntentParams
import App.State.SetupDiscountCategoryByItem
import App.State.SetupDiscountItem
import App.State.ShopCard
import App.State.ShopCardExpired
import App.State.ShopCardRemoved
import App.State.ShopCardSold
import App.State.ShopItem
import App.State.ToggleShopActivity
import App.State.TryToBuyOneTimePurchase
import App.State.AddCoins
import App.State.AddRubies
import App.State.SetBatteryLevel
import App.State.SetClanInfo
import App.State.SetCoins
import App.State.SetDisabledWithoutPremiumLocations
import App.State.SetHasTemporaryCash
import App.State.SetPremiumExpirationTime
import App.State.SetRankNames
import App.State.SetRubies
import App.State.SetUserPropertiesAction
import App.State.SetUserId
import App.State.BattleTeam
import App.State.ClearInputUid
import App.State.Fight
import App.State.Language
import App.State.PasswordStatus
import App.State.SetAccountRemovalDays
import App.State.SetSkipNextPromotedAnnounce
import App.State.SettingsCategory
import App.State.SpendCoins
import App.State.SpendCrystals
import App.State.SpendRubies
import App.State.UpdateClanTag
import App.State.UpdateCrystals
import App.State.UpdateEventPass
import App.State.UpdateGearScore
import App.State.UpdatePremium
import App.State.UpdateRank
import App.State.UpdateScore
import App.State.UpdateUid
import App.State.UpdateUserRating
import App.State.UserPropertiesCC
import App.State.Reducers.AddCategoriesWithBonus
import App.State.Reducers.BlockBackButton
import App.State.Reducers.CategoryType
import App.State.Reducers.ClearPreviousScreen
import App.State.Reducers.ClearTargetCategory
import App.State.Reducers.ClearTargetProductId
import App.State.Reducers.InitPaymentModeAsShopItem
import App.State.Reducers.MobilePaymentState
import App.State.Reducers.PaymentProviderFinishFail
import App.State.Reducers.PaymentProviderFinishSuccess
import App.State.Reducers.PaymentProviderInProgress
import App.State.Reducers.PaymentProviderState
import App.State.Reducers.PaymentUrlReceiveFailed
import App.State.Reducers.ReduceShop
import App.State.Reducers.ReduceShopCategories
import App.State.Reducers.ReduceShopItems
import App.State.Reducers.RemoveDiscountItemForItem
import App.State.Reducers.ResetPaymentState
import App.State.Reducers.ResetShop
import App.State.Reducers.SelectShopCategory
import App.State.Reducers.SetGoToHomeAfterBuy
import App.State.Reducers.SetupDiscountItemForItem
import App.State.Reducers.ShopCategoriesState
import App.State.Reducers.ShopItemsState
import App.State.Reducers.ShopLoaded
import App.State.Reducers.ShopStateSlice
import App.State.Reducers.UnblockBackButton
import App.State.RequestsNumber2FA
import App.State.SetAccount
import App.State.SetDaysForNextLinking
import App.State.SetInviteNeed
import App.State.SetInviteStatus
import App.State.SetLocalWeaponResistanceProperty
import App.State.SetLogin
import App.State.SetPassword
import App.State.SetReArmorTemporaryDisabled
import App.State.SetRememberMe
import App.State.SetStatus
import App.State.SetTwitchEnabled
import App.State.DisplayMainEntranceScreen
import App.State.DisplayReauthorizationMessage
import App.State.EntranceScreen
import App.State.GoTo
import App.State.ActivatePromoCode
import App.State.AddNewsItemAction
import App.State.AddNotification
import App.State.ChangeEmailStatus
import App.State.Init as InitNotificationByCountry
import App.State.InitNotificationSound
import App.State.NewsItemData
import App.State.RemoveNewsItemAction
import App.State.SetCodeValidateStatus
import App.State.SetCurrentCategory
import App.State.SetLoadedData
import App.State.SetNewsItemAction
import App.State.SetUnreadNewsCounter
import App.State.SetUrl
import App.State.SuccessfullyPurchase
import App.State.UpdateRules
import App.State.LoaderScreen
import App.State.NamedScreen
import App.State.PressedAnyButton
import App.State.Reducers.ClearScreenStack
import App.State.Reducers.DelayNextGoTo
import App.State.Reducers.DisableBack
import App.State.Reducers.DisableForeground
import App.State.Reducers.EnableBack
import App.State.Reducers.EnableForeground
import App.State.Reducers.FilterScreenStack
import App.State.Reducers.PopScreen
import App.State.Reducers.ReduceScreen
import App.State.Reducers.ResetScreenStackOnNextGoTo
import App.State.Reducers.ScreenStateSlice
import App.State.AddKeys
import App.State.ContainerGivenItem
import App.State.ContainerType
import App.State.SelectCategory
import App.State.SetContainerData
import App.State.SetCurrentContainer
import App.State.SetGoToGarage
import App.State.SetIsAccelerated
import App.State.SetIsOpening
import App.State.SetIsShopVisited
import App.State.SetKeys
import App.State.SetPossibleRewards
import App.State.SetRewards
import App.State.ShownRewards
import App.State.Reducers.ClearRewards
import App.State.Reducers.LootBoxOpeningFailed
import App.State.Reducers.LootBoxesStateSlice
import App.State.Reducers.ReduceLootBoxes
import App.State.Reducers.SetIsBlockOpenButtons
import App.State.Reducers.SetIsReopening
import App.State.Reducers.SetPeriodOfGiveLootBox
import App.State.Reducers.SetTimeToGiveLootBox
import App.State.Devices
import App.State.AddRelatedBazooka
import App.State.AddRelatedResistanceModule
import App.State.AddToFavoriteItem
import App.State.AddOverdriveDescription
import App.State.BuyProtectionSlot
import App.State.GarageAddItemToDepot
import App.State.GarageApplyResistanceMount
import App.State.GarageBaseItems
import App.State.GarageBuyDevice
import App.State.GarageDevice
import App.State.GarageDeviceObject
import App.State.GarageDeviceTrySetInfinityLifetime
import App.State.GarageDevicesLoadedAction
import App.State.GarageInsertDeviceClientAndServer
import App.State.GarageItem
import App.State.GarageItemModification
import App.State.GarageItemObject
import App.State.GarageItemsState
import App.State.GarageObject
import App.State.GarageRemoveDevice
import App.State.GarageRemoveDiscountForDevice
import App.State.GarageRemoveItemFromDepot
import App.State.GarageRemoveItemFromStore
import App.State.GarageResistanceUnMount
import App.State.GarageSelectAlteration
import App.State.GarageSelectItem
import App.State.GarageSelectShotSkin
import App.State.GarageSelectSkin
import App.State.GarageUpdateItemCount
import App.State.GarageUpgradeItemAction
import App.State.GarageUpgradeItemConfirm
import App.State.GarageUpgradeItemCoolDownFinished
import App.State.GarageUpgradeItemCoolDownStarted
import App.State.OverdriveDescriptionData
import App.State.RemoveFromFavoriteItem
import App.State.RemoveFromSale
import App.State.SetGarageSkinsAction
import App.State.SetItemRarity
import App.State.SetRestrictionMount
import App.State.SwitchingRowsScroll
import App.State.UpdateDiscount
import App.State.UpgradableItemParams
import App.State.AddRequestedItem
import App.State.Coloring
import App.State.EmissionColors
import App.State.GaragePreviewModelCC
import App.State.HullEngineType
import App.State.InitGarageResources
import App.State.ResetItem3dModelActionCache
import App.State.SetAgeWarnShow
import App.State.SetCaptureActions
import App.State.SetDrone
import App.State.SetExpectedItemId
import App.State.SetMountedItemsProperties
import App.State.SetPaint
import App.State.ShowRequest
import App.State.TankPartData
import App.State.TankPreviewSettings
import App.State.TextureFrameAnimation
import App.State.TurretMechanism
import App.State.UpdatePreviewSettings
import App.State.Reducers.AddGarageItemCount
import App.State.Reducers.AddNewItem
import App.State.Reducers.CaptureScreenshot
import App.State.Reducers.CaptureVideo
import App.State.Reducers.ClearDroneData
import App.State.Reducers.ClearGaragePreviewParams
import App.State.Reducers.ClearGrenadeData
import App.State.Reducers.ClearHasGrenades
import App.State.Reducers.ClearInDebt
import App.State.Reducers.DisableAccountSettings
import App.State.Reducers.DisableLobbyTutorial
import App.State.Reducers.ReduceSettings
import App.State.Reducers.ReduceUser
import App.State.Reducers.AccountSettingsStateSlice
import App.State.Reducers.AnnouncementStateSlice
import App.State.Reducers.ClearAccountRemovalTimer
import App.State.Reducers.DisableAnnouncement
import App.State.Reducers.ProBattleStateSlice
import App.State.Reducers.ReduceAccountSettings
import App.State.Reducers.ReduceAnnouncement
import App.State.Reducers.ReduceLanguage
import App.State.Reducers.ReduceProBattle
import App.State.Reducers.ResetPasswordFields
import App.State.Reducers.SetAnnouncementItems
import App.State.Reducers.SetAnnouncementModelLoaded
import App.State.Reducers.SetCountry
import App.State.Reducers.SetCurrentPassword
import App.State.Reducers.SetCurrentPasswordMandatory
import App.State.Reducers.SetEmail
import App.State.Reducers.SetEmailConfirmed
import App.State.Reducers.SetInDebt
import App.State.Reducers.SetLanguage
import App.State.Reducers.SetNewPassword
import App.State.Reducers.SetPassword as SetBattlePassword
import App.State.Reducers.SetPasswordStatus
import App.State.Reducers.SetRepeatNewPassword
import App.State.Reducers.SetSettingsCategory
import App.State.Reducers.SetUnlockButtonForRenameUid
import App.State.Reducers.SettingsStateSlice
import App.State.ChallengeScreen
import App.State.Reducers.CloseNews
import App.State.Reducers.NewsStateSlice
import App.State.Reducers.NotificationsStateSlice
import App.State.Reducers.OpenNews
import App.State.QuestCategoryScreen
import App.State.QuestSection
import App.State.Reducers.QuestsStateSlice
import App.State.Reducers.ReduceNews
import App.State.Reducers.ReduceNotifications
import App.State.Reducers.ReduceQuests
import App.State.Reducers.RemoveFirstNotification
import App.State.Reducers.RemoveNotifications
import App.State.Reducers.SetNewsFlag
import App.State.Reducers.SetNotCompletedQuests
import App.State.Reducers.AvailabilityByRankStateSlice
import App.State.Reducers.ClearSupportUrl
import App.State.Reducers.HideLoader
import App.State.Reducers.LobbyLoaderStateSlice
import App.State.Reducers.MarkHomeScreenDialogShown
import App.State.Reducers.NotificationByCountryStateSlice
import App.State.Reducers.PromoCodeStateSlice
import App.State.Reducers.PromoCodeStatus
import App.State.Reducers.PurchaseDialogStateSlice
import App.State.Reducers.PurchaseEmailStatus
import App.State.Reducers.ReduceAvailabilityByRank
import App.State.Reducers.ReduceLobbyLoader
import App.State.Reducers.ReduceNotificationByCountry
import App.State.Reducers.ReducePromoCode
import App.State.Reducers.ReducePurchaseDialog
import App.State.Reducers.ReduceRulesUpdated
import App.State.Reducers.ReduceSupportUrl
import App.State.Reducers.ResetPurchaseDialog
import App.State.Reducers.RulesUpdatedStateSlice
import App.State.Reducers.ShowLoader
import App.State.Reducers.SupportUrlStateSlice
import App.State.IconData
import App.State.SetIcons
import App.State.Reducers.IconsStateSlice
import App.State.Reducers.ReduceIcons
import App.State.QuestCategoryData
import App.State.QuestCategoryStatistic
import App.State.RemoveQuestCategory
import App.State.RequestQuestCategoriesStatistic
import App.State.SetQuestCategories
import App.State.SetQuestCategoriesStatistic
import App.State.Reducers.QuestCategoriesStateSlice
import App.State.Reducers.ReduceQuestCategories
import App.State.AddRewardedQuest
import App.State.MainQuestInfo
import App.State.QuestsLoaded
import App.State.SetBattleUpdatedQuests
import App.State.SetEventPassShopItemId
import App.State.SetQuests
import App.State.UpdateQuests
import App.State.Reducers.MainQuestStateSlice
import App.State.Reducers.ReduceMainQuest
import App.State.Reducers.ResetQuestsLoaded
import App.State.SetProfileBlockStatus
import App.State.Reducers.ReduceSecuritySettings
import App.State.Reducers.SecuritySettingsStateSlice
import App.State.ReferralEventUserModelCC
import App.State.SetPromoCode
import App.State.SetReferralEventData
import App.State.SetReferrals
import App.State.SetReferrer
import App.State.Reducers.ReduceReferralEvent
import App.State.Reducers.ReferralEventStateSlice
import App.State.UpdateTips
import App.State.Reducers.ReduceTips
import App.State.Reducers.TipsStateSlice
import App.State.ShowAvailableItems
import App.State.Reducers.RankUpDialogStateSlice
import App.State.Reducers.ReduceRankUpDialog
import App.State.Reducers.ResetRankUpDialog
import App.State.DoInit
import App.State.SetHasBattleResult
import App.State.Reducers.BattleResultStateSlice
import App.State.Reducers.ReduceBattleResult
import App.State.Reducers.ResetBattleRewards
import App.State.Reducers.SetIncreasedReward
import App.State.Reducers.SetSimpleReward
import App.State.LocationsUpdated
import App.State.SetLocations
import App.State.Reducers.AddCaptchaLocationToReset
import App.State.Reducers.AddVerifiedCaptchaLocation
import App.State.Reducers.CaptchaStateSlice
import App.State.Reducers.ReduceCaptcha
import App.State.Reducers.RemoveCaptchaLocationToReset
import App.State.Reducers.RemoveVerifiedCaptchaLocation
import App.State.Reducers.ShowFakeCaptcha
import App.State.AddModal
import App.State.RemoveCurrentByName
import App.State.RemoveModal
import App.State.Reducers.CloseCurrentModal
import App.State.Reducers.ModalScreenStateSlice
import App.State.Reducers.ReduceModalScreen
import App.State.Reducers.DroppableGoldStateSlice
import App.State.Reducers.HideDroppableGoldAuthor
import App.State.Reducers.ReduceDroppableGold
import App.State.Reducers.ShowDroppableGoldAuthor
import App.State.InitEventGoods
import App.State.Reducers.EventGoodsStateSlice
import App.State.Reducers.ReduceEventGoods
import App.State.SetAttachedCreatorTag
import App.State.SetCreatorTag
import App.State.SetCreatorTags
import App.State.Reducers.ClearCreatorTag
import App.State.Reducers.CreatorTagStateSlice
import App.State.Reducers.ReduceCreatorTag
import App.State.SetDisplayState
import App.State.Reducers.ReduceRewardedVideos
import App.State.Reducers.RewardVideoDisplayState
import App.State.Reducers.RewardedVideosStateSlice
import App.State.SetPasswordChangeHash
import App.State.ShowEmailAndPasswordChangeDialog
import App.State.Reducers.PasswordChangeStateSlice
import App.State.Reducers.ReducePasswordChange
import App.State.DoShowMessage
import App.State.SetFatalError
import App.State.Reducers.SystemStateSlice
import App.State.Reducers.ReduceSystem
import App.State.LoadCurrentElement
import App.State.MarkElementAsViewed
import App.State.SetLoadedElements
import App.State.SetMissionToElement
import App.State.SetUnViewedElements
import App.State.Reducers.ContextTutorialStateSlice
import App.State.Reducers.ReduceContextTutorial
import App.State.SetFragsCount
import App.State.ShowMessage
import App.State.Reducers.BattleTutorialStateSlice
import App.State.Reducers.MarkMovementStarted
import App.State.Reducers.ReduceBattleTutorial
import App.State.Reducers.ResetBattleTutorial
import App.State.SetSpectator
import App.State.UpdateHudLayout
import App.State.SpectatorInfo
import App.State.Reducers.AddSpectator
import App.State.Reducers.ReduceSpectator
import App.State.Reducers.RemoveSpectator
import App.State.Reducers.SetSpectators
import App.State.Reducers.SetSpectatorsListVisible
import App.State.Reducers.SpectatorStateSlice
import App.State.ActivationStatus2FA
import App.State.Enable2FaRequest
import App.State.SetSecretAndQRCode
import App.State.SetUse2FA
import App.State.VerificationStatus
import App.State.Reducers.ReduceTwoStepVerification
import App.State.Reducers.TwoStepVerificationStateSlice
import App.State.SetRealNameAndIdNumber
import App.State.SetValidateAntiAddictionUserDataStatus
import App.State.Reducers.AntiAddictionStateSlice
import App.State.Reducers.EnableRealNameCheck
import App.State.Reducers.ReduceAntiAddiction
import App.State.Reducers.RequireUserIdVerification
import App.State.Reducers.UpdatePlayingTime
import App.State.Reducers.AddSelectedUser
import App.State.Reducers.AddSelectedUserFromChat
import App.State.Reducers.ClearSelectedUsers
import App.State.Reducers.HideBattleInfo
import App.State.Reducers.HideBlockUserMenu
import App.State.Reducers.HideModerationSystemMessage
import App.State.Reducers.ModerationStateSlice
import App.State.Reducers.ModerationUserRecord
import App.State.Reducers.ReduceModeration
import App.State.Reducers.RemoveSelectedUser
import App.State.Reducers.ResetSelectedUsers
import App.State.Reducers.SetModerationUrlTemplates
import App.State.Reducers.SetModeratorModes
import App.State.Reducers.ShowBattleInfo
import App.State.Reducers.ShowBlockUserMenu
import App.State.Reducers.ShowModerationSystemMessage
import App.State.EnteredUidIsBusy
import App.State.ForceSetUid
import App.State.SetUid
import App.State.ReceiveOAuthUri
import App.State.SetOpenedPassword
import App.State.SetPasswordRestrictions
import App.State.SocialNetworkAvailability
import App.State.SocialNetworkClientIds
import App.State.Reducers.DisableRegister
import App.State.Reducers.MigrationLoginStatus
import App.State.Reducers.MigrationRegisterStatus
import App.State.Reducers.MigrationStateSlice
import App.State.Reducers.ReduceMigrationState
import App.State.Reducers.ReduceRegister
import App.State.Reducers.RegisterFailed
import App.State.Reducers.RegisterPasswordCorrect
import App.State.Reducers.RegisterPasswordIncorrect
import App.State.Reducers.RegisterStateSlice
import App.State.Reducers.RegistrationStatus
import App.State.Reducers.ResetAuthenticationForm
import App.State.Reducers.ResetRegisterStatus
import App.State.Reducers.ResetUidStatus
import App.State.Reducers.UidIsFree
import App.State.Reducers.UidIsIncorrect
import App.State.Reducers.UidStatus
import App.State.Reducers.UidTooShort
import App.State.InitUserReports
import App.State.ReportBattleChatMessage
import App.State.ReportLobbyChatMessage
import App.State.ReportProBattleName
import App.State.SetAvailableGameReports
import App.State.SetAvailableReports
import App.State.Reducers.ReduceUserReports
import App.State.Reducers.ReportEntry
import App.State.Reducers.ReportKind
import App.State.Reducers.UserReportsStateSlice
import App.State.ChangeFriendAddFindState
import App.State.AddUsersToAccepted
import App.State.AddUsersToIncoming
import App.State.AddUsersToNewAccepted
import App.State.AddUsersToNewIncoming
import App.State.RemoveUserFromAccepted
import App.State.RemoveUserFromIncoming
import App.State.RemoveUserFromNewAccepted
import App.State.RemoveUsersFromNewIncoming
import App.State.SetLimits
import App.State.SetValidationStatus
import App.State.Reducers.ClearContextMenuUser
import App.State.Reducers.FriendSearchState
import App.State.Reducers.FriendsStateSlice
import App.State.Reducers.ReduceFriends
import App.State.Reducers.SetContextMenuUser
import App.State.Reducers.ReduceSupplies
import App.State.Reducers.SetSupplyCount
import App.State.Reducers.SuppliesStateSlice
import App.State.ClanNotifierData
import App.State.Loaded
import App.State.Reducers.ClanUserInfoStateSlice
import App.State.Reducers.ReduceClanUserInfo
import App.State.EnableSetBattleInviteNotifications
import App.State.InvitationResponse
import App.State.RemoveUserFromInvited
import App.State.SelectUserToInvite
import App.State.Reducers.AddInvitedUser
import App.State.Reducers.BattleInviteStateSlice
import App.State.Reducers.ClearSelectedForInvite
import App.State.Reducers.ReduceBattleInvite
import App.State.InitPartnerInviteAction
import App.State.InitSocialNetworkParameters
import App.State.SetLoginParams
import App.State.SetLoginStatusForBinding
import App.State.SetPartnerId
import App.State.SetPartnerParameters
import App.State.SetPartnerReady
import App.State.Reducers.MarkAccountLinked
import App.State.Reducers.MarkAccountNotLinked
import App.State.Reducers.PartnerStateSlice
import App.State.Reducers.ReducePartner
import App.State.Reducers.SetPartnerItemEnabled
import App.State.Reducers.UpdatePartnerInviteAction
import App.State.ChangeUidInputState
import App.State.ResponseActual
import App.State.ResponseDelivered
import App.State.SetChangeUidParams
import App.State.SetRubiesForRename
import App.State.Reducers.HideLinkErrorWarning
import App.State.Reducers.MarkFreeRenameAvailable
import App.State.Reducers.MarkRenameFailed
import App.State.Reducers.MarkRenameSucceeded
import App.State.Reducers.ReduceUserRename
import App.State.Reducers.ResetRenameDialog
import App.State.Reducers.RenameUidState
import App.State.Reducers.UserRenameStateSlice
import App.State.OnPauseMode
import App.State.SetIdleKickPeriod
import App.State.Reducers.BattlePauseStateSlice
import App.State.Reducers.CancelAutoPause
import App.State.Reducers.ReduceBattlePause
import App.State.Reducers.ResetBattlePause
import App.State.SetElementsVisibility
import App.State.SetVisibility
import App.State.Reducers.BattleViewStateSlice
import App.State.Reducers.ClearInvisibleElements
import App.State.Reducers.DisableBattleInput
import App.State.Reducers.DisableBattleMenu
import App.State.Reducers.DisableBattleStatistics
import App.State.Reducers.EnableBattleInput
import App.State.Reducers.EnableBattleMenu
import App.State.Reducers.EnableBattleStatistics
import App.State.Reducers.EnterBackground
import App.State.Reducers.HideBattleView
import App.State.Reducers.LeaveBackground
import App.State.Reducers.ReduceBattleView
import App.State.Reducers.ResetBattleView
import App.State.Reducers.ShowBattleView
import App.State.OnUserJoinTheBattle
import App.State.OnUserLeaveTheBattle
import App.State.UpdateLog
import App.State.Reducers.ReduceTankActionsLog
import App.State.Reducers.ResetTankActionsLog
import App.State.Reducers.TankActionsLogStateSlice
import App.State.ActivatedInCurrentBattle
import App.State.SetLoadedElement
import App.State.Reducers.ReduceSuppliesTutorial
import App.State.Reducers.SuppliesTutorialStateSlice
import App.State.Configure
import App.State.DoAddMessages
import App.State.Reducers.ClanChatStateSlice
import App.State.Reducers.CloseClanChat
import App.State.Reducers.OpenClanChat
import App.State.Reducers.ReduceClanChat
import App.State.Reducers.ResetClanChat
import App.State.BattleFormat
import App.State.EnterToBattle
import App.State.RemoveBattle
import App.State.SelectBattle
import App.State.SelectBattleFormat
import App.State.SetFilter
import App.State.SetSearch
import App.State.SetSuspiciousFilter
import App.State.Reducers.BattleSearchStateSlice
import App.State.Reducers.ClearSelectedBattle
import App.State.Reducers.ReduceBattleSearch
import App.State.InitBattleModeWithLevels
import App.State.Range
import App.State.SetBattleMode
import App.State.SetDisplayClientInfo
import App.State.SetFlagOrBallOwner
import App.State.SetRankRange
import App.State.SetReArmor
import App.State.SetRemainingTime
import App.State.SetRoundRestartTime
import App.State.UpdateFund
import App.State.Reducers.BattleStatisticsStateSlice
import App.State.Reducers.BeginRound
import App.State.Reducers.ClearFlagOrBallOwner
import App.State.Reducers.CloseStatMenu
import App.State.Reducers.ContinueBattle
import App.State.Reducers.HideStat
import App.State.Reducers.InitBattle
import App.State.Reducers.MarkBattleLoaded
import App.State.Reducers.MarkBattleUnloaded
import App.State.Reducers.ReduceBattleStatistics
import App.State.Reducers.ResetBattleStatistics
import App.State.Reducers.ResetRoundState
import App.State.Reducers.ShowStat
import App.State.Reducers.ToggleStatMenu
import App.State.ChangeAction
import App.State.RemoveAction
import App.State.ResetAction
import App.State.Reducers.ApplyGameSettings
import App.State.Reducers.ApplySoundSettings
import App.State.Reducers.ApplyTouchscreenHudSettings
import App.State.Reducers.EnableEntranceSound
import App.State.Reducers.GameSettingsStateSlice
import App.State.Reducers.KeyboardSettingsStateSlice
import App.State.Reducers.ReduceGameSettings
import App.State.Reducers.ReduceKeyboardSettings
import App.State.Reducers.ReduceSoundSettings
import App.State.Reducers.ReduceTouchscreenHudSettings
import App.State.Reducers.ResetKeyMappingFromDefaults
import App.State.Reducers.ResetSettingsFromStorage
import App.State.Reducers.ResetTouchscreenHudMapping
import App.State.Reducers.SetEntranceSoundSettings
import App.State.Reducers.SoundSettingsStateSlice
import App.State.Reducers.TouchscreenHudSettingsStateSlice
import App.State.Reducers.ApplyBattleSettings
import App.State.Reducers.ApplyControlSettings
import App.State.Reducers.ApplyGraphicsSettings
import App.State.Reducers.BattleSettingsStateSlice
import App.State.Reducers.ControlGamepadSettings
import App.State.Reducers.ControlMouseSettings
import App.State.Reducers.ControlSettingsStateSlice
import App.State.Reducers.ControlTouchscreenSettings
import App.State.Reducers.GraphicsSettingsStateSlice
import App.State.Reducers.ReduceBattleSettings
import App.State.Reducers.ReduceControlSettings
import App.State.Reducers.ReduceGraphicsSettings
import App.State.SetActiveContract
import App.State.SetBattleProgress
import App.State.SetContract
import App.State.UpdateProgress
import App.State.Reducers.ClaimContractReward
import App.State.Reducers.ContractEntry
import App.State.Reducers.ContractStateSlice
import App.State.Reducers.ReduceContractState
import App.State.Reducers.RemoveCompletedContracts
import App.State.SetKode
import App.State.SetParts
import App.State.SetPartsInfo
import App.State.Reducers.ReduceTankBuilder
import App.State.Reducers.ResetTankBuilder
import App.State.Reducers.TankBuilderPaintInfo
import App.State.Reducers.TankBuilderPart
import App.State.Reducers.TankBuilderPartInfo
import App.State.Reducers.TankBuilderStateSlice
import App.State.DoAddLocalizedMessage
import App.State.DoAddLocalizedTeamMessage
import App.State.DoAddLocalizedUserMessage
import App.State.DoAddTextMessage
import App.State.HideMessage
import App.State.RemoveMessage
import App.State.Reducers.BattleMessageEntry
import App.State.Reducers.BattleMessageStateSlice
import App.State.Reducers.LocalizedMessage
import App.State.Reducers.LocalizedTeamMessage
import App.State.Reducers.LocalizedUserMessage
import App.State.Reducers.ReduceBattleMessage
import App.State.Reducers.ResetBattleMessages
import App.State.Reducers.TextMessage
import App.State.AddMap
import App.State.MapInfoCC
import App.State.Reducers.MapsDescriptionEntry
import App.State.Reducers.MapsStateSlice
import App.State.Reducers.MapsThemeEntry
import App.State.Reducers.ReduceMaps
import App.State.AddMessage
import App.State.ChatLoaded
import App.State.ChatMessage
import App.State.Reducers.BattleChatStateSlice
import App.State.Reducers.CloseBattleChatInput
import App.State.Reducers.OpenBattleChatInput
import App.State.Reducers.ReduceBattleChat
import App.State.Reducers.ResetBattleChat
import App.State.Reducers.ToggleBattleChatHistory
import App.State.BattleNotifierData
import App.State.RemoteUserData
import App.State.Reducers.ReduceRemoteUserDataStorage
import App.State.Reducers.RemoteUserDataStorageStateSlice
import App.State.UpdateClanTags
import App.State.UpdateOnlineStatus
import App.State.UpdateRanks
import App.State.UpdateUids
import App.State.UserEnterBattle
import App.State.UserLeaveBattle
import App.State.UserLeaveGroup
import App.State.EnterToBattleApply
import App.State.MatchmakingModeRank
import App.State.Play
import App.State.SelectMatchmakingMode
import App.State.SetRankIndexToMinGS
import App.State.WasRegistered
import App.State.Reducers.CancelLoadingBattle
import App.State.Reducers.MatchmakingStateSlice
import App.State.Reducers.ReduceMatchmaking
import App.State.Reducers.ResetMatchmakingProgress
import App.State.Reducers.StartLoadingBattle
import App.State.AddBattle
import App.State.AddUser
import App.State.AddUsers
import App.State.BattleCreateParameters
import App.State.BattleInfoUser
import App.State.BattleParams
import App.State.ChangeName
import App.State.EnterToBattleInfo
import App.State.InitProBattleRewardLimit
import App.State.LimitReached
import App.State.ProBattleRewardLimitData
import App.State.RemoveUser
import App.State.RoundFinish
import App.State.RoundStarted
import App.State.SetReEnterProtectBattleId
import App.State.SwapTeam
import App.State.UpdateSuspicion
import App.State.UpdateTeamScore
import App.State.UpdateUserScore
import App.State.UpdateUserSuspicion
import App.State.Reducers.BattlesSelectStateSlice
import App.State.Reducers.MarkBattlesLoaded
import App.State.Reducers.ReduceBattlesSelect
import App.State.Reducers.ResetBattlesSelect
import App.State.InitSuspiciousState
import App.State.InitUserLevels
import App.State.SetGearScore
import App.State.SetTankInfo
import App.State.SetTankResistance
import App.State.TankInfoCC
import App.State.TankResistance
import App.State.UpdateUserLevel
import App.State.UpdateUserRank
import App.State.UpdateUserScoreKillsAndDeaths
import App.State.UpdateUserSuspiciousState
import App.State.UpdateUsersClanTag
import App.State.UpdateUsersScoreKillsAndDeaths
import App.State.Reducers.AddBattleUser
import App.State.Reducers.AddBattleUsers
import App.State.Reducers.BattleUsersStateSlice
import App.State.Reducers.MigrateBattleUserTeams
import App.State.Reducers.ReduceBattleUsers
import App.State.Reducers.RemoveOnlineUser
import App.State.Reducers.ResetBattleUsers
import App.State.Reducers.SetUserPremium
import App.State.RemoveTankInfo
import App.State.UserStat
import App.State.Challenge
import App.State.RemoveChallenge
import App.State.SetBattlePass
import App.State.SetBattlePassPackages
import App.State.SetChallenge
import App.State.SetRewardForDialog
import App.State.SetTakenBattlePassRewardIndex
import App.State.SetTakenFreeRewardIndex
import App.State.UpdateStars
import App.State.Reducers.ChallengesStateSlice
import App.State.Reducers.ReduceChallenges
import App.State.Reducers.RefreshBattlePasses
import App.State.ClanEditPriceCC
import App.State.ClanInfo
import App.State.ClanUnloaded
import App.State.BuyAdditionalPlace
import App.State.ForeignClanLoaded
import App.State.MinimalRankChanged
import App.State.RecruitmentChanged
import App.State.ShowForeignClan
import App.State.UpdateClan
import App.State.Reducers.AddClanUser
import App.State.Reducers.LoadClanIntoStorage
import App.State.Reducers.ClanStorageStateSlice
import App.State.Reducers.ClearForeignClan
import App.State.Reducers.ReduceClanStorage
import App.State.Reducers.RemoveClanUser
import App.State.AddIncomingClan
import App.State.AddNewIncoming
import App.State.AddNewIncomingClan
import App.State.AddOutgoingClan
import App.State.AcceptedUserViewed
import App.State.ClanBanned
import App.State.ClanModalVisible
import App.State.IncomingAdded
import App.State.IncomingRemoved
import App.State.IncomingUserViewed
import App.State.MembersFetched
import App.State.NewAcceptedUserNotification
import App.State.NewClanInviteNotification
import App.State.NewIncomingUserNotification
import App.State.OutgoingAdded
import App.State.OutgoingRemoved
import App.State.ClanInfoLoaded
import App.State.RemoveIncomingClan
import App.State.RemoveNewIncoming
import App.State.RemoveNewIncomingClan
import App.State.RemoveOutgoingClan
import App.State.SetClanEditPrice
import App.State.SetMinRankForCreate
import App.State.UpdateClanLicense
import App.State.UpdateClanNameValidation
import App.State.UpdateHasClan
import App.State.UpdateTimeToJoinRestriction
import App.State.UpdateValidation
import App.State.Reducers.ClanStateSlice
import App.State.Reducers.DecrementClanNotifications
import App.State.Reducers.IncrementClanNotifications
import App.State.Reducers.ReduceClan
import App.State.Reducers.ResetClanNameTagValidation
import App.State.Reducers.ResetClanUserValidation
import App.State.Reducers.SetClanNotificationsCount
import App.State.Reducers.UpdateIntermediateDescription
import App.State.Reducers.UpdateIntermediateMinRank
import App.State.Reducers.UpdateIntermediateRequests
import App.State.ChangeChannel
import App.State.CleanUsersMessage
import App.State.IgnoreUserMessages
import App.State.SaveChatMessage
import App.State.SendMessageToServer
import App.State.SetMessages
import App.State.SetSelectedUid
import App.State.ShowContextMenu
import App.State.ShowUserMessages
import App.State.UpdateTypingSpeedAntifloodParams
import App.State.UpdateUsers
import App.State.Reducers.CancelHideChatKeyboard
import App.State.Reducers.ChatStateSlice
import App.State.Reducers.ClearChatContextMenu
import App.State.Reducers.CloseChat
import App.State.Reducers.DisableChatScroll
import App.State.Reducers.EnableChatScroll
import App.State.Reducers.HideChatKeyboard
import App.State.Reducers.MarkFirstChannelMessagesLoaded
import App.State.Reducers.OpenChat
import App.State.Reducers.ReduceChat
import App.State.Reducers.RequireHideChatKeyboard
import App.State.Reducers.ResetChat
import App.State.Reducers.SetChatConfig
import App.State.Reducers.ShowChatKeyboard
import App.State.AddItemNotification
import App.State.AddItemNotifications
import App.State.ItemNotificationData
import App.State.RemoveItemNotification
import App.State.UpdateItemNotification
import App.State.Reducers.ItemNotificationsStateSlice
import App.State.Reducers.PersistItemNotifications
import App.State.Reducers.ReduceItemNotifications
import App.State.AcceptedInviteToGroup
import App.State.AddUserToSelected
import App.State.InitMatchmakingGroup
import App.State.Invite
import App.State.RejectedInviteToGroup
import App.State.RemoveUserToSelected
import App.State.SetAllowedFromFriends
import App.State.SetSelectedUsers
import App.State.UpdateMountItems
import App.State.UpdateUserReadiness
import App.State.UserRemoved
import App.State.Reducers.AddGroupMember
import App.State.Reducers.MatchmakingGroupMember
import App.State.Reducers.MatchmakingGroupStateSlice
import App.State.Reducers.ReduceMatchmakingGroup
import App.State.Reducers.ResetMatchmakingGroup
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
import App.State.Reducers.ClearUpgradeItemInProcessing
import App.State.Reducers.ClearWaitServerItemMount
import App.State.Reducers.ClearWeaponData
import App.State.Reducers.GarageDefaultCategory
import App.State.Reducers.GarageLoaded
import App.State.Reducers.GarageMountItem
import App.State.Reducers.GarageMountItems
import App.State.Reducers.GaragePreviewStateSlice
import App.State.Reducers.GarageResistanceCategoryKey
import App.State.Reducers.GarageStateSlice
import App.State.Reducers.GarageUnmountItem
import App.State.Reducers.HideGaragePreview
import App.State.Reducers.HideVideoInDescription
import App.State.Reducers.PreviewDroneCategory
import App.State.Reducers.PreviewHullCategory
import App.State.Reducers.PreviewPaintCategory
import App.State.Reducers.PreviewWeaponCategory
import App.State.Reducers.ReduceGarage
import App.State.Reducers.EntranceStateSlice
import App.State.Reducers.HideSocialNetworks
import App.State.Reducers.ReduceEntrance
import App.State.Reducers.ReduceGaragePreview
import App.State.Reducers.RemoveNewItem
import App.State.Reducers.SetAuthorizedByChinaSdkToken
import App.State.Reducers.SetClassic
import App.State.Reducers.SetControlsForChina
import App.State.Reducers.SetRunningInChinaDataCenter
import App.State.Reducers.SetRunningInChinaPartners
import App.State.Reducers.RemoveWaitServerItemMount
import App.State.Reducers.ResetGarage
import App.State.Reducers.ResetMountDelay
import App.State.Reducers.SelectGarageCategory
import App.State.Reducers.SetCooldownReduceEndTime
import App.State.Reducers.SetGarageNotLoaded
import App.State.Reducers.SetGarageObject
import App.State.Reducers.SetGrenadeData
import App.State.Reducers.SetHasGrenades
import App.State.Reducers.SetHullData
import App.State.Reducers.SetItemsOnDepot
import App.State.Reducers.SetItemsOnMarket
import App.State.Reducers.SetMountedItems
import App.State.Reducers.SetMountedShotSkinForItems
import App.State.Reducers.SetMountedSkinForItems
import App.State.Reducers.SetNewItems
import App.State.Reducers.SetResistanceReadyToMount
import App.State.Reducers.SetWeaponData
import App.State.Reducers.ShowGarageDiscountNotification
import App.State.Reducers.ShowGaragePreview
import App.State.Reducers.StartCooldownReduce
import App.State.Reducers.UpdateViewStyle
import State.InitState
private data class AddFrags(val fragsCount: Int)
private data class SetScreen(val screen: String)
private data class UiState(val Frags: Int = 0, val Screen: String = "Preloader")
private val ExitWith: (Int) -> Unit = js("(function (C) { process.exit(C); })")
/** 按顶层逗号切分(尊重圆括号/方括号/花括号)。 */
private fun SplitTop(Text: String): List<String> {
val Out = ArrayList<String>()
var Depth = 0
val Cur = StringBuilder()
for (C in Text) {
when (C) {
'(', '[', '{' -> { Depth++; Cur.append(C) }
')', ']', '}' -> { Depth--; Cur.append(C) }
',' -> if (Depth == 0) { Out.add(Cur.toString()); Cur.clear() } else Cur.append(C)
else -> Cur.append(C)
}
}
if (Cur.isNotBlank()) Out.add(Cur.toString())
return Out
}
/** 运行时值形态是否与生成类里声明的类型相符(只做能一锤定音的判断)。 */
private fun ValueFits(Type: String, Value: String): Boolean {
val V = Value.trim()
if (V.isEmpty()) return true
// null 只允许出现在可空类型上
if (V == "null") return Type == "Any?" || Type.endsWith("?")
return when {
Type == "Any?" || Type == "Any" -> true
Type == "Boolean" -> V == "true" || V == "false"
Type == "Int" || Type == "Long" -> Regex("-?\\d+").matches(V)
Type == "Double" -> Regex("-?\\d+(\\.\\d+)?([eE][-+]?\\d+)?").matches(V)
Type == "String" -> true
Type.startsWith("List<") -> V.startsWith("[")
Type.startsWith("Map<") -> V.startsWith("{")
else -> true
}
}
object StateCheck {
fun Wanted(): Boolean = js("process.env.TANKIOFFLINE_STATE_TEST === '1'") as Boolean
fun Run() {
val Results = ArrayList<Pair<String, Boolean>>()
fun Check(Name: String, Condition: Boolean) { Results.add(Name to Condition) }
// ① Store:归约 + 监听器
run {
val Store = ReduxStore(UiState(), { S, A ->
when (A) {
is AddFrags -> S.copy(Frags = S.Frags + A.fragsCount)
is SetScreen -> S.copy(Screen = A.screen)
else -> S
}
})
var Notified = 0
var Seen = -1
Store.Subscribe(StoreListener { Notified++; Seen = Store.State.Frags })
Store.Dispatch(AddFrags(3))
Store.Dispatch(AddFrags(4))
Check("Store 归约:两次派发累加", Store.State.Frags == 7)
Check("Store 通知:每次状态变化通知一次", Notified == 2)
Check("Store 通知:监听器看到的是新状态", Seen == 7)
}
// ② 中间件顺序
run {
val Log = ArrayList<String>()
val First = Middleware<UiState> { _, Next ->
{ A -> Log.add("First.In"); val R = Next(A); Log.add("First.Out"); R }
}
val Second = Middleware<UiState> { _, Next ->
{ A -> Log.add("Second.In"); val R = Next(A); Log.add("Second.Out"); R }
}
val Store = ReduxStore(UiState(), { S, A -> if (A is AddFrags) S.copy(Frags = S.Frags + A.fragsCount) else S }, listOf(First, Second))
Store.Dispatch(AddFrags(1))
Check("中间件:靠后的包在更外层", Log == listOf("Second.In", "First.In", "First.Out", "Second.Out"))
}
// ③ 网关:动作监听器先于状态更新
run {
val Store = ReduxStore(UiState(), { S, A ->
if (A is AddFrags) S.copy(Frags = S.Frags + A.fragsCount) else S
})
val Gateway = ReduxGateway(Store) { Task -> Task() }
var FragsAtListener = -1
Gateway.OnAction(AddFrags::class) { FragsAtListener = Gateway.StoreState.Frags }
Gateway.Dispatch(AddFrags(5))
Check("网关:动作监听器在归约之前触发", FragsAtListener == 0)
Check("网关:派发后状态已更新", Gateway.StoreState.Frags == 5)
}
// ④ 模型订阅 / 退订 / 重复订阅
run {
val Store = ReduxStore(UiState(), { S, A -> S })
val Gateway = ReduxGateway(Store) { Task -> Task() }
val Model = Any()
var Hits = 0
Gateway.SubscribeModel(Model) { Scope -> Scope.Listen(SetScreen::class) { Hits++ } }
Gateway.Dispatch(SetScreen("Lobby"))
val AfterFirst = Hits
Gateway.UnsubscribeModel(Model)
Gateway.Dispatch(SetScreen("Garage"))
Check("网关:模型订阅收到动作", AfterFirst == 1)
Check("网关:退订后不再收到动作", Hits == 1)
val Twin = Any()
Gateway.SubscribeModel(Twin) { }
val DuplicateThrew = try {
Gateway.SubscribeModel(Twin) { }
false
} catch (E: IllegalArgumentException) {
true
}
Check("网关:重复订阅模型直接失败", DuplicateThrew)
}
// ⑤ 调度器:派发确实经过调度器
run {
val Store = ReduxStore(UiState(), { S, A -> if (A is AddFrags) S.copy(Frags = S.Frags + A.fragsCount) else S })
var Scheduled = 0
val Gateway = ReduxGateway(Store) { Task -> Scheduled++; Task() }
Gateway.Dispatch(AddFrags(2))
Check("网关:派发经过调度器", Scheduled == 1 && Store.State.Frags == 2)
}
// ⑥ 动作目录(由 module_10032 的 816 个数据类生成)
run {
val All = ActionCatalog.All
Check("动作目录:非空", All.isNotEmpty())
Check("动作目录:名字唯一", All.map { it.Name }.toSet().size == All.size)
Check("动作目录:每个条目都能拿到 Kotlin 类", All.all { it.Type.simpleName != null })
Check("动作目录:运行时 dispatch 过的动作名全部在册", ActionCatalog.MissingTraced().isEmpty())
Check("动作目录:ByName 与 All 对齐", ActionCatalog.ByName.size == All.size)
println(" 目录 ${All.size} 条;打点动作名 ${ActionCatalog.TracedNames.size} 个;" +
"缺口 ${ActionCatalog.MissingTraced().size} 个")
}
// ⑦ 回放对照:用运行时真实打点原文校验目录里的类与字段
run {
var Actions = 0
val NoClass = ArrayList<String>()
var MissingField = 0
var TypeMismatch = 0
val MismatchSamples = ArrayList<String>()
for ((Name, Raw) in ActionSamples.Raw) {
Actions++
val Meta = ActionCatalog.ByName[Name]
if (Meta == null) { NoClass.add(Name); continue }
val Open = Raw.indexOf('(')
val Close = Raw.lastIndexOf(')')
if (Open < 0 || Close <= Open) continue
val Body = Raw.substring(Open + 1, Close)
if (Body.isBlank()) continue
for (Part in SplitTop(Body)) {
val Eq = Part.indexOf('=')
if (Eq < 0) continue
val Field = Part.substring(0, Eq).trim()
val Value = Part.substring(Eq + 1).trim()
val Decl = Meta.Fields.firstOrNull { it.Name == Field }
if (Decl == null) {
MissingField++
if (MismatchSamples.size < 8) MismatchSamples.add("$Name.$Field 不在类里")
continue
}
if (!ValueFits(Decl.Type, Value)) {
TypeMismatch++
if (MismatchSamples.size < 8) {
MismatchSamples.add("类型不符 $Name.$Field 声明 ${Decl.Type} 实际 $Value")
}
}
}
}
Check("回放:样例里的动作名都能在目录里找到", NoClass.isEmpty())
Check("回放:样例字段都能在类里找到", MissingField == 0)
Check("回放:字段值形态与声明类型一致", TypeMismatch == 0)
println(" 回放 $Actions 个动作;缺类 ${NoClass.size};缺字段 $MissingField;类型不符 $TypeMismatch")
for (S in MismatchSamples) println(" · " + S.replace(Regex("\\s+"), " ").take(120))
}
// ⑧ 全目录动作类注册:验证 KClass 身份唯一、按类路由正确
run {
val Store = ReduxStore(UiState(), { S, _ -> S })
val Gateway = ReduxGateway(Store) { Task -> Task() }
var Hits = 0
for (Meta in ActionCatalog.All) Gateway.OnAction(Meta.Type) { Hits++ }
Gateway.OnAction(AddFrags::class) { Hits++ }
Gateway.Dispatch(AddFrags(1))
Check("网关:全目录 ${ActionCatalog.All.size} 个动作类可注册且只路由命中项", Hits == 1)
}
// ⑨ 切片 reducer:InviteState(原 module_10032 的 aD / pD.er5)
run {
val S0 = InviteStateSlice()
Check(
"切片 Invite:默认值与原构造一致",
S0.IsInviteNeed && S0.Invite == "" && S0.Status == InviteStatus.UNKNOWN &&
S0.ExistsUid == null && !S0.InviteAccepted
)
val S1 = ReduceInvite(S0, SetInviteNeed(false))
Check("切片 Invite:SetInviteNeed 只改 isInviteNeed", !S1.IsInviteNeed && S1.Status == S0.Status)
val S2 = ReduceInvite(S1, SetInviteStatus("FREE", 12345L))
Check(
"切片 Invite:SetInviteStatus 更新 status/existsUid/inviteAccepted",
S2.Status == InviteStatus.FREE && S2.ExistsUid == 12345L && S2.InviteAccepted
)
val S3 = ReduceInvite(S2, SetInviteStatus("NOT_FOUND", null))
Check("切片 Invite:非 FREE 状态 inviteAccepted 归 false", !S3.InviteAccepted && S3.Status == InviteStatus.NOT_FOUND)
val S4 = ReduceInvite(S3, ApplyInvite("INVITE-CODE"))
Check(
"切片 Invite:ApplyInvite 写入邀请码并重置 status/existsUid",
S4.Invite == "INVITE-CODE" && S4.Status == InviteStatus.UNKNOWN && S4.ExistsUid == null &&
!S4.InviteAccepted
)
val S5 = ReduceInvite(S4, AddFrags(1))
Check("切片 Invite:无关动作不改状态", S5 == S4)
val Store = ReduxStore(S0, { S, A -> ReduceInvite(S, A) })
var Notified = false
Store.Subscribe(StoreListener { Notified = true })
Store.Dispatch(SetInviteStatus("ALREADY_ACTIVATED", 7L))
Check(
"切片 Invite:接 Store 派发后状态已归约",
Store.State.Status == InviteStatus.ALREADY_ACTIVATED && Store.State.ExistsUid == 7L && Notified
)
}
// ⑩ 切片 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:HomeScreen(原 module_10032 的 s1 + 根 reducer `(inline@home)` 分支)
run {
val S0 = HomeStateSlice()
Check("切片 Home:默认值与 HomeScreen 一致", !S0.ShopDiscountNotification)
// 动作 o1(无 toString,定名 ShowShopDiscountNotification)→ copy(shopDiscountNotification = true)
val S1 = ReduceHome(S0, ShowShopDiscountNotification())
Check("切片 Home:ShowShopDiscountNotification 把折扣提示置 true", S1.ShopDiscountNotification)
Check("切片 Home:无关动作不改状态", ReduceHome(S1, AddFrags(1)) == S1)
val Store = ReduxStore(S0, { S, A -> ReduceHome(S, A) })
Store.Dispatch(ShowShopDiscountNotification())
Check("切片 Home:接 Store 派发后状态已归约", Store.State.ShopDiscountNotification)
}
// ⑬ 切片 reducer:LocalBattleUserState(原 module_10032 的 lE + 根 reducer `(inline@localBattleUserState)`)
run {
val S0 = LocalBattleUserStateSlice()
Check(
"切片 LocalBattleUser:默认值与原构造一致",
S0.WeaponResistanceProperty == null && !S0.IsReArmorTemporaryDisabled
)
val S1 = ReduceLocalBattleUser(S0, SetLocalWeaponResistanceProperty("FREEZE_RESISTANCE"))
Check(
"切片 LocalBattleUser:SetLocalWeaponResistanceProperty 只写武器抗性属性",
S1.WeaponResistanceProperty == "FREEZE_RESISTANCE" && !S1.IsReArmorTemporaryDisabled
)
val S2 = ReduceLocalBattleUser(S1, SetReArmorTemporaryDisabled(true))
Check(
"切片 LocalBattleUser:SetReArmorTemporaryDisabled 只改开关",
S2.IsReArmorTemporaryDisabled && S2.WeaponResistanceProperty == "FREEZE_RESISTANCE"
)
Check("切片 LocalBattleUser:SetReArmorTemporaryDisabled(false) 关掉", !ReduceLocalBattleUser(S2, SetReArmorTemporaryDisabled(false)).IsReArmorTemporaryDisabled)
Check("切片 LocalBattleUser:无关动作不改状态", ReduceLocalBattleUser(S2, AddFrags(1)) == S2)
val Store = ReduxStore(S0, { S, A -> ReduceLocalBattleUser(S, A) })
Store.Dispatch(SetReArmorTemporaryDisabled(true))
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 88 个已迁移切片(87 归约 + launchParams 透传)
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 已按字段具名并抓到动作",
RootWiring.All.filter { it.Reducer.startsWith("(inline@") }.map { it.Field } ==
listOf("shop", "home", "localBattleUserState", "partner") &&
RootWiring.All.filter { it.Reducer.startsWith("(inline@") }.all { it.Actions.isNotEmpty() }
)
Check(
"根:内联动作名与原包一致(shop/home/localBattle/partner)",
RootWiring.ByField("shop")!!.Actions.containsAll(listOf("SelectItem", "SetPurchaseCompleted", "TryToBuyOneTimePurchase")) &&
RootWiring.ByField("home")!!.Actions == listOf("ShowShopDiscountNotification") &&
RootWiring.ByField("localBattleUserState")!!.Actions.contains("SetReArmorTemporaryDisabled") &&
RootWiring.ByField("partner")!!.Actions.containsAll(listOf("SetPartnerId", "InitPartnerInviteAction"))
)
Check(
"根:无 toString 的 ?o1 已按效果定名(HomeScreen 折扣提示)",
RootWiring.ByField("home")!!.Actions.none { it.startsWith("?") }
)
Check(
"根:唯一纯透传切片是 launchParams(已强类型收口)",
RootWiring.All.count { it.Reducer == "(pass-through)" } == 1 &&
RootWiring.ByField("launchParams")!!.Actions.isEmpty()
)
// 已迁移三片的委派与原包一致: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"
)
// 后两片是内联 IIFE,委派名是 `(inline@<field>)`
val HomeW = RootWiring.ByField("home")
val LocalBattleW = RootWiring.ByField("localBattleUserState")
Check(
"根:home 委派到 s1 / (inline@home)",
HomeW != null && HomeW.SliceClass == "s1" && HomeW.Reducer == "(inline@home)"
)
Check(
"根:localBattleUserState 委派到 lE / (inline@localBattleUserState)",
LocalBattleW != null && LocalBattleW.SliceClass == "lE" &&
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 88 个已迁移字段,其余 1 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
"localBattleUserState", "shop", "screen", "lootBoxes", "garage",
"garagePreview", "entrance", "user", "settings", "accountSettings",
"proBattle", "language", "announcement", "launchParams",
"news", "notifications", "quests",
"lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl",
"rulesUpdated", "notificationByCountry", "availabilityByRank",
"icons", "questCategories", "mainQuest", "securitySettings", "referralEvent",
"tipsState", "rankUpDialogState", "battleResult", "captchaState",
"modalScreen", "droppableGold", "eventGoods", "creatorTagState",
"rewardedVideosState", "passwordChangeState",
"system", "contextTutorial", "battleTutorial", "spectator",
"twoStepVerificationState", "antiAddiction", "moderation",
"migrationState", "register", "userReportsState",
"friends", "suppliesState", "clanUserInfo", "battleInviteState",
"partner", "userRenameState",
"battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial",
"clanChatState", "battleSearchState", "battleStatistics",
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
"graphicsSettings", "controlSettings", "battleSettings", "contractState",
"tankBuilderState", "battleMessage", "maps", "battleChat",
"remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications",
"matchmakingGroup",
) &&
RootWiring.Unmigrated.size == 1
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
val S0 = RootState(User = UserStateSlice(Uid = "user-1"), Screen = ScreenStateSlice(Current = NamedScreen("Preloader")))
val S1 = ReduceRoot(S0, ApplyInvite("uid-1"))
Check(
"根:ApplyInvite 只动 inviteState",
S1.InviteState.Invite == "uid-1" &&
S1.LoginByPassword == S0.LoginByPassword &&
S1.TwitchState == S0.TwitchState &&
S1.Home == S0.Home
)
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
)
val S4 = ReduceRoot(S3, ShowShopDiscountNotification())
Check(
"根:ShowShopDiscountNotification 只动 home",
S4.Home.ShopDiscountNotification && S4.TwitchState == S3.TwitchState
)
val S5 = ReduceRoot(S4, SetReArmorTemporaryDisabled(true))
Check(
"根:SetReArmorTemporaryDisabled 只动 localBattleUserState",
S5.LocalBattleUserState.IsReArmorTemporaryDisabled && S5.Home == S4.Home
)
Check(
"根:五次归约后五个切片各自保留结果",
S5.InviteState.Invite == "uid-1" &&
S5.LoginByPassword.Login == "pilot" &&
S5.TwitchState.TwitchAccount == "tank_tv" &&
S5.Home.ShopDiscountNotification &&
S5.LocalBattleUserState.IsReArmorTemporaryDisabled
)
// 未迁移字段原样透传;无关动作不改变任何字段
Check(
"根:未迁移字段原样透传",
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 88/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
Store.Dispatch(SetTwitchEnabled(true))
Store.Dispatch(ShowShopDiscountNotification())
Check(
"根:接 Store 派发后三片同时归约",
Store.State.LoginByPassword.Status == ByPasswordStatus.LOGGING_IN &&
Store.State.TwitchState.IsTwitchEnabled &&
Store.State.Home.ShopDiscountNotification
)
}
// ⑮ 切片 reducer:Shop(原 module_10032 的 j4 + `(inline@shop)`;U7 类别 7 字段、w3 物品 23 字段)
run {
val S0 = ShopStateSlice()
Check(
"切片 Shop:默认值与原构造一致(enabled 缺省 true、previousScreen 缺省 null)",
S0.ShopCategories == ShopCategoriesState() && S0.ShopItems == ShopItemsState() &&
S0.InitShopItemId == null && S0.SelectedItemId == null && !S0.IsLoaded &&
S0.ItemForPurchase == null && S0.Enabled && S0.CoinPaymentProviderUrl == null &&
S0.PayModes.isEmpty() && S0.SelectedPayModeId == null && S0.PaymentViewType.isEmpty() &&
S0.PaymentUrl == null && !S0.ReceivePaymentUrlFailed && S0.MobileCountryCode == "+7 " &&
S0.MobilePhoneLength == 13 && S0.MobilePaymentState == MobilePaymentState.NONE &&
S0.PaymentProviderState == PaymentProviderState.NONE && S0.AsiassoftAmountInAtc == 0 &&
S0.AsiassoftEnoughMoney == null && S0.BraintreeToken == null && !S0.IsBlockedBackButton &&
!S0.BraintreeLoadingInterrupt && !S0.GoToHomeAfterBuy && S0.PreviousScreen == null &&
S0.TargetCategory == null && S0.TargetProductId == null && S0.LastPurchasedItem == null &&
S0.OrdersInProcessing.isEmpty()
)
val S1 = ReduceShop(S0, SetCoinPaymentProviderUrl("https://pay"))
val S2 = ReduceShop(S1, ShopLoaded())
Check("切片 Shop:SetCoinPaymentProviderUrl / ShopLoaded 各改本字段", S1.CoinPaymentProviderUrl == "https://pay" && S2.IsLoaded)
// F5:复位但保留 coinPaymentProviderUrl
val S3 = ReduceShop(S2.copy(PaymentUrl = "u", MobilePaymentState = MobilePaymentState.PAYMENT_INITED), ResetShop())
Check(
"切片 Shop:ResetShop 复位其余字段但保留 coinPaymentProviderUrl",
S3.CoinPaymentProviderUrl == "https://pay" && S3.PaymentUrl == null && !S3.IsLoaded &&
S3.MobilePaymentState == MobilePaymentState.NONE
)
val S4 = ReduceShop(S3, PaymentUrlReceiveFailed())
val S5 = ReduceShop(S4, ResetPaymentState())
Check(
"切片 Shop:支付失败后 ResetPaymentState 清 url/失败位/两个状态",
S4.PaymentUrl == null && S4.ReceivePaymentUrlFailed &&
!S5.ReceivePaymentUrlFailed && S5.MobilePaymentState == MobilePaymentState.NONE &&
S5.PaymentProviderState == PaymentProviderState.NONE
)
val S6 = ReduceShop(S5, MobilePaymentStateChanged("PHONE_VALID"))
Check("切片 Shop:MobilePaymentStateChanged 按枚举名解析", S6.MobilePaymentState == MobilePaymentState.PHONE_VALID)
Check(
"切片 Shop:三种 provider 状态各自落到枚举",
ReduceShop(S6, PaymentProviderInProgress()).PaymentProviderState == PaymentProviderState.IN_PROGRESS &&
ReduceShop(S6, PaymentProviderFinishSuccess()).PaymentProviderState == PaymentProviderState.FINISH_SUCCESS &&
ReduceShop(S6, PaymentProviderFinishFail()).PaymentProviderState == PaymentProviderState.FINISH_FAIL
)
val Mode = PayMode(3L, "card", "desc", 1, 0L, "")
val S7 = ReduceShop(S6, InitPayModeAction(Mode))
val S8 = ReduceShop(S7, InitPaymentViewType(3L, "BANK_CARD"))
Check(
"切片 Shop:InitPayModeAction 加集合项、InitPaymentViewType 按 payModeId 建表",
S8.PayModes.contains(Mode) && S8.PaymentViewType[3L] == "BANK_CARD"
)
val S9 = ReduceShop(S8, AddItemToOrderProcessing(11L, "ok"))
Check(
"切片 Shop:AddItemToOrderProcessing 写 itemId → (itemId, onSuccess)",
S9.OrdersInProcessing[11L] == (11L to "ok")
)
Check(
"切片 Shop:RemoveItemFromOrderProcessing itemId=null 时原样返回",
ReduceShop(S9, RemoveItemFromOrderProcessing(null, false)) == S9
)
Check(
"切片 Shop:RemoveItemFromOrderProcessing 删键",
ReduceShop(S9, RemoveItemFromOrderProcessing(11L, false)).OrdersInProcessing.isEmpty()
)
val S10 = ReduceShop(S9, SavePreviousScreen("Garage"))
val S11 = ReduceShop(S10, SaveTargetVisitedCategory("CRYSTALS"))
val S12 = ReduceShop(S11, SaveTargetProductId(77L))
Check(
"切片 Shop:SavePreviousScreen / SaveTargetVisitedCategory / SaveTargetProductId",
S12.PreviousScreen == "Garage" && S12.TargetCategory == "CRYSTALS" && S12.TargetProductId == 77L
)
Check(
"切片 Shop:ClearPreviousScreen / ClearTargetCategory / ClearTargetProductId 置空",
ReduceShop(S12, ClearPreviousScreen()).PreviousScreen == null &&
ReduceShop(S12, ClearTargetCategory()).TargetCategory == null &&
ReduceShop(S12, ClearTargetProductId()).TargetProductId == null
)
Check(
"切片 Shop:ToggleShopActivity / SelectItem / SetInitShopItemId 各改本字段",
!ReduceShop(S12, ToggleShopActivity(false)).Enabled &&
ReduceShop(S12, SelectItem(42L, true)).SelectedItemId == 42L &&
ReduceShop(S12, SetInitShopItemId(7L)).InitShopItemId == 7L
)
Check(
"切片 Shop:BlockBackButton / UnblockBackButton 切换返回键锁",
ReduceShop(S12, BlockBackButton()).IsBlockedBackButton &&
!ReduceShop(S12, UnblockBackButton()).IsBlockedBackButton &&
ReduceShop(S12, SetGoToHomeAfterBuy()).GoToHomeAfterBuy
)
// U7:类别表 7 字段 + 5 臂
val C = Category(
id = 1L,
orderIndex = 0,
name = "Crystals",
description = "desc",
type = "CRYSTALS",
preview = null,
isSpecial = false,
icon = null,
categoryBackground = null,
cardHoverBackground = 0L,
confirmPurchaseForCrystals = false,
canBeShownWhenUserIsInDebt = false,
showTimerOnCard = false,
)
val C1 = ReduceShopCategories(ShopCategoriesState(), InitCategory(C))
Check(
"切片 Shop/Categories:InitCategory 同时写 categories / type→id / id→type",
C1.Categories[1L] == C && C1.CategoryTypeToId[CategoryType.CRYSTALS] == 1L &&
C1.CategoryIdToType[1L] == CategoryType.CRYSTALS
)
val C2 = ReduceShopCategories(C1, SetupDiscountCategoryByItem(1L))
val C3 = ReduceShopCategories(C2, AddCategoriesWithBonus(setOf(2L, 3L)))
Check(
"切片 Shop/Categories:折扣集合加单个 id、奖励集合并入多个 id",
C3.CategoriesWithDiscount == setOf<Any?>(1L) && C3.CategoriesWithBonus == setOf<Any?>(2L, 3L)
)
val C4 = ReduceShopCategories(C3, SelectShopCategory("COINS"))
val C5 = ReduceShopCategories(C4, SelectCategoryId(9L))
Check(
"切片 Shop/Categories:SelectShopCategory 解析枚举、SelectCategoryId 写 id",
C5.SelectedCategoryType == CategoryType.COINS && C5.SelectedCategoryId == 9L
)
// w3:物品表 23 字段 + 32 臂(抽 12 臂覆盖增删改)
val Item = ShopItem(
id = 1L,
gameObject = null,
categoryId = 1L,
orderIndex = 0,
price = 100,
originalPrice = 100.0,
currencyName = "coin",
currencyId = 1L,
roundingPrecision = 0,
previewImageURL = 0L,
isSellingForCoin = true,
isSellingForCrystal = false,
isSellingForRuby = false,
isSellingForMoney = false,
isSellingForADS = false,
needPaymentProviderItem = false,
)
val I1 = ReduceShopItems(ShopItemsState(), InitShopItem(1L, Item))
Check("切片 Shop/Items:InitShopItem 写 items", I1.Items[1L] == Item)
val Card = ShopCard(
activeUntil = 100L,
goodInfo = null,
name = "card",
sold = false,
pinned = false,
type = 0,
view = null,
discount = null,
sortIndex = 1,
shopCategoryId = 1L,
)
val I2 = ReduceShopItems(I1, InitShopCard(2L, Card, "goods", 1L))
Check(
"切片 Shop/Items:InitShopCard 重建卡片(goodInfo=goods、view/discount 置空)",
I2.ShopCards[2L]?.name == "card" && I2.ShopCards[2L]?.goodInfo == "goods" &&
I2.ShopCards[2L]?.view == null && I2.ShopCards[2L]?.discount == null
)
val Discount = DiscountItem(90, 10, 5)
val I3 = ReduceShopItems(I2, InitShopCardView(2L, "view"))
val I4 = ReduceShopItems(I3, InitShopCardDiscount(2L, Discount))
Check(
"切片 Shop/Items:InitShopCardView / InitShopCardDiscount 只改对应字段",
I4.ShopCards[2L]?.view == "view" && I4.ShopCards[2L]?.discount == Discount
)
val I5 = ReduceShopItems(I4, ShopCardSold(2L))
val I6 = ReduceShopItems(I5, ShopCardExpired(2L))
Check(
"切片 Shop/Items:ShopCardSold 置 sold、ShopCardExpired 把 activeUntil 归零",
I6.ShopCards[2L]?.sold == true && I6.ShopCards[2L]?.activeUntil == 0L
)
Check("切片 Shop/Items:ShopCardRemoved 删卡片", ReduceShopItems(I6, ShopCardRemoved(2L)).ShopCards.isEmpty())
val I7 = ReduceShopItems(I6, SetupDiscountItem(1L, Discount))
Check("切片 Shop/Items:SetupDiscountItem 写 discountItems", I7.DiscountItems[1L] == Discount)
val I8 = ReduceShopItems(I7, SetupDiscountItemForItem(4L, Discount))
Check(
"切片 Shop/Items:o5 同族动作写/删 discountItems",
I8.DiscountItems[4L] == Discount &&
ReduceShopItems(I8, RemoveDiscountItemForItem(4L)).DiscountItems[4L] == null
)
val ProviderItem = PaymentProviderItem(5L, 1.0, "USD", "US", "name", "desc", "1 USD")
val I9 = ReduceShopItems(I8, InitPaymentProviderItems(listOf(ProviderItem)))
Check(
"切片 Shop/Items:InitPaymentProviderItems 按 itemId 建表",
I9.PaymentProviderItems[5L] == ProviderItem
)
val I10 = ReduceShopItems(I9, InitAdditionalDescription(1L, "title", "desc", 0L))
Check(
"切片 Shop/Items:InitAdditionalDescription 组装 AdditionalDescription",
I10.AdditionalDescription[1L] == AdditionalDescription("title", "desc", 0L)
)
val I11 = ReduceShopItems(I10, InitOneTimePurchaseItem(3L, OneTimePurchaseItem(false, 1)))
val I12 = ReduceShopItems(I11, TryToBuyOneTimePurchase(3L))
Check(
"切片 Shop/Items:TryToBuyOneTimePurchase 复制已有项并置 isTriedToBuy",
I12.OneTimePurchaseItems[3L]?.isTriedToBuy == true
)
val I13 = ReduceShopItems(I12, InitGroup(4L, 2))
Check("切片 Shop/Items:InitGroup 写 groups", I13.Groups[4L] == 2)
Check(
"切片 Shop/Items:InitAmountString / InitShopItemName 写各自表",
ReduceShopItems(I13, InitAmountString(1L, "100")).AmountString[1L] == "100" &&
ReduceShopItems(I13, InitShopItemName(1L, "name")).ShopItemName[1L] == "name"
)
val I14 = ReduceShopItems(I13, DeleteOneTimePurchaseItem(1L))
Check(
"切片 Shop/Items:DeleteOneTimePurchaseItem 从六个表删同一键",
I14.Items[1L] == null && I14.FeatureItems[1L] == null && I14.SpecialKitPackage[1L] == null &&
I14.KitBanner[1L] == null && I14.KitPackage[1L] == null && I14.Groups[1L] == null
)
Check("切片 Shop:无关动作不改状态", ReduceShop(S12, AddFrags(1)) == S12)
// 根:一次 ReduceRoot 里商店动作只动 shop
val R0 = RootState()
val R1 = ReduceRoot(R0, InitShopItem(1L, Item))
Check(
"根:InitShopItem 只动 shop",
R1.Shop.ShopItems.Items[1L] == Item && R1.User == R0.User && R1.Home == R0.Home
)
val R2 = ReduceRoot(R1, InitCategory(C))
Check(
"根:InitCategory 与 InitShopItem 在同一片的两张子表里共存",
R2.Shop.ShopCategories.Categories[1L] == C && R2.Shop.ShopItems.Items[1L] == Item
)
val R3 = ReduceRoot(R2, SetCoinPaymentProviderUrl("https://pay"))
Check(
"根:SetCoinPaymentProviderUrl 只动 shop 顶层字段",
R3.Shop.CoinPaymentProviderUrl == "https://pay" &&
R3.Shop.ShopItems == R2.Shop.ShopItems && R3.LoginByPassword == R0.LoginByPassword
)
Check("根:shop 迁移后无关动作仍返回等值状态", ReduceRoot(R3, AddFrags(1)) == R3)
// ⑯ shop 真实派发回放:用 action_trace_merged.json 里实际出现过的两条商店动作
val UrlRaw = ActionSamples.Raw["SetCoinPaymentProviderUrl"]
val ToggleRaw = ActionSamples.Raw["ToggleShopActivity"]
Check("shop 回放:两条真实动作都在样例表里", UrlRaw != null && ToggleRaw != null)
val Url = UrlRaw!!.substringAfter("url=").substringBeforeLast(")")
val Enabled = ToggleRaw!!.substringAfter("enabled=").substringBeforeLast(")") == "true"
Check(
"shop 回放:样例值与运行时一致",
Url.startsWith("https://open-api.3dtank.com/mgr/tank-pay/") && Enabled
)
val P0 = RootState()
val P1 = ReduceRoot(P0, SetCoinPaymentProviderUrl(Url))
Check(
"shop 回放:真实 SetCoinPaymentProviderUrl 写入完整 URL",
P1.Shop.CoinPaymentProviderUrl == Url
)
val P2 = ReduceRoot(P1.copy(Shop = P1.Shop.copy(Enabled = false)), ToggleShopActivity(Enabled))
Check(
"shop 回放:真实 ToggleShopActivity(true) 把 enabled 置 true",
P2.Shop.Enabled && P2.Shop.CoinPaymentProviderUrl == Url
)
Check(
"shop 回放:只动 shop,其他切片不变",
P2.LoginByPassword == P0.LoginByPassword && P2.Home == P0.Home &&
P2.TwitchState == P0.TwitchState && P2.User == P0.User
)
Check(
"shop 回放:两条动作都在动作目录里",
listOf("SetCoinPaymentProviderUrl", "ToggleShopActivity").all { ActionCatalog.ByName[it] != null }
)
// 69 个动作里只有 x4 是标记接口(没有独立效果),其余都已有名字
val ShopUnknown = RootWiring.ByField("shop")!!.Actions.filter { it.startsWith("?") }
Check("shop 回放:唯一未具名的是标记 x4", ShopUnknown == listOf("?x4"))
// ⑰ 切片 reducer:Screen(原 module_10032 的 WK / wY.ks5,13 个 instanceof 臂)
run {
val Lobby = NamedScreen("Lobby.Home")
val Shop = NamedScreen("Shop")
val S0 = ScreenStateSlice()
Check(
"切片 Screen:默认值与原构造一致(current=System.Loader、enableBack=true)",
S0.Current == LoaderScreen && S0.LoaderBackgroundImage == null && !S0.Foreground &&
S0.ScreenStack.isEmpty() && S0.EnableBack && !S0.IsResetBackStackOnNextGoTo &&
!S0.IsDelayedGoto && !S0.PressedAnyButton && !S0.DisplayMainEntranceScreen &&
!S0.NeedReauthorization
)
Check(
"切片 Screen:前景开关只改 foreground",
!ReduceScreen(S0, DisableForeground).Foreground &&
ReduceScreen(S0, EnableForeground).Foreground &&
ReduceScreen(S0, EnableForeground).Current == LoaderScreen
)
val S1 = S0.copy(Current = Lobby, ScreenStack = listOf(NamedScreen("Preloader")))
Check(
"切片 Screen:ClearScreenStack 清历史但保留 current",
ReduceScreen(S1, ClearScreenStack).let { it.ScreenStack.isEmpty() && it.Current == Lobby }
)
val S2 = ReduceScreen(S1, GoTo(Shop, false))
Check(
"切片 Screen:GoTo 无 history 只换 current",
S2.Current == Shop && S2.ScreenStack == S1.ScreenStack
)
val S3 = ReduceScreen(S1, GoTo(Shop, true))
Check(
"切片 Screen:GoTo history 把原 current 压栈",
S3.Current == Shop && S3.ScreenStack == S1.ScreenStack + Lobby
)
Check(
"切片 Screen:GoTo 到当前屏仍按 history 压栈(原包只在 analytics 里去重)",
ReduceScreen(S1, GoTo(Lobby, true)).ScreenStack == listOf(NamedScreen("Preloader"), Lobby)
)
val S4 = ReduceScreen(S3.copy(IsResetBackStackOnNextGoTo = true), GoTo(Lobby, true))
Check(
"切片 Screen:复位标志优先于 history,且 GoTo 后归 false",
S4.Current == Lobby && S4.ScreenStack.isEmpty() && !S4.IsResetBackStackOnNextGoTo
)
Check(
"切片 Screen:GoTo 清 isDelayedGoto",
!ReduceScreen(S3.copy(IsDelayedGoto = true), GoTo(Shop, false)).IsDelayedGoto
)
Check(
"切片 Screen:ResetScreenStackOnNextGoTo / DelayNextGoTo 各置本标志",
ReduceScreen(S0, ResetScreenStackOnNextGoTo).IsResetBackStackOnNextGoTo &&
ReduceScreen(S0, DelayNextGoTo).IsDelayedGoto
)
val S5 = S1.copy(ScreenStack = listOf(NamedScreen("Battle"), Lobby, Shop))
Check(
"切片 Screen:FilterScreenStack 只保留 predicate 命中项",
ReduceScreen(S5, FilterScreenStack { it.name == "Battle" }).ScreenStack ==
listOf(NamedScreen("Battle"))
)
Check(
"切片 Screen:FilterScreenStack 缺省谓词(原 YK)清空历史",
ReduceScreen(S5, FilterScreenStack()).ScreenStack.isEmpty()
)
Check(
"切片 Screen:EnableBack / DisableBack 切返回键",
ReduceScreen(S0.copy(EnableBack = false), EnableBack).EnableBack &&
!ReduceScreen(S0, DisableBack).EnableBack
)
Check(
"切片 Screen:PopScreen 丢最后一个,空栈安全",
ReduceScreen(S1, PopScreen).ScreenStack.isEmpty() &&
ReduceScreen(S0, PopScreen).ScreenStack.isEmpty()
)
Check(
"切片 Screen:PressedAnyButton 写 action.isPressed(false 也生效)",
!ReduceScreen(S0.copy(PressedAnyButton = true), PressedAnyButton(false)).PressedAnyButton &&
ReduceScreen(S0, PressedAnyButton(true)).PressedAnyButton
)
Check(
"切片 Screen:DisplayMainEntranceScreen / DisplayReauthorizationMessage 各写本字段",
ReduceScreen(S0, DisplayMainEntranceScreen(true)).DisplayMainEntranceScreen &&
ReduceScreen(S0, DisplayReauthorizationMessage(true)).NeedReauthorization
)
Check("切片 Screen:无关动作不改状态", ReduceScreen(S1, AddFrags(1)) == S1)
// 根:screen 动作只动 screen,且与已迁移的其余六片共存
val R0 = RootState()
val R1 = ReduceRoot(R0, GoTo(Shop, false))
Check(
"根:GoTo 只动 screen",
R1.Screen.Current == Shop && R1.User == R0.User && R1.Shop == R0.Shop &&
R1.InviteState == R0.InviteState && R1.Home == R0.Home
)
val R2 = ReduceRoot(R1, EnableForeground)
Check(
"根:EnableForeground 只动 screen 前景位",
R2.Screen.Foreground && R2.Screen.Current == Shop &&
R2.Screen.ScreenStack == R1.Screen.ScreenStack && R2.Shop == R1.Shop
)
Check("根:screen 迁移后无关动作仍返回等值状态", ReduceRoot(R2, AddFrags(1)) == R2)
val W = RootWiring.ByField("screen")!!
Check(
"根:screen 委派到 WK / dY().ks5 且 13 个动作全部具名",
W.SliceClass == "WK" && W.Reducer == "dY().ks5" && W.Actions.size == 13 &&
W.Actions.none { it.startsWith("?") }
)
Check(
"根:screen 的动作名覆盖 13 个 instanceof 臂",
listOf(
"DisableForeground", "EnableForeground", "ClearScreenStack",
"ResetScreenStackOnNextGoTo", "DelayNextGoTo", "GoTo", "FilterScreenStack",
"EnableBack", "DisableBack", "PopScreen", "PressedAnyButton",
"DisplayMainEntranceScreen", "DisplayReauthorizationMessage",
).all { it in W.Actions }
)
}
// ⑱ 切片 reducer:LootBoxes(原 module_10032 的 aH / EH.wrv,18 个 instanceof 臂)
run {
val Item = { Name: String ->
ContainerGivenItem("INVENTORY", 1, "INVENTORY", 10007277, null, Name, "mine")
}
val S0 = LootBoxesStateSlice()
Check(
"切片 LootBoxes:默认值与原构造一致",
S0.RewardsForKeys == emptyList<ContainerGivenItem>() &&
S0.CurrentContainerType == ContainerType.STANDARD &&
S0.Keys.isEmpty() && S0.ContainerNames.isEmpty() &&
S0.ContainerDescriptions.isEmpty() && S0.Rewards.isEmpty() &&
S0.ShownRewards.isEmpty() && !S0.GotoGarage && S0.TimeToGiveLootBox == 0L &&
S0.PeriodOfGiveLootBoxInSec == 0 && !S0.OpeningFailed && !S0.AdsAvailable &&
S0.CurrentCategory == null && !S0.IsOpening && !S0.IsAccelerated &&
!S0.IsShopVisited && !S0.IsReopening && !S0.IsBlockOpenButtons
)
val Rewards = listOf(Item("mine"))
Check(
"切片 LootBoxes:SetPossibleRewards / SetCurrentContainer 各写本字段",
ReduceLootBoxes(S0, SetPossibleRewards(Rewards)).RewardsForKeys == Rewards &&
ReduceLootBoxes(S0, SetCurrentContainer(ContainerType.EPIC))
.CurrentContainerType == ContainerType.EPIC &&
ReduceLootBoxes(S0, SetCurrentContainer(ContainerType.EPIC))
.CurrentContainerType != ContainerType.RARE
)
val Keyed = ReduceLootBoxes(S0, SetKeys(mapOf("STANDARD" to 3)))
Check(
"切片 LootBoxes:SetKeys 覆盖整张表",
Keyed.Keys == mapOf("STANDARD" to 3) &&
ReduceLootBoxes(S0, SetKeys(emptyMap())).Keys.isEmpty()
)
Check(
"切片 LootBoxes:AddKeys 在原值上加 delta",
ReduceLootBoxes(Keyed, AddKeys("STANDARD", 2)).Keys == mapOf("STANDARD" to 5) &&
ReduceLootBoxes(Keyed, AddKeys("STANDARD", -3)).Keys == mapOf("STANDARD" to 0)
)
Check(
"切片 LootBoxes:AddKeys 缺键抛错(原 nm 是 Map.getValue)",
runCatching { ReduceLootBoxes(Keyed, AddKeys("MISSING", 1)) }.isFailure
)
val Described = ReduceLootBoxes(S0, SetContainerData("STANDARD", "标准箱", "普通补给箱"))
Check(
"切片 LootBoxes:SetContainerData 同时写名字与描述两张表",
Described.ContainerNames == mapOf("STANDARD" to "标准箱") &&
Described.ContainerDescriptions == mapOf("STANDARD" to "普通补给箱") &&
Described.Keys.isEmpty() && Described.Rewards.isEmpty()
)
Check(
"切片 LootBoxes:SetGoToGarage 写 gotoGarage",
ReduceLootBoxes(S0, SetGoToGarage(true)).GotoGarage &&
!ReduceLootBoxes(S0.copy(GotoGarage = true), SetGoToGarage(false)).GotoGarage
)
val Rewarded = ReduceLootBoxes(
S0.copy(IsReopening = true, ShownRewards = listOf(Item("old"))),
SetRewards(Rewards)
)
Check(
"切片 LootBoxes:SetRewards 换奖励并把 isReopening 归 false",
Rewarded.Rewards == Rewards && !Rewarded.IsReopening &&
Rewarded.ShownRewards == listOf(Item("old"))
)
val Before = kotlin.js.Date.now().toLong()
val Timed = ReduceLootBoxes(S0, SetTimeToGiveLootBox(3))
val After = kotlin.js.Date.now().toLong()
Check(
"切片 LootBoxes:SetTimeToGiveLootBox = 当前毫秒 + 秒 * 1000",
Timed.TimeToGiveLootBox >= Before + 3000L && Timed.TimeToGiveLootBox <= After + 3000L
)
Check(
"切片 LootBoxes:SetPeriodOfGiveLootBox 写周期(秒)",
ReduceLootBoxes(S0, SetPeriodOfGiveLootBox(3600)).PeriodOfGiveLootBoxInSec == 3600
)
val Cleared = ReduceLootBoxes(
S0.copy(Rewards = Rewards, ShownRewards = Rewards, OpeningFailed = true),
ClearRewards
)
Check(
"切片 LootBoxes:ClearRewards 只清两个奖励表",
Cleared.Rewards.isEmpty() && Cleared.ShownRewards.isEmpty() && Cleared.OpeningFailed
)
Check(
"切片 LootBoxes:LootBoxOpeningFailed 置 openingFailed",
ReduceLootBoxes(S0, LootBoxOpeningFailed).OpeningFailed
)
Check(
"切片 LootBoxes:SelectCategory 写可空 currentCategory",
ReduceLootBoxes(S0, SelectCategory(ContainerType.RARE)).CurrentCategory == ContainerType.RARE &&
ReduceLootBoxes(S0.copy(CurrentCategory = ContainerType.RARE), SelectCategory(null))
.CurrentCategory == null
)
val Shown = ReduceLootBoxes(S0.copy(ShownRewards = listOf(Item("old"))), ShownRewards(Rewards))
Check(
"切片 LootBoxes:ShownRewards 追加到 shownRewards(原 Im 是 List.plus)",
Shown.ShownRewards == listOf(Item("old"), Item("mine")) && Shown.Rewards.isEmpty()
)
Check(
"切片 LootBoxes:四个布尔动作各写本字段",
!ReduceLootBoxes(S0.copy(IsOpening = true), SetIsOpening(false)).IsOpening &&
ReduceLootBoxes(S0, SetIsOpening(true)).IsOpening &&
ReduceLootBoxes(S0, SetIsAccelerated(true)).IsAccelerated &&
ReduceLootBoxes(S0, SetIsShopVisited(true)).IsShopVisited
)
Check(
"切片 LootBoxes:SetIsReopening 置 true;SetIsBlockOpenButtons 写值",
ReduceLootBoxes(S0, SetIsReopening).IsReopening &&
ReduceLootBoxes(S0, SetIsBlockOpenButtons(true)).IsBlockOpenButtons &&
!ReduceLootBoxes(S0.copy(IsBlockOpenButtons = true), SetIsBlockOpenButtons(false))
.IsBlockOpenButtons
)
Check("切片 LootBoxes:无关动作不改状态", ReduceLootBoxes(S0, AddFrags(1)) == S0)
// 根:lootBoxes 动作只动 lootBoxes
val R0 = RootState()
val R1 = ReduceRoot(R0, SetCurrentContainer(ContainerType.LEGENDARY))
Check(
"根:SetCurrentContainer 只动 lootBoxes",
R1.LootBoxes.CurrentContainerType == ContainerType.LEGENDARY &&
R1.Shop == R0.Shop && R1.Screen == R0.Screen &&
R1.InviteState == R0.InviteState && R1.Home == R0.Home
)
val R2 = ReduceRoot(R1, SetRewards(listOf(Item("root"))))
Check(
"根:SetRewards 只动 lootBoxes,且保留前一次归约",
R2.LootBoxes.Rewards == listOf(Item("root")) &&
R2.LootBoxes.CurrentContainerType == ContainerType.LEGENDARY &&
R2.Shop == R1.Shop
)
Check("根:lootBoxes 迁移后无关动作仍返回等值状态", ReduceRoot(R2, AddFrags(1)) == R2)
val W = RootWiring.ByField("lootBoxes")!!
Check(
"根:lootBoxes 委派到 aH / ws.wrv 且 18 个动作全部具名",
W.SliceClass == "aH" && W.Reducer == "ws.wrv" && W.Actions.size == 18 &&
W.Actions.none { it.startsWith("?") }
)
Check(
"根:lootBoxes 的动作名覆盖 18 个 instanceof 臂",
listOf(
"SetPossibleRewards", "SetCurrentContainer", "SetKeys", "AddKeys",
"SetContainerData", "SetGoToGarage", "SetRewards", "SetTimeToGiveLootBox",
"SetPeriodOfGiveLootBox", "ClearRewards", "LootBoxOpeningFailed",
"SelectCategory", "ShownRewards", "SetIsOpening", "SetIsAccelerated",
"SetIsShopVisited", "SetIsReopening", "SetIsBlockOpenButtons",
).all { it in W.Actions }
)
}
}
// ⑲ 切片 reducer:Garage(原 module_10032 的 UB / hF,31 字段;58 个 instanceof + 2 个接口/单例臂)
run {
fun ToObj(
Id: Long,
Base: Long? = null,
Category: Int = 1,
View: Int = 1,
Count: Int = 3,
Buyable: Boolean = true,
): GarageItemObject = GarageItemObject(
id = Id,
description = "obj-$Id",
item = "preview-$Id",
isPremium = false,
itemCategory = Category,
itemViewCategory = View,
buyable = Buyable,
availableForAll = false,
discount = null,
timePeriod = null,
modification = if (Base != null) GarageItemModification(Base, 0) else null,
properties = null,
upgradeableParams = null,
devices = null,
count = Count,
kit = null,
countable = false,
mountIndex = null,
availableSkins = null,
availableShotSkins = null,
mountedSkin = null,
mountedShotSkin = null,
skinPreview = null,
grouped = null,
rarity = null,
favorite = false,
isSellingForCoin = false,
isSellingForCrystal = false,
isSellingForRuby = false,
resistanceBackgroundImg = null,
visible = true,
descriptionMedia = null,
saleByKitItems = null,
)
fun ToItem(
Id: Long,
Base: Long? = null,
Category: Int = 1,
View: Int = 1,
Owned: Boolean = true,
Mounted: Boolean = false,
Count: Int = 1,
Buyable: Boolean = true,
Favorite: Boolean = false,
HasDeviceSale: Boolean = false,
DevicePreview: Any? = null,
Level: Int = 0,
Confirmed: Boolean = false,
Discount: Any? = null,
): GarageItem = GarageItem(
id = Id,
name = "item-$Id",
category = Category,
viewCategory = View,
priceWithoutDiscount = 100,
description = "item-$Id",
descriptionMedia = null,
preview = "preview-$Id",
isPremium = false,
owned = Owned,
mountable = true,
mounted = Mounted,
mountIndex = null,
position = 0,
minRank = 0,
maxRank = 0,
discount = Discount,
modification = if (Base != null) GarageItemModification(Base, Level) else null,
properties = null,
upgradeableParams = UpgradableItemParams(
itemPropertyParams = null,
properties = null,
currentLevel = Level,
upgradeParams = null,
upgradeDiscount = 0,
upgradeConfirmed = Confirmed,
),
count = Count,
kit = null,
countable = false,
hasDeviceSale = HasDeviceSale,
devicesAvailable = false,
devicePreview = DevicePreview,
buyable = Buyable,
availableForAll = false,
availableSkins = null,
availableShotSkins = null,
mountedSkin = null,
mountedShotSkin = null,
skinPreview = null,
group = null,
rarity = null,
favorite = Favorite,
resistanceBackgroundImg = null,
visible = true,
secret = false,
saleByKitItems = null,
isSellingForCoin = false,
isSellingForCrystal = false,
isSellingForRuby = false,
isSellingForMoney = false,
isSellingForADS = false,
)
fun ToDevice(
Id: Long,
Base: Long,
Installed: Boolean = false,
Preview: Any? = "device-$Id",
Infinity: Boolean = false,
): GarageDevice = GarageDevice(
id = Id,
baseItemId = Base,
installed = Installed,
name = "device-$Id",
category = 1,
viewCategory = 1,
priceWithoutDiscount = 50,
description = "device-$Id",
descriptionMedia = null,
previewImage = Preview,
isPremium = false,
position = 0,
minRank = 0,
maxRank = 0,
discount = 0,
timeDiscountEnd = null,
timeDiscountToStart = null,
infinityLifetimeItem = Infinity,
lifeTimeInSec = 0,
property = "",
properties = null,
isSellingForCoin = false,
isSellingForCrystal = false,
isSellingForRuby = false,
isSellingForADS = false,
isSellingForMoney = false,
rarity = null,
visible = true,
secret = false,
buyable = true,
)
fun ToDeviceObj(Id: Long, Base: Long, Preview: Any? = "device-$Id"): GarageDeviceObject =
GarageDeviceObject(
id = Id,
baseItemId = Base,
temporaryItemCC = null,
description = "device-$Id",
item = Preview,
isPremium = false,
itemCategory = 1,
itemViewCategory = 1,
buyable = true,
discount = null,
timePeriod = null,
deviceProperty = "",
rarity = null,
visible = true,
descriptionMedia = null,
)
fun BaseState(
ItemList: List<GarageItem>,
DeviceMap: Map<Long, List<GarageDevice>> = emptyMap(),
): GarageStateSlice {
val Grouped = ItemList
.groupBy { it.modification?.vcf_1 ?: it.id }
.mapValues { GarageBaseItems(it.value) }
return GarageStateSlice(
Items = GarageItemsState(Grouped),
Devices = Devices(DeviceMap),
)
}
fun ItemIn(State: GarageStateSlice, Id: Long): GarageItem =
State.Items.itemsByBase.values.flatMap { it.Items }.first { it.id == Id }
val S0 = GarageStateSlice()
Check(
"切片 Garage:默认值与原构造一致(31 字段)",
S0.Items.itemsByBase.isEmpty() && S0.Devices.baseItemIdToDevices.isEmpty() &&
S0.GarageObject == null && !S0.IsLoaded &&
S0.CurrentCategory == GarageDefaultCategory && S0.CurrentItemId == null &&
S0.SelectAlterationId == null && S0.SelectedSkinId == null &&
S0.SelectedShotSkinId == null && S0.NewItems.isEmpty() &&
!S0.ResistanceReadyToMount && S0.DelayMountTimeMs == 0L &&
S0.CooldownReduceEndTime == 0L && S0.TimeToReduceUpdateInMin == 0 &&
S0.CurrentRarity == null && S0.PurchaseOfUpgradesShopItemId == 920109776942L &&
!S0.GarageDiscountNotification && S0.ShowVideoInDescription &&
S0.UnlockedProtectionSlots == 1 && S0.PricesToUnlockSlot.isEmpty() &&
S0.NumberRowsInPaintsUI == 2 && S0.OverdriveDescriptions.isEmpty() &&
S0.WeaponToResistanceModule.isEmpty() && S0.WaitServerItemMount.isEmpty() &&
S0.BazookaToSupply.isEmpty() && S0.UpgradeCoolDownEndTimes.isEmpty() &&
S0.UpgradeItemInProcessingId == null
)
val O1 = ToObj(101, Count = 3)
val O2 = ToObj(202, Count = 1, Buyable = false)
val O3 = ToObj(303, Count = 2)
val O4 = ToObj(404, Count = 1)
val GarageObj = GarageObject("temp")
val S1 = ReduceGarage(S0, SetGarageObject(GarageObj))
Check("切片 Garage:SetGarageObject", S1.GarageObject == GarageObj)
val S1a = ReduceGarage(S1, SetItemsOnDepot(listOf(O1, O3)))
Check(
"切片 Garage:SetItemsOnDepot",
S1a.ItemsOnDepot.toList() == listOf(O1, O3) && S1a.GarageObject == GarageObj
)
val S1b = ReduceGarage(S1a, SetItemsOnMarket(listOf(O2)))
Check("切片 Garage:SetItemsOnMarket", S1b.ItemsOnMarket.toList() == listOf(O2))
val S1c = ReduceGarage(S1b, SetMountedItems(listOf(O3)))
Check("切片 Garage:SetMountedItems", S1c.MountedItems.toList() == listOf(O3))
val S1d = ReduceGarage(S1c, SetGarageSkinsAction(listOf(O4)))
Check("切片 Garage:SetGarageSkinsAction", S1d.SkinsItems.toList() == listOf(O4))
val Loaded0 = GarageStateSlice(
ItemsOnDepot = listOf(O1, O3, ToObj(505, Count = 1)),
MountedItems = listOf(O3),
ItemsOnMarket = listOf(O2),
SkinsItems = listOf(O4),
)
val Loaded1 = ReduceGarage(Loaded0, GarageLoaded())
val LoadedItems = Loaded1.Items.itemsByBase.values.flatMap { it.Items }
Check(
"切片 Garage:GarageLoaded 重建 PB(depot/mounted/market/skins)",
Loaded1.IsLoaded && LoadedItems.map { it.id }.sorted() == listOf(101L, 202L, 303L, 404L, 505L) &&
LoadedItems.first { it.id == 101L }.owned &&
LoadedItems.first { it.id == 303L }.owned && LoadedItems.first { it.id == 303L }.mounted &&
!LoadedItems.first { it.id == 202L }.owned && !LoadedItems.first { it.id == 202L }.buyable &&
!LoadedItems.first { it.id == 404L }.owned
)
Check(
"切片 Garage:SetGarageNotLoaded",
!ReduceGarage(Loaded1, SetGarageNotLoaded()).IsLoaded
)
val SC = ReduceGarage(S0, SelectGarageCategory("paints"))
Check(
"切片 Garage:SelectGarageCategory 同时清空 currentItemId",
SC.CurrentCategory == "paints" && SC.CurrentItemId == null
)
val SI = ReduceGarage(SC, GarageSelectItem(101))
Check(
"切片 Garage:GarageSelectItem 只改 currentItemId",
SI.CurrentItemId == 101L && SI.CurrentCategory == "paints"
)
val I101 = ToItem(101, Count = 3)
val SU = ReduceGarage(BaseState(listOf(I101)), GarageUpdateItemCount(I101, 9))
Check("切片 Garage:GarageUpdateItemCount", ItemIn(SU, 101).count == 9)
val L1 = ToItem(110, Base = 110, Level = 1, DevicePreview = "lvl-1")
val L2 = ToItem(111, Base = 110, Level = 2, DevicePreview = "lvl-2")
val L3 = ToItem(112, Base = 110, Level = 3, DevicePreview = "lvl-3")
val AddCount = ReduceGarage(BaseState(listOf(L1, L2, L3)), AddGarageItemCount(L2, 4))
Check(
"切片 Garage:AddGarageItemCount(owned=true、count+=、devicePreview 取等级项)",
ItemIn(AddCount, 111).owned && ItemIn(AddCount, 111).count == 5 &&
ItemIn(AddCount, 111).devicePreview == "lvl-2"
)
val M1 = ToItem(201, Mounted = true)
val M2 = ToItem(202)
val Mounted = ReduceGarage(BaseState(listOf(M1, M2)), GarageMountItem(M2))
Check(
"切片 Garage:GarageMountItem 先卸同 viewCategory 再装,并写 waitServerItemMount",
!ItemIn(Mounted, 201).mounted && ItemIn(Mounted, 202).mounted &&
Mounted.WaitServerItemMount[1] == true
)
val Unmounted = ReduceGarage(Mounted, GarageUnmountItem(202))
Check("切片 Garage:GarageUnmountItem", !ItemIn(Unmounted, 202).mounted)
val MountAll = ReduceGarage(
BaseState(listOf(M1, M2)),
GarageMountItems(listOf(201, 202)),
)
Check(
"切片 Garage:GarageMountItems 批量 owned+mounted",
ItemIn(MountAll, 201).mounted && ItemIn(MountAll, 201).owned &&
ItemIn(MountAll, 202).mounted && ItemIn(MountAll, 202).owned
)
val SkinBase = BaseState(listOf(ToItem(301, Base = 301), ToItem(302, Base = 301)))
val Skinned = ReduceGarage(SkinBase, SetMountedSkinForItems(9001, ItemIn(SkinBase, 302)))
Check(
"切片 Garage:SetMountedSkinForItems 整组写入 mountedSkin",
ItemIn(Skinned, 301).mountedSkin == 9001L && ItemIn(Skinned, 302).mountedSkin == 9001L
)
val Shot = ReduceGarage(SkinBase, SetMountedShotSkinForItems(9002, ItemIn(SkinBase, 302)))
Check(
"切片 Garage:SetMountedShotSkinForItems 整组写入 mountedShotSkin",
ItemIn(Shot, 301).mountedShotSkin == 9002L && ItemIn(Shot, 302).mountedShotSkin == 9002L
)
val R1 = ToItem(401, View = 5)
val R2 = ToItem(402, View = 5)
val Resist = ReduceGarage(BaseState(listOf(R1, R2)), GarageApplyResistanceMount(R1, 3, false))
Check(
"切片 Garage:GarageApplyResistanceMount(mounted + mountIndex + waitServerItemMount)",
ItemIn(Resist, 401).mounted && ItemIn(Resist, 401).mountIndex == 3 &&
Resist.WaitServerItemMount[GarageResistanceCategoryKey] == true
)
var ResistanceThrew = false
try {
ReduceGarage(Resist, GarageApplyResistanceMount(R2, 4, false))
} catch (_: Throwable) {
ResistanceThrew = true
}
Check("切片 Garage:抵抗模块装配数受 unlockedProtectionSlots 限制", ResistanceThrew)
val ResistOff = ReduceGarage(Resist, GarageResistanceUnMount(401, true))
Check(
"切片 Garage:GarageResistanceUnMount",
!ItemIn(ResistOff, 401).mounted &&
ResistOff.WaitServerItemMount[GarageResistanceCategoryKey] == true
)
val Up0 = ReduceGarage(
BaseState(listOf(ToItem(501, Level = 2, Confirmed = true))),
GarageUpgradeItemAction(501, 100, "coin"),
)
Check(
"切片 Garage:GarageUpgradeItemAction(level+1、confirmed=false、processing=itemId)",
ItemIn(Up0, 501).upgradeableParams!!.currentLevel == 3 &&
ItemIn(Up0, 501).upgradeableParams!!.upgradeConfirmed == false &&
Up0.UpgradeItemInProcessingId == 501L
)
val Up1 = ReduceGarage(Up0, GarageUpgradeItemCoolDownStarted(501, 12345))
Check("切片 Garage:GarageUpgradeItemCoolDownStarted", Up1.UpgradeCoolDownEndTimes[501L] == 12345L)
val Up2 = ReduceGarage(Up1, GarageUpgradeItemCoolDownFinished(501, 12345))
Check("切片 Garage:GarageUpgradeItemCoolDownFinished 命中时删键", Up2.UpgradeCoolDownEndTimes.isEmpty())
Check(
"切片 Garage:GarageUpgradeItemCoolDownFinished 不命中时原样返回",
ReduceGarage(Up1, GarageUpgradeItemCoolDownFinished(501, 999)) == Up1
)
val Confirmed = ReduceGarage(Up0, GarageUpgradeItemConfirm(501, 3))
Check(
"切片 Garage:GarageUpgradeItemConfirm 确认并清 processing",
ItemIn(Confirmed, 501).upgradeableParams!!.upgradeConfirmed == true &&
Confirmed.UpgradeItemInProcessingId == null
)
val ConfirmedOther = ReduceGarage(
Up0.copy(UpgradeItemInProcessingId = 999L),
GarageUpgradeItemConfirm(501, 3),
)
Check(
"切片 Garage:Confirm 其他 processing id 不清",
ConfirmedOther.UpgradeItemInProcessingId == 999L
)
Check(
"切片 Garage:ClearUpgradeItemInProcessing(原 xU 单例)",
ReduceGarage(Up0, ClearUpgradeItemInProcessing()).UpgradeItemInProcessingId == null
)
val DItem = ToItem(601)
val DBase = BaseState(listOf(DItem), mapOf(601L to listOf(ToDevice(9001, 601))))
val DLoaded = ReduceGarage(
DBase,
GarageDevicesLoadedAction(
601,
listOf(ToDeviceObj(9001, 601), ToDeviceObj(9002, 601)),
ToDevice(9002, 601),
),
)
val DList = DLoaded.Devices.baseItemIdToDevices[601L].orEmpty()
Check(
"切片 Garage:GarageDevicesLoadedAction 合并旧列表并按 mountedDevice 置 installed",
DList.map { it.id }.sorted() == listOf(9001L, 9002L) &&
!DList.first { it.id == 9001L }.installed &&
DList.first { it.id == 9002L }.installed
)
val DRemoved = ReduceGarage(DLoaded, GarageRemoveDevice(ToDevice(9001, 601), DItem))
Check(
"切片 Garage:GarageRemoveDevice(devicePreview=null、installed=false)",
ItemIn(DRemoved, 601).devicePreview == null &&
!DRemoved.Devices.baseItemIdToDevices[601L]!!.first { it.id == 9001L }.installed
)
val DInserted = ReduceGarage(
DLoaded,
GarageInsertDeviceClientAndServer(ToDevice(9002, 601, Preview = "dev-preview"), DItem),
)
Check(
"切片 Garage:GarageInsertDeviceClientAndServer(devicePreview=设备预览图、installed=true)",
ItemIn(DInserted, 601).devicePreview == "dev-preview" &&
DInserted.Devices.baseItemIdToDevices[601L]!!.first { it.id == 9002L }.installed
)
val DBought = ReduceGarage(
DLoaded,
GarageBuyDevice(ToDevice(9002, 601, Preview = "bought"), DItem, "coin", true),
)
Check(
"切片 Garage:GarageBuyDevice(installed=mount、infinityLifetimeItem=true、devicePreview)",
ItemIn(DBought, 601).devicePreview == "bought" &&
DBought.Devices.baseItemIdToDevices[601L]!!.first { it.id == 9002L }.installed &&
DBought.Devices.baseItemIdToDevices[601L]!!.first { it.id == 9002L }.infinityLifetimeItem
)
val DInf = ReduceGarage(DLoaded, GarageDeviceTrySetInfinityLifetime(9001))
Check(
"切片 Garage:GarageDeviceTrySetInfinityLifetime",
DInf.Devices.baseItemIdToDevices[601L]!!.first { it.id == 9001L }.infinityLifetimeItem
)
val Sel = ReduceGarage(S0, GarageSelectAlteration(7001))
val Sel2 = ReduceGarage(Sel, GarageSelectSkin(7002))
val Sel3 = ReduceGarage(Sel2, GarageSelectShotSkin(7003))
Check(
"切片 Garage:三个选择动作",
Sel3.SelectAlterationId == 7001L && Sel3.SelectedSkinId == 7002L &&
Sel3.SelectedShotSkinId == 7003L
)
val DiscItem = ToItem(801, Owned = true, HasDeviceSale = true)
val NoDisc = ReduceGarage(BaseState(listOf(DiscItem)), GarageRemoveDiscountForDevice(DiscItem))
Check(
"切片 Garage:GarageRemoveDiscountForDevice(hasDeviceSale && owned)",
ItemIn(NoDisc, 801).hasDeviceSale
)
val UnownedDisc = ToItem(802, Owned = false, HasDeviceSale = true)
val NoDisc2 = ReduceGarage(BaseState(listOf(UnownedDisc)), GarageRemoveDiscountForDevice(UnownedDisc))
Check("切片 Garage:未拥有项去折扣后 hasDeviceSale=false", !ItemIn(NoDisc2, 802).hasDeviceSale)
Check(
"切片 Garage:UpdateDiscount 保留原 discount 值",
ReduceGarage(BaseState(listOf(ToItem(803, Discount = "d"))), UpdateDiscount(ToItem(803, Discount = "d")))
.let { ItemIn(it, 803).discount == "d" }
)
val DepotItem = ToItem(901, Owned = false, Count = 1)
val DepotState = BaseState(listOf(DepotItem))
val Added = ReduceGarage(DepotState, GarageAddItemToDepot(901, 5))
Check("切片 Garage:GarageAddItemToDepot(owned=true、count 覆盖)",
ItemIn(Added, 901).owned && ItemIn(Added, 901).count == 5)
val Removed = ReduceGarage(Added, GarageRemoveItemFromDepot(ToObj(901, Count = 2)))
Check("切片 Garage:GarageRemoveItemFromDepot(owned=false、count 取动作 item)",
!ItemIn(Removed, 901).owned && ItemIn(Removed, 901).count == 2)
val NotSold = ReduceGarage(Added, GarageRemoveItemFromStore(901))
Check("切片 Garage:GarageRemoveItemFromStore(buyable=false)", !ItemIn(NotSold, 901).buyable)
val New0 = ReduceGarage(S0, SetNewItems(mapOf(1L to setOf(10L, 20L))))
val New1 = ReduceGarage(New0, AddNewItem(1, 30))
val New2 = ReduceGarage(New1, AddNewItem(2, 5))
Check(
"切片 Garage:SetNewItems / AddNewItem",
New2.NewItems[1L] == setOf(10L, 20L, 30L) && New2.NewItems[2L] == setOf(5L)
)
val New3 = ReduceGarage(New2, RemoveNewItem(1, 10))
val New4 = ReduceGarage(New3, RemoveNewItem(1, 20))
val New5 = ReduceGarage(New4, RemoveNewItem(1, 30))
Check(
"切片 Garage:RemoveNewItem 逐个移除并在空集时删键",
New3.NewItems[1L] == setOf(20L, 30L) && !New5.NewItems.containsKey(1L) &&
New5.NewItems[2L] == setOf(5L)
)
Check(
"切片 Garage:SetResistanceReadyToMount",
ReduceGarage(S0, SetResistanceReadyToMount(true)).ResistanceReadyToMount
)
val SaleBase = BaseState(
listOf(ToItem(1001, Base = 1001), ToItem(1002, Base = 1001), ToItem(1003, Base = 1003))
)
val Sale = ReduceGarage(SaleBase, RemoveFromSale(listOf(1001L)))
Check(
"切片 Garage:RemoveFromSale 只影响给定 baseItemId 组",
!ItemIn(Sale, 1001).buyable && !ItemIn(Sale, 1002).buyable && ItemIn(Sale, 1003).buyable
)
val Delay = ReduceGarage(S0, SetRestrictionMount(5000))
Check(
"切片 Garage:SetRestrictionMount 正延迟写成 now+delay",
Delay.DelayMountTimeMs > 0L && ReduceGarage(S0, SetRestrictionMount(0)).DelayMountTimeMs == 0L
)
Check("切片 Garage:ResetMountDelay", ReduceGarage(Delay, ResetMountDelay()).DelayMountTimeMs == 0L)
val Cool = ReduceGarage(S0, StartCooldownReduce(60, 5))
Check(
"切片 Garage:StartCooldownReduce(now+seconds*1000、minutes)",
Cool.CooldownReduceEndTime > 0L && Cool.TimeToReduceUpdateInMin == 5
)
Check(
"切片 Garage:SetCooldownReduceEndTime(now+seconds*1000)",
ReduceGarage(S0, SetCooldownReduceEndTime(10)).CooldownReduceEndTime > 0L
)
Check("切片 Garage:SetItemRarity", ReduceGarage(S0, SetItemRarity("rare")).CurrentRarity == "rare")
val Fav = BaseState(listOf(ToItem(1101)))
val Favorited = ReduceGarage(Fav, AddToFavoriteItem(1101))
Check("切片 Garage:AddToFavoriteItem", ItemIn(Favorited, 1101).favorite)
Check(
"切片 Garage:RemoveFromFavoriteItem",
!ItemIn(ReduceGarage(Favorited, RemoveFromFavoriteItem(1101)), 1101).favorite
)
val GoGarage = ReduceGarage(SC.copy(CurrentItemId = 101L), GoTo(NamedScreen("Garage"), false))
Check(
"切片 Garage:GoTo Garage 复位 currentCategory 并原样返回 currentItemId",
GoGarage.CurrentCategory == GarageDefaultCategory && GoGarage.CurrentItemId == 101L
)
val GoShop = ReduceGarage(SC, GoTo(NamedScreen("Shop"), false))
Check("切片 Garage:GoTo 其他屏幕不改分类", GoShop.CurrentCategory == "paints")
Check(
"切片 Garage:ShowGarageDiscountNotification",
ReduceGarage(S0, ShowGarageDiscountNotification()).GarageDiscountNotification
)
Check(
"切片 Garage:HideVideoInDescription",
!ReduceGarage(S0, HideVideoInDescription()).ShowVideoInDescription
)
Check(
"切片 Garage:BuyProtectionSlot",
ReduceGarage(S0, BuyProtectionSlot(1)).UnlockedProtectionSlots == 2
)
Check(
"切片 Garage:SwitchingRowsScroll",
ReduceGarage(S0, SwitchingRowsScroll(4)).NumberRowsInPaintsUI == 4
)
val Desc = OverdriveDescriptionData("name", "icon", "text")
Check(
"切片 Garage:AddOverdriveDescription",
ReduceGarage(S0, AddOverdriveDescription(12, Desc)).OverdriveDescriptions[12L] == Desc
)
Check(
"切片 Garage:AddRelatedResistanceModule",
ReduceGarage(S0, AddRelatedResistanceModule(12, 34)).WeaponToResistanceModule[12L] == 34L
)
Check(
"切片 Garage:AddRelatedBazooka",
ReduceGarage(S0, AddRelatedBazooka(12, 34)).BazookaToSupply[12L] == 34L
)
val Wait0 = S0.copy(WaitServerItemMount = mapOf(1 to true, 2 to true))
val Wait1 = ReduceGarage(Wait0, RemoveWaitServerItemMount(listOf(1)))
Check(
"切片 Garage:RemoveWaitServerItemMount 给定键置 false",
Wait1.WaitServerItemMount[1] == false && Wait1.WaitServerItemMount[2] == true
)
Check(
"切片 Garage:ClearWaitServerItemMount",
ReduceGarage(Wait1, ClearWaitServerItemMount()).WaitServerItemMount.isEmpty()
)
val Reset0 = S0.copy(
Devices = Devices(mapOf(1L to listOf(ToDevice(1, 1)))),
UpgradeCoolDownEndTimes = mapOf(1L to 2L),
UpgradeItemInProcessingId = 3L,
)
val Reset1 = ReduceGarage(Reset0, ResetGarage())
Check(
"切片 Garage:ResetGarage 清 devices / coolDown / processing",
Reset1.Devices.baseItemIdToDevices.isEmpty() &&
Reset1.UpgradeCoolDownEndTimes.isEmpty() && Reset1.UpgradeItemInProcessingId == null
)
val Init = ReduceGarage(
S0,
InitState(GarageStateSlice::class, { S: GarageStateSlice -> S.copy(IsLoaded = true) }),
)
Check("切片 Garage:InitState 按 GarageStateSlice::class 灌入状态", Init.IsLoaded)
Check(
"切片 Garage:InitState 其他类型不改状态",
ReduceGarage(S0, InitState(String::class, { S: GarageStateSlice -> S.copy(IsLoaded = true) })) == S0
)
Check("切片 Garage:无关动作返回等值状态", ReduceGarage(BaseState(listOf(I101)), AddFrags(1)) == BaseState(listOf(I101)))
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, GarageSelectItem(101))
Check(
"根:GarageSelectItem 只动 garage",
Root1.Garage.CurrentItemId == 101L && Root1.Shop == Root0.Shop && Root1.Screen == Root0.Screen &&
Root1.LootBoxes == Root0.LootBoxes && Root1.Home == Root0.Home
)
val Root2 = ReduceRoot(Root1, SetGarageObject(GarageObj))
Check(
"根:SetGarageObject 保留 garage 前一次归约",
Root2.Garage.GarageObject == GarageObj && Root2.Garage.CurrentItemId == 101L &&
Root2.LootBoxes == Root1.LootBoxes
)
Check("根:garage 迁移后无关动作仍返回等值状态", ReduceRoot(Root2, AddFrags(1)) == Root2)
val W = RootWiring.ByField("garage")!!
Check(
"根:garage 委派到 UB / hF 且 58 个动作全部具名",
W.SliceClass == "UB" && W.Reducer == "hF" && W.Actions.size == 58 &&
W.Actions.none { it.startsWith("?") }
)
Check(
"根:garage 的动作名覆盖 58 个 instanceof 臂",
listOf(
"ResetGarage", "SetGarageObject", "SetItemsOnDepot", "SetItemsOnMarket",
"SetMountedItems", "GarageLoaded", "SetGarageNotLoaded", "SelectGarageCategory",
"GarageSelectItem", "GarageUpdateItemCount", "AddGarageItemCount", "GarageMountItem", "GarageUnmountItem",
"GarageMountItems", "SetMountedSkinForItems", "SetMountedShotSkinForItems",
"SetGarageSkinsAction",
"GarageApplyResistanceMount", "GarageResistanceUnMount", "GarageUpgradeItemAction",
"GarageUpgradeItemCoolDownStarted", "GarageUpgradeItemCoolDownFinished",
"GarageUpgradeItemConfirm", "GarageDevicesLoadedAction", "GarageRemoveDevice",
"GarageBuyDevice",
"GarageDeviceTrySetInfinityLifetime", "GarageSelectAlteration", "GarageSelectSkin",
"GarageSelectShotSkin", "GarageRemoveDiscountForDevice", "UpdateDiscount",
"GarageAddItemToDepot", "GarageRemoveItemFromDepot", "GarageRemoveItemFromStore",
"SetNewItems", "AddNewItem", "RemoveNewItem", "SetResistanceReadyToMount",
"RemoveFromSale", "SetRestrictionMount", "ResetMountDelay", "StartCooldownReduce",
"SetCooldownReduceEndTime", "SetItemRarity", "AddToFavoriteItem",
"RemoveFromFavoriteItem", "GoTo", "ShowGarageDiscountNotification",
"HideVideoInDescription", "BuyProtectionSlot", "SwitchingRowsScroll",
"AddOverdriveDescription", "AddRelatedResistanceModule", "AddRelatedBazooka",
"RemoveWaitServerItemMount", "ClearWaitServerItemMount", "InitState",
).all { it in W.Actions }
)
Check(
"根:garage 的动作表不含 FX / UB 标识类",
W.Actions.none { it == "FX" || it == "UB" || it == "Garage" }
)
}
// ⑳ 切片 reducer:GaragePreview(原 module_10032 的 IG / fs.eru,22 字段;25 个顶层臂)
run {
val S0 = GaragePreviewStateSlice()
Check(
"切片 GaragePreview:默认值与原构造一致(22 字段)",
!S0.IsVisible && !S0.PhotoMode && !S0.NeedVisible &&
S0.ViewStyle == null && S0.DynViewStyle == null &&
S0.TankPreviewSettings is TankPreviewSettings && !S0.NeedShowDrone &&
S0.Params == null && S0.HullData == null && S0.WeaponData == null &&
S0.DroneData == null && S0.GrenadeData == null && !S0.HasGrenades &&
S0.HullEngineType == HullEngineType.TRACKED &&
S0.TurretMechanism == TurretMechanism.ROTATING_TURRET &&
S0.Paint == null && S0.Properties.isEmpty() &&
S0.CaptureScreenshotAction == null && S0.CaptureVideoAction == null &&
S0.Item3dModelActionCache.isEmpty() && S0.RequestedItems.isEmpty() &&
S0.ExpectedItemId.isEmpty()
)
val Colors = EmissionColors(1, 2, 3, 4, "STRICT")
val OldSettings = TankPreviewSettings(cameraHeight = 111)
val NewSettings = TankPreviewSettings(cameraHeight = 777)
val Params = GaragePreviewModelCC(80, 1000, 1.57, -110, null, null)
val OldPaint = Coloring(null, TextureFrameAnimation(32, 4, 8, 16))
val NewPaint = Coloring(null, TextureFrameAnimation(64, 8, 8, 32))
val OldPart = TankPartData(resource = "old")
val Base = S0.copy(
IsVisible = true,
NeedVisible = false,
ViewStyle = "old",
DynViewStyle = "dyn-old",
TankPreviewSettings = OldSettings,
Params = Params,
HullData = OldPart,
WeaponData = OldPart,
DroneData = OldPart,
GrenadeData = OldPart,
Paint = OldPaint,
Properties = listOf("old"),
)
val ClearParams = ReduceGaragePreview(Base, ClearGaragePreviewParams())
Check("切片 GaragePreview:ClearGaragePreviewParams 只清 params", ClearParams.Params == null)
val Shown = ReduceGaragePreview(Base.copy(NeedVisible = true), ShowGaragePreview())
Check(
"切片 GaragePreview:ShowGaragePreview 置 isVisible=true、needVisible=false",
Shown.IsVisible && !Shown.NeedVisible
)
Check(
"切片 GaragePreview:HideGaragePreview 置 isVisible=false",
!ReduceGaragePreview(Base, HideGaragePreview()).IsVisible
)
val Req = ShowRequest(
viewStyle = "new",
tankPreviewSettings = NewSettings,
needShowDrone = true,
photoMode = true,
withDelay = false,
)
val S1 = ReduceGaragePreview(Base, Req)
Check(
"切片 GaragePreview:ShowRequest(无延迟)保留 isVisible 并写 view/settings/photo/drone",
S1.IsVisible && !S1.NeedVisible && S1.ViewStyle == "new" &&
S1.TankPreviewSettings == NewSettings && S1.PhotoMode &&
S1.NeedShowDrone && S1.DynViewStyle == "dyn-old" &&
S1.Params == Params && S1.HullData == OldPart
)
val S2 = ReduceGaragePreview(Base, Req.copy(withDelay = true))
Check(
"切片 GaragePreview:ShowRequest(延迟)置 isVisible=false、needVisible=true",
!S2.IsVisible && S2.NeedVisible && S2.ViewStyle == "new"
)
Check(
"切片 GaragePreview:UpdatePreviewSettings 只写 tankPreviewSettings",
ReduceGaragePreview(Base, UpdatePreviewSettings(NewSettings)).TankPreviewSettings == NewSettings
)
Check(
"切片 GaragePreview:UpdateViewStyle 无字段,原 fru_1=undefined 被 trs 视作省略",
ReduceGaragePreview(Base, UpdateViewStyle()) == Base
)
val Init = ReduceGaragePreview(S0, InitGarageResources(Params))
Check("切片 GaragePreview:InitGarageResources 写 params", Init.Params == Params)
val Expected1 = ReduceGaragePreview(S0, SetExpectedItemId(PreviewHullCategory, 42L))
Check(
"切片 GaragePreview:SetExpectedItemId 写分类键",
Expected1.ExpectedItemId[PreviewHullCategory] == 42L
)
val Expected2 = ReduceGaragePreview(Expected1, SetExpectedItemId(PreviewHullCategory, null))
Check(
"切片 GaragePreview:SetExpectedItemId(itemId=null) 删除分类键",
!Expected2.ExpectedItemId.containsKey(PreviewHullCategory)
)
Check(
"切片 GaragePreview:AddRequestedItem 加入 requestedItems",
ReduceGaragePreview(S0, AddRequestedItem(42L)).RequestedItems == setOf(42L)
)
val HullAction = SetHullData(
itemId = 42L,
hullEngineType = HullEngineType.HOVER,
resource = "hull",
emissionColors = Colors,
turretMountPoint = "mount",
skinOffset = "skin",
leftTrackTexToWorldRatio = 7,
rightTrackTexToWorldRatio = 8,
)
val HullBase = S0.copy(RequestedItems = setOf(42L))
val HullHit = ReduceGaragePreview(HullBase, HullAction)
Check(
"切片 GaragePreview:SetHullData 命中时写 hullData/hullEngineType、缓存动作并删 requested",
HullHit.HullData == TankPartData("hull", Colors, "mount", "skin", 7, 8) &&
HullHit.HullEngineType == HullEngineType.HOVER &&
HullHit.Item3dModelActionCache[42L] == HullAction &&
HullHit.RequestedItems.isEmpty()
)
val HullMiss = ReduceGaragePreview(
HullBase.copy(ExpectedItemId = mapOf<Any?, Any?>(PreviewHullCategory to 99L)),
HullAction,
)
Check(
"切片 GaragePreview:SetHullData 未命中只缓存动作,不写 hullData",
HullMiss.HullData == null && HullMiss.HullEngineType == HullEngineType.TRACKED &&
HullMiss.Item3dModelActionCache[42L] == HullAction &&
HullMiss.RequestedItems.isEmpty()
)
val WeaponAction = SetWeaponData(
itemId = 43L,
turretMechanism = TurretMechanism.ELEVATING_BARREL,
resource = "weapon",
emissionColors = Colors,
)
val WeaponBase = S0.copy(RequestedItems = setOf(43L))
val WeaponHit = ReduceGaragePreview(WeaponBase, WeaponAction)
Check(
"切片 GaragePreview:SetWeaponData 命中时写 weaponData/turretMechanism、缓存动作并删 requested",
WeaponHit.WeaponData == TankPartData("weapon", Colors) &&
WeaponHit.TurretMechanism == TurretMechanism.ELEVATING_BARREL &&
WeaponHit.Item3dModelActionCache[43L] == WeaponAction &&
WeaponHit.RequestedItems.isEmpty()
)
val WeaponMiss = ReduceGaragePreview(
WeaponBase.copy(ExpectedItemId = mapOf<Any?, Any?>(PreviewWeaponCategory to 99L)),
WeaponAction,
)
Check(
"切片 GaragePreview:SetWeaponData 未命中只缓存动作,不写 weaponData",
WeaponMiss.WeaponData == null &&
WeaponMiss.TurretMechanism == TurretMechanism.ROTATING_TURRET &&
WeaponMiss.Item3dModelActionCache[43L] == WeaponAction
)
val DroneAction = SetDrone(44L, "drone", Colors)
val DroneHit = ReduceGaragePreview(S0.copy(RequestedItems = setOf(44L)), DroneAction)
Check(
"切片 GaragePreview:SetDrone 命中时写 droneData 并删 requested",
DroneHit.DroneData == TankPartData("drone", Colors) &&
DroneHit.Item3dModelActionCache[44L] == DroneAction &&
DroneHit.RequestedItems.isEmpty()
)
val DroneMiss = ReduceGaragePreview(
S0.copy(
DroneData = OldPart,
ExpectedItemId = mapOf<Any?, Any?>(PreviewDroneCategory to 99L),
),
DroneAction,
)
Check(
"切片 GaragePreview:SetDrone 未命中保留 droneData",
DroneMiss.DroneData == OldPart && DroneMiss.Item3dModelActionCache[44L] == DroneAction
)
Check(
"切片 GaragePreview:SetDrone(resource=null) 命中时写 null",
ReduceGaragePreview(S0.copy(DroneData = OldPart), SetDrone(45L, null, Colors)).DroneData == null
)
Check(
"切片 GaragePreview:SetGrenadeData 写 grenadeData",
ReduceGaragePreview(S0, SetGrenadeData("grenade", Colors)).GrenadeData ==
TankPartData("grenade", Colors)
)
Check(
"切片 GaragePreview:SetGrenadeData(resource=null) 写 null",
ReduceGaragePreview(S0.copy(GrenadeData = OldPart), SetGrenadeData(null, Colors)).GrenadeData == null
)
Check("切片 GaragePreview:ClearHullData", ReduceGaragePreview(Base, ClearHullData()).HullData == null)
Check("切片 GaragePreview:ClearWeaponData", ReduceGaragePreview(Base, ClearWeaponData()).WeaponData == null)
Check("切片 GaragePreview:ClearDroneData", ReduceGaragePreview(Base, ClearDroneData()).DroneData == null)
Check("切片 GaragePreview:ClearGrenadeData", ReduceGaragePreview(Base, ClearGrenadeData()).GrenadeData == null)
Check(
"切片 GaragePreview:SetHasGrenades",
ReduceGaragePreview(S0, SetHasGrenades()).HasGrenades
)
Check(
"切片 GaragePreview:ClearHasGrenades",
!ReduceGaragePreview(S0.copy(HasGrenades = true), ClearHasGrenades()).HasGrenades
)
val PaintAction = SetPaint(46L, NewPaint)
val PaintHit = ReduceGaragePreview(S0.copy(RequestedItems = setOf(46L)), PaintAction)
Check(
"切片 GaragePreview:SetPaint 命中时写 paint、缓存动作并删 requested",
PaintHit.Paint == NewPaint && PaintHit.Item3dModelActionCache[46L] == PaintAction &&
PaintHit.RequestedItems.isEmpty()
)
val PaintMiss = ReduceGaragePreview(
S0.copy(
Paint = OldPaint,
ExpectedItemId = mapOf<Any?, Any?>(PreviewPaintCategory to 99L),
),
PaintAction,
)
Check(
"切片 GaragePreview:SetPaint 未命中保留 paint",
PaintMiss.Paint == OldPaint && PaintMiss.Item3dModelActionCache[46L] == PaintAction
)
Check(
"切片 GaragePreview:ResetItem3dModelActionCache 删缓存",
!ReduceGaragePreview(PaintHit, ResetItem3dModelActionCache(46L))
.Item3dModelActionCache.containsKey(46L)
)
Check(
"切片 GaragePreview:SetMountedItemsProperties 写 properties",
ReduceGaragePreview(S0, SetMountedItemsProperties(listOf("ROTATING_WEAPON", "HOVER_CHASSIS")))
.Properties == listOf("ROTATING_WEAPON", "HOVER_CHASSIS")
)
var Shots = 0
var Videos = 0
val Capture = ReduceGaragePreview(S0, SetCaptureActions({ Shots += 1 }, { Videos += 1 }))
Check(
"切片 GaragePreview:SetCaptureActions 写两个回调",
Capture.CaptureScreenshotAction != null && Capture.CaptureVideoAction != null
)
Check(
"切片 GaragePreview:CaptureScreenshot 只调用回调、状态不变",
ReduceGaragePreview(Capture, CaptureScreenshot()) == Capture && Shots == 1
)
Check(
"切片 GaragePreview:CaptureVideo 只调用回调、状态不变",
ReduceGaragePreview(Capture, CaptureVideo()) == Capture && Videos == 1
)
Check(
"切片 GaragePreview:无关动作返回等值状态",
ReduceGaragePreview(Base, AddFrags(1)) == Base
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddRequestedItem(7L))
Check(
"根:AddRequestedItem 只动 garagePreview",
Root1.GaragePreview.RequestedItems == setOf(7L) && Root1.Garage == Root0.Garage &&
Root1.Shop == Root0.Shop && Root1.Screen == Root0.Screen && Root1.LootBoxes == Root0.LootBoxes
)
val Root2 = ReduceRoot(Root1, SetHasGrenades())
Check(
"根:SetHasGrenades 保留 garagePreview 前一次归约",
Root2.GaragePreview.HasGrenades && Root2.GaragePreview.RequestedItems == setOf(7L)
)
Check("根:garagePreview 迁移后无关动作仍返回等值状态", ReduceRoot(Root2, AddFrags(1)) == Root2)
val W = RootWiring.ByField("garagePreview")!!
Check(
"根:garagePreview 委派到 IG / fs.eru 且 22 个动作全部具名",
W.SliceClass == "IG" && W.Reducer == "fs.eru" && W.Actions.size == 22 &&
W.Actions.none { it.startsWith("?") }
)
Check(
"根:garagePreview 的动作名覆盖 22 个 instanceof 臂",
listOf(
"AddRequestedItem", "CaptureScreenshot", "CaptureVideo", "ClearDroneData",
"ClearGrenadeData", "ClearHasGrenades", "ClearHullData", "ClearWeaponData",
"InitGarageResources", "ResetItem3dModelActionCache", "SetCaptureActions",
"SetDrone", "SetExpectedItemId", "SetGrenadeData", "SetHasGrenades",
"SetHullData", "SetMountedItemsProperties", "SetPaint", "SetWeaponData",
"ShowRequest", "UpdatePreviewSettings", "UpdateViewStyle",
).all { it in W.Actions }
)
Check(
"根:garagePreview 的动作表不含 IG 标识和 3 个单例臂",
W.Actions.none {
it == "IG" || it == "GaragePreview" ||
it == "ClearGaragePreviewParams" || it == "ShowGaragePreview" || it == "HideGaragePreview"
}
)
}
// ㉑ 切片类型收口:launchParams(原 hpx_1;根 reducer 纯透传,无动作)
run {
val S0 = RootState()
Check(
"切片 LaunchParams:默认是空 Map",
S0.LaunchParams.isEmpty()
)
val Params = mapOf(
"token" to "token-1",
"singleUseHash" to "hash-1",
"changeUidHash" to "uid-hash-1",
"failed-redirect-url" to "https://example.test/failed",
)
val S1 = S0.copy(LaunchParams = Params)
Check(
"根:launchParams 在无关动作后原样保留",
ReduceRoot(S1, AddFrags(1)).LaunchParams == Params
)
Check(
"根:其他切片动作不修改 launchParams",
ReduceRoot(S1, ApplyInvite("uid-1")).LaunchParams == Params
)
val W = RootWiring.ByField("launchParams")!!
Check(
"根:launchParams 是无 SliceClass / 无动作的纯透传",
W.SliceClass == "?" && W.Reducer == "(pass-through)" &&
W.Delegate == "n.hpx_1" && W.Actions.isEmpty()
)
Check(
"根:launchParams 计入迁移表且不再出现在待迁清单",
"launchParams" in RootReducer.Migrated &&
RootWiring.Unmigrated.none { it.Field == "launchParams" }
)
}
// ㉒ 切片 reducer:Entrance(原 module_10032 的 hD / Bv.pth,8 字段;7 个 instanceof 臂)
run {
val S0 = EntranceStateSlice()
Check(
"切片 Entrance:默认值与原构造一致(showSocialNetworks 缺省 true,其余 false)",
S0.ShowSocialNetworks && !S0.ControlsForChina &&
!S0.IsRunningInChinaDataCenter && !S0.IsRunningInChinaPartners &&
!S0.IsAgeWarnShow && !S0.AuthorizedByChinaSdkToken &&
!S0.IsClassic && !S0.AuthorizedByClassicEarlyAccessToken
)
Check(
"切片 Entrance:HideSocialNetworks 置 showSocialNetworks=false",
!ReduceEntrance(S0, HideSocialNetworks()).ShowSocialNetworks
)
Check(
"切片 Entrance:SetControlsForChina 置 controlsForChina=true",
ReduceEntrance(S0, SetControlsForChina()).ControlsForChina
)
Check(
"切片 Entrance:SetRunningInChinaDataCenter 置对应字段",
ReduceEntrance(S0, SetRunningInChinaDataCenter()).IsRunningInChinaDataCenter
)
Check(
"切片 Entrance:SetRunningInChinaPartners 置对应字段",
ReduceEntrance(S0, SetRunningInChinaPartners()).IsRunningInChinaPartners
)
Check(
"切片 Entrance:SetAgeWarnShow 写入 isAgeWarnShow",
ReduceEntrance(S0, SetAgeWarnShow(true)).IsAgeWarnShow &&
!ReduceEntrance(S0.copy(IsAgeWarnShow = true), SetAgeWarnShow(false)).IsAgeWarnShow
)
Check(
"切片 Entrance:SetAuthorizedByChinaSdkToken 置对应字段",
ReduceEntrance(S0, SetAuthorizedByChinaSdkToken()).AuthorizedByChinaSdkToken
)
Check(
"切片 Entrance:SetClassic 置 isClassic",
ReduceEntrance(S0, SetClassic()).IsClassic
)
val All = listOf(
HideSocialNetworks(),
SetControlsForChina(),
SetRunningInChinaDataCenter(),
SetRunningInChinaPartners(),
SetAgeWarnShow(true),
SetAuthorizedByChinaSdkToken(),
SetClassic(),
).fold(S0) { S, A -> ReduceEntrance(S, A) }
Check(
"切片 Entrance:七个动作依次归约后只改对应字段",
!All.ShowSocialNetworks && All.ControlsForChina &&
All.IsRunningInChinaDataCenter && All.IsRunningInChinaPartners &&
All.IsAgeWarnShow && All.AuthorizedByChinaSdkToken &&
All.IsClassic && !All.AuthorizedByClassicEarlyAccessToken
)
Check("切片 Entrance:无关动作不改状态", ReduceEntrance(All, AddFrags(1)) == All)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetClassic())
Check(
"根:SetClassic 只动 entrance",
Root1.Entrance.IsClassic && Root1.LoginByPassword == Root0.LoginByPassword &&
Root1.GaragePreview == Root0.GaragePreview && Root1.Garage == Root0.Garage
)
Check(
"根:entrance 无关动作保留前一次归约",
ReduceRoot(Root1, AddFrags(1)).Entrance == Root1.Entrance
)
val W = RootWiring.ByField("entrance")!!
Check(
"根:entrance 委派到 hD / Bv.pth 且 7 个动作全部具名",
W.SliceClass == "hD" && W.Reducer == "Bv.pth" && W.Actions.size == 7 &&
W.Actions.none { it.startsWith("?") }
)
Check(
"根:entrance 的动作名覆盖 7 个 instanceof 臂",
listOf(
"HideSocialNetworks", "SetAgeWarnShow", "SetAuthorizedByChinaSdkToken",
"SetClassic", "SetControlsForChina", "SetRunningInChinaDataCenter",
"SetRunningInChinaPartners",
).all { it in W.Actions }
)
}
// ㉓ 切片 reducer:User(原 module_10032 的 t8 / M8().qtd,32 字段;28 个 instanceof 臂)
run {
val S0 = UserStateSlice()
Check(
"切片 User:默认值与原构造一致",
!S0.IsUserPropertiesLoaded && S0.Id == 0L && S0.Clan == null &&
S0.Crystals == 0 && S0.Coins == 0 && S0.SpentCoins == 0 &&
S0.Rubies == 0 && S0.SpentRubies == 0 && !S0.HasTemporaryCash &&
S0.Rank == 1 && S0.RankBounds == UserRankBounds(0, 0) &&
S0.Score == 0 && S0.GearScore == 0 && S0.WeaponGS == 0 &&
S0.HullGS == 0 && S0.DroneGS == 0 && S0.BazookaGS == 0 &&
S0.ResistanceGS == 0 && S0.Rating == 0 && S0.Uid == "" &&
S0.ClanTag == null && S0.PremiumExpirationTime == 0L &&
S0.DisabledWithoutPremiumLocations.isEmpty() && !S0.HasPremium &&
!S0.HasSpectatorPermissions && S0.RankNames.isEmpty() &&
S0.NormalizedRank == 1 && S0.ProfileUrl == "" &&
S0.EventPassEndTime == 0L && S0.LobbyTutorialEnabled &&
!S0.InDebt && S0.BatteryLevel == null
)
Check(
"切片 User:SetClanInfo 写 clan",
ReduceUser(S0, SetClanInfo("clan-1")).Clan == "clan-1"
)
Check(
"切片 User:UpdateCrystals / SpendCrystals 写 crystal",
ReduceUser(S0, UpdateCrystals(50)).Crystals == 50 &&
ReduceUser(S0.copy(Crystals = 50), SpendCrystals(20)).Crystals == 30
)
val Coins = ReduceUser(
ReduceUser(ReduceUser(S0, SetCoins(100)), SpendCoins(30)),
AddCoins(10),
)
Check(
"切片 User:SetCoins / SpendCoins / AddCoins 维护 coins 与 spentCoins",
Coins.Coins == 110 && Coins.SpentCoins == 0
)
val Rubies = ReduceUser(
ReduceUser(ReduceUser(S0, SetRubies(80)), SpendRubies(20)),
AddRubies(5),
)
Check(
"切片 User:SetRubies / SpendRubies / AddRubies 维护 rubies 与 spentRubies",
Rubies.Rubies == 85 && Rubies.SpentRubies == 0
)
Check(
"切片 User:SetHasTemporaryCash 写 hasTemporaryCash",
ReduceUser(S0, SetHasTemporaryCash(true)).HasTemporaryCash
)
Check(
"切片 User:UpdateUserRating 写 rating",
ReduceUser(S0, UpdateUserRating(12)).Rating == 12
)
Check(
"切片 User:UpdateUid / UpdateClanTag 写 uid / clanTag",
ReduceUser(S0, UpdateUid("uid-42")).Uid == "uid-42" &&
ReduceUser(S0, UpdateClanTag("TAG")).ClanTag == "TAG"
)
Check(
"切片 User:UpdateScore 写 score",
ReduceUser(S0, UpdateScore(team = "red", score = 123)).Score == 123
)
Check(
"切片 User:UpdateGearScore 写六个装备分",
ReduceUser(S0, UpdateGearScore(1, 2, 3, 4, 5, 6)).let {
it.GearScore == 1 && it.WeaponGS == 2 && it.HullGS == 3 &&
it.DroneGS == 4 && it.BazookaGS == 5 && it.ResistanceGS == 6
}
)
val Rank = ReduceUser(
S0.copy(RankNames = listOf("A", "B", "C")),
UpdateRank(rank = 5, score = 500, bounds = UserRankBounds(1, 10)),
)
Check(
"切片 User:UpdateRank 写 rank/rankBounds/score 并重算 normalizedRank",
Rank.Rank == 5 && Rank.Score == 500 &&
Rank.RankBounds == UserRankBounds(1, 10) && Rank.NormalizedRank == 3
)
val Props = UserPropertiesCC(
bazookaGearScore = 6,
canUseGroup = true,
crystals = 321,
crystalsRating = 0,
droneGearScore = 5,
eventPassEndTime = 0,
gearScore = 21,
goldsTakenRating = 0,
hasSpectatorPermissions = true,
hullGearScore = 4,
id = 99L,
partnerUid = null,
rank = 7,
rankBounds = "1-10",
topBound = "10",
registrationTimestamp = 0,
resistanceGearScore = 3,
score = 777,
scoreRating = 0,
uid = "uid-99",
userProfileUrl = "https://example.test/u/99",
userRating = 11,
weaponGearScore = 2,
)
val SP = ReduceUser(
S0.copy(RankNames = listOf("A", "B", "C")),
SetUserPropertiesAction(Props),
)
Check(
"切片 User:SetUserPropertiesAction 复制服务端字段并置 isUserPropertiesLoaded",
SP.IsUserPropertiesLoaded && SP.Id == 99L && SP.Crystals == 321 &&
SP.Rank == 7 && SP.RankBounds == "1-10" && SP.Score == 777 &&
SP.GearScore == 21 && SP.WeaponGS == 2 && SP.HullGS == 4 &&
SP.DroneGS == 5 && SP.BazookaGS == 6 && SP.ResistanceGS == 3 &&
SP.Rating == 11 && SP.Uid == "uid-99" &&
SP.HasSpectatorPermissions && SP.NormalizedRank == 3 &&
SP.ProfileUrl == "https://example.test/u/99"
)
Check(
"切片 User:SetPremiumExpirationTime / UpdateEventPass 写时间戳",
ReduceUser(S0, SetPremiumExpirationTime(1_700_000_000_000L))
.PremiumExpirationTime == 1_700_000_000_000L &&
ReduceUser(S0, UpdateEventPass(1_700_000_000_001L))
.EventPassEndTime == 1_700_000_000_001L
)
Check(
"切片 User:SetDisabledWithoutPremiumLocations 去重成 Set",
ReduceUser(S0, SetDisabledWithoutPremiumLocations(listOf(1, 2, 2)))
.DisabledWithoutPremiumLocations == setOf(1, 2)
)
Check(
"切片 User:UpdatePremium / UpdateEventPass 写对应字段",
ReduceUser(S0, UpdatePremium(true)).HasPremium
)
Check(
"切片 User:SetRankNames 只写 rankNames",
ReduceUser(S0.copy(NormalizedRank = 9), SetRankNames(listOf("A", "B")))
.let { it.RankNames == listOf("A", "B") && it.NormalizedRank == 9 }
)
Check(
"切片 User:SetUserId 写 id,null 归零",
ReduceUser(S0, SetUserId(42L)).Id == 42L &&
ReduceUser(S0, SetUserId(null)).Id == 0L
)
Check(
"切片 User:DisableLobbyTutorial / SetBatteryLevel",
!ReduceUser(S0, DisableLobbyTutorial()).LobbyTutorialEnabled &&
ReduceUser(S0, SetBatteryLevel(0.75)).BatteryLevel == 0.75
)
Check(
"切片 User:SetInDebt / ClearInDebt 切换 inDebt",
ReduceUser(S0, SetInDebt()).InDebt &&
!ReduceUser(S0.copy(InDebt = true), ClearInDebt()).InDebt
)
Check("切片 User:无关动作不改状态", ReduceUser(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateCrystals(7))
Check(
"根:UpdateCrystals 只动 user",
Root1.User.Crystals == 7 &&
Root1.LoginByPassword == Root0.LoginByPassword &&
Root1.Entrance == Root0.Entrance
)
Check(
"根:user 无关动作保留前一次归约",
ReduceRoot(Root1, AddFrags(1)).User == Root1.User
)
val W = RootWiring.ByField("user")!!
Check(
"根:user 委派到 t8 / M8().qtd 且 27 个动作全部具名",
W.SliceClass == "t8" && W.Reducer == "M8().qtd" &&
W.Actions.size == 27 && W.Actions.none { it.startsWith("?") }
)
Check(
"根:user 的匿名动作名覆盖三个分支",
listOf("DisableLobbyTutorial", "SetInDebt", "ClearInDebt")
.all { it in W.Actions }
)
}
// ㉔ 切片 reducer:Settings(原 module_10032 的 Uut / xw.ltg,2 字段;2 个 instanceof 臂)
run {
val S0 = SettingsStateSlice()
Check(
"切片 Settings:默认 GAME / disableAccountSettings=false",
S0.CurrentCategory == SettingsCategory.GAME && !S0.DisableAccountSettings
)
Check(
"切片 Settings:SetSettingsCategory 只写 currentCategory",
ReduceSettings(S0, SetSettingsCategory(SettingsCategory.ACCOUNT)).let {
it.CurrentCategory == SettingsCategory.ACCOUNT && !it.DisableAccountSettings
}
)
Check(
"切片 Settings:DisableAccountSettings 只把开关置 true",
ReduceSettings(
S0.copy(CurrentCategory = SettingsCategory.SOUND),
DisableAccountSettings(),
).let {
it.CurrentCategory == SettingsCategory.SOUND && it.DisableAccountSettings
}
)
Check("切片 Settings:无关动作不改状态", ReduceSettings(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetSettingsCategory(SettingsCategory.CONTROL))
Check(
"根:SetSettingsCategory 只动 settings",
Root1.Settings.CurrentCategory == SettingsCategory.CONTROL &&
Root1.User == Root0.User && Root1.Home == Root0.Home
)
Check(
"根:settings 无关动作保留前一次归约",
ReduceRoot(Root1, AddFrags(1)).Settings == Root1.Settings
)
val W = RootWiring.ByField("settings")!!
Check(
"根:settings 委派到 Uut / xw.ltg 且 2 个动作全部具名",
W.SliceClass == "Uut" && W.Reducer == "xw.ltg" &&
W.Actions.toSet() == setOf("DisableAccountSettings", "SetSettingsCategory")
)
}
// ㉕ 切片 reducer:AccountSettings(oet / Lp.mtg,13 字段;13 个 instanceof 臂)
run {
val S0 = AccountSettingsStateSlice()
Check(
"切片 AccountSettings:默认值完整",
!S0.IsCurrentPasswordMandatory && S0.CurrentPassword == "" &&
S0.NewPassword == "" && S0.RepeatNewPassword == "" &&
S0.NewPasswordStatus == PasswordStatus.NONE && S0.CountryCode == null &&
S0.CountryName == null && S0.Email == null && !S0.EmailConfirmed &&
!S0.LocationCheckEnabled && !S0.IsUnlockButtonForRenameUid &&
!S0.IsClearInputUid && S0.RemovalTimer == null
)
val Reset = ReduceAccountSettings(
S0.copy(
CurrentPassword = "old",
NewPassword = "new",
RepeatNewPassword = "new",
NewPasswordStatus = PasswordStatus.OK,
IsUnlockButtonForRenameUid = true,
Email = "keep@example.com",
RemovalTimer = 42L,
),
ResetPasswordFields,
)
Check(
"切片 AccountSettings:ResetPasswordFields 只清密码三件套/状态/解锁按钮",
Reset.CurrentPassword == "" && Reset.NewPassword == "" &&
Reset.RepeatNewPassword == "" && Reset.NewPasswordStatus == PasswordStatus.NONE &&
!Reset.IsUnlockButtonForRenameUid && Reset.Email == "keep@example.com" &&
Reset.RemovalTimer == 42L
)
val S1 = listOf<Any>(
SetCurrentPassword("cur"),
SetNewPassword("next"),
SetRepeatNewPassword("next"),
SetPasswordStatus(PasswordStatus.SIMPLE),
SetCurrentPasswordMandatory(true),
SetEmail("user@example.com"),
SetEmailConfirmed(true),
SetCountry("RU", "Russia"),
SetUnlockButtonForRenameUid(true),
ClearInputUid(true),
SetAccountRemovalDays(1_700_000_000_000L),
).fold(S0) { S, A -> ReduceAccountSettings(S, A) }
Check(
"切片 AccountSettings:11 个单字段动作按顺序写入",
S1.CurrentPassword == "cur" && S1.NewPassword == "next" &&
S1.RepeatNewPassword == "next" && S1.NewPasswordStatus == PasswordStatus.SIMPLE &&
S1.IsCurrentPasswordMandatory && S1.Email == "user@example.com" &&
S1.EmailConfirmed && S1.CountryCode == "RU" && S1.CountryName == "Russia" &&
S1.IsUnlockButtonForRenameUid && S1.IsClearInputUid &&
S1.RemovalTimer == 1_700_000_000_000L
)
Check(
"切片 AccountSettings:ClearAccountRemovalTimer 只清 removalTimer",
ReduceAccountSettings(S1, ClearAccountRemovalTimer).let {
it.RemovalTimer == null && it.Email == "user@example.com"
}
)
Check(
"切片 AccountSettings:无关动作不改状态",
ReduceAccountSettings(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetCurrentPassword("root-pass"))
Check(
"根:SetCurrentPassword 只动 accountSettings",
Root1.AccountSettings.CurrentPassword == "root-pass" &&
Root1.Language == Root0.Language && Root1.ProBattle == Root0.ProBattle
)
Check(
"根:accountSettings 无关动作保留前一次归约",
ReduceRoot(Root1, AddFrags(1)).AccountSettings == Root1.AccountSettings
)
val W = RootWiring.ByField("accountSettings")!!
Check(
"根:accountSettings 委派到 oet / Lp.mtg 且 13 个动作全部具名",
W.SliceClass == "oet" && W.Reducer == "Lp.mtg" && W.Actions.size == 13 &&
W.Actions.containsAll(
setOf(
"ClearAccountRemovalTimer", "ClearInputUid", "ResetPasswordFields",
"SetAccountRemovalDays", "SetCountry", "SetCurrentPassword",
"SetCurrentPasswordMandatory", "SetEmail", "SetEmailConfirmed",
"SetNewPassword", "SetPasswordStatus", "SetRepeatNewPassword",
"SetUnlockButtonForRenameUid",
)
)
)
}
// ㉖ 切片 reducer:ProBattle(Wtt / uv.wth,3 字段;2 个 instanceof 臂)
run {
val S0 = ProBattleStateSlice()
Check(
"切片 ProBattle:默认 -1 / NONE / 空密码表",
S0.LastFightBattleId == -1L && S0.LastFightBattleTeam == BattleTeam.NONE &&
S0.BattlePasswords.isEmpty()
)
val Fight1 = ReduceProBattle(S0, Fight(10L, BattleTeam.TEAM_A))
Check(
"切片 ProBattle:Fight 写上一场 id + team",
Fight1.LastFightBattleId == 10L && Fight1.LastFightBattleTeam == BattleTeam.TEAM_A
)
val WithPass = ReduceProBattle(Fight1, SetBattlePassword(10L, "secret"))
Check(
"切片 ProBattle:SetPassword 非空时写入密码表",
WithPass.BattlePasswords == mapOf(10L to "secret")
)
Check(
"切片 ProBattle:SetPassword 空串时删除密码表项",
ReduceProBattle(WithPass, SetBattlePassword(10L, "")).BattlePasswords.isEmpty()
)
Check("切片 ProBattle:无关动作不改状态", ReduceProBattle(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, Fight(77L, BattleTeam.TEAM_B))
Check(
"根:Fight 只动 proBattle",
Root1.ProBattle.LastFightBattleId == 77L &&
Root1.ProBattle.LastFightBattleTeam == BattleTeam.TEAM_B &&
Root1.AccountSettings == Root0.AccountSettings
)
val W = RootWiring.ByField("proBattle")!!
Check(
"根:proBattle 委派到 Wtt / uv.wth 且 2 个动作",
W.SliceClass == "Wtt" && W.Reducer == "uv.wth" &&
W.Actions.toSet() == setOf("Fight", "SetPassword")
)
}
// ㉗ 切片 reducer:Language(枚举值 / m6().jtg,1 个 instanceof 臂)
run {
Check("切片 Language:根状态默认 EN", RootState().Language == Language.EN)
Check(
"切片 Language:SetLanguage 写回枚举",
ReduceLanguage(Language.EN, SetLanguage(Language.ZH)) == Language.ZH
)
Check(
"切片 Language:无关动作保留当前语言",
ReduceLanguage(Language.ZH, AddFrags(1)) == Language.ZH
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetLanguage(Language.DE))
Check(
"根:SetLanguage 只动 language",
Root1.Language == Language.DE && Root1.Announcement == Root0.Announcement
)
val W = RootWiring.ByField("language")!!
Check(
"根:language 委派到 m6().jtg 且动作具名",
W.SliceClass == "?" && W.Reducer == "m6().jtg" &&
W.Actions == listOf("SetLanguage")
)
}
// ㉘ 切片 reducer:Announcement(lC / PC().rq5,5 字段;4 个 instanceof 臂)
run {
val S0 = AnnouncementStateSlice()
Check(
"切片 Announcement:默认值完整",
S0.IsEnabled && !S0.ModelLoaded && S0.Items.isEmpty() &&
!S0.WasInBattle && !S0.SkipNextPromotedAnnounce
)
val S1 = ReduceAnnouncement(
ReduceAnnouncement(
ReduceAnnouncement(
ReduceAnnouncement(S0, DisableAnnouncement),
SetAnnouncementModelLoaded,
),
SetAnnouncementItems(listOf("a", "b")),
),
SetSkipNextPromotedAnnounce(true),
)
Check(
"切片 Announcement:4 个臂分别写 enabled/modelLoaded/items/skip",
!S1.IsEnabled && S1.ModelLoaded && S1.Items == listOf<Any?>("a", "b") &&
S1.SkipNextPromotedAnnounce
)
Check(
"切片 Announcement:无关动作不改状态",
ReduceAnnouncement(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, DisableAnnouncement)
Check(
"根:DisableAnnouncement 只动 announcement",
!Root1.Announcement.IsEnabled && Root1.AccountSettings == Root0.AccountSettings
)
val W = RootWiring.ByField("announcement")!!
Check(
"根:announcement 委派到 lC / PC().rq5 且 4 个动作全部具名",
W.SliceClass == "lC" && W.Reducer == "PC().rq5" &&
W.Actions.toSet() == setOf(
"DisableAnnouncement", "SetAnnouncementItems",
"SetAnnouncementModelLoaded", "SetSkipNextPromotedAnnounce",
)
)
}
// ㉙ 切片 reducer:News(ZX / E_.asd,4 字段;7 个分支)
run {
val S0 = NewsStateSlice()
Check(
"切片 News:默认值完整",
!S0.IsOpen && S0.News.isEmpty() && S0.UnreadNewsCounter == 0 && !S0.P
)
val N1 = NewsItemData(10, "d1", 0, "h1", 1L, "")
val N2 = NewsItemData(30, "d2", 0, "h2", 2L, "")
val N3 = NewsItemData(20, "d3", 0, "h3", 3L, "")
Check(
"切片 News:SetNewsItemAction 按 dateInSeconds 降序",
ReduceNews(S0, SetNewsItemAction(listOf(N1, N2, N3))).News.map { it.id } ==
listOf(2L, 3L, 1L)
)
val S1 = ReduceNews(S0, SetNewsItemAction(listOf(N1, N2)))
Check(
"切片 News:AddNewsItemAction 追加后重新排序",
ReduceNews(S1, AddNewsItemAction(N3)).News.map { it.id } == listOf(2L, 3L, 1L)
)
Check(
"切片 News:RemoveNewsItemAction 按 id 删除",
ReduceNews(S1, RemoveNewsItemAction(2L)).News.map { it.id } == listOf(1L)
)
Check(
"切片 News:SetUnreadNewsCounter 写计数",
ReduceNews(S0, SetUnreadNewsCounter(5)).UnreadNewsCounter == 5
)
Check(
"切片 News:OpenNews/CloseNews 开关面板、SetNewsFlag 置 p",
ReduceNews(ReduceNews(ReduceNews(S0, OpenNews), CloseNews), SetNewsFlag) ==
NewsStateSlice(IsOpen = false, P = true)
)
Check("切片 News:无关动作不改状态", ReduceNews(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, OpenNews)
Check(
"根:OpenNews 只动 news",
Root1.News.IsOpen && Root1.Notifications == Root0.Notifications
)
val W = RootWiring.ByField("news")!!
Check(
"根:news 委派到 ZX / E_.asd 且 5 个动作全部具名",
W.SliceClass == "ZX" && W.Reducer == "E_.asd" &&
W.Actions.toSet() == setOf(
"AddNewsItemAction", "RemoveNewsItemAction", "SetNewsFlag",
"SetNewsItemAction", "SetUnreadNewsCounter",
)
)
}
// ㉚ 切片 reducer:Notifications(jJ / B_.ise,2 字段;4 个 instanceof 臂)
run {
val S0 = NotificationsStateSlice()
Check(
"切片 Notifications:默认值完整",
S0.Notifications.isEmpty() && S0.NotificationSound == null
)
val S1 = ReduceNotifications(S0, InitNotificationSound("ding"))
Check(
"切片 Notifications:InitNotificationSound 写音效",
S1.NotificationSound == "ding" && S1.Notifications.isEmpty()
)
val S2 = ReduceNotifications(
ReduceNotifications(S1, AddNotification("n1")),
AddNotification("n2"),
)
Check(
"切片 Notifications:AddNotification 追加到末尾",
S2.Notifications == listOf<Any?>("n1", "n2")
)
Check(
"切片 Notifications:RemoveNotifications 按谓词过滤",
ReduceNotifications(S2, RemoveNotifications { it == "n1" }).Notifications ==
listOf<Any?>("n2")
)
Check(
"切片 Notifications:RemoveFirstNotification 丢掉第一条",
ReduceNotifications(S2, RemoveFirstNotification).Notifications ==
listOf<Any?>("n2")
)
Check("切片 Notifications:无关动作不改状态", ReduceNotifications(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddNotification("x"))
Check(
"根:AddNotification 只动 notifications",
Root1.Notifications.Notifications == listOf<Any?>("x") && Root1.News == Root0.News
)
val W = RootWiring.ByField("notifications")!!
Check(
"根:notifications 委派到 jJ / B_.ise 且 4 个动作全部具名",
W.SliceClass == "jJ" && W.Reducer == "B_.ise" &&
W.Actions.toSet() == setOf(
"AddNotification", "InitNotificationSound",
"RemoveFirstNotification", "RemoveNotifications",
)
)
}
// ㉛ 切片 reducer:Quests(Frt / Yrt.otg,3 字段;GoTo 按目标屏幕切分区)
run {
val S0 = QuestsStateSlice()
Check(
"切片 Quests:默认值完整",
S0.CurrentSection == QuestSection.SPECIAL && S0.CurrentCategory == null &&
!S0.IsNotCompletedQuests
)
Check(
"切片 Quests:SetCurrentCategory 写分类",
ReduceQuests(S0, SetCurrentCategory("cat-1")).CurrentCategory == "cat-1"
)
Check(
"切片 Quests:SetNotCompletedQuests 读了不存在的字段,实际是 no-op",
ReduceQuests(QuestsStateSlice(IsNotCompletedQuests = true), SetNotCompletedQuests)
.IsNotCompletedQuests
)
Check(
"切片 Quests:屏幕名按原包拼接",
QuestCategoryScreen(QuestSection.SPECIAL, 7L).name ==
"Lobby.Quests.QuestCategory.SPECIAL.7" &&
ChallengeScreen(null).name == "Lobby.Quests.Challenge"
)
val S1 = ReduceQuests(S0, GoTo(QuestCategoryScreen(QuestSection.CONTRACT, 42L), false))
Check(
"切片 Quests:GoTo(QuestCategory) 切到目标分区",
S1.CurrentSection == QuestSection.CONTRACT
)
val S2 = ReduceQuests(S1, GoTo(ChallengeScreen(null), false))
Check(
"切片 Quests:GoTo(Challenge) 切到 CHALLENGE",
S2.CurrentSection == QuestSection.CHALLENGE
)
Check(
"切片 Quests:其他屏幕的 GoTo 不改状态",
ReduceQuests(S2, GoTo(NamedScreen("Lobby.Home"), true)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, GoTo(QuestCategoryScreen(QuestSection.CHALLENGE, null), false))
Check(
"根:GoTo 只动 quests 的分区",
Root1.Quests.CurrentSection == QuestSection.CHALLENGE &&
Root1.Notifications == Root0.Notifications
)
val W = RootWiring.ByField("quests")!!
Check(
"根:quests 委派到 Frt / Yrt.otg,屏幕标识类不算动作",
W.SliceClass == "Frt" && W.Reducer == "Yrt.otg" &&
W.Actions == listOf("GoTo", "SetCurrentCategory", "SetNotCompletedQuests")
)
}
// ㉜ 切片 reducer:LobbyLoader(KW / VW.es1,2 字段;FW/GW 两个匿名类)
run {
val S0 = LobbyLoaderStateSlice()
Check("切片 LobbyLoader:默认不显示、计数 0", !S0.IsShowLoader && S0.ActiveLoadersCount == 0)
val S1 = ReduceLobbyLoader(ReduceLobbyLoader(S0, ShowLoader), ShowLoader)
Check(
"切片 LobbyLoader:ShowLoader 计数 +1 并显示",
S1.IsShowLoader && S1.ActiveLoadersCount == 2
)
val S2 = ReduceLobbyLoader(S1, HideLoader)
Check(
"切片 LobbyLoader:HideLoader 计数 -1,仍大于 0 时继续显示",
S2.IsShowLoader && S2.ActiveLoadersCount == 1
)
val S3 = ReduceLobbyLoader(ReduceLobbyLoader(S2, HideLoader), HideLoader)
Check(
"切片 LobbyLoader:计数夹到 0 并隐藏",
!S3.IsShowLoader && S3.ActiveLoadersCount == 0
)
Check("切片 LobbyLoader:无关动作不改状态", ReduceLobbyLoader(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ShowLoader)
Check(
"根:ShowLoader 只动 lobbyLoader",
Root1.LobbyLoader.IsShowLoader && Root1.PurchaseDialogState == Root0.PurchaseDialogState
)
val W = RootWiring.ByField("lobbyLoader")!!
Check(
"根:lobbyLoader 委派到 KW / Bs.es1 且 2 个动作全部具名",
W.SliceClass == "KW" && W.Reducer == "Bs.es1" &&
W.Actions.toSet() == setOf("ShowLoader", "HideLoader")
)
}
// ㉝ 切片 reducer:PurchaseDialogState(CO / DO.ur2,4 字段;NO + 2 个实例臂)
run {
val S0 = PurchaseDialogStateSlice()
Check(
"切片 PurchaseDialog:默认值完整",
!S0.WithEmail && S0.Goods.isEmpty() && S0.EmailStatus == null && S0.Time == null
)
val S1 = ReducePurchaseDialog(S0, ChangeEmailStatus(PurchaseEmailStatus.BUSY))
Check(
"切片 PurchaseDialog:ChangeEmailStatus 只写邮箱状态",
S1.EmailStatus == PurchaseEmailStatus.BUSY && S1.Goods.isEmpty() && S1.Time == null
)
val S2 = ReducePurchaseDialog(S1, SuccessfullyPurchase(listOf("g1"), true, 123L))
Check(
"切片 PurchaseDialog:SuccessfullyPurchase 追加商品并写时间",
S2.WithEmail && S2.Goods == listOf<Any?>("g1") && S2.Time == 123L &&
S2.EmailStatus == PurchaseEmailStatus.BUSY
)
val S3 = ReducePurchaseDialog(S2, SuccessfullyPurchase(listOf("g2"), false, 456L))
Check(
"切片 PurchaseDialog:商品是追加而不是替换",
S3.Goods == listOf<Any?>("g1", "g2") && !S3.WithEmail && S3.Time == 456L
)
Check(
"切片 PurchaseDialog:ResetPurchaseDialog 复位四个字段",
ReducePurchaseDialog(S3, ResetPurchaseDialog) == PurchaseDialogStateSlice()
)
Check("切片 PurchaseDialog:无关动作不改状态", ReducePurchaseDialog(S3, AddFrags(1)) == S3)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ChangeEmailStatus(PurchaseEmailStatus.FREE))
Check(
"根:ChangeEmailStatus 只动 purchaseDialogState",
Root1.PurchaseDialogState.EmailStatus == PurchaseEmailStatus.FREE &&
Root1.LobbyLoader == Root0.LobbyLoader
)
val W = RootWiring.ByField("purchaseDialogState")!!
Check(
"根:purchaseDialogState 委派到 CO / je.ur2 且 3 个动作全部具名",
W.SliceClass == "CO" && W.Reducer == "je.ur2" &&
W.Actions.toSet() == setOf(
"ChangeEmailStatus", "ResetPurchaseDialog", "SuccessfullyPurchase",
)
)
}
// ㉞ 切片 reducer:PromoCode(cZ / wZ.ysg,1 字段;2 个实例臂)
run {
val S0 = PromoCodeStateSlice()
Check("切片 PromoCode:默认 NONE", S0.ValidateStatus == PromoCodeStatus.NONE)
Check(
"切片 PromoCode:ActivatePromoCode 进入 WAIT",
ReducePromoCode(S0, ActivatePromoCode("CODE-1")).ValidateStatus == PromoCodeStatus.WAIT
)
Check(
"切片 PromoCode:SetCodeValidateStatus 写服务端状态",
ReducePromoCode(
PromoCodeStateSlice(PromoCodeStatus.WAIT),
SetCodeValidateStatus(PromoCodeStatus.ACTIVATED),
).ValidateStatus == PromoCodeStatus.ACTIVATED
)
Check("切片 PromoCode:无关动作不改状态", ReducePromoCode(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ActivatePromoCode("X"))
Check(
"根:ActivatePromoCode 只动 promoCode",
Root1.PromoCode.ValidateStatus == PromoCodeStatus.WAIT &&
Root1.SupportUrl == Root0.SupportUrl
)
val W = RootWiring.ByField("promoCode")!!
Check(
"根:promoCode 委派到 cZ / Q_.ysg 且 2 个动作",
W.SliceClass == "cZ" && W.Reducer == "Q_.ysg" &&
W.Actions == listOf("ActivatePromoCode", "SetCodeValidateStatus")
)
}
// ㉟ 切片 reducer:SupportUrl(I4 / P4.ct7,1 字段;A4 + R4)
run {
val S0 = SupportUrlStateSlice()
Check("切片 SupportUrl:默认为空串", S0.Url == "")
Check(
"切片 SupportUrl:SetUrl 写入链接",
ReduceSupportUrl(S0, SetUrl("https://support")).Url == "https://support"
)
Check(
"切片 SupportUrl:ClearSupportUrl 清空",
ReduceSupportUrl(SupportUrlStateSlice("https://support"), ClearSupportUrl).Url == ""
)
Check("切片 SupportUrl:无关动作不改状态", ReduceSupportUrl(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetUrl("u"))
Check(
"根:SetUrl 只动 supportUrl",
Root1.SupportUrl.Url == "u" && Root1.RulesUpdated == Root0.RulesUpdated
)
val W = RootWiring.ByField("supportUrl")!!
Check(
"根:supportUrl 委派到 I4 / Yl.ct7 且 2 个动作",
W.SliceClass == "I4" && W.Reducer == "Yl.ct7" &&
W.Actions.toSet() == setOf("ClearSupportUrl", "SetUrl")
)
}
// ㊱ 切片 reducer:RulesUpdated(i1 / u1.dsm,1 字段;1 个实例臂)
run {
val S0 = RulesUpdatedStateSlice()
Check("切片 RulesUpdated:默认 text = null", S0.Text == null)
Check(
"切片 RulesUpdated:UpdateRules 写入正文",
ReduceRulesUpdated(S0, UpdateRules("新规则")).Text == "新规则"
)
Check("切片 RulesUpdated:无关动作不改状态", ReduceRulesUpdated(S0, AddFrags(1)) == S0)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateRules("t"))
Check(
"根:UpdateRules 只动 rulesUpdated",
Root1.RulesUpdated.Text == "t" && Root1.NotificationByCountry == Root0.NotificationByCountry
)
val W = RootWiring.ByField("rulesUpdated")!!
Check(
"根:rulesUpdated 委派到 i1 / Ch.dsm",
W.SliceClass == "i1" && W.Reducer == "Ch.dsm" && W.Actions == listOf("UpdateRules")
)
}
// ㊲ 切片 reducer:NotificationByCountry(gJ / $J.ssd,4 字段;Init + qJ)
run {
val S0 = NotificationByCountryStateSlice()
Check(
"切片 NotificationByCountry:默认三条消息为 null、弹窗未展示",
S0.DisableRegistrationMessage == null && S0.DisableShopMessage == null &&
S0.HomeScreenMessage == null && !S0.HomeScreenDialogShown
)
val S1 = ReduceNotificationByCountry(
S0,
InitNotificationByCountry("r", "s", "h"),
)
Check(
"切片 NotificationByCountry:Init 写三条消息且不动弹窗标志",
S1.DisableRegistrationMessage == "r" && S1.DisableShopMessage == "s" &&
S1.HomeScreenMessage == "h" && !S1.HomeScreenDialogShown
)
Check(
"切片 NotificationByCountry:qJ 只把弹窗标志置 true",
ReduceNotificationByCountry(S1, MarkHomeScreenDialogShown) ==
S1.copy(HomeScreenDialogShown = true)
)
Check(
"切片 NotificationByCountry:无关动作不改状态",
ReduceNotificationByCountry(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, MarkHomeScreenDialogShown)
Check(
"根:qJ 只动 notificationByCountry",
Root1.NotificationByCountry.HomeScreenDialogShown &&
Root1.RulesUpdated == Root0.RulesUpdated
)
val W = RootWiring.ByField("notificationByCountry")!!
Check(
"根:notificationByCountry 委派到 gJ / R_.ssd 且 2 个动作",
W.SliceClass == "gJ" && W.Reducer == "R_.ssd" &&
W.Actions.toSet() == setOf("Init", "MarkHomeScreenDialogShown")
)
}
// ㊳ 切片 reducer:AvailabilityByRank(tet / set.xth,1 字段;1 个实例臂)
run {
val S0 = AvailabilityByRankStateSlice()
Check("切片 AvailabilityByRank:默认空表", S0.Data.isEmpty())
val S1 = ReduceAvailabilityByRank(S0, SetLoadedData(mapOf(1 to "a", 2 to "b")))
Check(
"切片 AvailabilityByRank:SetLoadedData 写整张表",
S1.Data == mapOf<Any?, Any?>(1 to "a", 2 to "b")
)
Check("切片 AvailabilityByRank:无关动作不改状态", ReduceAvailabilityByRank(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetLoadedData(mapOf(3 to "c")))
Check(
"根:SetLoadedData 只动 availabilityByRank",
Root1.AvailabilityByRank.Data == mapOf<Any?, Any?>(3 to "c") &&
Root1.PromoCode == Root0.PromoCode
)
val W = RootWiring.ByField("availabilityByRank")!!
Check(
"根:availabilityByRank 委派到 tet / \$p.xth",
W.SliceClass == "tet" && W.Reducer == "\$p.xth" &&
W.Actions == listOf("SetLoadedData")
)
}
// ㊴ 切片 reducer:Icons(oH / vs.kru,1 字段;_H → SetIcons)
run {
val S0 = IconsStateSlice()
Check("切片 Icons:默认空表", S0.Icons.isEmpty())
val S1 = ReduceIcons(
S0,
SetIcons(listOf(IconData("rank", "r1"), IconData("gold", "r2"))),
)
Check(
"切片 Icons:SetIcons 建成 icon→resource 表",
S1.Icons == mapOf<String, Any?>("rank" to "r1", "gold" to "r2")
)
Check("切片 Icons:无关动作不改状态", ReduceIcons(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetIcons(listOf(IconData("a", null))))
Check(
"根:SetIcons 只动 icons",
Root1.Icons.Icons.keys == setOf("a") &&
Root1.QuestCategories == Root0.QuestCategories
)
val W = RootWiring.ByField("icons")!!
Check(
"根:icons 委派到 oH / vs.kru 且 1 个动作",
W.SliceClass == "oH" && W.Reducer == "vs.kru" && W.Actions == listOf("SetIcons")
)
}
// ㊵ 切片 reducer:QuestCategories(zZ / th.lsh,3 字段;4 个实例臂)
run {
fun Cat(Id: Long, Index: Int): QuestCategoryData = QuestCategoryData(
color = "", description = "", endDate = null, hideIfEmpty = false, id = Id,
image = null, index = Index, name = "c$Id", rankRange = "", min = "",
startDate = null, type = "",
)
val A = Cat(1L, 2)
val B = Cat(2L, 1)
val S0 = QuestCategoriesStateSlice()
Check(
"切片 QuestCategories:默认空表、加载时间 0",
S0.Categories.isEmpty() && S0.Statistic.isEmpty() && S0.StatisticLoadedTime == 0L
)
val S1 = ReduceQuestCategories(S0, SetQuestCategories(listOf(A, B)))
Check(
"切片 QuestCategories:SetQuestCategories 按 index 升序",
S1.Categories == listOf(B, A)
)
Check(
"切片 QuestCategories:RemoveQuestCategory 按对象删除",
ReduceQuestCategories(S1, RemoveQuestCategory(B)).Categories == listOf(A)
)
Check(
"切片 QuestCategories:隐藏刷新不改加载时间",
ReduceQuestCategories(S1, RequestQuestCategoriesStatistic(true)) == S1
)
Check(
"切片 QuestCategories:强制刷新把加载时间推到 Long.MAX_VALUE",
ReduceQuestCategories(S1, RequestQuestCategoriesStatistic(false))
.StatisticLoadedTime == Long.MAX_VALUE
)
val S2 = ReduceQuestCategories(
S1,
SetQuestCategoriesStatistic(
listOf(
QuestCategoryStatistic(nearEndTime = null, questCategoryId = 1L, quests = 3),
QuestCategoryStatistic(nearEndTime = 5L, questCategoryId = 2L, quests = 4),
)
),
)
Check(
"切片 QuestCategories:统计按 questCategoryId 建索引并写当前时间",
S2.Statistic.keys == setOf(1L, 2L) && S2.StatisticLoadedTime > 0L
)
Check("切片 QuestCategories:无关动作不改状态", ReduceQuestCategories(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetQuestCategories(listOf(A)))
Check(
"根:SetQuestCategories 只动 questCategories",
Root1.QuestCategories.Categories == listOf(A) && Root1.MainQuest == Root0.MainQuest
)
val W = RootWiring.ByField("questCategories")!!
Check(
"根:questCategories 委派到 zZ / th.lsh 且 4 个动作",
W.SliceClass == "zZ" && W.Reducer == "th.lsh" &&
W.Actions.toSet() == setOf(
"SetQuestCategories", "RemoveQuestCategory",
"RequestQuestCategoriesStatistic", "SetQuestCategoriesStatistic",
)
)
}
// ㊶ 切片 reducer:MainQuest(WZ / ih.bsk,5 字段;7 个动作,含 VZ→ResetQuestsLoaded)
run {
fun Quest(
Id: Long,
Name: String = "q$Id",
Completed: Boolean = false,
Count: Int = 0,
): MainQuestInfo = MainQuestInfo(
action = "", actionParams = null, activateTime = null, active = true,
battlePass = false, categoryId = 1L, completed = Completed, completedCount = Count,
conditions = null, battleMode = null, contextItemsInfo = null, id = Id, name = Name,
quantity = 1, skipForShowProgress = false, type = "", depends = emptyList(),
description = "", durationInMinutes = 0, eventPass = false,
extendedPeriodicalEndTime = null, extendedPeriodicalStartTime = null, icon = null,
minRank = 0, oneTime = false, pinned = false, premium = false, progress = null,
rewardExpireTime = null, rewards = "", count = 1, preview = null,
skipNewNotification = false, sortedIndex = 0, startTime = 0L, COMPLETE_QUEST = "",
)
val Q1 = Quest(1L)
val Q2 = Quest(2L, "q2", Completed = true, Count = 2)
val S0 = MainQuestStateSlice()
Check(
"切片 MainQuest:默认未加载、三个列表为空、活动商品 id 为 null",
!S0.IsLoaded && S0.Quests.isEmpty() && S0.RewardedQuests.isEmpty() &&
S0.EventPassShopItemId == null && S0.LastBattleUpdatedQuests.isEmpty()
)
val L1 = ReduceMainQuest(S0, QuestsLoaded(listOf(Q1)))
Check("切片 MainQuest:QuestsLoaded 只置 isLoaded", L1.IsLoaded && L1.Quests.isEmpty())
Check(
"切片 MainQuest:ResetQuestsLoaded 复位 isLoaded",
!ReduceMainQuest(L1, ResetQuestsLoaded).IsLoaded
)
val S1 = ReduceMainQuest(S0, SetQuests(listOf(Q1, Q2)))
Check(
"切片 MainQuest:SetQuests 按完成次数展开并清空已领奖",
S1.Quests.size == 3 &&
S1.Quests[0].completed && S1.Quests[0].completedCount == 1 &&
S1.Quests[1].completed && S1.Quests[1].completedCount == 2 &&
S1.Quests[2].id == 1L && !S1.Quests[2].completed &&
S1.RewardedQuests.isEmpty()
)
val S2 = ReduceMainQuest(S1, UpdateQuests(listOf(Quest(1L, "q1-new"), Quest(3L))))
Check(
"切片 MainQuest:UpdateQuests 按 id 就地替换、缺失追加",
S2.Quests.size == 4 && S2.Quests[2].name == "q1-new" && S2.Quests[3].id == 3L
)
val S3 = ReduceMainQuest(S2, AddRewardedQuest(Q1))
Check(
"切片 MainQuest:AddRewardedQuest 追加到已领奖列表",
S3.RewardedQuests == listOf(Q1)
)
Check(
"切片 MainQuest:SetEventPassShopItemId 写商品 id",
ReduceMainQuest(S3, SetEventPassShopItemId(77L)).EventPassShopItemId == 77L
)
Check(
"切片 MainQuest:SetBattleUpdatedQuests 写战斗增量",
ReduceMainQuest(S3, SetBattleUpdatedQuests(listOf("x")))
.LastBattleUpdatedQuests == listOf<Any?>("x")
)
Check("切片 MainQuest:无关动作不改状态", ReduceMainQuest(S3, AddFrags(1)) == S3)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetEventPassShopItemId(9L))
Check(
"根:SetEventPassShopItemId 只动 mainQuest",
Root1.MainQuest.EventPassShopItemId == 9L && Root1.Icons == Root0.Icons
)
val W = RootWiring.ByField("mainQuest")!!
Check(
"根:mainQuest 委派到 WZ / ih.bsk 且 7 个动作(含 ResetQuestsLoaded)",
W.SliceClass == "WZ" && W.Reducer == "ih.bsk" &&
W.Actions.toSet() == setOf(
"ResetQuestsLoaded", "QuestsLoaded", "SetQuests", "UpdateQuests",
"AddRewardedQuest", "SetEventPassShopItemId", "SetBattleUpdatedQuests",
)
)
}
// ㊷ 切片 reducer:SecuritySettings(V2 / pc.qsv,1 字段;K2 → SetProfileBlockStatus)
run {
val S0 = SecuritySettingsStateSlice()
Check("切片 SecuritySettings:默认隐藏个人资料", S0.IsProfileBlocked)
Check(
"切片 SecuritySettings:SetProfileBlockStatus 写布尔",
!ReduceSecuritySettings(S0, SetProfileBlockStatus(false)).IsProfileBlocked
)
Check(
"切片 SecuritySettings:无关动作不改状态",
ReduceSecuritySettings(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetProfileBlockStatus(false))
Check(
"根:SetProfileBlockStatus 只动 securitySettings",
!Root1.SecuritySettings.IsProfileBlocked && Root1.ReferralEvent == Root0.ReferralEvent
)
val W = RootWiring.ByField("securitySettings")!!
Check(
"根:securitySettings 委派到 V2 / pc.qsv",
W.SliceClass == "V2" && W.Reducer == "pc.qsv" &&
W.Actions == listOf("SetProfileBlockStatus")
)
}
// ㊸ 切片 reducer:ReferralEvent(pQ / lh.osk,14 字段;4 个实例臂)
run {
val S0 = ReferralEventStateSlice()
Check(
"切片 ReferralEvent:默认值完整",
S0.Referrer == null && S0.ReferrerUid == null && S0.ReferralsCount == 0 &&
S0.ReferralsData.isEmpty() && S0.Code == null && S0.ReferrerPanelTitle == "" &&
S0.ReferralQuestsSize == 0 && S0.HideReferrerCategory &&
S0.HideReferralCategory && S0.HidedAnnounce == null
)
val S1 = ReduceReferralEvent(S0, SetReferrer("rid", "ruid"))
Check(
"切片 ReferralEvent:SetReferrer 写前两字段",
S1.Referrer == "rid" && S1.ReferrerUid == "ruid" && S1.ReferralsCount == 0
)
val S2 = ReduceReferralEvent(S1, SetReferrals(5, mapOf<Any?, Any?>("a" to 1)))
Check(
"切片 ReferralEvent:SetReferrals 写人数与明细",
S2.ReferralsCount == 5 && S2.ReferralsData == mapOf<Any?, Any?>("a" to 1)
)
Check(
"切片 ReferralEvent:SetPromoCode 写邀请码",
ReduceReferralEvent(S2, SetPromoCode("CODE")).Code == "CODE"
)
val Data = ReferralEventUserModelCC(
hideReferralCategory = false, hideReferrerCategory = false, hidedAnnounce = 3L,
promoCodeDialogBackGround = "bg", promoCodeDialogImage = "img",
referralCategory = 7L, referralQuestsSize = 4, referrerCategory = 8L,
referrerPanelTitle = "title",
)
val S3 = ReduceReferralEvent(S2, SetReferralEventData(Data))
Check(
"切片 ReferralEvent:SetReferralEventData 写后 9 个字段",
S3.ReferrerPanelTitle == "title" && S3.PromoCodeDialogImage == "img" &&
S3.PromoCodeDialogBackGround == "bg" && S3.ReferrerCategoryId == 8L &&
S3.ReferralCategoryId == 7L && S3.ReferralQuestsSize == 4 &&
!S3.HideReferrerCategory && !S3.HideReferralCategory &&
S3.HidedAnnounce == 3L && S3.Referrer == "rid" && S3.ReferralsCount == 5
)
Check(
"切片 ReferralEvent:无关动作不改状态",
ReduceReferralEvent(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetPromoCode("X"))
Check(
"根:SetPromoCode 只动 referralEvent",
Root1.ReferralEvent.Code == "X" && Root1.SecuritySettings == Root0.SecuritySettings
)
val W = RootWiring.ByField("referralEvent")!!
Check(
"根:referralEvent 委派到 pQ / lh.osk 且 4 个动作",
W.SliceClass == "pQ" && W.Reducer == "lh.osk" &&
W.Actions.toSet() == setOf(
"SetReferrer", "SetReferrals", "SetPromoCode", "SetReferralEventData",
)
)
}
// ㊹ 切片 reducer:TipsState(dst / Hw.vth,1 字段;mst → UpdateTips)
run {
val S0 = TipsStateSlice()
Check("切片 TipsState:默认空列表", S0.Tips.isEmpty())
val S1 = ReduceTips(S0, UpdateTips(listOf("a", "b")))
Check("切片 TipsState:UpdateTips 整体替换", S1.Tips == listOf<Any?>("a", "b"))
Check("切片 TipsState:无关动作不改状态", ReduceTips(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateTips(listOf("x")))
Check(
"根:UpdateTips 只动 tipsState",
Root1.TipsState.Tips == listOf<Any?>("x") && Root1.BattleResult == Root0.BattleResult
)
val W = RootWiring.ByField("tipsState")!!
Check(
"根:tipsState 委派到 dst / Hw.vth",
W.SliceClass == "dst" && W.Reducer == "Hw.vth" && W.Actions == listOf("UpdateTips")
)
}
// ㊺ 切片 reducer:RankUpDialog(LO / Se.br3,1 字段;UO + FO)
run {
val S0 = RankUpDialogStateSlice()
Check("切片 RankUpDialog:默认空表", S0.AvailableItems.isEmpty())
val S1 = ReduceRankUpDialog(S0, ShowAvailableItems(listOf("a")))
Check(
"切片 RankUpDialog:ShowAvailableItems 追加整表",
S1.AvailableItems == listOf<Any?>("a")
)
val S2 = ReduceRankUpDialog(S1, ShowAvailableItems(listOf("b", "c")))
Check(
"切片 RankUpDialog:再次追加是拼接",
S2.AvailableItems == listOf<Any?>("a", "b", "c")
)
Check(
"切片 RankUpDialog:ResetRankUpDialog 清空列表",
ReduceRankUpDialog(S2, ResetRankUpDialog).AvailableItems.isEmpty()
)
Check(
"切片 RankUpDialog:无关动作不改状态",
ReduceRankUpDialog(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ShowAvailableItems(listOf("x")))
Check(
"根:ShowAvailableItems 只动 rankUpDialogState",
Root1.RankUpDialogState.AvailableItems == listOf<Any?>("x") &&
Root1.TipsState == Root0.TipsState
)
val W = RootWiring.ByField("rankUpDialogState")!!
Check(
"根:rankUpDialogState 委派到 LO / Se.br3 且 2 个动作",
W.SliceClass == "LO" && W.Reducer == "Se.br3" &&
W.Actions.toSet() == setOf("ShowAvailableItems", "ResetRankUpDialog")
)
}
// ㊻ 切片 reducer:BattleResult(sR / Fi.aqp,5 字段;6 个动作)
run {
val Item = ContainerGivenItem(
category = "cat", count = 1, garageItemCategory = "hull", garageItemId = 2,
image = null, name = "n", rewardType = "t",
)
val S0 = BattleResultStateSlice()
Check(
"切片 BattleResult:默认值完整",
!S0.HasBattleResult && S0.Rewards.isEmpty() &&
S0.InitialCurrentUserXP == null && S0.IncreasedReward == 0 &&
S0.SimpleReward == 0
)
val S1 = ReduceBattleResult(S0, SetRewards(listOf(Item)))
Check("切片 BattleResult:SetRewards 写奖励表", S1.Rewards == listOf(Item))
val S2 = ReduceBattleResult(S1, SetHasBattleResult(true))
Check("切片 BattleResult:SetHasBattleResult 写布尔", S2.HasBattleResult)
val S3 = ReduceBattleResult(
ReduceBattleResult(S2, SetIncreasedReward(15)),
SetSimpleReward(10),
)
Check(
"切片 BattleResult:cR / lR 分别写两个奖励数值",
S3.IncreasedReward == 15 && S3.SimpleReward == 10
)
Check(
"切片 BattleResult:ResetBattleRewards 只把两个数值归零",
ReduceBattleResult(S3, ResetBattleRewards) ==
S3.copy(IncreasedReward = 0, SimpleReward = 0)
)
val S4 = ReduceBattleResult(S3, DoInit(500))
Check(
"切片 BattleResult:DoInit 重建状态只带初始 XP",
!S4.HasBattleResult && S4.Rewards.isEmpty() &&
S4.InitialCurrentUserXP == 500 && S4.IncreasedReward == 0 &&
S4.SimpleReward == 0
)
Check(
"切片 BattleResult:无关动作不改状态",
ReduceBattleResult(S4, AddFrags(1)) == S4
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetHasBattleResult(true))
Check(
"根:SetHasBattleResult 只动 battleResult",
Root1.BattleResult.HasBattleResult && Root1.CaptchaState == Root0.CaptchaState
)
val W = RootWiring.ByField("battleResult")!!
Check(
"根:battleResult 委派到 sR / Fi.aqp 且 6 个动作",
W.SliceClass == "sR" && W.Reducer == "Fi.aqp" &&
W.Actions.toSet() == setOf(
"SetRewards", "SetHasBattleResult", "SetIncreasedReward",
"SetSimpleReward", "ResetBattleRewards", "DoInit",
)
)
}
// ㊼ 切片 reducer:Captcha(jP / ar.iqt,4 字段;7 个动作)
run {
val S0 = CaptchaStateSlice()
Check(
"切片 Captcha:默认不造假、三个集合为空",
!S0.UseFake && S0.EnabledLocations.isEmpty() &&
S0.VerifiedLocations.isEmpty() && S0.LocationsToReset.isEmpty()
)
val S1 = ReduceCaptcha(S0, ShowFakeCaptcha)
Check("切片 Captcha:ShowFakeCaptcha 置 useFake", S1.UseFake)
val S2 = ReduceCaptcha(S1, SetLocations(listOf("a", "b")))
Check(
"切片 Captcha:SetLocations 收成集合",
S2.EnabledLocations == setOf<Any?>("a", "b")
)
val S3 = ReduceCaptcha(
ReduceCaptcha(S2, AddVerifiedCaptchaLocation("a")),
AddVerifiedCaptchaLocation("b"),
)
Check(
"切片 Captcha:已验证集合加元素",
S3.VerifiedLocations == setOf<Any?>("a", "b")
)
Check(
"切片 Captcha:已验证集合删元素",
ReduceCaptcha(S3, RemoveVerifiedCaptchaLocation("a"))
.VerifiedLocations == setOf<Any?>("b")
)
val S4 = ReduceCaptcha(
ReduceCaptcha(S3, AddCaptchaLocationToReset("c")),
RemoveCaptchaLocationToReset("c"),
)
Check("切片 Captcha:待重置集合加减后为空", S4.LocationsToReset.isEmpty())
Check(
"切片 Captcha:LocationsUpdated 追加到启用集合",
ReduceCaptcha(S4, LocationsUpdated("d"))
.EnabledLocations == setOf<Any?>("a", "b", "d")
)
Check("切片 Captcha:无关动作不改状态", ReduceCaptcha(S4, AddFrags(1)) == S4)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ShowFakeCaptcha)
Check(
"根:ShowFakeCaptcha 只动 captchaState",
Root1.CaptchaState.UseFake && Root1.ModalScreen == Root0.ModalScreen
)
val W = RootWiring.ByField("captchaState")!!
Check(
"根:captchaState 委派到 jP / ar.iqt 且 7 个动作",
W.SliceClass == "jP" && W.Reducer == "ar.iqt" &&
W.Actions.toSet() == setOf(
"ShowFakeCaptcha", "SetLocations", "AddVerifiedCaptchaLocation",
"RemoveVerifiedCaptchaLocation", "AddCaptchaLocationToReset",
"RemoveCaptchaLocationToReset", "LocationsUpdated",
)
)
}
// ㊽ 切片 reducer:ModalScreen(nV / Fs.js1,2 字段;4 个动作)
run {
val S0 = ModalScreenStateSlice()
Check(
"切片 ModalScreen:默认无当前窗口、队列为空",
S0.Current == null && S0.ModalQueue.isEmpty()
)
val S1 = ReduceModalScreen(S0, AddModal(true))
Check(
"切片 ModalScreen:AddModal 写 current 并入队",
S1.Current == true && S1.ModalQueue == listOf<Any?>(true)
)
Check(
"切片 ModalScreen:同名窗口不重复入队",
ReduceModalScreen(S1, AddModal(true)) == S1
)
val S2 = ReduceModalScreen(S1, AddModal(false))
Check(
"切片 ModalScreen:第二个窗口排到队尾",
S2.Current == false && S2.ModalQueue == listOf<Any?>(true, false)
)
val S3 = ReduceModalScreen(S2, RemoveModal(true))
Check(
"切片 ModalScreen:RemoveModal 出队并把 current 交给队首",
S3.Current == false && S3.ModalQueue == listOf<Any?>(false)
)
Check(
"切片 ModalScreen:CloseCurrentModal 清掉当前窗口",
ReduceModalScreen(S3, CloseCurrentModal) == ModalScreenStateSlice()
)
Check(
"切片 ModalScreen:RemoveCurrentByName 名称不符时不动",
ReduceModalScreen(S3, RemoveCurrentByName("other")) == S3
)
Check(
"切片 ModalScreen:RemoveCurrentByName 名称匹配时关闭",
ReduceModalScreen(S3, RemoveCurrentByName("false")) == ModalScreenStateSlice()
)
Check(
"切片 ModalScreen:无关动作不改状态",
ReduceModalScreen(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddModal(true))
Check(
"根:AddModal 只动 modalScreen",
Root1.ModalScreen.Current == true &&
Root1.EventGoods == Root0.EventGoods
)
val W = RootWiring.ByField("modalScreen")!!
Check(
"根:modalScreen 委派到 nV / Fs.js1 且 4 个动作",
W.SliceClass == "nV" && W.Reducer == "Fs.js1" &&
W.Actions.toSet() == setOf(
"AddModal", "RemoveModal", "CloseCurrentModal", "RemoveCurrentByName",
)
)
}
// ㊾ 切片 reducer:DroppableGold(Vet / Wp.kth,1 字段;Ket/Yet 两个单例)
run {
val S0 = DroppableGoldStateSlice()
Check("切片 DroppableGold:默认显示作者署名", S0.ShowDroppableGoldAuthor)
Check(
"切片 DroppableGold:Hide 置 false、Show 置 true",
!ReduceDroppableGold(S0, HideDroppableGoldAuthor).ShowDroppableGoldAuthor &&
ReduceDroppableGold(S0, ShowDroppableGoldAuthor).ShowDroppableGoldAuthor
)
Check(
"切片 DroppableGold:无关动作不改状态",
ReduceDroppableGold(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, HideDroppableGoldAuthor)
Check(
"根:HideDroppableGoldAuthor 只动 droppableGold",
!Root1.DroppableGold.ShowDroppableGoldAuthor &&
Root1.CreatorTagState == Root0.CreatorTagState
)
val W = RootWiring.ByField("droppableGold")!!
Check(
"根:droppableGold 委派到 Vet / Wp.kth 且 2 个动作",
W.SliceClass == "Vet" && W.Reducer == "Wp.kth" &&
W.Actions.toSet() == setOf("ShowDroppableGoldAuthor", "HideDroppableGoldAuthor")
)
}
// ㊿ 切片 reducer:EventGoods(W7 / Wc.esx,1 字段;V7 → InitEventGoods)
run {
val S0 = EventGoodsStateSlice()
Check("切片 EventGoods:默认空表", S0.EventGoods.isEmpty())
val S1 = ReduceEventGoods(S0, InitEventGoods("spectatorMode", true))
Check(
"切片 EventGoods:InitEventGoods 写键值",
S1.EventGoods == mapOf<Any?, Any?>("spectatorMode" to true)
)
val S2 = ReduceEventGoods(S1, InitEventGoods("spectatorMode", null))
Check("切片 EventGoods:goods 为空时删键", S2.EventGoods.isEmpty())
Check(
"切片 EventGoods:无关动作不改状态",
ReduceEventGoods(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, InitEventGoods("k", 1))
Check(
"根:InitEventGoods 只动 eventGoods",
Root1.EventGoods.EventGoods == mapOf<Any?, Any?>("k" to 1) &&
Root1.RewardedVideosState == Root0.RewardedVideosState
)
val W = RootWiring.ByField("eventGoods")!!
Check(
"根:eventGoods 委派到 W7 / Wc.esx",
W.SliceClass == "W7" && W.Reducer == "Wc.esx" &&
W.Actions == listOf("InitEventGoods")
)
}
// 51 切片 reducer:CreatorTag(qO / ze.er2,2 字段;4 个动作)
run {
val S0 = CreatorTagStateSlice()
Check("切片 CreatorTag:默认两个字段都为 null", S0.CreatorTag == null && S0.CreatorTags == null)
val S1 = ReduceCreatorTag(S0, SetCreatorTags(listOf("a")))
Check("切片 CreatorTag:SetCreatorTags 写列表", S1.CreatorTags == listOf<Any?>("a"))
Check(
"切片 CreatorTag:SetCreatorTag 写当前标签",
ReduceCreatorTag(S1, SetCreatorTag("t1")).CreatorTag == "t1"
)
val S2 = ReduceCreatorTag(S1, SetAttachedCreatorTag("t2"))
Check(
"切片 CreatorTag:SetAttachedCreatorTag 覆盖当前标签",
S2.CreatorTag == "t2" && S2.CreatorTags == listOf<Any?>("a")
)
val S3 = ReduceCreatorTag(S2, ClearCreatorTag)
Check(
"切片 CreatorTag:ClearCreatorTag 只清当前标签",
S3.CreatorTag == null && S3.CreatorTags == listOf<Any?>("a")
)
Check("切片 CreatorTag:无关动作不改状态", ReduceCreatorTag(S3, AddFrags(1)) == S3)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetCreatorTag("x"))
Check(
"根:SetCreatorTag 只动 creatorTagState",
Root1.CreatorTagState.CreatorTag == "x" && Root1.EventGoods == Root0.EventGoods
)
val W = RootWiring.ByField("creatorTagState")!!
Check(
"根:creatorTagState 委派到 qO / ze.er2 且 4 个动作",
W.SliceClass == "qO" && W.Reducer == "ze.er2" &&
W.Actions.toSet() == setOf(
"SetCreatorTags", "SetCreatorTag", "SetAttachedCreatorTag", "ClearCreatorTag",
)
)
}
// 52 切片 reducer:RewardedVideos(JQ / xh.zsl,1 字段;t1 → SetDisplayState)
run {
val S0 = RewardedVideosStateSlice()
Check("切片 RewardedVideos:默认 NONE", S0.DisplayState == RewardVideoDisplayState.NONE)
Check(
"切片 RewardedVideos:SetDisplayState 写枚举",
ReduceRewardedVideos(S0, SetDisplayState(RewardVideoDisplayState.DISPLAY))
.DisplayState == RewardVideoDisplayState.DISPLAY
)
Check(
"切片 RewardedVideos:无关动作不改状态",
ReduceRewardedVideos(S0, AddFrags(1)) == S0
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetDisplayState(RewardVideoDisplayState.LOADING))
Check(
"根:SetDisplayState 只动 rewardedVideosState",
Root1.RewardedVideosState.DisplayState == RewardVideoDisplayState.LOADING &&
Root1.PasswordChangeState == Root0.PasswordChangeState
)
val W = RootWiring.ByField("rewardedVideosState")!!
Check(
"根:rewardedVideosState 委派到 JQ / xh.zsl",
W.SliceClass == "JQ" && W.Reducer == "xh.zsl" &&
W.Actions == listOf("SetDisplayState")
)
}
// 53 切片 reducer:PasswordChange(VD / uu.pr6,3 字段;UD + HD)
run {
val S0 = PasswordChangeStateSlice()
Check(
"切片 PasswordChange:默认三个空串",
S0.Hash == "" && S0.Uid == "" && S0.Email == ""
)
val S1 = ReducePasswordChange(S0, SetPasswordChangeHash("h"))
Check(
"切片 PasswordChange:SetPasswordChangeHash 只写 hash",
S1.Hash == "h" && S1.Uid == "" && S1.Email == ""
)
val S2 = ReducePasswordChange(S1, ShowEmailAndPasswordChangeDialog("e", "u", "r"))
Check(
"切片 PasswordChange:ShowEmailAndPasswordChangeDialog 写 uid/email 但保留 hash",
S2.Hash == "h" && S2.Uid == "u" && S2.Email == "e"
)
Check(
"切片 PasswordChange:无关动作不改状态",
ReducePasswordChange(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetPasswordChangeHash("x"))
Check(
"根:SetPasswordChangeHash 只动 passwordChangeState",
Root1.PasswordChangeState.Hash == "x" && Root1.TipsState == Root0.TipsState
)
val W = RootWiring.ByField("passwordChangeState")!!
Check(
"根:passwordChangeState 委派到 VD / uu.pr6 且 2 个动作",
W.SliceClass == "VD" && W.Reducer == "uu.pr6" &&
W.Actions.toSet() == setOf(
"SetPasswordChangeHash", "ShowEmailAndPasswordChangeDialog",
)
)
}
// 54 切片 reducer:System(_9 / w9().xt9,3 字段;2 个动作)
run {
val S0 = SystemStateSlice()
Check(
"切片 System:默认值完整",
S0.Message == null && !S0.Restart && S0.FatalError == null
)
val S1 = ReduceSystem(S0, DoShowMessage("m", true))
Check(
"切片 System:DoShowMessage 写 message/restart",
S1.Message == "m" && S1.Restart && S1.FatalError == null
)
val S2 = ReduceSystem(S1, SetFatalError("boom"))
Check(
"切片 System:SetFatalError 写 fatalError",
S2.FatalError == "boom" && S2.Message == "m"
)
Check("切片 System:无关动作不改状态", ReduceSystem(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetFatalError("x"))
Check(
"根:SetFatalError 只动 system",
Root1.System.FatalError == "x" && Root1.ContextTutorial == Root0.ContextTutorial
)
val W = RootWiring.ByField("system")!!
Check(
"根:system 委派到 _9 / w9().xt9",
W.SliceClass == "_9" && W.Reducer == "w9().xt9" &&
W.Actions.toSet() == setOf("DoShowMessage", "SetFatalError")
)
}
// 55 切片 reducer:ContextTutorial(Mst / Jst().tth,4 字段;5 个动作)
run {
val S0 = ContextTutorialStateSlice()
Check(
"切片 ContextTutorial:默认空表空 Map",
S0.CurrentElement == null && S0.LoadedElements.isEmpty() &&
S0.UnViewedElements.isEmpty() && S0.MissionToElement.isEmpty()
)
val S1 = ReduceContextTutorial(S0, LoadCurrentElement("e1"))
Check("切片 ContextTutorial:LoadCurrentElement 写当前元素", S1.CurrentElement == "e1")
val S2 = ReduceContextTutorial(
ReduceContextTutorial(S1, SetUnViewedElements(listOf("a", "b"))),
MarkElementAsViewed("a"),
)
Check(
"切片 ContextTutorial:MarkElementAsViewed 从待看列表删一个",
S2.UnViewedElements == listOf<Any?>("b")
)
val S3 = ReduceContextTutorial(
ReduceContextTutorial(S2, SetLoadedElements(mapOf<Any?, Any?>("k" to 1))),
SetMissionToElement(mapOf("m" to "e")),
)
Check(
"切片 ContextTutorial:loadedElements / missionToElement 整体替换",
S3.LoadedElements == mapOf<Any?, Any?>("k" to 1) &&
S3.MissionToElement == mapOf("m" to "e")
)
Check(
"切片 ContextTutorial:无关动作不改状态",
ReduceContextTutorial(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, LoadCurrentElement("z"))
Check(
"根:LoadCurrentElement 只动 contextTutorial",
Root1.ContextTutorial.CurrentElement == "z" && Root1.System == Root0.System
)
val W = RootWiring.ByField("contextTutorial")!!
Check(
"根:contextTutorial 委派到 Mst / Jst().tth 且 5 个动作",
W.SliceClass == "Mst" && W.Reducer == "Jst().tth" &&
W.Actions.toSet() == setOf(
"LoadCurrentElement", "SetUnViewedElements", "MarkElementAsViewed",
"SetLoadedElements", "SetMissionToElement",
)
)
}
// 56 切片 reducer:BattleTutorial(rE / Q.sq8,3 字段;4 个动作)
run {
val S0 = BattleTutorialStateSlice()
Check(
"切片 BattleTutorial:默认值完整",
S0.Message == null && S0.FragsCount == 0 && !S0.MovementHasStarted
)
val S1 = ReduceBattleTutorial(S0, ShowMessage("m"))
Check("切片 BattleTutorial:ShowMessage 写 message", S1.Message == "m")
Check(
"切片 BattleTutorial:SetFragsCount 写击杀数",
ReduceBattleTutorial(S1, SetFragsCount(3)).FragsCount == 3
)
val S2 = ReduceBattleTutorial(S1, MarkMovementStarted)
Check("切片 BattleTutorial:MarkMovementStarted 置位", S2.MovementHasStarted)
Check(
"切片 BattleTutorial:ResetBattleTutorial 整片复位",
ReduceBattleTutorial(S2, ResetBattleTutorial) == BattleTutorialStateSlice()
)
Check(
"切片 BattleTutorial:无关动作不改状态",
ReduceBattleTutorial(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, MarkMovementStarted)
Check(
"根:MarkMovementStarted 只动 battleTutorial",
Root1.BattleTutorial.MovementHasStarted && Root1.Spectator == Root0.Spectator
)
val W = RootWiring.ByField("battleTutorial")!!
Check(
"根:battleTutorial 委派到 rE / Q.sq8 且 4 个动作",
W.SliceClass == "rE" && W.Reducer == "Q.sq8" &&
W.Actions.toSet() == setOf(
"ShowMessage", "SetFragsCount", "MarkMovementStarted", "ResetBattleTutorial",
)
)
}
// 57 切片 reducer:Spectator(HA / Li.jqm,4 字段;6 个动作)
run {
val S0 = SpectatorStateSlice()
Check(
"切片 Spectator:默认非观战、列表可见",
!S0.IsSpectator && S0.Spectators.isEmpty() && S0.SpectatorsListVisible
)
val S1 = ReduceSpectator(S0, SetSpectator(true))
Check("切片 Spectator:SetSpectator 写布尔", S1.IsSpectator)
val S2 = ReduceSpectator(S1, AddSpectator(1L, "uid1", true, false))
Check(
"切片 Spectator:AddSpectator 追加 SpectatorInfo",
S2.Spectators.size == 1 &&
(S2.Spectators[0] as SpectatorInfo).userId == 1L
)
val S3 = ReduceSpectator(S2, AddSpectator(2L, "uid2", false, false))
Check("切片 Spectator:第二个观战者入列", S3.Spectators.size == 2)
Check(
"切片 Spectator:RemoveSpectator 按 userId 删",
ReduceSpectator(S3, RemoveSpectator(1L)).Spectators.size == 1
)
Check(
"切片 Spectator:SetSpectators 整表替换",
ReduceSpectator(S3, SetSpectators(listOf("x"))).Spectators == listOf<Any?>("x")
)
Check(
"切片 Spectator:SetSpectatorsListVisible 写可见性",
!ReduceSpectator(S3, SetSpectatorsListVisible(false)).SpectatorsListVisible
)
Check(
"切片 Spectator:UpdateHudLayout 写布局",
ReduceSpectator(S3, UpdateHudLayout("hud")).SpectatorHudLayout == "hud"
)
Check("切片 Spectator:无关动作不改状态", ReduceSpectator(S3, AddFrags(1)) == S3)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetSpectator(true))
Check(
"根:SetSpectator 只动 spectator",
Root1.Spectator.IsSpectator && Root1.BattleTutorial == Root0.BattleTutorial
)
val W = RootWiring.ByField("spectator")!!
Check(
"根:spectator 委派到 HA / Li.jqm 且 6 个动作",
W.SliceClass == "HA" && W.Reducer == "Li.jqm" &&
W.Actions.toSet() == setOf(
"SetSpectator", "UpdateHudLayout", "SetSpectators", "AddSpectator",
"RemoveSpectator", "SetSpectatorsListVisible",
)
)
}
// 58 切片 reducer:TwoStepVerification(x9 / _f.atb,6 字段;5 个动作)
run {
val S0 = TwoStepVerificationStateSlice()
Check(
"切片 TwoStepVerification:默认值完整",
!S0.IsUse2FA && !S0.IsActivated2FA && S0.QrCodeDataURI == null &&
S0.SharedSecret == null && S0.VerificationStatus == null &&
!S0.Enable2FaRequest
)
val S1 = ReduceTwoStepVerification(S0, SetUse2FA(true))
Check("切片 TwoStepVerification:SetUse2FA 写开关", S1.IsUse2FA)
val S2 = ReduceTwoStepVerification(S1, SetSecretAndQRCode("secret", "data"))
Check(
"切片 TwoStepVerification:SetSecretAndQRCode 同时写两字段",
S2.SharedSecret == "secret" && S2.QrCodeDataURI == "data"
)
Check(
"切片 TwoStepVerification:VerificationStatus(false) 写失败",
ReduceTwoStepVerification(S2, VerificationStatus(false)).VerificationStatus == false
)
Check(
"切片 TwoStepVerification:VerificationStatus(true) 视为已启用",
ReduceTwoStepVerification(S2, VerificationStatus(true)).IsActivated2FA
)
Check(
"切片 TwoStepVerification:VerificationStatus(null) 清空状态",
ReduceTwoStepVerification(
ReduceTwoStepVerification(S2, VerificationStatus(false)),
VerificationStatus(null),
).VerificationStatus == null
)
Check(
"切片 TwoStepVerification:ActivationStatus2FA / Enable2FaRequest",
!ReduceTwoStepVerification(S2, ActivationStatus2FA(false)).IsActivated2FA &&
ReduceTwoStepVerification(S2, Enable2FaRequest(true)).Enable2FaRequest
)
Check(
"切片 TwoStepVerification:无关动作不改状态",
ReduceTwoStepVerification(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetUse2FA(true))
Check(
"根:SetUse2FA 只动 twoStepVerificationState",
Root1.TwoStepVerificationState.IsUse2FA && Root1.Spectator == Root0.Spectator
)
val W = RootWiring.ByField("twoStepVerificationState")!!
Check(
"根:twoStepVerificationState 委派到 x9 / _f.atb 且 5 个动作",
W.SliceClass == "x9" && W.Reducer == "_f.atb" &&
W.Actions.toSet() == setOf(
"SetUse2FA", "ActivationStatus2FA", "SetSecretAndQRCode",
"VerificationStatus", "Enable2FaRequest",
)
)
}
// 59 切片 reducer:AntiAddiction(UC / W.iq8,8 字段;5 个动作)
run {
val S0 = AntiAddictionStateSlice()
Check(
"切片 AntiAddiction:默认值完整",
!S0.IsEnableRealNameCheck && !S0.UnknownUserId && S0.UrlForEnterUserId == null &&
S0.PlayingTimeInMinutes == 0 && S0.AlertMessage == "" &&
S0.IdNumber == "" && S0.RealName == "" && S0.ValidateStatus == null
)
val S1 = ReduceAntiAddiction(S0, EnableRealNameCheck)
Check("切片 AntiAddiction:EnableRealNameCheck 置位", S1.IsEnableRealNameCheck)
val S2 = ReduceAntiAddiction(S1, RequireUserIdVerification("https://u"))
Check(
"切片 AntiAddiction:RequireUserIdVerification 写 unknown+url",
S2.UnknownUserId && S2.UrlForEnterUserId == "https://u"
)
val S3 = ReduceAntiAddiction(S2, UpdatePlayingTime(180, "休息"))
Check(
"切片 AntiAddiction:UpdatePlayingTime 写时长与提示",
S3.PlayingTimeInMinutes == 180 && S3.AlertMessage == "休息"
)
val S4 = ReduceAntiAddiction(S3, SetRealNameAndIdNumber("张三", "110"))
Check(
"切片 AntiAddiction:SetRealNameAndIdNumber 写实名与证件号",
S4.RealName == "张三" && S4.IdNumber == "110"
)
Check(
"切片 AntiAddiction:实名参数为 null 时保留旧值",
ReduceAntiAddiction(S4, SetRealNameAndIdNumber(null, null)) == S4
)
Check(
"切片 AntiAddiction:SetValidateAntiAddictionUserDataStatus 写状态",
ReduceAntiAddiction(S4, SetValidateAntiAddictionUserDataStatus("ok"))
.ValidateStatus == "ok"
)
Check("切片 AntiAddiction:无关动作不改状态", ReduceAntiAddiction(S4, AddFrags(1)) == S4)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, EnableRealNameCheck)
Check(
"根:EnableRealNameCheck 只动 antiAddiction",
Root1.AntiAddiction.IsEnableRealNameCheck && Root1.Moderation == Root0.Moderation
)
val W = RootWiring.ByField("antiAddiction")!!
Check(
"根:antiAddiction 委派到 UC / W.iq8 且 5 个动作",
W.SliceClass == "UC" && W.Reducer == "W.iq8" &&
W.Actions.toSet() == setOf(
"EnableRealNameCheck", "RequireUserIdVerification", "UpdatePlayingTime",
"SetRealNameAndIdNumber", "SetValidateAntiAddictionUserDataStatus",
)
)
}
// 60 切片 reducer:Moderation(NI / Ei.oqh,11 字段;13 个动作)
run {
val S0 = ModerationStateSlice()
Check(
"切片 Moderation:默认值完整",
!S0.ModeratorMode && !S0.BattleModeratorMode && !S0.EventOrganizerMode &&
S0.ModerationToolBattleUrlTemplate == "" &&
S0.ModerationToolUserUrlTemplate == "" && S0.BattleInfo == null &&
!S0.ForceShowBattleInfo && S0.SystemMessage == "" &&
!S0.ForceShowSystemMessage && !S0.ForceShowBlockUserMenu &&
S0.SelectedUsers.isEmpty()
)
val S1 = ReduceModeration(S0, SetModeratorModes(true, false, true))
Check(
"切片 Moderation:SetModeratorModes 写三个开关",
S1.ModeratorMode && !S1.BattleModeratorMode && S1.EventOrganizerMode
)
Check(
"切片 Moderation:SetModerationUrlTemplates 写两个模板",
ReduceModeration(S1, SetModerationUrlTemplates("b", "u"))
.ModerationToolBattleUrlTemplate == "b"
)
val S2 = ReduceModeration(S1, ShowBattleInfo("info"))
Check(
"切片 Moderation:ShowBattleInfo 写面板并强制显示",
S2.BattleInfo == "info" && S2.ForceShowBattleInfo
)
Check(
"切片 Moderation:HideBattleInfo 取消强制显示",
!ReduceModeration(S2, HideBattleInfo).ForceShowBattleInfo
)
val S3 = ReduceModeration(
ReduceModeration(S2, AddSelectedUser(1L, 5, "a")),
AddSelectedUserFromChat(1L, 9, "a2"),
)
Check(
"切片 Moderation:选中用户按 userId 去重后追加",
S3.SelectedUsers.size == 1 &&
(S3.SelectedUsers[0] as ModerationUserRecord).userRank == 9
)
Check(
"切片 Moderation:RemoveSelectedUser 按 userId 删",
ReduceModeration(
ReduceModeration(S3, AddSelectedUser(2L, 1, "b")),
RemoveSelectedUser(1L),
).SelectedUsers.size == 1
)
Check(
"切片 Moderation:ClearSelectedUsers / ResetSelectedUsers 清空",
ReduceModeration(S3, ClearSelectedUsers).SelectedUsers.isEmpty() &&
ReduceModeration(S3, ResetSelectedUsers).SelectedUsers.isEmpty()
)
val S4 = ReduceModeration(S3, ShowModerationSystemMessage("hello"))
Check(
"切片 Moderation:ShowModerationSystemMessage 写消息并强制显示",
S4.SystemMessage == "hello" && S4.ForceShowSystemMessage
)
Check(
"切片 Moderation:HideModerationSystemMessage 取消强制显示",
!ReduceModeration(S4, HideModerationSystemMessage).ForceShowSystemMessage
)
Check(
"切片 Moderation:ShowBlockUserMenu / HideBlockUserMenu 切换",
ReduceModeration(S4, ShowBlockUserMenu).ForceShowBlockUserMenu &&
!ReduceModeration(S4, HideBlockUserMenu).ForceShowBlockUserMenu
)
Check("切片 Moderation:无关动作不改状态", ReduceModeration(S4, AddFrags(1)) == S4)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ShowBlockUserMenu)
Check(
"根:ShowBlockUserMenu 只动 moderation",
Root1.Moderation.ForceShowBlockUserMenu && Root1.AntiAddiction == Root0.AntiAddiction
)
val W = RootWiring.ByField("moderation")!!
Check(
"根:moderation 委派到 NI / Ei.oqh 且 13 个动作",
W.SliceClass == "NI" && W.Reducer == "Ei.oqh" && W.Actions.size == 13
)
}
// 61 切片 reducer:MigrationState($it / Uv.rtg,7 字段;11 个动作)
run {
val S0 = MigrationStateSlice()
Check(
"切片 MigrationState:默认值完整",
!S0.RequestLogin && S0.Login == "" && S0.ForceLogin == null &&
S0.Password == "" &&
S0.LoginStatus == MigrationLoginStatus.UNKNOWN &&
S0.RegisterStatus == MigrationRegisterStatus.UNKNOWN &&
S0.AdvisedUids.isEmpty()
)
val S1 = ReduceMigrationState(S0, SetUid("alice"))
Check(
"切片 MigrationState:SetUid 写 login 并复位状态",
S1.Login == "alice" &&
S1.LoginStatus == MigrationLoginStatus.UNKNOWN &&
S1.RegisterStatus == MigrationRegisterStatus.UNKNOWN
)
Check(
"切片 MigrationState:ForceSetUid 写 forceLogin",
ReduceMigrationState(S1, ForceSetUid("forced")).ForceLogin == "forced"
)
Check(
"切片 MigrationState:SetPassword 写 password",
ReduceMigrationState(S1, SetPassword("pwd")).Password == "pwd"
)
Check(
"切片 MigrationState:UidIsFree/UidIsIncorrect/UidTooShort 写 loginStatus",
ReduceMigrationState(S1, UidIsFree).LoginStatus == MigrationLoginStatus.FREE &&
ReduceMigrationState(S1, UidIsIncorrect).LoginStatus ==
MigrationLoginStatus.INCORRECT &&
ReduceMigrationState(S1, UidTooShort).LoginStatus ==
MigrationLoginStatus.SHORT_NAME
)
val S2 = ReduceMigrationState(S1, EnteredUidIsBusy(listOf("u1")))
Check(
"切片 MigrationState:EnteredUidIsBusy 写 BUSY 与 advisedUids",
S2.LoginStatus == MigrationLoginStatus.BUSY && S2.AdvisedUids == listOf<Any?>("u1")
)
Check(
"切片 MigrationState:注册状态两支",
ReduceMigrationState(S1, RegisterPasswordIncorrect).RegisterStatus ==
MigrationRegisterStatus.PASSWORD_INCORRECT &&
ReduceMigrationState(S1, RegisterFailed).RegisterStatus ==
MigrationRegisterStatus.FAILED
)
Check(
"切片 MigrationState:ResetUidStatus / ResetAuthenticationForm",
ReduceMigrationState(S2, ResetUidStatus).LoginStatus ==
MigrationLoginStatus.UNKNOWN &&
ReduceMigrationState(S2, ResetAuthenticationForm).Login == ""
)
Check(
"切片 MigrationState:无关动作不改状态",
ReduceMigrationState(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetUid("bob"))
Check(
"根:SetUid 同时动 migrationState 与 register",
Root1.MigrationState.Login == "bob" && Root1.Register.Login == "bob" &&
Root1.System == Root0.System
)
val W = RootWiring.ByField("migrationState")!!
Check(
"根:migrationState 委派到 \$it / Uv.rtg 且 11 个动作",
W.SliceClass == "\$it" && W.Reducer == "Uv.rtg" && W.Actions.size == 11
)
}
// 62 切片 reducer:Register(Cit / cp.stg,16 字段;19 个动作)
run {
val S0 = RegisterStateSlice()
Check(
"切片 Register:默认值完整",
!S0.IsDisabled && S0.Login == "" && S0.ForceLogin == null && S0.Password == "" &&
S0.UidValidationStatus == UidStatus.UNKNOWN &&
S0.RegisterStatus == RegistrationStatus.UNKNOWN &&
S0.AdvisedUids.isEmpty() && S0.SocialNetworkAvailability.isEmpty() &&
S0.SocialNetworkClientIds == emptyMap<String, Any?>() &&
S0.SocialNetworkOAuthData == null && S0.MinPasswordLength == 6 &&
S0.MaxPasswordLength == 20 && !S0.PasswordNeedLower &&
!S0.PasswordNeedUpper && !S0.PasswordNeedDigit && S0.OpenedPassword
)
val S1 = ReduceRegister(S0, DisableRegister)
Check("切片 Register:DisableRegister 置灰", S1.IsDisabled)
val S2 = ReduceRegister(S1, SetUid("alice"))
Check(
"切片 Register:SetUid 写 login 并复位 uid 状态",
S2.Login == "alice" && S2.UidValidationStatus == UidStatus.UNKNOWN
)
Check(
"切片 Register:原状态为 INCORRECT 时保留 registerStatus,否则复位",
ReduceRegister(
ReduceRegister(
ReduceRegister(S2, RegisterFailed),
UidIsIncorrect,
),
SetUid("alice2"),
).RegisterStatus == RegistrationStatus.FAILED &&
ReduceRegister(
ReduceRegister(
ReduceRegister(S2, RegisterFailed),
ResetUidStatus,
),
SetUid("alice2"),
).RegisterStatus == RegistrationStatus.UNKNOWN
)
val S3 = ReduceRegister(S2, EnteredUidIsBusy(listOf("u1")))
Check(
"切片 Register:EnteredUidIsBusy 写 BUSY + advisedUids",
S3.UidValidationStatus == UidStatus.BUSY && S3.AdvisedUids == listOf<Any?>("u1")
)
Check(
"切片 Register:uid 三个状态臂",
ReduceRegister(S2, UidIsFree).UidValidationStatus == UidStatus.FREE &&
ReduceRegister(S2, UidIsIncorrect).UidValidationStatus == UidStatus.INCORRECT &&
ReduceRegister(S2, UidTooShort).UidValidationStatus == UidStatus.SHORT_NAME
)
Check(
"切片 Register:注册状态四臂",
ReduceRegister(S2, RegisterPasswordCorrect).RegisterStatus ==
RegistrationStatus.PASSWORD_CORRECT &&
ReduceRegister(S2, RegisterPasswordIncorrect).RegisterStatus ==
RegistrationStatus.PASSWORD_INCORRECT &&
ReduceRegister(S2, RegisterFailed).RegisterStatus == RegistrationStatus.FAILED &&
ReduceRegister(S2, ResetRegisterStatus).RegisterStatus ==
RegistrationStatus.UNKNOWN
)
val S4 = ReduceRegister(
ReduceRegister(S2, SetPasswordRestrictions(8, 32, true, false, true)),
SetOpenedPassword(false),
)
Check(
"切片 Register:密码规则与展开开关",
S4.MinPasswordLength == 8 && S4.MaxPasswordLength == 32 &&
S4.PasswordNeedLower && !S4.PasswordNeedUpper && S4.PasswordNeedDigit &&
!S4.OpenedPassword
)
Check(
"切片 Register:社媒三支",
ReduceRegister(S2, SocialNetworkAvailability(mapOf("vk" to true)))
.SocialNetworkAvailability == mapOf("vk" to true) &&
ReduceRegister(S2, SocialNetworkClientIds("cid"))
.SocialNetworkClientIds == "cid" &&
ReduceRegister(S2, ReceiveOAuthUri("oauth"))
.SocialNetworkOAuthData == "oauth"
)
run {
val R = ReduceRegister(S3, ResetAuthenticationForm)
Check(
"切片 Register:ResetAuthenticationForm 清 login 与两个状态",
R.Login == "" && R.UidValidationStatus == UidStatus.UNKNOWN &&
R.RegisterStatus == RegistrationStatus.UNKNOWN &&
R.AdvisedUids == S3.AdvisedUids
)
}
Check("切片 Register:无关动作不改状态", ReduceRegister(S4, AddFrags(1)) == S4)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, RegisterFailed)
Check(
"根:RegisterFailed 同时动 migrationState 与 register",
Root1.Register.RegisterStatus == RegistrationStatus.FAILED &&
Root1.MigrationState.RegisterStatus ==
MigrationRegisterStatus.FAILED
)
val W = RootWiring.ByField("register")!!
Check(
"根:register 委派到 Cit / cp.stg 且 19 个动作",
W.SliceClass == "Cit" && W.Reducer == "cp.stg" && W.Actions.size == 19
)
}
// 63 切片 reducer:UserReports(Zst / qot.zth,5 字段;6 个动作)
run {
val S0 = UserReportsStateSlice()
Check(
"切片 UserReports:默认值完整",
!S0.IsEnabled && !S0.HelpdeskTicketsEnabled && S0.AvailableReports == 0 &&
S0.AvailableGameReports == 0 && S0.Reports.isEmpty()
)
val S1 = ReduceUserReports(S0, InitUserReports(true, true, 5, 3))
Check(
"切片 UserReports:InitUserReports 写四个开关/额度",
S1.IsEnabled && S1.HelpdeskTicketsEnabled &&
S1.AvailableReports == 5 && S1.AvailableGameReports == 3
)
Check(
"切片 UserReports:SetAvailableReports / SetAvailableGameReports",
ReduceUserReports(S1, SetAvailableReports(7)).AvailableReports == 7 &&
ReduceUserReports(S1, SetAvailableGameReports(9)).AvailableGameReports == 9
)
val S2 = ReduceUserReports(S1, ReportProBattleName("b1"))
Check(
"切片 UserReports:ReportProBattleName 追加 BATTLE_NAME 记录",
S2.Reports.size == 1 &&
(S2.Reports[0] as ReportEntry).kind == ReportKind.BATTLE_NAME &&
(S2.Reports[0] as ReportEntry).targetId == "b1" &&
(S2.Reports[0] as ReportEntry).expireTime > 0.0
)
val S3 = ReduceUserReports(
ReduceUserReports(S2, ReportLobbyChatMessage("m1")),
ReportBattleChatMessage("m2"),
)
Check(
"切片 UserReports:两种聊天举报各追加一条",
S3.Reports.size == 3 &&
(S3.Reports[1] as ReportEntry).kind == ReportKind.LOBBY_CHAT_MESSAGE &&
(S3.Reports[2] as ReportEntry).kind == ReportKind.BATTLE_CHAT_MESSAGE
)
Check(
"切片 UserReports:无关动作不改状态",
ReduceUserReports(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ReportProBattleName("b"))
Check(
"根:ReportProBattleName 只动 userReportsState",
Root1.UserReportsState.Reports.size == 1 &&
Root1.Register == Root0.Register
)
val W = RootWiring.ByField("userReportsState")!!
Check(
"根:userReportsState 委派到 Zst / qot.zth 且 6 个动作",
W.SliceClass == "Zst" && W.Reducer == "qot.zth" && W.Actions.size == 6
)
}
// 64 切片 reducer:Friends(KD / NL().cr9,8 字段;13 个动作)
run {
val S0 = FriendsStateSlice()
Check(
"切片 Friends:默认值完整",
S0.UserSearchState == FriendSearchState.HIDDEN && S0.ContextMenuUser == null &&
S0.AcceptedLimit == 0 && S0.IncomingLimit == 0 &&
S0.Accepted.isEmpty() && S0.NewAccepted.isEmpty() &&
S0.Incoming.isEmpty() && S0.NewIncoming.isEmpty()
)
val S1 = ReduceFriends(S0, ChangeFriendAddFindState(FriendSearchState.SEARCHING))
Check(
"切片 Friends:ChangeFriendAddFindState 写搜索状态",
S1.UserSearchState == FriendSearchState.SEARCHING
)
Check(
"切片 Friends:SetValidationStatus 命中即 FOUND,null 即 NOT_FOUND",
ReduceFriends(S1, SetValidationStatus("ok")).UserSearchState ==
FriendSearchState.FOUND &&
ReduceFriends(S1, SetValidationStatus(null)).UserSearchState ==
FriendSearchState.NOT_FOUND
)
Check(
"切片 Friends:SetLimits 写两个上限",
ReduceFriends(S1, SetLimits(50, 30)).let {
it.AcceptedLimit == 50 && it.IncomingLimit == 30
}
)
Check(
"切片 Friends:右键菜单设置 / 清除",
ReduceFriends(S1, SetContextMenuUser("u1")).ContextMenuUser == "u1" &&
ReduceFriends(S1, ClearContextMenuUser).ContextMenuUser == null
)
val S2 = ReduceFriends(S1, AddUsersToAccepted(listOf(1L, 2L)))
Check(
"切片 Friends:AddUsersToAccepted 整批入集合",
S2.Accepted == setOf<Any?>(1L, 2L)
)
Check(
"切片 Friends:RemoveUserFromAccepted 删单值",
ReduceFriends(S2, RemoveUserFromAccepted(1L)).Accepted == setOf<Any?>(2L)
)
val S3 = ReduceFriends(S2, AddUsersToNewAccepted(clanId = 0, userIds = listOf(3L, 4L)))
Check(
"切片 Friends:AddUsersToNewAccepted 读 userIds",
S3.NewAccepted == setOf<Any?>(3L, 4L)
)
Check(
"切片 Friends:RemoveUserFromNewAccepted / AddUsersToIncoming",
ReduceFriends(S3, RemoveUserFromNewAccepted(3L)).NewAccepted == setOf<Any?>(4L) &&
ReduceFriends(S3, AddUsersToIncoming(listOf(5L))).Incoming == setOf<Any?>(5L)
)
Check(
"切片 Friends:RemoveUsersFromNewIncoming 按集合减",
ReduceFriends(
ReduceFriends(S3, AddUsersToNewIncoming(listOf(6L, 7L))),
RemoveUsersFromNewIncoming(listOf(6L)),
).NewIncoming == setOf<Any?>(7L)
)
Check(
"切片 Friends:无关动作不改状态",
ReduceFriends(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ChangeFriendAddFindState(FriendSearchState.FOUND))
Check(
"根:ChangeFriendAddFindState 只动 friends",
Root1.Friends.UserSearchState == FriendSearchState.FOUND &&
Root1.Partner == Root0.Partner
)
val W = RootWiring.ByField("friends")!!
Check(
"根:friends 委派到 KD / NL().cr9 且 13 个动作",
W.SliceClass == "KD" && W.Reducer == "NL().cr9" && W.Actions.size == 13
)
}
// 65 切片 reducer:Supplies(wE / $t.fq9,1 字段;SetSupplyCount)
run {
val S0 = SuppliesStateSlice()
Check("切片 Supplies:默认空 Map", S0.Count.isEmpty())
val S1 = ReduceSupplies(S0, SetSupplyCount("mine", 3))
Check(
"切片 Supplies:SetSupplyCount 写键值",
S1.Count == mapOf<Any?, Any?>("mine" to 3)
)
Check(
"切片 Supplies:再次写入覆盖旧值",
ReduceSupplies(S1, SetSupplyCount("mine", 1)).Count ==
mapOf<Any?, Any?>("mine" to 1)
)
Check("切片 Supplies:无关动作不改状态", ReduceSupplies(S1, AddFrags(1)) == S1)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetSupplyCount("k", 9))
Check(
"根:SetSupplyCount 只动 suppliesState",
Root1.SuppliesState.Count == mapOf<Any?, Any?>("k" to 9) &&
Root1.ClanUserInfo == Root0.ClanUserInfo
)
val W = RootWiring.ByField("suppliesState")!!
Check(
"根:suppliesState 委派到 wE / \$t.fq9",
W.SliceClass == "wE" && W.Reducer == "\$t.fq9" &&
W.Actions == listOf("SetSupplyCount")
)
}
// 66 切片 reducer:ClanUserInfo(hO / de.gr1,1 字段;Loaded 合并)
run {
val S0 = ClanUserInfoStateSlice()
Check("切片 ClanUserInfo:默认空 Map", S0.Users.isEmpty())
val S1 = ReduceClanUserInfo(S0, Loaded(mapOf("u1" to ClanNotifierData(1L, null, 11L))))
Check(
"切片 ClanUserInfo:Loaded 写入用户表",
S1.Users.keys == setOf<Any?>("u1") &&
(S1.Users["u1"] as ClanNotifierData).userId == 11L
)
Check(
"切片 ClanUserInfo:第二次 Loaded 是合并不是覆盖",
ReduceClanUserInfo(S1, Loaded(mapOf("u2" to ClanNotifierData(2L, null, 22L)))).Users.keys ==
setOf<Any?>("u1", "u2")
)
Check(
"切片 ClanUserInfo:无关动作不改状态",
ReduceClanUserInfo(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, Loaded(mapOf("x" to ClanNotifierData(3L, null, 33L))))
Check(
"根:Loaded 只动 clanUserInfo",
Root1.ClanUserInfo.Users.keys == setOf<Any?>("x") &&
Root1.SuppliesState == Root0.SuppliesState
)
val W = RootWiring.ByField("clanUserInfo")!!
Check(
"根:clanUserInfo 委派到 hO / de.gr1",
W.SliceClass == "hO" && W.Reducer == "de.gr1" &&
W.Actions == listOf("Loaded")
)
}
// 67 切片 reducer:BattleInvite(att / Ttt().nth,3 字段;6 个动作)
run {
val S0 = BattleInviteStateSlice()
Check(
"切片 BattleInvite:默认开启、两个集合为空",
S0.IsEnabled && S0.InvitedUsers.isEmpty() && S0.SelectedForInvite.isEmpty()
)
val S1 = ReduceBattleInvite(S0, AddInvitedUser(1L))
Check("切片 BattleInvite:AddInvitedUser 入已邀请集合", S1.InvitedUsers == setOf<Any?>(1L))
Check(
"切片 BattleInvite:InvitationResponse 从已邀请集合删",
ReduceBattleInvite(S1, InvitationResponse(1L, "ok")).InvitedUsers.isEmpty()
)
val S2 = ReduceBattleInvite(S1, SelectUserToInvite(2L))
Check(
"切片 BattleInvite:SelectUserToInvite 入待邀请集合",
S2.SelectedForInvite == setOf<Any?>(2L)
)
Check(
"切片 BattleInvite:RemoveUserFromInvited 从待邀请集合删",
ReduceBattleInvite(S2, RemoveUserFromInvited(2L)).SelectedForInvite.isEmpty()
)
Check(
"切片 BattleInvite:ClearSelectedForInvite 清空",
ReduceBattleInvite(S2, ClearSelectedForInvite).SelectedForInvite.isEmpty()
)
Check(
"切片 BattleInvite:EnableSetBattleInviteNotifications 写开关",
!ReduceBattleInvite(S2, EnableSetBattleInviteNotifications(false)).IsEnabled
)
Check(
"切片 BattleInvite:无关动作不改状态",
ReduceBattleInvite(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddInvitedUser(9L))
Check(
"根:AddInvitedUser 只动 battleInviteState",
Root1.BattleInviteState.InvitedUsers == setOf<Any?>(9L) &&
Root1.UserRenameState == Root0.UserRenameState
)
val W = RootWiring.ByField("battleInviteState")!!
Check(
"根:battleInviteState 委派到 att / Ttt().nth 且 6 个动作",
W.SliceClass == "att" && W.Reducer == "Ttt().nth" &&
W.Actions.toSet() == setOf(
"AddInvitedUser", "InvitationResponse", "SelectUserToInvite",
"RemoveUserFromInvited", "ClearSelectedForInvite",
"EnableSetBattleInviteNotifications",
)
)
}
// 68 切片 reducer:Partner(UJ / (inline@partner),15 字段;11 个动作)
run {
val S0 = PartnerStateSlice()
Check(
"切片 Partner:默认值完整",
S0.PartnerId == null && S0.LoginParams.isEmpty() && S0.Params.isEmpty() &&
!S0.HasOwnPaymentSystem && !S0.HasSocialFunction &&
!S0.HasClientPaymentAction && S0.PaymentAction == null &&
S0.InviteAction == null && !S0.CanOpenExternalLinks &&
!S0.HasAccountBinding && S0.FailedRedirectUrl == null &&
S0.LoginForBindingStatus == ByPasswordStatus.NONE &&
!S0.IsAccountAlreadyLinked && S0.ItemsEnabledForPartner.isEmpty() &&
!S0.IsPartnerReady
)
val S1 = ReducePartner(S0, SetPartnerId("p1", "https://fail"))
Check(
"切片 Partner:SetPartnerId 同时写失败跳转",
S1.PartnerId == "p1" && S1.FailedRedirectUrl == "https://fail"
)
val S2 = ReducePartner(
S1,
InitSocialNetworkParameters(
hasOwnPaymentSystem = true, hasSocialFunction = true,
hasClientPaymentAction = false, failedRedirectUrl = null,
canOpenExternalLinks = true, hasAccountBinding = true,
),
)
Check(
"切片 Partner:InitSocialNetworkParameters 写 6 个字段",
S2.HasOwnPaymentSystem && S2.HasSocialFunction && !S2.HasClientPaymentAction &&
S2.CanOpenExternalLinks && S2.HasAccountBinding
)
Check(
"切片 Partner:loginParams / params 分别写入",
ReducePartner(S2, SetLoginParams(mapOf("a" to 1))).LoginParams ==
mapOf<Any?, Any?>("a" to 1) &&
ReducePartner(S2, SetPartnerParameters(mapOf("b" to 2))).Params ==
mapOf<Any?, Any?>("b" to 2)
)
Check(
"切片 Partner:两种邀请动作都写 inviteAction",
ReducePartner(S2, InitPartnerInviteAction("i1")).InviteAction == "i1" &&
ReducePartner(S2, UpdatePartnerInviteAction("i2")).InviteAction == "i2"
)
Check(
"切片 Partner:SetLoginStatusForBinding 写绑定状态",
ReducePartner(S2, SetLoginStatusForBinding(ByPasswordStatus.WRONG_PASSWORD))
.LoginForBindingStatus == ByPasswordStatus.WRONG_PASSWORD
)
Check(
"切片 Partner:已绑定标记两态",
ReducePartner(S2, MarkAccountLinked).IsAccountAlreadyLinked &&
!ReducePartner(S2, MarkAccountNotLinked).IsAccountAlreadyLinked
)
Check(
"切片 Partner:SetPartnerItemEnabled 写物品开关 Map",
ReducePartner(S2, SetPartnerItemEnabled("item", true))
.ItemsEnabledForPartner == mapOf<Any?, Any?>("item" to true)
)
Check(
"切片 Partner:SetPartnerReady 写就绪位",
ReducePartner(S2, SetPartnerReady(true)).IsPartnerReady
)
Check("切片 Partner:无关动作不改状态", ReducePartner(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetPartnerReady(true))
Check(
"根:SetPartnerReady 只动 partner",
Root1.Partner.IsPartnerReady && Root1.Friends == Root0.Friends
)
val W = RootWiring.ByField("partner")!!
Check(
"根:partner 是内联 reducer 且 11 个动作",
W.SliceClass == "UJ" && W.Reducer == "(inline@partner)" && W.Actions.size == 11
)
}
// 69 切片 reducer:UserRename(O8 / Rf.pte,10 字段;11 个动作)
run {
val S0 = UserRenameStateSlice()
Check(
"切片 UserRename:默认值完整",
!S0.IsFreeRenameAvailable && S0.UserRenameShopItemId == null &&
S0.UidInputState == RenameUidState.EMPTY && S0.RenameSuccessful == null &&
S0.RubiesForRename == Int.MAX_VALUE && !S0.IsResponseDelivered &&
!S0.IsResponseActual && S0.ChangeUidHash == null && S0.Email == null &&
S0.ShowLinkErrorWarning
)
val S1 = ReduceUserRename(S0, ChangeUidInputState(RenameUidState.SHORT))
Check(
"切片 UserRename:ChangeUidInputState 写输入状态",
S1.UidInputState == RenameUidState.SHORT
)
Check(
"切片 UserRename:免费改名 / 改名成功 / 改名失败",
ReduceUserRename(S1, MarkFreeRenameAvailable).IsFreeRenameAvailable &&
ReduceUserRename(S1, MarkRenameSucceeded).let {
!it.IsFreeRenameAvailable && it.RenameSuccessful == true
} &&
ReduceUserRename(S1, MarkRenameFailed).RenameSuccessful == false
)
Check(
"切片 UserRename:SetValidationStatus 按 ordinal 映射输入状态",
ReduceUserRename(S1, SetValidationStatus(RenameUidState.FREE)).let {
it.UidInputState == RenameUidState.FREE && it.IsResponseDelivered
} &&
ReduceUserRename(S1, SetValidationStatus(RenameUidState.SHORT)).UidInputState ==
RenameUidState.NOT_VALID &&
ReduceUserRename(S1, SetValidationStatus(RenameUidState.NOT_VALID))
.UidInputState == RenameUidState.FORBIDDEN
)
Check(
"切片 UserRename:SetRubiesForRename 写价格",
ReduceUserRename(S1, SetRubiesForRename(99)).RubiesForRename == 99
)
val S2 = ReduceUserRename(
ReduceUserRename(
ReduceUserRename(S1, SetChangeUidParams("hash", "mail")),
ResponseDelivered(true),
),
ResponseActual(true),
)
Check(
"切片 UserRename:改绑参数与两个 response 位",
S2.ChangeUidHash == "hash" && S2.Email == "mail" &&
S2.IsResponseDelivered && S2.IsResponseActual
)
Check(
"切片 UserRename:ResetRenameDialog 复位弹窗字段",
ReduceUserRename(S2, ResetRenameDialog).let {
it.UserRenameShopItemId == null &&
it.UidInputState == RenameUidState.EMPTY &&
it.RenameSuccessful == null && !it.IsResponseDelivered &&
!it.IsResponseActual && it.ChangeUidHash == "hash"
}
)
Check(
"切片 UserRename:HideLinkErrorWarning 关提示",
!ReduceUserRename(S2, HideLinkErrorWarning).ShowLinkErrorWarning
)
Check(
"切片 UserRename:无关动作不改状态",
ReduceUserRename(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetRubiesForRename(5))
Check(
"根:SetRubiesForRename 只动 userRenameState",
Root1.UserRenameState.RubiesForRename == 5 &&
Root1.Partner == Root0.Partner
)
val W = RootWiring.ByField("userRenameState")!!
Check(
"根:userRenameState 委派到 O8 / Rf.pte 且 11 个动作",
W.SliceClass == "O8" && W.Reducer == "Rf.pte" && W.Actions.size == 11
)
}
// 70 切片 reducer:BattlePause(dT / NT().mqc,3 字段;4 个动作)
run {
val S0 = BattlePauseStateSlice()
Check(
"切片 BattlePause:默认不暂停、无剩余时间",
!S0.InAutoPauseMode && S0.TimeForAutoLeave == null && S0.IdleKickPeriodInMsec == 0
)
val S1 = ReduceBattlePause(S0, OnPauseMode("30s"))
Check(
"切片 BattlePause:OnPauseMode 置暂停并写剩余时间",
S1.InAutoPauseMode && S1.TimeForAutoLeave == "30s"
)
Check(
"切片 BattlePause:CancelAutoPause 只清暂停位",
ReduceBattlePause(S1, CancelAutoPause).let {
!it.InAutoPauseMode && it.TimeForAutoLeave == "30s"
}
)
Check(
"切片 BattlePause:SetIdleKickPeriod 写周期",
ReduceBattlePause(S1, SetIdleKickPeriod(120)).IdleKickPeriodInMsec == 120
)
Check(
"切片 BattlePause:ResetBattlePause 保留挂机周期",
ReduceBattlePause(
ReduceBattlePause(S1, SetIdleKickPeriod(120)),
ResetBattlePause,
).let { !it.InAutoPauseMode && it.IdleKickPeriodInMsec == 120 }
)
Check(
"切片 BattlePause:无关动作不改状态",
ReduceBattlePause(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, CancelAutoPause)
Check(
"根:CancelAutoPause 只动 battlePauseState",
!Root1.BattlePauseState.InAutoPauseMode &&
Root1.BattleViewState == Root0.BattleViewState
)
val W = RootWiring.ByField("battlePauseState")!!
Check(
"根:battlePauseState 委派到 dT / NT().mqc 且 4 个动作",
W.SliceClass == "dT" && W.Reducer == "NT().mqc" &&
W.Actions.toSet() == setOf(
"ResetBattlePause", "OnPauseMode", "CancelAutoPause", "SetIdleKickPeriod",
)
)
}
// 71 切片 reducer:BattleView(DT / sI.jqd,7 字段;14 个动作)
run {
val S0 = BattleViewStateSlice()
Check(
"切片 BattleView:默认在后台、隐藏、输入开",
S0.InBackgroundState && S0.IsHidden && S0.InvisibleElements.isEmpty() &&
S0.InputEnabled && !S0.BattleStatisticsActive && !S0.BattleMenuActive &&
S0.JoystickPosition == null
)
val S1 = ReduceBattleView(S0, SetVisibility(true, "e1"))
Check(
"切片 BattleView:SetVisibility(true) 从隐藏集合删元素",
S1.InvisibleElements.isEmpty()
)
val S2 = ReduceBattleView(S1, SetVisibility(false, "e1"))
Check(
"切片 BattleView:SetVisibility(false) 加入隐藏集合",
S2.InvisibleElements == setOf<Any?>("e1")
)
val S3 = ReduceBattleView(
S2,
SetElementsVisibility(true, listOf("e1", "e2")),
)
Check(
"切片 BattleView:SetElementsVisibility 按集合增删",
S3.InvisibleElements.isEmpty() &&
ReduceBattleView(S3, SetElementsVisibility(false, listOf("x", "y")))
.InvisibleElements == setOf<Any?>("x", "y")
)
Check(
"切片 BattleView:ClearInvisibleElements 清空",
ReduceBattleView(S2, ClearInvisibleElements).InvisibleElements.isEmpty()
)
Check(
"切片 BattleView:输入开关两态",
ReduceBattleView(S2, DisableBattleInput).let { !it.InputEnabled } &&
ReduceBattleView(S2, EnableBattleInput).InputEnabled
)
Check(
"切片 BattleView:统计与菜单开关两态",
ReduceBattleView(S2, EnableBattleStatistics).BattleStatisticsActive &&
!ReduceBattleView(S2, DisableBattleStatistics).BattleStatisticsActive &&
ReduceBattleView(S2, EnableBattleMenu).BattleMenuActive &&
!ReduceBattleView(S2, DisableBattleMenu).BattleMenuActive
)
Check(
"切片 BattleView:显示 / 后台开关两态",
!ReduceBattleView(S2, ShowBattleView).IsHidden &&
ReduceBattleView(S2, HideBattleView).IsHidden &&
!ReduceBattleView(S2, LeaveBackground).InBackgroundState &&
ReduceBattleView(S2, EnterBackground).InBackgroundState
)
Check(
"切片 BattleView:ResetBattleView 整片复位",
ReduceBattleView(S2, ResetBattleView) == BattleViewStateSlice()
)
Check(
"切片 BattleView:无关动作不改状态",
ReduceBattleView(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, DisableBattleInput)
Check(
"根:DisableBattleInput 只动 battleViewState",
!Root1.BattleViewState.InputEnabled &&
Root1.BattlePauseState == Root0.BattlePauseState
)
val W = RootWiring.ByField("battleViewState")!!
Check(
"根:battleViewState 委派到 DT / sI.jqd 且 14 个动作",
W.SliceClass == "DT" && W.Reducer == "sI.jqd" && W.Actions.size == 14
)
}
// 72 切片 reducer:TankActionsLog(vI / wi.gqe,2 字段;4 个动作)
run {
val S0 = TankActionsLogStateSlice()
Check(
"切片 TankActionsLog:默认两个集合为空",
S0.Logs.isEmpty() && S0.Noticed.isEmpty()
)
val S1 = ReduceTankActionsLog(S0, OnUserJoinTheBattle(1L))
Check(
"切片 TankActionsLog:OnUserJoinTheBattle 记入 noticed",
S1.Noticed == setOf<Any?>(1L)
)
Check(
"切片 TankActionsLog:OnUserLeaveTheBattle 移除 noticed",
ReduceTankActionsLog(S1, OnUserLeaveTheBattle(1L)).Noticed.isEmpty()
)
var S2 = S1
for (I in 1..12) S2 = ReduceTankActionsLog(S2, UpdateLog("log$I"))
Check(
"切片 TankActionsLog:超过 10 条从头截断",
S2.Logs.size == 10 && S2.Logs.first() == "log3" && S2.Logs.last() == "log12"
)
Check(
"切片 TankActionsLog:ResetTankActionsLog 整片复位",
ReduceTankActionsLog(S2, ResetTankActionsLog) == TankActionsLogStateSlice()
)
Check(
"切片 TankActionsLog:无关动作不改状态",
ReduceTankActionsLog(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateLog("x"))
Check(
"根:UpdateLog 只动 tankActionsLogState",
Root1.TankActionsLogState.Logs == listOf<Any?>("x") &&
Root1.SuppliesTutorial == Root0.SuppliesTutorial
)
val W = RootWiring.ByField("tankActionsLogState")!!
Check(
"根:tankActionsLogState 委派到 vI / wi.gqe 且 4 个动作",
W.SliceClass == "vI" && W.Reducer == "wi.gqe" &&
W.Actions.toSet() == setOf(
"ResetTankActionsLog", "OnUserJoinTheBattle", "OnUserLeaveTheBattle",
"UpdateLog",
)
)
}
// 73 切片 reducer:SuppliesTutorial(ust / Uw.uth,2 字段;2 个动作)
run {
val S0 = SuppliesTutorialStateSlice()
Check(
"切片 SuppliesTutorial:默认未加载、未激活",
S0.LoadedElement == null && !S0.ActivatedInCurrentBattle
)
val S1 = ReduceSuppliesTutorial(S0, SetLoadedElement("hull"))
Check("切片 SuppliesTutorial:SetLoadedElement 写元素", S1.LoadedElement == "hull")
Check(
"切片 SuppliesTutorial:ActivatedInCurrentBattle 写激活位",
ReduceSuppliesTutorial(S1, ActivatedInCurrentBattle(true))
.ActivatedInCurrentBattle
)
Check(
"切片 SuppliesTutorial:无关动作不改状态",
ReduceSuppliesTutorial(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetLoadedElement("x"))
Check(
"根:SetLoadedElement 只动 suppliesTutorial",
Root1.SuppliesTutorial.LoadedElement == "x" &&
Root1.TankActionsLogState == Root0.TankActionsLogState
)
val W = RootWiring.ByField("suppliesTutorial")!!
Check(
"根:suppliesTutorial 委派到 ust / Uw.uth",
W.SliceClass == "ust" && W.Reducer == "Uw.uth" &&
W.Actions.toSet() == setOf("ActivatedInCurrentBattle", "SetLoadedElement")
)
}
// 74 切片 reducer:ClanChat(qnt / yv.qtg,3 字段;5 个动作)
run {
val S0 = ClanChatStateSlice()
Check(
"切片 ClanChat:默认关闭、无消息、无配置",
!S0.Open && S0.Messages.isEmpty() && S0.Config == null
)
val S1 = ReduceClanChat(S0, OpenClanChat)
Check("切片 ClanChat:OpenClanChat 打开", S1.Open)
Check(
"切片 ClanChat:CloseClanChat 关闭",
!ReduceClanChat(S1, CloseClanChat).Open
)
val S2 = ReduceClanChat(S1, DoAddMessages(listOf("m1", "m2")))
Check(
"切片 ClanChat:DoAddMessages 追加消息",
S2.Messages == listOf<Any?>("m1", "m2")
)
var S3 = S2
for (I in 1..120) S3 = ReduceClanChat(S3, DoAddMessages(listOf("x$I")))
Check(
"切片 ClanChat:超过 100 条只保留最后 100",
S3.Messages.size == 100 && S3.Messages.last() == "x120"
)
Check(
"切片 ClanChat:Configure 写配置",
ReduceClanChat(S1, Configure(emptyList(), emptyList(), false, "", null, null, "", 0, 0, false, null, ""))
.Config != null
)
Check(
"切片 ClanChat:ResetClanChat 整片复位",
ReduceClanChat(S3, ResetClanChat) == ClanChatStateSlice()
)
Check(
"切片 ClanChat:无关动作不改状态",
ReduceClanChat(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, OpenClanChat)
Check(
"根:OpenClanChat 只动 clanChatState",
Root1.ClanChatState.Open &&
Root1.BattleSearchState == Root0.BattleSearchState
)
val W = RootWiring.ByField("clanChatState")!!
Check(
"根:clanChatState 委派到 qnt / yv.qtg 且 5 个动作",
W.SliceClass == "qnt" && W.Reducer == "yv.qtg" &&
W.Actions.toSet() == setOf(
"OpenClanChat", "CloseClanChat", "Configure", "DoAddMessages",
"ResetClanChat",
)
)
}
// 75 切片 reducer:BattleSearch(Itt / Htt().vtg,5 字段;9 个动作)
run {
val Format = BattleFormat("fmt", "name", "desc", null, null)
val S0 = BattleSearchStateSlice()
Check(
"切片 BattleSearch:默认值完整",
S0.Filter == null && S0.SelectedFormat == null && S0.Search == null &&
!S0.SuspiciousBattlesOnly && S0.SelectedBattleId == null
)
val S1 = ReduceBattleSearch(S0, SetFilter(true))
Check("切片 BattleSearch:SetFilter 写过滤位", S1.Filter == true)
val S2 = ReduceBattleSearch(S1, SetSearch("abc"))
Check("切片 BattleSearch:SetSearch 写搜索串", S2.Search == "abc")
Check(
"切片 BattleSearch:SetSuspiciousFilter 写布尔",
ReduceBattleSearch(S2, SetSuspiciousFilter(true)).SuspiciousBattlesOnly
)
Check(
"切片 BattleSearch:EnterToBattle 清空搜索串",
ReduceBattleSearch(S2, EnterToBattle(7L)).Search == null
)
Check(
"切片 BattleSearch:SelectBattle / SelectBattleFormat",
ReduceBattleSearch(S2, SelectBattle(7L)).SelectedBattleId == 7L &&
ReduceBattleSearch(S2, SelectBattleFormat(Format)).SelectedFormat == Format
)
Check(
"切片 BattleSearch:RemoveBattle 只清匹配的那条",
ReduceBattleSearch(ReduceBattleSearch(S2, SelectBattle(7L)), RemoveBattle(7L))
.SelectedBattleId == null &&
ReduceBattleSearch(
ReduceBattleSearch(S2, SelectBattle(7L)),
RemoveBattle(8L),
).SelectedBattleId == 7L
)
Check(
"切片 BattleSearch:ClearSelectedBattle 清空选中",
ReduceBattleSearch(S2, ClearSelectedBattle).SelectedBattleId == null
)
Check("切片 BattleSearch:Fight 不改状态", ReduceBattleSearch(S2, Fight(1L, BattleTeam.NONE)) == S2)
Check(
"切片 BattleSearch:无关动作不改状态",
ReduceBattleSearch(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetSearch("q"))
Check(
"根:SetSearch 只动 battleSearchState",
Root1.BattleSearchState.Search == "q" &&
Root1.ClanChatState == Root0.ClanChatState
)
val W = RootWiring.ByField("battleSearchState")!!
Check(
"根:battleSearchState 委派到 Itt / Htt().vtg 且 9 个动作",
W.SliceClass == "Itt" && W.Reducer == "Htt().vtg" && W.Actions.size == 9
)
}
// 76 切片 reducer:BattleStatistics(jR / rr.fqr,27 字段;22 个动作)
run {
val S0 = BattleStatisticsStateSlice()
Check(
"切片 BattleStatistics:默认值完整",
S0.BattleId == null && S0.MapName == "" && S0.Mode == null &&
S0.IsMMBattle && !S0.IsParkourMode && !S0.IsTutorial &&
S0.ValuableBattle && S0.IsReArmorEnabled && S0.RankRange == null &&
!S0.IsContinueBattle && S0.RoundRestartTime == null && S0.Score.isEmpty() &&
S0.Fund == 0 && S0.GameModeLevelsCount == 0 && S0.FlagOrBallOwner == null &&
!S0.StatVisible && !S0.RoundStarted && S0.ScoreLimit == 0 &&
!S0.MenuVisible && S0.RemainingTimeInSec == -1 && !S0.BattleLoaded &&
S0.Options.isEmpty() && S0.CustomBattleName == null &&
S0.BattleFormat == null && !S0.IsMatchmakingMark &&
S0.MapNameWithoutMode == "" && S0.DisplayClientInfo
)
val S1 = ReduceBattleStatistics(
S0,
InitBattle(
battleId = "b1", remainingTime = 180, battleName = "Desert",
matchBattle = true, isParkourMode = false, isTutorial = false,
valuableBattle = true, scoreLimit = 100, fund = 50,
options = listOf("o"), customBattleName = "custom",
battleFormat = "fmt", isMatchmakingMark = true, mapNameWithoutMode = "Desert",
),
)
Check(
"切片 BattleStatistics:InitBattle 灌入元信息并清分数 / 菜单",
S1.MapName == "Desert" && S1.IsMMBattle && S1.Fund == 50 &&
S1.ScoreLimit == 100 && S1.RemainingTimeInSec == 180 &&
S1.CustomBattleName == "custom" && S1.IsMatchmakingMark &&
S1.MapNameWithoutMode == "Desert" && S1.Options == listOf<Any?>("o") &&
S1.Score.isEmpty() && !S1.MenuVisible && !S1.IsContinueBattle
)
Check(
"切片 BattleStatistics:模式 / 护甲 / 段位 / 客户端信息",
ReduceBattleStatistics(S1, SetBattleMode("DM")).Mode == "DM" &&
!ReduceBattleStatistics(S1, SetReArmor(false)).IsReArmorEnabled &&
ReduceBattleStatistics(S1, SetRankRange(Range(10, 1))).RankRange ==
Range(10, 1) &&
!ReduceBattleStatistics(S1, SetDisplayClientInfo(false)).DisplayClientInfo
)
Check(
"切片 BattleStatistics:UpdateScore / UpdateFund 写表与资金",
ReduceBattleStatistics(S1, UpdateScore("A", 3)).Score ==
mapOf<Any?, Any?>("A" to 3) &&
ReduceBattleStatistics(S1, UpdateFund(0L, 9)).Fund == 9
)
Check(
"切片 BattleStatistics:SetRemainingTime 时长转秒",
ReduceBattleStatistics(S1, SetRemainingTime(90)).RemainingTimeInSec == 90
)
Check(
"切片 BattleStatistics:旗主设置 / 清空",
ReduceBattleStatistics(S1, SetFlagOrBallOwner(7L)).FlagOrBallOwner == 7L &&
ReduceBattleStatistics(
ReduceBattleStatistics(S1, SetFlagOrBallOwner(7L)),
ClearFlagOrBallOwner,
).FlagOrBallOwner == null
)
Check(
"切片 BattleStatistics:ShowStat / HideStat / 菜单两态",
ReduceBattleStatistics(S1, ShowStat).StatVisible &&
!ReduceBattleStatistics(S1, HideStat).StatVisible &&
!ReduceBattleStatistics(S1, CloseStatMenu).MenuVisible &&
ReduceBattleStatistics(S1, ToggleStatMenu).MenuVisible
)
val S2 = ReduceBattleStatistics(S1, BeginRound)
Check(
"切片 BattleStatistics:BeginRound 开回合并清分数",
S2.RoundStarted && !S2.StatVisible && !S2.MenuVisible &&
S2.Score.isEmpty() && S2.FlagOrBallOwner == null
)
Check(
"切片 BattleStatistics:ContinueBattle / ResetRoundState",
ReduceBattleStatistics(S2, ContinueBattle).IsContinueBattle &&
!ReduceBattleStatistics(S2, ResetRoundState).RoundStarted
)
Check(
"切片 BattleStatistics:BattleLoaded 两态与 levelsCount",
ReduceBattleStatistics(S2, MarkBattleLoaded).BattleLoaded &&
!ReduceBattleStatistics(S2, MarkBattleUnloaded).BattleLoaded &&
ReduceBattleStatistics(S2, InitBattleModeWithLevels(4))
.GameModeLevelsCount == 4
)
Check(
"切片 BattleStatistics:ResetBattleStatistics 整片复位",
ReduceBattleStatistics(S2, ResetBattleStatistics) == BattleStatisticsStateSlice()
)
Check(
"切片 BattleStatistics:无关动作不改状态",
ReduceBattleStatistics(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, MarkBattleLoaded)
Check(
"根:MarkBattleLoaded 只动 battleStatistics",
Root1.BattleStatistics.BattleLoaded &&
Root1.BattleSearchState == Root0.BattleSearchState
)
val W = RootWiring.ByField("battleStatistics")!!
Check(
"根:battleStatistics 委派到 jR / rr.fqr 且 22 个动作",
W.SliceClass == "jR" && W.Reducer == "rr.fqr" && W.Actions.size == 22
)
}
// 77 切片 reducer:SoundSettings(Det / Wet().jth,10 字段;4 个动作)
run {
val S0 = SoundSettingsStateSlice()
Check(
"切片 SoundSettings:默认音量与开关",
S0.MasterVolume == 100.0 && S0.UiVolume == 100.0 &&
S0.MusicVolume == 50.0 && S0.AmbientVolume == 50.0 &&
S0.SfxVolume == 50.0 && S0.HitmarkPlayerVolume == 50.0 &&
S0.HitmarkEnemyVolume == 50.0 && !S0.Mute &&
!S0.MuteInBackground && S0.IsEntranceSoundOn
)
val Custom = SoundSettingsStateSlice(
MasterVolume = 10.0, UiVolume = 20.0, MusicVolume = 30.0,
AmbientVolume = 40.0, SfxVolume = 25.0, HitmarkPlayerVolume = 15.0,
HitmarkEnemyVolume = 5.0, Mute = true, MuteInBackground = true,
IsEntranceSoundOn = false,
)
Check(
"切片 SoundSettings:ApplySoundSettings 整体替换",
ReduceSoundSettings(S0, ApplySoundSettings(Custom)) == Custom
)
Check(
"切片 SoundSettings:SetEntranceSoundSettings 整体替换",
ReduceSoundSettings(S0, SetEntranceSoundSettings(Custom)) == Custom
)
Check(
"切片 SoundSettings:EnableEntranceSound 只开入口音效",
ReduceSoundSettings(Custom, EnableEntranceSound)
.let { it.IsEntranceSoundOn && it.Mute }
)
Check(
"切片 SoundSettings:ResetSettingsFromStorage 不做 IO、保留状态",
ReduceSoundSettings(Custom, ResetSettingsFromStorage) == Custom
)
Check(
"切片 SoundSettings:无关动作不改状态",
ReduceSoundSettings(Custom, AddFrags(1)) == Custom
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, EnableEntranceSound)
Check(
"根:EnableEntranceSound 只动 soundSettings",
Root1.SoundSettings.IsEntranceSoundOn &&
Root1.GameSettings == Root0.GameSettings
)
val W = RootWiring.ByField("soundSettings")!!
Check(
"根:soundSettings 委派到 Det / Wet().jth 且 4 个动作",
W.SliceClass == "Det" && W.Reducer == "Wet().jth" &&
W.Actions.toSet() == setOf(
"ApplySoundSettings", "SetEntranceSoundSettings", "EnableEntranceSound",
"ResetSettingsFromStorage",
)
)
}
// 78 切片 reducer:GameSettings(c2 / g2().zss,6 字段;2 个动作)
run {
val S0 = GameSettingsStateSlice()
Check(
"切片 GameSettings:默认五个开关全开、padding 为 null",
S0.ShowDropZones && S0.ShowYourDrone && S0.ShowChat &&
S0.ShowNotifications && S0.ShowOtherDrone && S0.MobilePadding == null
)
val Custom = GameSettingsStateSlice(
ShowDropZones = false, ShowYourDrone = false, ShowChat = false,
ShowNotifications = false, ShowOtherDrone = false, MobilePadding = 32,
)
Check(
"切片 GameSettings:ApplyGameSettings 整体替换",
ReduceGameSettings(S0, ApplyGameSettings(Custom)) == Custom
)
Check(
"切片 GameSettings:ResetSettingsFromStorage 保留状态",
ReduceGameSettings(Custom, ResetSettingsFromStorage) == Custom
)
Check(
"切片 GameSettings:无关动作不改状态",
ReduceGameSettings(Custom, AddFrags(1)) == Custom
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ApplyGameSettings(Custom))
Check(
"根:ApplyGameSettings 只动 gameSettings",
Root1.GameSettings == Custom && Root1.KeyboardSettings == Root0.KeyboardSettings
)
val W = RootWiring.ByField("gameSettings")!!
Check(
"根:gameSettings 委派到 c2 / g2().zss 且 2 个动作",
W.SliceClass == "c2" && W.Reducer == "g2().zss" &&
W.Actions.toSet() == setOf("ApplyGameSettings", "ResetSettingsFromStorage")
)
}
// 79 切片 reducer:KeyboardSettings(Sut / bw.oth,1 字段;5 个动作)
run {
val S0 = KeyboardSettingsStateSlice()
Check("切片 KeyboardSettings:默认空绑定表", S0.Mapping.isEmpty())
val S1 = ReduceKeyboardSettings(S0, ChangeAction("fire", 0, "KeyF"))
Check(
"切片 KeyboardSettings:ChangeAction 写入 action→index→keyState",
S1.Mapping == mapOf<Any?, Any?>(
"fire" to mapOf<Any?, Any?>(0 to "KeyF"),
)
)
val S2 = ReduceKeyboardSettings(S1, ChangeAction("fire", 1, "KeyG"))
Check(
"切片 KeyboardSettings:同一 action 不同 index 追加绑定",
S2.Mapping == mapOf<Any?, Any?>(
"fire" to mapOf<Any?, Any?>(0 to "KeyF", 1 to "KeyG"),
)
)
Check(
"切片 KeyboardSettings:同一 keyState 从别的 action 上解绑",
ReduceKeyboardSettings(
ReduceKeyboardSettings(S0, ChangeAction("fire", 0, "KeyF")),
ChangeAction("jump", 0, "KeyF"),
).Mapping == mapOf<Any?, Any?>(
"fire" to emptyMap<Any?, Any?>(),
"jump" to mapOf<Any?, Any?>(0 to "KeyF"),
)
)
Check(
"切片 KeyboardSettings:RemoveAction 解绑一个 index",
ReduceKeyboardSettings(S2, RemoveAction("fire", 1)).Mapping ==
mapOf<Any?, Any?>("fire" to mapOf<Any?, Any?>(0 to "KeyF"))
)
Check(
"切片 KeyboardSettings:RemoveAction 对未知 action 是 no-op",
ReduceKeyboardSettings(S2, RemoveAction("nope", 0)) == S2
)
Check(
"切片 KeyboardSettings:ResetAction 保留状态(外部 helper)",
ReduceKeyboardSettings(S2, ResetAction("fire")) == S2
)
Check(
"切片 KeyboardSettings:两个默认复位都清空绑定表",
ReduceKeyboardSettings(S2, ResetSettingsFromStorage) ==
KeyboardSettingsStateSlice() &&
ReduceKeyboardSettings(S2, ResetKeyMappingFromDefaults) ==
KeyboardSettingsStateSlice()
)
Check(
"切片 KeyboardSettings:无关动作不改状态",
ReduceKeyboardSettings(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, RemoveAction("fire", 1))
Check(
"根:RemoveAction 只动 keyboardSettings",
Root1.KeyboardSettings == KeyboardSettingsStateSlice() &&
Root1.TouchscreenHudSettings == Root0.TouchscreenHudSettings
)
val W = RootWiring.ByField("keyboardSettings")!!
Check(
"根:keyboardSettings 委派到 Sut / bw.oth 且 5 个动作",
W.SliceClass == "Sut" && W.Reducer == "bw.oth" && W.Actions.size == 5
)
}
// 80 切片 reducer:TouchscreenHudSettings(j7 / N7().xsv,1 字段;3 个动作)
run {
val S0 = TouchscreenHudSettingsStateSlice()
Check("切片 TouchscreenHudSettings:默认空布局", S0.Mapping.isEmpty())
val Custom = TouchscreenHudSettingsStateSlice(mapOf<Any?, Any?>("fire" to 1))
Check(
"切片 TouchscreenHudSettings:ApplyTouchscreenHudSettings 整体替换",
ReduceTouchscreenHudSettings(S0, ApplyTouchscreenHudSettings(Custom)) == Custom
)
Check(
"切片 TouchscreenHudSettings:ResetTouchscreenHudMapping 恢复默认",
ReduceTouchscreenHudSettings(Custom, ResetTouchscreenHudMapping) ==
TouchscreenHudSettingsStateSlice()
)
Check(
"切片 TouchscreenHudSettings:ResetSettingsFromStorage 恢复默认",
ReduceTouchscreenHudSettings(Custom, ResetSettingsFromStorage) ==
TouchscreenHudSettingsStateSlice()
)
Check(
"切片 TouchscreenHudSettings:无关动作不改状态",
ReduceTouchscreenHudSettings(Custom, AddFrags(1)) == Custom
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ResetTouchscreenHudMapping)
Check(
"根:ResetTouchscreenHudMapping 只动 touchscreenHudSettings",
Root1.TouchscreenHudSettings == TouchscreenHudSettingsStateSlice() &&
Root1.SoundSettings == Root0.SoundSettings
)
val W = RootWiring.ByField("touchscreenHudSettings")!!
Check(
"根:touchscreenHudSettings 委派到 j7 / N7().xsv 且 3 个动作",
W.SliceClass == "j7" && W.Reducer == "N7().xsv" &&
W.Actions.toSet() == setOf(
"ApplyTouchscreenHudSettings", "ResetTouchscreenHudMapping",
"ResetSettingsFromStorage",
)
)
}
// 81 切片 reducer:GraphicsSettings(L2 / lc.isu,17 字段;1 个动作)
run {
val S0 = GraphicsSettingsStateSlice()
Check(
"切片 GraphicsSettings:默认值完整",
S0.QualityDetectByCardCompleted == null && !S0.Extended && S0.FpsLimit == 0 &&
S0.ShowFPSAndPing && S0.ShowSkybox && !S0.DynamicShadows &&
!S0.DynamicLighting && !S0.SoftParticles && S0.Fog && !S0.Antialiasing &&
S0.Vsync && S0.DecalsProfile == null && !S0.DustEnabled &&
S0.AnisotropicFilteringLevel == 0 && S0.TreesQuality == null &&
S0.EffectsQuality == null && S0.ResolutionScale == null
)
val Custom = GraphicsSettingsStateSlice(
QualityDetectByCardCompleted = true, Extended = true, FpsLimit = 144,
ShowFPSAndPing = false, ShowSkybox = false, DynamicShadows = true,
DynamicLighting = true, SoftParticles = true, Fog = false,
Antialiasing = true, Vsync = false, DecalsProfile = "high", DustEnabled = true,
AnisotropicFilteringLevel = 8, TreesQuality = "high",
EffectsQuality = "high", ResolutionScale = "1.0",
)
Check(
"切片 GraphicsSettings:ApplyGraphicsSettings 整体替换",
ReduceGraphicsSettings(S0, ApplyGraphicsSettings(Custom)) == Custom
)
Check(
"切片 GraphicsSettings:无关动作不改状态",
ReduceGraphicsSettings(Custom, AddFrags(1)) == Custom
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ApplyGraphicsSettings(Custom))
Check(
"根:ApplyGraphicsSettings 只动 graphicsSettings",
Root1.GraphicsSettings == Custom &&
Root1.ControlSettings == Root0.ControlSettings
)
val W = RootWiring.ByField("graphicsSettings")!!
Check(
"根:graphicsSettings 委派到 L2 / lc.isu",
W.SliceClass == "L2" && W.Reducer == "lc.isu" &&
W.Actions == listOf("ApplyGraphicsSettings")
)
}
// 82 切片 reducer:ControlSettings(i2 / _2().ess,7 字段;2 个动作)
run {
val S0 = ControlSettingsStateSlice()
Check(
"切片 ControlSettings:默认值完整",
S0.AvailableInputModes.isEmpty() && S0.InputMode == null &&
!S0.InversedBackwardMoving && !S0.InvertShaftVerticalAxis &&
S0.Mouse == ControlMouseSettings(50.0, true) &&
S0.Gamepad == ControlGamepadSettings(true, 13, 13) &&
S0.Touchscreen ==
ControlTouchscreenSettings(50.0, true, false, true, true, true, true)
)
val Custom = ControlSettingsStateSlice(
AvailableInputModes = setOf<Any?>("mouse", "touch"),
InputMode = "touch",
InversedBackwardMoving = true,
InvertShaftVerticalAxis = true,
Mouse = ControlMouseSettings(30.0, false),
Gamepad = ControlGamepadSettings(false, 20, 25),
Touchscreen = ControlTouchscreenSettings(70.0, false, true, false, false, false, false),
)
Check(
"切片 ControlSettings:ApplyControlSettings 整体替换",
ReduceControlSettings(S0, ApplyControlSettings(Custom)) == Custom
)
Check(
"切片 ControlSettings:ResetSettingsFromStorage 保留状态",
ReduceControlSettings(Custom, ResetSettingsFromStorage) == Custom
)
Check(
"切片 ControlSettings:无关动作不改状态",
ReduceControlSettings(Custom, AddFrags(1)) == Custom
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ApplyControlSettings(Custom))
Check(
"根:ApplyControlSettings 只动 controlSettings",
Root1.ControlSettings == Custom && Root1.BattleSettings == Root0.BattleSettings
)
val W = RootWiring.ByField("controlSettings")!!
Check(
"根:controlSettings 委派到 i2 / _2().ess 且 2 个动作",
W.SliceClass == "i2" && W.Reducer == "_2().ess" &&
W.Actions.toSet() == setOf("ApplyControlSettings", "ResetSettingsFromStorage")
)
}
// 83 切片 reducer:BattleSettings(W0 / X0().esq,11 字段;2 个动作)
run {
val S0 = BattleSettingsStateSlice()
Check(
"切片 BattleSettings:默认值完整",
S0.ShowAimHelperDot && S0.ShowDamage && S0.ShowTankPointers &&
S0.ShowButtonLabels && S0.AutoSuicide && S0.ShowStatusName &&
!S0.IsCenteredLayout && S0.BattleUISize == 50.0 &&
S0.AimhelperDotOpacity == 50.0 && S0.AimhelperDotScale == 1.0 &&
S0.AutoSupply
)
val Custom = BattleSettingsStateSlice(
ShowAimHelperDot = false, ShowDamage = false, ShowTankPointers = false,
ShowButtonLabels = false, AutoSuicide = false, ShowStatusName = false,
IsCenteredLayout = true, BattleUISize = 80.0, AimhelperDotOpacity = 20.0,
AimhelperDotScale = 2.0, AutoSupply = false,
)
Check(
"切片 BattleSettings:ApplyBattleSettings 整体替换",
ReduceBattleSettings(S0, ApplyBattleSettings(Custom)) == Custom
)
Check(
"切片 BattleSettings:ResetSettingsFromStorage 保留状态",
ReduceBattleSettings(Custom, ResetSettingsFromStorage) == Custom
)
Check(
"切片 BattleSettings:无关动作不改状态",
ReduceBattleSettings(Custom, AddFrags(1)) == Custom
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, ApplyBattleSettings(Custom))
Check(
"根:ApplyBattleSettings 只动 battleSettings",
Root1.BattleSettings == Custom && Root1.GraphicsSettings == Root0.GraphicsSettings
)
val W = RootWiring.ByField("battleSettings")!!
Check(
"根:battleSettings 委派到 W0 / X0().esq 且 2 个动作",
W.SliceClass == "W0" && W.Reducer == "X0().esq" &&
W.Actions.toSet() == setOf("ApplyBattleSettings", "ResetSettingsFromStorage")
)
}
// 84 切片 reducer:ContractState(Ont / xv.bti,3 字段;7 个动作)
run {
fun Contract(Id: Any?, Progress: Any? = 0, Rewards: List<Any?> = emptyList()) =
ContractEntry(ContractId = Id, Progress = Progress, Rewards = Rewards)
val S0 = ContractStateSlice()
Check(
"切片 ContractState:默认值完整",
S0.ActiveContractId == null && S0.Contracts.isEmpty() &&
S0.LastBattleProgress == 0
)
val S1 = ReduceContractState(S0, SetActiveContract("c1"))
Check("切片 ContractState:SetActiveContract 写 id", S1.ActiveContractId == "c1")
val S2 = ReduceContractState(S1, SetContract(listOf(Contract("c1"), Contract("c2"))))
Check(
"切片 ContractState:SetContract 按合同 id 合并",
S2.Contracts.keys == setOf<Any?>("c1", "c2")
)
Check(
"切片 ContractState:UpdateProgress 复制该合同",
ReduceContractState(S2, UpdateProgress("c1", 5))
.let { (it.Contracts["c1"] as ContractEntry).Progress == 5 }
)
Check(
"切片 ContractState:SetBattleProgress 写战斗进度",
ReduceContractState(S2, SetBattleProgress(7)).LastBattleProgress == 7
)
val S3 = ReduceContractState(
ReduceContractState(S2, SetContract(listOf(Contract("c2", 1, listOf("r"))))),
ClaimContractReward("c2"),
)
Check(
"切片 ContractState:ClaimContractReward 清空该合同奖励",
(S3.Contracts["c2"] as ContractEntry).Rewards.isEmpty() &&
(S3.Contracts["c1"] as ContractEntry).Rewards.isEmpty()
)
Check(
"切片 ContractState:RemoveCompletedContracts 只留还有奖励的",
ReduceContractState(
ReduceContractState(
S2,
SetContract(listOf(Contract("c1", 2, listOf("r")))),
),
RemoveCompletedContracts,
).Contracts.keys == setOf<Any?>("c1")
)
Check(
"切片 ContractState:InitBattle 只把战斗进度归零",
ReduceContractState(S3, InitBattle()).let {
it.LastBattleProgress == 0 && it.Contracts.keys == setOf<Any?>("c1", "c2")
}
)
Check(
"切片 ContractState:无关动作不改状态",
ReduceContractState(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetActiveContract("x"))
Check(
"根:SetActiveContract 只动 contractState",
Root1.ContractState.ActiveContractId == "x" &&
Root1.BattleSettings == Root0.BattleSettings
)
val W = RootWiring.ByField("contractState")!!
Check(
"根:contractState 委派到 Ont / xv.bti 且 7 个动作",
W.SliceClass == "Ont" && W.Reducer == "xv.bti" && W.Actions.size == 7
)
}
// 85 切片 reducer:TankBuilder(brt / wp.ati,6 字段;4 个动作)
run {
val S0 = TankBuilderStateSlice()
Check(
"切片 TankBuilder:默认值完整",
S0.Hulls.isEmpty() && S0.Weapons.isEmpty() && S0.Paints.isEmpty() &&
S0.PaintInfo.isEmpty() && S0.PartInfo.isEmpty() && S0.Kode.isEmpty()
)
val S1 = ReduceTankBuilder(S0, SetKode(listOf("k1")))
Check("切片 TankBuilder:SetKode 写 kode", S1.Kode == listOf<Any?>("k1"))
val S2 = ReduceTankBuilder(
S1,
SetParts(
parts = listOf(
TankBuilderPart("hull1", listOf("w1")),
TankBuilderPart("hull2", listOf("w2", "w3")),
),
paints = listOf("p1"),
),
)
Check(
"切片 TankBuilder:SetParts 拆出 hulls / weapons / paints",
S2.Hulls == listOf<Any?>("hull1", "hull2") &&
S2.Weapons == mapOf<Any?, Any?>(
0 to listOf("w1"),
1 to listOf("w2", "w3"),
) &&
S2.Paints == listOf<Any?>("p1")
)
val S3 = ReduceTankBuilder(
S2,
SetPartsInfo(
partsInfo = listOf(TankBuilderPartInfo("part1", "info1")),
paintsInfo = listOf(TankBuilderPaintInfo("paint1", "info2")),
),
)
Check(
"切片 TankBuilder:SetPartsInfo 按各自 id 建索引",
S3.PartInfo == mapOf<Any?, Any?>("part1" to "info1") &&
S3.PaintInfo == mapOf<Any?, Any?>("paint1" to "info2")
)
Check(
"切片 TankBuilder:ResetTankBuilder 整片复位",
ReduceTankBuilder(S3, ResetTankBuilder) == TankBuilderStateSlice()
)
Check(
"切片 TankBuilder:无关动作不改状态",
ReduceTankBuilder(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetKode(listOf("x")))
Check(
"根:SetKode 只动 tankBuilderState",
Root1.TankBuilderState.Kode == listOf<Any?>("x") &&
Root1.ContractState == Root0.ContractState
)
val W = RootWiring.ByField("tankBuilderState")!!
Check(
"根:tankBuilderState 委派到 brt / wp.ati 且 4 个动作",
W.SliceClass == "brt" && W.Reducer == "wp.ati" &&
W.Actions.toSet() == setOf(
"SetKode", "SetParts", "SetPartsInfo", "ResetTankBuilder",
)
)
}
// 86 切片 reducer:BattleMessage(FE / vT().rqb,1 字段;7 个动作)
run {
val S0 = BattleMessageStateSlice()
Check("切片 BattleMessage:默认空消息表", S0.Messages.isEmpty())
val S1 = ReduceBattleMessage(S0, DoAddTextMessage("info", 100L, "hello"))
Check(
"切片 BattleMessage:DoAddTextMessage 追加 TextMessage",
S1.Messages.size == 1 &&
(S1.Messages[0] as TextMessage).Text == "hello" &&
(S1.Messages[0] as TextMessage).HideTimeMs == 100L
)
val S2 = ReduceBattleMessage(
ReduceBattleMessage(
ReduceBattleMessage(S1, DoAddLocalizedMessage("sys", 200L, "KILL", listOf("a"))),
DoAddLocalizedUserMessage("enemy", "user", 300L, "MSG", "uid1"),
),
DoAddLocalizedTeamMessage("team", 400L, "TEAM", "red"),
)
Check(
"切片 BattleMessage:四种消息按各自字段入表",
S2.Messages.size == 4 &&
(S2.Messages[1] as LocalizedMessage).Content == "KILL" &&
(S2.Messages[1] as LocalizedMessage).Arguments == listOf<Any?>("a") &&
(S2.Messages[2] as LocalizedUserMessage).UserTeamRelation == "enemy" &&
(S2.Messages[2] as LocalizedUserMessage).UserUid == "uid1" &&
(S2.Messages[3] as LocalizedTeamMessage).TeamRelation == "red"
)
val S3 = ReduceBattleMessage(S2, HideMessage(100L))
Check(
"切片 BattleMessage:HideMessage 只把匹配 hideTimeMs 的标隐藏",
(S3.Messages[0] as TextMessage).IsHidden &&
!(S3.Messages[1] as LocalizedMessage).IsHidden
)
val S4 = ReduceBattleMessage(S3, RemoveMessage(200L))
Check(
"切片 BattleMessage:RemoveMessage 删掉匹配的那条",
S4.Messages.size == 3 &&
S4.Messages.none { (it as BattleMessageEntry).HideTimeMs == 200L }
)
Check(
"切片 BattleMessage:ResetBattleMessages 整片复位",
ReduceBattleMessage(S4, ResetBattleMessages) == BattleMessageStateSlice()
)
Check(
"切片 BattleMessage:无关动作不改状态",
ReduceBattleMessage(S4, AddFrags(1)) == S4
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, DoAddTextMessage("info", 1L, "x"))
Check(
"根:DoAddTextMessage 只动 battleMessage",
Root1.BattleMessage.Messages.size == 1 &&
Root1.TankBuilderState == Root0.TankBuilderState
)
val W = RootWiring.ByField("battleMessage")!!
Check(
"根:battleMessage 委派到 FE / vT().rqb 且 7 个动作",
W.SliceClass == "FE" && W.Reducer == "vT().rqb" && W.Actions.size == 7
)
}
// 87 切片 reducer:Maps(OA / Oi.sql,1 字段;AddMap)
run {
fun Info(Theme: String): MapInfoCC = MapInfoCC(
defaultTheme = "summer", enabled = true, mapId = 1L, mapName = "Desert",
matchmakingMark = true, maxPeople = 16, preview = null, rankLimit = "1-5",
min = "1", supportedModes = listOf("DM"), theme = Theme,
)
val S0 = MapsStateSlice()
Check("切片 Maps:默认空表", S0.MapDescriptionsById.isEmpty())
val S1 = ReduceMaps(S0, AddMap(0L, Info("summer")))
val Desc1 = S1.MapDescriptionsById[1L] as MapsDescriptionEntry
Check(
"切片 Maps:AddMap 新建地图描述并写入模式条目",
Desc1.Name == "Desert" && Desc1.DefaultTheme == "summer" &&
Desc1.Themes.keys == setOf<Any?>("summer") &&
(Desc1.Themes["summer"] as MapsThemeEntry).MapName == "Desert"
)
val S2 = ReduceMaps(S1, AddMap(1L, Info("winter")))
val Desc2 = S2.MapDescriptionsById[1L] as MapsDescriptionEntry
Check(
"切片 Maps:同地图新增主题是合并而不是覆盖",
Desc2.Themes.keys == setOf<Any?>("summer", "winter") &&
(Desc2.Themes["winter"] as MapsThemeEntry).Theme == "winter"
)
Check("切片 Maps:无关动作不改状态", ReduceMaps(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddMap(0L, Info("summer")))
Check(
"根:AddMap 只动 maps",
Root1.Maps.MapDescriptionsById.keys == setOf<Any?>(1L) &&
Root1.BattleMessage == Root0.BattleMessage
)
val W = RootWiring.ByField("maps")!!
Check(
"根:maps 委派到 OA / Oi.sql",
W.SliceClass == "OA" && W.Reducer == "Oi.sql" &&
W.Actions == listOf("AddMap")
)
}
// 88 切片 reducer:BattleChat(Q6 / Hf.wtg,6 字段;6 个动作)
run {
fun Msg(Id: Int): ChatMessage = ChatMessage(
addressMode = "", battleLinks = emptyList(), channel = "battle", id = Id,
links = emptyList(), messageType = "text", sourceUser = "u", ip = "",
uid = null, userId = "1", targetUser = null, text = "m$Id", timePassedInSec = 0,
)
val S0 = BattleChatStateSlice()
Check(
"切片 BattleChat:默认两个频道开、未加载",
S0.CommonBattleChatEnabled && S0.TeamBattleChatEnabled && !S0.Loaded &&
S0.Messages.isEmpty() && !S0.IsInputActive && !S0.IsHistoryOpen
)
val S1 = ReduceBattleChat(S0, ChatLoaded(false, true))
Check(
"切片 BattleChat:ChatLoaded 写频道开关并置 loaded",
!S1.CommonBattleChatEnabled && S1.TeamBattleChatEnabled && S1.Loaded
)
val S2 = ReduceBattleChat(S1, AddMessage(Msg(1)))
Check(
"切片 BattleChat:AddMessage 追加消息",
S2.Messages.size == 1 && (S2.Messages[0] as ChatMessage).id == 1
)
var S3 = S2
for (I in 2..130) S3 = ReduceBattleChat(S3, AddMessage(Msg(I)))
Check(
"切片 BattleChat:超过 120 条只保留最后 120",
S3.Messages.size == 120 &&
(S3.Messages.last() as ChatMessage).id == 130
)
Check(
"切片 BattleChat:输入框开关两态",
ReduceBattleChat(S3, OpenBattleChatInput).IsInputActive &&
!ReduceBattleChat(S3, CloseBattleChatInput).IsInputActive
)
Check(
"切片 BattleChat:ToggleBattleChatHistory 取反",
ReduceBattleChat(S3, ToggleBattleChatHistory).IsHistoryOpen
)
Check(
"切片 BattleChat:ResetBattleChat 整片复位",
ReduceBattleChat(S3, ResetBattleChat) == BattleChatStateSlice()
)
Check(
"切片 BattleChat:无关动作不改状态",
ReduceBattleChat(S3, AddFrags(1)) == S3
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, OpenBattleChatInput)
Check(
"根:OpenBattleChatInput 只动 battleChat",
Root1.BattleChat.IsInputActive && Root1.Maps == Root0.Maps
)
val W = RootWiring.ByField("battleChat")!!
Check(
"根:battleChat 委派到 Q6 / Hf.wtg 且 6 个动作",
W.SliceClass == "Q6" && W.Reducer == "Hf.wtg" &&
W.Actions.size == 6
)
}
// 89 切片 reducer:RemoteUserDataStorage($Q / vh.isl,1 字段;8 个动作)
run {
val S0 = RemoteUserDataStorageStateSlice()
Check("切片 RemoteUserDataStorage:默认空缓存", S0.UserIdToData.isEmpty())
val S1 = ReduceRemoteUserDataStorage(S0, UpdateUids(mapOf("1" to "uid1")))
Check(
"切片 RemoteUserDataStorage:UpdateUids 建条目并写 uid",
(S1.UserIdToData["1"] as RemoteUserData).uid == "uid1" &&
(S1.UserIdToData["1"] as RemoteUserData).userId == "1"
)
val S2 = ReduceRemoteUserDataStorage(S1, UpdateRanks(mapOf("1" to 5)))
Check(
"切片 RemoteUserDataStorage:UpdateRanks 就地更新",
(S2.UserIdToData["1"] as RemoteUserData).rank == 5 &&
(S2.UserIdToData["1"] as RemoteUserData).uid == "uid1"
)
Check(
"切片 RemoteUserDataStorage:UpdateClanTags / UpdateOnlineStatus",
(ReduceRemoteUserDataStorage(S2, UpdateClanTags(mapOf("1" to "TAG")))
.UserIdToData["1"] as RemoteUserData).clanTag == "TAG" &&
(ReduceRemoteUserDataStorage(S2, UpdateOnlineStatus(mapOf("1" to App.State.OnlineNotifierData("online"))))
.UserIdToData["1"] as RemoteUserData).onlineStatus == "online"
)
val S3 = ReduceRemoteUserDataStorage(
S2,
UserEnterBattle(mapOf("1" to BattleNotifierData(true, true, ""))),
)
Check(
"切片 RemoteUserDataStorage:UserEnterBattle 写 battle",
(S3.UserIdToData["1"] as RemoteUserData).battle is BattleNotifierData
)
val S4 = ReduceRemoteUserDataStorage(S3, UserLeaveBattle("1"))
Check(
"切片 RemoteUserDataStorage:UserLeaveBattle 保留组标记、清战斗位",
(S4.UserIdToData["1"] as RemoteUserData).battle ==
BattleNotifierData(false, true, "")
)
Check(
"切片 RemoteUserDataStorage:UserLeaveGroup 清空 battle",
(ReduceRemoteUserDataStorage(S4, UserLeaveGroup("1"))
.UserIdToData["1"] as RemoteUserData).battle == null
)
Check(
"切片 RemoteUserDataStorage:未知用户不新建条目",
ReduceRemoteUserDataStorage(S0, UserLeaveBattle("9")).UserIdToData.isEmpty()
)
Check(
"切片 RemoteUserDataStorage:无关动作不改状态",
ReduceRemoteUserDataStorage(S4, AddFrags(1)) == S4
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateUids(mapOf("2" to "u2")))
Check(
"根:UpdateUids 只动 remoteUserDataStorage",
(Root1.RemoteUserDataStorage.UserIdToData["2"] as RemoteUserData).uid == "u2" &&
Root1.BattleChat == Root0.BattleChat
)
val W = RootWiring.ByField("remoteUserDataStorage")!!
Check(
"根:remoteUserDataStorage 委派到 \$Q / vh.isl 且 8 个动作",
W.SliceClass == "\$Q" && W.Reducer == "vh.isl" && W.Actions.size == 8
)
}
// 90 切片 reducer:Matchmaking(TH / JH().qrx,19 字段;9 个动作)
run {
val S0 = MatchmakingStateSlice()
Check(
"切片 Matchmaking:默认值完整",
S0.MatchmakingModeRanks.isEmpty() && S0.SelectedMatchmakingMode == null &&
S0.DisabledModes.isEmpty() && !S0.MatchmakingAndLoadingBattleInProgress &&
!S0.StartLoadingBattle && !S0.HolidayEnabled && S0.HolidayTitle == "" &&
S0.HolidayIcon == null && S0.EventBigTile == null &&
S0.HolidayDescription == "" && S0.MinRankForProBattle == Int.MAX_VALUE &&
S0.AvgWaitTimeInSeconds == 0 && S0.Mode == null && S0.MaxUsersInGroup == 3 &&
!S0.GroupsInEventMode && S0.EventEndDate == null &&
S0.BorderColorForEventButton == null && !S0.RequestSent &&
S0.RankIndexToMinGS.isEmpty()
)
val S1 = ReduceMatchmaking(S0, Configure(
modes = listOf(MatchmakingModeRank("dm", 1)),
disabledModes = listOf("tdm"),
holidayEnabled = true,
holidayTitle = "NY",
holidayIcon = "icon",
eventBigTile = "tile",
holidayDescription = "desc",
minRankForProBattle = 5,
maxUsersInGroup = 4,
groupsInEventMode = true,
eventEndDate = "date",
borderColorForEventButton = "#fff",
))
Check(
"切片 Matchmaking:Configure 写 12 个字段",
S1.MatchmakingModeRanks == listOf(MatchmakingModeRank("dm", 1)) &&
S1.DisabledModes == setOf<Any?>("tdm") && S1.HolidayEnabled &&
S1.HolidayTitle == "NY" && S1.HolidayIcon == "icon" &&
S1.EventBigTile == "tile" && S1.HolidayDescription == "desc" &&
S1.MinRankForProBattle == 5 && S1.MaxUsersInGroup == 4 &&
S1.GroupsInEventMode && S1.EventEndDate == "date" &&
S1.BorderColorForEventButton == "#fff"
)
Check(
"切片 Matchmaking:SelectMatchmakingMode 写选中模式",
ReduceMatchmaking(S1, SelectMatchmakingMode("dm")).SelectedMatchmakingMode == "dm"
)
Check(
"切片 Matchmaking:Play 只置 requestSent",
ReduceMatchmaking(S1, Play(true, null, null)).RequestSent
)
val S2 = ReduceMatchmaking(S1, WasRegistered(42, "dm"))
Check(
"切片 Matchmaking:WasRegistered 写等待时间 / 模式并清 requestSent",
S2.MatchmakingAndLoadingBattleInProgress &&
S2.AvgWaitTimeInSeconds == 42 && S2.Mode == "dm" && !S2.RequestSent
)
Check(
"切片 Matchmaking:EnterToBattleApply 同时置两个进行中位",
ReduceMatchmaking(S1, EnterToBattleApply(7L)).let {
it.MatchmakingAndLoadingBattleInProgress && it.StartLoadingBattle
}
)
Check(
"切片 Matchmaking:ResetMatchmakingProgress 清三个位",
ReduceMatchmaking(S2, ResetMatchmakingProgress).let {
!it.MatchmakingAndLoadingBattleInProgress && !it.StartLoadingBattle &&
!it.RequestSent
}
)
Check(
"切片 Matchmaking:读条开关两态",
ReduceMatchmaking(S1, StartLoadingBattle).StartLoadingBattle &&
!ReduceMatchmaking(S1, CancelLoadingBattle).StartLoadingBattle
)
Check(
"切片 Matchmaking:SetRankIndexToMinGS 写表",
ReduceMatchmaking(S1, SetRankIndexToMinGS(mapOf("1" to 100)))
.RankIndexToMinGS == mapOf("1" to 100)
)
Check(
"切片 Matchmaking:无关动作不改状态",
ReduceMatchmaking(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, StartLoadingBattle)
Check(
"根:StartLoadingBattle 只动 matchmaking",
Root1.Matchmaking.StartLoadingBattle &&
Root1.RemoteUserDataStorage == Root0.RemoteUserDataStorage
)
val W = RootWiring.ByField("matchmaking")!!
Check(
"根:matchmaking 委派到 TH / JH().qrx 且 9 个动作",
W.SliceClass == "TH" && W.Reducer == "JH().qrx" && W.Actions.size == 9
)
}
// 91 切片 reducer:BattlesSelect(fA / Mi.vqk,15 字段;20 个动作)
run {
fun Params(): BattleCreateParameters = BattleCreateParameters(
battleMode = "DM", battleModeForMatchmaking = null,
battleOptions = listOf("VALUABLE"), crossPlayMode = "", formatMode = "fmt",
limits = "10", timeLimitInSec = "600", mapId = 1L, maxPeopleCount = 8,
name = "b1", proBattle = true, rankRange = "1-5", min = "1", theme = "2",
)
val S0 = BattlesSelectStateSlice()
Check("切片 BattlesSelect:默认值完整", !S0.Loaded && S0.Battles.isEmpty())
val S1 = ReduceBattlesSelect(S0, AddBattle(1L, 99L, Params(), "LOW"))
val Battle = S1.Battles[1L] as BattleParams
Check(
"切片 BattlesSelect:AddBattle 建列表项与四个子表",
Battle.name == "b1" && Battle.mapId == 1L && Battle.proBattle &&
Battle.parkourMode == false && S1.Users[1L] == emptyMap<Any?, Any?>() &&
S1.Score[1L] == emptyMap<Any?, Any?>() && S1.Creators[1L] == 99L
)
val S2 = ReduceBattlesSelect(S1, RoundStarted(1L, 60))
Check(
"切片 BattlesSelect:RoundStarted 写回合计时并清分数",
S2.RoundEndTimes[1L] == 60 && S2.Score[1L] == emptyMap<Any?, Any?>()
)
val S3 = ReduceBattlesSelect(S2, UpdateTeamScore(1L, "RED", 3))
Check(
"切片 BattlesSelect:UpdateTeamScore 写队伍分",
(S3.Score[1L] as Map<Any?, Any?>)["RED"] == 3
)
val S4 = ReduceBattlesSelect(
S3,
AddUsers(1L, listOf(BattleInfoUser(0L, 0, false, false, 7L)), "RED"),
)
Check(
"切片 BattlesSelect:AddUsers 写用户 / 队伍 / 个人分",
(S4.Users[1L] as Map<Any?, Any?>).keys == setOf<Any?>(7L) &&
(S4.UsersTeam[1L] as Map<Any?, Any?>)[7L] == "RED" &&
S4.UserScore[7L] == 0
)
val S5 = ReduceBattlesSelect(
ReduceBattlesSelect(S4, UpdateUserScore(1L, 7L, 9)),
UpdateUserSuspicion(1L, 7L, true, false),
)
Check(
"切片 BattlesSelect:个人分与可疑标记",
S5.UserScore[7L] == 9 && S5.UserSuspicion[7L] == true &&
S5.UserSuspicionByIp[7L] == false
)
val S6 = ReduceBattlesSelect(S5, SwapTeam(1L))
Check(
"切片 BattlesSelect:SwapTeam 换队并把该用户分数归零",
(S6.UsersTeam[1L] as Map<Any?, Any?>)[7L] == "BLUE" && S6.UserScore[7L] == 0
)
Check(
"切片 BattlesSelect:ChangeName / UpdateSuspicion",
(ReduceBattlesSelect(S1, ChangeName(1L, "renamed"))
.Battles[1L] as BattleParams).name == "renamed" &&
(ReduceBattlesSelect(S1, UpdateSuspicion(1L, "HIGH"))
.Battles[1L] as BattleParams).suspicionLevel == "HIGH"
)
Check(
"切片 BattlesSelect:EnterToBattleInfo / UpdateFund / LimitReached",
ReduceBattlesSelect(S1, EnterToBattleInfo(1L))
.AlreadySeeBattleInfo == setOf<Any?>(1L) &&
ReduceBattlesSelect(S1, UpdateFund(1L, 20)).Fund[1L] == 20 &&
ReduceBattlesSelect(S1, LimitReached(true)).IsLimitReached
)
Check(
"切片 BattlesSelect:InitProBattleRewardLimit / SetReEnterProtectBattleId",
ReduceBattlesSelect(
S1,
InitProBattleRewardLimit(ProBattleRewardLimitData(1, 2, 3, 4)),
).ProLimit == ProBattleRewardLimitData(1, 2, 3, 4) &&
ReduceBattlesSelect(S1, SetReEnterProtectBattleId(5L))
.ReEnterProtectBattleId == 5L
)
Check(
"切片 BattlesSelect:RemoveUser 清四个个人表",
ReduceBattlesSelect(S5, RemoveUser(1L, 7L)).let {
(it.Users[1L] as Map<Any?, Any?>).isEmpty() &&
it.UserScore[7L] == null && it.UserSuspicion[7L] == null
}
)
Check(
"切片 BattlesSelect:RemoveBattle 清四个战斗表",
ReduceBattlesSelect(
ReduceBattlesSelect(S5, EnterToBattleInfo(1L)),
RemoveBattle(1L),
).let {
it.Battles.isEmpty() && it.Users.isEmpty() && it.Creators.isEmpty() &&
it.AlreadySeeBattleInfo.isEmpty()
}
)
Check(
"切片 BattlesSelect:RoundFinish 删计时与资金",
ReduceBattlesSelect(ReduceBattlesSelect(S2, UpdateFund(1L, 5)), RoundFinish(1L))
.let { it.RoundEndTimes[1L] == null && it.Fund[1L] == null }
)
Check(
"切片 BattlesSelect:MarkBattlesLoaded / ResetBattlesSelect",
ReduceBattlesSelect(S0, MarkBattlesLoaded).Loaded &&
ReduceBattlesSelect(S5, ResetBattlesSelect) == BattlesSelectStateSlice()
)
Check(
"切片 BattlesSelect:无关动作不改状态",
ReduceBattlesSelect(S5, AddFrags(1)) == S5
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, MarkBattlesLoaded)
Check(
"根:MarkBattlesLoaded 只动 battlesSelect",
Root1.BattlesSelect.Loaded && Root1.Matchmaking == Root0.Matchmaking
)
val W = RootWiring.ByField("battlesSelect")!!
Check(
"根:battlesSelect 委派到 fA / Mi.vqk 且 20 个动作",
W.SliceClass == "fA" && W.Reducer == "Mi.vqk" && W.Actions.size == 20
)
}
// 92 切片 reducer:BattleUsers(uP / sr.hqs,13 字段;18 个动作)
run {
fun User(Id: Long): RemoteUserData = RemoteUserData(
userId = Id, uid = "u$Id", rank = 5, hasPremium = true,
clanTag = "T", onlineStatus = "online", battle = null,
)
val TankInfo = TankInfoCC(1L, null, 2L, 3L, null, 4L)
val Resistance = TankResistance(10, "fire")
val S0 = BattleUsersStateSlice()
Check("切片 BattleUsers:默认值完整", S0.OnlineUsers.isEmpty() && S0.Stats.isEmpty())
val S1 = ReduceBattleUsers(S0, AddBattleUser(User(1L), "RED"))
Check(
"切片 BattleUsers:AddBattleUser 写七张表",
S1.OnlineUsers == setOf<Any?>(1L) && S1.Uids[1L] == "u1" &&
S1.Ranks[1L] == 5 && S1.ClanTags[1L] == "T" &&
S1.UsersPremium[1L] == true && S1.Teams[1L] == "RED" &&
S1.Stats[1L] == User(1L)
)
Check(
"切片 BattleUsers:AddBattleUsers 批量加入",
ReduceBattleUsers(S1, AddBattleUsers(listOf(User(2L), User(3L)), "BLUE"))
.OnlineUsers == setOf<Any?>(1L, 2L, 3L)
)
Check(
"切片 BattleUsers:UpdateUserRank / UpdateUsersClanTag",
ReduceBattleUsers(S1, UpdateUserRank(1L, 9)).Ranks[1L] == 9 &&
ReduceBattleUsers(S1, UpdateUsersClanTag(mapOf(1L to "X")))
.ClanTags[1L] == "X"
)
Check(
"切片 BattleUsers:两种 stats 写入",
ReduceBattleUsers(S1, UpdateUserScoreKillsAndDeaths(UserStat(0, 0, 0, 0, 4L)))
.Stats[4L] == UserStat(0, 0, 0, 0, 4L) &&
ReduceBattleUsers(S1, UpdateUsersScoreKillsAndDeaths(listOf(UserStat(0, 0, 0, 0, 5L))))
.Stats[5L] == UserStat(0, 0, 0, 0, 5L)
)
Check(
"切片 BattleUsers:SetGearScore / SetTankResistance",
ReduceBattleUsers(S1, SetGearScore(1L, 100)).GearScores[1L] == 100 &&
ReduceBattleUsers(S1, SetTankResistance(1L, listOf(Resistance)))
.TankResistance[1L] == listOf(Resistance)
)
Check(
"切片 BattleUsers:RemoveOnlineUser / SetUserPremium",
ReduceBattleUsers(S1, RemoveOnlineUser(1L)).OnlineUsers.isEmpty() &&
ReduceBattleUsers(S1, SetUserPremium(1L, false)).UsersPremium[1L] == false
)
Check(
"切片 BattleUsers:可疑用户开关与整体替换",
ReduceBattleUsers(S1, UpdateUserSuspiciousState(1L, true, false))
.let { it.SuspiciousUsers == setOf<Any?>(1L) && it.SuspiciousByIpUsers.isEmpty() } &&
ReduceBattleUsers(S1, InitSuspiciousState(listOf(2L), listOf(3L)))
.let {
it.SuspiciousUsers == setOf<Any?>(2L) &&
it.SuspiciousByIpUsers == setOf<Any?>(3L)
}
)
Check(
"切片 BattleUsers:等级两臂",
ReduceBattleUsers(S1, InitUserLevels(mapOf(1L to 3))).TankLevels[1L] == 3 &&
ReduceBattleUsers(S1, UpdateUserLevel(1L, 4)).TankLevels[1L] == 4
)
Check(
"切片 BattleUsers:坦克信息写入与移除",
ReduceBattleUsers(S1, SetTankInfo(1L, TankInfo)).TankInfo[1L] == TankInfo &&
ReduceBattleUsers(S1, SetTankInfo(1L, TankInfo)).let {
ReduceBattleUsers(it, RemoveTankInfo(1L)).TankInfo.isEmpty()
}
)
Check(
"切片 BattleUsers:枚举迁移是 no-op、复位清空",
ReduceBattleUsers(S1, MigrateBattleUserTeams) == S1 &&
ReduceBattleUsers(S1, ResetBattleUsers) == BattleUsersStateSlice()
)
Check(
"切片 BattleUsers:无关动作不改状态",
ReduceBattleUsers(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddBattleUser(User(7L), "RED"))
Check(
"根:AddBattleUser 只动 battleUsers",
Root1.BattleUsers.OnlineUsers == setOf<Any?>(7L) &&
Root1.BattlesSelect == Root0.BattlesSelect
)
val W = RootWiring.ByField("battleUsers")!!
Check(
"根:battleUsers 委派到 uP / sr.hqs 且 18 个动作",
W.SliceClass == "uP" && W.Reducer == "sr.hqs" && W.Actions.size == 18
)
}
// 93 切片 reducer:Challenges(CZ / nh.hsj,4 字段;9 个动作)
run {
fun ChallengeOf(Version: Any?): Challenge = Challenge(
version = Version, endTime = null, tiers = emptyList<Any?>(),
battlePassFlavor = 1, mainPrizeImage = 0L, background = 0L,
tierReward = null, takenFreeRewardIndex = 0,
takenBattlePassRewardIndex = 0,
)
val S0 = ChallengesStateSlice()
Check("切片 Challenges:默认值完整", S0.Stars.isEmpty() && S0.Challenge.isEmpty())
val S1 = ReduceChallenges(S0, UpdateStars(mapOf("a" to 3)))
Check("切片 Challenges:UpdateStars 整体替换", S1.Stars == mapOf("a" to 3))
val S2 = ReduceChallenges(S1, SetChallenge(ChallengeOf(1)))
Check(
"切片 Challenges:SetChallenge 按 version 建索引",
(S2.Challenge[1] as Challenge).version == 1
)
Check(
"切片 Challenges:RemoveChallenge 按 version 删",
ReduceChallenges(S2, RemoveChallenge(1)).Challenge.isEmpty()
)
Check(
"切片 Challenges:SetBattlePassPackages 写包表",
ReduceChallenges(S2, SetBattlePassPackages(mapOf("x" to 5L)))
.BattlePassPackages == mapOf("x" to 5L)
)
val S3 = ReduceChallenges(S2, SetBattlePass(1))
Check(
"切片 Challenges:SetBattlePass 追加战令条目",
S3.BattlePasses.size == 1 && S3.BattlePasses.first().version == 1
)
val S4 = ReduceChallenges(
ReduceChallenges(S3, SetTakenFreeRewardIndex(1, 2)),
SetTakenBattlePassRewardIndex(1, 4),
)
Check(
"切片 Challenges:两个已领取索引按 flavor 命中",
S4.BattlePasses.first().takenFreeRewardIndex == 2 &&
S4.BattlePasses.first().takenBattlePassRewardIndex == 4
)
Check(
"切片 Challenges:SetRewardForDialog / RefreshBattlePasses 是 no-op",
ReduceChallenges(S4, SetRewardForDialog("1", 1)) == S4 &&
ReduceChallenges(S4, RefreshBattlePasses) == S4
)
Check(
"切片 Challenges:无关动作不改状态",
ReduceChallenges(S4, AddFrags(1)) == S4
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, UpdateStars(mapOf("b" to 1)))
Check(
"根:UpdateStars 只动 challenges",
Root1.Challenges.Stars == mapOf("b" to 1) &&
Root1.BattleUsers == Root0.BattleUsers
)
val W = RootWiring.ByField("challenges")!!
Check(
"根:challenges 委派到 CZ / nh.hsj 且 9 个动作",
W.SliceClass == "CZ" && W.Reducer == "nh.hsj" && W.Actions.size == 9
)
}
// 94 切片 reducer:ClansStorage(lO / me.vr1,3 字段;13 个动作)
run {
fun Clan(Id: Any? = 1L): ClanInfo = ClanInfo(
id = Id, name = "n", tag = "t", description = "d",
minRankIndexForAddClan = 1, incomingRequestEnabled = true,
creatorId = 9L, createTime = 0L, users = emptySet<Any?>(),
gameObject = null, maxCharactersDescription = 100, blocked = false,
reasonForBlocking = "", self = false, maxMembers = 10,
newUsers = emptySet<Any?>(),
)
val S0 = ClanStorageStateSlice()
Check("切片 ClansStorage:默认值完整", S0.Clans.isEmpty() && S0.ForeignClan == null)
val S1 = ReduceClanStorage(S0, LoadClanIntoStorage(Clan()))
Check(
"切片 ClansStorage:ClanLoaded 按 id 入表",
(S1.Clans[1L] as ClanInfo).name == "n"
)
Check(
"切片 ClansStorage:MinimalRankChanged / RecruitmentChanged 就地 copy",
(ReduceClanStorage(S1, MinimalRankChanged(1L, 5)).Clans[1L] as ClanInfo)
.minRankIndexForAddClan == 5 &&
!(ReduceClanStorage(S1, RecruitmentChanged(1L, false)).Clans[1L] as ClanInfo)
.incomingRequestEnabled
)
Check(
"切片 ClansStorage:成员与待审用户的增删",
(ReduceClanStorage(
ReduceClanStorage(S1, AddClanUser(1L, 7L)),
AddUsersToNewAccepted(1, listOf(8L)),
).Clans[1L] as ClanInfo).let {
it.users == setOf<Any?>(7L) && it.newUsers == setOf<Any?>(8L)
} &&
(ReduceClanStorage(
ReduceClanStorage(S1, AddClanUser(1L, 7L)),
RemoveClanUser(1L, 7L),
).Clans[1L] as ClanInfo).users == emptySet<Any?>()
)
Check(
"切片 ClansStorage:UpdateClan 用 null 保留原值",
(ReduceClanStorage(S1, UpdateClan(1L, "n2", null, null))
.Clans[1L] as ClanInfo).let { it.name == "n2" && it.tag == "t" }
)
Check(
"切片 ClansStorage:BuyAdditionalPlace 让 maxMembers +1",
(ReduceClanStorage(S1, BuyAdditionalPlace(1L)).Clans[1L] as ClanInfo)
.maxMembers == 11
)
Check(
"切片 ClansStorage:外战队三臂",
ReduceClanStorage(S1, ForeignClanLoaded(Clan(2L))).ForeignClan != null &&
ReduceClanStorage(S1, ClearForeignClan).ForeignClan == null &&
ReduceClanStorage(S1, ShowForeignClan(2L)).ForeignClanId == 2L
)
Check(
"切片 ClansStorage:ClanUnloaded 删表",
ReduceClanStorage(S1, ClanUnloaded(1L)).Clans.isEmpty()
)
Check(
"切片 ClansStorage:无关动作不改状态",
ReduceClanStorage(S1, AddFrags(1)) == S1
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, LoadClanIntoStorage(Clan()))
Check(
"根:ClanLoaded 只动 clansStorage",
Root1.ClansStorage.Clans.keys == setOf<Any?>(1L) &&
Root1.Challenges == Root0.Challenges
)
val W = RootWiring.ByField("clansStorage")!!
Check(
"根:clansStorage 委派到 lO / me.vr1 且 13 个动作",
W.SliceClass == "lO" && W.Reducer == "me.vr1" && W.Actions.size == 13
)
}
// 95 切片 reducer:Clan(jN / we.hr0,36 字段;43 个动作)
run {
val S0 = ClanStateSlice()
Check(
"切片 Clan:默认值完整",
S0.CurrentClanId == null && !S0.HasClan && !S0.HasLicense &&
S0.ClanAcceptedMembers.isEmpty()
)
val S1 = ReduceClan(S0, SetMinRankForCreate(5))
Check("切片 Clan:SetMinRankForCreate 写最小等级", S1.MinRankForCreateClan == 5)
val S2 = ReduceClan(S1, AddUsersToAccepted(listOf(1L, 2L)))
Check(
"切片 Clan:AddUsersToAccepted 批量入集合",
S2.ClanAcceptedMembers == setOf<Any?>(1L, 2L)
)
Check(
"切片 Clan:RemoveUserFromAccepted 删单值",
ReduceClan(S2, RemoveUserFromAccepted(1L))
.ClanAcceptedMembers == setOf<Any?>(2L)
)
val Info = ClanInfo(
id = 7L, name = "n", tag = "t", description = "d",
minRankIndexForAddClan = 1, incomingRequestEnabled = true,
creatorId = 9L, createTime = 0L, users = emptySet<Any?>(),
gameObject = null, maxCharactersDescription = 100, blocked = false,
reasonForBlocking = "", self = false, maxMembers = 10,
newUsers = emptySet<Any?>(),
)
val S3 = ReduceClan(S2, ClanInfoLoaded(Info))
Check(
"切片 Clan:ClanInfoLoaded 写 6 个身份字段",
S3.CurrentClanId == 7L && S3.CreatorId == 9L && S3.Name == "n" &&
S3.Tag == "t" && S3.Description == "d"
)
Check(
"切片 Clan:人员 / 战队列表四臂",
ReduceClan(S3, IncomingAdded(listOf(3L))).IncomingUsers == setOf<Any?>(3L) &&
ReduceClan(S3, OutgoingAdded(listOf(4L))).OutgoingUsers == setOf<Any?>(4L) &&
ReduceClan(S3, AddOutgoingClan(11L)).OutgoingClans == setOf<Any?>(11L) &&
ReduceClan(S3, AddNewIncomingClan(listOf(12L)))
.NewIncomingClans == setOf<Any?>(12L)
)
Check(
"切片 Clan:中间态三臂",
ReduceClan(S3, UpdateIntermediateMinRank(2)).IntermediateMinRankIndex == 2 &&
ReduceClan(S3, UpdateIntermediateRequests(true))
.IntermediateRequestsEnabled == true &&
ReduceClan(S3, UpdateIntermediateDescription("x"))
.IntermediateDescription == "x"
)
Check(
"切片 Clan:校验状态写 / 复位",
ReduceClan(S3, UpdateValidation("bad")).ClanUserValidation == "bad" &&
ReduceClan(ReduceClan(S3, UpdateValidation("bad")), ResetClanUserValidation)
.ClanUserValidation == null &&
ReduceClan(ReduceClan(S3, UpdateClanNameValidation("bad")),
ResetClanNameTagValidation).ClanNameNameValidation == null
)
Check(
"切片 Clan:HasClan=false 整片复位、true 只置位",
ReduceClan(
ReduceClan(S3, UpdateClanLicense(true)),
UpdateHasClan(false),
) == ClanStateSlice() &&
ReduceClan(S3, UpdateHasClan(true)).HasClan
)
Check(
"切片 Clan:封禁 / 弹窗 / 改名价格 / 入队限制",
ReduceClan(S3, ClanBanned("why")).let { it.Banned && it.BanReason == "why" } &&
ReduceClan(S3, ClanModalVisible(true)).ClanModalVisible &&
ReduceClan(S3, SetClanEditPrice(ClanEditPriceCC(1, 2, null, 3, 4, 5)))
.ClanEditPrice == ClanEditPriceCC(1, 2, null, 3, 4, 5) &&
ReduceClan(S3, UpdateTimeToJoinRestriction(60))
.RestrictionToJoinInClan == 60
)
Check(
"切片 Clan:通知计数 +1 / -1 / 直接写",
ReduceClan(S3, IncrementClanNotifications).NotificationsCount == 1 &&
ReduceClan(ReduceClan(S3, IncrementClanNotifications),
DecrementClanNotifications).NotificationsCount == 0 &&
ReduceClan(S3, SetClanNotificationsCount(4)).NotificationsCount == 4
)
Check(
"切片 Clan:三个通知徽标与两个已读动作",
ReduceClan(S3, NewIncomingUserNotification(2))
.IncomingUserNotificationCount == 2 &&
ReduceClan(ReduceClan(S3, NewAcceptedUserNotification(3)),
AcceptedUserViewed(1L)).AcceptedUserNotificationCount == 2 &&
ReduceClan(ReduceClan(S3, NewIncomingUserNotification(2)),
IncomingUserViewed(1L)).IncomingUserNotificationCount == 1 &&
ReduceClan(S3, NewClanInviteNotification(1)).ClanInviteNotificationCount == 1
)
Check("切片 Clan:无关动作不改状态", ReduceClan(S3, AddFrags(1)) == S3)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, SetMinRankForCreate(3))
Check(
"根:SetMinRankForCreate 只动 clan",
Root1.Clan.MinRankForCreateClan == 3 && Root1.ClansStorage == Root0.ClansStorage
)
val W = RootWiring.ByField("clan")!!
Check(
"根:clan 委派到 jN / we.hr0 且 43 个动作",
W.SliceClass == "jN" && W.Reducer == "we.hr0" && W.Actions.size == 43
)
}
// 96 切片 reducer:ChatState(LP / $r.wqv,16 字段;24 个动作)
run {
fun Msg(Id: Int, Source: String, Target: String? = null): ChatMessage = ChatMessage(
addressMode = "", battleLinks = emptyList(), channel = "general", id = Id,
links = emptyList(), messageType = "text", sourceUser = Source, ip = "",
uid = null, userId = "1", targetUser = Target, text = "m", timePassedInSec = 0,
)
val S0 = ChatStateSlice()
Check(
"切片 ChatState:默认值完整",
!S0.Open && S0.CurrentChannel == null && S0.ChatMessage == null &&
S0.SelectedUid == "" && S0.MessagesByChannel.isEmpty() &&
!S0.IsShowKeyboard && !S0.IsNeedToHideKeyboard && !S0.IsNeedScroll &&
S0.IsFirstChannelMessagesLoad && S0.IgnoredUserIds.isEmpty()
)
Check(
"切片 ChatState:开关 / 频道 / 配置",
ReduceChat(S0, OpenChat).Open && !ReduceChat(S0, CloseChat).Open &&
ReduceChat(S0, ChangeChannel("general")).CurrentChannel == "general" &&
ReduceChat(S0, SetChatConfig(null)).Config == null
)
val S1 = ReduceChat(S0, SaveChatMessage(Msg(1, "u1", "u2")))
Check(
"切片 ChatState:SaveChatMessage 写消息并选中目标用户",
(S1.ChatMessage as ChatMessage).id == 1 && S1.SelectedUid == "u2"
)
val S2 = ReduceChat(S0, SetMessages(mapOf("general" to listOf(Msg(1, "u1")))))
Check(
"切片 ChatState:SetMessages 整体替换频道表",
(S2.MessagesByChannel["general"] as List<Any?>).size == 1
)
Check(
"切片 ChatState:CleanUsersMessage 按 sourceUser 过滤",
(ReduceChat(S2, CleanUsersMessage("u1"))
.MessagesByChannel["general"] as List<Any?>).isEmpty()
)
Check(
"切片 ChatState:右键菜单设置 / 清除",
ReduceChat(S0, ShowContextMenu("ctx")).ContextMenuData == "ctx" &&
ReduceChat(S0, ClearChatContextMenu).ContextMenuData == null
)
Check(
"切片 ChatState:键盘 / 滚动开关",
ReduceChat(S0, ShowChatKeyboard).let { it.IsShowKeyboard && !it.IsNeedToHideKeyboard } &&
!ReduceChat(S0, HideChatKeyboard).IsShowKeyboard &&
ReduceChat(S0, RequireHideChatKeyboard).IsNeedToHideKeyboard &&
!ReduceChat(S0, CancelHideChatKeyboard).IsNeedToHideKeyboard &&
ReduceChat(S0, EnableChatScroll).IsNeedScroll &&
!ReduceChat(S0, DisableChatScroll).IsNeedScroll
)
Check(
"切片 ChatState:SendMessageToServer 置两个待处理位",
ReduceChat(S0, SendMessageToServer("t", "mode", "m")).let {
it.IsNeedToHideKeyboard && it.IsNeedScroll
}
)
Check(
"切片 ChatState:速率参数 / 选中 uid 转小写 / 首屏标记",
ReduceChat(S0, UpdateTypingSpeedAntifloodParams(1, 2)).let {
it.SymbolCost == 1 && it.EnterCost == 2
} &&
ReduceChat(S0, SetSelectedUid("ABC")).SelectedUid == "abc" &&
!ReduceChat(S0, MarkFirstChannelMessagesLoaded).IsFirstChannelMessagesLoad
)
Check(
"切片 ChatState:屏蔽 / 取消屏蔽 / 重载清空",
ReduceChat(S0, IgnoreUserMessages("u1")).IgnoredUserIds == setOf<Any?>("u1") &&
ReduceChat(ReduceChat(S0, IgnoreUserMessages("u1")), ShowUserMessages("u1"))
.IgnoredUserIds.isEmpty() &&
ReduceChat(ReduceChat(S0, IgnoreUserMessages("u1")), ResetSettingsFromStorage)
.IgnoredUserIds.isEmpty()
)
Check(
"切片 ChatState:UpdateUsers 是 no-op、ResetChat 复位",
ReduceChat(S2, UpdateUsers(listOf("u1"))) == S2 &&
ReduceChat(S2, ResetChat) == ChatStateSlice()
)
Check("切片 ChatState:无关动作不改状态", ReduceChat(S2, AddFrags(1)) == S2)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, OpenChat)
Check(
"根:OpenChat 只动 chatState",
Root1.ChatState.Open && Root1.Clan == Root0.Clan
)
val W = RootWiring.ByField("chatState")!!
Check(
"根:chatState 委派到 LP / \$r.wqv 且 24 个动作",
W.SliceClass == "LP" && W.Reducer == "\$r.wqv" && W.Actions.size == 24
)
}
// 97 切片 reducer:ItemNotifications(_1 / A1().dso,1 字段;6 个动作)
run {
fun Item(Id: Long, End: Any? = null): ItemNotificationData = ItemNotificationData(
section = "shop", itemId = Id, type = "new", parentId = null,
startTime = null, endTime = End,
)
val S0 = ItemNotificationsStateSlice()
Check("切片 ItemNotifications:默认空集合", S0.Notifications.isEmpty())
val S1 = ReduceItemNotifications(S0, AddItemNotification("shop", 1L, "new", null, null, null))
Check(
"切片 ItemNotifications:AddItemNotification 构造并入集合",
S1.Notifications.size == 1 &&
(S1.Notifications.first() as ItemNotificationData).itemId == 1L
)
val S2 = ReduceItemNotifications(S1, AddItemNotifications(listOf(Item(2L), Item(3L))))
Check(
"切片 ItemNotifications:AddItemNotifications 批量加入",
(S2.Notifications.map { (it as ItemNotificationData).itemId }).toSet() ==
setOf(1L, 2L, 3L)
)
Check(
"切片 ItemNotifications:RemoveItemNotification 按 section+itemId 删",
ReduceItemNotifications(
S2,
RemoveItemNotification("shop", 2L, null, null, null, true),
).Notifications.size == 2
)
Check(
"切片 ItemNotifications:excludeType 命中时保留",
ReduceItemNotifications(
S2,
RemoveItemNotification("shop", null, null, null, "new", true),
).Notifications.size == 3
)
Check(
"切片 ItemNotifications:UpdateItemNotification 对匹配项应用 handler",
ReduceItemNotifications(
S2,
UpdateItemNotification("shop", 1L, null, null, null, { Item: ItemNotificationData ->
Item.copy(section = "updated")
}),
).let { Updated ->
(Updated.Notifications.first { (it as ItemNotificationData).itemId == 1L }
as ItemNotificationData).section == "updated"
}
)
Check(
"切片 ItemNotifications:PersistItemNotifications 是 no-op",
ReduceItemNotifications(S2, PersistItemNotifications) == S2
)
Check(
"切片 ItemNotifications:过期项在重载时被过滤",
ReduceItemNotifications(
ReduceItemNotifications(S0, AddItemNotification("shop", 9L, "new", null, null, 1L)),
ResetSettingsFromStorage,
).Notifications.isEmpty()
)
Check(
"切片 ItemNotifications:无关动作不改状态",
ReduceItemNotifications(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddItemNotification("shop", 1L, "new", null, null, null))
Check(
"根:AddItemNotification 只动 itemNotifications",
Root1.ItemNotifications.Notifications.size == 1 &&
Root1.ChatState == Root0.ChatState
)
val W = RootWiring.ByField("itemNotifications")!!
Check(
"根:itemNotifications 委派到 _1 / A1().dso 且 6 个动作",
W.SliceClass == "_1" && W.Reducer == "A1().dso" && W.Actions.size == 6
)
}
// 98 切片 reducer:MatchmakingGroup(rW / NW().is0,4 字段;13 个动作)
run {
fun Member(Id: Any?, Ready: Boolean = false): MatchmakingGroupMember =
MatchmakingGroupMember(UserId = Id, IsReady = Ready)
val S0 = MatchmakingGroupStateSlice()
Check(
"切片 MatchmakingGroup:默认值完整",
S0.Members.isEmpty() && S0.AllowedFromFriends.isEmpty() &&
S0.PendingInvitationsUsers.isEmpty() &&
S0.SelectedPlayersSendInvite.isEmpty()
)
val S1 = ReduceMatchmakingGroup(S0, InitMatchmakingGroup(listOf(Member(1L))))
Check("切片 MatchmakingGroup:InitMatchmakingGroup 整体替换", S1.Members.size == 1)
val S2 = ReduceMatchmakingGroup(S1, AddGroupMember(Member(2L)))
Check(
"切片 MatchmakingGroup:AddGroupMember 追加组员",
S2.Members.size == 2
)
Check(
"切片 MatchmakingGroup:AddGroupMember 按 userId 去重",
ReduceMatchmakingGroup(S2, AddGroupMember(Member(2L, true))).let {
it.Members.size == 2 &&
(it.Members.last() as MatchmakingGroupMember).IsReady
}
)
Check(
"切片 MatchmakingGroup:UserRemoved 删组员",
ReduceMatchmakingGroup(S2, UserRemoved(1L)).Members.size == 1
)
Check(
"切片 MatchmakingGroup:邀请 / 接受 / 拒绝",
ReduceMatchmakingGroup(S2, Invite(3L))
.PendingInvitationsUsers == setOf<Any?>(3L) &&
ReduceMatchmakingGroup(
ReduceMatchmakingGroup(S2, Invite(3L)),
AcceptedInviteToGroup(3L),
).let {
it.PendingInvitationsUsers.isEmpty() &&
it.Members.any { M -> (M as MatchmakingGroupMember).UserId == 3L }
} &&
ReduceMatchmakingGroup(
ReduceMatchmakingGroup(S2, Invite(4L)),
RejectedInviteToGroup(4L, "no"),
).PendingInvitationsUsers.isEmpty()
)
Check(
"切片 MatchmakingGroup:UpdateUserReadiness 命中 copy",
(ReduceMatchmakingGroup(S2, UpdateUserReadiness(1L, true))
.Members.first() as MatchmakingGroupMember).IsReady
)
Check(
"切片 MatchmakingGroup:UpdateMountItems 按 userId 写挂件",
(ReduceMatchmakingGroup(S2, UpdateMountItems(mapOf<Any?, Any?>(1L to "mount")))
.Members.first() as MatchmakingGroupMember).MountItems == "mount"
)
Check(
"切片 MatchmakingGroup:选中列表三臂",
ReduceMatchmakingGroup(S2, AddUserToSelected(5L))
.SelectedPlayersSendInvite == setOf<Any?>(5L) &&
ReduceMatchmakingGroup(
ReduceMatchmakingGroup(S2, AddUserToSelected(5L)),
RemoveUserToSelected(5L),
).SelectedPlayersSendInvite.isEmpty() &&
ReduceMatchmakingGroup(S2, SetSelectedUsers(listOf(6L, 7L)))
.SelectedPlayersSendInvite == setOf<Any?>(6L, 7L)
)
Check(
"切片 MatchmakingGroup:SetAllowedFromFriends / Reset",
ReduceMatchmakingGroup(
ReduceMatchmakingGroup(S2, SetAllowedFromFriends(listOf(1L))),
ResetMatchmakingGroup,
).let { it.AllowedFromFriends == setOf<Any?>(1L) && it.Members.isEmpty() }
)
Check(
"切片 MatchmakingGroup:无关动作不改状态",
ReduceMatchmakingGroup(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, Invite(9L))
Check(
"根:Invite 只动 matchmakingGroup",
Root1.MatchmakingGroup.PendingInvitationsUsers == setOf<Any?>(9L) &&
Root1.ItemNotifications == Root0.ItemNotifications
)
val W = RootWiring.ByField("matchmakingGroup")!!
Check(
"根:matchmakingGroup 委派到 rW / NW().is0 且 13 个动作",
W.SliceClass == "rW" && W.Reducer == "NW().is0" && W.Actions.size == 13
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)
println(if (Failed.isEmpty()) "结果:通过" else "结果:失败 ${Failed.size} 项")
ExitWith(if (Failed.isEmpty()) 0 else 1)
}
}