From eed36f1633fe3ab52a5d4f8f760f78a9c4d73436 Mon Sep 17 00:00:00 2001 From: LA <1245661240@qq.com> Date: Wed, 15 Oct 2025 10:12:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BB=A3=E7=A0=81=EF=BC=8C?= =?UTF-8?q?=E6=B3=A8=E6=84=8F=E5=8F=AF=E8=83=BD=E6=9C=89=E8=99=9A=E5=8C=96?= =?UTF-8?q?=E8=83=8C=E6=99=AF=E7=9A=84=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 2 + osu.Android.props | 2 +- .../BenchmarkDifficultyCalculation.cs | 77 ++ .../TestSceneCatcher.cs | 6 +- .../Difficulty/CatchPerformanceCalculator.cs | 8 +- .../Evaluators/MovementEvaluator.cs | 65 ++ .../Preprocessing/CatchDifficultyHitObject.cs | 60 +- .../Difficulty/Skills/Movement.cs | 88 +-- osu.Game.Rulesets.Catch/UI/Catcher.cs | 28 +- .../Evaluators/IndividualStrainEvaluator.cs | 37 + .../Evaluators/OverallStrainEvaluator.cs | 61 ++ .../Difficulty/ManiaDifficultyCalculator.cs | 11 +- .../Preprocessing/ManiaDifficultyHitObject.cs | 52 +- .../Difficulty/Skills/Strain.cs | 74 +- .../Edit/DrawableManiaEditorRuleset.cs | 1 + .../ManiaSettingsSubsection.cs | 1 + .../Mods/LAsMods/ManiaModEz2Settings.cs | 8 +- .../Legacy/HitTargetInsetContainer.cs | 10 +- .../Components/HitPositionPaddedContainer.cs | 15 +- .../UI/DrawableManiaRuleset.cs | 45 +- .../UI/GameplayBackgroundSource.cs | 118 +++ .../UI/IGameplayBackgroundSource.cs | 36 + .../UI/IPlayfieldDimensionProvider.cs | 39 + .../UI/ManiaBackgroundBlurManager.cs | 177 +++++ .../OsuDifficultyCalculatorTest.cs | 20 +- .../special-skin/spinner-clear@2x.png | Bin 0 -> 9933 bytes .../Resources/special-skin/spinner-rpm@2x.png | Bin 0 -> 14667 bytes .../special-skin/spinner-spin@2x.png | Bin 0 -> 20956 bytes .../Resources/special-skin/spinner-top@2x.png | Bin 0 -> 659281 bytes .../TestSceneSpinnerInput.cs | 1 + .../Difficulty/Evaluators/AimEvaluator.cs | 89 ++- .../Evaluators/FlashlightEvaluator.cs | 4 +- .../Difficulty/Evaluators/RhythmEvaluator.cs | 31 +- .../Difficulty/Evaluators/SpeedEvaluator.cs | 7 +- .../Difficulty/OsuDifficultyAttributes.cs | 35 + .../Difficulty/OsuDifficultyCalculator.cs | 146 ++-- .../OsuLegacyScoreMissCalculator.cs | 200 +++++ .../Difficulty/OsuPerformanceAttributes.cs | 12 + .../Difficulty/OsuPerformanceCalculator.cs | 234 +++--- .../Difficulty/OsuRatingCalculator.cs | 213 ++++++ .../Preprocessing/OsuDifficultyHitObject.cs | 103 +-- .../Difficulty/Skills/Aim.cs | 8 +- .../Difficulty/Skills/Speed.cs | 14 +- .../Difficulty/Utils/LegacyScoreUtils.cs | 101 +++ .../Difficulty/Utils/OsuStrainUtils.cs | 26 + .../Components/PathControlPointVisualiser.cs | 15 +- .../Sliders/SliderSelectionBlueprint.cs | 34 +- .../Edit/OsuSelectionHandler.cs | 2 +- .../Edit/OsuSelectionScaleHandler.cs | 10 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 18 - .../Skinning/Legacy/LegacySpinner.cs | 36 +- .../UI/ReplayAnalysisSettings.cs | 13 +- ...OsuHitObjectGenerationUtils.Reposition.cs} | 0 .../TaikoDifficultyCalculatorTest.cs | 8 +- .../Difficulty/Evaluators/ColourEvaluator.cs | 21 +- .../Data/SameRhythmHitObjectGrouping.cs | 47 +- .../Difficulty/Skills/Stamina.cs | 20 +- .../Difficulty/TaikoDifficultyAttributes.cs | 19 +- .../Difficulty/TaikoDifficultyCalculator.cs | 119 ++- .../Difficulty/TaikoPerformanceAttributes.cs | 3 - .../Difficulty/TaikoPerformanceCalculator.cs | 126 ++-- .../Difficulty/Utils/DeltaTimeNormaliser.cs | 66 ++ .../Difficulty/Utils/IntervalGroupingUtils.cs | 11 +- osu.Game.Tests/Database/RulesetStoreTests.cs | 14 +- .../NumberFormattingExtensionsTest.cs | 7 +- .../NonVisual/Filtering/FilterMatchingTest.cs | 52 ++ .../TestSceneSubmissionStageProgress.cs | 19 + .../TestScenePlayerLocalScoreImport.cs | 8 + .../TestScenePlayerScoreSubmission.cs | 20 + .../Matchmaking/TestSceneBeatmapPanel.cs | 28 - ...nGrid.cs => TestSceneBeatmapSelectGrid.cs} | 39 +- ...anel.cs => TestSceneBeatmapSelectPanel.cs} | 13 +- .../TestSceneBeatmapSelectionOverlay.cs | 68 -- .../Visual/Matchmaking/TestSceneIdleScreen.cs | 89 --- .../Matchmaking/TestSceneMatchmakingCloud.cs | 6 +- .../TestSceneMatchmakingPoolSelector.cs | 4 +- .../TestSceneMatchmakingQueueScreen.cs | 20 +- .../Matchmaking/TestSceneMatchmakingScreen.cs | 146 ++-- .../TestSceneMatchmakingScreenStack.cs | 119 --- ...ticPanel.cs => TestScenePanelRoomAward.cs} | 6 +- .../Visual/Matchmaking/TestScenePickScreen.cs | 9 +- .../Matchmaking/TestScenePlayerPanel.cs | 18 +- .../TestScenePlayerPanelOverlay.cs | 159 ++++ .../Matchmaking/TestSceneResultsScreen.cs | 61 +- .../TestSceneRoundResultsScreen.cs | 6 +- .../Matchmaking/TestSceneStageBubble.cs | 49 -- .../Matchmaking/TestSceneStageDisplay.cs | 51 +- .../Visual/Matchmaking/TestSceneStageText.cs | 43 -- .../Visual/Multiplayer/QueueModeTestScene.cs | 12 +- .../TestSceneDrawableRoomPlaylist.cs | 12 +- .../Multiplayer/TestSceneMultiplayer.cs | 11 +- .../TestSceneMultiplayerMatchSongSelect.cs | 9 + .../TestSceneMultiplayerMatchSubScreen.cs | 12 +- .../TestSceneMultiplayerPlaylist.cs | 12 +- .../TestSceneMultiplayerQueueList.cs | 12 +- .../TestSceneMultiplayerSpectateButton.cs | 12 +- .../TestScenePlaylistsSongSelect.cs | 12 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 12 +- .../Navigation/TestSceneScreenNavigation.cs | 3 +- .../TestSceneSkinEditorNavigation.cs | 5 +- .../TestSceneSongSelectNavigation.cs | 84 +++ .../TestSceneAddPlaylistToCollectionButton.cs | 12 +- .../TestScenePlaylistsRoomCreation.cs | 12 +- .../TestScenePlaylistsRoomSubScreen.cs | 12 +- .../Ranking/TestSceneSoloResultsScreen.cs | 9 + .../Ranking/TestSceneStatisticsPanel.cs | 20 +- .../Visual/Ranking/TestSceneUserTagControl.cs | 12 +- .../SongSelect/TestSceneCollectionDropdown.cs | 20 +- .../TestSceneManageCollectionsDialog.cs | 12 +- .../SongSelect/TestSceneTopLocalRank.cs | 9 + .../BeatmapCarouselFilterGroupingTest.cs | 2 +- .../SongSelectV2/SongSelectTestScene.cs | 8 + .../TestSceneBeatmapCarouselArtistGrouping.cs | 47 ++ ...tSceneBeatmapCarouselDifficultyGrouping.cs | 26 + .../TestSceneBeatmapLeaderboardSorting.cs | 8 + .../TestSceneBeatmapLeaderboardWedge.cs | 8 + .../TestSceneCollectionDropdown.cs | 20 +- .../SongSelectV2/TestScenePanelGroup.cs | 59 ++ .../SongSelectV2/TestSceneSongSelect.cs | 36 + ...neSongSelectCurrentSelectionInvalidated.cs | 18 +- .../TestSceneSongSelectGrouping.cs | 4 +- .../TestSceneDeleteLocalScore.cs | 15 +- .../UserInterface/TestSceneModPresetColumn.cs | 8 + .../TestSceneModSelectOverlay.cs | 9 + .../UserInterface/TestScenePlaylistOverlay.cs | 12 +- osu.Game/Beatmaps/BeatmapManager.cs | 1 + .../Beatmaps/Drawables/Cards/BeatmapCard.cs | 38 +- .../Cards/BeatmapCardContentBackground.cs | 4 +- .../Drawables/Cards/BeatmapCardExtra.cs | 4 +- .../Drawables/Cards/BeatmapCardNano.cs | 2 + .../Drawables/Cards/BeatmapCardNormal.cs | 2 + .../Drawables/Cards/BeatmapCardThumbnail.cs | 4 +- .../Cards/Buttons/GoToBeatmapButton.cs | 32 +- .../Cards/CollapsibleButtonContainer.cs | 24 +- osu.Game/Database/RealmAccess.cs | 42 +- .../Database/RealmDetachedBeatmapStore.cs | 37 +- osu.Game/Database/RealmExtensions.cs | 2 + .../Extensions/NumberFormattingExtensions.cs | 2 +- ...ntainer.cs => Carousel.ScrollContainer.cs} | 0 osu.Game/Graphics/Carousel/Carousel.cs | 26 +- .../Containers/OsuClickableContainer.cs | 12 +- osu.Game/Graphics/OsuColour.cs | 2 +- osu.Game/Graphics/OsuIcon.cs | 106 +-- .../UserInterface/HoverClickSounds.cs | 33 +- osu.Game/Localisation/BreakInfoStrings.cs | 24 + osu.Game/Localisation/CommonStrings.cs | 5 + .../PlayerSettingsOverlayStrings.cs | 55 ++ osu.Game/Online/Leaderboards/DrawableRank.cs | 26 +- .../Events/MatchmakingAvatarAction.cs | 13 + .../Events/MatchmakingAvatarActionEvent.cs | 29 + .../Events/MatchmakingAvatarActionRequest.cs | 23 + .../Online/Multiplayer/MatchServerEvent.cs | 2 + .../Online/Multiplayer/MatchUserRequest.cs | 2 + .../Online/Multiplayer/MultiplayerClient.cs | 5 +- osu.Game/Online/Rooms/Room.cs | 1 + osu.Game/Online/SignalRWorkaroundTypes.cs | 5 +- osu.Game/OsuGame.cs | 5 +- ..._Importing.cs => OsuGameBase.Importing.cs} | 0 osu.Game/OsuGameBase.cs | 2 - osu.Game/Overlays/HoldToConfirmOverlay.cs | 2 + .../Components/DailyChallengeStatsDisplay.cs | 2 +- .../Components/DailyChallengeStatsTooltip.cs | 13 +- .../Header/Components/DrawableBadge.cs | 2 + .../Profile/Header/Components/LevelBadge.cs | 5 +- ...cs => KeyBindingRow.ConflictResolution.cs} | 0 ...eyButton.cs => KeyBindingRow.KeyButton.cs} | 0 osu.Game/Overlays/Settings/SettingsSidebar.cs | 4 +- osu.Game/Overlays/SettingsToolboxGroup.cs | 8 +- .../SkinSelectionRotationHandler.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 13 +- .../Difficulty/DifficultyAttributes.cs | 7 + .../Difficulty/DifficultyCalculator.cs | 15 +- .../Rulesets/Difficulty/Skills/StrainSkill.cs | 2 + .../Utils/DifficultyCalculationUtils.cs | 88 +++ ...ifficultyCalculationUtils_ErrorFunction.cs | 688 ------------------ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 - .../Edit/HitObjectCompositionToolButton.cs | 5 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 20 +- osu.Game/Scoring/ScoreRank.cs | 10 - .../Backgrounds/BackgroundScreenBeatmap.cs | 3 + .../Screens/Edit/Compose/ComposeScreen.cs | 17 +- .../Submission/SubmissionStageProgress.cs | 2 + .../Edit/Timing/WaveformComparisonDisplay.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 4 +- osu.Game/Screens/Menu/MainMenu.cs | 3 +- .../ScreenIntro.cs} | 12 +- .../BeatmapSelect/BeatmapCardMatchmaking.cs | 458 ++++++++++++ .../BeatmapSelect/BeatmapSelectGrid.cs} | 33 +- .../Match/BeatmapSelect/BeatmapSelectPanel.cs | 227 ++++++ .../BeatmapSelect/SubScreenBeatmapSelect.cs} | 40 +- .../Gameplay/ScreenGameplay.cs} | 6 +- .../{ => Match}/MatchmakingAvatar.cs | 6 +- .../Match/MatchmakingChatDisplay.cs | 70 ++ .../MatchmakingSubScreen.cs | 17 +- .../Matchmaking/Match/PlayerPanel.cs | 471 ++++++++++++ .../Matchmaking/Match/PlayerPanelOverlay.cs | 312 ++++++++ .../Match/Results/PanelRoomAward.cs | 137 ++++ .../Match/Results/PanelUserStatistic.cs | 88 +++ .../Match/Results/SubScreenResults.cs | 373 ++++++++++ .../RoundResults/SubScreenRoundResults.cs} | 37 +- .../Match/RoundWarmup/SubScreenRoundWarmup.cs | 24 + .../Match/ScreenMatchmaking.ScreenStack.cs | 133 ++++ .../ScreenMatchmaking.cs} | 95 ++- .../Match/StageDisplay.StageSegment.cs | 234 ++++++ .../Match/StageDisplay.StatusText.cs | 132 ++++ .../Matchmaking/Match/StageDisplay.cs | 292 ++++++++ .../CloudVisualisation.cs} | 9 +- .../PoolSelector.cs} | 123 +++- .../QueueController.cs} | 30 +- .../ScreenQueue.cs} | 90 ++- .../Matchmaking/Screens/Idle/IdleScreen.cs | 27 - .../Matchmaking/Screens/Idle/PlayerPanel.cs | 197 ----- .../Screens/Idle/PlayerPanelList.cs | 93 --- .../Screens/MatchmakingScreenStack.cs | 121 --- .../Matchmaking/Screens/Pick/BeatmapPanel.cs | 192 ----- .../Screens/Pick/BeatmapSelectionOverlay.cs | 157 ---- .../Screens/Pick/BeatmapSelectionPanel.cs | 213 ------ .../Screens/Results/ResultsScreen.cs | 339 --------- .../Screens/Results/RoomStatisticPanel.cs | 57 -- .../Screens/Results/UserStatisticPanel.cs | 49 -- .../RoundResults/RoundResultsScorePanel.cs | 36 - .../OnlinePlay/Matchmaking/StageBubble.cs | 171 ----- .../OnlinePlay/Matchmaking/StageDisplay.cs | 91 --- .../OnlinePlay/Matchmaking/StageText.cs | 97 --- .../Spectate/MultiSpectatorPlayer.cs | 5 + ...{PlayerGrid_Cell.cs => PlayerGrid.Cell.cs} | 0 .../Screens/Play/BeatmapMetadataDisplay.cs | 3 +- osu.Game/Screens/Play/Break/BreakInfo.cs | 6 +- .../Play/PlayerSettings/AudioSettings.cs | 2 +- .../Play/PlayerSettings/InputSettings.cs | 2 +- .../Play/PlayerSettings/PlaybackSettings.cs | 4 +- .../PlayerSettings/PlayerSettingsGroup.cs | 3 +- .../Play/PlayerSettings/VisualSettings.cs | 2 +- osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 4 + osu.Game/Screens/Play/SoloPlayer.cs | 3 + osu.Game/Screens/Play/SoloSpectatorPlayer.cs | 8 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 6 - .../Ranking/Expanded/Accuracy/RankText.cs | 4 +- ...er.cs => UserTagControl.AddTagsPopover.cs} | 0 ...g.cs => UserTagControl.DrawableUserTag.cs} | 8 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 15 +- .../Select/Carousel/CarouselBeatmap.cs | 41 +- osu.Game/Screens/Select/FilterCriteria.cs | 3 +- osu.Game/Screens/Select/SongSelect.cs | 31 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 81 ++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 4 +- .../SelectV2/BeatmapCarouselFilterMatching.cs | 41 +- ...Header.cs => BeatmapDetailsArea.Header.cs} | 0 ...cs => BeatmapDetailsArea.WedgeSelector.cs} | 0 ....cs => BeatmapLeaderboardScore.Tooltip.cs} | 0 .../SelectV2/BeatmapLeaderboardScore.cs | 4 +- .../SelectV2/BeatmapLeaderboardWedge.cs | 6 + ... BeatmapMetadataWedge.FailRetryDisplay.cs} | 82 ++- ...> BeatmapMetadataWedge.MetadataDisplay.cs} | 0 ...atmapMetadataWedge.RatingSpreadDisplay.cs} | 0 ...eatmapMetadataWedge.SuccessRateDisplay.cs} | 0 ...ne.cs => BeatmapMetadataWedge.TagsLine.cs} | 0 ...BeatmapMetadataWedge.UserRatingDisplay.cs} | 0 .../Screens/SelectV2/BeatmapMetadataWedge.cs | 44 +- ...=> BeatmapTitleWedge.DifficultyDisplay.cs} | 0 ...TitleWedge.DifficultyStatisticsDisplay.cs} | 0 ...s => BeatmapTitleWedge.FavouriteButton.cs} | 0 ...stic.cs => BeatmapTitleWedge.Statistic.cs} | 0 ... BeatmapTitleWedge.StatisticDifficulty.cs} | 0 ...> BeatmapTitleWedge.StatisticPlayCount.cs} | 0 .../Screens/SelectV2/BeatmapTitleWedge.cs | 37 +- ...=> FilterControl.DifficultyRangeSlider.cs} | 0 ...over.cs => FooterButtonOptions.Popover.cs} | 0 osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 12 +- .../Screens/SelectV2/PanelGroupRankDisplay.cs | 226 ++++++ .../RealmPopulatingOnlineLookupSource.cs | 4 +- osu.Game/Screens/SelectV2/SongSelect.cs | 87 ++- osu.Game/Skinning/RealmBackedResourceStore.cs | 9 +- .../Tests/Visual/EditorSavingTestScene.cs | 2 +- .../Multiplayer/MultiplayerTestScene.cs | 4 +- .../Multiplayer/TestMultiplayerClient.cs | 53 +- .../Visual/OnlinePlay/OnlinePlayTestScene.cs | 9 +- osu.Game/Tests/Visual/OsuTestScene.cs | 3 + osu.Game/Users/UserListPanel.cs | 3 + osu.Game/Utils/GeometryUtils.cs | 18 +- osu.iOS.props | 2 +- 282 files changed, 7889 insertions(+), 4271 deletions(-) create mode 100644 osu.Game.Benchmarks/BenchmarkDifficultyCalculation.cs create mode 100644 osu.Game.Rulesets.Catch/Difficulty/Evaluators/MovementEvaluator.cs create mode 100644 osu.Game.Rulesets.Mania/Difficulty/Evaluators/IndividualStrainEvaluator.cs create mode 100644 osu.Game.Rulesets.Mania/Difficulty/Evaluators/OverallStrainEvaluator.cs create mode 100644 osu.Game.Rulesets.Mania/UI/GameplayBackgroundSource.cs create mode 100644 osu.Game.Rulesets.Mania/UI/IGameplayBackgroundSource.cs create mode 100644 osu.Game.Rulesets.Mania/UI/IPlayfieldDimensionProvider.cs create mode 100644 osu.Game.Rulesets.Mania/UI/ManiaBackgroundBlurManager.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-clear@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-rpm@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-spin@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-top@2x.png create mode 100644 osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs create mode 100644 osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs create mode 100644 osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs create mode 100644 osu.Game.Rulesets.Osu/Difficulty/Utils/OsuStrainUtils.cs rename osu.Game.Rulesets.Osu/Utils/{OsuHitObjectGenerationUtils_Reposition.cs => OsuHitObjectGenerationUtils.Reposition.cs} (100%) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Utils/DeltaTimeNormaliser.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs rename osu.Game.Tests/Visual/Matchmaking/{TestSceneBeatmapSelectionGrid.cs => TestSceneBeatmapSelectGrid.cs} (81%) rename osu.Game.Tests/Visual/Matchmaking/{TestSceneBeatmapSelectionPanel.cs => TestSceneBeatmapSelectPanel.cs} (83%) delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs rename osu.Game.Tests/Visual/Matchmaking/{TestSceneRoomStatisticPanel.cs => TestScenePanelRoomAward.cs} (65%) create mode 100644 osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs delete mode 100644 osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs rename osu.Game/Graphics/Carousel/{Carousel_ScrollContainer.cs => Carousel.ScrollContainer.cs} (100%) create mode 100644 osu.Game/Localisation/BreakInfoStrings.cs create mode 100644 osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs create mode 100644 osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs create mode 100644 osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs rename osu.Game/{OsuGameBase_Importing.cs => OsuGameBase.Importing.cs} (100%) rename osu.Game/Overlays/Settings/Sections/Input/{KeyBindingRow_ConflictResolution.cs => KeyBindingRow.ConflictResolution.cs} (100%) rename osu.Game/Overlays/Settings/Sections/Input/{KeyBindingRow_KeyButton.cs => KeyBindingRow.KeyButton.cs} (100%) delete mode 100644 osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/MatchmakingIntroScreen.cs => Intro/ScreenIntro.cs} (96%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Pick/BeatmapSelectionGrid.cs => Match/BeatmapSelect/BeatmapSelectGrid.cs} (92%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/Pick/PickScreen.cs => Match/BeatmapSelect/SubScreenBeatmapSelect.cs} (58%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingPlayer.cs => Match/Gameplay/ScreenGameplay.cs} (77%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{ => Match}/MatchmakingAvatar.cs (87%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens => Match}/MatchmakingSubScreen.cs (64%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/RoundResults/RoundResultsScreen.cs => Match/RoundResults/SubScreenRoundResults.cs} (81%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingScreen.cs => Match/ScreenMatchmaking.cs} (82%) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingCloud.cs => Queue/CloudVisualisation.cs} (92%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingPoolSelector.cs => Queue/PoolSelector.cs} (52%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{MatchmakingController.cs => Queue/QueueController.cs} (79%) rename osu.Game/Screens/OnlinePlay/Matchmaking/{Screens/MatchmakingQueueScreen.cs => Queue/ScreenQueue.cs} (85%) delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs delete mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{PlayerGrid_Cell.cs => PlayerGrid.Cell.cs} (100%) rename osu.Game/Screens/Ranking/{UserTagControl_AddTagsPopover.cs => UserTagControl.AddTagsPopover.cs} (100%) rename osu.Game/Screens/Ranking/{UserTagControl_DrawableUserTag.cs => UserTagControl.DrawableUserTag.cs} (96%) rename osu.Game/Screens/SelectV2/{BeatmapDetailsArea_Header.cs => BeatmapDetailsArea.Header.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapDetailsArea_WedgeSelector.cs => BeatmapDetailsArea.WedgeSelector.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapLeaderboardScore_Tooltip.cs => BeatmapLeaderboardScore.Tooltip.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_FailRetryDisplay.cs => BeatmapMetadataWedge.FailRetryDisplay.cs} (66%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_MetadataDisplay.cs => BeatmapMetadataWedge.MetadataDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_RatingSpreadDisplay.cs => BeatmapMetadataWedge.RatingSpreadDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_SuccessRateDisplay.cs => BeatmapMetadataWedge.SuccessRateDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_TagsLine.cs => BeatmapMetadataWedge.TagsLine.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapMetadataWedge_UserRatingDisplay.cs => BeatmapMetadataWedge.UserRatingDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_DifficultyDisplay.cs => BeatmapTitleWedge.DifficultyDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_DifficultyStatisticsDisplay.cs => BeatmapTitleWedge.DifficultyStatisticsDisplay.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_FavouriteButton.cs => BeatmapTitleWedge.FavouriteButton.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_Statistic.cs => BeatmapTitleWedge.Statistic.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_StatisticDifficulty.cs => BeatmapTitleWedge.StatisticDifficulty.cs} (100%) rename osu.Game/Screens/SelectV2/{BeatmapTitleWedge_StatisticPlayCount.cs => BeatmapTitleWedge.StatisticPlayCount.cs} (100%) rename osu.Game/Screens/SelectV2/{FilterControl_DifficultyRangeSlider.cs => FilterControl.DifficultyRangeSlider.cs} (100%) rename osu.Game/Screens/SelectV2/{FooterButtonOptions_Popover.cs => FooterButtonOptions.Popover.cs} (100%) create mode 100644 osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs diff --git a/.editorconfig b/.editorconfig index e42b8b6a8a..a145efc348 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,6 +21,8 @@ trim_trailing_whitespace = true # temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint +# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false +dotnet_diagnostic.CS1591.severity = none #license header file_header_template = Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. diff --git a/osu.Android.props b/osu.Android.props index 498d6f267e..4be825cea9 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + Release Difference / ms + // release_threshold + if (isOverlapping) + holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold); + + return (1 + holdAddition) * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 06b8018f2b..bcf16e6808 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { var sortedObjects = beatmap.HitObjects.ToArray(); + int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns; LegacySortHelper.Sort(sortedObjects, Comparer.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime))); List objects = new List(); + List[] perColumnObjects = new List[totalColumns]; + + for (int column = 0; column < totalColumns; column++) + perColumnObjects[column] = new List(); for (int i = 1; i < sortedObjects.Length; i++) - objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count)); + { + var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count); + objects.Add(currentObject); + perColumnObjects[currentObject.Column].Add(currentObject); + } return objects; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs index a67d38b29f..91b6a2b861 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Preprocessing/ManiaDifficultyHitObject.cs @@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing { public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject; - public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, int index) + private readonly List[] perColumnObjects; + + private readonly int columnIndex; + + public readonly int Column; + + // The hit object earlier in time than this note in each column + public readonly ManiaDifficultyHitObject?[] PreviousHitObjects; + + public readonly double ColumnStrainTime; + + public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, List[] perColumnObjects, int index) : base(hitObject, lastObject, clockRate, objects, index) { + int totalColumns = perColumnObjects.Length; + this.perColumnObjects = perColumnObjects; + Column = BaseObject.Column; + columnIndex = perColumnObjects[Column].Count; + PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns]; + ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime; + + if (index > 0) + { + ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1]; + + for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++) + PreviousHitObjects[i] = prevNote.PreviousHitObjects[i]; + + // intentionally depends on processing order to match live. + PreviousHitObjects[prevNote.Column] = prevNote; + } + } + + /// + /// The previous object in the same column as this , exclusive of Long Note tails. + /// + /// The number of notes to go back. + /// The object in this column notes back, or null if this is the first note in the column. + public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex) + { + int index = columnIndex - (backwardsIndex + 1); + return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null; + } + + /// + /// The next object in the same column as this , exclusive of Long Note tails. + /// + /// The number of notes to go forward. + /// The object in this column notes forward, or null if this is the last note in the column. + public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex) + { + int index = columnIndex + (forwardsIndex + 1); + return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null; } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index bb4261ea13..037b7e3511 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mania.Difficulty.Evaluators; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; @@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; - private const double release_threshold = 30; protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; - private readonly double[] startTimes; - private readonly double[] endTimes; private readonly double[] individualStrains; - - private double individualStrain; + private double highestIndividualStrain; private double overallStrain; public Strain(Mod[] mods, int totalColumns) : base(mods) { - startTimes = new double[totalColumns]; - endTimes = new double[totalColumns]; individualStrains = new double[totalColumns]; overallStrain = 1; } @@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { var maniaCurrent = (ManiaDifficultyHitObject)current; - double startTime = maniaCurrent.StartTime; - double endTime = maniaCurrent.EndTime; - int column = maniaCurrent.BaseObject.Column; - bool isOverlapping = false; - double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information - double holdFactor = 1.0; // Factor to all additional strains in case something else is held - double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base); + individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current); - for (int i = 0; i < endTimes.Length; ++i) - { - // The current note is overlapped if a previous note or end is overlapping the current note body - isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && - Precision.DefinitelyBigger(endTime, endTimes[i], 1) && - Precision.DefinitelyBigger(startTime, startTimes[i], 1); + // Take the hardest individualStrain for notes that happen at the same time (in a chord). + // This is to ensure the order in which the notes are processed does not affect the resultant total strain. + highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column]; - // We give a slight bonus to everything if something is held meanwhile - if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) && - Precision.DefinitelyBigger(startTime, startTimes[i], 1)) - holdFactor = 1.25; - - closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i])); - } - - // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending. - // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away. - // holdAddition - // ^ - // 1.0 + - - - - - -+----------- - // | / - // 0.5 + - - - - -/ Sigmoid Curve - // | /| - // 0.0 +--------+-+---------------> Release Difference / ms - // release_threshold - if (isOverlapping) - holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold); - - // Decay and increase individualStrains in own column - individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); - individualStrains[column] += 2.0 * holdFactor; - - // For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns - individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column]; - - // Decay and increase overallStrain - overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base); - overallStrain += (1 + holdAddition) * holdFactor; - - // Update startTimes and endTimes arrays - startTimes[column] = startTime; - endTimes[column] = endTime; + overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base); + overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current); // By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section. - return individualStrain + overallStrain - CurrentStrain; + return highestIndividualStrain + overallStrain - CurrentStrain; } - protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) - => applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base) - + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base); + protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) => + applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base) + + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base); private double applyDecay(double value, double deltaTime, double decayBase) => value * Math.Pow(decayBase, deltaTime / 1000); diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs index 9443a0a7f6..ea7e3dfe3e 100644 --- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs +++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditorRuleset.cs @@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override void Update() { + // 使用ez2lazer特色调速系统 TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get(ManiaRulesetSetting.ScrollSpeed), Config.Get(ManiaRulesetSetting.ScrollBaseSpeed), Config.Get(ManiaRulesetSetting.ScrollTimePerSpeed)) : TimelineTimeRange.Value; base.Update(); } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 504945318d..739e76f862 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Mania private partial class ManiaScrollSlider : RoundedSliderBar { + // 自定义提示 private ManiaRulesetConfigManager config = null!; [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/Mods/LAsMods/ManiaModEz2Settings.cs b/osu.Game.Rulesets.Mania/Mods/LAsMods/ManiaModEz2Settings.cs index 5a2e88073d..1fa18f297b 100644 --- a/osu.Game.Rulesets.Mania/Mods/LAsMods/ManiaModEz2Settings.cs +++ b/osu.Game.Rulesets.Mania/Mods/LAsMods/ManiaModEz2Settings.cs @@ -149,12 +149,16 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods if (NoScratch.Value && NoScratchTemplate.TryGetValue(keys, out var scratchToRemove)) { - objectsToMakeAuto.AddRange(maniaBeatmap.HitObjects.Where(h => h is ManiaHitObject maniaHitObject && scratchToRemove.Contains(maniaHitObject.Column))); + objectsToMakeAuto.AddRange(maniaBeatmap.HitObjects + .Where(h => h is ManiaHitObject maniaHitObject && + scratchToRemove.Contains(maniaHitObject.Column))); } if (NoPanel.Value && NoPanelTemplate.TryGetValue(keys, out var panelToRemove)) { - objectsToMakeAuto.AddRange(maniaBeatmap.HitObjects.Where(h => h is ManiaHitObject maniaHitObject && panelToRemove.Contains(maniaHitObject.Column))); + objectsToMakeAuto.AddRange(maniaBeatmap.HitObjects + .Where(h => h is ManiaHitObject maniaHitObject && + panelToRemove.Contains(maniaHitObject.Column))); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs index da4ed122c9..ee56b9c7e2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs @@ -35,12 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); - bool globalHitPositionValue = ezSkinConfig.GetBindable(EzSkinSetting.GlobalHitPosition).Value; - double hitPositionValue = ezSkinConfig.GetBindable(EzSkinSetting.HitPosition).Value; + globalHitPosition = ezSkinConfig.GetBindable(EzSkinSetting.GlobalHitPosition); + hitPositonBindable = ezSkinConfig.GetBindable(EzSkinSetting.HitPosition); - hitPosition = globalHitPositionValue - ? (float)hitPositionValue - : skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? (float)hitPositionValue; + hitPosition = globalHitPosition.Value + ? (float)hitPositonBindable.Value + : skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? (float)hitPositonBindable.Value; } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs index eb0ec3211e..fdaeed562a 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitPositionPaddedContainer.cs @@ -48,16 +48,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components protected virtual void UpdateHitPosition() { - float hitPosition; - - if (globalHitPosition.Value) - hitPosition = (float)hitPositonBindable.Value; - else - { - hitPosition = skin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value - ?? (float)hitPositonBindable.Value; - } + float hitPosition = globalHitPosition.Value + ? (float)hitPositonBindable.Value + : skin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value + ?? (float)hitPositonBindable.Value; Padding = Direction.Value == ScrollingDirection.Up ? new MarginPadding { Top = hitPosition } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 187bee4e23..45dc33edd2 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -10,6 +10,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Platform; @@ -38,7 +39,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI { - public partial class DrawableManiaRuleset : DrawableScrollingRuleset + public partial class DrawableManiaRuleset : DrawableScrollingRuleset, IPlayfieldDimensionProvider { /// /// The minimum time range. This occurs at a of 40. @@ -63,16 +64,17 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); + private readonly BindableDouble configScrollSpeed = new BindableDouble(); + private readonly Bindable mobileLayout = new Bindable(); + private readonly Bindable touchOverlay = new Bindable(); + + //自定义判定系统 private readonly Bindable scrollingStyle = new Bindable(); private readonly BindableDouble configBaseMs = new BindableDouble(); private readonly BindableDouble configTimePerSpeed = new BindableDouble(); - private readonly BindableDouble configScrollSpeed = new BindableDouble(); // private readonly ManiaHitModeConvertor hitModeConvertor; - // private readonly Bindable hitMode = new Bindable(); - private readonly Bindable mobileLayout = new Bindable(); - private readonly Bindable touchOverlay = new Bindable(); public double TargetTimeRange { get; protected set; } @@ -92,6 +94,15 @@ namespace osu.Game.Rulesets.Mania.UI private Bindable hitPositonBindable = new Bindable(); private readonly Bindable globalHitPosition = new Bindable(); + // 背景虚化管理器 + private readonly ManiaBackgroundBlurManager blurManager = null!; + + public void SetGameplayBackgroundSource(IGameplayBackgroundSource source) + { + // 可能在Load前或后调用,blurManager在load()里创建,因此先判断null + blurManager.SetSource(source); + } + public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { @@ -158,7 +169,7 @@ namespace osu.Game.Rulesets.Mania.UI { base.LoadComplete(); - // 启动独立的异步任务 + // 启动独立的异步任务,预加载EzPro皮肤中会用到的贴图 Schedule(() => { _ = Task.Run(async () => @@ -186,13 +197,13 @@ namespace osu.Game.Rulesets.Mania.UI private void updateMobileLayout() { if (touchOverlay.Value) - KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this)); + KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this)); else { - if (touchInputArea != null) - KeyBindingInputManager.Remove(touchInputArea, true); + if (touchInputArea != null) + KeyBindingInputManager.Remove(touchInputArea, true); - touchInputArea = null; + touchInputArea = null; } } @@ -291,5 +302,19 @@ namespace osu.Game.Rulesets.Mania.UI if (currentSkin.IsNotNull()) currentSkin.SourceChanged -= onSkinChange; } + + public bool SupportsBackgroundBlurMasking => true; + + public RectangleF? GetPlayfieldBounds() + { + try + { + return Playfield.ScreenSpaceDrawQuad.AABBFloat; + } + catch + { + return null; + } + } } } diff --git a/osu.Game.Rulesets.Mania/UI/GameplayBackgroundSource.cs b/osu.Game.Rulesets.Mania/UI/GameplayBackgroundSource.cs new file mode 100644 index 0000000000..e50ddb79a9 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/GameplayBackgroundSource.cs @@ -0,0 +1,118 @@ +// filepath: osu.Game/Screens/Play/GameplayBackgroundSource.cs +// Implementation of IGameplayBackgroundSource combining beatmap background + storyboard overlay layers. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class GameplayBackgroundSource : IGameplayBackgroundSource + { + private readonly Player player; + + public GameplayBackgroundSource(Player player) + { + this.player = player; + } + + public Drawable CreateCompositeProxy() + { + var composite = new Container + { + RelativeSizeAxes = Axes.Both + }; + + try + { + player.ApplyToBackground(b => + { + var bgProxy = b.CreateBackdropProxy(); + if (bgProxy != null) + composite.Add(bgProxy); + }); + } + catch + { + // ignore if background not available. + } + + if (player.DimmableStoryboard != null) + { + composite.Add(player.DimmableStoryboard.CreateProxy()); + } + + return composite; + } + + public Drawable? CreateBackgroundOnlyProxy() + { + Drawable? result = null; + + try + { + player.ApplyToBackground(b => + { + result = b.CreateBackdropProxy(); + }); + } + catch { } + + return result; + } + + public Drawable? CreateBackgroundWithVideoProxy() + { + // Use real background (non-proxy) to avoid dim / black issues from proxy state. + var container = new Container { RelativeSizeAxes = Axes.Both }; + + try + { + var bg = CreateStandaloneBackground(); + if (bg != null) + container.Add(bg); + } + catch { } + + try + { + var storyboard = player.GameplayState?.Storyboard; + + if (storyboard != null) + { + foreach (var layer in storyboard.Layers) + { + if (layer is StoryboardVideoLayer videoLayer) + { + var drawableLayer = videoLayer.CreateDrawable(); + + if (drawableLayer != null) + { + drawableLayer.RelativeSizeAxes = Axes.Both; + container.Add(drawableLayer); + } + + break; + } + } + } + } + catch { } + + return container; + } + + public Drawable CreateStandaloneBackground() + { + // Prefer the working beatmap from GameplayState if available. + var working = player.Beatmap.Value; // WorkingBeatmap + var background = new BeatmapBackground(working) + { + RelativeSizeAxes = Axes.Both + }; + return background; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/IGameplayBackgroundSource.cs b/osu.Game.Rulesets.Mania/UI/IGameplayBackgroundSource.cs new file mode 100644 index 0000000000..24b53ae52a --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/IGameplayBackgroundSource.cs @@ -0,0 +1,36 @@ +// filepath: osu.Game/Screens/Play/IGameplayBackgroundSource.cs +// Provides a proxy-able composite of gameplay background-related visual content (background + storyboard layers if desired) +// to allow rulesets (e.g. Mania) to apply localised effects (blur, dim, masking) without re-implementing loading logic +// or performing full-screen framebuffer captures. + +using osu.Framework.Graphics; + +namespace osu.Game.Rulesets.Mania.UI +{ + public interface IGameplayBackgroundSource + { + /// + /// Creates a proxy drawable containing the background-related composite (eg. storyboard, background imagery). + /// Returned drawable is expected to be RelativeSizeAxes = Both (or otherwise scalable by consumer) and safe to proxy repeatedly. + /// + Drawable CreateCompositeProxy(); + + /// + /// Creates a proxy drawable containing only the background (without storyboard layers). + /// Returned drawable is expected to be RelativeSizeAxes = Both (or otherwise scalable by consumer) and safe to proxy repeatedly. + /// + Drawable? CreateBackgroundOnlyProxy(); + + /// + /// Creates a proxy drawable containing the background and video (if available). + /// Returned drawable is expected to be RelativeSizeAxes = Both (or otherwise scalable by consumer) and safe to proxy repeatedly. + /// + Drawable? CreateBackgroundWithVideoProxy(); + + /// + /// Creates a standalone drawable containing the background. + /// Returned drawable is expected to be RelativeSizeAxes = Both (or otherwise scalable by consumer) and safe to proxy repeatedly. + /// + Drawable CreateStandaloneBackground(); + } +} diff --git a/osu.Game.Rulesets.Mania/UI/IPlayfieldDimensionProvider.cs b/osu.Game.Rulesets.Mania/UI/IPlayfieldDimensionProvider.cs new file mode 100644 index 0000000000..6989b7be72 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/IPlayfieldDimensionProvider.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// 用于为背景效果提供游戏区域尺寸信息的规则集接口。 + /// + public interface IPlayfieldDimensionProvider + { + /// + /// 获取游戏区域在屏幕空间中的边界。 + /// + /// 游戏区域在屏幕坐标中的边界,如果不可用则为 null。 + RectangleF? GetPlayfieldBounds(); + + /// + /// 此规则集是否支持背景模糊遮罩。 + /// + bool SupportsBackgroundBlurMasking { get; } + + /// + /// 背景图像是否应在游戏区域外模糊。 + /// + bool ShouldBlurBackground => true; + + /// + /// 故事板是否应在游戏区域外模糊。 + /// + bool ShouldBlurStoryboard => true; + + /// + /// 故事板中的视频是否应在游戏区域外模糊。 + /// + bool ShouldBlurVideo => true; + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaBackgroundBlurManager.cs b/osu.Game.Rulesets.Mania/UI/ManiaBackgroundBlurManager.cs new file mode 100644 index 0000000000..d9a19f9f51 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaBackgroundBlurManager.cs @@ -0,0 +1,177 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens; +using osu.Game.Screens.Backgrounds; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// 专门用于处理Mania模式背景虚化的组件 + /// + public partial class ManiaBackgroundBlurManager : CompositeDrawable + { + // Removed ruleset reference; will be placed inside ManiaPlayfield below columns. + private IGameplayBackgroundSource? backgroundSource; + private Container? proxyHost; + + [Resolved] + private EzSkinSettingsManager ezSkinConfig { get; set; } = null!; + + private Bindable columnBlur = new BindableDouble(); + private BufferedContainer? blurContainer; + private Drawable? compositeProxy; + + public bool HasSource => backgroundSource != null; + + public ManiaBackgroundBlurManager() + { + RelativeSizeAxes = Axes.Both; + Depth = -10; // draw behind columns (columns default depth 0) + } + + [BackgroundDependencyLoader] + private void load() + { + // 绑定ColumnBlur设置 + columnBlur = ezSkinConfig.GetBindable(EzSkinSetting.ColumnBlur); + columnBlur.BindValueChanged(_ => updateBlur(), true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + if (HasSource) + Schedule(initializeBlur); + } + + private void initializeBlur() + { + if (!HasSource) + { + Console.WriteLine("[ManiaBlur] initializeBlur called without source"); + return; + } + + // Prefer background+video, fallback to background only. + compositeProxy ??= backgroundSource!.CreateBackgroundWithVideoProxy() ?? backgroundSource.CreateBackgroundOnlyProxy(); + + if (compositeProxy == null) + { + Console.WriteLine("[ManiaBlur] No proxy content (null). Disabling blur."); + Alpha = 0; + return; + } + + Console.WriteLine($"[ManiaBlur] Source acquired (video-only attempt). Proxy type={compositeProxy.GetType().Name}"); + if (blurContainer == null) + setupBlurContainer(); + updateBlur(true); + } + + private void setupBlurContainer() + { + if (blurContainer != null) return; + + Console.WriteLine("[ManiaBlur] Setting up blur container"); + + blurContainer = new BufferedContainer(cachedFrameBuffer: true) + { + RelativeSizeAxes = Axes.Both, + Name = "ManiaPlayfieldBlur", + RedrawOnScale = false, + FrameBufferScale = new osuTK.Vector2(0.5f, 0.5f), + Children = new Drawable[] + { + proxyHost = new Container + { + RelativeSizeAxes = Axes.Both, + Name = "ProxyHost", + Child = compositeProxy ?? new Container { Name = "ProxyPlaceholder" } + } + } + }; + + AddInternal(blurContainer); + } + + private void updateBlur(bool force = false) + { + if (blurContainer == null) + return; + + if (compositeProxy == null) + { + if (force) + { + // One retry scheduled if first force update finds no proxy. + Schedule(() => + { + compositeProxy = backgroundSource?.CreateBackgroundOnlyProxy(); + + if (compositeProxy != null && proxyHost != null) + { + proxyHost.Clear(); + proxyHost.Child = compositeProxy; + Console.WriteLine("[ManiaBlur] Late proxy resolved."); + } + }); + } + + blurContainer.Alpha = 0; + return; + } + + float raw = (float)columnBlur.Value; + float blurAmount = raw * BackgroundScreenBeatmap.USER_BLUR_FACTOR; + if (raw > 0 && blurAmount < 2) + blurAmount = 2; + + bool enable = blurAmount > 0.001f; + + if (!enable) + { + blurContainer.BlurSigma = osuTK.Vector2.Zero; + blurContainer.Alpha = 0; + return; + } + + blurContainer.Alpha = 1; + var target = new osuTK.Vector2(blurAmount * 0.6f); + if (force || blurContainer.BlurSigma != target) + blurContainer.BlurSigma = target; + } + + public void SetSource(IGameplayBackgroundSource source) + { + backgroundSource = source; + Console.WriteLine("[ManiaBlur] SetSource invoked"); + + if (IsLoaded) + { + compositeProxy = backgroundSource.CreateBackgroundWithVideoProxy() ?? backgroundSource.CreateBackgroundOnlyProxy(); + if (compositeProxy == null) Console.WriteLine("[ManiaBlur] SetSource produced null proxy, will retry later."); + + if (proxyHost != null) + { + proxyHost.Clear(); + if (compositeProxy != null) + proxyHost.Child = compositeProxy; + } + + initializeBlur(); + } + } + + protected override void Update() + { + base.Update(); + updateBlur(); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index defd02b830..e7a6d8ecff 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; - [TestCase(6.7331304290522747d, 239, "diffcalc-test")] - [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] - [TestCase(0.43052813047866129d, 4, "very-fast-slider")] - [TestCase(0.14143808967817237d, 2, "nan-slider")] + [TestCase(6.6232533278125061d, 239, "diffcalc-test")] + [TestCase(1.5045783545699611d, 54, "zero-length-sliders")] + [TestCase(0.43333836671191595d, 4, "very-fast-slider")] + [TestCase(0.13841532030395723d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(9.6779746353001634d, 239, "diffcalc-test")] - [TestCase(1.7691451263718989d, 54, "zero-length-sliders")] - [TestCase(0.55785578988249407d, 4, "very-fast-slider")] + [TestCase(9.6491691624112761d, 239, "diffcalc-test")] + [TestCase(1.756936832498702d, 54, "zero-length-sliders")] + [TestCase(0.57771197086735004d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.7331304290522747d, 239, "diffcalc-test")] - [TestCase(1.4602604078137214d, 54, "zero-length-sliders")] - [TestCase(0.43052813047866129d, 4, "very-fast-slider")] + [TestCase(6.6232533278125061d, 239, "diffcalc-test")] + [TestCase(1.5045783545699611d, 54, "zero-length-sliders")] + [TestCase(0.43333836671191595d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-clear@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-clear@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8d1ac13c235d169a5412c1692980a8add6bac3e3 GIT binary patch literal 9933 zcmeHN&5zqe6!!{5OG^7cP z$lGCiJzsB~_ITW@F%kQ+Hs~cFttiKi4-&@D%Ut#4W*9ZJ->+WN)KE0Eb9T#UB}=js zE?-FH*$Zdd{K9!og?9X9_1M4z1wENFb^^+ z@pQf4@7MZfElxLeOexh3q7#BZ0%bc<&ITyTj!rZ*WyaGm$-_8O3ysW=xAKMtu8v~p zC8gRZ8{-6s=>wMNxMmbF4Fv@+<&v$mJ2Wo1F1xZPqdWsyT*@Y$IFGYVJVSLkeTxCW zw$&zpC*)b3pr{hNW~TxG5e7S@sEvSd1h)zHY~o>hizv`ou$;{m7TgQsw8vl% z!XDd{dJ=7FYPqxCQrwMGFbv+ALoZq_Zzald7IC@KY-kWdEewT6OhO&ZEo2#%i%4KG zQOyT&5Tb6}fVPWJ_ZX(OZmdl7MZHH-UvD+S~0hd^j zUkwe92iFVMb+<-?4l&@P(f&k(^KG1Q5=T(<}HUA&~9g3ZqCX#$9LUO9j2Ja_$Ufq5L60nJRI(_ zmwQ8Pq_*FYfY)7!I}*#{An&tOE^Y$Ia}z_K<$T7r@9ugKScbzK*kM!p$aR2=3>b(w zcB#n;2?z%o&vG474SS?aqDs*qLwxBnOanw6$3iBx1fqUGkfam{ z91y`d5a+gFpemS97>WX7)jhK;Aon>K9!J|Uog!AkQU=XbrddM^(zvIV1gD-ak%s6l>x&Wh3cvhs1T?Ss1T?Ss1T?Ss1T?Ss1T?Ss1T?Ss1T?S ys1W#nBJhdXxcJ=lUl+drLs3uf$94U{qn|xNK3)9$ui_uhR!*F0e*Eeim;MGCTucoB literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-rpm@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-rpm@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..544c860972d5b85d3448aecdafbda401f6d5f7e3 GIT binary patch literal 14667 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 S=s50Q0p+A`N##fwdi*amGri9M literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-spin@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-spin@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..57e602549b720ca3f71c91256672b00f44afd2ae GIT binary patch literal 20956 zcmeI4&5ImG6u>*0h?^KRen1exrjvsosp;`2xrjLm@mt9yAXd#+c4aoR#*K))ftl{AdiCnn ztM`6YJuvg;#F1xrZhv^YBuP8R$7++3wC%VgjXZMqUA*Se7v5>`%SbkMvXG=54~X{` z>HP9;NxJ8|cxt+wu0Q9~v{@lx8n8;MnQ>`J+JB&x5qgT1a=>Qdq^kV%$!CfjhgIc- zRoCj-D4UJPmU4D<>BtmaIz_!uIq;OczvXiUO;!@Q)odh%->NEYy*|Ge)v6-5MaolE z<)9c)o~}>GqiN1$uc9NWX;^l=N@Q3ji|9TXYuHeAUDZ6KW8c(#?8x1h!iUSl$e*ko z?heLFRb{p;GhbB~78WWCMkUQ>RNeDDRl_RAh)bYiF)2w4CB>eeh8inq9%p5oCbG~- zf^@E|D%{l09GY2QZBlgo#670ANT%u)P0Xn+7}CC6HkUWrgNIaQ4c25yS#VjsFU#Fj zulL0Vs%c3Bs0%k>`XVvTe)^V!N*88j5^l+|jg= z=Jhn>KIipfwSBIhn@U7}RTaHkIW0o9G1sYdu*P4iEMBHMH=~~RaR&)$8Z0^o>pLW~HRc+WXu%JUbOZ~C9;BmLu<;_v{ za`&zg%k9MU3GGaSiU?()(yfjMEb_+O*-^VN%ecdC61g3*B8|!glCy&|+{;5VLtW4K zg3RCCbi=U&!*g}Sh{eOoG9yIn$U}}zZKkn6$4=0j`XJMFo=3QsN4H{n*R6DxBr}Zf zM5=P@lfkZQ>zWrKBcK)vbd!Hrd@@?7^Q;MVSHnD_)>#M4w<7c3n+#2pd6usVtc>J@ z@1`s-s!EinO}Wn(xlCn}WsR7&(@mXE!tRFE&(QUq7P35E6$7hB>6;*L;&?+N&W%hO z8G49tmy4>C<2Coon4cf;&f6(r;(HP|CKc62ch8!_i7)1OCVDP;_XUM?fh+$%eC_hR${3x&M zjwb?0XiNX;5ksK9?S@sS^gtWl6ep;jQcU{`}t6-`kHvXD;kI zzD3+AU8Pxw=azl@9zey-i%ty?#tw&Wu43 zyaB-gxd12t5&{WmV= oC-RUB@CONi0wCevC*iw0UVLM>ySyA;7JJF~p(C{qpLyx*pNgmnga7~l literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-top@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/spinner-top@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0eb76b60e0d8918e31bdb3bc1f6d962452a63f GIT binary patch literal 659281 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

!Tz}slGJAh&HKZjq?*S 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 R}39yke@_AArLl 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 z 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

05FqQ-=XAodMr>?-J7)cah!^jffD2|%w|j$(s= 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�*Z=D9CYFb-;rqAFiqHs?GgGkeHs_ zWF<9fn|szzPm!mEc#p*HC=xFC0k4$vbC42V59p@$bK=bt{D}I+A@8nH{W5A1GeXt4Bf>SD(F&K4Di(v) zmB920csQC<6@vvlf1wuIHFBR^JxlV{s1l%&mWUZ-Z7F5svmv#T7G-s`e22Ji4-|<; z74l#B<-hSgSlDRh&SbR$y> zV{@7O!6y^chN?M|fo|BFm#9G>vNjR7paaEVX-p=ojqx9VKg87!O<#&lhU!yR+>%ld zd#HS)=E4k*;)1N*AQ+>K;RLDavv4e4Df8#pmt!C#ceu+#gpR?^j3f*c68qba-t~RZ zX>{j*y%ndTjwfNr(CftT*g^fP|4-rgbA$FfiCwt&0m$kwnPSb3pJ^B^9=8thp3m@b z(VzuAuA%v8&EuC-sL5~cWf0U4ke0z3Iu!N&6L{0G2rGvh&yJVR>?kIXNXQ8yK&?fA z#diR(x}#oz3Leb(vy$Rcx2G>%u`_< zbi2%2Z-+$mK5Mv_qeI&yrIQ*SyL+ctxBMv2!8ecbL;*cxhOZ6T#KxN2_z{EV@#KG4 z&+B(0OPN=5G+8-1V8oT(3>R{6hs(`5V2tVe(sO64As57bwQrjdbXms4;fW@J);G}F z=vSg9mnUQ};5^G>EsB8fsh)o0>=FL!Tt)~Dylm6lc5*cO-N`yU-X^?`{v2tE&G^zo zim=(aay4DJeL=nBM%a^C$uHv3bxix6Q^qJQw=!9tMR37-PUYf!8V^JPJNdLXw<|qrl1F3JINEW>4W}*vL zKeX?216M%!yZ1Xi{LD^RRCb&5Fk4x{(V2_pbS$r#R{SCdNn^~+oi7L1y_|R;+57G! z$mq{>!tKkJ+fc%v({wtmLJKR6f00*i%hy*w;4{Rxck*4dB`vt~CrUnggq_!%0#SRCztZcNdT{3&LRdpB;K+hW|g`b$!L6A6pemF4u(IEEgD?JszwRkakle3J&UPU zzcUh!1mVAhhbu4&8A5lFD{y;tw~*s6ap5Iz_B+@743r#+QPmYm{lrKjz--@gFl6i| z`NeqUS%5}&z5sdjUE5y?P0K^%WdVF~7|%#=qQEE$5Uis6AXLVlkVa9IpCu8a^pvUo zi~*+M+=ocMQ*pMhSlD^h zqk#Z-b#ufR?Pv~VK)<;7Y&Yk1jwtQ`3+oG%TWELeHSuR9GXe(8PJ29= zQ);)YHbw)YO`nx;^PGC8HD}n`3RcSt1mZ`pl{mPchbmf4f+@Mx(WP4~h-@USq^EvS z3I44+;(%=1+qhlAkLqOrd+A*1lYz$gA82MOC;^e)PBd#}plIm7?_5Aq<;fY2SoY({ z@IsXTkVI=Grr`c9&inQ-lMCc$MX${W@h1+h#4N`Bf}c00M@h6IB5{S=oj6=L?<66Y z7aJv?%%TWUR(7;z0r)||zxLTa6Mv}5uSP<~Kl075^Lg-j z*aALk1Rqio5{QC3o3DuDuWpO%R;;=~@u;}&%5-_5-1>57*%s?x^RKwy0hBj;QDlX% z&jce=S>HphB$%)x=X|Ifduq+ zZt@>ddCbz}diW5@2Z8M0M&zxB*0H1be>^ZdX}pbOP5(asF|%hZc>q;ROKP3b=xb@t z!x~VpX#bVhAj74`(iuMBpYbQ1C-BVtNnh5*wVRC9K#xfXYDVuHBi$q+eH?t55@`=4 z<{Bg0n=V7iVf7Sz&0X8U%F6TXZIK4S=V0(kCOZ@@bs>NfKe+v?F4#`!R+9Dwm-E}- zv^^jzHF^*M_a?Ehb*QQULbl6y85QK@E2Uq#ee1F6!_1r-St_!&=v
-wEMKLjUtk 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

gS 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 manualClock = new ManualClock diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index d1c92ed6a7..dcf8ac0fed 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.6; + private const double acute_angle_multiplier = 2.55; private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; private const double wiggle_multiplier = 1.02; @@ -34,12 +34,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var osuCurrObj = (OsuDifficultyHitObject)current; var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); + var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2); const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS; const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER; // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. - double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; + double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.AdjustedDeltaTime; // But if the last object is a slider, then we extend the travel velocity through the slider into the current object. if (osuLastObj.BaseObject is Slider && withSliderTravelDistance) @@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } // As above, do the same for the previous hitobject. - double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; + double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.AdjustedDeltaTime; if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance) { @@ -69,59 +70,77 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double aimStrain = currVelocity; // Start strain with regular velocity. - if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same. + if (osuCurrObj.Angle != null && osuLastObj.Angle != null) { - if (osuCurrObj.Angle != null && osuLastObj.Angle != null) + double currAngle = osuCurrObj.Angle.Value; + double lastAngle = osuLastObj.Angle.Value; + + // Rewarding angles, take the smaller velocity as base. + double angleBonus = Math.Min(currVelocity, prevVelocity); + + if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same. { - double currAngle = osuCurrObj.Angle.Value; - double lastAngle = osuLastObj.Angle.Value; - - // Rewarding angles, take the smaller velocity as base. - double angleBonus = Math.Min(currVelocity, prevVelocity); - - wideAngleBonus = calcWideAngleBonus(currAngle); acuteAngleBonus = calcAcuteAngleBonus(currAngle); // Penalize angle repetition. - wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3))); - // Apply full wide angle bonus for distance more than one diameter - wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); - // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter acuteAngleBonus *= angleBonus * - DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) * + DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2); + } - // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle - // https://www.desmos.com/calculator/dp0v0nvowc - wiggleBonus = angleBonus - * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) - * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) - * DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)) - * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter) - * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8) - * DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)); + wideAngleBonus = calcWideAngleBonus(currAngle); + + // Penalize angle repetition. + wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)); + + // Apply full wide angle bonus for distance more than one diameter + wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter); + + // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // https://www.desmos.com/calculator/dp0v0nvowc + wiggleBonus = angleBonus + * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)) + * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter) + * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8) + * DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60)); + + if (osuLast2Obj != null) + { + // If objects just go back and forth through a middle point - don't give as much wide bonus + // Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object + var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject; + var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject; + + float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length; + + if (distance < 1) + { + wideAngleBonus *= 1 - 0.35 * (1 - distance); + } } } if (Math.Max(prevVelocity, currVelocity) != 0) { // We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities. - prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime; - currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime; + prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.AdjustedDeltaTime; + currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.AdjustedDeltaTime; // Scale with ratio of difference compared to 0.5 * max dist. - double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); + double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1); // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. - double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); + double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity)); velocityChangeBonus = overlapVelocityBuff * distRatio; // Penalize for rhythm changes. - velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); + velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2); } if (osuLastObj.BaseObject is Slider) @@ -131,9 +150,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators } aimStrain += wiggleBonus * wiggle_multiplier; + aimStrain += velocityChangeBonus * velocity_change_multiplier; - // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. - aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); + // Add in acute angle bonus or wide angle bonus, whichever is larger. + aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier); + + // Apply high circle size bonus + aimStrain *= osuCurrObj.SmallCircleBonus; // Add in additional slider velocity bonus. if (withSliderTravelDistance) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 9d05f0b074..55192df7af 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var currentObj = (OsuDifficultyHitObject)current.Previous(i); var currentHitObject = (OsuHitObject)(currentObj.BaseObject); - cumulativeStrainTime += lastObj.StrainTime; + cumulativeStrainTime += lastObj.AdjustedDeltaTime; if (!(currentObj.BaseObject is Spinner)) { @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (osuCurrent.BaseObject is Slider osuSlider) { // Invert the scaling factor to determine the true travel distance independent of circle size. - double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor; + double pixelTravelDistance = osuCurrent.LazyTravelDistance / scalingFactor; // Reward sliders based on velocity. sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index d503dd2bcc..9349083951 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { private const int history_time_max = 5 * 1000; // 5 seconds private const int history_objects_max = 32; - private const double rhythm_overall_multiplier = 0.95; - private const double rhythm_ratio_multiplier = 12.0; + private const double rhythm_overall_multiplier = 1.0; + private const double rhythm_ratio_multiplier = 15.0; ///

/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (current.BaseObject is Spinner) return 0; + var currentOsuObject = (OsuDifficultyHitObject)current; + double rhythmComplexitySum = 0; double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3; @@ -62,22 +64,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count. - double currDelta = currObj.StrainTime; - double prevDelta = prevObj.StrainTime; - double lastDelta = lastObj.StrainTime; + // Use custom cap value to ensure that that at this point delta time is actually zero + double currDelta = Math.Max(currObj.DeltaTime, 1e-7); + double prevDelta = Math.Max(prevObj.DeltaTime, 1e-7); + double lastDelta = Math.Max(lastObj.DeltaTime, 1e-7); // calculate how much current delta difference deserves a rhythm bonus // this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200) - double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); - double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2)); + double deltaDifference = Math.Max(prevDelta, currDelta) / Math.Min(prevDelta, currDelta); + + // Take only the fractional part of the value since we're only interested in punishing multiples + double deltaDifferenceFraction = deltaDifference - Math.Truncate(deltaDifference); + + double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, DifficultyCalculationUtils.SmoothstepBellCurve(deltaDifferenceFraction)); // reduce ratio bonus if delta difference is too big - double fraction = Math.Max(prevDelta / currDelta, currDelta / prevDelta); - double fractionMultiplier = Math.Clamp(2.0 - fraction / 8.0, 0.0, 1.0); + double differenceMultiplier = Math.Clamp(2.0 - deltaDifference / 8.0, 0.0, 1.0); double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon); - double effectiveRatio = windowPenalty * currRatio * fractionMultiplier; + double effectiveRatio = windowPenalty * currRatio * differenceMultiplier; if (firstDeltaSwitch) { @@ -170,7 +176,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators prevObj = currObj; } - return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though) + double rhythmDifficulty = Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though) + rhythmDifficulty *= 1 - currentOsuObject.GetDoubletapness((OsuDifficultyHitObject)current.Next(0)); + + return rhythmDifficulty; } private class Island : IEquatable diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 769220ece0..a58c1d3685 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers private const double min_speed_bonus = 200; // 200 BPM 1/4th private const double speed_balancing_factor = 40; - private const double distance_multiplier = 0.9; + private const double distance_multiplier = 0.8; /// /// Evaluates the difficulty of tapping the current object, based on: @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; - double strainTime = osuCurrObj.StrainTime; + double strainTime = osuCurrObj.AdjustedDeltaTime; double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0)); // Cap deltatime to the OD 300 hitwindow. @@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier; + // Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps + distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus); + if (mods.OfType().Any()) distanceBonus = 0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index f7d8c649c1..9cab454142 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -53,12 +53,37 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("slider_factor")] public double SliderFactor { get; set; } + /// + /// Describes how much of is contributed to by hitcircles or sliders + /// A value closer to 0.0 indicates most of is contributed by hitcircles + /// A value closer to Infinity indicates most of is contributed by sliders + /// + [JsonProperty("aim_top_weighted_slider_factor")] + public double AimTopWeightedSliderFactor { get; set; } + + /// + /// Describes how much of is contributed to by hitcircles or sliders + /// A value closer to 0.0 indicates most of is contributed by hitcircles + /// A value closer to Infinity indicates most of is contributed by sliders + /// + [JsonProperty("speed_top_weighted_slider_factor")] + public double SpeedTopWeightedSliderFactor { get; set; } + [JsonProperty("aim_difficult_strain_count")] public double AimDifficultStrainCount { get; set; } [JsonProperty("speed_difficult_strain_count")] public double SpeedDifficultStrainCount { get; set; } + [JsonProperty("nested_score_per_object")] + public double NestedScorePerObject { get; set; } + + [JsonProperty("legacy_score_base_multiplier")] + public double LegacyScoreBaseMultiplier { get; set; } + + [JsonProperty("maximum_legacy_combo_score")] + public double MaximumLegacyComboScore { get; set; } + /// /// The beatmap's drain rate. This doesn't scale with rate-adjusting mods. /// @@ -97,6 +122,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount); + yield return (ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR, AimTopWeightedSliderFactor); + yield return (ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR, SpeedTopWeightedSliderFactor); + yield return (ATTRIB_ID_NESTED_SCORE_PER_OBJECT, NestedScorePerObject); + yield return (ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER, LegacyScoreBaseMultiplier); + yield return (ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE, MaximumLegacyComboScore); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -112,6 +142,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT]; + AimTopWeightedSliderFactor = values[ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR]; + SpeedTopWeightedSliderFactor = values[ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR]; + NestedScorePerObject = values[ATTRIB_ID_NESTED_SCORE_PER_OBJECT]; + LegacyScoreBaseMultiplier = values[ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER]; + MaximumLegacyComboScore = values[ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE]; DrainRate = onlineInfo.DrainRate; HitCircleCount = onlineInfo.CircleCount; SliderCount = onlineInfo.SliderCount; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index eb2cb95972..d7fa159d10 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -13,14 +11,17 @@ using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; +using osu.Game.Rulesets.Osu.Difficulty.Utils; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuDifficultyCalculator : DifficultyCalculator { - private const double difficulty_multiplier = 0.0675; + private const double star_rating_multiplier = 0.0265; public override int Version => 20250306; @@ -29,53 +30,84 @@ namespace osu.Game.Rulesets.Osu.Difficulty { } + public static double CalculateRateAdjustedApproachRate(double approachRate, double clockRate) + { + double preempt = IBeatmapDifficultyInfo.DifficultyRange(approachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN) / clockRate; + return IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); + } + + public static double CalculateRateAdjustedOverallDifficulty(double overallDifficulty, double clockRate) + { + HitWindows hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(overallDifficulty); + + double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; + + return (79.5 - hitWindowGreat) / 6; + } + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) return new OsuDifficultyAttributes { Mods = mods }; var aim = skills.OfType().Single(a => a.IncludeSliders); - double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier; - double aimDifficultyStrainCount = aim.CountTopWeightedStrains(); + var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders); + var speed = skills.OfType().Single(); + var flashlight = skills.OfType().SingleOrDefault(); + + double speedNotes = speed.RelevantNoteCount(); + + double aimDifficultStrainCount = aim.CountTopWeightedStrains(); + double speedDifficultStrainCount = speed.CountTopWeightedStrains(); + + double aimNoSlidersTopWeightedSliderCount = aimWithoutSliders.CountTopWeightedSliders(); + double aimNoSlidersDifficultStrainCount = aimWithoutSliders.CountTopWeightedStrains(); + + double aimTopWeightedSliderFactor = aimNoSlidersTopWeightedSliderCount / Math.Max(1, aimNoSlidersDifficultStrainCount - aimNoSlidersTopWeightedSliderCount); + + double speedTopWeightedSliderCount = speed.CountTopWeightedSliders(); + double speedTopWeightedSliderFactor = speedTopWeightedSliderCount / Math.Max(1, speedDifficultStrainCount - speedTopWeightedSliderCount); + double difficultSliders = aim.GetDifficultSliders(); - var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders); - double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier; - double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; + double approachRate = CalculateRateAdjustedApproachRate(beatmap.Difficulty.ApproachRate, clockRate); + double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate); - var speed = skills.OfType().Single(); - double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier; - double speedNotes = speed.RelevantNoteCount(); - double speedDifficultyStrainCount = speed.CountTopWeightedStrains(); + int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle); + int sliderCount = beatmap.HitObjects.Count(h => h is Slider); + int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); - var flashlight = skills.OfType().SingleOrDefault(); - double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier; + int totalHits = beatmap.HitObjects.Count; - if (mods.Any(m => m is OsuModTouchDevice)) - { - aimRating = Math.Pow(aimRating, 0.8); - flashlightRating = Math.Pow(flashlightRating, 0.8); - } + double drainRate = beatmap.Difficulty.DrainRate; - if (mods.Any(h => h is OsuModRelax)) - { - aimRating *= 0.9; - speedRating = 0.0; - flashlightRating *= 0.7; - } - else if (mods.Any(h => h is OsuModAutopilot)) - { - speedRating *= 0.5; - aimRating = 0.0; - flashlightRating *= 0.4; - } + double aimDifficultyValue = aim.DifficultyValue(); + double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue(); + double speedDifficultyValue = speed.DifficultyValue(); + + double mechanicalDifficultyRating = calculateMechanicalDifficultyRating(aimDifficultyValue, speedDifficultyValue); + double sliderFactor = aimDifficultyValue > 0 ? OsuRatingCalculator.CalculateDifficultyRating(aimNoSlidersDifficultyValue) / OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue) : 1; + + var osuRatingCalculator = new OsuRatingCalculator(mods, totalHits, approachRate, overallDifficulty, mechanicalDifficultyRating, sliderFactor); + + double aimRating = osuRatingCalculator.ComputeAimRating(aimDifficultyValue); + double speedRating = osuRatingCalculator.ComputeSpeedRating(speedDifficultyValue); + + double flashlightRating = 0.0; + + if (flashlight is not null) + flashlightRating = osuRatingCalculator.ComputeFlashlightRating(flashlight.DifficultyValue()); + + double sliderNestedScorePerObject = LegacyScoreUtils.CalculateNestedScorePerObject(beatmap, totalHits); + double legacyScoreBaseMultiplier = LegacyScoreUtils.CalculateDifficultyPeppyStars(beatmap); + + var simulator = new OsuLegacyScoreSimulator(); + var scoreAttributes = simulator.Simulate(WorkingBeatmap, beatmap); double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating); double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating); - double baseFlashlightPerformance = 0.0; - - if (mods.Any(h => h is OsuModFlashlight)) - baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating); + double baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating); double basePerformance = Math.Pow( @@ -84,15 +116,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 ); - double starRating = basePerformance > 0.00001 - ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) - : 0; - - double drainRate = beatmap.Difficulty.DrainRate; - - int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); - int sliderCount = beatmap.HitObjects.Count(h => h is Slider); - int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); + double starRating = calculateStarRating(basePerformance); OsuDifficultyAttributes attributes = new OsuDifficultyAttributes { @@ -104,18 +128,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty SpeedNoteCount = speedNotes, FlashlightDifficulty = flashlightRating, SliderFactor = sliderFactor, - AimDifficultStrainCount = aimDifficultyStrainCount, - SpeedDifficultStrainCount = speedDifficultyStrainCount, + AimDifficultStrainCount = aimDifficultStrainCount, + SpeedDifficultStrainCount = speedDifficultStrainCount, + AimTopWeightedSliderFactor = aimTopWeightedSliderFactor, + SpeedTopWeightedSliderFactor = speedTopWeightedSliderFactor, DrainRate = drainRate, MaxCombo = beatmap.GetMaxCombo(), - HitCircleCount = hitCirclesCount, + HitCircleCount = hitCircleCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, + NestedScorePerObject = sliderNestedScorePerObject, + LegacyScoreBaseMultiplier = legacyScoreBaseMultiplier, + MaximumLegacyComboScore = scoreAttributes.ComboScore }; return attributes; } + private double calculateMechanicalDifficultyRating(double aimDifficultyValue, double speedDifficultyValue) + { + double aimValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(aimDifficultyValue)); + double speedValue = OsuStrainSkill.DifficultyToPerformance(OsuRatingCalculator.CalculateDifficultyRating(speedDifficultyValue)); + + double totalValue = Math.Pow(Math.Pow(aimValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1); + + return calculateStarRating(totalValue); + } + + private double calculateStarRating(double basePerformance) + { + if (basePerformance <= 0.00001) + return 0; + + return Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * star_rating_multiplier * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4); + } + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List objects = new List(); @@ -124,8 +171,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // If the map has less than two OsuHitObjects, the enumerator will not return anything. for (int i = 1; i < beatmap.HitObjects.Count; i++) { - var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null; - objects.Add(new OsuDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], lastLast, clockRate, objects, objects.Count)); + objects.Add(new OsuDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate, objects, objects.Count)); } return objects; @@ -154,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty new OsuModEasy(), new OsuModHardRock(), new OsuModFlashlight(), - new MultiMod(new OsuModFlashlight(), new OsuModHidden()) + new OsuModHidden(), }; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs new file mode 100644 index 0000000000..0d406ea72a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreMissCalculator.cs @@ -0,0 +1,200 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Osu.Difficulty +{ + public class OsuLegacyScoreMissCalculator + { + private readonly ScoreInfo score; + private readonly OsuDifficultyAttributes attributes; + + public OsuLegacyScoreMissCalculator(ScoreInfo scoreInfo, OsuDifficultyAttributes attributes) + { + score = scoreInfo; + this.attributes = attributes; + } + + public double Calculate() + { + if (attributes.MaxCombo == 0 || score.LegacyTotalScore == null) + return 0; + + double scoreV1Multiplier = attributes.LegacyScoreBaseMultiplier * getLegacyScoreMultiplier(); + double relevantComboPerObject = calculateRelevantScoreComboPerObject(); + + double maximumMissCount = calculateMaximumComboBasedMissCount(); + + double scoreObtainedDuringMaxCombo = calculateScoreAtCombo(score.MaxCombo, relevantComboPerObject, scoreV1Multiplier); + double remainingScore = score.LegacyTotalScore.Value - scoreObtainedDuringMaxCombo; + + if (remainingScore <= 0) + return maximumMissCount; + + double remainingCombo = attributes.MaxCombo - score.MaxCombo; + double expectedRemainingScore = calculateScoreAtCombo(remainingCombo, relevantComboPerObject, scoreV1Multiplier); + + double scoreBasedMissCount = expectedRemainingScore / remainingScore; + + // If there's less then one miss detected - let combo-based miss count decide if this is FC or not + scoreBasedMissCount = Math.Max(scoreBasedMissCount, 1); + + // Cap result by very harsh version of combo-based miss count + return Math.Min(scoreBasedMissCount, maximumMissCount); + } + + /// + /// Calculates the amount of score that would be achieved at a given combo. + /// + private double calculateScoreAtCombo(double combo, double relevantComboPerObject, double scoreV1Multiplier) + { + int countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); + int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); + int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); + int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + + int totalHits = countGreat + countOk + countMeh + countMiss; + + double estimatedObjects = combo / relevantComboPerObject - 1; + + // The combo portion of ScoreV1 follows arithmetic progression + // Therefore, we calculate the combo portion of score using the combo per object and our current combo. + double comboScore = relevantComboPerObject > 0 ? (2 * (relevantComboPerObject - 1) + (estimatedObjects - 1) * relevantComboPerObject) * estimatedObjects / 2 : 0; + + // We then apply the accuracy and ScoreV1 multipliers to the resulting score. + comboScore *= score.Accuracy * 300 / 25 * scoreV1Multiplier; + + double objectsHit = (totalHits - countMiss) * combo / attributes.MaxCombo; + + // Score also has a non-combo portion we need to create the final score value. + double nonComboScore = (300 + attributes.NestedScorePerObject) * score.Accuracy * objectsHit; + + return comboScore + nonComboScore; + } + + /// + /// Calculates the relevant combo per object for legacy score. + /// This assumes a uniform distribution for circles and sliders. + /// This handles cases where objects (such as buzz sliders) do not fit a normal arithmetic progression model. + /// + private double calculateRelevantScoreComboPerObject() + { + double comboScore = attributes.MaximumLegacyComboScore; + + // We then reverse apply the ScoreV1 multipliers to get the raw value. + comboScore /= 300.0 / 25.0 * attributes.LegacyScoreBaseMultiplier; + + // Reverse the arithmetic progression to work out the amount of combo per object based on the score. + double result = (attributes.MaxCombo - 2) * attributes.MaxCombo; + result /= Math.Max(attributes.MaxCombo + 2 * (comboScore - 1), 1); + + return result; + } + + /// + /// This function is a harsher version of current combo-based miss count, used to provide reasonable value for cases where score-based miss count can't do this. + /// + private double calculateMaximumComboBasedMissCount() + { + int countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + + if (attributes.SliderCount <= 0) + return countMiss; + + int countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); + int countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); + + int totalImperfectHits = countOk + countMeh + countMiss; + + double missCount = 0; + + // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it + // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map + double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount; + + if (score.MaxCombo < fullComboThreshold) + missCount = Math.Pow(fullComboThreshold / Math.Max(1.0, score.MaxCombo), 2.5); + + // In classic scores there can't be more misses than a sum of all non-perfect judgements + missCount = Math.Min(missCount, totalImperfectHits); + + // Every slider has *at least* 2 combo attributed in classic mechanics. + // If they broke on a slider with a tick, then this still works since they would have lost at least 2 combo (the tick and the end) + // Using this as a max means a score that loses 1 combo on a map can't possibly have been a slider break. + // It must have been a slider end. + int maxPossibleSliderBreaks = Math.Min(attributes.SliderCount, (attributes.MaxCombo - score.MaxCombo) / 2); + + int scoreMissCount = score.Statistics.GetValueOrDefault(HitResult.Miss); + + double sliderBreaks = missCount - scoreMissCount; + + if (sliderBreaks > maxPossibleSliderBreaks) + missCount = scoreMissCount + maxPossibleSliderBreaks; + + return missCount; + } + + /// + /// Logic copied from . + /// + private double getLegacyScoreMultiplier() + { + bool scoreV2 = score.Mods.Any(m => m is ModScoreV2); + + double multiplier = 1.0; + + foreach (var mod in score.Mods) + { + switch (mod) + { + case OsuModNoFail: + multiplier *= scoreV2 ? 1.0 : 0.5; + break; + + case OsuModEasy: + multiplier *= 0.5; + break; + + case OsuModHalfTime: + case OsuModDaycore: + multiplier *= 0.3; + break; + + case OsuModHidden: + multiplier *= 1.06; + break; + + case OsuModHardRock: + multiplier *= scoreV2 ? 1.10 : 1.06; + break; + + case OsuModDoubleTime: + case OsuModNightcore: + multiplier *= scoreV2 ? 1.20 : 1.12; + break; + + case OsuModFlashlight: + multiplier *= 1.12; + break; + + case OsuModSpunOut: + multiplier *= 0.9; + break; + + case OsuModRelax: + case OsuModAutopilot: + return 0; + } + } + + return multiplier; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index de4491a31b..8577eff11f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -27,6 +27,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("speed_deviation")] public double? SpeedDeviation { get; set; } + [JsonProperty("combo_based_estimated_miss_count")] + public double ComboBasedEstimatedMissCount { get; set; } + + [JsonProperty("score_based_estimated_miss_count")] + public double? ScoreBasedEstimatedMissCount { get; set; } + + [JsonProperty("aim_estimated_slider_breaks")] + public double AimEstimatedSliderBreaks { get; set; } + + [JsonProperty("speed_estimated_slider_breaks")] + public double SpeedEstimatedSliderBreaks { get; set; } + public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 41b0947fbb..741ddb3d4f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -4,25 +4,25 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Utils; namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. private bool usingClassicSliderAccuracy; + private bool usingScoreV2; private double accuracy; private int scoreMaxCombo; @@ -55,6 +55,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double? speedDeviation; + private double aimEstimatedSliderBreaks; + private double speedEstimatedSliderBreaks; + public OsuPerformanceCalculator() : base(new OsuRuleset()) { @@ -65,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty var osuAttributes = (OsuDifficultyAttributes)attributes; usingClassicSliderAccuracy = score.Mods.OfType().Any(m => m.NoSliderHeadAccuracy.Value); + usingScoreV2 = score.Mods.Any(m => m is ModScoreV2); accuracy = score.Accuracy; scoreMaxCombo = score.MaxCombo; @@ -80,9 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty)); - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - clockRate = track.Rate; + clockRate = ModUtils.CalculateRateWithMods(score.Mods); HitWindows hitWindows = new OsuHitWindows(); hitWindows.SetDifficulty(difficulty.OverallDifficulty); @@ -91,35 +93,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate; mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate; - double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + approachRate = OsuDifficultyCalculator.CalculateRateAdjustedApproachRate(difficulty.ApproachRate, clockRate); + overallDifficulty = OsuDifficultyCalculator.CalculateRateAdjustedOverallDifficulty(difficulty.OverallDifficulty, clockRate); - overallDifficulty = (79.5 - greatHitWindow) / 6; - approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; + double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes); + double? scoreBasedEstimatedMissCount = null; - if (osuAttributes.SliderCount > 0) + if (usingClassicSliderAccuracy && score.LegacyTotalScore != null) { - if (usingClassicSliderAccuracy) - { - // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it - // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map - double fullComboThreshold = attributes.MaxCombo - 0.1 * osuAttributes.SliderCount; + var legacyScoreMissCalculator = new OsuLegacyScoreMissCalculator(score, osuAttributes); + scoreBasedEstimatedMissCount = legacyScoreMissCalculator.Calculate(); - if (scoreMaxCombo < fullComboThreshold) - effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); - - // In classic scores there can't be more misses than a sum of all non-perfect judgements - effectiveMissCount = Math.Min(effectiveMissCount, totalImperfectHits); - } - else - { - double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped; - - if (scoreMaxCombo < fullComboThreshold) - effectiveMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); - - // Combine regular misses with tick misses since tick misses break combo as well - effectiveMissCount = Math.Min(effectiveMissCount, countSliderTickMiss + countMiss); - } + effectiveMissCount = scoreBasedEstimatedMissCount.Value; + } + else + { + // Use combo-based miss count if this isn't a legacy score + effectiveMissCount = comboBasedEstimatedMissCount; } effectiveMissCount = Math.Max(countMiss, effectiveMissCount); @@ -135,10 +125,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) { - // https://www.desmos.com/calculator/bc9eybdthb + // https://www.desmos.com/calculator/vspzsop6td // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) - double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0); + double okMultiplier = 0.75 * Math.Max(0.0, overallDifficulty > 0.0 ? 1 - overallDifficulty / 13.33 : 1.0); double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0); // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. @@ -167,6 +157,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + ComboBasedEstimatedMissCount = comboBasedEstimatedMissCount, + ScoreBasedEstimatedMissCount = scoreBasedEstimatedMissCount, + AimEstimatedSliderBreaks = aimEstimatedSliderBreaks, + SpeedEstimatedSliderBreaks = speedEstimatedSliderBreaks, SpeedDeviation = speedDeviation, Total = totalValue }; @@ -207,30 +201,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; if (effectiveMissCount > 0) - aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount); + { + aimEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.AimTopWeightedSliderFactor, attributes); - double approachRateFactor = 0.0; - if (approachRate > 10.33) - approachRateFactor = 0.3 * (approachRate - 10.33); - else if (approachRate < 8.0) - approachRateFactor = 0.05 * (8.0 - approachRate); + double relevantMissCount = Math.Min(effectiveMissCount + aimEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss); - if (score.Mods.Any(h => h is OsuModRelax)) - approachRateFactor = 0.0; - - aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + aimValue *= calculateMissPenalty(relevantMissCount, attributes.AimDifficultStrainCount); + } + // TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen. if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); - else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) + else if (score.Mods.Any(m => m is OsuModTraceable)) { - // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - aimValue *= 1.0 + 0.04 * (12.0 - approachRate); + aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate, sliderFactor: attributes.SliderFactor); } aimValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return aimValue; } @@ -247,26 +234,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; if (effectiveMissCount > 0) - speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount); + { + speedEstimatedSliderBreaks = calculateEstimatedSliderBreaks(attributes.SpeedTopWeightedSliderFactor, attributes); - double approachRateFactor = 0.0; - if (approachRate > 10.33) - approachRateFactor = 0.3 * (approachRate - 10.33); + double relevantMissCount = Math.Min(effectiveMissCount + speedEstimatedSliderBreaks, totalImperfectHits + countSliderTickMiss); - if (score.Mods.Any(h => h is OsuModAutopilot)) - approachRateFactor = 0.0; - - speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + speedValue *= calculateMissPenalty(relevantMissCount, attributes.SpeedDifficultStrainCount); + } + // TC bonuses are excluded when blinds is present as the increased visual difficulty is unimportant when notes cannot be seen. if (score.Mods.Any(m => m is OsuModBlinds)) { // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. speedValue *= 1.12; } - else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) + else if (score.Mods.Any(m => m is OsuModTraceable)) { - // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - speedValue *= 1.0 + 0.04 * (12.0 - approachRate); + speedValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate); } double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes); @@ -280,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2); + speedValue *= Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2); return speedValue; } @@ -293,7 +277,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = attributes.HitCircleCount; - if (!usingClassicSliderAccuracy) + if (!usingClassicSliderAccuracy || usingScoreV2) amountHitObjectsWithAccuracy += attributes.SliderCount; if (amountHitObjectsWithAccuracy > 0) @@ -316,7 +300,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModBlinds)) accuracyValue *= 1.14; else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable)) - accuracyValue *= 1.08; + { + // Decrease bonus for AR > 10 + accuracyValue *= 1 + 0.08 * DifficultyCalculationUtils.ReverseLerp(approachRate, 11.5, 10); + } if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; @@ -337,18 +324,73 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= getComboScalingFactor(attributes); - // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. - flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + - (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); - // Scale the flashlight value with accuracy _slightly_. flashlightValue *= 0.5 + accuracy / 2.0; - // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; return flashlightValue; } + private double calculateComboBasedEstimatedMissCount(OsuDifficultyAttributes attributes) + { + if (attributes.SliderCount <= 0) + return countMiss; + + double missCount = countMiss; + + if (usingClassicSliderAccuracy) + { + // Consider that full combo is maximum combo minus dropped slider tails since they don't contribute to combo but also don't break it + // In classic scores we can't know the amount of dropped sliders so we estimate to 10% of all sliders on the map + double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount; + + if (scoreMaxCombo < fullComboThreshold) + missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); + + // In classic scores there can't be more misses than a sum of all non-perfect judgements + missCount = Math.Min(missCount, totalImperfectHits); + + // Every slider has *at least* 2 combo attributed in classic mechanics. + // If they broke on a slider with a tick, then this still works since they would have lost at least 2 combo (the tick and the end) + // Using this as a max means a score that loses 1 combo on a map can't possibly have been a slider break. + // It must have been a slider end. + int maxPossibleSliderBreaks = Math.Min(attributes.SliderCount, (attributes.MaxCombo - scoreMaxCombo) / 2); + + double sliderBreaks = missCount - countMiss; + + if (sliderBreaks > maxPossibleSliderBreaks) + missCount = countMiss + maxPossibleSliderBreaks; + } + else + { + double fullComboThreshold = attributes.MaxCombo - countSliderEndsDropped; + + if (scoreMaxCombo < fullComboThreshold) + missCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); + + // Combine regular misses with tick misses since tick misses break combo as well + missCount = Math.Min(missCount, countSliderTickMiss + countMiss); + } + + return missCount; + } + + private double calculateEstimatedSliderBreaks(double topWeightedSliderFactor, OsuDifficultyAttributes attributes) + { + if (!usingClassicSliderAccuracy || countOk == 0) + return 0; + + double missedComboPercent = 1.0 - (double)scoreMaxCombo / attributes.MaxCombo; + double estimatedSliderBreaks = Math.Min(countOk, effectiveMissCount * topWeightedSliderFactor); + + // Scores with more Oks are more likely to have slider breaks. + double okAdjustment = ((countOk - estimatedSliderBreaks) + 0.5) / countOk; + + // There is a low probability of extra slider breaks on effective miss counts close to 1, as score based calculations are good at indicating if only a single break occurred. + estimatedSliderBreaks *= DifficultyCalculationUtils.Smoothstep(effectiveMissCount, 1, 2); + + return estimatedSliderBreaks * okAdjustment * DifficultyCalculationUtils.Logistic(missedComboPercent, 0.33, 15); + } + /// /// Estimates player's deviation on speed notes using , assuming worst-case. /// Treats all speed notes as hit circles. @@ -368,7 +410,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh); double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk); - return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + return calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh); } /// @@ -377,45 +419,45 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution. /// - private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss) + private double? calculateDeviation(double relevantCountGreat, double relevantCountOk, double relevantCountMeh) { if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) return null; - double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; - - // The probability that a player hits a circle is unknown, but we can estimate it to be - // the number of greats on circles divided by the number of circles, and then add one - // to the number of circles as a bias correction. - double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh); - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - - // Proportion of greats hit on circles, ignoring misses and 50s. + // The sample proportion of successful hits. + double n = Math.Max(1, relevantCountGreat + relevantCountOk); double p = relevantCountGreat / n; - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); + // 99% critical value for the normal distribution (one-tailed). + const double z = 2.32634787404; - // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. - // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: - double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); + // We can be 99% confident that the population proportion is at least this value. + double pLowerBound = Math.Min(p, (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4)); - double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) - / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); + double deviation; - deviation *= Math.Sqrt(1 - randomValue); + // Tested max precision for the deviation calculation. + if (pLowerBound > 0.01) + { + // Compute deviation assuming greats and oks are normally distributed. + deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound)); - // Value deviation approach as greatCount approaches 0 - double limitValue = okHitWindow / Math.Sqrt(3); + // Subtract the deviation provided by tails that land outside the ok hit window from the deviation computed above. + // This is equivalent to calculating the deviation of a normal distribution truncated at +-okHitWindow. + double okHitWindowTailAmount = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2)) + / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation))); - // If precision is not enough to compute true deviation - use limit value - if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue) - deviation = limitValue; + deviation *= Math.Sqrt(1 - okHitWindowTailAmount); + } + else + { + // A tested limit value for the case of a score only containing oks. + deviation = okHitWindow / Math.Sqrt(3); + } - // Then compute the variance for mehs. + // Compute and add the variance for mehs, assuming that they are uniformly distributed. double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3; - // Find the total deviation. deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); return deviation; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs new file mode 100644 index 0000000000..2a050c0920 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuRatingCalculator.cs @@ -0,0 +1,213 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Difficulty +{ + public class OsuRatingCalculator + { + private const double difficulty_multiplier = 0.0675; + + private readonly Mod[] mods; + private readonly int totalHits; + private readonly double approachRate; + private readonly double overallDifficulty; + private readonly double mechanicalDifficultyRating; + private readonly double sliderFactor; + + public OsuRatingCalculator(Mod[] mods, int totalHits, double approachRate, double overallDifficulty, double mechanicalDifficultyRating, double sliderFactor) + { + this.mods = mods; + this.totalHits = totalHits; + this.approachRate = approachRate; + this.overallDifficulty = overallDifficulty; + this.mechanicalDifficultyRating = mechanicalDifficultyRating; + this.sliderFactor = sliderFactor; + } + + public double ComputeAimRating(double aimDifficultyValue) + { + if (mods.Any(m => m is OsuModAutopilot)) + return 0; + + double aimRating = CalculateDifficultyRating(aimDifficultyValue); + + if (mods.Any(m => m is OsuModTouchDevice)) + aimRating = Math.Pow(aimRating, 0.8); + + if (mods.Any(m => m is OsuModRelax)) + aimRating *= 0.9; + + if (mods.Any(m => m is OsuModMagnetised)) + { + float magnetisedStrength = mods.OfType().First().AttractionStrength.Value; + aimRating *= 1.0 - magnetisedStrength; + } + + double ratingMultiplier = 1.0; + + double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); + + double approachRateFactor = 0.0; + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); + else if (approachRate < 8.0) + approachRateFactor = 0.05 * (8.0 - approachRate); + + if (mods.Any(h => h is OsuModRelax)) + approachRateFactor = 0.0; + + ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR. + + if (mods.Any(m => m is OsuModHidden)) + { + double visibilityFactor = calculateAimVisibilityFactor(approachRate); + ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor, sliderFactor); + } + + // It is important to consider accuracy difficulty when scaling with accuracy. + ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; + + return aimRating * Math.Cbrt(ratingMultiplier); + } + + public double ComputeSpeedRating(double speedDifficultyValue) + { + if (mods.Any(m => m is OsuModRelax)) + return 0; + + double speedRating = CalculateDifficultyRating(speedDifficultyValue); + + if (mods.Any(m => m is OsuModAutopilot)) + speedRating *= 0.5; + + if (mods.Any(m => m is OsuModMagnetised)) + { + // reduce speed rating because of the speed distance scaling, with maximum reduction being 0.7x + float magnetisedStrength = mods.OfType().First().AttractionStrength.Value; + speedRating *= 1.0 - magnetisedStrength * 0.3; + } + + double ratingMultiplier = 1.0; + + double approachRateLengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); + + double approachRateFactor = 0.0; + if (approachRate > 10.33) + approachRateFactor = 0.3 * (approachRate - 10.33); + + if (mods.Any(m => m is OsuModAutopilot)) + approachRateFactor = 0.0; + + ratingMultiplier += approachRateFactor * approachRateLengthBonus; // Buff for longer maps with high AR. + + if (mods.Any(m => m is OsuModHidden)) + { + double visibilityFactor = calculateSpeedVisibilityFactor(approachRate); + ratingMultiplier += CalculateVisibilityBonus(mods, approachRate, visibilityFactor); + } + + ratingMultiplier *= 0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750; + + return speedRating * Math.Cbrt(ratingMultiplier); + } + + public double ComputeFlashlightRating(double flashlightDifficultyValue) + { + if (!mods.Any(m => m is OsuModFlashlight)) + return 0; + + double flashlightRating = CalculateDifficultyRating(flashlightDifficultyValue); + + if (mods.Any(m => m is OsuModTouchDevice)) + flashlightRating = Math.Pow(flashlightRating, 0.8); + + if (mods.Any(m => m is OsuModRelax)) + flashlightRating *= 0.7; + else if (mods.Any(m => m is OsuModAutopilot)) + flashlightRating *= 0.4; + + if (mods.Any(m => m is OsuModMagnetised)) + { + float magnetisedStrength = mods.OfType().First().AttractionStrength.Value; + flashlightRating *= 1.0 - magnetisedStrength; + } + + if (mods.Any(m => m is OsuModDeflate)) + { + float deflateInitialScale = mods.OfType().First().StartScale.Value; + flashlightRating *= Math.Clamp(DifficultyCalculationUtils.ReverseLerp(deflateInitialScale, 11, 1), 0.1, 1); + } + + double ratingMultiplier = 1.0; + + // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. + ratingMultiplier *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + + (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); + + // It is important to consider accuracy difficulty when scaling with accuracy. + ratingMultiplier *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500; + + return flashlightRating * Math.Sqrt(ratingMultiplier); + } + + private double calculateAimVisibilityFactor(double approachRate) + { + const double ar_factor_end_point = 11.5; + + double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10); + double arFactorStartingPoint = double.Lerp(9, 10.33, mechanicalDifficultyFactor); + + return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint); + } + + private double calculateSpeedVisibilityFactor(double approachRate) + { + const double ar_factor_end_point = 11.5; + + double mechanicalDifficultyFactor = DifficultyCalculationUtils.ReverseLerp(mechanicalDifficultyRating, 5, 10); + double arFactorStartingPoint = double.Lerp(10, 10.33, mechanicalDifficultyFactor); + + return DifficultyCalculationUtils.ReverseLerp(approachRate, ar_factor_end_point, arFactorStartingPoint); + } + + /// + /// Calculates a visibility bonus that is applicable to Hidden and Traceable. + /// + public static double CalculateVisibilityBonus(Mod[] mods, double approachRate, double visibilityFactor = 1, double sliderFactor = 1) + { + // NOTE: TC's effect is only noticeable in performance calculations until lazer mods are accounted for server-side. + bool isAlwaysPartiallyVisible = mods.OfType().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType().Any(); + + // Start from normal curve, rewarding lower AR up to AR7 + // TC forcefully requires a lower reading bonus for now as it's post-applied in PP which makes it multiplicative with the regular AR bonuses + // This means it has an advantage over HD, so we decrease the multiplier to compensate + // This should be removed once we're able to apply TC bonuses in SR (depends on real-time difficulty calculations being possible) + double readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) * (12.0 - Math.Max(approachRate, 7)); + + readingBonus *= visibilityFactor; + + // We want to reward slideraim on low AR less + double sliderVisibilityFactor = Math.Pow(sliderFactor, 3); + + // For AR up to 0 - reduce reward for very low ARs when object is visible + if (approachRate < 7) + readingBonus += (isAlwaysPartiallyVisible ? 0.02 : 0.045) * (7.0 - Math.Max(approachRate, 0)) * sliderVisibilityFactor; + + // Starting from AR0 - cap values so they won't grow to infinity + if (approachRate < 0) + readingBonus += (isAlwaysPartiallyVisible ? 0.01 : 0.1) * (1 - Math.Pow(1.5, approachRate)) * sliderVisibilityFactor; + + return readingBonus; + } + + public static double CalculateDifficultyRating(double difficultyValue) => Math.Sqrt(difficultyValue) * difficulty_multiplier; + } +} diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 5e4c5c1ee9..5e9fc10ef8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -28,11 +28,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; + protected new OsuHitObject LastObject => (OsuHitObject)base.LastObject; /// - /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. + /// capped to a minimum of ms. /// - public readonly double StrainTime; + public readonly double AdjustedDeltaTime; /// /// Normalised distance from the "lazy" end position of the previous to the start position of this . @@ -75,6 +76,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double TravelTime { get; private set; } + /// + /// The position of the cursor at the point of completion of this if it is a + /// and was hit with as few movements as possible. + /// + public Vector2? LazyEndPosition { get; private set; } + + /// + /// The distance travelled by the cursor upon completion of this if it is a + /// and was hit with as few movements as possible. + /// + public double LazyTravelDistance { get; private set; } + + /// + /// The time taken by the cursor upon completion of this if it is a + /// and was hit with as few movements as possible. + /// + public double LazyTravelTime { get; private set; } + /// /// Angle the player has to take to hit this . /// Calculated as the angle between the circles (current-2, current-1, current). @@ -86,17 +105,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double HitWindowGreat { get; private set; } - private readonly OsuHitObject? lastLastObject; - private readonly OsuHitObject lastObject; + /// + /// Selective bonus for maps with higher circle size. + /// + public double SmallCircleBonus { get; private set; } - public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject? lastLastObject, double clockRate, List objects, int index) + private readonly OsuDifficultyHitObject? lastLastDifficultyObject; + private readonly OsuDifficultyHitObject? lastDifficultyObject; + + public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List objects, int index) : base(hitObject, lastObject, clockRate, objects, index) { - this.lastLastObject = lastLastObject as OsuHitObject; - this.lastObject = (OsuHitObject)lastObject; + lastLastDifficultyObject = index > 1 ? (OsuDifficultyHitObject)objects[index - 2] : null; + lastDifficultyObject = index > 0 ? (OsuDifficultyHitObject)objects[index - 1] : null; // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. - StrainTime = Math.Max(DeltaTime, MIN_DELTA_TIME); + AdjustedDeltaTime = Math.Max(DeltaTime, MIN_DELTA_TIME); + + SmallCircleBonus = Math.Max(1.0, 1.0 + (30 - BaseObject.Radius) / 40); if (BaseObject is Slider sliderObject) { @@ -107,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing HitWindowGreat = 2 * BaseObject.HitWindows.WindowFor(HitResult.Great) / clockRate; } + computeSliderCursorPosition(); setDistances(clockRate); } @@ -161,35 +188,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { if (BaseObject is Slider currentSlider) { - computeSliderCursorPosition(currentSlider); // Bonus for repeat sliders until a better per nested object strain system can be achieved. - TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5); - TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME); + TravelDistance = LazyTravelDistance * Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5); + TravelTime = Math.Max(LazyTravelTime / clockRate, MIN_DELTA_TIME); } // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner - if (BaseObject is Spinner || lastObject is Spinner) + if (BaseObject is Spinner || LastObject is Spinner) return; // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius; - if (BaseObject.Radius < 30) - { - float smallCircleBonus = Math.Min(30 - (float)BaseObject.Radius, 5) / 50; - scalingFactor *= 1 + smallCircleBonus; - } - - Vector2 lastCursorPosition = getEndCursorPosition(lastObject); + Vector2 lastCursorPosition = lastDifficultyObject != null ? getEndCursorPosition(lastDifficultyObject) : LastObject.StackedPosition; LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; - MinimumJumpTime = StrainTime; + MinimumJumpTime = AdjustedDeltaTime; MinimumJumpDistance = LazyJumpDistance; - if (lastObject is Slider lastSlider) + if (LastObject is Slider lastSlider && lastDifficultyObject != null) { - double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME); - MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, MIN_DELTA_TIME); + double lastTravelTime = Math.Max(lastDifficultyObject.LazyTravelTime / clockRate, MIN_DELTA_TIME); + MinimumJumpTime = Math.Max(AdjustedDeltaTime - lastTravelTime, MIN_DELTA_TIME); // // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects. @@ -217,11 +237,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius)); } - if (lastLastObject != null && !(lastLastObject is Spinner)) + if (lastLastDifficultyObject != null && lastLastDifficultyObject.BaseObject is not Spinner) { - Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject); + Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastDifficultyObject); - Vector2 v1 = lastLastCursorPosition - lastObject.StackedPosition; + Vector2 v1 = lastLastCursorPosition - LastObject.StackedPosition; Vector2 v2 = BaseObject.StackedPosition - lastCursorPosition; float dot = Vector2.Dot(v1, v2); @@ -231,9 +251,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing } } - private void computeSliderCursorPosition(Slider slider) + private void computeSliderCursorPosition() { - if (slider.LazyEndPosition != null) + if (BaseObject is not Slider slider) + return; + + if (LazyEndPosition != null) return; // TODO: This commented version is actually correct by the new lazer implementation, but intentionally held back from @@ -280,15 +303,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing nestedObjects = reordered; } - slider.LazyTravelTime = trackingEndTime - slider.StartTime; + LazyTravelTime = trackingEndTime - slider.StartTime; - double endTimeMin = slider.LazyTravelTime / slider.SpanDuration; + double endTimeMin = LazyTravelTime / slider.SpanDuration; if (endTimeMin % 2 >= 1) endTimeMin = 1 - endTimeMin % 1; else endTimeMin %= 1; - slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. + LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. Vector2 currCursorPosition = slider.StackedPosition; @@ -310,7 +333,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // There is both a lazy end position as well as the actual end slider position. We assume the player takes the simpler movement. // For sliders that are circular, the lazy end position may actually be farther away than the sliders true end. // This code is designed to prevent buffing situations where lazy end is actually a less efficient movement. - Vector2 lazyMovement = Vector2.Subtract((Vector2)slider.LazyEndPosition, currCursorPosition); + Vector2 lazyMovement = Vector2.Subtract((Vector2)LazyEndPosition, currCursorPosition); if (lazyMovement.Length < currMovement.Length) currMovement = lazyMovement; @@ -328,25 +351,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // this finds the positional delta from the required radius and the current position, and updates the currCursorPosition accordingly, as well as rewarding distance. currCursorPosition = Vector2.Add(currCursorPosition, Vector2.Multiply(currMovement, (float)((currMovementLength - requiredMovement) / currMovementLength))); currMovementLength *= (currMovementLength - requiredMovement) / currMovementLength; - slider.LazyTravelDistance += (float)currMovementLength; + LazyTravelDistance += currMovementLength; } if (i == nestedObjects.Count - 1) - slider.LazyEndPosition = currCursorPosition; + LazyEndPosition = currCursorPosition; } } - private Vector2 getEndCursorPosition(OsuHitObject hitObject) + private Vector2 getEndCursorPosition(OsuDifficultyHitObject difficultyHitObject) { - Vector2 pos = hitObject.StackedPosition; - - if (hitObject is Slider slider) - { - computeSliderCursorPosition(slider); - pos = slider.LazyEndPosition ?? pos; - } - - return pos; + return difficultyHitObject.LazyEndPosition ?? difficultyHitObject.BaseObject.StackedPosition; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 6f1b680211..5816d27a5e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; +using osu.Game.Rulesets.Osu.Difficulty.Utils; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty.Skills @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 25.6; + private double skillMultiplier => 26; private double strainDecayBase => 0.15; private readonly List sliderStrains = new List(); @@ -41,9 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier; if (current.BaseObject is Slider) - { sliderStrains.Add(currentStrain); - } return currentStrain; } @@ -54,10 +53,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return 0; double maxSliderStrain = sliderStrains.Max(); + if (maxSliderStrain == 0) return 0; return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0)))); } + + public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue()); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index bdeea0e918..8fe3df4347 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Objects; using System.Linq; +using osu.Game.Rulesets.Osu.Difficulty.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -15,12 +18,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1.46; + private double skillMultiplier => 1.47; private double strainDecayBase => 0.3; private double currentStrain; private double currentRhythm; + private readonly List sliderStrains = new List(); + protected override int ReducedSectionCount => 5; public Speed(Mod[] mods) @@ -34,13 +39,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { - currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); + currentStrain *= strainDecay(((OsuDifficultyHitObject)current).AdjustedDeltaTime); currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier; currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; + if (current.BaseObject is Slider) + sliderStrains.Add(totalStrain); + return totalStrain; } @@ -55,5 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); } + + public double CountTopWeightedSliders() => OsuStrainUtils.CountTopWeightedSliders(sliderStrains, DifficultyValue()); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs b/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs new file mode 100644 index 0000000000..df1683fb29 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/Utils/LegacyScoreUtils.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Difficulty.Utils +{ + public static class LegacyScoreUtils + { + /// + /// Calculates the average amount of score per object that is caused by nested judgements such as slider-ticks and spinners. + /// + public static double CalculateNestedScorePerObject(IBeatmap beatmap, int objectCount) + { + const double big_tick_score = 30; + const double small_tick_score = 10; + + var sliders = beatmap.HitObjects.OfType().ToArray(); + + // 1 for head, 1 for tail + int amountOfBigTicks = sliders.Length * 2; + + // Add slider repeats + amountOfBigTicks += sliders.Select(s => s.RepeatCount).Sum(); + + int amountOfSmallTicks = sliders.Select(s => s.NestedHitObjects.Count(nho => nho is SliderTick)).Sum(); + + double sliderScore = amountOfBigTicks * big_tick_score + amountOfSmallTicks * small_tick_score; + + double spinnerScore = 0; + + foreach (var spinner in beatmap.HitObjects.OfType()) + { + spinnerScore += calculateSpinnerScore(spinner); + } + + return (sliderScore + spinnerScore) / objectCount; + } + + /// + /// Logic borrowed from for basic score calculations. + /// + private static double calculateSpinnerScore(Spinner spinner) + { + const int spin_score = 100; + const int bonus_spin_score = 1000; + + // The spinner object applies a lenience because gameplay mechanics differ from osu-stable. + // We'll redo the calculations to match osu-stable here... + const double maximum_rotations_per_second = 477.0 / 60; + + // Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises bonus score. + // As we're primarily concerned with computing the maximum theoretical final score, + // this will have the final effect of slightly underestimating bonus score achieved on stable when converting from score V1. + const double minimum_rotations_per_second = 3; + + double secondsDuration = spinner.Duration / 1000; + + // The total amount of half spins possible for the entire spinner. + int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2); + // The amount of half spins that are required to successfully complete the spinner (i.e. get a 300). + int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimum_rotations_per_second); + // To be able to receive bonus points, the spinner must be rotated another 1.5 times. + int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3; + + long score = 0; + + int fullSpins = (totalHalfSpinsPossible / 2); + + // Normal spin score + score += spin_score * fullSpins; + + int bonusSpins = (totalHalfSpinsPossible - halfSpinsRequiredBeforeBonus) / 2; + + // Reduce amount of bonus spins because we want to represent the more average case, rather than the best one. + bonusSpins = Math.Max(0, bonusSpins - fullSpins / 2); + + score += bonus_spin_score * bonusSpins; + + return score; + } + + public static int CalculateDifficultyPeppyStars(IBeatmap beatmap) + { + int objectCount = beatmap.HitObjects.Count; + int drainLength = 0; + + if (objectCount > 0) + { + int breakLength = beatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum(); + drainLength = ((int)Math.Round(beatmap.HitObjects[^1].StartTime) - (int)Math.Round(beatmap.HitObjects[0].StartTime) - breakLength) / 1000; + } + + return LegacyRulesetExtensions.CalculateDifficultyPeppyStars(beatmap.Difficulty, objectCount, drainLength); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Difficulty/Utils/OsuStrainUtils.cs b/osu.Game.Rulesets.Osu/Difficulty/Utils/OsuStrainUtils.cs new file mode 100644 index 0000000000..8a78192ee4 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/Utils/OsuStrainUtils.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Difficulty.Utils; + +namespace osu.Game.Rulesets.Osu.Difficulty.Utils +{ + public static class OsuStrainUtils + { + public static double CountTopWeightedSliders(IReadOnlyCollection sliderStrains, double difficultyValue) + { + if (sliderStrains.Count == 0) + return 0; + + double consistentTopStrain = difficultyValue / 10; // What would the top strain be if all strain values were identical + + if (consistentTopStrain == 0) + return 0; + + // Use a weighted sum of all strains. Constants are arbitrary and give nice values + return sliderStrains.Sum(s => DifficultyCalculationUtils.Logistic(s / consistentTopStrain, 0.88, 10, 1.1)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 5ae9b194be..bff6701826 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -469,9 +469,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else { - SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition)); + Vector2 newControlPointPosition = Parent!.ToScreenSpace(e.MousePosition); - Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; + // Snapping inherited B-spline control points to nearby objects would be unintuitive, because snapping them does not equate to snapping the interpolated slider path. + bool shouldSnapToNearbyObjects = dragPathTypes[draggedControlPointIndex] is not null || + dragPathTypes[..draggedControlPointIndex].LastOrDefault(t => t is not null)?.Type != SplineType.BSpline; + + SnapResult result = null; + if (shouldSnapToNearbyObjects) + result = positionSnapProvider?.TrySnapToNearbyObjects(newControlPointPosition, oldStartTime); + if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newControlPointPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult) + result = gridSnapResult; + result ??= new SnapResult(newControlPointPosition, oldStartTime); + + Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; for (int i = 0; i < controlPoints.Count; ++i) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 363533ae76..b46dd44ce5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -626,10 +626,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); - protected override Vector2[] ScreenSpaceAdditionalNodes => new[] - { + protected override Vector2[] ScreenSpaceAdditionalNodes => getScreenSpaceControlPointNodes().Prepend( DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation) - }; + ).ToArray(); + + private IEnumerable getScreenSpaceControlPointNodes() + { + // Returns the positions of control points that produce visible kinks on the slider's path + // This excludes inherited control points from Bezier, B-Spline, Perfect, and Catmull curves + if (DrawableObject.SliderBody == null) + yield break; + + PathType? currentPathType = null; + + // Skip the last control point because its always either not on the slider path or exactly on the slider end + for (int i = 0; i < DrawableObject.HitObject.Path.ControlPoints.Count - 1; i++) + { + var controlPoint = DrawableObject.HitObject.Path.ControlPoints[i]; + + if (controlPoint.Type is not null) + currentPathType = controlPoint.Type; + + // Skip the first control point because it is already covered by the slider head + if (i == 0) + continue; + + if (controlPoint.Type is null && currentPathType != PathType.LINEAR) + continue; + + var screenSpacePosition = DrawableObject.SliderBody.ToScreenSpace(DrawableObject.SliderBody.PathOffset + controlPoint.Position); + yield return screenSpacePosition; + } + } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 3a1ff34fb9..c591b79b29 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); + Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects, true); Vector2 delta = Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 9a5d3c3bc1..d5f3137769 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -81,12 +81,8 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); - originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 - ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) - : GeometryUtils.GetConvexHull(objectsInScale.Keys); + OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); + originalConvexHull = GeometryUtils.GetConvexHull(objectsInScale.Keys); defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1; } @@ -312,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void moveSelectionInBounds() { - Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys); + Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys, true); Vector2 delta = Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e484efb408..94e98fbef7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -68,24 +68,6 @@ namespace osu.Game.Rulesets.Osu.Objects } } - /// - /// The position of the cursor at the point of completion of this if it was hit - /// with as few movements as possible. This is set and used by difficulty calculation. - /// - internal Vector2? LazyEndPosition; - - /// - /// The distance travelled by the cursor upon completion of this if it was hit - /// with as few movements as possible. This is set and used by difficulty calculation. - /// - internal float LazyTravelDistance; - - /// - /// The time taken by the cursor upon completion of this if it was hit - /// with as few movements as possible. This is set and used by difficulty calculation. - /// - internal double LazyTravelTime; - public IList> NodeSamples { get; set; } = new List>(); [JsonIgnore] diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5a95eac0f1..569e01ae56 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -62,24 +62,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - spin = new Sprite - { - Alpha = 0, - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-spin"), - Scale = new Vector2(SPRITE_SCALE), - Y = SPINNER_TOP_OFFSET + 335, - }, - clear = new Sprite - { - Alpha = 0, - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-clear"), - Scale = new Vector2(SPRITE_SCALE), - Y = SPINNER_TOP_OFFSET + 115, - }, bonusCounter = new LegacySpriteText(LegacyFont.Score) { Alpha = 0, @@ -103,6 +85,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Scale = new Vector2(SPRITE_SCALE * 0.9f), Position = new Vector2(80, 448 + spm_hide_offset), }, + spin = new Sprite + { + Alpha = 0, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-spin"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 335, + }, + clear = new Sprite + { + Alpha = 0, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-clear"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 115, + }, } }); } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index dc4730d76a..f05f3aa03a 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; +using osu.Game.Localisation; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Screens.Play.PlayerSettings; @@ -13,19 +14,19 @@ namespace osu.Game.Rulesets.Osu.UI { private readonly OsuRulesetConfigManager config; - [SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))] + [SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowClickMarkers), SettingControlType = typeof(PlayerCheckbox))] public BindableBool ShowClickMarkers { get; } = new BindableBool(); - [SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))] + [SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowFrameMarkers), SettingControlType = typeof(PlayerCheckbox))] public BindableBool ShowAimMarkers { get; } = new BindableBool(); - [SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))] + [SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowCursorPath), SettingControlType = typeof(PlayerCheckbox))] public BindableBool ShowCursorPath { get; } = new BindableBool(); - [SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))] + [SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.HideGameplayCursor), SettingControlType = typeof(PlayerCheckbox))] public BindableBool HideSkinCursor { get; } = new BindableBool(); - [SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar))] + [SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.DisplayLength), SettingControlType = typeof(PlayerSliderBar))] public BindableInt DisplayLength { get; } = new BindableInt { MinValue = 200, @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI }; public ReplayAnalysisSettings(OsuRulesetConfigManager config) - : base("Analysis Settings") + : base(PlayerSettingsOverlayStrings.AnalysisSettingsTitle) { this.config = config; } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.Reposition.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs rename to osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.Reposition.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 76b86eb4d6..a4b33b7c15 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(3.305554470092722d, 200, "diffcalc-test")] - [TestCase(3.305554470092722d, 200, "diffcalc-test-strong")] + [TestCase(3.3190848563395079d, 200, "diffcalc-test")] + [TestCase(3.3190848563395079d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(4.4472572672057815d, 200, "diffcalc-test")] - [TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")] + [TestCase(4.4551414906554987d, 200, "diffcalc-test")] + [TestCase(4.4551414906554987d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index b715dfc37a..d8d30e3fef 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -24,7 +26,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators int consistentRatioCount = 0; double totalRatioCount = 0.0; + List recentRatios = new List(); TaikoDifficultyHitObject current = hitObject; + var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); for (int i = 0; i < maxObjectsToCheck; i++) { @@ -32,11 +36,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators if (current.Index <= 1) break; - var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1); - double currentRatio = current.RhythmData.Ratio; double previousRatio = previousHitObject.RhythmData.Ratio; + recentRatios.Add(currentRatio); + // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error. if (Math.Abs(1 - currentRatio / previousRatio) <= threshold) { @@ -45,14 +49,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators break; } - // Move to the previous object current = previousHitObject; } // Ensure no division by zero - double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80; + if (consistentRatioCount > 0) + return 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80; - return ratioPenalty; + if (recentRatios.Count <= 1) return 1.0; + + // As a fallback, calculate the maximum deviation from the average of the recent ratios to ensure slightly off-snapped objects don't bypass the penalty. + double maxRatioDeviation = recentRatios.Max(r => Math.Abs(r - recentRatios.Average())); + + double consistentRatioPenalty = 0.7 + 0.3 * DifficultyCalculationUtils.Smootherstep(maxRatioDeviation, 0.0, 1.0); + + return consistentRatioPenalty; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs index 9caa9b9958..59215c043b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Utils; @@ -18,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public readonly SameRhythmHitObjectGrouping? Previous; + private const double snap_tolerance = IntervalGroupingUtils.MARGIN_OF_ERROR; + /// /// of the first hit object. /// @@ -29,35 +33,60 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime; /// - /// The interval in ms of each hit object in this . This is only defined if there is + /// The normalised interval in ms of each hit object in this . This is only defined if there is /// more than two hit objects in this . /// public readonly double? HitObjectInterval; /// - /// The ratio of between this and the previous . In the + /// The normalised ratio of between this and the previous . In the /// case where one or both of the is undefined, this will have a value of 1. /// public readonly double HitObjectIntervalRatio; /// - public double Interval { get; } + public double Interval { get; } = double.PositiveInfinity; public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List hitObjects) { Previous = previous; HitObjects = hitObjects; - // Calculate the average interval between hitobjects, or null if there are fewer than two - HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1); + // Cluster and normalise each hitobjects delta-time. + var normaliseHitObjects = DeltaTimeNormaliser.Normalise(hitObjects, snap_tolerance); + + var normalisedHitObjectDeltaTime = hitObjects + .Skip(1) + .Select(hitObject => normaliseHitObjects[hitObject]) + .ToList(); + + // Secondary check to ensure there isn't any 'noise' or outliers by taking the modal delta time. + double modalDelta = normalisedHitObjectDeltaTime.Count > 0 + ? Math.Round(normalisedHitObjectDeltaTime[0]) + : 0; + + // Calculate the average interval between hitobjects. + if (normalisedHitObjectDeltaTime.Count > 0) + { + if (previous?.HitObjectInterval is double previousDelta && Math.Abs(modalDelta - previousDelta) <= snap_tolerance) + HitObjectInterval = previousDelta; + else + HitObjectInterval = modalDelta; + } // Calculate the ratio between this group's interval and the previous group's interval - HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null - ? HitObjectInterval.Value / Previous.HitObjectInterval.Value - : 1; + HitObjectIntervalRatio = previous?.HitObjectInterval is double previousInterval && HitObjectInterval is double currentInterval + ? currentInterval / previousInterval + : 1.0; // Calculate the interval from the previous group's start time - Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity; + if (previous != null) + { + if (Math.Abs(StartTime - previous.StartTime) <= snap_tolerance) + Interval = 0; + else + Interval = StartTime - previous.StartTime; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 0e1f3d41cf..5e18163fe0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -42,20 +42,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { currentStrain *= strainDecay(current.DeltaTime); - currentStrain += StaminaEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; + double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; // Safely prevents previous strains from shifting as new notes are added. var currentObject = current as TaikoDifficultyHitObject; int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0; - double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30); + double monoLengthBonus = isConvert ? 1.0 : 1.0 + 0.5 * DifficultyCalculationUtils.ReverseLerp(index, 5, 20); - if (SingleColourStamina) - return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain); + // Mono-streak bonus is only applied to colour-based stamina to reward longer sequences of same-colour hits within patterns. + if (!SingleColourStamina) + staminaDifficulty *= monoLengthBonus; - return currentStrain * monolengthBonus; + currentStrain += staminaDifficulty; + + // For converted maps, difficulty often comes entirely from long mono streams with no colour variation. + // To avoid over-rewarding these maps based purely on stamina strain, we dampen the strain value once the index exceeds 10. + return SingleColourStamina ? DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain) : currentStrain; } - protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime); + protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => + SingleColourStamina + ? 0 + : currentStrain * strainDecay(time - current.Previous(0).StartTime); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index b8051054e7..c5cc04449c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -10,9 +10,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { + /// + /// The difficulty corresponding to the mechanical skills in osu!taiko. + /// This includes colour and stamina combined. + /// + public double MechanicalDifficulty { get; set; } + /// /// The difficulty corresponding to the rhythm skill. /// + [JsonProperty("rhythm_difficulty")] public double RhythmDifficulty { get; set; } /// @@ -36,9 +43,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("mono_stamina_factor")] public double MonoStaminaFactor { get; set; } - public double RhythmTopStrains { get; set; } - - public double ColourTopStrains { get; set; } + /// + /// The factor corresponding to the consistency of a map. + /// + [JsonProperty("consistency_factor")] + public double ConsistencyFactor { get; set; } public double StaminaTopStrains { get; set; } @@ -48,7 +57,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty yield return v; yield return (ATTRIB_ID_DIFFICULTY, StarRating); + yield return (ATTRIB_ID_RHYTHM_DIFFICULTY, RhythmDifficulty); yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor); + yield return (ATTRIB_ID_CONSISTENCY_FACTOR, ConsistencyFactor); } public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) @@ -56,7 +67,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_DIFFICULTY]; + RhythmDifficulty = values[ATTRIB_ID_RHYTHM_DIFFICULTY]; MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR]; + ConsistencyFactor = values[ATTRIB_ID_CONSISTENCY_FACTOR]; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e0bc0e177c..cdb5a36f65 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.084375; - private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier; + private const double rhythm_skill_multiplier = 0.750 * difficulty_multiplier; private const double reading_skill_multiplier = 0.100 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier; @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double strainLengthBonus; private double patternMultiplier; + private bool isRelax; private bool isConvert; public override int Version => 20250306; @@ -46,6 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; + isRelax = mods.Any(h => h is TaikoModRelax); return new Skill[] { @@ -100,47 +102,50 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; - bool isRelax = mods.Any(h => h is TaikoModRelax); - var rhythm = skills.OfType().Single(); var reading = skills.OfType().Single(); var colour = skills.OfType().Single(); var stamina = skills.OfType().Single(s => !s.SingleColourStamina); var singleColourStamina = skills.OfType().Single(s => s.SingleColourStamina); - double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; - double readingRating = reading.DifficultyValue() * reading_skill_multiplier; - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; - double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; - double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; - double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); + double rhythmSkill = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double readingSkill = reading.DifficultyValue() * reading_skill_multiplier; + double colourSkill = colour.DifficultyValue() * colour_skill_multiplier; + double staminaSkill = stamina.DifficultyValue() * stamina_skill_multiplier; + double monoStaminaSkill = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; + double monoStaminaFactor = staminaSkill == 0 ? 1 : Math.Pow(monoStaminaSkill / staminaSkill, 5); - double colourDifficultStrains = colour.CountTopWeightedStrains(); - double rhythmDifficultStrains = rhythm.CountTopWeightedStrains(); double staminaDifficultStrains = stamina.CountTopWeightedStrains(); // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm. - patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10); + patternMultiplier = Math.Pow(staminaSkill * colourSkill, 0.10); - strainLengthBonus = 1 - + Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15) - + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05); + strainLengthBonus = 1 + 0.15 * DifficultyCalculationUtils.ReverseLerp(staminaDifficultStrains, 1000, 1555); - double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert); + double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, out double consistencyFactor); double starRating = rescale(combinedRating * 1.4); + // Calculate proportional contribution of each skill to the combinedRating. + double skillRating = starRating / (rhythmSkill + readingSkill + colourSkill + staminaSkill); + + double rhythmDifficulty = rhythmSkill * skillRating; + double readingDifficulty = readingSkill * skillRating; + double colourDifficulty = colourSkill * skillRating; + double staminaDifficulty = staminaSkill * skillRating; + double mechanicalDifficulty = colourDifficulty + staminaDifficulty; // Mechanical difficulty is the sum of colour and stamina difficulties. + TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes { StarRating = starRating, Mods = mods, - RhythmDifficulty = rhythmRating, - ReadingDifficulty = readingRating, - ColourDifficulty = colourRating, - StaminaDifficulty = staminaRating, + MechanicalDifficulty = mechanicalDifficulty, + RhythmDifficulty = rhythmDifficulty, + ReadingDifficulty = readingDifficulty, + ColourDifficulty = colourDifficulty, + StaminaDifficulty = staminaDifficulty, MonoStaminaFactor = monoStaminaFactor, - RhythmTopStrains = rhythmDifficultStrains, - ColourTopStrains = colourDifficultStrains, StaminaTopStrains = staminaDifficultStrains, + ConsistencyFactor = consistencyFactor, MaxCombo = beatmap.GetMaxCombo(), }; @@ -154,14 +159,59 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert) + private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, out double consistencyFactor) { - List peaks = new List(); + List peaks = combinePeaks( + rhythm.GetCurrentStrainPeaks().ToList(), + reading.GetCurrentStrainPeaks().ToList(), + colour.GetCurrentStrainPeaks().ToList(), + stamina.GetCurrentStrainPeaks().ToList() + ); - var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); - var readingPeaks = reading.GetCurrentStrainPeaks().ToList(); - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); - var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); + if (peaks.Count == 0) + { + consistencyFactor = 0; + return 0; + } + + double difficulty = 0; + double weight = 1; + + foreach (double strain in peaks.OrderDescending()) + { + difficulty += strain * weight; + weight *= 0.9; + } + + List hitObjectStrainPeaks = combinePeaks( + rhythm.GetObjectStrains().ToList(), + reading.GetObjectStrains().ToList(), + colour.GetObjectStrains().ToList(), + stamina.GetObjectStrains().ToList() + ); + + if (hitObjectStrainPeaks.Count == 0) + { + consistencyFactor = 0; + return 0; + } + + // The average of the top 5% of strain peaks from hit objects. + double topAverageHitObjectStrain = hitObjectStrainPeaks.OrderDescending().Take(1 + hitObjectStrainPeaks.Count / 20).Average(); + + // Calculates a consistency factor as the sum of difficulty from hit objects compared to if every object were as hard as the hardest. + // The top average strain is used instead of the very hardest to prevent exceptionally hard objects lowering the factor. + consistencyFactor = hitObjectStrainPeaks.Sum() / (topAverageHitObjectStrain * hitObjectStrainPeaks.Count); + + return difficulty; + } + + /// + /// Combines lists of peak strains from multiple skills into a list of single peak strains for each section. + /// + private List combinePeaks(List rhythmPeaks, List readingPeaks, List colourPeaks, List staminaPeaks) + { + var combinedPeaks = new List(); for (int i = 0; i < colourPeaks.Count; i++) { @@ -176,19 +226,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. if (peak > 0) - peaks.Add(peak); + combinedPeaks.Add(peak); } - double difficulty = 0; - double weight = 1; - - foreach (double strain in peaks.OrderDescending()) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; + return combinedPeaks; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 7c74e43db1..ef40c2e58b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("accuracy")] public double Accuracy { get; set; } - [JsonProperty("effective_miss_count")] - public double EffectiveMissCount { get; set; } - [JsonProperty("estimated_unstable_rate")] public double? EstimatedUnstableRate { get; set; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9e049df87c..df9da49c4b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Utils; @@ -13,6 +12,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; +using osu.Game.Utils; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double clockRate; private double greatHitWindow; - private double effectiveMissCount; + private double totalDifficultHits; public TaikoPerformanceCalculator() : base(new TaikoRuleset()) @@ -43,9 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - clockRate = track.Rate; + clockRate = ModUtils.CalculateRateWithMods(score.Mods); var difficulty = score.BeatmapInfo!.Difficulty.Clone(); @@ -56,70 +54,92 @@ namespace osu.Game.Rulesets.Taiko.Difficulty greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate; - estimatedUnstableRate = computeDeviationUpperBound() * 10; + estimatedUnstableRate = (countGreat == 0 || greatHitWindow <= 0) + ? null + : computeDeviationUpperBound(countGreat / (double)totalHits) * 10; - // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. - if (totalSuccessfulHits > 0) - effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; + // Total difficult hits measures the total difficulty of a map based on its consistency factor. + totalDifficultHits = totalHits * taikoAttributes.ConsistencyFactor; - // Converts are detected and omitted from mod-specific bonuses due to the scope of current difficulty calculation. + // Converts and the classic mod are detected and omitted from mod-specific bonuses due to the scope of current difficulty calculation. bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1; + bool isClassic = score.Mods.Any(m => m is ModClassic); - double multiplier = 1.13; - - if (score.Mods.Any(m => m is ModHidden) && !isConvert) - multiplier *= 1.075; - - if (score.Mods.Any(m => m is ModEasy)) - multiplier *= 0.950; - - double difficultyValue = computeDifficultyValue(score, taikoAttributes); - double accuracyValue = computeAccuracyValue(score, taikoAttributes, isConvert); - double totalValue = - Math.Pow( - Math.Pow(difficultyValue, 1.1) + - Math.Pow(accuracyValue, 1.1), 1.0 / 1.1 - ) * multiplier; + double difficultyValue = computeDifficultyValue(score, taikoAttributes, isConvert, isClassic) * 1.08; + double accuracyValue = computeAccuracyValue(score, taikoAttributes, isConvert) * 1.1; return new TaikoPerformanceAttributes { Difficulty = difficultyValue, Accuracy = accuracyValue, - EffectiveMissCount = effectiveMissCount, EstimatedUnstableRate = estimatedUnstableRate, - Total = totalValue + Total = difficultyValue + accuracyValue }; } - private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) + private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert, bool isClassic) { - double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0; + if (estimatedUnstableRate == null || totalDifficultHits == 0) + return 0; + + // The estimated unstable rate for 100% accuracy, at which all rhythm difficulty has been played successfully. + double rhythmExpectedUnstableRate = computeDeviationUpperBound(1.0) * 10; + + // The unstable rate at which it can be assumed all rhythm difficulty has been ignored. + // 0.8 represents 80% of total hits being greats, or 90% accuracy in-game + double rhythmMaximumUnstableRate = computeDeviationUpperBound(0.8) * 10; + + // The fraction of star rating made up by rhythm difficulty, normalised to represent rhythm's perceived contribution to star rating. + double rhythmFactor = DifficultyCalculationUtils.ReverseLerp(attributes.RhythmDifficulty / attributes.StarRating, 0.15, 0.4); + + // A penalty removing improperly played rhythm difficulty from star rating based on estimated unstable rate. + double rhythmPenalty = 1 - DifficultyCalculationUtils.Logistic( + estimatedUnstableRate.Value, + midpointOffset: (rhythmExpectedUnstableRate + rhythmMaximumUnstableRate) / 2, + multiplier: 10 / (rhythmMaximumUnstableRate - rhythmExpectedUnstableRate), + maxValue: 0.25 * Math.Pow(rhythmFactor, 3) + ); + + double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating * rhythmPenalty / 0.110) - 4.0; double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0); difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10); - double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + // Applies a bonus to maps with more total difficulty. + double lengthBonus = 1 + 0.25 * totalDifficultHits / (totalDifficultHits + 4000); difficultyValue *= lengthBonus; - difficultyValue *= Math.Pow(0.986, effectiveMissCount); - - if (score.Mods.Any(m => m is ModEasy)) - difficultyValue *= 0.90; + // Scales miss penalty by the total difficult hits of a map, making misses more punishing on maps with less total difficulty. + double missPenalty = 0.97 + 0.03 * totalDifficultHits / (totalDifficultHits + 1500); + difficultyValue *= Math.Pow(missPenalty, countMiss); if (score.Mods.Any(m => m is ModHidden)) - difficultyValue *= 1.025; + { + double hiddenBonus = isConvert ? 0.025 : 0.1; + + // Hidden+flashlight plays are excluded from reading-based penalties to hidden. + if (!score.Mods.Any(m => m is ModFlashlight)) + { + // A penalty is applied to the bonus for hidden on non-classic scores, as the playfield can be made wider to make fast reading easier. + if (!isClassic) + hiddenBonus *= 0.2; + + // A penalty is applied to classic easy+hidden scores, as notes disappear later making fast reading easier. + if (score.Mods.Any(m => m is ModEasy) && isClassic) + hiddenBonus *= 0.5; + } + + difficultyValue *= 1 + hiddenBonus; + } if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus); - if (estimatedUnstableRate == null) - return 0; - // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. - double accScalingExponent = 2 + attributes.MonoStaminaFactor; - double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3); + double monoAccScalingExponent = 2 + attributes.MonoStaminaFactor; + double monoAccScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3); - return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); + return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(monoAccScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), monoAccScalingExponent); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) @@ -127,13 +147,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (greatHitWindow <= 0 || estimatedUnstableRate == null) return 0; - double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0; + double accuracyValue = 470 * Math.Pow(0.9885, estimatedUnstableRate.Value); - double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + // Scales up the bonus for lower unstable rate as star rating increases. + accuracyValue *= 1 + Math.Pow(50 / estimatedUnstableRate.Value, 2) * Math.Pow(attributes.StarRating, 2.8) / 600; + + if (score.Mods.Any(m => m is ModHidden) && !isConvert) + accuracyValue *= 1.075; + + // Applies a bonus to maps with more total difficulty, calculating this with a map's total hits and consistency factor. + accuracyValue *= 1 + 0.3 * totalDifficultHits / (totalDifficultHits + 4000); + + // Applies a bonus to maps with more total memory required with HDFL. + double memoryLengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); - // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values. if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden) && !isConvert) - accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus); + accuracyValue *= Math.Max(1.0, 1.05 * memoryLengthBonus); return accuracyValue; } @@ -143,17 +172,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that /// two SS scores on the same map with the same settings will always return the same deviation. /// - private double? computeDeviationUpperBound() + private double computeDeviationUpperBound(double accuracy) { - if (countGreat == 0 || greatHitWindow <= 0) - return null; - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). double n = totalHits; // Proportion of greats hit. - double p = countGreat / n; + double p = accuracy; // We can be 99% confident that p is at least this value. double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/DeltaTimeNormaliser.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/DeltaTimeNormaliser.cs new file mode 100644 index 0000000000..5e959f3f25 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/DeltaTimeNormaliser.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Utils +{ + /// + /// Normalises deltaTime values for TaikoDifficultyHitObjects. + /// + public static class DeltaTimeNormaliser + { + /// + /// Combines deltaTime values that differ by at most + /// and replaces each value with the median of its range. This is used to reduce timing noise + /// and improve rhythm grouping consistency, especially for maps with inconsistent or 'off-snapped' timing. + /// + public static Dictionary Normalise( + IReadOnlyList hitObjects, + double marginOfError) + { + var deltaTimes = hitObjects.Select(h => h.DeltaTime).Distinct().OrderBy(d => d).ToList(); + + var sets = new List>(); + List? current = null; + + foreach (double value in deltaTimes) + { + // Add to the current group if within margin of error + if (current != null && Math.Abs(value - current[0]) <= marginOfError) + { + current.Add(value); + continue; + } + + // Otherwise begin a new group + current = new List { value }; + sets.Add(current); + } + + // Compute median for each group + var medianLookup = new Dictionary(); + + foreach (var set in sets) + { + set.Sort(); + int mid = set.Count / 2; + double median = set.Count % 2 == 1 + ? set[mid] + : (set[mid - 1] + set[mid]) / 2; + + foreach (double v in set) + medianLookup[v] = median; + } + + // Assign each hitobjects deltaTime the corresponding median value + return hitObjects.ToDictionary( + h => h, + h => medianLookup.TryGetValue(h.DeltaTime, out double median) ? median : h.DeltaTime + ); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs index 5ab58ad4f3..38129b24e6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs @@ -8,6 +8,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils { public static class IntervalGroupingUtils { + // The margin of error when comparing intervals for grouping, or snapping intervals to a common value. + public const double MARGIN_OF_ERROR = 5.0; + public static List> GroupByInterval(IReadOnlyList objects) where T : IHasInterval { var groups = new List>(); @@ -21,8 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils private static List createNextGroup(IReadOnlyList objects, ref int i) where T : IHasInterval { - const double margin_of_error = 5; - // This never compares the first two elements in the group. // This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329) var groupedObjects = new List { objects[i] }; @@ -30,11 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils for (; i < objects.Count - 1; i++) { - if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error)) + if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, MARGIN_OF_ERROR)) { // When an interval change occurs, include the object with the differing interval in the case it increased // See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale. - if (objects[i + 1].Interval > objects[i].Interval + margin_of_error) + if (objects[i + 1].Interval > objects[i].Interval + MARGIN_OF_ERROR) { groupedObjects.Add(objects[i]); i++; @@ -49,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils // Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error. // If true, add the current object to the group and increment the index to process the next object. - if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error)) + if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, MARGIN_OF_ERROR)) { groupedObjects.Add(objects[i]); i++; diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index ddf207342a..29aec73770 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, storage) => { - var rulesets = new RealmRulesetStore(realm, storage); + using var rulesets = new RealmRulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, realm.Realm.All().Count()); @@ -36,8 +36,8 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, storage) => { - var rulesets = new RealmRulesetStore(realm, storage); - var rulesets2 = new RealmRulesetStore(realm, storage); + using var rulesets = new RealmRulesetStore(realm, storage); + using var rulesets2 = new RealmRulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realm, storage) => { - var rulesets = new RealmRulesetStore(realm, storage); + using var rulesets = new RealmRulesetStore(realm, storage); Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore - _ = new RealmRulesetStore(realm, storage); + using var _ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); }); @@ -104,13 +104,13 @@ namespace osu.Game.Tests.Database Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); // Availability is updated on construction of a RealmRulesetStore - _ = new RealmRulesetStore(realm, storage); + using var _ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.False); // Simulate the ruleset getting updated LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; - _ = new RealmRulesetStore(realm, storage); + using var __ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName)!.Available), Is.True); }); diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs index b02bf01019..3a96459b73 100644 --- a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -46,9 +46,12 @@ namespace osu.Game.Tests.Extensions [Test] [SetCulture("fr-FR")] - public void TestCultureInsensitivity() + [TestCase(0.4, true, 2, ExpectedResult = "40%")] + [TestCase(1e-6, false, 6, ExpectedResult = "0.000001")] + [TestCase(0.48333, true, 4, ExpectedResult = "48.33%")] + public string TestCultureInsensitivity(double input, bool percent, int decimalDigits) { - Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%")); + return input.ToStandardFormattedString(decimalDigits, percent); } } } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index db76782350..12aab055ad 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -299,6 +299,23 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + [Test] + [TestCase("artist")] + [TestCase("unicode")] + public void TestCriteriaNotMatchingArtist(string excludedTerm) + { + var beatmap = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = excludedTerm, ExcludeTerm = true } + }; + + var carouselItem = new CarouselBeatmap(beatmap); + carouselItem.Filter(criteria); + + Assert.True(carouselItem.Filtered.Value); + } + [TestCase("simple", false)] [TestCase("\"style/clean\"", false)] [TestCase("\"style/clean\"!", false)] @@ -350,6 +367,41 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(true, carouselItem.Filtered.Value); } + [Test] + public void TestCriteriaMatchingTagExcluded() + { + var beatmap = getExampleBeatmap(); + var criteria = new FilterCriteria + { + UserTags = + [ + new FilterCriteria.OptionalTextFilter { SearchTerm = "\"song representation/simple\"!", ExcludeTerm = true }, + ] + }; + var carouselItem = new CarouselBeatmap(beatmap); + carouselItem.Filter(criteria); + + Assert.AreEqual(true, carouselItem.Filtered.Value); + } + + [Test] + public void TestCriteriaOneTagIncludedAndOneTagExcluded() + { + var beatmap = getExampleBeatmap(); + var criteria = new FilterCriteria + { + UserTags = + [ + new FilterCriteria.OptionalTextFilter { SearchTerm = "\"song representation/simple\"!" }, + new FilterCriteria.OptionalTextFilter { SearchTerm = "\"style/clean\"!", ExcludeTerm = true } + ] + }; + var carouselItem = new CarouselBeatmap(beatmap); + carouselItem.Filter(criteria); + + Assert.AreEqual(true, carouselItem.Filtered.Value); + } + [Test] public void TestBeatmapMustHaveAtLeastOneTagIfUserTagFilterActive() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs index ee22cbda71..2dc9077a14 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneSubmissionStageProgress.cs @@ -68,6 +68,25 @@ namespace osu.Game.Tests.Visual.Editing progress.SetInProgress(incrementingProgress += RNG.NextSingle(0.08f)); }, 0, true); }); + AddStep("increase progress slowly then fail", () => + { + incrementingProgress = 0; + + ScheduledDelegate? task = null; + + task = Scheduler.AddDelayed(() => + { + if (incrementingProgress >= 1) + { + progress.SetFailed("nope"); + // ReSharper disable once AccessToModifiedClosure + task?.Cancel(); + return; + } + + progress.SetInProgress(incrementingProgress += RNG.NextSingle(0.001f)); + }, 0, true); + }); AddUntilStep("wait for completed", () => incrementingProgress >= 1); AddStep("completed", () => progress.SetCompleted()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 046ae6d953..0e6fd8f519 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -257,6 +257,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private class CustomRuleset : OsuRuleset, ILegacyRuleset { public override string Description => "custom"; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 381f49d9eb..c0cddf0f6a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -316,6 +316,26 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } + [Test] + public void TestNoSubmissionOnLocallyModifiedBeatmapWithOnlineId() + { + prepareTestAPI(true); + + createPlayerTest(false, r => + { + var beatmap = createTestBeatmap(r); + beatmap.BeatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + return beatmap; + }); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + addFakeHit(); + + AddStep("exit", () => Player.Exit()); + AddAssert("ensure no submission", () => Player.SubmittedScore == null); + } + [TestCase(null)] [TestCase(10)] public void TestNoSubmissionOnCustomRuleset(int? rulesetId) diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs deleted file mode 100644 index c46beba037..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapPanel.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; -using osu.Game.Tests.Visual.Multiplayer; -using osuTK; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneBeatmapPanel : MultiplayerTestScene - { - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("add beatmap panel", () => - { - Child = new BeatmapPanel(CreateAPIBeatmap()) - { - Size = new Vector2(300, 70), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs similarity index 81% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 79ed79e388..4271742b1b 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -10,18 +10,20 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.OnlinePlay; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneBeatmapSelectionGrid : OnlinePlayTestScene + public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene { private MultiplayerPlaylistItem[] items = null!; - private BeatmapSelectionGrid grid = null!; + private BeatmapSelectGrid grid = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; @@ -58,7 +60,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add grid", () => Child = grid = new BeatmapSelectionGrid + AddStep("add grid", () => Child = grid = new BeatmapSelectGrid { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -75,6 +77,31 @@ namespace osu.Game.Tests.Visual.Matchmaking AddWaitStep("wait for panels", 3); } + [Test] + public void TestBasic() + { + AddStep("do nothing", () => + { + // test scene is weird. + }); + + AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser + { + Id = DummyAPIAccess.DUMMY_USER_ID, + Username = "Maarvin", + })); + AddStep("add selection 2", () => grid.ChildrenOfType().Skip(5).First().AddUser(new APIUser + { + Id = 2, + Username = "peppy", + })); + AddStep("add selection 3", () => grid.ChildrenOfType().Skip(10).First().AddUser(new APIUser + { + Id = 1040328, + Username = "smoogipoo", + })); + } + [Test] public void TestCompleteRollAnimation() { @@ -145,7 +172,7 @@ namespace osu.Game.Tests.Visual.Matchmaking var (candidateItems, _) = pickRandomItems(count); grid.TransferCandidatePanelsToRollContainer(candidateItems); - grid.Delay(BeatmapSelectionGrid.ARRANGE_DELAY) + grid.Delay(BeatmapSelectGrid.ARRANGE_DELAY) .Schedule(() => grid.ArrangeItemsForRollAnimation()); }); @@ -153,7 +180,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("display roll order", () => { - var panels = grid.ChildrenOfType().ToArray(); + var panels = grid.ChildrenOfType().ToArray(); for (int i = 0; i < panels.Length; i++) { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs similarity index 83% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index addb0ed3a0..fee03cd737 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -4,15 +4,16 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneBeatmapSelectionPanel : MultiplayerTestScene + public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -20,9 +21,9 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBeatmapPanel() { - BeatmapSelectionPanel? panel = null; + BeatmapSelectPanel? panel = null; - AddStep("add panel", () => Child = panel = new BeatmapSelectionPanel(new MultiplayerPlaylistItem()) + AddStep("add panel", () => Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("add maarvin", () => panel!.AddUser(new APIUser { - Id = 6411631, + Id = DummyAPIAccess.DUMMY_USER_ID, Username = "Maarvin", - }, isOwnUser: true)); + })); AddStep("add peppy", () => panel!.AddUser(new APIUser { Id = 2, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs deleted file mode 100644 index 4e596d65cc..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectionOverlay.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; -using osuTK; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneBeatmapSelectionOverlay : OsuTestScene - { - private BeatmapSelectionOverlay selectionOverlay = null!; - - [SetUpSteps] - public void SetupSteps() - { - AddStep("add drawable", () => Child = new Container - { - Width = 100, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(2), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f, - }, - selectionOverlay = new BeatmapSelectionOverlay - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - } - }); - } - - [Test] - public void TestSelectionOverlay() - { - AddStep("add maarvin", () => selectionOverlay.AddUser(new APIUser - { - Id = 6411631, - Username = "Maarvin", - }, isOwnUser: true)); - AddStep("add peppy", () => selectionOverlay.AddUser(new APIUser - { - Id = 2, - Username = "peppy", - }, false)); - AddStep("add smogipoo", () => selectionOverlay.AddUser(new APIUser - { - Id = 1040328, - Username = "smoogipoo", - }, false)); - AddStep("remove smogipoo", () => selectionOverlay.RemoveUser(1040328)); - AddStep("remove peppy", () => selectionOverlay.RemoveUser(2)); - AddStep("remove maarvin", () => selectionOverlay.RemoveUser(6411631)); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs deleted file mode 100644 index 49daedb6a3..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneIdleScreen.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Screens; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; -using osu.Game.Tests.Visual.Multiplayer; -using osuTK; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneIdleScreen : MultiplayerTestScene - { - private const int user_count = 8; - - private (MultiplayerRoomUser user, int score)[] userScores = null!; - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); - WaitForJoined(); - - AddStep("add list", () => - { - userScores = Enumerable.Range(1, user_count).Select(i => - { - var user = new MultiplayerRoomUser(i) - { - User = new APIUser - { - Username = $"Player {i}" - } - }; - - return (user, 0); - }).ToArray(); - - Child = new ScreenStack(new IdleScreen()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.8f) - }; - }); - - AddStep("join users", () => - { - foreach (var (user, _) in userScores) - MultiplayerClient.AddUser(user); - }); - } - - [Test] - public void TestRandomChanges() - { - AddStep("apply random changes", () => - { - int[] deltas = Enumerable.Range(1, userScores.Length).ToArray(); - new Random().Shuffle(deltas); - - for (int i = 0; i < userScores.Length; i++) - userScores[i] = (userScores[i].user, userScores[i].score + deltas[i]); - userScores = userScores.OrderByDescending(u => u.score).ToArray(); - - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Users = - { - UserDictionary = userScores.Select((tuple, i) => new MatchmakingUser - { - UserId = tuple.user.UserID, - Points = tuple.score, - Placement = i + 1 - }).ToDictionary(s => s.UserId) - } - }).WaitSafely(); - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs index c25057c84b..d656971b5a 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingCloud.cs @@ -6,20 +6,20 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Users; namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneMatchmakingCloud : OsuTestScene { - private MatchmakingCloud cloud = null!; + private CloudVisualisation cloud = null!; protected override void LoadComplete() { base.LoadComplete(); - Child = cloud = new MatchmakingCloud + Child = cloud = new CloudVisualisation { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs index 442a06606b..5971cd9091 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingPoolSelector.cs @@ -3,7 +3,7 @@ using osu.Framework.Graphics; using osu.Game.Online.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add selector", () => Child = new MatchmakingPoolSelector + AddStep("add selector", () => Child = new PoolSelector { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs index 72eba6e1c8..5193d58ee6 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingQueueScreen.cs @@ -7,8 +7,8 @@ using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Screens.OnlinePlay.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Intro; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -17,16 +17,16 @@ namespace osu.Game.Tests.Visual.Matchmaking public partial class TestSceneMatchmakingQueueScreen : MultiplayerTestScene { [Cached] - private readonly MatchmakingController controller = new MatchmakingController(); + private readonly QueueController controller = new QueueController(); - private MatchmakingQueueScreen? queueScreen => Stack.CurrentScreen as MatchmakingQueueScreen; + private ScreenQueue? queueScreen => Stack.CurrentScreen as ScreenQueue; [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); - AddStep("load screen", () => LoadScreen(new MatchmakingIntroScreen())); + AddStep("load screen", () => LoadScreen(new IntroScreen())); } [Test] @@ -44,15 +44,15 @@ namespace osu.Game.Tests.Visual.Matchmaking }).ToArray(); }); - AddStep("change state to idle", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Idle)); + AddStep("change state to idle", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.Idle)); - AddStep("change state to queueing", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.Queueing)); + AddStep("change state to queueing", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.Queueing)); - AddStep("change state to found match", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept)); + AddStep("change state to found match", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.PendingAccept)); - AddStep("change state to waiting for room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.AcceptedWaitingForRoom)); + AddStep("change state to waiting for room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.AcceptedWaitingForRoom)); - AddStep("change state to in room", () => queueScreen!.SetState(MatchmakingQueueScreen.MatchmakingScreenState.InRoom)); + AddStep("change state to in room", () => queueScreen!.SetState(ScreenQueue.MatchmakingScreenState.InRoom)); } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs index 416811d345..a598ce9a39 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreen.cs @@ -6,9 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; -using osu.Framework.Graphics.Primitives; using osu.Framework.Screens; -using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -18,11 +16,8 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; -using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.Matchmaking { @@ -32,7 +27,7 @@ namespace osu.Game.Tests.Visual.Matchmaking private const int beatmap_count = 50; private MultiplayerRoomUser[] users = null!; - private MatchmakingScreen screen = null!; + private ScreenMatchmaking screen = null!; public override void SetUpSteps() { @@ -40,7 +35,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => { - var room = CreateDefaultRoom(); + var room = CreateDefaultRoom(MatchType.Matchmaking); room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem { ID = i, @@ -53,6 +48,20 @@ namespace osu.Game.Tests.Visual.Matchmaking WaitForJoined(); + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + setupRequestHandler(); AddStep("load match", () => @@ -71,7 +80,7 @@ namespace osu.Game.Tests.Visual.Matchmaking StarRating = i / 10.0 }).ToArray(); - LoadScreen(screen = new MatchmakingScreen(new MultiplayerRoom(0) + LoadScreen(screen = new ScreenMatchmaking(new MultiplayerRoom(0) { Users = users, Playlist = beatmaps @@ -83,104 +92,51 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestGameplayFlow() { - // Initial "ready" status of the room". - AddWaitStep("wait", 5); - - AddStep("round start", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + for (int round = 1; round <= 3; round++) { - Stage = MatchmakingStage.RoundWarmupTime - }).WaitSafely()); + AddLabel($"Round {round}"); - // Next round starts with picks. - AddWaitStep("wait", 5); - - AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.UserBeatmapSelect - }).WaitSafely()); - - // Make some selections - AddWaitStep("wait", 5); - - for (int i = 0; i < 3; i++) - { - int j = i * 2; - AddStep("click a beatmap", () => + int r = round; + changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r); + changeStage(MatchmakingStage.UserBeatmapSelect); + changeStage(MatchmakingStage.ServerBeatmapFinalised, state => { - Quad panelQuad = this.ChildrenOfType().ElementAt(j).ScreenSpaceDrawQuad; + MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem + { + ID = i, + BeatmapID = i, + StarRating = i / 10.0, + }).ToArray(); - InputManager.MoveMouseTo(new Vector2(panelQuad.Centre.X, panelQuad.TopLeft.Y + 5)); - InputManager.Click(MouseButton.Left); - }); + state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); + state.CandidateItem = beatmaps[0].ID; + }, waitTime: 35); - AddWaitStep("wait", 2); + changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); + changeStage(MatchmakingStage.GameplayWarmupTime); + changeStage(MatchmakingStage.Gameplay); + changeStage(MatchmakingStage.ResultsDisplaying); } - // Lock in the gameplay beatmap - - AddStep("selection", () => + changeStage(MatchmakingStage.Ended, state => { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); + int i = 1; - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState + foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next())) { - Stage = MatchmakingStage.ServerBeatmapFinalised, - CandidateItems = beatmaps.Select(b => b.ID).ToArray(), - CandidateItem = beatmaps[0].ID - }).WaitSafely(); + state.Users[user.UserID].Placement = i++; + state.Users[user.UserID].Points = (8 - i) * 7; + state.Users[user.UserID].Rounds[1].Placement = 1; + state.Users[user.UserID].Rounds[1].TotalScore = 1; + state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1; + } }); + } - // Prepare gameplay. - AddWaitStep("wait", 25); - - AddStep("prepare gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.GameplayWarmupTime - }).WaitSafely()); - - // Start gameplay. - AddWaitStep("wait", 5); - - AddStep("gameplay", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.Gameplay - }).WaitSafely()); - - AddStep("start gameplay", () => MultiplayerClient.StartMatch().WaitSafely()); - // AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); - - // Finish gameplay. - AddWaitStep("wait", 5); - - AddStep("round end", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.ResultsDisplaying - }).WaitSafely()); - - AddWaitStep("wait", 10); - - AddStep("room end", () => - { - MatchmakingRoomState state = new MatchmakingRoomState - { - CurrentRound = 1, - Stage = MatchmakingStage.Ended - }; - - int localUserId = API.LocalUser.Value.OnlineID; - - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Rounds[1].Placement = 1; - state.Users[localUserId].Rounds[1].TotalScore = 1; - state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; - - MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); - }); + private void changeStage(MatchmakingStage stage, Action? prepare = null, int waitTime = 5) + { + AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely()); + AddWaitStep("wait", waitTime); } private void setupRequestHandler() diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs deleted file mode 100644 index be3d7463d6..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneMatchmakingScreenStack : MultiplayerTestScene - { - private const int user_count = 8; - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("join room", () => - { - var room = CreateDefaultRoom(); - room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - })).ToArray(); - - JoinRoom(room); - }); - - WaitForJoined(); - - AddStep("add carousel", () => - { - Child = new MatchmakingScreenStack - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }; - }); - - AddStep("join users", () => - { - var users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i) - { - User = new APIUser - { - Username = $"Player {i}" - } - }).ToArray(); - - foreach (var user in users) - MultiplayerClient.AddUser(user); - }); - } - - [Test] - public void TestStatus() - { - AddWaitStep("wait for scroll", 5); - AddStep("pick", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.UserBeatmapSelect - }).WaitSafely()); - - AddWaitStep("wait for scroll", 5); - AddStep("selection", () => - { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); - - beatmaps = Random.Shared.GetItems(beatmaps, 8); - - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = MatchmakingStage.ServerBeatmapFinalised, - CandidateItems = beatmaps.Select(b => b.ID).ToArray(), - CandidateItem = beatmaps[0].ID - }).WaitSafely(); - }); - - AddWaitStep("wait for scroll", 35); - AddStep("room end", () => - { - var state = new MatchmakingRoomState - { - CurrentRound = 1, - Stage = MatchmakingStage.Ended - }; - - int localUserId = API.LocalUser.Value.OnlineID; - - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Rounds[1].Placement = 1; - state.Users[localUserId].Rounds[1].TotalScore = 1; - state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; - - state.Users[1].Placement = 2; - state.Users[1].Rounds[1].Placement = 2; - - MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs similarity index 65% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs rename to osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs index 494f9b6517..bdae656855 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoomStatisticPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePanelRoomAward.cs @@ -2,18 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneRoomStatisticPanel : MultiplayerTestScene + public partial class TestScenePanelRoomAward : MultiplayerTestScene { public override void SetUpSteps() { base.SetUpSteps(); - AddStep("add statistic", () => Child = new RoomStatisticPanel("Statistic description", 1) + AddStep("add award", () => Child = new PanelRoomAward("Award name", "Description of what this award means", 1) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs index 16f687d772..e894616f9e 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePickScreen.cs @@ -5,11 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking @@ -58,7 +59,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => { - var room = CreateDefaultRoom(); + var room = CreateDefaultRoom(MatchType.Matchmaking); room.Playlist = items; JoinRoom(room); @@ -78,9 +79,9 @@ namespace osu.Game.Tests.Visual.Matchmaking { var selectedItems = new List(); - PickScreen screen = null!; + SubScreenBeatmapSelect screen = null!; - AddStep("add screen", () => LoadScreen(screen = new PickScreen())); + AddStep("add screen", () => Child = new ScreenStack(screen = new SubScreenBeatmapSelect())); AddStep("select maps", () => { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index dafb2d9f03..bef4b26b6f 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -1,13 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Users; @@ -21,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) @@ -64,7 +67,10 @@ namespace osu.Game.Tests.Visual.Matchmaking } }).WaitSafely()); - AddToggleStep("toggle horizontal", h => panel.Horizontal = h); + foreach (var layout in Enum.GetValues()) + { + AddStep($"set layout to {layout}", () => panel.DisplayMode = layout); + } } [Test] @@ -90,5 +96,11 @@ namespace osu.Game.Tests.Visual.Matchmaking } }).WaitSafely()); } + + [Test] + public void TestJump() + { + AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely()); + } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs new file mode 100644 index 0000000000..d5ab571a7d --- /dev/null +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanelOverlay.cs @@ -0,0 +1,159 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; +using osu.Game.Tests.Visual.Multiplayer; +using osuTK; + +namespace osu.Game.Tests.Visual.Matchmaking +{ + public partial class TestScenePlayerPanelOverlay : MultiplayerTestScene + { + private PlayerPanelOverlay list = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); + WaitForJoined(); + + AddStep("add list", () => Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Child = list = new PlayerPanelOverlay() + }); + } + + [Test] + public void TestChangeDisplayMode() + { + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + + AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split); + AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid); + AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden); + } + + [Test] + public void AddPanelsGrid() + { + AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid); + + int userId = 0; + + AddRepeatStep("join user", () => + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(userId) + { + User = new APIUser + { + Username = $"User {userId}" + } + }); + + userId++; + }, 8); + } + + [Test] + public void AddPanelsSplit() + { + AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split); + + int userId = 0; + + AddRepeatStep("join user", () => + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(userId) + { + User = new APIUser + { + Username = $"User {userId}" + } + }); + + userId++; + }, 8); + } + + [Test] + public void RemovePanels() + { + AddStep("join another user", () => + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(1) + { + User = new APIUser + { + Username = "User 1" + } + }); + }); + + AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + + AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); + AddUntilStep("one panel displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + } + + [Test] + public void ChangeRankings() + { + AddStep("join users", () => + { + for (int i = 0; i < 7; i++) + { + MultiplayerClient.AddUser(new MultiplayerRoomUser(i) + { + User = new APIUser + { + Username = $"User {i}" + } + }); + } + }); + + AddStep("set random placements", () => + { + MultiplayerRoom room = MultiplayerClient.ServerRoom!; + + int[] placements = Enumerable.Range(1, room.Users.Count).ToArray(); + Random.Shared.Shuffle(placements); + + MatchmakingRoomState state = new MatchmakingRoomState(); + + for (int i = 0; i < room.Users.Count; i++) + state.Users[room.Users[i].UserID].Placement = placements[i]; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs index 5fd5b1c906..4d1a40cc10 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneResultsScreen.cs @@ -8,8 +8,9 @@ using osu.Framework.Screens; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -17,25 +18,71 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneResultsScreen : MultiplayerTestScene { - private const int invalid_user_id = 1; - public override void SetUpSteps() { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); + AddStep("set initial results", () => + { + var state = new MatchmakingRoomState + { + CurrentRound = 6, + Stage = MatchmakingStage.Ended + }; + + int localUserId = API.LocalUser.Value.OnlineID; + + // Overall state. + state.Users[localUserId].Placement = 1; + state.Users[localUserId].Points = 8; + for (int round = 1; round <= state.CurrentRound; round++) + state.Users[localUserId].Rounds[round].Placement = round; + + // Highest score. + state.Users[localUserId].Rounds[1].TotalScore = 1000; + + // Highest accuracy. + state.Users[localUserId].Rounds[2].Accuracy = 0.9995; + + // Highest combo. + state.Users[localUserId].Rounds[3].MaxCombo = 100; + + // Most bonus score. + state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50; + + // Smallest score difference. + state.Users[localUserId].Rounds[5].TotalScore = 1000; + + // Largest score difference. + state.Users[localUserId].Rounds[6].TotalScore = 1000; + + MultiplayerClient.ChangeMatchRoomState(state).WaitSafely(); + }); + AddStep("add results screen", () => { - Child = new ScreenStack(new ResultsScreen()) + Child = new ScreenStack(new SubScreenResults()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(0.8f) }; }); + } + [Test] + public void TestBasic() + { + AddStep("do nothing", () => { }); + } + + [Test] + public void TestInvalidUser() + { + const int invalid_user_id = 1; AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(invalid_user_id) { User = new APIUser @@ -44,11 +91,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Username = "Invalid user" } })); - } - [Test] - public void TestResults() - { AddStep("set results stage", () => { var state = new MatchmakingRoomState diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs index e19d228c85..cbdbd33158 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneRoundResultsScreen.cs @@ -13,7 +13,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults; using osu.Game.Tests.Visual.Multiplayer; using osuTK; @@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); setupRequestHandler(); AddStep("load screen", () => { - Child = new ScreenStack(new RoundResultsScreen()) + Child = new ScreenStack(new SubScreenRoundResults()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs deleted file mode 100644 index 6349f01f28..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageBubble.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Game.Online.Matchmaking; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneStageBubble : MultiplayerTestScene - { - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); - WaitForJoined(); - - AddStep("add bubble", () => Child = new StageBubble(MatchmakingStage.RoundWarmupTime, "Next Round") - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 100 - }); - } - - [Test] - public void TestStartStopCountdown() - { - MultiplayerCountdown countdown = null!; - - AddStep("start countdown", () => MultiplayerClient.StartCountdown(countdown = new MatchmakingStageCountdown - { - Stage = MatchmakingStage.RoundWarmupTime, - TimeRemaining = TimeSpan.FromSeconds(5) - }).WaitSafely()); - - AddWaitStep("wait a bit", 10); - - AddStep("stop countdown", () => MultiplayerClient.StopCountdown(countdown).WaitSafely()); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs index 49680acd64..dc4f09c555 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageDisplay.cs @@ -1,56 +1,61 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Game.Online.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneStageDisplay : MultiplayerTestScene { + [Cached] + protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); + public override void SetUpSteps() { base.SetUpSteps(); - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add bubble", () => Child = new StageDisplay + AddStep("add display", () => Child = new StageDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, - Width = 0.5f, }); } [Test] - public void TestStartCountdown() + public void TestChangeStage() { - foreach (var status in Enum.GetValues()) + addStage(MatchmakingStage.WaitingForClientsJoin); + + for (int i = 1; i <= 5; i++) { - AddStep($"{status}", () => - { - MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState - { - Stage = status - }).WaitSafely(); - - MultiplayerClient.StartCountdown(new MatchmakingStageCountdown - { - Stage = status, - TimeRemaining = TimeSpan.FromSeconds(5) - }).WaitSafely(); - }); - - AddWaitStep("wait a bit", 10); + addStage(MatchmakingStage.RoundWarmupTime); + addStage(MatchmakingStage.UserBeatmapSelect); + addStage(MatchmakingStage.ServerBeatmapFinalised); + addStage(MatchmakingStage.WaitingForClientsBeatmapDownload); + addStage(MatchmakingStage.GameplayWarmupTime); + addStage(MatchmakingStage.Gameplay); + addStage(MatchmakingStage.ResultsDisplaying); } + + addStage(MatchmakingStage.Ended); + } + + private void addStage(MatchmakingStage stage) + { + AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely()); + AddWaitStep("wait a bit", 10); } } } diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs deleted file mode 100644 index 0094c7645a..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneStageText.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneStageText : MultiplayerTestScene - { - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("join room", () => JoinRoom(CreateDefaultRoom())); - WaitForJoined(); - - AddStep("create display", () => Child = new StageText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - } - - [TestCase(MatchmakingStage.WaitingForClientsJoin)] - [TestCase(MatchmakingStage.RoundWarmupTime)] - [TestCase(MatchmakingStage.UserBeatmapSelect)] - [TestCase(MatchmakingStage.ServerBeatmapFinalised)] - [TestCase(MatchmakingStage.WaitingForClientsBeatmapDownload)] - [TestCase(MatchmakingStage.GameplayWarmupTime)] - [TestCase(MatchmakingStage.Gameplay)] - [TestCase(MatchmakingStage.ResultsDisplaying)] - [TestCase(MatchmakingStage.Ended)] - public void TestStatus(MatchmakingStage status) - { - AddStep("set status", () => MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState { Stage = status }).WaitSafely()); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 0e8093f459..184bb33c2f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -36,6 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; + private RulesetStore rulesets = null!; private TestMultiplayerComponents multiplayerComponents = null!; @@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { BeatmapStore beatmapStore; - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); @@ -115,5 +117,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 7e19f45a00..c8216c54be 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -8,6 +8,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -37,13 +38,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneDrawableRoomPlaylist : MultiplayerTestScene { + private RulesetStore rulesets = null!; private TestPlaylist playlist = null!; private BeatmapManager manager = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } @@ -436,6 +438,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private partial class TestPlaylist : DrawableRoomPlaylist { public new IReadOnlyDictionary> ItemMap => base.ItemMap; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 083b5b14fb..4c487c8288 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiplayer : ScreenTestScene { + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapSetInfo importedSet2 = null!; @@ -67,7 +68,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { BeatmapStore beatmapStore; - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); @@ -1247,5 +1248,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 9c85bdd57a..e6f3d7e5ac 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Platform; using osu.Framework.Screens; @@ -170,6 +171,14 @@ namespace osu.Game.Tests.Visual.Multiplayer .All(b => b.Mod.GetType() != type)); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private partial class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect { public new Bindable> Mods => base.Mods; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index aa4c4949fb..792bff63d3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; @@ -44,6 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneMultiplayerMatchSubScreen : MultiplayerTestScene { private MultiplayerMatchSubScreen screen = null!; + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private Room room = null!; @@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); Dependencies.CacheAs(new RealmDetachedBeatmapStore()); @@ -462,6 +464,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("settings still open", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index c6a203c77a..44177c080c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; @@ -28,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene { private MultiplayerPlaylist list = null!; + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; @@ -35,7 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } @@ -290,5 +292,13 @@ namespace osu.Game.Tests.Visual.Multiplayer .Single() .Items.Any(i => i.ID == playlistItemId); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index d7659351bb..2f54551fa8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public partial class TestSceneMultiplayerQueueList : MultiplayerTestScene { private MultiplayerQueueList playlist = null!; + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; @@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } @@ -168,5 +170,13 @@ namespace osu.Game.Tests.Visual.Multiplayer var button = playlist.ChildrenOfType().ElementAtOrDefault(index); return (button?.Alpha > 0) == visible; }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 12bc3c1418..fe9ea632cf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -32,12 +33,13 @@ namespace osu.Game.Tests.Visual.Multiplayer private Room room = null!; private BeatmapSetInfo importedSet = null!; + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); @@ -162,5 +164,13 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertReadyButtonEnablement(bool shouldBeEnabled) => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 066c981cd2..7135ff930d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; @@ -31,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestScenePlaylistsSongSelect : OnlinePlayTestScene { + private RulesetStore rulesets = null!; private BeatmapManager manager = null!; private TestPlaylistsSongSelect songSelect = null!; private Room room = null!; @@ -40,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { BeatmapStore beatmapStore; - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); @@ -189,6 +191,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("mod select visible", () => this.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Visible)); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private partial class TestPlaylistsSongSelect : PlaylistsSongSelect { public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 05136ebee1..2e08b494bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -27,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneTeamVersus : ScreenTestScene { + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; @@ -37,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } @@ -182,5 +184,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 4c395dd17e..013d2a9315 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -472,7 +472,8 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep($"config value is {configValue}", () => getConfigManager().Get(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue)); AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType().Single().TargetTimeRange, - () => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue, 2000, 1.0 / 60.0))); + () => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue, 200, 1))); + // 按自定义的方式计算 } ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get().GetConfigFor(new ManiaRuleset())!); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 02b2db6e31..0e1fa63439 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -278,6 +278,8 @@ namespace osu.Game.Tests.Visual.Navigation { advanceToSongSelect(); openSkinEditor(); + AddUntilStep("skin editor visible", () => skinEditor.State.Value == Visibility.Visible); + AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -290,8 +292,9 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0)); toggleSkinEditor(); + AddUntilStep("skin editor hidden", () => skinEditor.State.Value == Visibility.Hidden); - AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1))); + AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(2))); AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0)); AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0))); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs index 9a1f1dc515..4295e9e88e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs @@ -9,18 +9,24 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; +using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.Navigation @@ -217,6 +223,84 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("leaderboard matches gameplay beatmap", () => Game.ChildrenOfType().Single().CurrentCriteria?.Beatmap, () => Is.EqualTo(beatmap().BeatmapInfo)); } + [Test] + public void TestEnterKeyProgressesToGameplayEvenIfCarouselFilteredOut() + { + PushAndConfirm(() => new SoloSongSelect()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("filter out active beatmap", () => this.ChildrenOfType().First().Text = "abacadabadaeba"); + AddUntilStep("wait for filter", () => this.ChildrenOfType().Single().IsFiltering, () => Is.False); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("player entered", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + } + + [Test] + public void TestSelectionNotLostWithConvertedBeatmapsShown() + { + BeatmapSetInfo beatmapSet = null!; + BeatmapInfo selectedBeatmap = null!; + + AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadOszIntoOsu(Game).GetResultSafely()); + PushAndConfirm(() => new SoloSongSelect()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("change ruleset to taiko", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Number2); + InputManager.ReleaseKey(Key.ControlLeft); + }); + AddStep("show converts", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); + AddStep("select osu! beatmap", () => + { + selectedBeatmap = beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); + Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(selectedBeatmap); + }); + + pushEscape(); + AddUntilStep("went back to main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + PushAndConfirm(() => new SoloSongSelect()); + + AddUntilStep("selected beatmap is still osu! ruleset", () => Game.Beatmap.Value.BeatmapInfo, () => Is.EqualTo(selectedBeatmap)); + } + + /// + /// Note: This test was written to demonstrate the failure described at https://github.com/ppy/osu/issues/35023, + /// but because the failure scenario there entailed a race condition, it was possible for the test to pass regardless + /// unless was increased. + /// + [Test] + public void TestPresentFromResults() + { + BeatmapSetInfo beatmapToPresent = null!; + BeatmapSetInfo beatmapToPlay = null!; + AddStep("manually insert beatmap to be presented", () => + { + Game.Realm.Write(r => + { + var beatmapSet = TestResources.CreateTestBeatmapSetInfo(3, [r.Find("osu")]); + r.Add(beatmapSet); + beatmapToPresent = beatmapSet.Detach(); + }); + }); + AddStep("import beatmap", () => beatmapToPlay = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); + AddStep("set global beatmap", () => Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(beatmapToPlay.Beatmaps.First())); + playToResults(); + AddStep("present beatmap from results", () => Game.PresentBeatmap(beatmapToPresent)); + AddUntilStep("back at song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect); + AddUntilStep("presented beatmap is current", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapToPresent)); + } + private Func playToResults() { var player = playToCompletion(); diff --git a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs index abfc5c4d0e..7a11581d27 100644 --- a/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs +++ b/osu.Game.Tests/Visual/Playlists/TestSceneAddPlaylistToCollectionButton.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; @@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene { + private RulesetStore rulesets = null!; private BeatmapManager manager = null!; private BeatmapSetInfo importedBeatmap = null!; private Room room = null!; @@ -33,7 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); @@ -112,5 +114,13 @@ namespace osu.Game.Tests.Visual.Playlists } ]; }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 2e90f08d47..44c2e7eb55 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -32,6 +33,7 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsRoomCreation : OnlinePlayTestScene { + private RulesetStore rulesets = null!; private BeatmapManager manager = null!; private TestPlaylistsRoomSubScreen match = null!; private BeatmapSetInfo importedBeatmap = null!; @@ -40,7 +42,7 @@ namespace osu.Game.Tests.Visual.Playlists [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } @@ -220,6 +222,14 @@ namespace osu.Game.Tests.Visual.Playlists }); }); + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen { public new Bindable SelectedItem => base.SelectedItem; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 0eed6c9f5f..87f65111b0 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -38,6 +39,7 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene { + private RulesetStore rulesets = null!; private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; @@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.Playlists { BeatmapStore beatmapStore; - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); Dependencies.Cache(Realm); @@ -579,6 +581,14 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("mods set", () => SelectedMods.Value.Count == 1 && SelectedMods.Value.OfType().Any()); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } + private partial class TestPlaylistsScreen : OsuScreen { public TestPlaylistsScreen(PlaylistsRoomSubScreen screen) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs index cd8f234f04..e86ed8cd89 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; @@ -531,5 +532,13 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("only one score with ID 12345", () => this.ChildrenOfType().Count(s => s.Score.OnlineID == 12345), () => Is.EqualTo(1)); AddUntilStep("user best position preserved", () => this.ChildrenOfType().Any(p => p.ScorePosition.Value == 133_337)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesetStore.IsNotNull()) + rulesetStore.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index b682ec7265..88e381a468 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -219,8 +220,15 @@ namespace osu.Game.Tests.Visual.Ranking Tags = [ new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", }, - new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", }, - new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", }, + new APITag + { + Id = 2, Name = "style/clean", + Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", + }, + new APITag + { + Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", + }, new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", }, ] }), 500); @@ -403,6 +411,14 @@ namespace osu.Game.Tests.Visual.Ranking return hitEvents; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesetStore.IsNotNull()) + rulesetStore?.Dispose(); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index b7836b6e44..4f91e355c0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -71,9 +71,9 @@ namespace osu.Game.Tests.Visual.Ranking var beatmapSet = CreateAPIBeatmapSet(Beatmap.Value.BeatmapInfo); beatmapSet.Beatmaps.Single().TopTags = [ - new APIBeatmapTag { TagId = 3, VoteCount = 9 }, - new APIBeatmapTag { TagId = 2, VoteCount = 8 }, - new APIBeatmapTag { TagId = 0, VoteCount = 7 }, + new APIBeatmapTag { TagId = 3, VoteCount = 4 }, + new APIBeatmapTag { TagId = 2, VoteCount = 3 }, + new APIBeatmapTag { TagId = 0, VoteCount = 2 }, ]; Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500); return true; @@ -155,14 +155,14 @@ namespace osu.Game.Tests.Visual.Ranking InputManager.MoveMouseTo(getDrawableTagById(2)); InputManager.Click(MouseButton.Left); }); - AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9)); + AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(4)); AddStep("remove vote for tag 2", () => { InputManager.MoveMouseTo(getDrawableTagById(2)); InputManager.Click(MouseButton.Left); }); - AddUntilStep("tag 2 not voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(8)); + AddUntilStep("tag 2 not voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(3)); AddAssert("tag 2 is still second", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(1)); AddStep("vote for tag 2", () => @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Ranking InputManager.MoveMouseTo(getDrawableTagById(2)); InputManager.Click(MouseButton.Left); }); - AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(9)); + AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(4)); AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("tag 2 reordered to first", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(0)); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs index db004b1d0d..8525e33a33 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -28,6 +29,7 @@ namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { + private RulesetStore rulesets = null!; private BeatmapManager beatmapManager = null!; private CollectionDropdown dropdown = null!; @@ -37,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); @@ -86,11 +88,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddUntilStep("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddUntilStep("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -185,11 +187,11 @@ namespace osu.Game.Tests.Visual.SongSelect assertFirstButtonIs(FontAwesome.Solid.PlusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.MinusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare); } @@ -269,5 +271,13 @@ namespace osu.Game.Tests.Visual.SongSelect CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs index 475d8ec461..b690bb2708 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -27,13 +28,14 @@ namespace osu.Game.Tests.Visual.SongSelect protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; private DialogOverlay dialogOverlay = null!; + private RulesetStore rulesets = null!; private BeatmapManager beatmapManager = null!; private ManageCollectionsDialog dialog = null!; [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); @@ -379,5 +381,13 @@ namespace osu.Game.Tests.Visual.SongSelect private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType().First().Text == name); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 93b9efed6a..cb0845ede8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; @@ -211,5 +212,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank, () => Is.Null); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 5f3cd26d55..dcd7a5a8fc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var groupModel = (GroupDefinition)groupItem.Model; - Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); + Assert.That(groupModel.Title.ToString(), Is.EqualTo(expectedTitle)); Assert.That(itemsInGroup.Select(i => i.Model).OfType().Select(gb => gb.Beatmap), Is.EquivalentTo(expectedBeatmaps)); totalItems += itemsInGroup.Count() + 1; diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index e3b02e5905..ac8591699a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -190,5 +190,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen()); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (Rulesets.IsNotNull()) + Rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index c34077889d..cf71df914a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -249,5 +249,52 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapSetsCount(10); CheckDisplayedBeatmapsCount(30); } + + [Test] + public void TestGroupDoesNotExpandAgainOnRefilterIfManuallyCollapsed() + { + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title); + + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); + + CheckHasSelection(); + + ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty); + + CheckDisplayedGroupsCount(5); + CheckDisplayedBeatmapSetsCount(10); + CheckDisplayedBeatmapsCount(30); + + ToggleGroupCollapse(); + + ApplyToFilterAndWaitForFilter("apply no-op filter", c => c.AllowConvertedBeatmaps = !c.AllowConvertedBeatmaps); + AddAssert("group didn't re-expand", () => Carousel.ExpandedGroup, () => Is.Null); + + ToggleGroupCollapse(); + AddAssert("beatmap set re-expanded correctly", () => Carousel.ExpandedBeatmapSet?.BeatmapSet, () => Is.EqualTo(BeatmapSets[2])); + + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[1].Metadata.Title); + + CheckDisplayedGroupsCount(1); + CheckDisplayedBeatmapSetsCount(1); + CheckDisplayedBeatmapsCount(3); + + CheckHasSelection(); + } + + [Test] + public void TestManuallyCollapsingCurrentGroupAndOpeningAnother() + { + SelectNextSet(); + ToggleGroupCollapse(); + SelectNextGroup(); + AddUntilStep("no beatmap panels visible", () => GetVisiblePanels().Count(), () => Is.Zero); + + SelectNextSet(); + SelectNextSet(); + AddUntilStep("no beatmap panels visible", () => GetVisiblePanels().Count(), () => Is.Zero); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 58ecfcbf3b..2cffe60ec1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -337,5 +337,31 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("expanded group is still first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); } + + [Test] + public void TestExpandedGroupDoesNotExpandAgainOnRefilterIfManuallyCollapsed() + { + SelectPrevSet(); + + WaitForBeatmapSelection(2, 9); + AddAssert("expanded group is last", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(6)); + + SelectNextPanel(); + Select(); + + WaitForBeatmapSelection(2, 9); + AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); + + ToggleGroupCollapse(); + + // doesn't actually filter anything away, but triggers a filter. + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Some"); + AddAssert("group didn't re-expand", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.Null); + + ToggleGroupCollapse(); + + ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = "Som"); + AddAssert("expanded group is first", () => (Carousel.ExpandedGroup as StarDifficultyGroupDefinition)?.Difficulty.Stars, () => Is.EqualTo(0)); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs index 6e3fafdd6a..a37700f6be 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardSorting.cs @@ -151,5 +151,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, }); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesetStore.IsNotNull()) + rulesetStore.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index 8fcb3d7acc..1c3a5e4bab 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -578,5 +578,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }, }; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesetStore.IsNotNull()) + rulesetStore.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs index 5c4969f9ad..8cee78e0b8 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -29,6 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { + private RulesetStore rulesets = null!; private BeatmapManager beatmapManager = null!; private CollectionDropdown dropdown = null!; @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); @@ -87,11 +89,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddUntilStep("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddUntilStep("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -186,11 +188,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 assertFirstButtonIs(FontAwesome.Solid.PlusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.MinusSquare); addClickAddOrRemoveButtonStep(1); - AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + AddUntilStep("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); assertFirstButtonIs(FontAwesome.Solid.PlusSquare); } @@ -260,5 +262,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index 3b9a07437a..f678ec372a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Cursor; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -86,6 +87,64 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } } + [Test] + public void TestRanks() + { + for (int i = -1; i <= 7; i++) + { + ScoreRank rank = (ScoreRank)i; + + AddStep($"display rank {rank}", () => + { + ContentContainer.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + }, + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new[] + { + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)) + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), + KeyboardSelected = { Value = true }, + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), + Expanded = { Value = true }, + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), + Expanded = { Value = true }, + KeyboardSelected = { Value = true }, + }, + }, + } + } + }; + }); + } + } + protected override Drawable CreateContent() { return new OsuContextMenuContainer diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 945ec5d207..8419684b27 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -22,6 +22,7 @@ using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; @@ -143,6 +144,41 @@ namespace osu.Game.Tests.Visual.SongSelectV2 void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen); } + [TestCase(true)] + [TestCase(false)] + public void TestHoveringLeftSideReexpandsGroupSelectionIsIn(bool mouseOverPanel) + { + ImportBeatmapForRuleset(0); + + LoadSongSelect(); + SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); + + AddStep("move mouse to carousel", () => InputManager.MoveMouseTo(Carousel)); + + AddUntilStep("expanded group is below 1 star", + () => (Carousel.ChildrenOfType().SingleOrDefault(p => p.Expanded.Value)?.Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars, + () => Is.EqualTo(0)); + + AddStep("select next group", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.Right); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + AddUntilStep("expanded group is 3 star", + () => (Carousel.ChildrenOfType().SingleOrDefault(p => p.Expanded.Value)?.Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars, + () => Is.EqualTo(3)); + + if (mouseOverPanel) + AddStep("move mouse over left panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + else + AddStep("move mouse to left side container", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + + AddUntilStep("expanded group is below 1 star", + () => (Carousel.ChildrenOfType().Single(p => p.Expanded.Value).Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars, + () => Is.EqualTo(0)); + } + #region Hotkeys [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs index 7c604eb37b..0ec61f59da 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectCurrentSelectionInvalidated.cs @@ -55,22 +55,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2 waitForFiltering(6); BeatmapInfo? initiallySelected = null; - AddAssert("selected is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddAssert("carousel beatmap is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddUntilStep("global beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1)); ChangeRuleset(0); waitForFiltering(7); - AddAssert("selected is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0)); - AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); + AddAssert("carousel beatmap is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0)); + AddUntilStep("global beatmap is osu", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(0)); + AddAssert("carousel beatmap is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); ChangeRuleset(1); waitForFiltering(8); - AddAssert("selected is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1)); - AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); + AddAssert("carousel beatmap is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddUntilStep("global beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1)); + AddAssert("carousel beatmap is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet)); ChangeRuleset(2); waitForFiltering(9); - AddAssert("selected is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2)); - AddAssert("selected is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet)); + AddAssert("carousel beatmap is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2)); + AddUntilStep("global beatmap is catch", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(2)); + AddAssert("carousel beatmap is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet)); } /// diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index aa80321033..e65c9553c2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -245,7 +245,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 GroupBy(GroupMode.RankAchieved); WaitForFiltering(); - assertGroupPresent("S+", () => new[] { beatmapSets[0] }); + assertGroupPresent("Silver S", () => new[] { beatmapSets[0] }); assertGroupPresent("A", () => new[] { beatmapSets[1] }); assertGroupPresent("C", () => new[] { beatmapSets[2] }); assertGroupPresent("Unplayed", () => new[] { beatmapSets[3], beatmapSets[4] }); @@ -316,7 +316,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddAssert($"\"{name}\" present", () => { - var group = grouping.GroupItems.Single(g => g.Key.Title == name); + var group = grouping.GroupItems.Single(g => g.Key.Title.ToString() == name); var actualBeatmaps = group.Value.Select(i => i.Model).OfType().Select(gb => gb.Beatmap).OrderBy(b => b.ID); var expectedBeatmaps = getBeatmaps().SelectMany(s => s.Beatmaps).OrderBy(b => b.ID); return actualBeatmaps.SequenceEqual(expectedBeatmaps); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index f7bdda6b57..c2277f2c7c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; @@ -37,6 +38,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly ContextMenuContainer contextMenuContainer; private readonly BeatmapLeaderboard leaderboard; + private RulesetStore rulesets = null!; private BeatmapManager beatmapManager; private ScoreManager scoreManager; @@ -71,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(new RealmRulesetStore(Realm)); + dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); @@ -151,7 +153,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click delete option", () => { - InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType().First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase))); + InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType() + .First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase))); InputManager.Click(MouseButton.Left); }); @@ -178,5 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for fetch", () => leaderboard.Scores.Any()); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index b7c1428397..c202442f9c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -469,5 +469,13 @@ namespace osu.Game.Tests.Visual.UserInterface Ruleset = rulesets.GetRuleset(3).AsNonNull() } }; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 017d246461..6127be481c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -1057,6 +1058,14 @@ namespace osu.Game.Tests.Visual.UserInterface private ModPanel getPanelForMod(Type modType) => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesetStore.IsNotNull()) + rulesetStore.Dispose(); + } + private partial class TestModSelectOverlay : UserModSelectOverlay { public TestModSelectOverlay() diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 2672854e19..b6d9bad5bb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -21,6 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface { protected override bool UseFreshStoragePerRun => true; + private RulesetStore rulesets = null!; private BeatmapManager beatmapManager = null!; private const int item_count = 20; @@ -30,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); } @@ -62,5 +64,13 @@ namespace osu.Game.Tests.Visual.UserInterface // Ensure all the initial imports are present before running any tests. Realm.Run(r => r.Refresh()); }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (rulesets.IsNotNull()) + rulesets.Dispose(); + } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a3e7c1365e..b828d88591 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -284,6 +284,7 @@ namespace osu.Game.Beatmaps /// /// Returns a list of all usable s. + /// IMPORTANT: This should not be used outside of tests. Consider using instead. /// /// A list of available . public List GetAllUsableBeatmapSets() diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 54f8d656fe..8cd0ac965a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards public const float TRANSITION_DURATION = 340; public const float CORNER_RADIUS = 8; - protected const float WIDTH = 345; + public const float WIDTH = 345; public IBindable Expanded { get; } @@ -77,25 +77,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards containingInputManager = GetContainingInputManager(); - Action = () => - { - if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) - { - switch (DownloadTracker.State.Value) - { - case DownloadState.NotDownloaded: - if (!BeatmapSet.Availability.DownloadDisabled) - beatmaps?.Download(BeatmapSet, preferNoVideo.Value); - break; + if (Action == null) + throw new InvalidOperationException($"An action should be assigned to this {nameof(BeatmapCard)}. To use the default, assign {nameof(DefaultAction)}."); + } - case DownloadState.LocallyAvailable: - game?.PresentBeatmap(BeatmapSet); - break; - } + protected void DefaultAction() + { + if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) + { + switch (DownloadTracker.State.Value) + { + case DownloadState.NotDownloaded: + if (!BeatmapSet.Availability.DownloadDisabled) beatmaps?.Download(BeatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + game?.PresentBeatmap(BeatmapSet); + break; } - else - beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); - }; + } + else + beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs index deb56bb281..a57f3e7ce7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs @@ -21,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo) + public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false) { InternalChildren = new Drawable[] { @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.Both, }, - cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), 500, 500) + cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000) { RelativeSizeAxes = Axes.Both, Colour = Colour4.Transparent diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 9428984115..222acbc039 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -46,6 +46,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, allowExpansion) { content = new BeatmapCardContent(height); + + Action = DefaultAction; } [BackgroundDependencyLoader(true)] @@ -278,8 +280,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards } createStatistics(); - - Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); } private LocalisableString createArtistText() diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 62108fe6f5..c23a03aabe 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -54,6 +54,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, false) { content = new BeatmapCardContent(height); + + Action = DefaultAction; } [BackgroundDependencyLoader] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 505a6fcdae..ac9ee94f56 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -47,6 +47,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, allowExpansion) { content = new BeatmapCardContent(HEIGHT); + + Action = DefaultAction; } [BackgroundDependencyLoader] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 1f6f638618..4a7054588e 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -35,11 +35,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false) { InternalChildren = new Drawable[] { - new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List, keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000) { RelativeSizeAxes = Axes.Both, OnlineInfo = onlineInfo diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs index d2c077d010..3d732b6683 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs @@ -16,10 +16,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private readonly Bindable state = new Bindable(); private readonly APIBeatmapSet beatmapSet; + private readonly bool allowNavigationToBeatmap; - public GoToBeatmapButton(APIBeatmapSet beatmapSet) + public GoToBeatmapButton(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap) { this.beatmapSet = beatmapSet; + this.allowNavigationToBeatmap = allowNavigationToBeatmap; } [BackgroundDependencyLoader(true)] @@ -27,7 +29,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons { Action = () => game?.PresentBeatmap(beatmapSet); Icon.Icon = FontAwesome.Solid.AngleDoubleRight; - TooltipText = "Go to beatmap"; } protected override void LoadComplete() @@ -40,8 +41,31 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void updateState() { - Enabled.Value = state.Value == DownloadState.LocallyAvailable; - this.FadeTo(Enabled.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + bool available = state.Value == DownloadState.LocallyAvailable; + Enabled.Value = allowNavigationToBeatmap && available; + + float alpha; + + if (available && allowNavigationToBeatmap) + { + TooltipText = "Go to beatmap"; + Enabled.Value = true; + alpha = 1f; + } + else if (available) + { + TooltipText = string.Empty; + Enabled.Value = false; + alpha = 0.3f; + } + else + { + TooltipText = string.Empty; + Enabled.Value = false; + alpha = 0; + } + + this.FadeTo(alpha, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 56d405ce3c..8262e787d8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards set { buttonsExpandedWidth = value; - buttonArea.Width = value; if (IsLoaded) updateState(); } @@ -67,7 +66,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public CollapsibleButtonContainer(APIBeatmapSet beatmapSet) + public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true, bool keepBackgroundLoaded = false) { downloadTracker = new BeatmapDownloadTracker(beatmapSet); @@ -116,14 +115,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Height = 0.5f, }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State }, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - } } } }, @@ -135,7 +126,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Masking = true, Children = new Drawable[] { - new BeatmapCardContentBackground(beatmapSet) + new BeatmapCardContentBackground(beatmapSet, keepBackgroundLoaded) { RelativeSizeAxes = Axes.Both, Dimmed = { BindTarget = ShowDetails } @@ -152,6 +143,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } }; + + buttons.Add(new GoToBeatmapButton(beatmapSet, allowNavigationToBeatmap) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State }, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + }); } protected override void LoadComplete() @@ -165,6 +165,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { + buttonArea.Width = buttonsExpandedWidth; + float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth; float mainAreaWidth = Width - buttonAreaWidth; diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 0e97f431b8..068af75750 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -109,6 +109,8 @@ namespace osu.Game.Database /// private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1); + private readonly CountdownEvent pendingAsyncOperations = new CountdownEvent(0); + /// /// true when the current thread has already entered the . /// @@ -467,6 +469,30 @@ namespace osu.Game.Database } } + /// + /// Run work on realm on a TPL thread, in a way that ensures that the realm isn't disposed before the work is done. + /// + public Task RunAsync(Func action, CancellationToken token = default) + { + ObjectDisposedException.ThrowIf(isDisposed, this); + + // Required to ensure the read is tracked and accounted for before disposal. + // Can potentially be avoided if we have a need to do so in the future. + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException($@"{nameof(RunAsync)} must be called from the update thread."); + + // CountdownEvent will fail if already at zero. + if (!pendingAsyncOperations.TryAddCount()) + pendingAsyncOperations.Reset(1); + + return Task.Run(() => + { + var result = Run(action); + pendingAsyncOperations.Signal(); + return result; + }, token); + } + /// /// Write changes to realm. /// @@ -507,8 +533,6 @@ namespace osu.Game.Database } } - private readonly CountdownEvent pendingAsyncWrites = new CountdownEvent(0); - /// /// Write changes to realm asynchronously, guaranteeing order of execution. /// @@ -523,8 +547,8 @@ namespace osu.Game.Database throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread."); // CountdownEvent will fail if already at zero. - if (!pendingAsyncWrites.TryAddCount()) - pendingAsyncWrites.Reset(1); + if (!pendingAsyncOperations.TryAddCount()) + pendingAsyncOperations.Reset(1); // Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval. // Adding a forced Task.Run resolves this. @@ -539,7 +563,7 @@ namespace osu.Game.Database // ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]). await realm.WriteAsync(() => action(realm)).ConfigureAwait(false); - pendingAsyncWrites.Signal(); + pendingAsyncOperations.Signal(); }); return writeTask; @@ -559,8 +583,8 @@ namespace osu.Game.Database throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread."); // CountdownEvent will fail if already at zero. - if (!pendingAsyncWrites.TryAddCount()) - pendingAsyncWrites.Reset(1); + if (!pendingAsyncOperations.TryAddCount()) + pendingAsyncOperations.Reset(1); // Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval. // Adding a forced Task.Run resolves this. @@ -576,7 +600,7 @@ namespace osu.Game.Database // ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]). result = await realm.WriteAsync(() => action(realm)).ConfigureAwait(false); - pendingAsyncWrites.Signal(); + pendingAsyncOperations.Signal(); return result; }); @@ -1494,7 +1518,7 @@ namespace osu.Game.Database public void Dispose() { - if (!pendingAsyncWrites.Wait(10000)) + if (!pendingAsyncOperations.Wait(10000)) Logger.Log("Realm took too long waiting on pending async writes", level: LogLevel.Error); updateRealm?.Dispose(); diff --git a/osu.Game/Database/RealmDetachedBeatmapStore.cs b/osu.Game/Database/RealmDetachedBeatmapStore.cs index 6954bb320a..f9f84c52e5 100644 --- a/osu.Game/Database/RealmDetachedBeatmapStore.cs +++ b/osu.Game/Database/RealmDetachedBeatmapStore.cs @@ -82,6 +82,34 @@ namespace osu.Game.Database return; } + if (changes.InsertedIndices.Length == 1 && changes.DeletedIndices.Length == 1) + { + lock (detachedBeatmapSets) + { + var deletedSet = detachedBeatmapSets[changes.DeletedIndices[0]]; + var insertedSet = sender[changes.InsertedIndices[0]]; + + // this handles beatmap updates using a heuristic that a beatmap update will preserve the online ID. + // it relies on the fact that updates are performed by removing the old set and adding a new one, in a single transaction. + // instead of removing the old set and adding a new one to the collection too, which would trigger consumers' logic related to set removals, + // move the deleted set to the index occupied by the new one and then replace it in-place. + // due to this, the operation can be presented to consumer in a manner that permits them to actually handle this as a replace operation + // and not trigger any set removal logic that may result in selections changing or similar undesirable side effects. + if (deletedSet.OnlineID == insertedSet.OnlineID) + { + pendingOperations.Enqueue(new OperationArgs + { + Type = OperationType.MoveAndReplace, + BeatmapSet = insertedSet.Detach(), + Index = changes.DeletedIndices[0], + NewIndex = changes.InsertedIndices[0], + }); + + return; + } + } + } + foreach (int i in changes.DeletedIndices.OrderDescending()) { pendingOperations.Enqueue(new OperationArgs @@ -138,6 +166,11 @@ namespace osu.Game.Database detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); break; + case OperationType.MoveAndReplace: + detachedBeatmapSets.Move(op.Index, op.NewIndex!.Value); + detachedBeatmapSets.ReplaceRange(op.NewIndex!.Value, 1, [op.BeatmapSet!]); + break; + case OperationType.Remove: detachedBeatmapSets.RemoveAt(op.Index); break; @@ -160,13 +193,15 @@ namespace osu.Game.Database public OperationType Type; public BeatmapSetInfo? BeatmapSet; public int Index; + public int? NewIndex; } private enum OperationType { Insert, Update, - Remove + Remove, + MoveAndReplace, } } } diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index c84e1e35b8..1bb6b0aba4 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Logging; using Realms; namespace osu.Game.Database @@ -29,6 +30,7 @@ namespace osu.Game.Database // It may be that we access this from the update thread before a refresh has taken place. // To ensure that behaviour matches what we'd expect (the object generally *should be* available), force // a refresh to bring in any off-thread changes immediately. + Logger.Log($"{nameof(FindWithRefresh)} triggered a realm refresh because it couldn't find the requested guid {id}"); realm.Refresh(); found = realm.Find(id); } diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index 33252448fc..ff35dbc2a0 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -36,7 +36,7 @@ namespace osu.Game.Extensions string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"); + return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.InvariantCulture)}"; } /// diff --git a/osu.Game/Graphics/Carousel/Carousel_ScrollContainer.cs b/osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs similarity index 100% rename from osu.Game/Graphics/Carousel/Carousel_ScrollContainer.cs rename to osu.Game/Graphics/Carousel/Carousel.ScrollContainer.cs diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 0df183bb71..a63fee6914 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -453,8 +453,7 @@ namespace osu.Game.Graphics.Carousel // matching with exact modifier consideration (so Ctrl+Enter would be ignored). case Key.Enter: case Key.KeypadEnter: - activateSelection(); - return true; + return activateSelection(); } return base.OnKeyDown(e); @@ -465,8 +464,7 @@ namespace osu.Game.Graphics.Carousel switch (e.Action) { case GlobalAction.Select: - activateSelection(); - return true; + return activateSelection(); // the selection traversal handlers below are scheduled to avoid an issue // wherein if the update frame rate is low, keeping one of the actions below pressed leads to selection moving back to the start / end. @@ -560,10 +558,15 @@ namespace osu.Game.Graphics.Carousel { } - private void activateSelection() + private bool activateSelection() { if (currentKeyboardSelection.CarouselItem != null) + { Activate(currentKeyboardSelection.CarouselItem); + return true; + } + + return false; } private void traverseKeyboardSelection(int direction) @@ -761,10 +764,10 @@ namespace osu.Game.Graphics.Carousel updateItemYPosition(item, ref lastVisible, ref yPos); if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!)) - currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i); + currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i); if (CheckModelEquality(item.Model, currentSelection.Model!)) - currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i); + currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition + item.DrawHeight / 2, i); } // Update the total height of all items (to make the scroll container scrollable through the full height even though @@ -873,13 +876,18 @@ namespace osu.Game.Graphics.Carousel if (!scrollToSelection.IsValid) { - if (currentKeyboardSelection.YPosition != null) - Scroll.ScrollTo(currentKeyboardSelection.YPosition.Value - visibleHalfHeight + BleedTop); + if (GetScrollTarget() is double scrollTarget) + Scroll.ScrollTo(scrollTarget - visibleHalfHeight + BleedTop); scrollToSelection.Validate(); } } + /// + /// Returns the Y position to scroll to in order to show the most relevant carousel item(s). + /// + protected virtual double? GetScrollTarget() => currentKeyboardSelection.YPosition; + protected virtual float GetPanelXOffset(Drawable panel) { Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index fceee90d06..dbc354ae07 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -18,6 +18,8 @@ namespace osu.Game.Graphics.Containers private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + private HoverSounds samples = null!; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation). base.ReceivePositionalInputAt(screenSpacePos) @@ -33,6 +35,14 @@ namespace osu.Game.Graphics.Containers this.sampleSet = sampleSet; } + public void TriggerClickWithSound() + { + TriggerClick(); + + // TriggerClick doesn't recursively fire the event so we need to manually do this. + (samples as HoverClickSounds)?.PlayClickSample(); + } + public virtual LocalisableString TooltipText { get; set; } [BackgroundDependencyLoader] @@ -46,7 +56,7 @@ namespace osu.Game.Graphics.Containers AddRangeInternal(new Drawable[] { - CreateHoverSounds(sampleSet), + samples = CreateHoverSounds(sampleSet), content, }); } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index ec8a1ac981..ef5532cbeb 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -234,7 +234,7 @@ namespace osu.Game.Graphics /// Retrieves colour for a . /// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours /// - public ColourInfo ForRankingTier(RankingTier tier) + public static ColourInfo ForRankingTier(RankingTier tier) { switch (tier) { diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 3a8dfac826..0cf2acadda 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -16,77 +16,19 @@ namespace osu.Game.Graphics { public static class OsuIcon { - #region Legacy spritesheet-based icons - - private static IconUsage get(int icon) => new IconUsage((char)icon, @"osuFont"); - - // ruleset icons in circles - public static IconUsage RulesetOsu => get(0xe000); - public static IconUsage RulesetMania => get(0xe001); - public static IconUsage RulesetCatch => get(0xe002); - public static IconUsage RulesetTaiko => get(0xe003); - - // ruleset icons without circles - public static IconUsage FilledCircle => get(0xe004); - public static IconUsage Logo => get(0xe006); - public static IconUsage ChevronDownCircle => get(0xe007); - public static IconUsage EditCircle => get(0xe033); - public static IconUsage LeftCircle => get(0xe034); - public static IconUsage RightCircle => get(0xe035); - public static IconUsage Charts => get(0xe036); - public static IconUsage Solo => get(0xe037); - public static IconUsage Multi => get(0xe038); - public static IconUsage Gear => get(0xe039); - - // misc icons - public static IconUsage Bat => get(0xe008); - public static IconUsage Bubble => get(0xe009); - public static IconUsage BubblePop => get(0xe02e); - public static IconUsage Dice => get(0xe011); - public static IconUsage HeartBreak => get(0xe030); - public static IconUsage Hot => get(0xe031); - public static IconUsage ListSearch => get(0xe032); - - //osu! playstyles - public static IconUsage PlayStyleTablet => get(0xe02a); - public static IconUsage PlayStyleMouse => get(0xe029); - public static IconUsage PlayStyleKeyboard => get(0xe02b); - public static IconUsage PlayStyleTouch => get(0xe02c); - - // osu! difficulties - public static IconUsage EasyOsu => get(0xe015); - public static IconUsage NormalOsu => get(0xe016); - public static IconUsage HardOsu => get(0xe017); - public static IconUsage InsaneOsu => get(0xe018); - public static IconUsage ExpertOsu => get(0xe019); - - // taiko difficulties - public static IconUsage EasyTaiko => get(0xe01a); - public static IconUsage NormalTaiko => get(0xe01b); - public static IconUsage HardTaiko => get(0xe01c); - public static IconUsage InsaneTaiko => get(0xe01d); - public static IconUsage ExpertTaiko => get(0xe01e); - - // fruits difficulties - public static IconUsage EasyFruits => get(0xe01f); - public static IconUsage NormalFruits => get(0xe020); - public static IconUsage HardFruits => get(0xe021); - public static IconUsage InsaneFruits => get(0xe022); - public static IconUsage ExpertFruits => get(0xe023); - - // mania difficulties - public static IconUsage EasyMania => get(0xe024); - public static IconUsage NormalMania => get(0xe025); - public static IconUsage HardMania => get(0xe026); - public static IconUsage InsaneMania => get(0xe027); - public static IconUsage ExpertMania => get(0xe028); - - #endregion - - #region New single-file-based icons - public const string FONT_NAME = @"Icons"; + // ruleset icons + public static IconUsage RulesetOsu => get(OsuIconMapping.RulesetOsu); + public static IconUsage RulesetMania => get(OsuIconMapping.RulesetMania); + public static IconUsage RulesetCatch => get(OsuIconMapping.RulesetCatch); + public static IconUsage RulesetTaiko => get(OsuIconMapping.RulesetTaiko); + + public static IconUsage Logo => get(OsuIconMapping.Logo); + public static IconUsage EditCircle => get(OsuIconMapping.EditCircle); + public static IconUsage LeftCircle => get(OsuIconMapping.LeftCircle); + public static IconUsage RightCircle => get(OsuIconMapping.RightCircle); + public static IconUsage Audio => get(OsuIconMapping.Audio); public static IconUsage Beatmap => get(OsuIconMapping.Beatmap); public static IconUsage Calendar => get(OsuIconMapping.Calendar); @@ -246,6 +188,30 @@ namespace osu.Game.Graphics private enum OsuIconMapping { + [Description(@"Logo")] + Logo, + + [Description(@"RulesetOsu")] + RulesetOsu, + + [Description(@"RulesetMania")] + RulesetMania, + + [Description(@"RulesetCatch")] + RulesetCatch, + + [Description(@"RulesetTaiko")] + RulesetTaiko, + + [Description(@"EditCircle")] + EditCircle, + + [Description(@"LeftCircle")] + LeftCircle, + + [Description(@"RightCircle")] + RightCircle, + [Description(@"audio")] Audio, @@ -737,7 +703,5 @@ namespace osu.Game.Graphics textures.Dispose(); } } - - #endregion } } diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index 884834ebe8..fea33bfa9d 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -42,18 +42,20 @@ namespace osu.Game.Graphics.UserInterface this.buttons = buttons ?? new[] { MouseButton.Left }; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") + ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + + sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled") + ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled"); + } + protected override bool OnClick(ClickEvent e) { if (buttons.Contains(e.Button)) - { - var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel(); - - if (channel != null) - { - channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); - channel.Play(); - } - } + PlayClickSample(); return base.OnClick(e); } @@ -66,14 +68,15 @@ namespace osu.Game.Graphics.UserInterface base.PlayHoverSample(); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + public void PlayClickSample() { - sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select") - ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel(); - sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled") - ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled"); + if (channel != null) + { + channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02); + channel.Play(); + } } } } diff --git a/osu.Game/Localisation/BreakInfoStrings.cs b/osu.Game/Localisation/BreakInfoStrings.cs new file mode 100644 index 0000000000..e327676e27 --- /dev/null +++ b/osu.Game/Localisation/BreakInfoStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public class BreakInfoStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.BreakInfo"; + + /// + /// "Current Progress" + /// + public static LocalisableString CurrentProgressTitle => new TranslatableString(getKey(@"current_progress_title"), @"Current Progress"); + + /// + /// "Grade" + /// + public static LocalisableString ShowInfoGrade => new TranslatableString(getKey(@"show_info_grade"), @"Grade"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 9009785f1c..c8630f9332 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -194,6 +194,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Details => new TranslatableString(getKey(@"details"), @"Details..."); + /// + /// "Mapper" + /// + public static LocalisableString Mapper => new TranslatableString(getKey(@"mapper"), @"Mapper"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 60874da561..d659f950dc 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -29,6 +29,61 @@ namespace osu.Game.Localisation /// public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0); + /// + /// "Playback speed" + /// + public static LocalisableString PlaybackSpeed => new TranslatableString(getKey(@"playback_speed"), @"Playback speed"); + + /// + /// "Show click markers" + /// + public static LocalisableString ShowClickMarkers => new TranslatableString(getKey(@"show_click_markers"), @"Show click markers"); + + /// + /// "Show frame markers" + /// + public static LocalisableString ShowFrameMarkers => new TranslatableString(getKey(@"show_frame_markers"), @"Show frame markers"); + + /// + /// "Show cursor path" + /// + public static LocalisableString ShowCursorPath => new TranslatableString(getKey(@"show_cursor_path"), @"Show cursor path"); + + /// + /// "Hide gameplay cursor" + /// + public static LocalisableString HideGameplayCursor => new TranslatableString(getKey(@"hide_gameplay_cursor"), @"Hide gameplay cursor"); + + /// + /// "Display length" + /// + public static LocalisableString DisplayLength => new TranslatableString(getKey(@"display_length"), @"Display length"); + + /// + /// "Playback" + /// + public static LocalisableString PlaybackTitle => new TranslatableString(getKey(@"playback_title"), @"Playback"); + + /// + /// "Visual Settings" + /// + public static LocalisableString VisualSettingsTitle => new TranslatableString(getKey(@"visual_settings_title"), @"Visual Settings"); + + /// + /// "Audio Settings" + /// + public static LocalisableString AudioSettingsTitle => new TranslatableString(getKey(@"audio_settings_title"), @"Audio Settings"); + + /// + /// "Input Settings" + /// + public static LocalisableString InputSettingsTitle => new TranslatableString(getKey(@"input_settings_title"), @"Input Settings"); + + /// + /// "Analysis Settings" + /// + public static LocalisableString AnalysisSettingsTitle => new TranslatableString(getKey(@"analysis_settings_title"), @"Analysis Settings"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 3bc80c8b37..f4f4165c7f 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -53,9 +52,9 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.Centre, Spacing = new Vector2(-3, 0), Padding = new MarginPadding { Top = 5 }, - Colour = GetRankNameColour(rank), + Colour = GetRankLetterColour(rank), Font = OsuFont.Numeric.With(size: 25), - Text = GetRankName(rank), + Text = GetRankLetter(rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -65,12 +64,29 @@ namespace osu.Game.Online.Leaderboards }; } - public static string GetRankName(ScoreRank rank) => rank.GetDescription().TrimEnd('+'); + /// + /// Returns letters to be shown in places where ranks are shown on a badge or similar to the user. + /// + public static string GetRankLetter(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.SH: + return @"S"; + + case ScoreRank.X: + case ScoreRank.XH: + return @"SS"; + + default: + return rank.ToString(); + } + } /// /// Retrieves the grade text colour. /// - public static ColourInfo GetRankNameColour(ScoreRank rank) + public static ColourInfo GetRankLetterColour(ScoreRank rank) { switch (rank) { diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs new file mode 100644 index 0000000000..cab007327c --- /dev/null +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarAction.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Matchmaking.Events +{ + /// + /// An action performed on a user's avatar in a matchmaking room. + /// + public enum MatchmakingAvatarAction + { + Jump + } +} diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs new file mode 100644 index 0000000000..187d234855 --- /dev/null +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionEvent.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.Matchmaking.Events +{ + /// + /// An action performed by a user in a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingAvatarActionEvent : MatchServerEvent + { + /// + /// The user performing the action. + /// + [Key(0)] + public int UserId { get; set; } + + /// + /// The action. + /// + [Key(1)] + public MatchmakingAvatarAction Action { get; set; } + } +} diff --git a/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs new file mode 100644 index 0000000000..abee95f0e2 --- /dev/null +++ b/osu.Game/Online/Matchmaking/Events/MatchmakingAvatarActionRequest.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using MessagePack; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Online.Matchmaking.Events +{ + /// + /// Requests to perform an action on a user's avatar in a matchmaking room. + /// + [Serializable] + [MessagePackObject] + public class MatchmakingAvatarActionRequest : MatchUserRequest + { + /// + /// The action. + /// + [Key(0)] + public MatchmakingAvatarAction Action { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MatchServerEvent.cs b/osu.Game/Online/Multiplayer/MatchServerEvent.cs index 376ff4d261..529a299438 100644 --- a/osu.Game/Online/Multiplayer/MatchServerEvent.cs +++ b/osu.Game/Online/Multiplayer/MatchServerEvent.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer @@ -15,6 +16,7 @@ namespace osu.Game.Online.Multiplayer // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(0, typeof(CountdownStartedEvent))] [Union(1, typeof(CountdownStoppedEvent))] + [Union(2, typeof(MatchmakingAvatarActionEvent))] public abstract class MatchServerEvent { } diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 8515256581..02704ea161 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; @@ -17,6 +18,7 @@ namespace osu.Game.Online.Multiplayer [Union(0, typeof(ChangeTeamRequest))] [Union(1, typeof(StartMatchCountdownRequest))] [Union(2, typeof(StopCountdownRequest))] + [Union(3, typeof(MatchmakingAvatarActionRequest))] public abstract class MatchUserRequest { } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 09dd3a00ae..a58d433e7d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -117,6 +117,8 @@ namespace osu.Game.Online.Multiplayer public event Action? CountdownStopped; + public event Action? MatchEvent; + public event Action? UserStateChanged; public event Action? MatchmakingQueueJoined; @@ -704,7 +706,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - public Task MatchEvent(MatchServerEvent e) + Task IMultiplayerClient.MatchEvent(MatchServerEvent e) { handleRoomRequest(() => { @@ -737,6 +739,7 @@ namespace osu.Game.Online.Multiplayer break; } + MatchEvent?.Invoke(e); RoomUpdated?.Invoke(); }); diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 4200fed0dd..dda069bba0 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -358,6 +358,7 @@ namespace osu.Game.Online.Rooms public Room(MultiplayerRoom room) { RoomID = room.RoomID; + ChannelId = room.ChannelID; Name = room.Settings.Name; Password = room.Settings.Password; Type = room.Settings.MatchType; diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 04d4b8d7af..e509891486 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Game.Online.Matchmaking; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; @@ -53,7 +54,9 @@ namespace osu.Game.Online (typeof(MatchmakingQueueStatus.MatchFound), typeof(MatchmakingQueueStatus)), (typeof(MatchmakingQueueStatus.JoiningMatch), typeof(MatchmakingQueueStatus)), (typeof(MatchmakingRoomState), typeof(MatchRoomState)), - (typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)) + (typeof(MatchmakingStageCountdown), typeof(MultiplayerCountdown)), + (typeof(MatchmakingAvatarActionRequest), typeof(MatchUserRequest)), + (typeof(MatchmakingAvatarActionEvent), typeof(MatchServerEvent)), }; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d610bd64d5..4dd42b7fd2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -65,7 +65,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Screens.OnlinePlay.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; @@ -80,6 +80,7 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; using Sentry; +using IntroScreen = osu.Game.Screens.Menu.IntroScreen; using MatchType = osu.Game.Online.Rooms.MatchType; namespace osu.Game @@ -1271,7 +1272,7 @@ namespace osu.Game loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(detachedBeatmapStore = new RealmDetachedBeatmapStore(), Add, true); - loadComponentSingleFile(new MatchmakingController(), Add, true); + loadComponentSingleFile(new QueueController(), Add, true); Add(externalLinkOpener = new ExternalLinkOpener()); Add(new MusicKeyBindingHandler()); diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase.Importing.cs similarity index 100% rename from osu.Game/OsuGameBase_Importing.cs rename to osu.Game/OsuGameBase.Importing.cs diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 367939b054..6b0e0116b0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -482,8 +482,6 @@ namespace osu.Game protected virtual void InitialiseFonts() { - AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Torus/Torus-Regular"); AddFont(Resources, @"Fonts/Torus/Torus-Light"); AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index ac8b4ad0a8..6cdbc6450d 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -58,11 +58,13 @@ namespace osu.Game.Overlays }; audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioVolume); + audio.Samples.AddAdjustment(AdjustableProperty.Volume, audioVolume); } protected override void Dispose(bool isDisposing) { audio?.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); + audio?.Samples.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index d1be7cecce..3e48366ae2 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -157,7 +157,7 @@ namespace osu.Game.Overlays.Profile.Header.Components } dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); - dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); + dailyPlayCount.Colour = OsuColour.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); bool playedToday = stats.LastUpdate?.Date == DateTimeOffset.UtcNow.Date; bool userIsOnOwnProfile = stats.UserID == api.LocalUser.Value.Id; diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 826b40d70c..fa4937bd1f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -36,9 +36,6 @@ namespace osu.Game.Overlays.Profile.Header.Components private Box topBackground = null!; private Box background = null!; - [Resolved] - private OsuColour colours { get; set; } = null!; - [BackgroundDependencyLoader] private void load() { @@ -117,19 +114,19 @@ namespace osu.Game.Overlays.Profile.Header.Components topBackground.Colour = colourProvider.Background5; totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); - totalParticipation.ValueColour = colours.ForRankingTier(TierForPlayCount(statistics.PlayCount)); + totalParticipation.ValueColour = OsuColour.ForRankingTier(TierForPlayCount(statistics.PlayCount)); currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); - currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); + currentDaily.ValueColour = OsuColour.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); currentWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0")); - currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent)); + currentWeekly.ValueColour = OsuColour.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent)); bestDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0")); - bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest)); + bestDaily.ValueColour = OsuColour.ForRankingTier(TierForDaily(statistics.DailyStreakBest)); bestWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0")); - bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest)); + bestWeekly.ValueColour = OsuColour.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest)); topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0"); topTen.ValueColour = colourProvider.Content2; diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs index 5f100bc882..7e4c747ce8 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs @@ -31,6 +31,8 @@ namespace osu.Game.Overlays.Profile.Header.Components { Child = new Sprite { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, FillMode = FillMode.Fit, RelativeSizeAxes = Axes.Both, Texture = textures.Get(badge.ImageUrl), diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs index 9b4df7672d..543e353f18 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelBadge.cs @@ -27,9 +27,6 @@ namespace osu.Game.Overlays.Profile.Header.Components private OsuSpriteText levelText = null!; private Sprite sprite = null!; - [Resolved] - private OsuColour osuColour { get; set; } = null!; - public LevelBadge() { TooltipText = UsersStrings.ShowStatsLevel("0"); @@ -91,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Header.Components tier = RankingTier.Lustrous; } - return osuColour.ForRankingTier(tier); + return OsuColour.ForRankingTier(tier); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs similarity index 100% rename from osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_ConflictResolution.cs rename to osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.ConflictResolution.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs similarity index 100% rename from osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow_KeyButton.cs rename to osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.KeyButton.cs diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index d24c0a778c..bac8013a33 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -11,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Overlays.Settings @@ -94,7 +96,7 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), - Text = @"back", + Text = CommonStrings.Back.ToLower(), }, } } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index b1a0ca0ccd..d82118fa1a 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -3,12 +3,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -21,7 +23,7 @@ namespace osu.Game.Overlays { public partial class SettingsToolboxGroup : Container, IExpandable { - private readonly string title; + private readonly LocalisableString title; public const int CONTAINER_WIDTH = 270; private const float transition_duration = 250; @@ -60,7 +62,7 @@ namespace osu.Game.Overlays /// Create a new instance. /// /// The title to be displayed in the header of this group. - public SettingsToolboxGroup(string title) + public SettingsToolboxGroup(LocalisableString title) { this.title = title; @@ -102,7 +104,7 @@ namespace osu.Game.Overlays { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Text = title.ToUpperInvariant(), + Text = title.ToUpper(), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17), Padding = new MarginPadding { Left = 10, Right = 30 }, }, diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index c8799ad5ba..2438abe5d9 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -82,7 +82,7 @@ namespace osu.Game.Overlays.SkinEditor foreach (var drawableItem in objectsInRotation) { - var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation); + var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], ToScreenSpace(actualOrigin), rotation); UpdatePosition(drawableItem, rotatedPosition); drawableItem.Rotation = originalRotations[drawableItem] + rotation; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 221282ef13..5b75b8419c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -77,6 +78,8 @@ namespace osu.Game.Overlays.Toolbar protected readonly Container BackgroundContent; + private IDisposable? realmSubscription; + [Resolved] private RealmAccess realm { get; set; } = null!; @@ -184,7 +187,8 @@ namespace osu.Game.Overlays.Toolbar { if (Hotkey != null) { - realm.SubscribeToPropertyChanged(r => r.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), kb => kb.KeyCombinationString, updateKeyBindingTooltip); + realmSubscription = realm.SubscribeToPropertyChanged(r => r.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), + kb => kb.KeyCombinationString, updateKeyBindingTooltip); } } @@ -234,6 +238,13 @@ namespace osu.Game.Overlays.Toolbar ? $" ({keyBindingString})" : string.Empty; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + realmSubscription?.Dispose(); + } } public partial class OpaqueBackground : Container diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index 59511973f7..5e431dc357 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -26,6 +26,13 @@ namespace osu.Game.Rulesets.Difficulty protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25; protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29; protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31; + protected const int ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR = 33; + protected const int ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR = 35; + protected const int ATTRIB_ID_NESTED_SCORE_PER_OBJECT = 37; + protected const int ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER = 39; + protected const int ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE = 41; + protected const int ATTRIB_ID_RHYTHM_DIFFICULTY = 43; + protected const int ATTRIB_ID_CONSISTENCY_FACTOR = 45; /// /// The mods which were applied to the beatmap. diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f4d8e8518e..7acfbe651f 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using JetBrains.Annotations; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Lists; using osu.Game.Beatmaps; @@ -29,11 +28,15 @@ namespace osu.Game.Rulesets.Difficulty /// protected IBeatmap Beatmap { get; private set; } + /// + /// The working beatmap for which difficulty will be calculated. + /// + protected readonly IWorkingBeatmap WorkingBeatmap; + private Mod[] playableMods; private double clockRate; private readonly IRulesetInfo ruleset; - private readonly IWorkingBeatmap beatmap; /// /// A yymmdd version which is used to discern when reprocessing is required. @@ -43,7 +46,7 @@ namespace osu.Game.Rulesets.Difficulty protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) { this.ruleset = ruleset; - this.beatmap = beatmap; + WorkingBeatmap = beatmap; } /// @@ -181,11 +184,9 @@ namespace osu.Game.Rulesets.Difficulty private void preProcess([NotNull] IEnumerable mods, CancellationToken cancellationToken) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); + Beatmap = WorkingBeatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken); - var track = new TrackVirtual(10000); - playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); - clockRate = track.Rate; + clockRate = ModUtils.CalculateRateWithMods(playableMods); } /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 3ba67793dc..b6272bf56b 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -116,6 +116,8 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); + public IEnumerable GetObjectStrains() => ObjectStrains; + /// /// Returns the calculated difficulty value representing all s that have been processed up to this point. /// diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs index 78df8a139b..c813627d51 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -66,6 +66,20 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// The output of the bell curve function of public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2))); + /// + /// Calculates a Smoothstep Bellcurve that returns returns 1 for x = mean, and smoothly reducing it's value to 0 over width + /// + /// Value to calculate the function for + /// Value of x, for which return value will be the highest (=1) + /// Range [mean - width, mean + width] where function will change values + /// The output of the smoothstep bell curve function of + public static double SmoothstepBellCurve(double x, double mean = 0.5, double width = 0.5) + { + x -= mean; + x = x > 0 ? (width - x) : (width + x); + return Smoothstep(x, 0, width); + } + /// /// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep) /// @@ -102,5 +116,79 @@ namespace osu.Game.Rulesets.Difficulty.Utils { return Math.Clamp((x - start) / (end - start), 0.0, 1.0); } + + /// + /// Error function (https://en.wikipedia.org/wiki/Error_function) + /// + /// Value to calculate the function for + public static double Erf(double x) + { + if (x == 0) + return 0; + + if (double.IsPositiveInfinity(x)) + return 1; + + if (double.IsNegativeInfinity(x)) + return -1; + + if (double.IsNaN(x)) + return double.NaN; + + // Constants for approximation (Abramowitz and Stegun formula 7.1.26) + double t = 1.0 / (1.0 + 0.3275911 * Math.Abs(x)); + double tau = t * (0.254829592 + + t * (-0.284496736 + + t * (1.421413741 + + t * (-1.453152027 + + t * 1.061405429)))); + + double erf = 1.0 - tau * Math.Exp(-x * x); + + return x >= 0 ? erf : -erf; + } + + /// + /// Complementary error function (https://en.wikipedia.org/wiki/Error_function) + /// + /// Value to calculate the function for + public static double Erfc(double x) => 1 - Erf(x); + + /// + /// Inverse error function (https://en.wikipedia.org/wiki/Error_function) + /// + /// Value to calculate the function for + public static double ErfInv(double x) + { + if (x <= -1) + return double.NegativeInfinity; + + if (x >= 1) + return double.PositiveInfinity; + + if (x == 0) + return 0; + + const double a = 0.147; + double sgn = Math.Sign(x); + x = Math.Abs(x); + + double ln = Math.Log(1 - x * x); + double t1 = 2 / (Math.PI * a) + ln / 2; + double t2 = ln / a; + double baseApprox = Math.Sqrt(t1 * t1 - t2) - t1; + + // Correction reduces max error from -0.005 to -0.00045. + double c = x >= 0.85 ? Math.Pow((x - 0.85) / 0.293, 8) : 0; + double erfInv = sgn * (Math.Sqrt(baseApprox) + c); + + return erfInv; + } + + /// + /// Inverse complementary error function (https://en.wikipedia.org/wiki/Error_function) + /// + /// Value to calculate the function for + public static double ErfcInv(double x) => ErfInv(1 - x); } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs deleted file mode 100644 index 4b89cbe7cc..0000000000 --- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs +++ /dev/null @@ -1,688 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -// All code is referenced from the following: -// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs - -/* - Copyright (c) 2002-2022 Math.NET -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -using System; - -namespace osu.Game.Rulesets.Difficulty.Utils -{ - public partial class DifficultyCalculationUtils - { - /// - /// ************************************** - /// COEFFICIENTS FOR METHOD ErfImp * - /// ************************************** - /// - /// Polynomial coefficients for a numerator of ErfImp - /// calculation for Erf(x) in the interval [1e-10, 0.5]. - /// - private static readonly double[] erf_imp_an = { 0.00337916709551257388990745, -0.00073695653048167948530905, -0.374732337392919607868241, 0.0817442448733587196071743, -0.0421089319936548595203468, 0.0070165709512095756344528, -0.00495091255982435110337458, 0.000871646599037922480317225 }; - - /// Polynomial coefficients for a denominator of ErfImp - /// calculation for Erf(x) in the interval [1e-10, 0.5]. - /// - private static readonly double[] erf_imp_ad = { 1, -0.218088218087924645390535, 0.412542972725442099083918, -0.0841891147873106755410271, 0.0655338856400241519690695, -0.0120019604454941768171266, 0.00408165558926174048329689, -0.000615900721557769691924509 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [0.5, 0.75]. - /// - private static readonly double[] erf_imp_bn = { -0.0361790390718262471360258, 0.292251883444882683221149, 0.281447041797604512774415, 0.125610208862766947294894, 0.0274135028268930549240776, 0.00250839672168065762786937 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [0.5, 0.75]. - /// - private static readonly double[] erf_imp_bd = { 1, 1.8545005897903486499845, 1.43575803037831418074962, 0.582827658753036572454135, 0.124810476932949746447682, 0.0113724176546353285778481 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [0.75, 1.25]. - /// - private static readonly double[] erf_imp_cn = { -0.0397876892611136856954425, 0.153165212467878293257683, 0.191260295600936245503129, 0.10276327061989304213645, 0.029637090615738836726027, 0.0046093486780275489468812, 0.000307607820348680180548455 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [0.75, 1.25]. - /// - private static readonly double[] erf_imp_cd = { 1, 1.95520072987627704987886, 1.64762317199384860109595, 0.768238607022126250082483, 0.209793185936509782784315, 0.0319569316899913392596356, 0.00213363160895785378615014 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [1.25, 2.25]. - /// - private static readonly double[] erf_imp_dn = { -0.0300838560557949717328341, 0.0538578829844454508530552, 0.0726211541651914182692959, 0.0367628469888049348429018, 0.00964629015572527529605267, 0.00133453480075291076745275, 0.778087599782504251917881e-4 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [1.25, 2.25]. - /// - private static readonly double[] erf_imp_dd = { 1, 1.75967098147167528287343, 1.32883571437961120556307, 0.552528596508757581287907, 0.133793056941332861912279, 0.0179509645176280768640766, 0.00104712440019937356634038, -0.106640381820357337177643e-7 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [2.25, 3.5]. - /// - private static readonly double[] erf_imp_en = { -0.0117907570137227847827732, 0.014262132090538809896674, 0.0202234435902960820020765, 0.00930668299990432009042239, 0.00213357802422065994322516, 0.00025022987386460102395382, 0.120534912219588189822126e-4 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [2.25, 3.5]. - /// - private static readonly double[] erf_imp_ed = { 1, 1.50376225203620482047419, 0.965397786204462896346934, 0.339265230476796681555511, 0.0689740649541569716897427, 0.00771060262491768307365526, 0.000371421101531069302990367 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [3.5, 5.25]. - /// - private static readonly double[] erf_imp_fn = { -0.00546954795538729307482955, 0.00404190278731707110245394, 0.0054963369553161170521356, 0.00212616472603945399437862, 0.000394984014495083900689956, 0.365565477064442377259271e-4, 0.135485897109932323253786e-5 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [3.5, 5.25]. - /// - private static readonly double[] erf_imp_fd = { 1, 1.21019697773630784832251, 0.620914668221143886601045, 0.173038430661142762569515, 0.0276550813773432047594539, 0.00240625974424309709745382, 0.891811817251336577241006e-4, -0.465528836283382684461025e-11 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [5.25, 8]. - /// - private static readonly double[] erf_imp_gn = { -0.00270722535905778347999196, 0.0013187563425029400461378, 0.00119925933261002333923989, 0.00027849619811344664248235, 0.267822988218331849989363e-4, 0.923043672315028197865066e-6 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [5.25, 8]. - /// - private static readonly double[] erf_imp_gd = { 1, 0.814632808543141591118279, 0.268901665856299542168425, 0.0449877216103041118694989, 0.00381759663320248459168994, 0.000131571897888596914350697, 0.404815359675764138445257e-11 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [8, 11.5]. - /// - private static readonly double[] erf_imp_hn = { -0.00109946720691742196814323, 0.000406425442750422675169153, 0.000274499489416900707787024, 0.465293770646659383436343e-4, 0.320955425395767463401993e-5, 0.778286018145020892261936e-7 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [8, 11.5]. - /// - private static readonly double[] erf_imp_hd = { 1, 0.588173710611846046373373, 0.139363331289409746077541, 0.0166329340417083678763028, 0.00100023921310234908642639, 0.24254837521587225125068e-4 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [11.5, 17]. - /// - private static readonly double[] erf_imp_in = { -0.00056907993601094962855594, 0.000169498540373762264416984, 0.518472354581100890120501e-4, 0.382819312231928859704678e-5, 0.824989931281894431781794e-7 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [11.5, 17]. - /// - private static readonly double[] erf_imp_id = { 1, 0.339637250051139347430323, 0.043472647870310663055044, 0.00248549335224637114641629, 0.535633305337152900549536e-4, -0.117490944405459578783846e-12 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [17, 24]. - /// - private static readonly double[] erf_imp_jn = { -0.000241313599483991337479091, 0.574224975202501512365975e-4, 0.115998962927383778460557e-4, 0.581762134402593739370875e-6, 0.853971555085673614607418e-8 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [17, 24]. - /// - private static readonly double[] erf_imp_jd = { 1, 0.233044138299687841018015, 0.0204186940546440312625597, 0.000797185647564398289151125, 0.117019281670172327758019e-4 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [24, 38]. - /// - private static readonly double[] erf_imp_kn = { -0.000146674699277760365803642, 0.162666552112280519955647e-4, 0.269116248509165239294897e-5, 0.979584479468091935086972e-7, 0.101994647625723465722285e-8 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [24, 38]. - /// - private static readonly double[] erf_imp_kd = { 1, 0.165907812944847226546036, 0.0103361716191505884359634, 0.000286593026373868366935721, 0.298401570840900340874568e-5 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [38, 60]. - /// - private static readonly double[] erf_imp_ln = { -0.583905797629771786720406e-4, 0.412510325105496173512992e-5, 0.431790922420250949096906e-6, 0.993365155590013193345569e-8, 0.653480510020104699270084e-10 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [38, 60]. - /// - private static readonly double[] erf_imp_ld = { 1, 0.105077086072039915406159, 0.00414278428675475620830226, 0.726338754644523769144108e-4, 0.477818471047398785369849e-6 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [60, 85]. - /// - private static readonly double[] erf_imp_mn = { -0.196457797609229579459841e-4, 0.157243887666800692441195e-5, 0.543902511192700878690335e-7, 0.317472492369117710852685e-9 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [60, 85]. - /// - private static readonly double[] erf_imp_md = { 1, 0.052803989240957632204885, 0.000926876069151753290378112, 0.541011723226630257077328e-5, 0.535093845803642394908747e-15 }; - - /// Polynomial coefficients for a numerator in ErfImp - /// calculation for Erfc(x) in the interval [85, 110]. - /// - private static readonly double[] erf_imp_nn = { -0.789224703978722689089794e-5, 0.622088451660986955124162e-6, 0.145728445676882396797184e-7, 0.603715505542715364529243e-10 }; - - /// Polynomial coefficients for a denominator in ErfImp - /// calculation for Erfc(x) in the interval [85, 110]. - /// - private static readonly double[] erf_imp_nd = { 1, 0.0375328846356293715248719, 0.000467919535974625308126054, 0.193847039275845656900547e-5 }; - - /// - /// ************************************** - /// COEFFICIENTS FOR METHOD ErfInvImp * - /// ************************************** - /// - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0, 0.5]. - /// - private static readonly double[] erv_inv_imp_an = { -0.000508781949658280665617, -0.00836874819741736770379, 0.0334806625409744615033, -0.0126926147662974029034, -0.0365637971411762664006, 0.0219878681111168899165, 0.00822687874676915743155, -0.00538772965071242932965 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0, 0.5]. - /// - private static readonly double[] erv_inv_imp_ad = { 1, -0.970005043303290640362, -1.56574558234175846809, 1.56221558398423026363, 0.662328840472002992063, -0.71228902341542847553, -0.0527396382340099713954, 0.0795283687341571680018, -0.00233393759374190016776, 0.000886216390456424707504 }; - - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.5, 0.75]. - /// - private static readonly double[] erv_inv_imp_bn = { -0.202433508355938759655, 0.105264680699391713268, 8.37050328343119927838, 17.6447298408374015486, -18.8510648058714251895, -44.6382324441786960818, 17.445385985570866523, 21.1294655448340526258, -3.67192254707729348546 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.5, 0.75]. - /// - private static readonly double[] erv_inv_imp_bd = { 1, 6.24264124854247537712, 3.9713437953343869095, -28.6608180499800029974, -20.1432634680485188801, 48.5609213108739935468, 10.8268667355460159008, -22.6436933413139721736, 1.72114765761200282724 }; - - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x less than 3. - /// - private static readonly double[] erv_inv_imp_cn = { -0.131102781679951906451, -0.163794047193317060787, 0.117030156341995252019, 0.387079738972604337464, 0.337785538912035898924, 0.142869534408157156766, 0.0290157910005329060432, 0.00214558995388805277169, -0.679465575181126350155e-6, 0.285225331782217055858e-7, -0.681149956853776992068e-9 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x less than 3. - /// - private static readonly double[] erv_inv_imp_cd = { 1, 3.46625407242567245975, 5.38168345707006855425, 4.77846592945843778382, 2.59301921623620271374, 0.848854343457902036425, 0.152264338295331783612, 0.01105924229346489121 }; - - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 3 and 6. - /// - private static readonly double[] erv_inv_imp_dn = { -0.0350353787183177984712, -0.00222426529213447927281, 0.0185573306514231072324, 0.00950804701325919603619, 0.00187123492819559223345, 0.000157544617424960554631, 0.460469890584317994083e-5, -0.230404776911882601748e-9, 0.266339227425782031962e-11 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 3 and 6. - /// - private static readonly double[] erv_inv_imp_dd = { 1, 1.3653349817554063097, 0.762059164553623404043, 0.220091105764131249824, 0.0341589143670947727934, 0.00263861676657015992959, 0.764675292302794483503e-4 }; - - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 6 and 18. - /// - private static readonly double[] erv_inv_imp_en = { -0.0167431005076633737133, -0.00112951438745580278863, 0.00105628862152492910091, 0.000209386317487588078668, 0.149624783758342370182e-4, 0.449696789927706453732e-6, 0.462596163522878599135e-8, -0.281128735628831791805e-13, 0.99055709973310326855e-16 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 6 and 18. - /// - private static readonly double[] erv_inv_imp_ed = { 1, 0.591429344886417493481, 0.138151865749083321638, 0.0160746087093676504695, 0.000964011807005165528527, 0.275335474764726041141e-4, 0.282243172016108031869e-6 }; - - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 18 and 44. - /// - private static readonly double[] erv_inv_imp_fn = { -0.0024978212791898131227, -0.779190719229053954292e-5, 0.254723037413027451751e-4, 0.162397777342510920873e-5, 0.396341011304801168516e-7, 0.411632831190944208473e-9, 0.145596286718675035587e-11, -0.116765012397184275695e-17 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x between 18 and 44. - /// - private static readonly double[] erv_inv_imp_fd = { 1, 0.207123112214422517181, 0.0169410838120975906478, 0.000690538265622684595676, 0.145007359818232637924e-4, 0.144437756628144157666e-6, 0.509761276599778486139e-9 }; - - /// Polynomial coefficients for a numerator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x greater than 44. - /// - private static readonly double[] erv_inv_imp_gn = { -0.000539042911019078575891, -0.28398759004727721098e-6, 0.899465114892291446442e-6, 0.229345859265920864296e-7, 0.225561444863500149219e-9, 0.947846627503022684216e-12, 0.135880130108924861008e-14, -0.348890393399948882918e-21 }; - - /// Polynomial coefficients for a denominator of ErfInvImp - /// calculation for Erf^-1(z) in the interval [0.75, 1] with x greater than 44. - /// - private static readonly double[] erv_inv_imp_gd = { 1, 0.0845746234001899436914, 0.00282092984726264681981, 0.468292921940894236786e-4, 0.399968812193862100054e-6, 0.161809290887904476097e-8, 0.231558608310259605225e-11 }; - - /// Calculates the error function. - /// The value to evaluate. - /// the error function evaluated at given value. - /// - /// - /// returns 1 if x == double.PositiveInfinity. - /// returns -1 if x == double.NegativeInfinity. - /// - /// - public static double Erf(double x) - { - if (x == 0) - { - return 0; - } - - if (double.IsPositiveInfinity(x)) - { - return 1; - } - - if (double.IsNegativeInfinity(x)) - { - return -1; - } - - if (double.IsNaN(x)) - { - return double.NaN; - } - - return erfImp(x, false); - } - - /// Calculates the complementary error function. - /// The value to evaluate. - /// the complementary error function evaluated at given value. - /// - /// - /// returns 0 if x == double.PositiveInfinity. - /// returns 2 if x == double.NegativeInfinity. - /// - /// - public static double Erfc(double x) - { - if (x == 0) - { - return 1; - } - - if (double.IsPositiveInfinity(x)) - { - return 0; - } - - if (double.IsNegativeInfinity(x)) - { - return 2; - } - - if (double.IsNaN(x)) - { - return double.NaN; - } - - return erfImp(x, true); - } - - /// Calculates the inverse error function evaluated at z. - /// The inverse error function evaluated at given value. - /// - /// - /// returns double.PositiveInfinity if z >= 1.0. - /// returns double.NegativeInfinity if z <= -1.0. - /// - /// - /// Calculates the inverse error function evaluated at z. - /// value to evaluate. - /// the inverse error function evaluated at Z. - public static double ErfInv(double z) - { - if (z == 0.0) - { - return 0.0; - } - - if (z >= 1.0) - { - return double.PositiveInfinity; - } - - if (z <= -1.0) - { - return double.NegativeInfinity; - } - - double p, q, s; - - if (z < 0) - { - p = -z; - q = 1 - p; - s = -1; - } - else - { - p = z; - q = 1 - z; - s = 1; - } - - return erfInvImpl(p, q, s); - } - - /// - /// Implementation of the error function. - /// - /// Where to evaluate the error function. - /// Whether to compute 1 - the error function. - /// the error function. - private static double erfImp(double z, bool invert) - { - if (z < 0) - { - if (!invert) - { - return -erfImp(-z, false); - } - - if (z < -0.5) - { - return 2 - erfImp(-z, true); - } - - return 1 + erfImp(-z, false); - } - - double result; - - // Big bunch of selection statements now to pick which - // implementation to use, try to put most likely options - // first: - if (z < 0.5) - { - // We're going to calculate erf: - if (z < 1e-10) - { - result = (z * 1.125) + (z * 0.003379167095512573896158903121545171688); - } - else - { - // Worst case absolute error found: 6.688618532e-21 - result = (z * 1.125) + (z * evaluatePolynomial(z, erf_imp_an) / evaluatePolynomial(z, erf_imp_ad)); - } - } - else if (z < 110) - { - // We'll be calculating erfc: - invert = !invert; - double r, b; - - if (z < 0.75) - { - // Worst case absolute error found: 5.582813374e-21 - r = evaluatePolynomial(z - 0.5, erf_imp_bn) / evaluatePolynomial(z - 0.5, erf_imp_bd); - b = 0.3440242112F; - } - else if (z < 1.25) - { - // Worst case absolute error found: 4.01854729e-21 - r = evaluatePolynomial(z - 0.75, erf_imp_cn) / evaluatePolynomial(z - 0.75, erf_imp_cd); - b = 0.419990927F; - } - else if (z < 2.25) - { - // Worst case absolute error found: 2.866005373e-21 - r = evaluatePolynomial(z - 1.25, erf_imp_dn) / evaluatePolynomial(z - 1.25, erf_imp_dd); - b = 0.4898625016F; - } - else if (z < 3.5) - { - // Worst case absolute error found: 1.045355789e-21 - r = evaluatePolynomial(z - 2.25, erf_imp_en) / evaluatePolynomial(z - 2.25, erf_imp_ed); - b = 0.5317370892F; - } - else if (z < 5.25) - { - // Worst case absolute error found: 8.300028706e-22 - r = evaluatePolynomial(z - 3.5, erf_imp_fn) / evaluatePolynomial(z - 3.5, erf_imp_fd); - b = 0.5489973426F; - } - else if (z < 8) - { - // Worst case absolute error found: 1.700157534e-21 - r = evaluatePolynomial(z - 5.25, erf_imp_gn) / evaluatePolynomial(z - 5.25, erf_imp_gd); - b = 0.5571740866F; - } - else if (z < 11.5) - { - // Worst case absolute error found: 3.002278011e-22 - r = evaluatePolynomial(z - 8, erf_imp_hn) / evaluatePolynomial(z - 8, erf_imp_hd); - b = 0.5609807968F; - } - else if (z < 17) - { - // Worst case absolute error found: 6.741114695e-21 - r = evaluatePolynomial(z - 11.5, erf_imp_in) / evaluatePolynomial(z - 11.5, erf_imp_id); - b = 0.5626493692F; - } - else if (z < 24) - { - // Worst case absolute error found: 7.802346984e-22 - r = evaluatePolynomial(z - 17, erf_imp_jn) / evaluatePolynomial(z - 17, erf_imp_jd); - b = 0.5634598136F; - } - else if (z < 38) - { - // Worst case absolute error found: 2.414228989e-22 - r = evaluatePolynomial(z - 24, erf_imp_kn) / evaluatePolynomial(z - 24, erf_imp_kd); - b = 0.5638477802F; - } - else if (z < 60) - { - // Worst case absolute error found: 5.896543869e-24 - r = evaluatePolynomial(z - 38, erf_imp_ln) / evaluatePolynomial(z - 38, erf_imp_ld); - b = 0.5640528202F; - } - else if (z < 85) - { - // Worst case absolute error found: 3.080612264e-21 - r = evaluatePolynomial(z - 60, erf_imp_mn) / evaluatePolynomial(z - 60, erf_imp_md); - b = 0.5641309023F; - } - else - { - // Worst case absolute error found: 8.094633491e-22 - r = evaluatePolynomial(z - 85, erf_imp_nn) / evaluatePolynomial(z - 85, erf_imp_nd); - b = 0.5641584396F; - } - - double g = Math.Exp(-z * z) / z; - result = (g * b) + (g * r); - } - else - { - // Any value of z larger than 28 will underflow to zero: - result = 0; - invert = !invert; - } - - if (invert) - { - result = 1 - result; - } - - return result; - } - - /// Calculates the complementary inverse error function evaluated at z. - /// The complementary inverse error function evaluated at given value. - /// We have tested this implementation against the arbitrary precision mpmath library - /// and found cases where we can only guarantee 9 significant figures correct. - /// - /// returns double.PositiveInfinity if z <= 0.0. - /// returns double.NegativeInfinity if z >= 2.0. - /// - /// - /// calculates the complementary inverse error function evaluated at z. - /// value to evaluate. - /// the complementary inverse error function evaluated at Z. - public static double ErfcInv(double z) - { - if (z <= 0.0) - { - return double.PositiveInfinity; - } - - if (z >= 2.0) - { - return double.NegativeInfinity; - } - - double p, q, s; - - if (z > 1) - { - q = 2 - z; - p = 1 - q; - s = -1; - } - else - { - p = 1 - z; - q = z; - s = 1; - } - - return erfInvImpl(p, q, s); - } - - /// - /// The implementation of the inverse error function. - /// - /// First intermediate parameter. - /// Second intermediate parameter. - /// Third intermediate parameter. - /// the inverse error function. - private static double erfInvImpl(double p, double q, double s) - { - double result; - - if (p <= 0.5) - { - // Evaluate inverse erf using the rational approximation: - // - // x = p(p+10)(Y+R(p)) - // - // Where Y is a constant, and R(p) is optimized for a low - // absolute error compared to |Y|. - // - // double: Max error found: 2.001849e-18 - // long double: Max error found: 1.017064e-20 - // Maximum Deviation Found (actual error term at infinite precision) 8.030e-21 - const float y = 0.0891314744949340820313f; - double g = p * (p + 10); - double r = evaluatePolynomial(p, erv_inv_imp_an) / evaluatePolynomial(p, erv_inv_imp_ad); - result = (g * y) + (g * r); - } - else if (q >= 0.25) - { - // Rational approximation for 0.5 > q >= 0.25 - // - // x = sqrt(-2*log(q)) / (Y + R(q)) - // - // Where Y is a constant, and R(q) is optimized for a low - // absolute error compared to Y. - // - // double : Max error found: 7.403372e-17 - // long double : Max error found: 6.084616e-20 - // Maximum Deviation Found (error term) 4.811e-20 - const float y = 2.249481201171875f; - double g = Math.Sqrt(-2 * Math.Log(q)); - double xs = q - 0.25; - double r = evaluatePolynomial(xs, erv_inv_imp_bn) / evaluatePolynomial(xs, erv_inv_imp_bd); - result = g / (y + r); - } - else - { - // For q < 0.25 we have a series of rational approximations all - // of the general form: - // - // let: x = sqrt(-log(q)) - // - // Then the result is given by: - // - // x(Y+R(x-B)) - // - // where Y is a constant, B is the lowest value of x for which - // the approximation is valid, and R(x-B) is optimized for a low - // absolute error compared to Y. - // - // Note that almost all code will really go through the first - // or maybe second approximation. After than we're dealing with very - // small input values indeed: 80 and 128 bit long double's go all the - // way down to ~ 1e-5000 so the "tail" is rather long... - double x = Math.Sqrt(-Math.Log(q)); - - if (x < 3) - { - // Max error found: 1.089051e-20 - const float y = 0.807220458984375f; - double xs = x - 1.125; - double r = evaluatePolynomial(xs, erv_inv_imp_cn) / evaluatePolynomial(xs, erv_inv_imp_cd); - result = (y * x) + (r * x); - } - else if (x < 6) - { - // Max error found: 8.389174e-21 - const float y = 0.93995571136474609375f; - double xs = x - 3; - double r = evaluatePolynomial(xs, erv_inv_imp_dn) / evaluatePolynomial(xs, erv_inv_imp_dd); - result = (y * x) + (r * x); - } - else if (x < 18) - { - // Max error found: 1.481312e-19 - const float y = 0.98362827301025390625f; - double xs = x - 6; - double r = evaluatePolynomial(xs, erv_inv_imp_en) / evaluatePolynomial(xs, erv_inv_imp_ed); - result = (y * x) + (r * x); - } - else if (x < 44) - { - // Max error found: 5.697761e-20 - const float y = 0.99714565277099609375f; - double xs = x - 18; - double r = evaluatePolynomial(xs, erv_inv_imp_fn) / evaluatePolynomial(xs, erv_inv_imp_fd); - result = (y * x) + (r * x); - } - else - { - // Max error found: 1.279746e-20 - const float y = 0.99941349029541015625f; - double xs = x - 44; - double r = evaluatePolynomial(xs, erv_inv_imp_gn) / evaluatePolynomial(xs, erv_inv_imp_gd); - result = (y * x) + (r * x); - } - } - - return s * result; - } - - /// - /// Evaluate a polynomial at point x. - /// Coefficients are ordered ascending by power with power k at index k. - /// Example: coefficients [3,-1,2] represent y=2x^2-x+3. - /// - /// The location where to evaluate the polynomial at. - /// The coefficients of the polynomial, coefficient for power k at index k. - /// - /// is a null reference. - /// - private static double evaluatePolynomial(double z, params double[] coefficients) - { - // 2020-10-07 jbialogrodzki #730 Since this is public API we should probably - // handle null arguments? It doesn't seem to have been done consistently in this class though. - ArgumentNullException.ThrowIfNull(coefficients); - - // 2020-10-07 jbialogrodzki #730 Zero polynomials need explicit handling. - // Without this check, we attempted to peek coefficients at negative indices! - int n = coefficients.Length; - - if (n == 0) - { - return 0; - } - - double sum = coefficients[n - 1]; - - for (int i = n - 2; i >= 0; --i) - { - sum *= z; - sum += coefficients[i]; - } - - return sum; - } - } -} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b38b0291e8..c138808890 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -262,14 +262,6 @@ namespace osu.Game.Rulesets.Edit .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t))) .ToList(); - foreach (var item in toolboxCollection.Items) - { - item.Selected.DisabledChanged += isDisabled => - { - item.TooltipText = isDisabled ? "Add at least one timing point first!" : ((HitObjectCompositionToolButton)item).TooltipText; - }; - } - togglesCollection.AddRange(CreateTernaryButtons().ToArray()); sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates); diff --git a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs index 641d60dbd3..65a0fb983a 100644 --- a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs +++ b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs @@ -16,7 +16,10 @@ namespace osu.Game.Rulesets.Edit { Tool = tool; - TooltipText = tool.TooltipText; + Selected.BindDisabledChanged(isDisabled => + { + TooltipText = isDisabled ? "Add at least one timing point first!" : Tool.TooltipText; + }, true); } } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index d5fc1363bb..49bdd93bc6 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mods get { if (!SpeedChange.IsDefault) - yield return ("Speed change", $"{SpeedChange.Value:N2}x"); + yield return ("Speed change", FormattableString.Invariant($@"{SpeedChange.Value:N2}x")); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c5a6c9e83d..3010373252 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -476,12 +476,30 @@ namespace osu.Game.Rulesets.Objects.Legacy private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, IList> nodeSamples) { + var path = new SliderPath(controlPoints, length); + + // there are known instances of beatmaps (https://osu.ppy.sh/beatmapsets/594828#osu/1258033) which contain zero-length sliders with non-zero numbers of repeats. + // this was exploiting a bug in stable in which the slider repeats would be generated as objects but never actually judged as a hit *or* miss during gameplay, + // therefore increasing the theoretical possible max combo to be gained from a slider while in practice never giving that extra combo. + // due to lazer ensuring that an object has its nested part fully judged, this would result in broken behaviours + // (either the zero-length slider giving hundreds of combo for nothing if the repeats are judged as hit, or insta-failing the player due to HP if judged as miss). + // to remedy this in a way that seems least damaging, detect this situation via a heuristic and reset the number of repeats to zero. + // this technically *does not* match stable beatmap parsing or conversion, *does not* match in-gameplay behaviour of such broken sliders, + // and *will* fail conversion mapping tests, but again, this is supposed to be a least-worst measure to prevent exploits. + // it is also applied centrally to all rulesets rather than in specific ruleset converters because this failure scenario + // translates across rulesets (osu! and catch are both affected). + if (Precision.AlmostEquals(path.Distance, 0)) + { + repeatCount = 0; + nodeSamples = [nodeSamples[0], nodeSamples[^1]]; + } + return lastObject = new ConvertSlider { Position = position, NewCombo = firstObject || lastObject is ConvertSpinner || newCombo, ComboOffset = newCombo ? comboOffset : 0, - Path = new SliderPath(controlPoints, length), + Path = path, NodeSamples = nodeSamples, RepeatCount = repeatCount }; diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs index 957cfc9b95..7e44e46471 100644 --- a/osu.Game/Scoring/ScoreRank.cs +++ b/osu.Game/Scoring/ScoreRank.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; @@ -10,40 +9,31 @@ namespace osu.Game.Scoring public enum ScoreRank { // TODO: Localisable? - [Description(@"F")] F = -1, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankD))] - [Description(@"D")] D, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankC))] - [Description(@"C")] C, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankB))] - [Description(@"B")] B, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankA))] - [Description(@"A")] A, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankS))] - [Description(@"S")] S, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankSH))] - [Description(@"S+")] // ReSharper disable once InconsistentNaming SH, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankX))] - [Description(@"SS")] X, [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.RankXH))] - [Description(@"SS+")] // ReSharper disable once InconsistentNaming XH, } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index cadae8a5d3..25ed0e8c49 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -117,6 +117,9 @@ namespace osu.Game.Screens.Backgrounds dimmable.Background = Background = b; } + // 接口,提供给 PlayerLoader 使用 + public Drawable CreateBackdropProxy() => dimmable?.CreateProxy(); + public override bool Equals(BackgroundScreen other) { if (!(other is BackgroundScreenBeatmap otherBeatmapBackground)) return false; diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 83d0ec646e..195625dcde 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -64,22 +64,7 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); - // Create a container to hold both the composer content and the vertical timeline - var mainContent = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - wrapSkinnableContent(composer), - new EzVerticalTimeline - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - } - } - }; - - return mainContent; + return wrapSkinnableContent(composer); } protected override Drawable CreateTimelineContent() diff --git a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs index 8af4e3fe52..e7f8ff933d 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionStageProgress.cs @@ -263,6 +263,7 @@ namespace osu.Game.Screens.Edit.Submission iconContainer.Colour = colours.Red1; iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); errorSample?.Play(); + progressSampleChannel?.Stop(); break; case StageStatusType.Canceled: @@ -274,6 +275,7 @@ namespace osu.Game.Screens.Edit.Submission iconContainer.Colour = colours.Gray8; iconContainer.FlashColour(Colour4.White, 1000, Easing.OutQuint); cancelSample?.Play(); + progressSampleChannel?.Stop(); break; } } diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 57bf20de43..a344483894 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -320,7 +320,7 @@ namespace osu.Game.Screens.Edit.Timing { new Box { - Colour = colourProvider.Background3, + Colour = colourProvider.Background2, Alpha = isMainRow ? 1 : 0, RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 46a98dd5da..a73fafcffd 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Menu { Padding = new MarginPadding { Left = WEDGE_WIDTH } }); - buttonsMulti.Add(new MainMenuButton("quick play", @"button-default-select", FontAwesome.Solid.Bolt, new Color4(94, 63, 186, 255), onMatchmaking, Key.Q)); + buttonsMulti.Add(new MainMenuButton("quick play", @"button-daily-select", FontAwesome.Solid.Bolt, new Color4(94, 63, 186, 255), onMatchmaking, Key.Q)); buttonsMulti.ForEach(b => b.VisibleState = ButtonSystemState.Multi); buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, @@ -390,7 +390,7 @@ namespace osu.Game.Screens.Menu return false; case ButtonSystemState.Multi: - buttonsPlay.First().TriggerClick(); + buttonsMulti.First().TriggerClick(); return false; case ButtonSystemState.Edit: diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c74b60c5d7..c4ba3145b5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -37,7 +37,6 @@ using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.DailyChallenge; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.SelectV2; @@ -483,7 +482,7 @@ namespace osu.Game.Screens.Menu private void loadSongSelect() => this.Push(new SoloSongSelect()); - private void joinOrLeaveMatchmakingQueue() => this.Push(new MatchmakingIntroScreen()); + private void joinOrLeaveMatchmakingQueue() => this.Push(new OnlinePlay.Matchmaking.Intro.IntroScreen()); private partial class MobileDisclaimerDialog : PopupDialog { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs index 34c113c39f..b3fff7dc00 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingIntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Intro/ScreenIntro.cs @@ -14,10 +14,14 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Matchmaking.Queue; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Intro { - public partial class MatchmakingIntroScreen : OsuScreen + /// + /// A brief intro animation that introduces matchmaking to the user. + /// + public partial class IntroScreen : OsuScreen { public override bool DisallowExternalBeatmapRulesetChanges => false; @@ -51,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens protected override BackgroundScreen CreateBackground() => new MatchmakingIntroBackgroundScreen(colourProvider); - public MatchmakingIntroScreen() + public IntroScreen() { ValidForResume = false; } @@ -191,7 +195,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Schedule(() => { if (this.IsCurrentScreen()) - this.Push(new MatchmakingQueueScreen()); + this.Push(new ScreenQueue()); }); } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs new file mode 100644 index 0000000000..003b014586 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -0,0 +1,458 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class BeatmapCardMatchmaking : BeatmapCard + { + private readonly APIBeatmap beatmap; + + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + + public const float HEIGHT = 80; + + [Cached] + private readonly BeatmapCardContent content; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + public AvatarOverlay SelectionOverlay = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardMatchmaking(APIBeatmap beatmap) + : base(beatmap.BeatmapSet!, false) + { + this.beatmap = beatmap; + content = new BeatmapCardContent(HEIGHT); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Width = WIDTH; + Height = HEIGHT; + + FillFlowContainer leftIconArea = null!; + FillFlowContainer titleBadgeArea = null!; + GridContainer artistContainer = null!; + + Child = content.With(c => + { + c.MainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true) + { + Name = @"Left (icon) area", + Size = new Vector2(HEIGHT), + Padding = new MarginPadding { Right = CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer + { + Margin = new MarginPadding(4), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) + } + }, + buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true) + { + X = HEIGHT - CORNER_RADIUS, + Width = WIDTH - HEIGHT + CORNER_RADIUS, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = 0, + ButtonsExpandedWidth = 24, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + titleBadgeArea = new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + } + } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new TruncatingSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 1 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(BeatmapSet.Author); + }), + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2), + AlwaysPresent = true, + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Padding = new MarginPadding(4), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6, 0), + Children = new Drawable[] + { + new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(0.9f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + } + }, + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 5, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } + } + } + }, + SelectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + } + } + }; + c.Expanded.BindTarget = Expanded; + }); + + if (BeatmapSet.HasVideo) + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + + if (BeatmapSet.HasStoryboard) + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + + if (BeatmapSet.FeaturedInSpotlight) + { + titleBadgeArea.Add(new SpotlightBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (BeatmapSet.HasExplicitContent) + { + titleBadgeArea.Add(new ExplicitContentBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (BeatmapSet.TrackId != null) + { + artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }; + } + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + protected override void UpdateState() + { + base.UpdateState(); + + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + } + + public override MenuItem[] ContextMenuItems + { + get + { + var items = base.ContextMenuItems.ToList(); + + foreach (var button in buttonContainer.Buttons) + { + if (button.Enabled.Value) + items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick())); + } + + return items.ToArray(); + } + } + + public partial class AvatarOverlay : CompositeDrawable + { + private readonly Container avatars; + + private Sample? userAddedSample; + private double? lastSamplePlayback; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public AvatarOverlay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = avatars = new Container + { + AutoSizeAxes = Axes.X, + Height = SelectionAvatar.AVATAR_SIZE, + }; + + Padding = new MarginPadding { Vertical = 5 }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + + public bool AddUser(APIUser user) + { + if (avatars.Any(a => a.User.Id == user.Id)) + return false; + + var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value)); + + avatars.Add(avatar); + + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + updateAvatarLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) + return false; + + avatar.PopOutAndExpire(); + avatars.ChangeChildDepth(avatar, float.MaxValue); + + updateAvatarLayout(); + + return true; + } + + private void updateAvatarLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatars.Count - 1; i >= 0; i--) + { + var avatar = avatars[i]; + + if (avatar.Expired) + continue; + + avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); + + x -= avatar.LayoutSize.X + spacing; + + delay += stagger; + } + } + + public partial class SelectionAvatar : CompositeDrawable + { + public const float AVATAR_SIZE = 30; + + public APIUser User { get; } + + public bool Expired { get; private set; } + + private readonly MatchmakingAvatar avatar; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + User = user; + Size = new Vector2(AVATAR_SIZE); + + InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + avatar.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index 813e8efa0d..1d3153915f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -15,14 +15,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; using osu.Game.Graphics.Containers; -using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class BeatmapSelectionGrid : CompositeDrawable + public partial class BeatmapSelectGrid : CompositeDrawable { public const double ARRANGE_DELAY = 200; @@ -30,17 +29,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private const double arrange_duration = 1000; private const double roll_duration = 4000; private const double present_beatmap_delay = 1200; - private const float panel_spacing = 20; + private const float panel_spacing = 4; public event Action? ItemSelected; - [Resolved] - private IAPIProvider api { get; set; } = null!; - - private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary panelLookup = new Dictionary(); private readonly PanelGridContainer panelGridContainer; - private readonly Container rollContainer; + private readonly Container rollContainer; private readonly OsuScrollContainer scroll; private bool allowSelection = true; @@ -51,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private Sample? swooshSample; private double? lastSamplePlayback; - public BeatmapSelectionGrid() + public BeatmapSelectGrid() { InternalChildren = new Drawable[] { @@ -67,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick Spacing = new Vector2(panel_spacing) }, }, - rollContainer = new Container + rollContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -108,9 +104,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick public void AddItem(MultiplayerPlaylistItem item) { - var panel = panelLookup[item.ID] = new BeatmapSelectionPanel(item) + var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item) { - Size = new Vector2(300, 70), AllowSelection = allowSelection, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -135,7 +130,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick return; if (selected) - panel.AddUser(user, user.Equals(api.LocalUser.Value)); + panel.AddUser(user); else panel.RemoveUser(user); } @@ -176,7 +171,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick var rng = new Random(); - var remainingPanels = new List(); + var remainingPanels = new List(); foreach (var panel in panelGridContainer.Children.ToArray()) { @@ -216,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { var panel = rollContainer.Children[i]; - var position = positions[i] * (BeatmapPanel.SIZE + new Vector2(panel_spacing)); + var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); @@ -285,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) numSteps++; - BeatmapSelectionPanel? lastPanel = null; + BeatmapSelectPanel? lastPanel = null; for (int i = 0; i < numSteps; i++) { @@ -330,7 +325,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick { rollContainer.ChangeChildDepth(panel, float.MinValue); - panel.ShowBorder(); + panel.ShowChosenBorder(); panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); @@ -346,7 +341,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick PresentRolledBeatmap(finalItem); } - private partial class PanelGridContainer : FillFlowContainer + private partial class PanelGridContainer : FillFlowContainer { public bool LayoutDisabled; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs new file mode 100644 index 0000000000..c6e26d901c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -0,0 +1,227 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class BeatmapSelectPanel : Container + { + public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT); + + public bool AllowSelection { get; set; } + + public readonly MultiplayerPlaylistItem Item; + + public Action? Action { private get; init; } + + private const float border_width = 3; + + private Container scaleContainer = null!; + private Drawable lighting = null!; + + private Container border = null!; + private Container mainContent = null!; + + private readonly List users = new List(); + + private BeatmapCardMatchmaking? card; + + public override bool PropagatePositionalInputSubTree => AllowSelection; + + public BeatmapSelectPanel(MultiplayerPlaylistItem item) + { + Item = item; + Size = SIZE; + } + + [BackgroundDependencyLoader] + private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider) + { + InternalChild = scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + mainContent = new Container + { + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + lighting = new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + } + }, + border = new Container + { + Alpha = 0, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + BorderThickness = border_width, + BorderColour = colourProvider.Light1, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 40, + Roundness = 300, + Colour = colourProvider.Light3.Opacity(0.1f), + }, + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + } + }, + } + }; + lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => + { + Debug.Assert(card == null); + + var beatmap = b.GetResultSafely()!; + beatmap.StarRating = Item.StarRating; + + mainContent.Add(card = new BeatmapCardMatchmaking(beatmap) + { + Depth = float.MaxValue, + Action = () => Action?.Invoke(Item), + }); + + foreach (var user in users) + card.SelectionOverlay.AddUser(user); + })); + } + + public void AddUser(APIUser user) + { + users.Add(user); + card?.SelectionOverlay.AddUser(user); + } + + public void RemoveUser(APIUser user) + { + users.Remove(user); + card?.SelectionOverlay.RemoveUser(user.Id); + } + + protected override bool OnHover(HoverEvent e) + { + lighting.FadeTo(0.2f, 50) + .Then() + .FadeTo(0.1f, 300); + + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + lighting.FadeOut(200); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); + return true; + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); + } + } + + protected override bool OnClick(ClickEvent e) + { + lighting.FadeTo(0.5f, 50) + .Then() + .FadeTo(0.1f, 400); + + // pass through to let the beatmap card handle actual click. + return false; + } + + public void ShowChosenBorder() + { + border.FadeTo(1, 1000, Easing.OutQuint); + } + + public void ShowBorder() + { + border.FadeTo(1, 80, Easing.OutQuint) + .Then() + .FadeTo(0.7f, 800, Easing.OutQuint); + } + + public void HideBorder() + { + border.FadeOut(500, Easing.OutQuint); + } + + public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) + { + scaleContainer + .FadeOut() + .MoveToY(distance) + .Delay(delay) + .FadeIn(duration / 2) + .MoveToY(0, duration, Easing.OutExpo); + } + + public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) + { + AllowSelection = false; + + scaleContainer.Delay(delay) + .ScaleTo(0, duration, easing) + .FadeOut(duration); + + this.Delay(delay + duration).FadeOut().Expire(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs similarity index 58% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index 73e2188273..4b34125517 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/PickScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -9,25 +9,39 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class PickScreen : OsuScreen + public partial class SubScreenBeatmapSelect : MatchmakingSubScreen { - private BeatmapSelectionGrid selectionGrid = null!; + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Split; + public override Drawable PlayersDisplayArea { get; } + + private readonly BeatmapSelectGrid beatmapSelectGrid; [Resolved] private MultiplayerClient client { get; set; } = null!; - [BackgroundDependencyLoader] - private void load() + public SubScreenBeatmapSelect() { - InternalChild = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = selectionGrid = new BeatmapSelectionGrid + new Container { RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 200 }, + Child = beatmapSelectGrid = new BeatmapSelectGrid + { + RelativeSizeAxes = Axes.Both, + }, }, + new Container + { + RelativeSizeAxes = Axes.Both, + Child = PlayersDisplayArea = new Container().With(d => + { + d.RelativeSizeAxes = Axes.Both; + }) + } }; } @@ -40,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick foreach (var item in client.Room!.Playlist) onItemAdded(item); - selectionGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); + beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; @@ -51,22 +65,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick if (item.Expired) return; - selectionGrid.AddItem(item); + beatmapSelectGrid.AddItem(item); }); private void onItemSelected(int userId, long itemId) { var user = client.Room!.Users.First(it => it.UserID == userId).User!; - selectionGrid.SetUserSelection(user, itemId, true); + beatmapSelectGrid.SetUserSelection(user, itemId, true); } private void onItemDeselected(int userId, long itemId) { var user = client.Room!.Users.First(it => it.UserID == userId).User!; - selectionGrid.SetUserSelection(user, itemId, false); + beatmapSelectGrid.SetUserSelection(user, itemId, false); } - public void RollFinalBeatmap(long[] candidateItems, long finalItem) => selectionGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Gameplay/ScreenGameplay.cs similarity index 77% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/Gameplay/ScreenGameplay.cs index af19aa1252..f6f324eb90 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Gameplay/ScreenGameplay.cs @@ -8,11 +8,11 @@ using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay { - public partial class MatchmakingPlayer : MultiplayerPlayer + public partial class ScreenGameplay : MultiplayerPlayer { - public MatchmakingPlayer(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users) + public ScreenGameplay(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users) : base(room, playlistItem, users) { } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs similarity index 87% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs index e3d314844f..53db2114c7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingAvatar.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs @@ -11,8 +11,12 @@ using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { + /// + /// A circular player avatar used in matchmaking displays. + /// Is part of a but can also be used in isolation for a more ambient/decorative user display. + /// public partial class MatchmakingAvatar : CompositeDrawable { public static readonly Vector2 SIZE = new Vector2(30); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs new file mode 100644 index 0000000000..4ff6a3cdf6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingChatDisplay.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osu.Game.Online.Rooms; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.OnlinePlay.Match.Components; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class MatchmakingChatDisplay : MatchChatDisplay, IKeyBindingHandler + { + protected new ChatTextBox TextBox => base.TextBox!; + + public MatchmakingChatDisplay(Room room, bool leaveChannelOnDispose = true) + : base(room, leaveChannelOnDispose) + { + } + + [BackgroundDependencyLoader] + private void load(RealmKeyBindingStore keyBindingStore) + { + resetPlaceholderText(); + + TextBox.HoldFocus = false; + TextBox.ReleaseFocusOnCommit = true; + TextBox.Focus = () => TextBox.PlaceholderText = ChatStrings.InputPlaceholder; + TextBox.FocusLost = resetPlaceholderText; + + void resetPlaceholderText() => TextBox.PlaceholderText = Localisation.ChatStrings.InGameInputPlaceholder(keyBindingStore.GetBindingsStringFor(GlobalAction.ToggleChatFocus)); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.Back: + if (TextBox.HasFocus) + { + Schedule(() => TextBox.KillFocus()); + return true; + } + + break; + + case GlobalAction.ToggleChatFocus: + if (TextBox.HasFocus) + { + Schedule(() => TextBox.KillFocus()); + } + else + { + Schedule(() => TextBox.TakeFocus()); + } + + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingSubScreen.cs similarity index 64% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingSubScreen.cs index 86a46546ca..0141c424bd 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingSubScreen.cs @@ -4,11 +4,14 @@ using osu.Framework.Graphics; using osu.Framework.Screens; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { - public partial class MatchmakingSubScreen : Screen + public abstract partial class MatchmakingSubScreen : Screen { - public MatchmakingSubScreen() + public abstract PanelDisplayStyle PlayersDisplayStyle { get; } + public abstract Drawable? PlayersDisplayArea { get; } + + protected MatchmakingSubScreen() { RelativePositionAxes = Axes.X; } @@ -16,19 +19,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); - this.MoveToX(1).MoveToX(0, 200); + this.FadeInFromZero(200); } public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); - this.MoveToX(-1, 200); + this.FadeOutFromOne(200); } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); - this.MoveToX(0, 200); + this.FadeInFromZero(200); } public override bool OnExiting(ScreenExitEvent e) @@ -36,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens if (base.OnExiting(e)) return true; - this.MoveToX(1, 200); + this.FadeOutFromOne(200); return false; } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs new file mode 100644 index 0000000000..d8b3adabb9 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -0,0 +1,471 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Online.Matchmaking.Events; +using osu.Game.Online.Metadata; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; +using osu.Game.Screens.Play; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + /// + /// A panel used throughout matchmaking to represent a user, including local information like their + /// rank and high level statistics in the matchmaking system. + /// + public partial class PlayerPanel : OsuClickableContainer, IHasContextMenu + { + private static readonly Vector2 size_horizontal = new Vector2(250, 100); + private static readonly Vector2 size_vertical = new Vector2(150, 200); + private static readonly Vector2 avatar_size = new Vector2(80); + + public readonly MultiplayerRoomUser RoomUser; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private UserProfileOverlay? profileOverlay { get; set; } + + [Resolved] + private ChannelManager? channelManager { get; set; } + + [Resolved] + private ChatOverlay? chatOverlay { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Resolved] + protected OverlayColourProvider? ColourProvider { get; private set; } + + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + + [Resolved] + protected OsuColour Colours { get; private set; } = null!; + + [Resolved] + private MultiplayerClient? multiplayerClient { get; set; } + + [Resolved] + private MetadataClient? metadataClient { get; set; } + + private OsuSpriteText rankText = null!; + private OsuSpriteText scoreText = null!; + + private Drawable avatarPositionTarget = null!; + private Drawable avatarJumpTarget = null!; + private MatchmakingAvatar avatar = null!; + private OsuSpriteText username = null!; + + private Container mainContent = null!; + + private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal; + + public PlayerPanelDisplayMode DisplayMode + { + get => displayMode; + set + { + displayMode = value; + if (IsLoaded) + updateLayout(false); + } + } + + public readonly APIUser User; + + /// + /// Perform an action in addition to showing the user's profile. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). + /// + public new Action? Action; + + protected Action ViewProfile { get; private set; } = null!; + + public Box SolidBackgroundLayer { get; private set; } = null!; + + protected Drawable? Background { get; private set; } + + public PlayerPanel(MultiplayerRoomUser user) + : base(HoverSampleSet.Button) + { + ArgumentNullException.ThrowIfNull(user.User); + + User = user.User; + RoomUser = user; + } + + [BackgroundDependencyLoader] + private void load() + { + Add(SolidBackgroundLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider?.Background5 ?? Colours.Gray1 + }); + + Background = new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User + }; + if (Background != null) + Add(Background); + + base.Action = ViewProfile = () => + { + Action?.Invoke(); + profileOverlay?.ShowUser(User); + }; + + Content.Masking = true; + Content.CornerRadius = 10; + Content.CornerExponent = 10; + Content.Anchor = Anchor.Centre; + Content.Origin = Anchor.Centre; + + Add(new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = mainContent = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + avatarPositionTarget = new Container + { + Origin = Anchor.Centre, + Size = avatar_size, + Child = avatarJumpTarget = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One + } + } + }, + rankText = new OsuSpriteText + { + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomCentre, + Blending = BlendingParameters.Additive, + Margin = new MarginPadding(4), + Text = "-", + Font = OsuFont.Style.Title.With(size: 55), + }, + username = new OsuSpriteText + { + Alpha = 0, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Text = User.Username, + Font = OsuFont.Style.Heading1, + }, + scoreText = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Style.Heading2, + Text = "0 pts" + } + } + } + }); + + // Allow avatar to exist outside of masking for when it jumps around and stuff. + AddInternal(avatar.CreateProxy()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateLayout(true); + + client.MatchRoomStateChanged += onRoomStateChanged; + client.MatchEvent += onMatchEvent; + + onRoomStateChanged(client.Room!.MatchState); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal; + + private Vector2 avatarPosition + { + get + { + switch (displayMode) + { + case PlayerPanelDisplayMode.AvatarOnly: + return avatar_size / 2; + + case PlayerPanelDisplayMode.Horizontal: + return new Vector2(50); + + case PlayerPanelDisplayMode.Vertical: + return new Vector2(75, 50); + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private void updateLayout(bool instant) + { + double duration = instant ? 0 : 1000; + + avatarPositionTarget.MoveTo(avatarPosition, duration, Easing.OutPow10); + + switch (displayMode) + { + case PlayerPanelDisplayMode.AvatarOnly: + rankText.Hide(); + scoreText.Hide(); + username.Hide(); + + Background.FadeOut(200, Easing.OutQuint); + SolidBackgroundLayer.FadeOut(200, Easing.OutQuint); + + this.ResizeTo(avatar_size, duration, Easing.OutPow10); + break; + + case PlayerPanelDisplayMode.Horizontal: + case PlayerPanelDisplayMode.Vertical: + Background.FadeIn(200); + SolidBackgroundLayer.FadeIn(200); + + using (BeginDelayedSequence(100)) + { + username.FadeIn(600); + + using (BeginDelayedSequence(100)) + { + scoreText.FadeIn(600); + + using (BeginDelayedSequence(100)) + { + rankText.FadeTo(1, 600); + } + } + } + + this.ResizeTo(horizontal ? size_horizontal : size_vertical, duration, Easing.OutPow10); + + rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10); + username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10); + scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected override bool OnHover(HoverEvent e) + { + Content.ScaleTo(1.03f, 2000, Easing.OutPow10); + mainContent.ScaleTo(1.03f, 2000, Easing.OutPow10); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + Content.ScaleTo(1f, 750, Easing.OutPow10); + mainContent.ScaleTo(1, 750, Easing.OutPow10); + + mainContent.MoveTo(Vector2.Zero, 1250, Easing.OutPow10); + avatarPositionTarget.MoveTo(avatarPosition, 1250, Easing.OutPow10); + base.OnHoverLost(e); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + var offset = (avatarPositionTarget.ToLocalSpace(e.ScreenSpaceMousePosition) - avatarPositionTarget.DrawSize / 2) * 0.02f; + + mainContent.MoveTo(offset * 0.5f, 2000, Easing.OutPow10); + avatarPositionTarget.MoveTo(avatarPosition + offset, 2000, Easing.OutPow10); + return base.OnMouseMove(e); + } + + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore)) + return; + + rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture); + rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement)); + scoreText.Text = $"{userScore.Points} pts"; + }); + + private int consecutiveJumps; + + private void onMatchEvent(MatchServerEvent e) + { + switch (e) + { + case MatchmakingAvatarActionEvent action: + if (action.UserId != RoomUser.UserID) + break; + + switch (action.Action) + { + case MatchmakingAvatarAction.Jump: + var movement = avatarJumpTarget.Delay(0); + var scale = avatarJumpTarget.Delay(0); + + // only increase height if the user jumps again while in a "jumped" state. + // this avoids building up large jumps from very quick spam, and adds a timing game. + bool isConsecutive = avatarJumpTarget.Y < 0; + + if (isConsecutive) + { + consecutiveJumps++; + + if (avatarJumpTarget.Y > 0) + movement = movement.MoveToY(0); + + movement = movement.MoveToY(5, 100, Easing.Out); + scale = scale.ScaleTo(new Vector2(1, 0.95f), 100, Easing.Out); + } + else + { + consecutiveJumps = 0; + } + + float multiplier = 1 + 0.3f * Math.Min(10, consecutiveJumps); + + movement.Then().MoveToY(-10 * multiplier, 200, Easing.Out) + .Then().MoveToY(0, 200, Easing.In); + + scale.Then().ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out) + .Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In) + .Then().ScaleTo(Vector2.One, 800, Easing.OutElastic); + break; + } + + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.MatchRoomStateChanged -= onRoomStateChanged; + client.MatchEvent -= onMatchEvent; + } + } + + public MenuItem[] ContextMenuItems + { + get + { + List items = new List + { + new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile) + }; + + if (User.Equals(api.LocalUser.Value)) + return items.ToArray(); + + items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () => + { + channelManager?.OpenPrivateChannel(User); + chatOverlay?.Show(); + })); + + items.Add(!isUserBlocked() + ? new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(User))) + : new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(User)))); + + if (isUserOnline()) + { + items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => + { + if (isUserOnline()) + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))); + })); + + if (canInviteUser()) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => + { + if (canInviteUser()) + multiplayerClient!.InvitePlayer(User.Id); + })); + } + } + + return items.ToArray(); + + bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null; + bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true; + bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID); + } + } + } + + public enum PlayerPanelDisplayMode + { + AvatarOnly, + Horizontal, + Vertical + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs new file mode 100644 index 0000000000..510698f46e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -0,0 +1,312 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + /// + /// A component which maintains the layout of the players in a matchmaking room. + /// Can be controlled to display the panels in a certain location and in multiple styles. + /// + public partial class PlayerPanelOverlay : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Container panels = null!; + private PlayerPanelCellContainer gridLayout = null!; + private PlayerPanelCellContainer splitLayoutLeft = null!; + private PlayerPanelCellContainer splitLayoutRight = null!; + + private PanelDisplayStyle displayStyle; + private Drawable? displayArea; + private bool isAnimatingToDisplayArea; + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + gridLayout = new PlayerPanelCellContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(20), + }, + splitLayoutLeft = new PlayerPanelCellContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + }, + splitLayoutRight = new PlayerPanelCellContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + }, + panels = new Container + { + RelativeSizeAxes = Axes.Both + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Set position/size so we don't initially animate. + Position = getFinalPosition(); + Size = getFinalSize(); + + client.MatchRoomStateChanged += onRoomStateChanged; + client.UserJoined += onUserJoined; + client.UserLeft += onUserLeft; + + if (client.Room != null) + { + onRoomStateChanged(client.Room.MatchState); + foreach (var user in client.Room.Users) + onUserJoined(user); + } + + updateDisplay(); + } + + public PanelDisplayStyle DisplayStyle + { + set + { + displayStyle = value; + if (IsLoaded) + updateDisplay(); + } + } + + public Drawable? DisplayArea + { + set + { + displayArea = value; + isAnimatingToDisplayArea = true; + } + } + + private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => + { + panels.Add(new PlayerPanel(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f) + }); + + updateDisplay(); + }); + + private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => + { + panels.Single(p => p.RoomUser.Equals(user)).Expire(); + updateDisplay(); + }); + + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(updateDisplay); + + private void updateDisplay() + { + gridLayout.ReleasePanels(); + splitLayoutLeft.ReleasePanels(); + splitLayoutRight.ReleasePanels(); + + switch (displayStyle) + { + case PanelDisplayStyle.Grid: + foreach (var panel in panels) + { + panel.FadeTo(1, 200); + panel.DisplayMode = PlayerPanelDisplayMode.Vertical; + } + + gridLayout.AcquirePanels(panels.ToArray()); + break; + + case PanelDisplayStyle.Split: + foreach (var panel in panels) + { + panel.FadeTo(1, 200); + panel.DisplayMode = PlayerPanelDisplayMode.Horizontal; + } + + int leftCount = (int)Math.Ceiling(panels.Count / 2f); + + splitLayoutLeft.AcquirePanels(panels.Take(leftCount).ToArray()); + splitLayoutRight.AcquirePanels(panels.Skip(leftCount).ToArray()); + break; + + case PanelDisplayStyle.Hidden: + foreach (var panel in panels) + panel.FadeTo(0, 200); + return; + } + } + + protected override void Update() + { + base.Update(); + + var targetPos = getFinalPosition(); + var targetSize = getFinalSize(); + + double duration = isAnimatingToDisplayArea ? 60 : 0; + + if (Time.Elapsed > 0) + { + Position = new Vector2( + (float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed) + ); + + Size = new Vector2( + (float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed) + ); + } + + // If we don't track the animating state, the animation will also occur when resizing the window. + isAnimatingToDisplayArea &= !Precision.AlmostEquals(Size, targetSize, 0.5f); + } + + private Vector2 getFinalPosition() + => displayArea == null ? Vector2.Zero : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft); + + private Vector2 getFinalSize() + => displayArea == null ? Parent!.DrawSize : Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.BottomRight) - Parent!.ToLocalSpace(displayArea.ScreenSpaceDrawQuad.TopLeft); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.MatchRoomStateChanged -= onRoomStateChanged; + client.UserJoined -= onUserJoined; + client.UserLeft -= onUserLeft; + } + } + + private partial class PlayerPanelCellContainer : FillFlowContainer + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + public void AcquirePanels(PlayerPanel[] panels) + { + while (Count < panels.Length) + { + Add(new PlayerPanelCell + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + while (Count > panels.Length) + Remove(Children[^1], true); + + for (int i = 0; i < panels.Length; i++) + { + // We'll invalidate the layout position to represent the new placements and the re-flow will happen in UpdateAfterChildren(). + // But the cells expect their positions to be valid as they're updated, which won't be the case until the re-flow happens. + int i2 = i; + ScheduleAfterChildren(() => Children[i2].AcquirePanel(panels[i2])); + + if (client.Room?.MatchState is not MatchmakingRoomState matchmakingState) + continue; + + if (matchmakingState.Users.UserDictionary.TryGetValue(panels[i].User.Id, out MatchmakingUser? user)) + SetLayoutPosition(Children[i], user.Placement); + else + SetLayoutPosition(Children[i], float.MaxValue); + } + } + + public void ReleasePanels() + { + // Matches the schedule in AcquirePanels. + ScheduleAfterChildren(() => + { + foreach (var panel in Children) + panel.ReleasePanel(); + }); + } + } + + private partial class PlayerPanelCell : Drawable + { + private PlayerPanel? panel; + private bool isAnimating; + + public void AcquirePanel(PlayerPanel panel) + { + this.panel = panel; + isAnimating = true; + } + + public void ReleasePanel() + { + panel = null; + } + + protected override void Update() + { + base.Update(); + + if (panel?.Parent == null) + return; + + Size = panel.Size * panel.Scale; + + var targetPos = getFinalPosition(); + + double duration = isAnimating ? 60 : 0; + + if (Time.Elapsed > 0) + { + panel.Position = new Vector2( + (float)Interpolation.DampContinuously(panel.Position.X, targetPos.X, duration, Time.Elapsed), + (float)Interpolation.DampContinuously(panel.Position.Y, targetPos.Y, duration, Time.Elapsed) + ); + } + + // If we don't track the animating state, the animation will also occur when resizing the window. + isAnimating &= !Precision.AlmostEquals(panel.Position, targetPos, 0.5f); + + Vector2 getFinalPosition() + => panel.Parent.ToLocalSpace(ScreenSpaceDrawQuad.Centre) - panel.AnchorPosition; + } + } + } + + public enum PanelDisplayStyle + { + Grid, + Split, + Hidden + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs new file mode 100644 index 0000000000..fb93d5e804 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelRoomAward.cs @@ -0,0 +1,137 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results +{ + public partial class PanelRoomAward : OsuClickableContainer + { + private readonly string text; + private readonly string description; + private readonly int userId; + + private Box glossLayer = null!; + private Container scaleContainer = null!; + + public PanelRoomAward(string text, string description, int userId) + { + this.text = text; + this.description = description; + this.userId = userId; + + Height = 40; + RelativeSizeAxes = Axes.X; + + // Just make hover sounds work for now. + Action = () => { }; + } + + [BackgroundDependencyLoader] + private void load(UserLookupCache userLookupCache, OverlayColourProvider colourProvider) + { + // Should be cached by this point. + APIUser user = userLookupCache.GetUserAsync(userId).GetResultSafely()!; + + Child = scaleContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(10), + Spacing = new Vector2(10), + Children = new Drawable[] + { + new MatchmakingAvatar(user) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.Style.Caption1, + Text = user.Username + }, + new OsuSpriteText + { + Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold), + Text = text + } + } + }, + } + }, + glossLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Rotation = 30, + Scale = new Vector2(0.1f, 3), + Colour = ColourInfo.GradientHorizontal( + colourProvider.Background2.Opacity(0), + colourProvider.Background2), + Alpha = 0.1f, + Blending = BlendingParameters.Additive, + }, + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + scaleContainer.ScaleTo(1.15f, 2000, Easing.OutPow10); + glossLayer + .FadeTo(0.05f, 2000, Easing.OutPow10) + .MoveToX(-8, 2000, Easing.OutPow10); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + scaleContainer.ScaleTo(1f, 500, Easing.OutQuint); + glossLayer + .FadeTo(0.1f, 500, Easing.OutQuint) + .MoveToX(0, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + public override LocalisableString TooltipText => description; + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs new file mode 100644 index 0000000000..c1b1be0b2b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/PanelUserStatistic.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results +{ + public partial class PanelUserStatistic : CompositeDrawable + { + private readonly int position; + private readonly string text; + + public PanelUserStatistic(int position, string text) + { + this.position = position; + this.text = text; + + AutoSizeAxes = Axes.Both; + } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new CircularContainer + { + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Container + { + Width = 30, + Masking = true, + CornerRadius = 6, + CornerExponent = 10, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = SubScreenResults.ColourForPlacement(position), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = position.Ordinalize(CultureInfo.CurrentCulture), + Colour = colourProvider.Background4, + }, + } + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Style.Caption2, + Text = text + } + } + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs new file mode 100644 index 0000000000..797519a53c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/Results/SubScreenResults.cs @@ -0,0 +1,373 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Overlays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results +{ + /// + /// Final room results, during + /// + public partial class SubScreenResults : MatchmakingSubScreen + { + private const float grid_spacing = 5; + + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Grid; + + public override Drawable PlayersDisplayArea { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText placementText = null!; + private FillFlowContainer userStatistics = null!; + private FillFlowContainer roomAwards = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + Padding = new MarginPadding(5), + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, grid_spacing), + new Dimension(), + }, + Content = new[] + { + new[] + { + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 5, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(6), + Spacing = new Vector2(grid_spacing), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "How you played", + Font = OsuFont.Style.Heading2, + Margin = new MarginPadding { Vertical = 15 }, + }, + userStatistics = new FillFlowContainer + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(grid_spacing) + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Room Awards", + Font = OsuFont.Style.Heading2, + Margin = new MarginPadding { Vertical = 15 }, + }, + roomAwards = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(grid_spacing) + } + } + } + }, + }, + Empty(), + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, grid_spacing), + new Dimension(), + ], + Content = new Drawable[]?[] + { + [ + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(16), + Children = new[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Your final placement", + Font = OsuFont.Style.Heading2.With(size: 36), + }, + placementText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Heading1.With(size: 72), + UseFullGlyphHeight = false + } + } + } + ], + null, + [ + PlayersDisplayArea, + ], + } + }, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onRoomStateChanged; + + onRoomStateChanged(client.Room?.MatchState); + } + + private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState || matchmakingState.Stage != MatchmakingStage.Ended) + return; + + populateUserStatistics(matchmakingState); + populateRoomStatistics(matchmakingState); + }); + + private void populateUserStatistics(MatchmakingRoomState state) + { + userStatistics.Clear(); + + if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0) + { + placementText.Text = "-"; + placementText.Colour = OsuColour.Gray(1f); + return; + } + + int overallPlacement = state.Users[client.LocalUser!.UserID].Placement; + + placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture); + placementText.Colour = ColourForPlacement(overallPlacement); + + int overallPoints = state.Users[client.LocalUser!.UserID].Points; + addStatistic(overallPlacement, $"Overall position ({overallPoints} points)"); + + var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average())) + .OrderByDescending(t => t.avgAcc) + .Select((t, i) => (info: t, index: i)) + .Single(t => t.info.user.UserId == client.LocalUser!.UserID); + int accuracyPlacement = accuracyOrderedUsers.index + 1; + addStatistic(accuracyPlacement, $"Overall accuracy ({accuracyOrderedUsers.info.avgAcc.FormatAccuracy()})"); + + var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Max(r => r.MaxCombo))) + .OrderByDescending(t => t.maxCombo) + .Select((t, i) => (info: t, index: i)) + .Single(t => t.info.user.UserId == client.LocalUser!.UserID); + int maxComboPlacement = maxComboOrderedUsers.index + 1; + addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)"); + + var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement); + addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})"); + + void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text)); + } + + public static ColourInfo ColourForPlacement(int overallPlacement) + { + // for top 3 placements use special colours. + // don't for the rest. + + switch (overallPlacement) + { + case 1: + return OsuColour.ForRankingTier(RankingTier.Gold); + + case 2: + return OsuColour.ForRankingTier(RankingTier.Silver); + + case 3: + return OsuColour.ForRankingTier(RankingTier.Bronze); + + default: + return OsuColour.ForRankingTier(RankingTier.Iron); + } + } + + private void populateRoomStatistics(MatchmakingRoomState state) + { + roomAwards.Clear(); + + long maxScore = long.MinValue; + int maxScoreUserId = 0; + + double maxAccuracy = double.MinValue; + int maxAccuracyUserId = 0; + + int maxCombo = int.MinValue; + int maxComboUserId = 0; + + long maxBonusScore = 0; + int maxBonusScoreUserId = 0; + + long largestScoreDifference = long.MinValue; + int largestScoreDifferenceUserId = 0; + + long smallestScoreDifference = long.MaxValue; + int smallestScoreDifferenceUserId = 0; + + for (int round = 1; round <= state.CurrentRound; round++) + { + long roundHighestScore = long.MinValue; + int roundHighestScoreUserId = 0; + + long roundLowestScore = long.MaxValue; + + foreach (MatchmakingUser user in state.Users) + { + if (!user.Rounds.RoundsDictionary.TryGetValue(round, out MatchmakingRound? mmRound)) + continue; + + if (mmRound.TotalScore > maxScore) + { + maxScore = mmRound.TotalScore; + maxScoreUserId = user.UserId; + } + + if (mmRound.Accuracy > maxAccuracy) + { + maxAccuracy = mmRound.Accuracy; + maxAccuracyUserId = user.UserId; + } + + if (mmRound.MaxCombo > maxCombo) + { + maxCombo = mmRound.MaxCombo; + maxComboUserId = user.UserId; + } + + if (mmRound.TotalScore > roundHighestScore) + { + roundHighestScore = mmRound.TotalScore; + roundHighestScoreUserId = user.UserId; + } + + if (mmRound.TotalScore < roundLowestScore) + roundLowestScore = mmRound.TotalScore; + } + + long roundScoreDifference = roundHighestScore - roundLowestScore; + + if (roundScoreDifference > 0 && roundScoreDifference > largestScoreDifference) + { + largestScoreDifference = roundScoreDifference; + largestScoreDifferenceUserId = roundHighestScoreUserId; + } + + if (roundScoreDifference > 0 && roundScoreDifference < smallestScoreDifference) + { + smallestScoreDifference = roundScoreDifference; + smallestScoreDifferenceUserId = roundHighestScoreUserId; + } + } + + foreach (MatchmakingUser user in state.Users) + { + int userBonusScore = 0; + + foreach (MatchmakingRound round in user.Rounds) + { + userBonusScore += round.Statistics.TryGetValue(HitResult.LargeBonus, out int bonus) ? bonus * 5 : 0; + userBonusScore += round.Statistics.TryGetValue(HitResult.SmallBonus, out bonus) ? bonus : 0; + } + + if (userBonusScore > maxBonusScore) + { + maxBonusScore = userBonusScore; + maxBonusScoreUserId = user.UserId; + } + } + + addAward(maxScoreUserId, "Score champ", "Highest score in a single round"); + + addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round"); + + addAward(maxComboUserId, "Top combo", "Highest combo in a single round"); + + if (maxBonusScoreUserId > 0) + addAward(maxBonusScoreUserId, "Biggest bonus", "Biggest bonus score across all rounds"); + + if (smallestScoreDifferenceUserId > 0) + addAward(smallestScoreDifferenceUserId, "Most clutch", "Smallest winning score difference in a single round"); + + if (largestScoreDifferenceUserId > 0) + addAward(largestScoreDifferenceUserId, "Best finish", "Largest score difference in a single round"); + + void addAward(int userId, string text, string description) => roomAwards.Add(new PanelRoomAward(text, description, userId)); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onRoomStateChanged; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs similarity index 81% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs index d7837e96c6..580d157a8b 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundResults/SubScreenRoundResults.cs @@ -18,17 +18,24 @@ using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults { - public partial class RoundResultsScreen : MatchmakingSubScreen + /// + /// Per-round results, during + /// + public partial class SubScreenRoundResults : MatchmakingSubScreen { private const int panel_spacing = 5; + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Hidden; + public override Drawable? PlayersDisplayArea => null; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -150,6 +157,32 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults } }); + private partial class RoundResultsScorePanel : CompositeDrawable + { + public RoundResultsScorePanel(ScoreInfo score) + { + AutoSizeAxes = Axes.Both; + InternalChild = new InstantSizingScorePanel(score); + } + + public override bool PropagateNonPositionalInputSubTree => false; + public override bool PropagatePositionalInputSubTree => false; + + private partial class InstantSizingScorePanel : ScorePanel + { + public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false) + : base(score, isNewLocalScore) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + FinishTransforms(true); + } + } + } + private partial class AutoScrollContainer : UserTrackingScrollContainer { private const float initial_offset = -0.5f; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs new file mode 100644 index 0000000000..e389cbabfa --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/RoundWarmup/SubScreenRoundWarmup.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup +{ + /// + /// Shown during + /// + public partial class SubScreenRoundWarmup : MatchmakingSubScreen + { + public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Grid; + public override Drawable PlayersDisplayArea => this; + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + this.MoveToX(0); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs new file mode 100644 index 0000000000..279dd98a5e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class ScreenMatchmaking + { + public partial class ScreenStack : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Framework.Screens.ScreenStack screenStack = null!; + private PlayerPanelOverlay playersList = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(6) + { + Bottom = StageDisplay.HEIGHT + 6, + }, + Children = new Drawable[] + { + screenStack = new Framework.Screens.ScreenStack(), + } + }, + playersList = new PlayerPanelOverlay + { + DisplayArea = this + }, + new StageDisplay + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + screenStack.ScreenPushed += onScreenPushed; + screenStack.ScreenExited += onScreenExited; + + screenStack.Push(new SubScreenRoundWarmup()); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + onMatchRoomStateChanged(client.Room!.MatchState); + } + + private void onScreenPushed(IScreen lastScreen, IScreen newScreen) + { + if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) + return; + + playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; + playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; + } + + private void onScreenExited(IScreen lastScreen, IScreen newScreen) + { + if (newScreen is not MatchmakingSubScreen matchmakingSubScreen) + return; + + playersList.DisplayStyle = matchmakingSubScreen.PlayersDisplayStyle; + playersList.DisplayArea = matchmakingSubScreen.PlayersDisplayArea; + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + switch (matchmakingState.Stage) + { + case MatchmakingStage.WaitingForClientsJoin: + case MatchmakingStage.RoundWarmupTime: + while (screenStack.CurrentScreen is not SubScreenRoundWarmup) + screenStack.Exit(); + break; + + case MatchmakingStage.UserBeatmapSelect: + screenStack.Push(new SubScreenBeatmapSelect()); + break; + + case MatchmakingStage.ServerBeatmapFinalised: + Debug.Assert(screenStack.CurrentScreen is SubScreenBeatmapSelect); + ((SubScreenBeatmapSelect)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem); + break; + + case MatchmakingStage.ResultsDisplaying: + screenStack.Push(new SubScreenRoundResults()); + break; + + case MatchmakingStage.Ended: + screenStack.Push(new SubScreenResults()); + break; + } + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs similarity index 82% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs index b02583103d..e4031c5e98 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.cs @@ -4,36 +4,40 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Screens.Footer; using osu.Game.Screens.OnlinePlay.Match.Components; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; -using osuTK; +using osuTK.Input; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { - public partial class MatchmakingScreen : OsuScreen + /// + /// The main matchmaking screen which houses a custom through the life cycle of a single session. + /// + public partial class ScreenMatchmaking : OsuScreen { /// /// Padding between rows of the content. @@ -67,12 +71,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [Resolved] private IDialogOverlay dialogOverlay { get; set; } = null!; + [Resolved] + private AudioManager audio { get; set; } = null!; + private readonly MultiplayerRoom room; + private Sample? sampleStart; private CancellationTokenSource? downloadCheckCancellation; private int? lastDownloadCheckedBeatmapId; - public MatchmakingScreen(MultiplayerRoom room) + private MatchChatDisplay chat = null!; + + public ScreenMatchmaking(MultiplayerRoom room) { this.room = room; @@ -81,8 +91,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -99,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = ScreenFooter.HEIGHT + 20 + Top = row_padding, }, RowDimensions = new[] { @@ -120,9 +132,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + Colour = colourProvider.Background6, }, - new MatchmakingScreenStack(), + new ScreenStack(), } } ], @@ -130,31 +142,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking [ new Container { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 700, + Height = 130, + Padding = new MarginPadding { Bottom = row_padding }, + Child = chat = new MatchmakingChatDisplay(new Room(room)) { - new Container - { - RelativeSizeAxes = Axes.X, - Height = 100, - Padding = new MarginPadding - { - Horizontal = 200, - }, - Child = new MatchChatDisplay(new Room(room)) - { - RelativeSizeAxes = Axes.Both, - } - }, - new RoundedButton - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Text = "Don't click me", - Size = new Vector2(100, 30), - Action = () => client.MatchmakingSkipToNextStage() - } + RelativeSizeAxes = Axes.Both, } } ] @@ -175,6 +170,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking client.LoadRequested += onLoadRequested; beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); + + Footer!.Add(chat.CreateProxy()); } private void onRoomUpdated() @@ -248,7 +245,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onLoadRequested() => Scheduler.Add(() => { updateGameplayState(); - this.Push(new MultiplayerPlayerLoader(() => new MatchmakingPlayer(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); + + if (Beatmap.IsDefault) + { + Logger.Log("Aborting gameplay start - beatmap not downloaded."); + return; + } + + sampleStart?.Play(); + + this.Push(new MultiplayerPlayerLoader(() => new ScreenGameplay(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); }); private void checkForAutomaticDownload() @@ -287,6 +293,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking })); } + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Space: + if (e.Repeat) + return true; + + client.SendMatchRequest(new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).FireAndForget(); + return true; + } + + return false; + } + private bool exitConfirmed; public override bool OnExiting(ScreenExitEvent e) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs new file mode 100644 index 0000000000..7e3b7d4468 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs @@ -0,0 +1,234 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Matchmaking; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class StageDisplay + { + internal partial class StageSegment : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + public readonly int? Round; + + private readonly MatchmakingStage stage; + + private readonly LocalisableString displayText; + private Drawable progressBar = null!; + + private DateTimeOffset countdownStartTime; + private DateTimeOffset countdownEndTime; + private SpriteIcon arrow = null!; + + private Sample? segmentStartedSample; + + private Container mainContent = null!; + + public bool Active { get; private set; } + + public float Progress => progressBar.Width; + + public StageSegment(int? round, MatchmakingStage stage, LocalisableString displayText) + { + Round = round; + + this.stage = stage; + this.displayText = displayText; + + AutoSizeAxes = Axes.Both; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OverlayColourProvider colourProvider) + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + arrow = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = 0.5f, + Size = new Vector2(16), + Icon = FontAwesome.Solid.ArrowRight, + Margin = new MarginPadding { Horizontal = 10 } + }, + mainContent = new Container + { + Masking = true, + CornerRadius = 5, + CornerExponent = 10, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = + ColourInfo.GradientVertical( + colourProvider.Dark2, + colourProvider.Dark1 + ), + }, + progressBar = new Box + { + Blending = BlendingParameters.Additive, + EdgeSmoothness = new Vector2(1), + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = colourProvider.Dark3, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = displayText, + Padding = new MarginPadding(10) + } + } + } + } + }; + + Alpha = 0.5f; + segmentStartedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-segment"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + client.CountdownStarted += onCountdownStarted; + client.CountdownStopped += onCountdownStopped; + + if (client.Room != null) + { + onMatchRoomStateChanged(client.Room.MatchState); + foreach (var countdown in client.Room.ActiveCountdowns) + onCountdownStarted(countdown); + } + } + + protected override void Update() + { + base.Update(); + + if (!Active) + return; + + TimeSpan total = countdownEndTime - countdownStartTime; + TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; + + if (total.TotalMilliseconds <= 0) + { + progressBar.Width = 0; + return; + } + + progressBar.Width = (float)Math.Clamp(elapsed.TotalMilliseconds / total.TotalMilliseconds, 0, 1); + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + bool wasActive = Active; + + Active = false; + + if (state is not MatchmakingRoomState roomState) + return; + + if (Round != null && roomState.CurrentRound != Round) + return; + + Active = stage == roomState.Stage; + + if (wasActive) + progressBar.Width = 1; + + mainContent.ScaleTo(Active ? 1.3f : 1, 500, Easing.OutQuint); + + bool isPreparing = + (stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) || + (stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) || + (stage == MatchmakingStage.ResultsDisplaying && roomState.Stage == MatchmakingStage.Gameplay); + + if (isPreparing) + { + arrow.FadeTo(1, 500) + .Then() + .FadeTo(0.5f, 500) + .Loop(); + } + }); + + private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (!Active) + return; + + if (countdown is not MatchmakingStageCountdown) + return; + + countdownStartTime = DateTimeOffset.Now; + countdownEndTime = countdownStartTime + countdown.TimeRemaining; + arrow.FadeIn(500, Easing.OutQuint); + + this.FadeIn(200); + + segmentStartedSample?.Play(); + }); + + private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => + { + if (!Active) + return; + + if (countdown is not MatchmakingStageCountdown) + return; + + countdownEndTime = DateTimeOffset.Now; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + client.CountdownStarted -= onCountdownStarted; + client.CountdownStopped -= onCountdownStopped; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs new file mode 100644 index 0000000000..33692ffe03 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StatusText.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + public partial class StageDisplay + { + public partial class StatusText : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private OsuSpriteText text = null!; + + private Sample? textChangedSample; + private double? lastSamplePlayback; + + public StatusText() + { + AutoSizeAxes = Axes.X; + Height = 16; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + InternalChild = text = new OsuSpriteText + { + Alpha = 0, + Height = 16, + Font = OsuFont.Style.Caption1, + AlwaysPresent = true, + }; + + textChangedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-message"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.MatchRoomStateChanged += onMatchRoomStateChanged; + onMatchRoomStateChanged(client.Room!.MatchState); + } + + private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => + { + if (state is not MatchmakingRoomState matchmakingState) + return; + + text.Text = getTextForStatus(matchmakingState.Stage); + + if (text.Text == string.Empty || (lastSamplePlayback != null && Time.Current - lastSamplePlayback < OsuGameBase.SAMPLE_DEBOUNCE_TIME)) + return; + + if (matchmakingState.Stage is MatchmakingStage.WaitingForClientsJoin or MatchmakingStage.WaitingForClientsBeatmapDownload) + { + textChangedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + LocalisableString textForStatus = getTextForStatus(matchmakingState.Stage); + + if (string.IsNullOrEmpty(textForStatus.ToString())) + { + text.FadeOut(); + return; + } + + text.RotateTo(2f) + .RotateTo(0, 500, Easing.OutQuint); + + text.FadeInFromZero(500, Easing.OutQuint); + + using (text.BeginDelayedSequence(500)) + { + text + .FadeTo(0.6f, 400, Easing.In) + .Then() + .FadeTo(1, 400, Easing.Out) + .Loop(); + } + + text.ScaleTo(0.3f) + .ScaleTo(1, 500, Easing.OutQuint); + + text.Text = textForStatus; + }); + + private LocalisableString getTextForStatus(MatchmakingStage status) + { + switch (status) + { + case MatchmakingStage.WaitingForClientsJoin: + return "Players are joining the match..."; + + case MatchmakingStage.WaitingForClientsBeatmapDownload: + return "Players are downloading the beatmap..."; + + case MatchmakingStage.Gameplay: + return "Game is in progress..."; + + case MatchmakingStage.Ended: + return "Thanks for playing! The match will close shortly."; + + default: + return string.Empty; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.MatchRoomStateChanged -= onMatchRoomStateChanged; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs new file mode 100644 index 0000000000..9bb1fe1397 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -0,0 +1,292 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match +{ + /// + /// A "global" footer staple element in matchmaking which shows the current progression of the room, from start to finish. + /// + public partial class StageDisplay : CompositeDrawable + { + public const float HEIGHT = 96; + + // TODO: get this from somewhere? + private const int round_count = 5; + + private OsuScrollContainer scroll = null!; + private FillFlowContainer flow = null!; + + private CurrentRoundDisplay roundDisplay = null!; + + public StageDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Dark6, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = HEIGHT, + Children = new Drawable[] + { + scroll = new StageScrollContainer + { + ScrollbarVisible = false, + ClampExtension = 0, + RelativeSizeAxes = Axes.X, + Height = HEIGHT, + Child = flow = new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 2000 }, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, + }, + }, + new StatusText + { + Y = 32, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new Box + { + Colour = ColourInfo.GradientHorizontal( + colourProvider.Dark4, + colourProvider.Dark5.Opacity(0) + ), + RelativeSizeAxes = Axes.Y, + Width = 240, + }, + roundDisplay = new CurrentRoundDisplay + { + X = 12, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + }; + + flow.Add(new StageSegment(null, MatchmakingStage.WaitingForClientsJoin, "Waiting for other users")); + + for (int i = 1; i <= round_count; i++) + { + flow.Add(new StageSegment(i, MatchmakingStage.RoundWarmupTime, "Next Round")); + flow.Add(new StageSegment(i, MatchmakingStage.UserBeatmapSelect, "Beatmap Selection")); + flow.Add(new StageSegment(i, MatchmakingStage.GameplayWarmupTime, "Get Ready")); + flow.Add(new StageSegment(i, MatchmakingStage.ResultsDisplaying, "Results")); + } + + flow.Add(new StageSegment(null, MatchmakingStage.Ended, "Match End")); + } + + protected override void Update() + { + base.Update(); + var bubble = flow.OfType().FirstOrDefault(b => b.Active); + + if (bubble != null) + { + scroll.ScrollTo(flow.Padding.Left + bubble.X + bubble.Progress * bubble.DrawWidth - scroll.DrawWidth / 2); + roundDisplay.Round = bubble.Round; + } + } + + private partial class StageScrollContainer : OsuScrollContainer + { + public override bool HandlePositionalInput => false; + public override bool HandleNonPositionalInput => false; + + public StageScrollContainer() + : base(Direction.Horizontal) + { + } + } + + private partial class CurrentRoundDisplay : CompositeDrawable + { + private OsuSpriteText text = null!; + + private Circle innerCircle = null!; + private CircularProgress progress = null!; + + private Sample? swishSample; + private Sample? swooshSample; + private Sample? roundUpSample; + private SampleChannel? swishChannel; + private SampleChannel? swooshChannel; + private SampleChannel? roundUpChannel; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colours, AudioManager audio) + { + Size = new Vector2(76); + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = ColourInfo.GradientVertical( + colours.Dark2, + colours.Dark4 + ), + RelativeSizeAxes = Axes.Both, + }, + progress = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourInfo.GradientVertical( + colours.Light1, + colours.Dark2 + ), + InnerRadius = 0.1f, + RelativeSizeAxes = Axes.Both, + }, + innerCircle = new Circle + { + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourInfo.GradientVertical( + colours.Dark1, + colours.Dark2 + ), + Scale = new Vector2(0.9f), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Y = 10, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Caption2, + Text = "Round", + }, + text = new OsuSpriteText + { + Font = OsuFont.Style.Heading1, + Position = new Vector2(-8, -3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "1" + }, + new OsuSpriteText + { + Font = OsuFont.Style.Heading2, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 4, + Text = "/" + }, + new OsuSpriteText + { + Font = OsuFont.Style.Heading1, + Position = new Vector2(10, 11), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = $"{round_count}" + }, + }; + + swishSample = audio.Samples.Get(@"UI/overlay-pop-in"); + swooshSample = audio.Samples.Get(@"UI/overlay-big-pop-out"); + roundUpSample = audio.Samples.Get(@"Multiplayer/Matchmaking/round-up"); + } + + private int round; + + public int? Round + { + set + { + value ??= 1; + + if (round == value) + return; + + round = value.Value; + + this.ScaleTo(6, 1000, Easing.OutPow10) + .MoveToY(-300, 1000, Easing.OutPow10) + .Then() + .MoveToY(0, 500, Easing.InQuart) + .ScaleTo(1, 500, Easing.InQuart); + + swishChannel = swishSample?.GetChannel(); + + if (swishChannel != null) + { + swishChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; + swishChannel?.Play(); + } + + Scheduler.AddDelayed(() => + { + swooshChannel = swooshSample?.GetChannel(); + + if (swooshChannel == null) return; + + swooshChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; + swooshChannel?.Play(); + }, 1250); + + Scheduler.AddDelayed(() => + { + progress.ProgressTo((float)round / round_count, 500, Easing.InOutQuart); + + Scheduler.AddDelayed(() => + { + roundUpChannel = roundUpSample?.GetChannel(); + + if (roundUpChannel != null) + { + roundUpChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; + roundUpChannel.Frequency.Value = 1f + round * 0.05f; + roundUpChannel?.Play(); + } + + innerCircle + .FadeTo(1, 250, Easing.OutQuint) + .Then() + .FadeTo(0.2f, 5000, Easing.OutQuint); + + text.Text = $"{round}"; + }, 150); + }, 250); + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/CloudVisualisation.cs similarity index 92% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/CloudVisualisation.cs index 3fab5ab207..33ed21f3db 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/CloudVisualisation.cs @@ -11,12 +11,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Screens.Ranking; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingCloud : CompositeDrawable + /// + /// A visualisation at the top level of matchmaking which shows the overall system status. + /// This is intended to be something which users can watch while idle, for fun or otherwise. + /// + public partial class CloudVisualisation : CompositeDrawable { private APIUser[] users = []; private Container usersContainer = null!; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs similarity index 52% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs index 43e6acfaf7..71f976329f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingPoolSelector.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/PoolSelector.cs @@ -1,31 +1,35 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Matchmaking; +using osu.Game.Overlays; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; +using osuTK.Input; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingPoolSelector : CompositeDrawable + public partial class PoolSelector : CompositeDrawable { - private const float icon_size = 36; + private const float icon_size = 48; public readonly Bindable AvailablePools = new Bindable(); public readonly Bindable SelectedPool = new Bindable(); private FillFlowContainer poolFlow = null!; - public MatchmakingPoolSelector() + public PoolSelector() { AutoSizeAxes = Axes.Both; } @@ -35,9 +39,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { InternalChild = poolFlow = new FillFlowContainer { - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.X, + Height = icon_size * 1.2f, Direction = FillDirection.Horizontal, - Spacing = new Vector2(3) + Spacing = new Vector2(5), }; } @@ -48,13 +53,47 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking AvailablePools.BindValueChanged(pools => { poolFlow.Clear(); + foreach (var p in pools.NewValue) - poolFlow.Add(new SelectorButton(p) { SelectedPool = { BindTarget = SelectedPool } }); + { + poolFlow.Add(new SelectorButton(p) + { + SelectedPool = { BindTarget = SelectedPool }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } }, true); } - private partial class SelectorButton : CompositeDrawable + protected override bool OnKeyDown(KeyDownEvent e) { + var currentSelection = poolFlow.SingleOrDefault(b => b.IsSelected); + + switch (e.Key) + { + case Key.Left: + { + var next = poolFlow.Reverse().SkipWhile(b => b != currentSelection).Skip(1).FirstOrDefault(); + (next ?? poolFlow.Last()).TriggerClickWithSound(); + return true; + } + + case Key.Right: + { + var next = poolFlow.SkipWhile(b => b != currentSelection).Skip(1).FirstOrDefault(); + (next ?? poolFlow.First()).TriggerClickWithSound(); + return true; + } + } + + return false; + } + + private partial class SelectorButton : OsuAnimatedButton + { + public bool IsSelected => SelectedPool.Value?.Equals(pool) == true; + public readonly Bindable SelectedPool = new Bindable(); [Resolved] @@ -63,7 +102,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private readonly MatchmakingPool pool; private Drawable iconSprite = null!; + private Box flashLayer = null!; + public SelectorButton(MatchmakingPool pool) + : base(HoverSampleSet.ButtonSidebar) { this.pool = pool; @@ -71,14 +113,39 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - InternalChild = new OsuAnimatedButton + Content.Masking = true; + Content.CornerRadius = 20; + Content.CornerExponent = 10; + + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = iconSprite = createIcon(), - Action = () => SelectedPool.Value = pool + new Box + { + Colour = colourProvider.Background2, + Alpha = 0.4f, + RelativeSizeAxes = Axes.Both, + }, + flashLayer = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new[] + { + iconSprite = createIcon(), + } + }, }; + + Action = () => SelectedPool.Value = pool; } protected override void LoadComplete() @@ -89,12 +156,34 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking FinishTransforms(true); } + protected override bool OnHover(HoverEvent e) + { + if (!IsSelected) + flashLayer.FadeTo(0.05f, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (!IsSelected) + flashLayer.FadeTo(0f, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + private void onSelectionChanged(ValueChangedEvent selection) { - if (selection.NewValue?.Equals(pool) == true) + if (IsSelected) + { + this.ScaleTo(1.2f, 200, Easing.OutQuint); iconSprite.FadeColour(Color4.Gold, 100, Easing.OutQuint); + flashLayer.FadeTo(0.1f, 200, Easing.OutQuint); + } else + { + this.ScaleTo(1f, 200, Easing.OutQuint); iconSprite.FadeColour(OsuColour.Gray(0.5f), 100); + flashLayer.FadeOut(200, Easing.OutQuint); + } } private Drawable createIcon() @@ -108,7 +197,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking if (pool.Variant == 0) return icon; - return new BufferedContainer + return new BufferedContainer(pixelSnapping: true) { RelativeSizeAxes = Axes.Both, Children = new[] @@ -118,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Size = new Vector2(14, 10), + Size = icon_size * new Vector2(0.4f, 0.28f), Children = new Drawable[] { new Box @@ -130,7 +219,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = $"{pool.Variant}K", - Font = OsuFont.Default.With(size: 8, fixedWidth: true, weight: FontWeight.Bold), + Font = OsuFont.Default.With(size: icon_size * 0.3f, weight: FontWeight.Bold), UseFullGlyphHeight = false, Blending = new BlendingParameters { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs similarity index 79% rename from osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs index 1a426501d7..40ac0e5777 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingController.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/QueueController.cs @@ -10,13 +10,21 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; +using osu.Game.Screens.OnlinePlay.Matchmaking.Intro; -namespace osu.Game.Screens.OnlinePlay.Matchmaking +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingController : Component + /// + /// A component which acts as a bridge between the online component (ie ) + /// and the visual representations and flow of queueing for matchmaking. + /// + /// Includes support for deferring to background. + /// + /// + /// This is initialised and cached in the but can be used throughout the system via DI. + public partial class QueueController : Component { - public readonly Bindable CurrentState = new Bindable(); + public readonly Bindable CurrentState = new Bindable(); [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -63,12 +71,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onRoomUpdated() => Scheduler.Add(() => { if (client.Room == null) - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.Idle; }); private void onMatchmakingQueueJoined() => Scheduler.Add(() => { - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Queueing; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.Queueing; if (isBackgrounded) { @@ -79,15 +87,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void onMatchmakingQueueLeft() => Scheduler.Add(() => { - if (CurrentState.Value != MatchmakingQueueScreen.MatchmakingScreenState.InRoom) - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.Idle; + if (CurrentState.Value != ScreenQueue.MatchmakingScreenState.InRoom) + CurrentState.Value = ScreenQueue.MatchmakingScreenState.Idle; closeNotifications(); }); private void onMatchmakingRoomInvited() => Scheduler.Add(() => { - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.PendingAccept; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.PendingAccept; if (backgroundNotification != null) { @@ -101,7 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking client.JoinRoom(new Room { RoomID = roomId }, password) .FireAndForget(() => Scheduler.Add(() => { - CurrentState.Value = MatchmakingQueueScreen.MatchmakingScreenState.InRoom; + CurrentState.Value = ScreenQueue.MatchmakingScreenState.InRoom; })); }); @@ -118,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking CompletionClickAction = () => { client.MatchmakingAcceptInvitation().FireAndForget(); - performer?.PerformFromScreen(s => s.Push(new MatchmakingIntroScreen())); + performer?.PerformFromScreen(s => s.Push(new IntroScreen())); closeNotifications(); return true; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs similarity index 85% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs index 8ec1505c1b..8eaa280794 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingQueueScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs @@ -15,11 +15,15 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking; @@ -27,18 +31,22 @@ using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue { - public partial class MatchmakingQueueScreen : OsuScreen + /// + /// The initial screen that users arrive at when preparing for a quick play session. + /// + public partial class ScreenQueue : OsuScreen { public override bool ShowFooter => true; private Container mainContent = null!; private MatchmakingScreenState state; - private MatchmakingCloud cloud = null!; + private CloudVisualisation cloud = null!; [Resolved] private IAPIProvider api { get; set; } = null!; @@ -56,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private IDialogOverlay dialogOverlay { get; set; } = null!; [Resolved] - private MatchmakingController controller { get; set; } = null!; + private QueueController controller { get; set; } = null!; [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -64,6 +72,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens [Resolved] private IBindable ruleset { get; set; } = null!; + [Resolved] + private MusicController musicController { get; set; } = null!; + private readonly IBindable currentState = new Bindable(); private readonly Bindable availablePools = new Bindable(); @@ -71,15 +82,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens private CancellationTokenSource userLookupCancellation = new CancellationTokenSource(); + private Sample? enqueueSample; + private Sample? waitingLoopSample; private Sample? matchFoundSample; + private SampleChannel? waitingLoopChannel; + private ScheduledDelegate? startLoopPlaybackDelegate; + protected override void LoadComplete() { base.LoadComplete(); InternalChildren = new Drawable[] { - cloud = new MatchmakingCloud + cloud = new CloudVisualisation { Y = -100, Anchor = Anchor.Centre, @@ -148,7 +164,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens [BackgroundDependencyLoader] private void load(AudioManager audio) { - matchFoundSample = audio.Samples.Get(@"Multiplayer/matchmaking/match-found"); + enqueueSample = audio.Samples.Get(@"Multiplayer/Matchmaking/enqueue"); + waitingLoopSample = audio.Samples.Get(@"Multiplayer/Matchmaking/waiting-loop"); + matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found"); } private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() => @@ -240,6 +258,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens mainContent.FadeInFromZero(500, Easing.OutQuint); mainContent.Clear(); + startLoopPlaybackDelegate?.Cancel(); + stopWaitingLoopPlayback(); + switch (newState) { case MatchmakingScreenState.Idle: @@ -252,7 +273,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Spacing = new Vector2(10), Children = new Drawable[] { - new MatchmakingPoolSelector + new PoolSelector { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -327,6 +348,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens sendToBackgroundButton.Enabled.Value = true; sendToBackgroundButton.TooltipText = "You will receive a notification when your game is ready. Make sure to watch out for it!"; }, 5000); + + enqueueSample?.Play(); + startLoopPlaybackDelegate = Scheduler.AddDelayed(startWaitingLoopPlayback, 2000); break; case MatchmakingScreenState.PendingAccept: @@ -346,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens Text = "Found a match!", Font = OsuFont.GetFont(size: 32, weight: FontWeight.Regular, typeface: Typeface.TorusAlternate), }, - new ShearedButton(200) + new SelectionButton(200) { DarkerColour = colours.YellowDark, LighterColour = colours.YellowLight, @@ -362,6 +386,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens } }; matchFoundSample?.Play(); + musicController.DuckMomentarily(1250); break; case MatchmakingScreenState.AcceptedWaitingForRoom: @@ -387,6 +412,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens }, } }; + + startWaitingLoopPlayback(); break; case MatchmakingScreenState.InRoom: @@ -411,7 +438,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens }; using (BeginDelayedSequence(2000)) - Schedule(() => this.Push(new MatchmakingScreen(client.Room!))); + Schedule(() => this.Push(new ScreenMatchmaking(client.Room!))); break; default: @@ -423,6 +450,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens { base.Dispose(isDisposing); + stopWaitingLoopPlayback(); + if (client.IsNotNull()) client.MatchmakingLobbyStatusChanged -= onMatchmakingLobbyStatusChanged; } @@ -436,7 +465,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens InRoom } - private partial class BeginQueueingButton : ShearedButton + private void startWaitingLoopPlayback() + { + stopWaitingLoopPlayback(); + + waitingLoopChannel = waitingLoopSample?.GetChannel(); + if (waitingLoopChannel == null) + return; + + waitingLoopChannel.Looping = true; + waitingLoopChannel?.Play(); + } + + private void stopWaitingLoopPlayback() + { + waitingLoopChannel?.Stop(); + waitingLoopChannel?.Dispose(); + } + + private partial class BeginQueueingButton : SelectionButton { public readonly IBindable SelectedPool = new Bindable(); @@ -452,5 +499,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens SelectedPool.BindValueChanged(p => Enabled.Value = p.NewValue != null, true); } } + + private partial class SelectionButton : ShearedButton, IKeyBindingHandler + { + public SelectionButton(float? width = null, float height = DEFAULT_HEIGHT) + : base(width, height) + { + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Select && !e.Repeat) + { + TriggerClickWithSound(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs deleted file mode 100644 index e67e2a520a..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/IdleScreen.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Screens; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle -{ - public partial class IdleScreen : MatchmakingSubScreen - { - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new PlayerPanelList - { - RelativeSizeAxes = Axes.Both - }; - } - - public override void OnEntering(ScreenTransitionEvent e) - { - base.OnEntering(e); - this.MoveToX(0); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs deleted file mode 100644 index eaddb0f2e4..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanel.cs +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Users; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle -{ - public partial class PlayerPanel : UserPanel - { - public readonly MultiplayerRoomUser RoomUser; - - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - private OsuSpriteText rankText = null!; - private OsuSpriteText scoreText = null!; - - private MatchmakingAvatar avatar = null!; - private OsuSpriteText username = null!; - - private Container mainContent = null!; - - public bool Horizontal - { - get => horizontal; - set - { - horizontal = value; - if (IsLoaded) - updateLayout(false); - } - } - - private bool horizontal; - - public PlayerPanel(MultiplayerRoomUser user) - : base(user.User!) - { - RoomUser = user; - } - - [BackgroundDependencyLoader] - private void load() - { - Masking = true; - CornerRadius = 10; - - Add(mainContent = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - avatar = new MatchmakingAvatar(User, isOwnUser: User.Id == api.LocalUser.Value.Id) - { - Anchor = Anchor.TopLeft, - Origin = Anchor.Centre, - Size = new Vector2(80), - }, - rankText = new OsuSpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomCentre, - Blending = BlendingParameters.Additive, - Margin = new MarginPadding(4), - Font = OsuFont.Style.Title.With(size: 70), - }, - username = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = User.Username, - Font = OsuFont.Style.Heading1, - }, - scoreText = new OsuSpriteText - { - Margin = new MarginPadding(10), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = OsuFont.Style.Heading2, - Text = "0 pts" - } - } - }); - } - - protected override Drawable CreateLayout() => Empty(); - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateLayout(true); - - client.MatchRoomStateChanged += onRoomStateChanged; - onRoomStateChanged(client.Room!.MatchState); - - avatar.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - - rankText.Hide(); - scoreText.Hide(); - username.Hide(); - - using (BeginDelayedSequence(100)) - { - username.FadeInFromZero(600); - - using (BeginDelayedSequence(100)) - { - scoreText.FadeInFromZero(600); - - using (BeginDelayedSequence(100)) - { - rankText.FadeTo(0.6f, 600); - } - } - } - } - - private Vector2 avatarPosition => horizontal ? new Vector2(50) : new Vector2(75, 50); - - private void updateLayout(bool instant) - { - double duration = instant ? 0 : 1000; - - avatar.MoveTo(avatarPosition, duration, Easing.OutPow10); - this.ResizeTo(horizontal ? new Vector2(250, 100) : new Vector2(150, 200), duration, Easing.OutPow10); - - rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10); - username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10); - scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10); - } - - protected override bool OnHover(HoverEvent e) - { - this.ScaleTo(1.02f, 1000, Easing.OutQuint); - mainContent.ScaleTo(1.03f, 1000, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - this.ScaleTo(1f, 500, Easing.OutQuint); - mainContent.ScaleTo(1, 500, Easing.OutQuint); - - mainContent.MoveTo(Vector2.Zero, 500, Easing.OutElasticHalf); - avatar.MoveTo(avatarPosition, 1500, Easing.OutElastic); - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - var offset = (avatar.ToLocalSpace(e.ScreenSpaceMousePosition) - avatar.DrawSize / 2) * 0.02f; - - mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutQuint); - avatar.MoveTo(avatarPosition + offset, 400, Easing.OutQuint); - return base.OnMouseMove(e); - } - - private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore)) - return; - - rankText.Text = $"#{userScore.Placement}"; - scoreText.Text = $"{userScore.Points} pts"; - }); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.MatchRoomStateChanged -= onRoomStateChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs deleted file mode 100644 index aa294f5bd3..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Idle/PlayerPanelList.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle -{ - public partial class PlayerPanelList : CompositeDrawable - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - public bool Horizontal { get; init; } - - private FillFlowContainer panels = null!; - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = panels = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Spacing = new Vector2(20, 5), - LayoutEasing = Easing.InOutQuint, - LayoutDuration = 500 - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - client.MatchRoomStateChanged += onRoomStateChanged; - client.UserJoined += onUserJoined; - client.UserLeft += onUserLeft; - - if (client.Room != null) - { - onRoomStateChanged(client.Room.MatchState); - foreach (var user in client.Room.Users) - onUserJoined(user); - } - } - - private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => - { - panels.Add(new PlayerPanel(user) - { - Horizontal = Horizontal, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - }); - - private void onUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => - { - panels.Single(p => p.RoomUser.Equals(user)).Expire(); - }); - - private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - foreach (var panel in panels) - { - if (matchmakingState.Users.UserDictionary.TryGetValue(panel.User.Id, out MatchmakingUser? user)) - panels.SetLayoutPosition(panel, user.Placement); - else - panels.SetLayoutPosition(panel, float.MaxValue); - } - }); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - { - client.MatchRoomStateChanged -= onRoomStateChanged; - client.UserJoined -= onUserJoined; - client.UserLeft -= onUserLeft; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs deleted file mode 100644 index cba5c89385..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/MatchmakingScreenStack.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Screens; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens -{ - public partial class MatchmakingScreenStack : CompositeDrawable - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private ScreenStack screenStack = null!; - - [BackgroundDependencyLoader] - private void load() - { - RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding(10); - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, - Content = new Drawable[][] - { - [ - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.Absolute, 20), new Dimension(GridSizeMode.AutoSize) }, - Padding = new MarginPadding { Bottom = 20 }, - Content = new Drawable?[][] - { - [ - screenStack = new ScreenStack(), - null, - new PlayerPanelList - { - Horizontal = true, - RelativeSizeAxes = Axes.Y, - Width = 250, - Scale = new Vector2(0.8f), - } - ] - } - } - ], - [ - new StageDisplay - { - RelativeSizeAxes = Axes.X - } - ] - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - screenStack.Push(new IdleScreen()); - - client.MatchRoomStateChanged += onMatchRoomStateChanged; - onMatchRoomStateChanged(client.Room!.MatchState); - } - - private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - switch (matchmakingState.Stage) - { - case MatchmakingStage.WaitingForClientsJoin: - case MatchmakingStage.RoundWarmupTime: - while (screenStack.CurrentScreen is not IdleScreen) - screenStack.Exit(); - break; - - case MatchmakingStage.UserBeatmapSelect: - screenStack.Push(new PickScreen()); - break; - - case MatchmakingStage.ServerBeatmapFinalised: - Debug.Assert(screenStack.CurrentScreen is PickScreen); - ((PickScreen)screenStack.CurrentScreen).RollFinalBeatmap(matchmakingState.CandidateItems, matchmakingState.CandidateItem); - break; - - case MatchmakingStage.ResultsDisplaying: - screenStack.Push(new RoundResultsScreen()); - break; - - case MatchmakingStage.Ended: - screenStack.Push(new ResultsScreen()); - break; - } - }); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.MatchRoomStateChanged -= onMatchRoomStateChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs deleted file mode 100644 index d3e5249c73..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapPanel.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick -{ - public partial class BeatmapPanel : CompositeDrawable - { - public static readonly Vector2 SIZE = new Vector2(300, 70); - - public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; - - public APIBeatmap? Beatmap - { - get => beatmap; - set - { - if (beatmap?.OnlineID == value?.OnlineID) - return; - - beatmap = value; - - if (IsLoaded) - updateContent(); - } - } - - private APIBeatmap? beatmap; - - private Container content = null!; - private UpdateableOnlineBeatmapSetCover cover = null!; - - public BeatmapPanel(APIBeatmap? beatmap = null) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Masking = true; - CornerRadius = 6; - - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3 - }, - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.1f), Color4.White.Opacity(0.3f)) - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 6, - BorderThickness = 2, - BorderColour = ColourInfo.GradientVertical(colourProvider.Background1, colourProvider.Background1.Opacity(0)), - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - }, - OverlayLayer, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateContent(); - FinishTransforms(true); - } - - private void updateContent() - { - foreach (var child in content.Children) - child.FadeOut(300).Expire(); - - cover.OnlineInfo = beatmap?.BeatmapSet; - - if (beatmap != null) - { - var panelContent = new BeatmapPanelContent(beatmap) - { - RelativeSizeAxes = Axes.Both, - }; - - content.Add(panelContent); - - panelContent.FadeInFromZero(300); - } - } - - private partial class BeatmapPanelContent : CompositeDrawable - { - private readonly APIBeatmap beatmap; - - public BeatmapPanelContent(APIBeatmap beatmap) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Horizontal = 12 }, - Children = new Drawable[] - { - new TruncatingSpriteText - { - Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), - Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), - Shadow = false, - RelativeSizeAxes = Axes.X, - }, - new TextFlowContainer(s => - { - s.Shadow = false; - s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); - }).With(d => - { - d.RelativeSizeAxes = Axes.X; - d.AutoSizeAxes = Axes.Y; - d.AddText("by "); - d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); - }), - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 6 }, - Spacing = new Vector2(4), - Children = new Drawable[] - { - new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Shadow = false, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - }, - }; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs deleted file mode 100644 index 2a15201d11..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionOverlay.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick -{ - public partial class BeatmapSelectionOverlay : CompositeDrawable - { - private readonly Dictionary avatars = new Dictionary(); - - private readonly Container avatarContainer; - - private Sample? userAddedSample; - private double? lastSamplePlayback; - - public new Axes AutoSizeAxes - { - get => base.AutoSizeAxes; - set => base.AutoSizeAxes = value; - } - - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - - public BeatmapSelectionOverlay() - { - InternalChild = avatarContainer = new Container(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatarContainer.AutoSizeAxes = AutoSizeAxes; - avatarContainer.RelativeSizeAxes = RelativeSizeAxes; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); - } - - public bool AddUser(APIUser user, bool isOwnUser) - { - if (avatars.ContainsKey(user.Id)) - return false; - - var avatar = new SelectionAvatar(user, isOwnUser) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }; - - avatarContainer.Add(avatars[user.Id] = avatar); - - if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) - { - userAddedSample?.Play(); - lastSamplePlayback = Time.Current; - } - - updateLayout(); - - avatar.FinishTransforms(); - - return true; - } - - public bool RemoveUser(int id) - { - if (!avatars.Remove(id, out var avatar)) - return false; - - avatar.PopOutAndExpire(); - avatarContainer.ChangeChildDepth(avatar, float.MaxValue); - - updateLayout(); - - return true; - } - - private void updateLayout() - { - const double stagger = 30; - const float spacing = 4; - - double delay = 0; - float x = 0; - - for (int i = avatarContainer.Count - 1; i >= 0; i--) - { - var avatar = avatarContainer[i]; - - if (avatar.Expired) - continue; - - avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); - - x -= avatar.LayoutSize.X + spacing; - - delay += stagger; - } - } - - public partial class SelectionAvatar : CompositeDrawable - { - public bool Expired { get; private set; } - - private readonly Container content; - - public SelectionAvatar(APIUser user, bool isOwnUser) - { - Size = new Vector2(30); - - InternalChildren = new Drawable[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = new MatchmakingAvatar(user, isOwnUser) - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - content.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - } - - public void PopOutAndExpire() - { - content.ScaleTo(0, 400, Easing.OutExpo); - - this.FadeOut(100).Expire(); - Expired = true; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs deleted file mode 100644 index 029bf48e30..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionPanel.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Database; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick -{ - public partial class BeatmapSelectionPanel : Container - { - private const float corner_radius = 6; - private const float border_width = 3; - - public readonly MultiplayerPlaylistItem Item; - - private readonly Container scaleContainer; - private readonly BeatmapPanel beatmapPanel; - private readonly BeatmapSelectionOverlay selectionOverlay; - private readonly Container border; - private readonly Box flash; - private readonly Container shadow; - - public bool AllowSelection; - - public Action? Action; - - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - - public override bool PropagatePositionalInputSubTree => AllowSelection; - - public BeatmapSelectionPanel(MultiplayerPlaylistItem item) - { - Item = item; - Size = BeatmapPanel.SIZE; - - InternalChildren = new Drawable[] - { - scaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - shadow = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(4), - Y = 8, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 7, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.15f, - } - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-border_width), - Child = border = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius + border_width, - Alpha = 0, - Child = new Box { RelativeSizeAxes = Axes.Both }, - } - }, - beatmapPanel = new BeatmapPanel - { - RelativeSizeAxes = Axes.Both, - OverlayLayer = - { - Children = new[] - { - flash = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - } - } - }, - selectionOverlay = new BeatmapSelectionOverlay - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10 }, - Origin = Anchor.CentreLeft, - }, - } - }, - new HoverClickSounds(), - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => - { - var beatmap = b.GetResultSafely()!; - - beatmap.StarRating = Item.StarRating; - - beatmapPanel.Beatmap = beatmap; - })); - } - - public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); - - public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId); - - public bool RemoveUser(APIUser user) => RemoveUser(user.Id); - - protected override bool OnHover(HoverEvent e) - { - flash.FadeTo(0.2f, 50) - .Then() - .FadeTo(0.1f, 300); - - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - - flash.FadeOut(200); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button == MouseButton.Left) - { - scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); - shadow.MoveToY(4, 400, Easing.OutExpo) - .TransformTo(nameof(Padding), new MarginPadding(2), 400, Easing.OutExpo); - return true; - } - - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - base.OnMouseUp(e); - - if (e.Button == MouseButton.Left) - { - scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); - shadow.MoveToY(8, 500, Easing.OutElasticHalf) - .TransformTo(nameof(Padding), new MarginPadding(4), 400, Easing.OutExpo); - } - } - - protected override bool OnClick(ClickEvent e) - { - Action?.Invoke(Item); - - flash.FadeTo(0.5f, 50) - .Then() - .FadeTo(0.1f, 400); - - return true; - } - - public void ShowBorder() => border.Show(); - - public void HideBorder() => border.Hide(); - - public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) - { - scaleContainer - .FadeOut() - .MoveToY(distance) - .Delay(delay) - .FadeIn(duration / 2) - .MoveToY(0, duration, Easing.OutExpo); - } - - public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) - { - AllowSelection = false; - - scaleContainer.Delay(delay) - .ScaleTo(0, duration, easing) - .FadeOut(duration); - - this.Delay(delay + duration).FadeOut().Expire(); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs deleted file mode 100644 index 3fe4cc6d7a..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/ResultsScreen.cs +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Idle; -using osu.Game.Utils; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results -{ - public partial class ResultsScreen : MatchmakingSubScreen - { - private const float grid_spacing = 5; - - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private OsuSpriteText placementText = null!; - private FillFlowContainer userStatistics = null!; - private FillFlowContainer roomStatistics = null!; - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = - [ - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, grid_spacing), - new Dimension(), - new Dimension(GridSizeMode.Absolute, grid_spacing), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 75) - ], - Content = new Drawable[]?[] - { - [ - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(grid_spacing), - Children = new[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Placement", - Font = OsuFont.Default.With(size: 12) - }, - placementText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.Default.With(size: 72), - UseFullGlyphHeight = false - } - } - } - ], - null, - [ - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = - [ - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, grid_spacing), - new Dimension() - ], - Content = new Drawable?[][] - { - [ - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(grid_spacing), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Breakdown", - Font = OsuFont.Default.With(size: 12) - }, - userStatistics = new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(grid_spacing) - } - } - }, - null, - new PlayerPanelList - { - RelativeSizeAxes = Axes.Both - } - ] - } - } - ], - null, - [ - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(grid_spacing), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Statistics", - Font = OsuFont.Default.With(size: 12) - }, - roomStatistics = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(grid_spacing) - } - } - }, - ], - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - client.MatchRoomStateChanged += onRoomStateChanged; - - onRoomStateChanged(client.Room?.MatchState); - } - - private void onRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState || matchmakingState.Stage != MatchmakingStage.Ended) - return; - - populateUserStatistics(matchmakingState); - populateRoomStatistics(matchmakingState); - }); - - private void populateUserStatistics(MatchmakingRoomState state) - { - userStatistics.Clear(); - - if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0) - { - placementText.Text = "-"; - addStatistic("No rounds played"); - return; - } - - int overallPlacement = state.Users[client.LocalUser!.UserID].Placement; - int overallPoints = state.Users[client.LocalUser!.UserID].Points; - int bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.Min(r => r.Placement); - var accuracyPlacement = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average())) - .OrderByDescending(t => t.avgAcc) - .Select((t, i) => (info: t, index: i)) - .Single(t => t.info.user.UserId == client.LocalUser!.UserID); - - placementText.Text = $"#{state.Users[client.LocalUser!.UserID].Placement}"; - addStatistic($"#{overallPlacement} overall ({overallPoints}pts)"); - addStatistic($"#{bestPlacement} best placement"); - addStatistic($"#{accuracyPlacement.index + 1} accuracy ({accuracyPlacement.info.avgAcc.FormatAccuracy()})"); - - void addStatistic(string text) - { - userStatistics.Add(new UserStatisticPanel(text) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }); - } - } - - private void populateRoomStatistics(MatchmakingRoomState state) - { - roomStatistics.Clear(); - - long maxScore = long.MinValue; - int maxScoreUserId = 0; - - double maxAccuracy = double.MinValue; - int maxAccuracyUserId = 0; - - int maxCombo = int.MinValue; - int maxComboUserId = 0; - - long maxBonusScore = 0; - int maxBonusScoreUserId = 0; - - long largestScoreDifference = long.MinValue; - int largestScoreDifferenceUserId = 0; - - long smallestScoreDifference = long.MaxValue; - int smallestScoreDifferenceUserId = 0; - - for (int round = 1; round <= state.CurrentRound; round++) - { - long roundHighestScore = long.MinValue; - int roundHighestScoreUserId = 0; - - long roundLowestScore = long.MaxValue; - - foreach (MatchmakingUser user in state.Users) - { - if (!user.Rounds.RoundsDictionary.TryGetValue(round, out MatchmakingRound? mmRound)) - continue; - - if (mmRound.TotalScore > maxScore) - { - maxScore = mmRound.TotalScore; - maxScoreUserId = user.UserId; - } - - if (mmRound.Accuracy > maxAccuracy) - { - maxAccuracy = mmRound.Accuracy; - maxAccuracyUserId = user.UserId; - } - - if (mmRound.MaxCombo > maxCombo) - { - maxCombo = mmRound.MaxCombo; - maxComboUserId = user.UserId; - } - - if (mmRound.TotalScore > roundHighestScore) - { - roundHighestScore = mmRound.TotalScore; - roundHighestScoreUserId = user.UserId; - } - - if (mmRound.TotalScore < roundLowestScore) - roundLowestScore = mmRound.TotalScore; - } - - long roundScoreDifference = roundHighestScore - roundLowestScore; - - if (roundScoreDifference > 0 && roundScoreDifference > largestScoreDifference) - { - largestScoreDifference = roundScoreDifference; - largestScoreDifferenceUserId = roundHighestScoreUserId; - } - - if (roundScoreDifference > 0 && roundScoreDifference < smallestScoreDifference) - { - smallestScoreDifference = roundScoreDifference; - smallestScoreDifferenceUserId = roundHighestScoreUserId; - } - } - - foreach (MatchmakingUser user in state.Users) - { - int userBonusScore = 0; - - foreach (MatchmakingRound round in user.Rounds) - { - userBonusScore += round.Statistics.TryGetValue(HitResult.LargeBonus, out int bonus) ? bonus * 5 : 0; - userBonusScore += round.Statistics.TryGetValue(HitResult.SmallBonus, out bonus) ? bonus : 0; - } - - if (userBonusScore > maxBonusScore) - { - maxBonusScore = userBonusScore; - maxBonusScoreUserId = user.UserId; - } - } - - // Highest score - highest score across all rounds. - addStatistic(maxScoreUserId, "Highest score"); - - // Most accurate - highest accuracy across all rounds. - addStatistic(maxAccuracyUserId, "Most accurate"); - - // Most combo - highest combo across all rounds. - addStatistic(maxComboUserId, "Most combo"); - - // Most bonus - most bonus score across all rounds. - if (maxBonusScoreUserId > 0) - addStatistic(maxBonusScoreUserId, "Most bonus"); - - // Most clutch - smallest victory in any round. - if (smallestScoreDifferenceUserId > 0) - addStatistic(smallestScoreDifferenceUserId, "Most clutch"); - - // Best finish - largest victory in any round. - if (largestScoreDifferenceUserId > 0) - addStatistic(largestScoreDifferenceUserId, "Best finish"); - - void addStatistic(int userId, string text) - { - roomStatistics.Add(new RoomStatisticPanel(text, userId) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.MatchRoomStateChanged -= onRoomStateChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs deleted file mode 100644 index 5988a73ef8..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/RoomStatisticPanel.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Database; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results -{ - public partial class RoomStatisticPanel : CompositeDrawable - { - private readonly Color4 backgroundColour = Color4.SaddleBrown; - - private readonly string text; - private readonly int userId; - - public RoomStatisticPanel(string text, int userId) - { - this.text = text; - this.userId = userId; - - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(UserLookupCache userLookupCache) - { - // Should be cached by this point. - APIUser? user = userLookupCache.GetUserAsync(userId).GetResultSafely(); - - InternalChild = new CircularContainer - { - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - new OsuSpriteText - { - Margin = new MarginPadding(10), - Text = $"{text}: {user?.Username}" - } - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs deleted file mode 100644 index 3a39fc714d..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Results/UserStatisticPanel.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Sprites; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Results -{ - public partial class UserStatisticPanel : CompositeDrawable - { - private readonly Color4 backgroundColour = Color4.SaddleBrown; - - private readonly string text; - - public UserStatisticPanel(string text) - { - this.text = text; - - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new CircularContainer - { - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - new OsuSpriteText - { - Margin = new MarginPadding(10), - Text = text - } - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs deleted file mode 100644 index ad30c19c02..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/RoundResults/RoundResultsScorePanel.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.RoundResults -{ - internal partial class RoundResultsScorePanel : CompositeDrawable - { - public RoundResultsScorePanel(ScoreInfo score) - { - AutoSizeAxes = Axes.Both; - InternalChild = new InstantSizingScorePanel(score); - } - - public override bool PropagateNonPositionalInputSubTree => false; - public override bool PropagatePositionalInputSubTree => false; - - private partial class InstantSizingScorePanel : ScorePanel - { - public InstantSizingScorePanel(ScoreInfo score, bool isNewLocalScore = false) - : base(score, isNewLocalScore) - { - } - - protected override void LoadComplete() - { - base.LoadComplete(); - FinishTransforms(true); - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs deleted file mode 100644 index 2ebd3376d3..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Matchmaking; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osuTK.Graphics; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking -{ - internal partial class StageBubble : CompositeDrawable - { - private readonly Color4 backgroundColour = Color4.Salmon; - - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private readonly MatchmakingStage stage; - private readonly LocalisableString displayText; - private Drawable progressBar = null!; - - private DateTimeOffset countdownStartTime; - private DateTimeOffset countdownEndTime; - - private Sample? stageProgressSample; - private double? lastSamplePlayback; - - public StageBubble(MatchmakingStage stage, LocalisableString displayText) - { - this.stage = stage; - this.displayText = displayText; - - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - InternalChild = new CircularContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour.Darken(0.2f) - }, - progressBar = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = displayText, - Padding = new MarginPadding(10) - } - } - }; - - stageProgressSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - client.MatchRoomStateChanged += onMatchRoomStateChanged; - client.CountdownStarted += onCountdownStarted; - client.CountdownStopped += onCountdownStopped; - - if (client.Room != null) - { - onMatchRoomStateChanged(client.Room.MatchState); - foreach (var countdown in client.Room.ActiveCountdowns) - onCountdownStarted(countdown); - } - } - - protected override void Update() - { - base.Update(); - - TimeSpan duration = countdownEndTime - countdownStartTime; - - if (duration.TotalMilliseconds == 0) - progressBar.Width = 0; - else - { - TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; - progressBar.Width = (float)(elapsed.TotalMilliseconds / duration.TotalMilliseconds); - - bool enoughTimeElapsed = lastSamplePlayback == null || Time.Current - lastSamplePlayback >= 1000f; - if (elapsed.TotalMilliseconds < 1000f || !enoughTimeElapsed || elapsed.TotalMilliseconds >= duration.TotalMilliseconds) - return; - - stageProgressSample?.Play(); - lastSamplePlayback = Time.Current; - } - } - - private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - if (matchmakingState.Stage == MatchmakingStage.RoundWarmupTime) - { - countdownStartTime = countdownEndTime = DateTimeOffset.Now; - activate(); - } - }); - - private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() => - { - if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage) - return; - - countdownStartTime = DateTimeOffset.Now; - countdownEndTime = countdownStartTime + countdown.TimeRemaining; - activate(); - }); - - private void onCountdownStopped(MultiplayerCountdown countdown) => Scheduler.Add(() => - { - if (countdown is not MatchmakingStageCountdown matchmakingStatusCountdown || matchmakingStatusCountdown.Stage != stage) - return; - - countdownEndTime = DateTimeOffset.Now; - deactivate(); - }); - - private void activate() - { - this.FadeTo(1, 200); - } - - private void deactivate() - { - this.FadeTo(0.5f, 200); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - { - client.MatchRoomStateChanged -= onMatchRoomStateChanged; - client.CountdownStarted -= onCountdownStarted; - client.CountdownStopped -= onCountdownStopped; - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs deleted file mode 100644 index 1f426ec8e6..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageDisplay.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking -{ - public partial class StageDisplay : CompositeDrawable - { - public static readonly (MatchmakingStage status, LocalisableString text)[] DISPLAYED_STAGES = - [ - (MatchmakingStage.RoundWarmupTime, "Next Round"), - (MatchmakingStage.UserBeatmapSelect, "Beatmap Selection"), - (MatchmakingStage.GameplayWarmupTime, "Get Ready"), - (MatchmakingStage.ResultsDisplaying, "Results"), - (MatchmakingStage.Ended, "Match End") - ]; - - public StageDisplay() - { - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load() - { - List columnDimensions = new List(); - List columnContent = new List(); - - for (int i = 0; i < DISPLAYED_STAGES.Length; i++) - { - if (i > 0) - { - columnDimensions.Add(new Dimension(GridSizeMode.AutoSize)); - columnContent.Add(new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(16), - Icon = FontAwesome.Solid.ArrowRight, - Margin = new MarginPadding { Horizontal = 10 } - }); - } - - columnDimensions.Add(new Dimension()); - columnContent.Add(new StageBubble(DISPLAYED_STAGES[i].status, DISPLAYED_STAGES[i].text) - { - RelativeSizeAxes = Axes.X - }); - } - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - RowDimensions = - [ - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize) - ], - Content = new Drawable[][] - { - [ - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = columnDimensions.ToArray(), - RowDimensions = [new Dimension(GridSizeMode.AutoSize)], - Content = new[] { columnContent.ToArray() } - } - ], - [ - new StageText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - } - ] - } - }; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs deleted file mode 100644 index b47e135004..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking -{ - public partial class StageText : CompositeDrawable - { - [Resolved] - private MultiplayerClient client { get; set; } = null!; - - private OsuSpriteText text = null!; - - private Sample? textChangedSample; - private double? lastSamplePlayback; - - public StageText() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - InternalChild = text = new OsuSpriteText - { - Height = 16, - Font = OsuFont.Default, - AlwaysPresent = true, - }; - - textChangedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-message"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - client.MatchRoomStateChanged += onMatchRoomStateChanged; - onMatchRoomStateChanged(client.Room!.MatchState); - } - - private void onMatchRoomStateChanged(MatchRoomState? state) => Scheduler.Add(() => - { - if (state is not MatchmakingRoomState matchmakingState) - return; - - text.Text = getTextForStatus(matchmakingState.Stage); - - if (text.Text == string.Empty || (lastSamplePlayback != null && Time.Current - lastSamplePlayback < OsuGameBase.SAMPLE_DEBOUNCE_TIME)) - return; - - textChangedSample?.Play(); - lastSamplePlayback = Time.Current; - }); - - private LocalisableString getTextForStatus(MatchmakingStage status) - { - switch (status) - { - case MatchmakingStage.WaitingForClientsJoin: - return "Players are joining the match..."; - - case MatchmakingStage.WaitingForClientsBeatmapDownload: - return "Players are downloading the beatmap..."; - - case MatchmakingStage.Gameplay: - return "Game is in progress..."; - - case MatchmakingStage.Ended: - return "Thanks for playing! The match will close shortly."; - - default: - return string.Empty; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.MatchRoomStateChanged -= onMatchRoomStateChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 0dd547bfbb..e557c6821b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -25,6 +26,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments(); private readonly SpectatorPlayerClock spectatorPlayerClock; + // purposefully cached as empty - the multi spectator screen already has one leaderboard, on the left of all the player instances + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); + /// /// Creates a new . /// diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.Cell.cs similarity index 100% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.Cell.cs diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 08ea0d0a90..23264c4518 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -19,6 +19,7 @@ using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osuTK; +using CommonStrings = osu.Game.Localisation.CommonStrings; namespace osu.Game.Screens.Play { @@ -165,7 +166,7 @@ namespace osu.Game.Screens.Play }, new Drawable[] { - new MetadataLineLabel("Mapper"), + new MetadataLineLabel(CommonStrings.Mapper), new MetadataLineInfo(metadata.Author.Username) } } diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index ef453405b5..28c38dce2b 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Scoring; using osuTK; @@ -32,7 +34,7 @@ namespace osu.Game.Screens.Play.Break { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "current progress".ToUpperInvariant(), + Text = BreakInfoStrings.CurrentProgressTitle.ToUpper(), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15), }, new FillFlowContainer @@ -46,7 +48,7 @@ namespace osu.Game.Screens.Play.Break AccuracyDisplay = new PercentageBreakInfoLine(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy), // See https://github.com/ppy/osu/discussions/15185 // RankDisplay = new BreakInfoLine("Rank"), - GradeDisplay = new BreakInfoLine("Grade"), + GradeDisplay = new BreakInfoLine(BreakInfoStrings.ShowInfoGrade), }, } }, diff --git a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs index 3c79721590..9ff90f6fef 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AudioSettings.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerCheckbox beatmapHitsoundsToggle; public AudioSettings() - : base("Audio Settings") + : base(PlayerSettingsOverlayStrings.AudioSettingsTitle) { Children = new Drawable[] { diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 1387e01305..9c9f31e903 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play.PlayerSettings public partial class InputSettings : PlayerSettingsGroup { public InputSettings() - : base("Input Settings") + : base(PlayerSettingsOverlayStrings.InputSettingsTitle) { } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index b3d07421ed..be84d498fa 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private IconButton pausePlay = null!; public PlaybackSettings() - : base("playback") + : base(PlayerSettingsOverlayStrings.PlaybackTitle) { } @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { rateSlider = new PlayerSliderBar { - LabelText = "Playback speed", + LabelText = PlayerSettingsOverlayStrings.PlaybackSpeed, Current = UserPlaybackRate, }, multiplierText = new OsuSpriteText diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index 838106e198..0f9a00dfd2 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -2,13 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Overlays; namespace osu.Game.Screens.Play.PlayerSettings { public partial class PlayerSettingsGroup : SettingsToolboxGroup { - public PlayerSettingsGroup(string title) + public PlayerSettingsGroup(LocalisableString title) : base(title) { } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index ff857ddb12..6a09e627c1 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerCheckbox beatmapColorsToggle; public VisualSettings() - : base("Visual Settings") + : base(PlayerSettingsOverlayStrings.VisualSettingsTitle) { Children = new Drawable[] { diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs index 74ee7e1868..4e4d35bd30 100644 --- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs +++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -35,6 +36,9 @@ namespace osu.Game.Screens.Play if (beatmapId <= 0) return null; + if (Beatmap.Value.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified) + return null; + if (!Ruleset.Value.IsLegacyRuleset()) return null; diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index 03a41cde15..1e9222e40a 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -42,6 +42,9 @@ namespace osu.Game.Screens.Play if (beatmapId <= 0) return null; + if (Beatmap.Value.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified) + return null; + if (!Ruleset.Value.IsLegacyRuleset()) return null; diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index 87d77db847..16b1ff7ccc 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -6,6 +6,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Screens; using osu.Game.Online.Spectator; using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -14,10 +15,13 @@ namespace osu.Game.Screens.Play { private readonly Score score; + [Cached(typeof(IGameplayLeaderboardProvider))] + private SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); + protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo); public SoloSpectatorPlayer(Score score) - : base(score, new PlayerConfiguration { AllowUserInteraction = false }) + : base(score, new PlayerConfiguration { AllowUserInteraction = false, ShowLeaderboard = true }) { this.score = score; } @@ -26,6 +30,8 @@ namespace osu.Game.Screens.Play private void load() { SpectatorClient.OnUserBeganPlaying += userBeganPlaying; + + AddInternal(leaderboardProvider); } public override bool OnExiting(ScreenExitEvent e) diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 4bd9bfafc0..22c966e0af 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -13,17 +13,11 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.Play { public abstract partial class SpectatorPlayer : Player { - // TODO: maybe consider giving this proper scores. - // `SoloGameplayLeaderboardProvider` doesn't immediately work because there's no guarantee that `LeaderboardManager` global state matches the currently spectated beatmap. - [Cached(typeof(IGameplayLeaderboardProvider))] - private readonly EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider(); - [Resolved] protected SpectatorClient SpectatorClient { get; private set; } = null!; diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs index 76e59b32b8..ce8bd941c2 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Origin = Anchor.Centre, GlowColour = OsuColour.ForRank(rank), Spacing = new Vector2(-15, 0), - Text = DrawableRank.GetRankName(rank), + Text = DrawableRank.GetRankLetter(rank), Font = OsuFont.Numeric.With(size: 76), UseFullGlyphHeight = false }, @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-15, 0), - Text = DrawableRank.GetRankName(rank), + Text = DrawableRank.GetRankLetter(rank), Font = OsuFont.Numeric.With(size: 76), UseFullGlyphHeight = false, Shadow = false diff --git a/osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs b/osu.Game/Screens/Ranking/UserTagControl.AddTagsPopover.cs similarity index 100% rename from osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs rename to osu.Game/Screens/Ranking/UserTagControl.AddTagsPopover.cs diff --git a/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs b/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs similarity index 96% rename from osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs rename to osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs index ff3c0711c0..0f88515f59 100644 --- a/osu.Game/Screens/Ranking/UserTagControl_DrawableUserTag.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.DrawableUserTag.cs @@ -20,6 +20,12 @@ namespace osu.Game.Screens.Ranking { public partial class DrawableUserTag : OsuAnimatedButton { + /// + /// Minimum count of votes required to display a tag on the beatmap's page. + /// Should match value specified web-side as https://github.com/ppy/osu-web/blob/cae2fdf03cfb8c30c8e332cfb142e03188ceffef/config/osu.php#L59. + /// + public const int MIN_VOTES_DISPLAY = 5; + public readonly UserTag UserTag; public Action? OnSelected { get; set; } @@ -154,7 +160,7 @@ namespace osu.Game.Screens.Ranking { voteCount.BindValueChanged(_ => { - confirmed.Value = voteCount.Value >= 10; + confirmed.Value = voteCount.Value >= MIN_VOTES_DISPLAY; }, true); voted.BindValueChanged(v => { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9ccb8170f3..0d75ddb0f0 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -237,26 +237,29 @@ namespace osu.Game.Screens.Select private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed) { + IEnumerable? oldBeatmapSets = changed.OldItems?.Cast(); + HashSet oldBeatmapSetIDs = oldBeatmapSets?.Select(s => s.ID).ToHashSet() ?? []; + IEnumerable? newBeatmapSets = changed.NewItems?.Cast(); + HashSet newBeatmapSetIDs = newBeatmapSets?.Select(s => s.ID).ToHashSet() ?? []; switch (changed.Action) { case NotifyCollectionChangedAction.Add: - HashSet newBeatmapSetIDs = newBeatmapSets!.Select(s => s.ID).ToHashSet(); - setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID)); setsRequiringUpdate.AddRange(newBeatmapSets!); break; case NotifyCollectionChangedAction.Remove: - IEnumerable oldBeatmapSets = changed.OldItems!.Cast(); - HashSet oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet(); - setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID)); - setsRequiringRemoval.AddRange(oldBeatmapSets); + setsRequiringRemoval.AddRange(oldBeatmapSets!); break; case NotifyCollectionChangedAction.Replace: + setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID)); + setsRequiringRemoval.AddRange(oldBeatmapSets!); + + setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID)); setsRequiringUpdate.AddRange(newBeatmapSets!); break; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 4cd91a85e2..39bf4e134b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -77,10 +77,23 @@ namespace osu.Game.Screens.Select.Carousel if (!match) return false; match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username); - match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || - criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); - match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) || - criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode); + + if (criteria.Artist.HasFilter) + { + if (criteria.Artist.ExcludeTerm) + match &= criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) && criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); + else + match &= criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); + } + + if (criteria.Title.HasFilter) + { + if (criteria.Title.ExcludeTerm) + match &= criteria.Title.Matches(BeatmapInfo.Metadata.Title) && criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode); + else + match &= criteria.Title.Matches(BeatmapInfo.Metadata.Title) || criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode); + } + match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName); match &= !criteria.Source.HasFilter || criteria.Source.Matches(BeatmapInfo.Metadata.Source); @@ -88,12 +101,24 @@ namespace osu.Game.Screens.Select.Carousel { foreach (var tagFilter in criteria.UserTags) { - bool anyTagMatched = false; + if (tagFilter.ExcludeTerm) + { + // if `ExcludeTerm` is true, `Matches()` will return true if a user tag *doesn't match* the excluded term. + // thus, every user tag must pass this filter. + foreach (string tag in BeatmapInfo.Metadata.UserTags) + match &= tagFilter.Matches(tag); + } + else + { + // if `ExcludeTerm` is false, `Matches()` will return true if a user tag *matches* the expected term. + // the expected behaviour is that a beatmap should be displayed if at least one of the user tags passes the filter. + bool anyTagMatched = false; - foreach (string tag in BeatmapInfo.Metadata.UserTags) - anyTagMatched |= tagFilter.Matches(tag); + foreach (string tag in BeatmapInfo.Metadata.UserTags) + anyTagMatched |= tagFilter.Matches(tag); - match &= anyTagMatched; + match &= anyTagMatched; + } } } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 9fe00aaeb7..88eb7d8399 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -116,6 +116,7 @@ namespace osu.Game.Screens.Select /// public IEnumerable? CollectionBeatmapMD5Hashes { get; set; } + // 多条件过滤按钮的实现 public List? DiscreteCircleSizeValues { get; set; } public IRulesetFilterCriteria? RulesetCriteria { get; set; } @@ -207,7 +208,7 @@ namespace osu.Game.Screens.Select // search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching if (string.IsNullOrEmpty(value)) - return false; + return ExcludeTerm; bool result; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f923154873..606d53d884 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -1137,6 +1138,10 @@ namespace osu.Game.Screens.Select { private readonly Action? resetCarouselPosition; + private bool mouseContained; + + private InputManager inputManager = null!; + public LeftSideInteractionContainer(Action resetCarouselPosition) { this.resetCarouselPosition = resetCarouselPosition; @@ -1149,10 +1154,30 @@ namespace osu.Game.Screens.Select protected override bool OnMouseDown(MouseDownEvent e) => true; - protected override bool OnHover(HoverEvent e) + protected override void LoadComplete() { - resetCarouselPosition?.Invoke(); - return base.OnHover(e); + inputManager = GetContainingInputManager()!; + base.LoadComplete(); + } + + protected override void Update() + { + base.Update(); + + // We want to trigger an action whenever the cursor is in the left area of song select. + // Other elements in song select handle input, so rather than using `OnHover` let's check the true mouse position. + if (Contains(inputManager.CurrentState.Mouse.Position)) + { + if (!mouseContained) + { + mouseContained = true; + resetCarouselPosition?.Invoke(); + } + } + else + { + mouseContained = false; + } } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 52d5989c8f..b959886c7c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,9 +13,12 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -342,6 +345,12 @@ namespace osu.Game.Screens.SelectV2 } } + /// + /// Tracks whether the user has manually requested to collapse an open group. + /// In this case, refilters should not forcibly expand groups until the user expands a group again themselves. + /// + private bool userCollapsedGroup; + protected override void HandleItemActivated(CarouselItem item) { try @@ -354,11 +363,19 @@ namespace osu.Game.Screens.SelectV2 { setExpansionStateOfGroup(ExpandedGroup, false); ExpandedGroup = null; + userCollapsedGroup = true; return; } setExpandedGroup(group); + if (userCollapsedGroup) + { + if (grouping.BeatmapSetsGroupedTogether && CurrentGroupedBeatmap != null && CheckModelEquality(group, CurrentGroupedBeatmap.Group)) + setExpandedSet(new GroupedBeatmapSet(CurrentGroupedBeatmap.Group, CurrentGroupedBeatmap.Beatmap.BeatmapSet!)); + userCollapsedGroup = false; + } + // If the active selection is within this group, it should get keyboard focus immediately. if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is GroupedBeatmap gb) RequestSelection(gb); @@ -397,6 +414,9 @@ namespace osu.Game.Screens.SelectV2 throw new InvalidOperationException("Groups should never become selected"); case GroupedBeatmap groupedBeatmap: + if (userCollapsedGroup) + break; + setExpandedGroup(groupedBeatmap.Group); if (grouping.BeatmapSetsGroupedTogether) @@ -641,6 +661,37 @@ namespace osu.Game.Screens.SelectV2 } } + public void ExpandGroupForCurrentSelection() + { + if (CurrentGroupedBeatmap?.Group == null) + return; + + if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group)) + return; + + var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group)); + if (groupItem != null) + HandleItemActivated(groupItem); + } + + protected override double? GetScrollTarget() + { + double? target = base.GetScrollTarget(); + + // if the base implementation returned null, it means that the keyboard selection has been filtered out and is no longer visible + // attempt a fallback to other possibly expanded panels (set first, then group) + if (target == null) + { + var items = GetCarouselItems(); + var targetItem = items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedBeatmapSet)) + ?? items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedGroup)); + + target = targetItem?.CarouselYPosition; + } + + return target; + } + #endregion #region Audio @@ -788,9 +839,11 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool setPanelPool = new DrawablePool(100); private readonly DrawablePool groupPanelPool = new DrawablePool(100); private readonly DrawablePool starsGroupPanelPool = new DrawablePool(11); + private readonly DrawablePool ranksGroupPanelPool = new DrawablePool(9); private void setupPools() { + AddInternal(ranksGroupPanelPool); AddInternal(starsGroupPanelPool); AddInternal(groupPanelPool); AddInternal(beatmapPanelPool); @@ -813,12 +866,20 @@ namespace osu.Game.Screens.SelectV2 if (x is GroupedBeatmap groupedBeatmapX && y is GroupedBeatmap groupedBeatmapY) return groupedBeatmapX.Equals(groupedBeatmapY); + // `BeatmapInfo` is no longer used directly in carousel items, but in rare circumstances still is used for model equality comparisons + // (see `beatmapSetsChanged()` deletion handling logic, which aims to find a beatmap close to the just-deleted one, disregarding grouping concerns) + if (x is BeatmapInfo beatmapInfoX && y is BeatmapInfo beatmapInfoY) + return beatmapInfoX.Equals(beatmapInfoY); + if (x is GroupDefinition groupX && y is GroupDefinition groupY) return groupX.Equals(groupY); if (x is StarDifficultyGroupDefinition starX && y is StarDifficultyGroupDefinition starY) return starX.Equals(starY); + if (x is RankDisplayGroupDefinition rankX && y is RankDisplayGroupDefinition rankY) + return rankX.Equals(rankY); + return base.CheckModelEquality(x, y); } @@ -829,6 +890,9 @@ namespace osu.Game.Screens.SelectV2 case StarDifficultyGroupDefinition: return starsGroupPanelPool.Get(); + case RankDisplayGroupDefinition: + return ranksGroupPanelPool.Get(); + case GroupDefinition: return groupPanelPool.Get(); @@ -948,13 +1012,13 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleGroupedSets = ExpandedGroup != null + ICollection visibleGroupedSets = ExpandedGroup != null && grouping.GroupItems.TryGetValue(ExpandedGroup, out var groupItems) // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + ? groupItems.Select(i => i.Model).OfType().ToArray() // This is the fastest way to retrieve sets for randomisation. : grouping.SetItems.Keys; @@ -1064,15 +1128,15 @@ namespace osu.Game.Screens.SelectV2 /// /// The title of this group. /// - public string Title { get; } + public LocalisableString Title { get; } private readonly string uncasedTitle; - public GroupDefinition(int order, string title) + public GroupDefinition(int order, LocalisableString title) { Order = order; Title = title; - uncasedTitle = title.ToLowerInvariant(); + uncasedTitle = title.ToLower().GetLocalised(LocalisationParameters.DEFAULT); } public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle; @@ -1083,7 +1147,12 @@ namespace osu.Game.Screens.SelectV2 /// /// Defines a grouping header for a set of carousel items grouped by star difficulty. /// - public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + public record StarDifficultyGroupDefinition(int Order, LocalisableString Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + + /// + /// Defines a grouping header for a set of carousel items grouped by achieved rank. + /// + public record RankDisplayGroupDefinition(ScoreRank Rank) : GroupDefinition(-(int)Rank, Rank.GetLocalisableDescription()); /// /// Used to represent a portion of a under a . diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 69f5596578..0b7e29c363 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -255,7 +255,7 @@ namespace osu.Game.Screens.SelectV2 return groups.Values .OrderBy(g => g.Group!.Order) - .ThenBy(g => g.Group!.Title) + .ThenBy(g => g.Group!.Title.ToString()) .ToList(); } @@ -433,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) { if (topRankMapping.TryGetValue(beatmap.ID, out var rank)) - return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield(); + return new RankDisplayGroupDefinition(rank).Yield(); return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 3eada92f9b..2a132a8a45 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -96,10 +96,23 @@ namespace osu.Game.Screens.SelectV2 if (!match) return false; match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(beatmap.Metadata.Author.Username); - match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(beatmap.Metadata.Artist) || - criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode); - match &= !criteria.Title.HasFilter || criteria.Title.Matches(beatmap.Metadata.Title) || - criteria.Title.Matches(beatmap.Metadata.TitleUnicode); + + if (criteria.Artist.HasFilter) + { + if (criteria.Artist.ExcludeTerm) + match &= criteria.Artist.Matches(beatmap.Metadata.Artist) && criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode); + else + match &= criteria.Artist.Matches(beatmap.Metadata.Artist) || criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode); + } + + if (criteria.Title.HasFilter) + { + if (criteria.Title.ExcludeTerm) + match &= criteria.Title.Matches(beatmap.Metadata.Title) && criteria.Title.Matches(beatmap.Metadata.TitleUnicode); + else + match &= criteria.Title.Matches(beatmap.Metadata.Title) || criteria.Title.Matches(beatmap.Metadata.TitleUnicode); + } + match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(beatmap.DifficultyName); match &= !criteria.Source.HasFilter || criteria.Source.Matches(beatmap.Metadata.Source); @@ -107,12 +120,24 @@ namespace osu.Game.Screens.SelectV2 { foreach (var tagFilter in criteria.UserTags) { - bool anyTagMatched = false; + if (tagFilter.ExcludeTerm) + { + // if `ExcludeTerm` is true, `Matches()` will return true if a user tag *doesn't match* the excluded term. + // thus, every user tag must pass this filter. + foreach (string tag in beatmap.Metadata.UserTags) + match &= tagFilter.Matches(tag); + } + else + { + // if `ExcludeTerm` is false, `Matches()` will return true if a user tag *matches* the expected term. + // the expected behaviour is that a beatmap should be displayed if at least one of the user tags passes the filter. + bool anyTagMatched = false; - foreach (string tag in beatmap.Metadata.UserTags) - anyTagMatched |= tagFilter.Matches(tag); + foreach (string tag in beatmap.Metadata.UserTags) + anyTagMatched |= tagFilter.Matches(tag); - match &= anyTagMatched; + match &= anyTagMatched; + } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.Header.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapDetailsArea_Header.cs rename to osu.Game/Screens/SelectV2/BeatmapDetailsArea.Header.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs b/osu.Game/Screens/SelectV2/BeatmapDetailsArea.WedgeSelector.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapDetailsArea_WedgeSelector.cs rename to osu.Game/Screens/SelectV2/BeatmapDetailsArea.WedgeSelector.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.Tooltip.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs rename to osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.Tooltip.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 80414d3f44..5013150f05 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -392,9 +392,9 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(Score.Rank), + Colour = DrawableRank.GetRankLetterColour(Score.Rank), Font = OsuFont.Numeric.With(size: 14), - Text = DrawableRank.GetRankName(Score.Rank), + Text = DrawableRank.GetRankLetter(Score.Rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index d34c202640..0c21d4f6ed 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -266,6 +266,12 @@ namespace osu.Game.Screens.SelectV2 if (scores == null) return; + // because leaderboard refetches are debounced, it is technically possible for the global leaderboard manager + // to contain scores for a different beatmap than the ones the wedge is currently on. + // in this case, ignore the incoming scores to avoid briefly flashing the wrong leaderboard. + if (leaderboardManager.CurrentCriteria?.Beatmap?.Equals(beatmap.Value.BeatmapInfo) != true) + return; + if (scores.FailState != null) SetState((LeaderboardState)scores.FailState); else diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.FailRetryDisplay.cs similarity index 66% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.FailRetryDisplay.cs index 048ec3c40d..9ee61b7c5c 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.FailRetryDisplay.cs @@ -8,6 +8,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; +using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -95,6 +97,14 @@ namespace osu.Game.Screens.SelectV2 } } + private IShader shader = null!; + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders) + { + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle"); + } + protected override void Update() { base.Update(); @@ -123,6 +133,8 @@ namespace osu.Game.Screens.SelectV2 private Vector2 drawSize; private float[] displayedData = null!; + private IShader shader = null!; + private IVertexBatch? quadBatch; public GraphDrawNode(GraphDrawable source) : base(source) @@ -136,6 +148,7 @@ namespace osu.Game.Screens.SelectV2 drawSize = source.DrawSize; displayedData = source.displayedData; + shader = source.shader; } protected override void Draw(IRenderer renderer) @@ -150,6 +163,9 @@ namespace osu.Game.Screens.SelectV2 float totalSpacing = drawSize.X - barWidth * displayedData.Length; float spacing = totalSpacing / (displayedData.Length - 1); + quadBatch ??= renderer.CreateQuadBatch(displayedData.Length * 4, 1); + shader.Bind(); + for (int i = 0; i < displayedData.Length; i++) { float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth); @@ -158,35 +174,61 @@ namespace osu.Game.Screens.SelectV2 position += barWidth + spacing; } + + shader.Unbind(); } private void drawBar(IRenderer renderer, float position, float width, float height) { - float cornerRadius = width / 2f; - - Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); - float blendRange = (scale.X + scale.Y) / 2; + // Since bars have corner radius, to avoid masking usage and draw all bars in a single draw call + // we are using FastCircle implementation. + // Not using FastCircle directly to minimize drawable count. RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height)); + Vector4 textureRectangle = new Vector4(0, 0, drawRectangle.Width, drawRectangle.Height); Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix; - renderer.PushMaskingInfo(new MaskingInfo - { - ScreenSpaceAABB = screenSpaceDrawQuad.AABB, - MaskingRect = drawRectangle.Normalize(), - ConservativeScreenSpaceQuad = screenSpaceDrawQuad, - ToMaskingSpace = DrawInfo.MatrixInverse, - CornerRadius = cornerRadius, - CornerExponent = 2f, - // We are setting the linear blend range to the approximate size of a _pixel_ here. - // This results in the optimal trade-off between crispness and smoothness of the - // edges of the masked region according to sampling theory. - BlendRange = blendRange, - AlphaExponent = 1, - }); + var blend = new Vector2(Math.Min(drawRectangle.Width, drawRectangle.Height) / Math.Min(screenSpaceDrawQuad.Width, screenSpaceDrawQuad.Height)); - renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour); - renderer.PopMaskingInfo(); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomLeft, + TexturePosition = new Vector2(0, drawRectangle.Height), + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomLeft.SRGB, + }); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.BottomRight, + TexturePosition = new Vector2(drawRectangle.Width, drawRectangle.Height), + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.BottomRight.SRGB, + }); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopRight, + TexturePosition = new Vector2(drawRectangle.Width, 0), + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopRight.SRGB, + }); + quadBatch?.AddAction(new TexturedVertex2D(renderer) + { + Position = screenSpaceDrawQuad.TopLeft, + TexturePosition = Vector2.Zero, + TextureRect = textureRectangle, + BlendRange = blend, + Colour = DrawColourInfo.Colour.TopLeft.SRGB, + }); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + quadBatch?.Dispose(); } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.MetadataDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.MetadataDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.RatingSpreadDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.RatingSpreadDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.SuccessRateDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.SuccessRateDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.TagsLine.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.TagsLine.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.UserRatingDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapMetadataWedge.UserRatingDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index 818176b3c4..d516f4b846 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -3,10 +3,12 @@ using System; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -402,24 +404,46 @@ namespace osu.Game.Screens.SelectV2 updateSubWedgeVisibility(); } + private CancellationTokenSource? userTagsCancellationSource; + private void updateUserTags() { - string[] tags = realm.Run(r => + userTagsCancellationSource?.Cancel(); + userTagsCancellationSource = new CancellationTokenSource(); + + var token = userTagsCancellationSource.Token; + + realm.RunAsync(r => { // need to refetch because `beatmap.Value.BeatmapInfo` is not going to have the latest tags - r.Refresh(); var refetchedBeatmap = r.Find(beatmap.Value.BeatmapInfo.ID); return refetchedBeatmap?.Metadata.UserTags.ToArray() ?? []; - }); - - if (tags.Length == 0) + }, token).ContinueWith(t => { - userTags.FadeOut(transition_duration, Easing.OutQuint); - return; - } + string[] tags = t.GetResultSafely(); - userTags.FadeIn(transition_duration, Easing.OutQuint); - userTags.Tags = (tags, t => songSelect?.Search($@"tag=""{t}""!")); + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + if (tags.Length == 0) + { + userTags.FadeOut(transition_duration, Easing.OutQuint); + return; + } + + userTags.FadeIn(transition_duration, Easing.OutQuint); + userTags.Tags = (tags, tag => songSelect?.Search($@"tag=""{tag}""!")); + }); + }, token); + } + + protected override void Dispose(bool isDisposing) + { + userTagsCancellationSource?.Cancel(); + userTagsCancellationSource = null; + base.Dispose(isDisposing); } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyStatisticsDisplay.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyStatisticsDisplay.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.DifficultyStatisticsDisplay.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_FavouriteButton.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.FavouriteButton.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.Statistic.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_Statistic.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.Statistic.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticDifficulty.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticDifficulty.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticDifficulty.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticPlayCount.cs similarity index 100% rename from osu.Game/Screens/SelectV2/BeatmapTitleWedge_StatisticPlayCount.cs rename to osu.Game/Screens/SelectV2/BeatmapTitleWedge.StatisticPlayCount.cs diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs index 21ac04b18a..530b1348dd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; @@ -278,8 +279,13 @@ namespace osu.Game.Screens.SelectV2 }, token); } + private CancellationTokenSource? onlineDisplayCancellationSource; + private void updateOnlineDisplay() { + onlineDisplayCancellationSource?.Cancel(); + onlineDisplayCancellationSource = null; + if (onlineLookupResult.Value?.Status != SongSelect.BeatmapSetLookupStatus.Completed) { playCount.Value = null; @@ -291,21 +297,42 @@ namespace osu.Game.Screens.SelectV2 playCount.Value = new StatisticPlayCount.Data(onlineBeatmap?.PlayCount ?? -1, onlineBeatmap?.UserPlayCount ?? -1); favouriteButton.SetBeatmapSet(onlineLookupResult.Value.Result); + onlineDisplayCancellationSource = new CancellationTokenSource(); + var token = onlineDisplayCancellationSource.Token; + // the online fetch may have also updated the beatmap's status. // this needs to be checked against the *local* beatmap model rather than the online one, because it's not known here whether the status change has occurred or not // (think scenarios like the beatmap being locally modified). // it also has to be handled explicitly like this because the working beatmap's `BeatmapInfo` will not receive these updates due to being detached // (and because of https://github.com/ppy/osu/blob/4b73afd1957a9161e2956fc4191c8114d9958372/osu.Game/Screens/SelectV2/SongSelect.cs#L487-L488 // which prevents working beatmap refetches caused by changes to the realm model of perceived low importance). - var status = realm.Run(r => + realm.RunAsync(r => { - r.Refresh(); var refetchedBeatmap = r.Find(working.Value.BeatmapInfo.ID); return refetchedBeatmap?.Status; - }); - if (status != null) - statusPill.Status = status.Value; + }, token).ContinueWith(t => + { + var status = t.GetResultSafely(); + + if (status != null) + { + Schedule(() => + { + if (token.IsCancellationRequested) + return; + + statusPill.Status = status.Value; + }); + } + }, token); } } + + protected override void Dispose(bool isDisposing) + { + onlineDisplayCancellationSource?.Dispose(); + onlineDisplayCancellationSource = null; + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs b/osu.Game/Screens/SelectV2/FilterControl.DifficultyRangeSlider.cs similarity index 100% rename from osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs rename to osu.Game/Screens/SelectV2/FilterControl.DifficultyRangeSlider.cs diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs similarity index 100% rename from osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs rename to osu.Game/Screens/SelectV2/FooterButtonOptions.Popover.cs diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 1a6e886cb7..a52d3fa216 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -134,12 +134,6 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 4f }, Children = new Drawable[] { - updateButton = new PanelUpdateBeatmapButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5f, Top = -2f }, - }, statusPill = new BeatmapSetOnlineStatusPill { Origin = Anchor.CentreLeft, @@ -148,6 +142,12 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Right = 5f }, Animated = false, }, + updateButton = new PanelUpdateBeatmapButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, difficultiesDisplay = new DifficultySpectrumDisplay { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs new file mode 100644 index 0000000000..6895c30fee --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs @@ -0,0 +1,226 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Scoring; +using osuTK; +using osuTK.Graphics; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroupRankDisplay : Panel + { + public const float HEIGHT = PanelGroup.HEIGHT; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Drawable iconContainer = null!; + private Box backgroundBorder = null!; + private Box contentBackground = null!; + private OsuSpriteText starRatingText = null!; + private CircularContainer countPill = null!; + private OsuSpriteText countText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = PanelGroup.HEIGHT; + + Icon = iconContainer = new Container + { + AlwaysPresent = true, + RelativeSizeAxes = Axes.Y, + Alpha = 0f, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + }, + }; + + Background = backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Highlight1, + }; + + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5) + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + UseFullGlyphHeight = false, + Font = OsuFont.Style.Heading2, + } + } + }, + countPill = new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + countText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + private Color4 rankColour; + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var group = (RankDisplayGroupDefinition)Item.Model; + ScoreRank rank = group.Rank; + + rankColour = OsuColour.ForRank(rank); + + AccentColour = rankColour; + backgroundBorder.Colour = rankColour; + contentBackground.Colour = rankColour.Darken(1f); + glow.Colour = ColourInfo.GradientHorizontal(rankColour, rankColour.Opacity(0f)); + + switch (rank) + { + case ScoreRank.SH: + case ScoreRank.XH: + starRatingText.Colour = DrawableRank.GetRankLetterColour(rank); + iconContainer.Colour = colourProvider.Background5; + break; + + case ScoreRank.X: + case ScoreRank.S: + starRatingText.Colour = DrawableRank.GetRankLetterColour(rank); + iconContainer.Colour = colourProvider.Background5; + break; + + case ScoreRank.F: + starRatingText.Colour = DrawableRank.GetRankLetterColour(rank); + iconContainer.Colour = colourProvider.Content1; + break; + + default: + starRatingText.Colour = Color4.White; + iconContainer.Colour = colourProvider.Background5; + break; + } + + starRatingText.Text = group.Title; + + ColourInfo colour = ColourInfo.GradientHorizontal(rankColour.Darken(0.6f), rankColour.Darken(0.8f)); + + triangles.Colour = colour; + + countText.Text = Item.NestedItemCount.ToLocalisableString(@"N0"); + + onExpanded(); + } + + private void onExpanded() + { + const float duration = 500; + + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); + } + + protected override void Update() + { + base.Update(); + + // Move the count pill in the opposite direction to keep it pinned to the screen regardless of the X position of TopLevelContent. + countPill.X = -TopLevelContent.X; + } + + public override MenuItem[] ContextMenuItems + { + get + { + if (Item == null) + return Array.Empty(); + + return new MenuItem[] + { + new OsuMenuItem(Expanded.Value ? WebCommonStrings.ButtonsCollapse.ToSentence() : WebCommonStrings.ButtonsExpand.ToSentence(), MenuItemType.Highlighted, () => TriggerClick()) + }; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 832095058a..16df414037 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -82,7 +82,9 @@ namespace osu.Game.Screens.SelectV2 // unfortunately in terms of subscriptions realm treats *every* write to any realm object as a modification, // even if the write was redundant and had no observable effect. - if (dbBeatmapSet.Status != onlineBeatmapSet.Status) + // notably, `LocallyModified` status is preserved on the set until the user performs an explicit action to get rid of it + // (be it updating the set or deciding to discard their changes, removing the set and re-downloading it, etc.) + if (dbBeatmapSet.Status != onlineBeatmapSet.Status && dbBeatmapSet.Status != BeatmapOnlineStatus.LocallyModified) dbBeatmapSet.Status = onlineBeatmapSet.Status; foreach (var dbBeatmap in dbBeatmapSet.Beatmaps) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 262336571f..c167241637 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -43,6 +43,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Volume; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Footer; @@ -63,7 +64,7 @@ namespace osu.Game.Screens.SelectV2 /// This will be gradually built upon and ultimately replace once everything is in place. /// [Cached(typeof(ISongSelect))] - public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler, ISongSelect + public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler, ISongSelect, IHandlePresentBeatmap { /// /// A debounce that governs how long after a panel is selected before the rest of song select (and the game at large) @@ -155,7 +156,7 @@ namespace osu.Game.Screens.SelectV2 private readonly RealmPopulatingOnlineLookupSource onlineLookupSource = new RealmPopulatingOnlineLookupSource(); private Bindable configBackgroundBlur = null!; - + private Bindable showConvertedBeatmaps = null!; private EzPreviewTrackManager ezPreviewManager = null!; [BackgroundDependencyLoader] @@ -214,7 +215,11 @@ namespace osu.Game.Screens.SelectV2 // Pad enough to only reset scroll when well into the left wedge areas. Padding = new MarginPadding { Right = 40 }, RelativeSizeAxes = Axes.Both, - Child = new Select.SongSelect.LeftSideInteractionContainer(() => carousel.ScrollToSelection()) + Child = new Select.SongSelect.LeftSideInteractionContainer(() => + { + carousel.ExpandGroupForCurrentSelection(); + carousel.ScrollToSelection(); + }) { RelativeSizeAxes = Axes.Both, }, @@ -305,6 +310,8 @@ namespace osu.Game.Screens.SelectV2 updateBackgroundDim(); }); + + showConvertedBeatmaps = config.GetBindable(OsuSetting.ShowConvertedBeatmaps); } private void requestRecommendedSelection(IEnumerable groupedBeatmaps) @@ -326,7 +333,13 @@ namespace osu.Game.Screens.SelectV2 { Hotkey = GlobalAction.ToggleModSelection, Current = Mods, - RequestDeselectAllMods = () => Mods.Value = Array.Empty() + RequestDeselectAllMods = () => + { + if (modSelectOverlay.State.Value == Visibility.Visible) + modSelectOverlay.DeselectAll(); + else + Mods.Value = Array.Empty(); + } }, new FooterButtonRandom { @@ -530,7 +543,7 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; - if (!checkBeatmapValidForSelection(beatmap, carousel.Criteria)) + if (!checkBeatmapValidForSelection(beatmap)) return; // To ensure sanity, cancel any pending selection as we are about to force a selection. @@ -581,7 +594,7 @@ namespace osu.Game.Screens.SelectV2 // Refetch to be confident that the current selection is still valid. It may have been deleted or hidden. var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); - bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, filterControl.CreateCriteria()); + bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo); if (validSelection) { @@ -602,13 +615,14 @@ namespace osu.Game.Screens.SelectV2 { // In the case a difficulty was hidden or removed, prefer selecting another difficulty from the same set. var activeSet = currentBeatmap.BeatmapSetInfo; - var criteria = filterControl.CreateCriteria(); - var validBeatmaps = activeSet.Beatmaps.Where(b => checkBeatmapValidForSelection(b, criteria)).ToArray(); + var validBeatmaps = activeSet.Beatmaps.Where(checkBeatmapValidForSelection).ToArray(); if (validBeatmaps.Any()) { - carousel.CurrentBeatmap = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); + var beatmap = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First(); + carousel.CurrentBeatmap = beatmap; + debounceQueueSelection(beatmap); return true; } } @@ -620,12 +634,9 @@ namespace osu.Game.Screens.SelectV2 return validSelection; } - private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria) + private bool checkBeatmapValidForSelection(BeatmapInfo beatmap) { - if (criteria == null) - return false; - - if (!beatmap.AllowGameplayWithRuleset(Ruleset.Value, criteria.AllowConvertedBeatmaps)) + if (!beatmap.AllowGameplayWithRuleset(Ruleset.Value, showConvertedBeatmaps.Value)) return false; if (beatmap.Hidden) @@ -723,7 +734,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(); updateBackgroundDim(); - fetchOnlineInfo(); + fetchOnlineInfo(force: true); } private void onLeavingScreen() @@ -783,7 +794,7 @@ namespace osu.Game.Screens.SelectV2 // This avoids a flicker of a placeholder or invalid beatmap before a proper selection. // // After the carousel finishes filtering, it will attempt a selection then call this method again. - if (!CarouselItemsPresented && !checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, filterControl.CreateCriteria())) + if (!CarouselItemsPresented && !checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo)) return; if (carousel.VisuallyFocusSelected) @@ -840,7 +851,7 @@ namespace osu.Game.Screens.SelectV2 bool isFirstFilter = filterDebounce == null; // Criteria change may have included a ruleset change which made the current selection invalid. - bool isSelectionValid = checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, criteria); + bool isSelectionValid = checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo); filterDebounce = Scheduler.AddDelayed(() => carousel.Filter(criteria, !isSelectionValid), isFirstFilter || !isSelectionValid ? 0 : filter_delay); } @@ -993,6 +1004,14 @@ namespace osu.Game.Screens.SelectV2 switch (e.Action) { + case GlobalAction.Select: + // in most circumstances this is handled already by the carousel itself, but there are cases where it will not be. + // one of which is filtering out all visible beatmaps and attempting to start gameplay. + // in that case, users still expect a `Select` press to advance to gameplay anyway, using the ambient selected beatmap if there is one, + // which matches the behaviour resulting from clicking the osu! cookie in that scenario. + SelectAndRun(Beatmap.Value.BeatmapInfo, OnStart); + return true; + case GlobalAction.IncreaseModSpeed: return modSpeedHotkeyHandler.ChangeSpeed(0.05, flattenedMods); @@ -1064,11 +1083,11 @@ namespace osu.Game.Screens.SelectV2 private CancellationTokenSource? onlineLookupCancellation; private Task? currentOnlineLookup; - private void fetchOnlineInfo() + private void fetchOnlineInfo(bool force = false) { var beatmapSetInfo = Beatmap.Value.BeatmapSetInfo; - if (lastLookupResult.Value?.Result?.OnlineID == beatmapSetInfo.OnlineID) + if (lastLookupResult.Value?.Result?.OnlineID == beatmapSetInfo.OnlineID && !force) return; onlineLookupCancellation?.Cancel(); @@ -1112,6 +1131,36 @@ namespace osu.Game.Screens.SelectV2 #endregion + #region IHandlePresentBeatmap + + void IHandlePresentBeatmap.PresentBeatmap(WorkingBeatmap workingBeatmap, RulesetInfo ruleset) + { + cancelDebounceSelection(); + + var beatmapInfo = workingBeatmap.BeatmapInfo; + + // Don't change the local ruleset if the user is on another ruleset and is showing converted beatmaps. + // Eventually we probably want to check whether conversion is actually possible for the current ruleset. + bool requiresRulesetSwitch = !beatmapInfo.Ruleset.Equals(Ruleset.Value) + && (beatmapInfo.Ruleset.OnlineID > 0 || !showConvertedBeatmaps.Value); + + if (requiresRulesetSwitch) + { + Ruleset.Value = beatmapInfo.Ruleset; + Beatmap.Value = workingBeatmap; + + Logger.Log($"Completing {nameof(IHandlePresentBeatmap.PresentBeatmap)} with beatmap {workingBeatmap} ruleset {beatmapInfo.Ruleset}"); + } + else + { + Beatmap.Value = workingBeatmap; + + Logger.Log($"Completing {nameof(IHandlePresentBeatmap.PresentBeatmap)} with beatmap {workingBeatmap} (maintaining ruleset)"); + } + } + + #endregion + #region Beatmap management [Resolved] diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index f41bd89b7a..0932485349 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -53,13 +53,8 @@ namespace osu.Game.Skinning } } - private string? getPathForFile(string filename) - { - if (fileToStoragePathMapping.Value.TryGetValue(filename.ToLowerInvariant(), out string? path)) - return path; - - return null; - } + private string? getPathForFile(string filename) => + fileToStoragePathMapping.Value.GetValueOrDefault(filename.ToLowerInvariant()); private void invalidateCache() => fileToStoragePathMapping = new Lazy>(initialiseFileCache); diff --git a/osu.Game/Tests/Visual/EditorSavingTestScene.cs b/osu.Game/Tests/Visual/EditorSavingTestScene.cs index d2b216caa8..8d27618c00 100644 --- a/osu.Game/Tests/Visual/EditorSavingTestScene.cs +++ b/osu.Game/Tests/Visual/EditorSavingTestScene.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual SoloSongSelect songSelect = null; PushAndConfirm(() => songSelect = new SoloSongSelect()); - AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented); + AddUntilStep("wait for carousel load", () => songSelect.CarouselItemsPresented && !songSelect.IsFiltering); AddStep("Present same beatmap", () => Game.PresentBeatmap(Game.BeatmapManager.QueryBeatmapSet(set => set.ID == beatmapSetGuid)!.Value, beatmap => beatmap.ID == beatmapGuid)); AddUntilStep("Wait for beatmap selected", () => Game.Beatmap.Value.BeatmapInfo.ID == beatmapGuid); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index ac587d3bb2..316e90d7d3 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -24,12 +24,12 @@ namespace osu.Game.Tests.Visual.Multiplayer public bool RoomJoined => MultiplayerClient.RoomJoined; - protected Room CreateDefaultRoom() + protected Room CreateDefaultRoom(MatchType type = MatchType.HeadToHead) { return new Room { Name = "test name", - Type = MatchType.HeadToHead, + Type = type, Playlist = [ new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 1cea38667e..bd16c36eec 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -15,8 +15,10 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Matchmaking; +using osu.Game.Online.Matchmaking.Events; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; @@ -248,6 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Host = localUser }; + await changeMatchType(ServerRoom.Settings.MatchType).ConfigureAwait(false); await updatePlaylistOrder(ServerRoom).ConfigureAwait(false); await updateCurrentItem(ServerRoom, false).ConfigureAwait(false); @@ -260,10 +263,6 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override void OnRoomJoined() { Debug.Assert(ServerRoom != null); - - // emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join). - changeMatchType(ServerRoom.Settings.MatchType).WaitSafely(); - RoomJoined = true; } @@ -388,7 +387,10 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public override async Task SendMatchRequest(MatchUserRequest request) + public override Task SendMatchRequest(MatchUserRequest request) + => SendUserMatchRequest(api.LocalUser.Value.OnlineID, request); + + public async Task SendUserMatchRequest(int userId, MatchUserRequest request) { request = clone(request); @@ -406,7 +408,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (targetTeam != null) { userState.TeamID = targetTeam.ID; - await ((IMultiplayerClient)this).MatchUserStateChanged(clone(LocalUser.UserID), clone(userState)).ConfigureAwait(false); + await ((IMultiplayerClient)this).MatchUserStateChanged(userId, clone(userState)).ConfigureAwait(false); } break; @@ -418,6 +420,14 @@ namespace osu.Game.Tests.Visual.Multiplayer case StopCountdownRequest stopCountdown: await StopCountdown(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)).ConfigureAwait(false); break; + + case MatchmakingAvatarActionRequest avatarAction: + await ((IMultiplayerClient)this).MatchEvent(new MatchmakingAvatarActionEvent + { + UserId = userId, + Action = avatarAction.Action + }).ConfigureAwait(false); + break; } } @@ -592,6 +602,18 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).ConfigureAwait(false); } + break; + + case MatchType.Matchmaking: + ServerRoom.MatchState = new MatchmakingRoomState(); + await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false); + + foreach (var user in ServerRoom.Users) + { + user.MatchState = null; + await ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).ConfigureAwait(false); + } + break; } } @@ -814,6 +836,25 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMatchmakingClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false); } + public async Task MatchmakingChangeStage(MatchmakingStage stage, Action? prepare = null) + { + MatchmakingRoomState state = clone((MatchmakingRoomState)ServerRoom!.MatchState!); + + state.Stage = stage; + + if (stage == MatchmakingStage.RoundWarmupTime) + state.CurrentRound++; + + prepare?.Invoke(state); + + await ChangeMatchRoomState(state).ConfigureAwait(false); + await StartCountdown(new MatchmakingStageCountdown + { + Stage = stage, + TimeRemaining = TimeSpan.FromSeconds(10) + }).ConfigureAwait(false); + } + #region API Room Handling public IReadOnlyList ServerSideRooms diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index c687815270..75932bbfef 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -32,9 +32,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay protected override Container Content => content; - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - private readonly Container content; private readonly Container drawableDependenciesContainer; private DelegatedDependencyContainer dependencies = null!; @@ -100,7 +97,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay Room[] rooms = new Room[count]; // Can't reference Osu ruleset project here. - ruleset ??= rulesets.GetRuleset(0)!; + if (ruleset == null) + { + using var assemblyRulesetStore = new AssemblyRulesetStore(); + ruleset = assemblyRulesetStore.GetRuleset(0)!; + } for (int i = 0; i < count; i++) { diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 1cb7b2c840..9b0b66a18c 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -332,6 +332,9 @@ namespace osu.Game.Tests.Visual if (MusicController?.TrackLoaded == true) MusicController.Stop(); + if (realm?.IsValueCreated == true) + Realm.Dispose(); + RecycleLocalStorage(true); } diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 4942cc7512..77ff13f260 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; @@ -26,6 +27,8 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { + Debug.Assert(Background != null); + Background.Width = 0.5f; Background.Origin = Anchor.CentreRight; Background.Anchor = Anchor.CentreRight; diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index eac86a9c02..185b1cc4f1 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -144,8 +144,9 @@ namespace osu.Game.Utils /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable hitObjects) => - GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects)); + /// Whether to only include the start and end positions of the slider, or include every control point in the slider. + public static Quad GetSurroundingQuad(IEnumerable hitObjects, bool startAndEndOnly = false) => + GetSurroundingQuad(startAndEndOnly ? enumerateStartAndEndPositions(hitObjects) : enumeratePositions(hitObjects)); /// /// Returns the points that make up the convex hull of the provided points. @@ -202,7 +203,7 @@ namespace osu.Game.Utils } public static List GetConvexHull(IEnumerable hitObjects) => - GetConvexHull(enumerateStartAndEndPositions(hitObjects)); + GetConvexHull(enumeratePositions(hitObjects)); private static IEnumerable enumerateStartAndEndPositions(IEnumerable hitObjects) => hitObjects.SelectMany(h => @@ -220,6 +221,17 @@ namespace osu.Game.Utils return new[] { h.Position }; }); + private static IEnumerable enumeratePositions(IEnumerable hitObjects) => + hitObjects.SelectMany(h => + { + if (h is IHasPath path) + { + return path.Path.ControlPoints.Select(p => h.Position + p.Position); + } + + return new[] { h.Position }; + }); + #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle diff --git a/osu.iOS.props b/osu.iOS.props index 4ce5be23bc..891e9377be 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - +

lU=NOpvx;@?n3Rp)6x(QGjoza%0 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