diff --git a/README.md b/README.md index 7ace47a74f..f64240f67a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Please make sure you have the following prerequisites: - A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. - When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). -- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). +- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). - When running on Linux, please have a system-wide FFmpeg installation available to support video decoding. ### Downloading the source code @@ -72,7 +72,7 @@ git pull Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). -- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations. +- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations. You can also build and run *osu!* from the command-line with a single command: diff --git a/osu.Android.props b/osu.Android.props index 526ce959a6..5b26b8f36e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ <Reference Include="Java.Interop" /> </ItemGroup> <ItemGroup> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.211.0" /> - <PackageReference Include="ppy.osu.Framework.Android" Version="2022.223.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> + <PackageReference Include="ppy.osu.Framework.Android" Version="2022.304.0" /> </ItemGroup> <ItemGroup Label="Transitive Dependencies"> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 3642f70a56..d87b25a4c7 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; @@ -108,10 +109,7 @@ namespace osu.Desktop presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); // update ruleset - int onlineID = ruleset.Value.OnlineID; - bool isLegacyRuleset = onlineID >= 0 && onlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID; - - presence.Assets.SmallImageKey = isLegacyRuleset ? $"mode_{onlineID}" : "mode_custom"; + presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom"; presence.Assets.SmallImageText = ruleset.Value.Name; client.SetPresence(presence); diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 36fa336d0c..bd3b8c3b10 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -45,10 +45,5 @@ namespace osu.Game.Rulesets.Mania } }; } - - private class TimeSlider : OsuSliderBar<double> - { - public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms"; - } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 88862ea28b..6457a23a1b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -1,29 +1,23 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -118,59 +112,6 @@ namespace osu.Game.Tests.Gameplay AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); } - [TestCase(typeof(OsuModDoubleTime), 1.5)] - [TestCase(typeof(OsuModHalfTime), 0.75)] - [TestCase(typeof(ModWindUp), 1.5)] - [TestCase(typeof(ModWindDown), 0.75)] - [TestCase(typeof(OsuModDoubleTime), 2)] - [TestCase(typeof(OsuModHalfTime), 0.5)] - [TestCase(typeof(ModWindUp), 2)] - [TestCase(typeof(ModWindDown), 0.5)] - public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate) - { - GameplayClockContainer gameplayContainer = null; - StoryboardSampleInfo sampleInfo = null; - TestDrawableStoryboardSample sample = null; - - Mod testedMod = Activator.CreateInstance(expectedMod) as Mod; - - switch (testedMod) - { - case ModRateAdjust m: - m.SpeedChange.Value = expectedRate; - break; - - case ModTimeRamp m: - m.FinalRate.Value = m.InitialRate.Value = expectedRate; - break; - } - - AddStep("setup storyboard sample", () => - { - Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this); - SelectedMods.Value = new[] { testedMod }; - - var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - - Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) - { - Child = beatmapSkinSourceContainer - }); - - beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1)) - { - Clock = gameplayContainer.GameplayClock - }); - }); - - AddStep("start", () => gameplayContainer.Start()); - - AddAssert("sample playback rate matches mod rates", () => - testedMod != null && Precision.AlmostEquals( - sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value, - ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime))); - } - [Test] public void TestSamplePlaybackWithBeatmapHitsoundsOff() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 67f5db548b..8ca49837da 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestDisplay() + public void TestCalibrationFromZero() { const double average_error = -4.5; @@ -62,11 +62,39 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - AddAssert("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any()); + AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any()); AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick()); AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error); - AddAssert("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value); + AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value); + AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any()); + } + + /// <summary> + /// When a beatmap offset was already set, the calibration should take it into account. + /// </summary> + [Test] + public void TestCalibrationFromNonZero() + { + const double average_error = -4.5; + const double initial_offset = -2; + + AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any()); + AddStep("Set reference score", () => + { + offsetControl.ReferenceScore.Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error) + }; + }); + + AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any()); + AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error); + + AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value); AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any()); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 3b6d02c67c..014ccb1652 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; - storyboard = working.Storyboard.CreateDrawable(Beatmap.Value); + storyboard = working.Storyboard.CreateDrawable(SelectedMods.Value); storyboard.Passing = false; storyboardContainer.Add(storyboard); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay sb = decoder.Decode(bfr); } - storyboard = sb.CreateDrawable(Beatmap.Value); + storyboard = sb.CreateDrawable(SelectedMods.Value); storyboardContainer.Add(storyboard); decoupledClock.ChangeSource(Beatmap.Value.Track); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 95603b5c04..909cab5e3d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Audio; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -19,6 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay { private Storyboard storyboard; + private IReadOnlyList<Mod> storyboardMods; + + protected override bool HasCustomSteps => true; + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -31,42 +41,107 @@ namespace osu.Game.Tests.Visual.Gameplay backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); } + [SetUp] + public void SetUp() => storyboardMods = Array.Empty<Mod>(); + [Test] public void TestStoryboardSamplesStopDuringPause() { - checkForFirstSamplePlayback(); + createPlayerTest(); AddStep("player paused", () => Player.Pause()); AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value); - AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + allStoryboardSamplesStopped(); AddStep("player resume", () => Player.Resume()); - AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + waitUntilStoryboardSamplesPlay(); } [Test] public void TestStoryboardSamplesStopOnSkip() { - checkForFirstSamplePlayback(); + createPlayerTest(); - AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space)); - AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + skipIntro(); + allStoryboardSamplesStopped(); - AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + waitUntilStoryboardSamplesPlay(); } - private void checkForFirstSamplePlayback() + [TestCase(typeof(OsuModDoubleTime), 1.5)] + [TestCase(typeof(OsuModDoubleTime), 2)] + [TestCase(typeof(OsuModHalfTime), 0.75)] + [TestCase(typeof(OsuModHalfTime), 0.5)] + public void TestStoryboardSamplesPlaybackWithRateAdjustMods(Type expectedMod, double expectedRate) { - AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); - AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + AddStep("setup mod", () => + { + ModRateAdjust testedMod = (ModRateAdjust)Activator.CreateInstance(expectedMod).AsNonNull(); + testedMod.SpeedChange.Value = expectedRate; + storyboardMods = new[] { testedMod }; + }); + + createPlayerTest(); + skipIntro(); + + AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => + sound.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == expectedRate)); } + [TestCase(typeof(ModWindUp), 0.5, 2)] + [TestCase(typeof(ModWindUp), 1.51, 2)] + [TestCase(typeof(ModWindDown), 2, 0.5)] + [TestCase(typeof(ModWindDown), 0.99, 0.5)] + public void TestStoryboardSamplesPlaybackWithTimeRampMods(Type expectedMod, double initialRate, double finalRate) + { + AddStep("setup mod", () => + { + ModTimeRamp testedMod = (ModTimeRamp)Activator.CreateInstance(expectedMod).AsNonNull(); + testedMod.InitialRate.Value = initialRate; + testedMod.FinalRate.Value = finalRate; + storyboardMods = new[] { testedMod }; + }); + + createPlayerTest(); + skipIntro(); + + ModTimeRamp gameplayMod = null; + + AddUntilStep("mod speed change updated", () => + { + gameplayMod = Player.GameplayState.Mods.OfType<ModTimeRamp>().Single(); + return gameplayMod.SpeedChange.Value != initialRate; + }); + + AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => + sound.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == gameplayMod.SpeedChange.Value)); + } + + private void createPlayerTest() + { + CreateTest(null); + + AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); + waitUntilStoryboardSamplesPlay(); + } + + private void waitUntilStoryboardSamplesPlay() => AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + + private void allStoryboardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + + private void skipIntro() => AddStep("skip intro", () => InputManager.Key(Key.Space)); + private IEnumerable<DrawableStoryboardSample> allStoryboardSamples => Player.ChildrenOfType<DrawableStoryboardSample>(); protected override bool AllowFail => false; + protected override TestPlayer CreatePlayer(Ruleset ruleset) + { + SelectedMods.Value = SelectedMods.Value.Concat(storyboardMods).ToArray(); + return new TestPlayer(true, false); + } + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio); diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 779d72190d..a21647712d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -9,7 +9,6 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat; using osuTK.Input; @@ -207,7 +206,28 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestUserScrollOverride() + public void TestOverrideChatScrolling() + { + fillChat(); + + sendMessage(); + checkScrolledToBottom(); + + AddStep("Scroll to start", () => chatDisplay.ScrollContainer.ScrollToStart()); + + checkNotScrolledToBottom(); + sendMessage(); + checkNotScrolledToBottom(); + + AddStep("Scroll to bottom", () => chatDisplay.ScrollContainer.ScrollToEnd()); + + checkScrolledToBottom(); + sendMessage(); + checkScrolledToBottom(); + } + + [Test] + public void TestOverrideChatScrollingByUser() { fillChat(); @@ -314,9 +334,9 @@ namespace osu.Game.Tests.Visual.Online { } - protected DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First(); + public DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First(); - protected UserTrackingScrollContainer ScrollContainer => (UserTrackingScrollContainer)((Container)DrawableChannel.Child).Child; + public ChannelScrollContainer ScrollContainer => (ChannelScrollContainer)((Container)DrawableChannel.Child).Child; public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 221001e40b..7471b6acf2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -17,22 +18,33 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneHitEventTimingDistributionGraph : OsuTestScene { + private HitEventTimingDistributionGraph graph; + + private static readonly HitObject placeholder_object = new HitCircle(); + [Test] public void TestManyDistributedEvents() { createTest(CreateDistributedHitEvents()); + AddStep("add adjustment", () => graph.UpdateOffset(10)); + } + + [Test] + public void TestManyDistributedEventsOffset() + { + createTest(CreateDistributedHitEvents(-3.5)); } [Test] public void TestAroundCentre() { - createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); + createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] public void TestZeroTimeOffset() { - createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -47,9 +59,9 @@ namespace osu.Game.Tests.Visual.Ranking createTest(Enumerable.Range(0, 100).Select(i => { if (i % 2 == 0) - return new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null); + return new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null); - return new HitEvent(30, HitResult.Miss, new HitCircle(), new HitCircle(), null); + return new HitEvent(30, HitResult.Miss, placeholder_object, placeholder_object, null); }).ToList()); } @@ -62,7 +74,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new HitEventTimingDistributionGraph(events) + graph = new HitEventTimingDistributionGraph(events) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,10 +89,10 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < range * 2; i++) { - int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)); + int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10; for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); + hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null)); } return hitEvents; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 31bd3a203c..1ed6648131 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -119,7 +119,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure)); AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn)); - AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardState.Unavailable)); + AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable)); + AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable)); AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected)); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs new file mode 100644 index 0000000000..22a8fa8a46 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestScenePopupScreenTitle : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestPopupScreenTitle() + { + AddStep("create content", () => + { + Child = new PopupScreenTitle + { + Title = "Popup Screen Title", + Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)), + Close = () => { } + }; + }); + } + + [Test] + public void TestDisabledExit() + { + AddStep("create content", () => + { + Child = new PopupScreenTitle + { + Title = "Popup Screen Title", + Description = "This is a description." + }; + }); + } + } +} diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index 3949e84f4a..93c2fccbc7 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; @@ -83,7 +84,7 @@ namespace osu.Game.Beatmaps requestedUserId = api.LocalUser.Value.Id; // only query API for built-in rulesets - rulesets.AvailableRulesets.Where(ruleset => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo => + rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 07d2026c65..c279ce1220 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -140,7 +140,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); - SetDefault(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f); + SetDefault(OsuSetting.UIHoldActivationDelay, 200.0, 0.0, 500.0, 50.0); SetDefault(OsuSetting.IntroSequence, IntroSequence.Triangles); @@ -240,9 +240,9 @@ namespace osu.Game.Configuration }; } - public Func<Guid, string> LookupSkinName { private get; set; } + public Func<Guid, string> LookupSkinName { private get; set; } = _ => @"unknown"; - public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; } + public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; } = _ => @"unknown"; } // IMPORTANT: These are used in user configuration files. @@ -270,7 +270,13 @@ namespace osu.Game.Configuration MouseDisableButtons, MouseDisableWheel, ConfineMouseMode, + + /// <summary> + /// Globally applied audio offset. + /// This is added to the audio track's current time. Higher values will cause gameplay to occur earlier, relative to the audio track. + /// </summary> AudioOffset, + VolumeInactive, MenuMusic, MenuVoice, diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index f178a5c97b..13c25e45c8 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -72,6 +72,11 @@ namespace osu.Game.Extensions return result; } + /// <summary> + /// Check whether this <see cref="IRulesetInfo"/>'s online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania). + /// </summary> + public static bool IsLegacyRuleset(this IRulesetInfo ruleset) => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID; + /// <summary> /// Check whether the online ID of two <see cref="IBeatmapSetInfo"/>s match. /// </summary> diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 56ef87c1f4..7aed442800 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -3,12 +3,15 @@ #nullable enable +using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; namespace osu.Game.Graphics.Backgrounds @@ -20,6 +23,9 @@ namespace osu.Game.Graphics.Backgrounds [Resolved(CanBeNull = true)] private MusicController? musicController { get; set; } + [Resolved] + private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!; + public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1") : base(beatmap, fallbackTextureName) { @@ -39,7 +45,7 @@ namespace osu.Game.Graphics.Backgrounds { RelativeSizeAxes = Axes.Both, Volume = { Value = 0 }, - Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock } + Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock } }, AddInternal); } diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index fcf445a878..999dd183aa 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -30,12 +30,12 @@ namespace osu.Game.Graphics.Containers public Bindable<double> Progress = new BindableDouble(); - private Bindable<float> holdActivationDelay; + private Bindable<double> holdActivationDelay; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - holdActivationDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay); + holdActivationDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay); } protected void BeginConfirm() diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 0d543bdbc8..d331b818a1 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Screens; @@ -38,24 +39,24 @@ namespace osu.Game.Graphics.Containers private BackgroundScreenStack backgroundStack; - private bool allowScaling = true; + private RectangleF? customRect; + private bool customRectIsRelativePosition; /// <summary> - /// Whether user scaling preferences should be applied. Enabled by default. + /// Set a custom position and scale which overrides any user specification. /// </summary> - public bool AllowScaling + /// <param name="rect">A rectangle with positional and sizing information for this container to conform to. <c>null</c> will clear the custom rect and revert to user settings.</param> + /// <param name="relativePosition">Whether the position portion of the provided rect is in relative coordinate space or not.</param> + public void SetCustomRect(RectangleF? rect, bool relativePosition = false) { - get => allowScaling; - set - { - if (value == allowScaling) - return; + customRect = rect; + customRectIsRelativePosition = relativePosition; - allowScaling = value; - if (IsLoaded) Scheduler.AddOnce(updateSize); - } + if (IsLoaded) Scheduler.AddOnce(updateSize); } + private const float corner_radius = 10; + /// <summary> /// Create a new instance. /// </summary> @@ -69,7 +70,7 @@ namespace osu.Game.Graphics.Containers { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, - CornerRadius = 10, + CornerRadius = corner_radius, Child = content = new ScalingDrawSizePreservingFillContainer(targetMode != ScalingMode.Gameplay) }; } @@ -137,7 +138,7 @@ namespace osu.Game.Graphics.Containers private void updateSize() { - const float fade_time = 500; + const float duration = 500; if (targetMode == ScalingMode.Everything) { @@ -156,17 +157,31 @@ namespace osu.Game.Graphics.Containers backgroundStack.Push(new ScalingBackgroundScreen()); } - backgroundStack.FadeIn(fade_time); + backgroundStack.FadeIn(duration); } else - backgroundStack?.FadeOut(fade_time); + backgroundStack?.FadeOut(duration); } - bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode); + RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One); - var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; - var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; - bool requiresMasking = (scaling && targetSize != Vector2.One) + if (customRect != null) + { + sizableContainer.RelativePositionAxes = customRectIsRelativePosition ? Axes.Both : Axes.None; + + targetRect = customRect.Value; + } + else if (targetMode == null || scalingMode.Value == targetMode) + { + sizableContainer.RelativePositionAxes = Axes.Both; + + Vector2 scale = new Vector2(sizeX.Value, sizeY.Value); + Vector2 pos = new Vector2(posX.Value, posY.Value) * (Vector2.One - scale); + + targetRect = new RectangleF(pos, scale); + } + + bool requiresMasking = targetRect.Size != Vector2.One // For the top level scaling container, for now we apply masking if safe areas are in use. // In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas. || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero); @@ -174,8 +189,14 @@ namespace osu.Game.Graphics.Containers if (requiresMasking) sizableContainer.Masking = true; - sizableContainer.MoveTo(targetPosition, 500, Easing.OutQuart); - sizableContainer.ResizeTo(targetSize, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); + sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart); + sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart); + + // Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius. + // Masking and corner radius should likely only be applied at one point in the full game stack to fix this. + // An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything". + sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None) + .OnComplete(_ => { sizableContainer.Masking = requiresMasking; }); } private class ScalingBackgroundScreen : BackgroundScreenDefault diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index 0561051e35..44afaf77ea 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers /// </summary> public bool UserScrolling { get; private set; } - public void CancelUserScroll() => UserScrolling = false; - public UserTrackingScrollContainer() { } diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 0cc751ea21..03fad00e41 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -140,6 +140,7 @@ namespace osu.Game.Graphics.Cursor // Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird) channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75; channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range); + channel.Volume.Value = baseFrequency; channel.Play(); } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 333ae4f832..21c8dfcfa4 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface protected override void LoadComplete() { base.LoadComplete(); - CurrentNumber.BindValueChanged(current => updateTooltipText(current.NewValue), true); + CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true); } protected override bool OnHover(HoverEvent e) @@ -178,7 +178,7 @@ namespace osu.Game.Graphics.UserInterface { base.OnUserChange(value); playSample(value); - updateTooltipText(value); + TooltipText = getTooltipText(value); } private void playSample(T value) @@ -203,28 +203,22 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - private void updateTooltipText(T value) + private LocalisableString getTooltipText(T value) { if (CurrentNumber.IsInteger) - TooltipText = value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0"); - else - { - double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo); + return value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0"); - if (DisplayAsPercentage) - { - TooltipText = floatValue.ToString("0%"); - } - else - { - decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits); + double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo); - // Find the number of significant digits (we could have less than 5 after normalize()) - int significantDigits = FormatUtils.FindPrecision(decimalPrecision); + if (DisplayAsPercentage) + return floatValue.ToString("0%"); - TooltipText = floatValue.ToString($"N{significantDigits}"); - } - } + decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits); + + // Find the number of significant digits (we could have less than 5 after normalize()) + int significantDigits = FormatUtils.FindPrecision(decimalPrecision); + + return floatValue.ToString($"N{significantDigits}"); } protected override void UpdateAfterChildren() diff --git a/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs new file mode 100644 index 0000000000..5b7db09e77 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs @@ -0,0 +1,154 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class PopupScreenTitle : CompositeDrawable + { + public LocalisableString Title + { + set => titleSpriteText.Text = value; + } + + public LocalisableString Description + { + set => descriptionText.Text = value; + } + + public Action? Close + { + get => closeButton.Action; + set => closeButton.Action = value; + } + + private const float corner_radius = 14; + private const float main_area_height = 70; + + private readonly Container underlayContainer; + private readonly Box underlayBackground; + private readonly Container contentContainer; + private readonly Box contentBackground; + private readonly OsuSpriteText titleSpriteText; + private readonly OsuTextFlowContainer descriptionText; + private readonly IconButton closeButton; + + public PopupScreenTitle() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 70, + Top = -corner_radius + }, + Children = new Drawable[] + { + underlayContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = main_area_height + 2 * corner_radius, + CornerRadius = corner_radius, + Masking = true, + BorderThickness = 2, + Child = underlayBackground = new Box + { + RelativeSizeAxes = Axes.Both + } + }, + contentContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = main_area_height + corner_radius, + CornerRadius = corner_radius, + Masking = true, + BorderThickness = 2, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Colour4.Black.Opacity(0.1f), + Offset = new Vector2(0, 1), + Radius = 3 + }, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Top = corner_radius }, + Padding = new MarginPadding { Horizontal = 100 }, + Children = new Drawable[] + { + titleSpriteText = new OsuSpriteText + { + Font = OsuFont.TorusAlternate.With(size: 20) + }, + descriptionText = new OsuTextFlowContainer(t => + { + t.Font = OsuFont.Default.With(size: 12); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + closeButton = new IconButton + { + Icon = FontAwesome.Solid.Times, + Scale = new Vector2(0.6f), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding + { + Right = 21, + Top = corner_radius + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + underlayContainer.BorderColour = ColourInfo.GradientVertical(Colour4.Black, colourProvider.Dark4); + underlayBackground.Colour = colourProvider.Dark4; + + contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Dark3, colourProvider.Dark1); + contentBackground.Colour = colourProvider.Dark3; + + closeButton.IconHoverColour = colourProvider.Highlight1; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/TimeSlider.cs b/osu.Game/Graphics/UserInterface/TimeSlider.cs new file mode 100644 index 0000000000..82b02f1b48 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/TimeSlider.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Graphics.UserInterface +{ + /// <summary> + /// A slider bar which displays a millisecond time value. + /// </summary> + public class TimeSlider : OsuSliderBar<double> + { + public override LocalisableString TooltipText => $"{Current.Value:N0} ms"; + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 4da8d6a554..fd64cc2056 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => Component.Text = value; } - public Container TabbableContentContainer + public CompositeDrawable TabbableContentContainer { set => Component.TabbableContentContainer = value; } diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs index 7b2a9e50b2..632a1ad0ea 100644 --- a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs @@ -29,6 +29,16 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString CalibrateUsingLastPlay => new TranslatableString(getKey(@"calibrate_using_last_play"), @"Calibrate using last play"); + /// <summary> + /// "(hit objects appear later)" + /// </summary> + public static LocalisableString HitObjectsAppearLater => new TranslatableString(getKey(@"hit_objects_appear_later"), @"(hit objects appear later)"); + + /// <summary> + /// "(hit objects appear earlier)" + /// </summary> + public static LocalisableString HitObjectsAppearEarlier => new TranslatableString(getKey(@"hit_objects_appear_earlier"), @"(hit objects appear earlier)"); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} \ No newline at end of file +} diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 996a1350eb..1c9aa64df5 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -54,6 +54,11 @@ namespace osu.Game.Localisation /// </summary> public static LocalisableString Resolution => new TranslatableString(getKey(@"resolution"), @"Resolution"); + /// <summary> + /// "Display" + /// </summary> + public static LocalisableString Display => new TranslatableString(getKey(@"display"), @"Display"); + /// <summary> /// "UI scaling" /// </summary> diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs new file mode 100644 index 0000000000..8e53f8e88c --- /dev/null +++ b/osu.Game/Localisation/LeaderboardStrings.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 static class LeaderboardStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.Leaderboard"; + + /// <summary> + /// "Couldn't fetch scores!" + /// </summary> + public static LocalisableString CouldntFetchScores => new TranslatableString(getKey(@"couldnt_fetch_scores"), @"Couldn't fetch scores!"); + + /// <summary> + /// "Please select a beatmap!" + /// </summary> + public static LocalisableString PleaseSelectABeatmap => new TranslatableString(getKey(@"please_select_a_beatmap"), @"Please select a beatmap!"); + + /// <summary> + /// "Leaderboards are not available for this ruleset!" + /// </summary> + public static LocalisableString LeaderboardsAreNotAvailableForThisRuleset => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_ruleset"), @"Leaderboards are not available for this ruleset!"); + + /// <summary> + /// "Leaderboards are not available for this beatmap!" + /// </summary> + public static LocalisableString LeaderboardsAreNotAvailableForThisBeatmap => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_beatmap"), @"Leaderboards are not available for this beatmap!"); + + /// <summary> + /// "No records yet!" + /// </summary> + public static LocalisableString NoRecordsYet => new TranslatableString(getKey(@"no_records_yet"), @"No records yet!"); + + /// <summary> + /// "Please sign in to view online leaderboards!" + /// </summary> + public static LocalisableString PleaseSignInToViewOnlineLeaderboards => new TranslatableString(getKey(@"please_sign_in_to_view_online_leaderboards"), @"Please sign in to view online leaderboards!"); + + /// <summary> + /// "Please invest in an osu!supporter tag to view this leaderboard!" + /// </summary> + public static LocalisableString PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard => new TranslatableString(getKey(@"please_invest_in_an_osu_supporter_tag_to_view_this_leaderboard"), @"Please invest in an osu!supporter tag to view this leaderboard!"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Online/API/Requests/GetWikiRequest.cs b/osu.Game/Online/API/Requests/GetWikiRequest.cs index 248fcc03e3..09571ab0a8 100644 --- a/osu.Game/Online/API/Requests/GetWikiRequest.cs +++ b/osu.Game/Online/API/Requests/GetWikiRequest.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Extensions; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests @@ -8,14 +10,14 @@ namespace osu.Game.Online.API.Requests public class GetWikiRequest : APIRequest<APIWikiPage> { private readonly string path; - private readonly string locale; + private readonly Language language; - public GetWikiRequest(string path, string locale = "en") + public GetWikiRequest(string path, Language language = Language.en) { this.path = path; - this.locale = locale; + this.language = language; } - protected override string Target => $"wiki/{locale}/{path}"; + protected override string Target => $"wiki/{language.ToCultureCode()}/{path}"; } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 5dd3e46b4a..c94a6d3361 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Online.Leaderboards { @@ -311,25 +312,28 @@ namespace osu.Game.Online.Leaderboards switch (state) { case LeaderboardState.NetworkFailure: - return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) + return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync) { Action = RefetchScores }; case LeaderboardState.NoneSelected: - return new MessagePlaceholder(@"Please select a beatmap!"); + return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap); - case LeaderboardState.Unavailable: - return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!"); + case LeaderboardState.RulesetUnavailable: + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset); + + case LeaderboardState.BeatmapUnavailable: + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap); case LeaderboardState.NoScores: - return new MessagePlaceholder(@"No records yet!"); + return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet); case LeaderboardState.NotLoggedIn: - return new LoginPlaceholder(@"Please sign in to view online leaderboards!"); + return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards); case LeaderboardState.NotSupporter: - return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!"); + return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard); case LeaderboardState.Retrieving: return null; diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index 75e2c6e6db..6b07500a98 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -8,7 +8,8 @@ namespace osu.Game.Online.Leaderboards Success, Retrieving, NetworkFailure, - Unavailable, + BeatmapUnavailable, + RulesetUnavailable, NoneSelected, NoScores, NotLoggedIn, diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index f8a326a52e..d03b3d8ffc 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Overlays; namespace osu.Game.Online.Placeholders @@ -12,7 +13,7 @@ namespace osu.Game.Online.Placeholders [Resolved(CanBeNull = true)] private LoginOverlay login { get; set; } - public LoginPlaceholder(string actionMessage) + public LoginPlaceholder(LocalisableString actionMessage) : base(actionMessage, FontAwesome.Solid.UserLock) { Action = () => login?.Show(); diff --git a/osu.Game/Overlays/Chat/ChannelScrollContainer.cs b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs new file mode 100644 index 0000000000..58b2b9a075 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelScrollContainer.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Overlays.Chat +{ + /// <summary> + /// An <see cref="OsuScrollContainer"/> with functionality to automatically scroll whenever the maximum scrollable distance increases. + /// </summary> + public class ChannelScrollContainer : OsuScrollContainer + { + /// <summary> + /// The chat will be automatically scrolled to end if and only if + /// the distance between the current scroll position and the end of the scroll + /// is less than this value. + /// </summary> + private const float auto_scroll_leniency = 10f; + + /// <summary> + /// Whether to keep this container scrolled to end on new content. + /// </summary> + /// <remarks> + /// This is specifically controlled by whether the latest scroll operation made the container scrolled to end. + /// </remarks> + private bool trackNewContent = true; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (trackNewContent && !IsScrolledToEnd()) + ScrollToEnd(); + } + + private void updateTrackState() => trackNewContent = IsScrolledToEnd(auto_scroll_leniency); + + // todo: we may eventually want this encapsulated in a "OnScrollChange" event handler method provided by ScrollContainer. + // important to note that this intentionally doesn't consider OffsetScrollPosition, but could make it do so with side changes. + + #region Scroll handling + + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + { + base.OnUserScroll(value, animated, distanceDecay); + updateTrackState(); + } + + public new void ScrollIntoView(Drawable d, bool animated = true) + { + base.ScrollIntoView(d, animated); + updateTrackState(); + } + + public new void ScrollToStart(bool animated = true, bool allowDuringDrag = false) + { + base.ScrollToStart(animated, allowDuringDrag); + updateTrackState(); + } + + public new void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) + { + base.ScrollToEnd(animated, allowDuringDrag); + updateTrackState(); + } + + #endregion + } +} diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 41e70bbfae..6220beeb82 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -11,9 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Utils; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; @@ -236,52 +234,5 @@ namespace osu.Game.Overlays.Chat }; } } - - /// <summary> - /// An <see cref="OsuScrollContainer"/> with functionality to automatically scroll whenever the maximum scrollable distance increases. - /// </summary> - private class ChannelScrollContainer : UserTrackingScrollContainer - { - /// <summary> - /// The chat will be automatically scrolled to end if and only if - /// the distance between the current scroll position and the end of the scroll - /// is less than this value. - /// </summary> - private const float auto_scroll_leniency = 10f; - - private float? lastExtent; - - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) - { - base.OnUserScroll(value, animated, distanceDecay); - lastExtent = null; - } - - protected override void Update() - { - base.Update(); - - // If the user has scrolled to the bottom of the container, we should resume tracking new content. - if (UserScrolling && IsScrolledToEnd(auto_scroll_leniency)) - CancelUserScroll(); - - // If the user hasn't overridden our behaviour and there has been new content added to the container, we should update our scroll position to track it. - bool requiresScrollUpdate = !UserScrolling && (lastExtent == null || Precision.AlmostBigger(ScrollableExtent, lastExtent.Value)); - - if (requiresScrollUpdate) - { - // Schedule required to allow FillFlow to be the correct size. - Schedule(() => - { - if (!UserScrolling) - { - if (Current < ScrollableExtent) - ScrollToEnd(); - lastExtent = ScrollableExtent; - } - }); - } - } - } } } diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs index 5f513582e5..922f3832e4 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -63,11 +64,17 @@ namespace osu.Game.Overlays.Profile.Header }; } + private CancellationTokenSource cancellationTokenSource; + private void updateDisplay(APIUser user) { - var badges = user.Badges; + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + badgeFlowContainer.Clear(); + var badges = user.Badges; + if (badges?.Length > 0) { Show(); @@ -79,7 +86,7 @@ namespace osu.Game.Overlays.Profile.Header { // load in stable order regardless of async load order. badgeFlowContainer.Insert(displayIndex, asyncBadge); - }); + }, cancellationTokenSource.Token); } } else @@ -87,5 +94,11 @@ namespace osu.Game.Overlays.Profile.Header Hide(); } } + + protected override void Dispose(bool isDisposing) + { + cancellationTokenSource?.Cancel(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 9345d3fcc7..673252a99e 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { Children = new Drawable[] { - new SettingsSlider<double, OffsetSlider> + new SettingsSlider<double, TimeSlider> { LabelText = AudioSettingsStrings.AudioOffset, Current = config.GetBindable<double>(OsuSetting.AudioOffset), @@ -35,10 +35,5 @@ namespace osu.Game.Overlays.Settings.Sections.Audio } }; } - - private class OffsetSlider : OsuSliderBar<double> - { - public override LocalisableString TooltipText => Current.Value.ToString(@"0ms"); - } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index adf1453d1a..602ace6dea 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private FillFlowContainer<SettingsSlider<float>> scalingSettings; - private readonly IBindable<Display> currentDisplay = new Bindable<Display>(); + private readonly Bindable<Display> currentDisplay = new Bindable<Display>(); private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>(); private Bindable<ScalingMode> scalingMode; @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private OsuGameBase game { get; set; } private SettingsDropdown<Size> resolutionDropdown; + private SettingsDropdown<Display> displayDropdown; private SettingsDropdown<WindowMode> windowModeDropdown; private Bindable<float> scalingPositionX; @@ -72,6 +73,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics ItemSource = windowModes, Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode), }, + displayDropdown = new DisplaySettingsDropdown + { + LabelText = GraphicsSettingsStrings.Display, + Items = host.Window?.Displays, + Current = currentDisplay, + }, resolutionDropdown = new ResolutionSettingsDropdown { LabelText = GraphicsSettingsStrings.Resolution, @@ -142,7 +149,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown.Current.BindValueChanged(mode => { - updateResolutionDropdown(); + updateDisplayModeDropdowns(); windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default; }, true); @@ -168,7 +175,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics .Distinct()); } - updateResolutionDropdown(); + updateDisplayModeDropdowns(); }), true); scalingMode.BindValueChanged(mode => @@ -183,12 +190,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics // initial update bypasses transforms updateScalingModeVisibility(); - void updateResolutionDropdown() + void updateDisplayModeDropdowns() { if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen) resolutionDropdown.Show(); else resolutionDropdown.Hide(); + + if (displayDropdown.Items.Count() > 1) + displayDropdown.Show(); + else + displayDropdown.Hide(); } void updateScalingModeVisibility() @@ -243,6 +255,19 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics public override LocalisableString TooltipText => base.TooltipText + "x"; } + private class DisplaySettingsDropdown : SettingsDropdown<Display> + { + protected override OsuDropdown<Display> CreateDropdown() => new DisplaySettingsDropdownControl(); + + private class DisplaySettingsDropdownControl : DropdownControl + { + protected override LocalisableString GenerateItemText(Display item) + { + return $"{item.Index}: {item.Name} ({item.Bounds.Width}x{item.Bounds.Height})"; + } + } + } + private class ResolutionSettingsDropdown : SettingsDropdown<Size> { protected override OsuDropdown<Size> CreateDropdown() => new ResolutionDropdownControl(); diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 0afbed5df5..59894cbcae 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -35,18 +35,13 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = UserInterfaceStrings.Parallax, Current = config.GetBindable<bool>(OsuSetting.MenuParallax) }, - new SettingsSlider<float, TimeSlider> + new SettingsSlider<double, TimeSlider> { LabelText = UserInterfaceStrings.HoldToConfirmActivationTime, - Current = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay), + Current = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay), KeyboardStep = 50 }, }; } - - private class TimeSlider : OsuSliderBar<float> - { - public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms"; - } } } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 08321f68fe..b4178359a4 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -22,8 +22,9 @@ namespace osu.Game.Overlays { public class SettingsToolboxGroup : Container, IExpandable { + public const int CONTAINER_WIDTH = 270; + private const float transition_duration = 250; - private const int container_width = 270; private const int border_thickness = 2; private const int header_height = 30; private const int corner_radius = 5; @@ -49,7 +50,7 @@ namespace osu.Game.Overlays public SettingsToolboxGroup(string title) { AutoSizeAxes = Axes.Y; - Width = container_width; + Width = CONTAINER_WIDTH; Masking = true; CornerRadius = corner_radius; BorderColour = Color4.Black; @@ -201,7 +202,5 @@ namespace osu.Game.Overlays } protected override Container<Drawable> Content => content; - - protected override bool OnMouseDown(MouseDownEvent e) => true; } } diff --git a/osu.Game/Overlays/WikiOverlay.cs b/osu.Game/Overlays/WikiOverlay.cs index 44713d637d..4015d8e196 100644 --- a/osu.Game/Overlays/WikiOverlay.cs +++ b/osu.Game/Overlays/WikiOverlay.cs @@ -7,6 +7,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -100,7 +101,12 @@ namespace osu.Game.Overlays cancellationToken?.Cancel(); request?.Cancel(); - request = new GetWikiRequest(e.NewValue); + string[] values = e.NewValue.Split('/', 2); + + if (values.Length > 1 && LanguageExtensions.TryParseCultureCode(values[0], out var language)) + request = new GetWikiRequest(values[1], language); + else + request = new GetWikiRequest(e.NewValue); Loading.Show(); diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs index 9baa252caf..7cf480a11b 100644 --- a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs +++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs @@ -5,8 +5,19 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { + /// <summary> + /// An interface for <see cref="Mod"/>s that are updated every frame by a <see cref="Playfield"/>. + /// </summary> public interface IUpdatableByPlayfield : IApplicableMod { + /// <summary> + /// Update this <see cref="Mod"/>. + /// </summary> + /// <param name="playfield">The main <see cref="Playfield"/></param> + /// <remarks> + /// This method is called once per frame during gameplay by the main <see cref="Playfield"/> only. + /// To access nested <see cref="Playfield"/>s, use <see cref="Playfield.NestedPlayfields"/>. + /// </remarks> void Update(Playfield playfield); } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d0bbf859af..30e71dde1c 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -79,6 +79,11 @@ namespace osu.Game.Rulesets.UI private readonly List<Playfield> nestedPlayfields = new List<Playfield>(); + /// <summary> + /// Whether this <see cref="Playfield"/> is nested in another <see cref="Playfield"/>. + /// </summary> + public bool IsNested { get; private set; } + /// <summary> /// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s. /// </summary> @@ -206,6 +211,8 @@ namespace osu.Game.Rulesets.UI /// <param name="otherPlayfield">The <see cref="Playfield"/> to add.</param> protected void AddNested(Playfield otherPlayfield) { + otherPlayfield.IsNested = true; + otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); @@ -229,7 +236,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - if (mods != null) + if (!IsNested && mods != null) { foreach (var mod in mods) { diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 1326395695..f0ead05280 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -7,9 +7,9 @@ using System.Linq; using System.Text; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; -using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -48,7 +48,7 @@ namespace osu.Game.Scoring.Legacy if (beatmap == null && !score.Replay.Frames.All(f => f is LegacyReplayFrame)) throw new ArgumentException(@"Beatmap must be provided if frames are not already legacy frames.", nameof(beatmap)); - if (score.ScoreInfo.Ruleset.OnlineID < 0 || score.ScoreInfo.Ruleset.OnlineID > ILegacyRuleset.MAX_LEGACY_RULESET_ID) + if (!score.ScoreInfo.Ruleset.IsLegacyRuleset()) throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 7d52645aa1..fc0952d4f0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -75,9 +75,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load() { + FillFlowContainer flow; + Children = new Drawable[] { - new FillFlowContainer + flow = new FillFlowContainer { Width = 200, Direction = FillDirection.Vertical, @@ -94,6 +96,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }; + bank.TabbableContentContainer = flow; + volume.TabbableContentContainer = flow; + // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index e25d83cfb0..0cf2cf6c54 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Timing set => slider.KeyboardStep = value; } + public CompositeDrawable TabbableContentContainer + { + set => textBox.TabbableContentContainer = value; + } + private readonly BindableWithCurrent<T?> current = new BindableWithCurrent<T?>(); public Bindable<T?> Current diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index b0208a0ae8..e2d79b4015 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Menu protected override BackgroundScreen CreateBackground() => background; - private Bindable<float> holdDelay; + private Bindable<double> holdDelay; private Bindable<bool> loginDisplayed; private ExitConfirmOverlay exitConfirmOverlay; @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) { - holdDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay); + holdDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed); if (host.CanExit) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index f8cedddfbe..5a3ef1e9d3 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Mods; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; @@ -18,6 +20,8 @@ namespace osu.Game.Screens.Play public Container OverlayLayerContainer { get; private set; } private readonly Storyboard storyboard; + private readonly IReadOnlyList<Mod> mods; + private DrawableStoryboard drawableStoryboard; /// <summary> @@ -28,9 +32,10 @@ namespace osu.Game.Screens.Play /// </remarks> public IBindable<bool> HasStoryboardEnded = new BindableBool(true); - public DimmableStoryboard(Storyboard storyboard) + public DimmableStoryboard(Storyboard storyboard, IReadOnlyList<Mod> mods) { this.storyboard = storyboard; + this.mods = mods; } [BackgroundDependencyLoader] @@ -57,7 +62,7 @@ namespace osu.Game.Screens.Play if (!ShowStoryboard.Value && !IgnoreUserSettings.Value) return; - drawableStoryboard = storyboard.CreateDrawable(); + drawableStoryboard = storyboard.CreateDrawable(mods); HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded); if (async) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 430f001427..4087011933 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private OsuConfigManager config { get; set; } - private Bindable<float> activationDelay; + private Bindable<double> activationDelay; protected override void LoadComplete() { - activationDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay); + activationDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay); activationDelay.BindValueChanged(v => { text.Text = v.NewValue > 0 diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 86ea412488..b6f576ff2b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -359,7 +359,7 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); private Drawable createUnderlayComponents() => - DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; + DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both }; private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay) { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index f6d63a8ec5..41eb822e39 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -143,6 +143,8 @@ namespace osu.Game.Screens.Play muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); + const float padding = 25; + InternalChildren = new Drawable[] { (content = new LogoTrackingContainer @@ -158,20 +160,27 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - PlayerSettings = new FillFlowContainer<PlayerSettingsGroup> + new OsuScrollContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding(25), - Children = new PlayerSettingsGroup[] + RelativeSizeAxes = Axes.Y, + Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2, + Padding = new MarginPadding { Vertical = padding }, + Masking = false, + Child = PlayerSettings = new FillFlowContainer<PlayerSettingsGroup> { - VisualSettings = new VisualSettings(), - AudioSettings = new AudioSettings(), - new InputSettings() - } + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Padding = new MarginPadding { Horizontal = padding }, + Children = new PlayerSettingsGroup[] + { + VisualSettings = new VisualSettings(), + AudioSettings = new AudioSettings(), + new InputSettings() + } + }, }, idleTracker = new IdleTracker(750), }), diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c05c5af10d..c00b2f56dc 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; @@ -50,6 +52,8 @@ namespace osu.Game.Screens.Play.PlayerSettings private OsuColour colours { get; set; } = null!; private double lastPlayAverage; + private double lastPlayBeatmapOffset; + private HitEventTimingDistributionGraph? lastPlayGraph; private SettingsButton? useAverageButton; @@ -70,7 +74,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Spacing = new Vector2(10), Children = new Drawable[] { - new PlayerSliderBar<double> + new OffsetSliderBar { KeyboardStep = 5, LabelText = BeatmapOffsetControlStrings.BeatmapOffset, @@ -87,6 +91,28 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } + public class OffsetSliderBar : PlayerSliderBar<double> + { + protected override Drawable CreateControl() => new CustomSliderBar(); + + protected class CustomSliderBar : SliderBar + { + public override LocalisableString TooltipText => + Current.Value == 0 + ? new TranslatableString("_", @"{0} ms", base.TooltipText) + : new TranslatableString("_", @"{0} ms {1}", base.TooltipText, getEarlyLateText(Current.Value)); + + private LocalisableString getEarlyLateText(double value) + { + Debug.Assert(value != 0); + + return value > 0 + ? BeatmapOffsetControlStrings.HitObjectsAppearEarlier + : BeatmapOffsetControlStrings.HitObjectsAppearLater; + } + } + } + protected override void LoadComplete() { base.LoadComplete(); @@ -107,6 +133,12 @@ namespace osu.Game.Screens.Play.PlayerSettings void updateOffset() { + // the last play graph is relative to the offset at the point of the last play, so we need to factor that out. + double adjustmentSinceLastPlay = lastPlayBeatmapOffset - Current.Value; + + // Negative is applied here because the play graph is considering a hit offset, not track (as we currently use for clocks). + lastPlayGraph?.UpdateOffset(-adjustmentSinceLastPlay); + // ensure the previous write has completed. ignoring performance concerns, if we don't do this, the async writes could be out of sequence. if (realmWriteTask?.IsCompleted == false) { @@ -115,7 +147,9 @@ namespace osu.Game.Screens.Play.PlayerSettings } if (useAverageButton != null) - useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, -Current.Value, Current.Precision / 2); + { + useAverageButton.Enabled.Value = !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); + } realmWriteTask = realm.WriteAsync(r => { @@ -172,10 +206,11 @@ namespace osu.Game.Screens.Play.PlayerSettings } lastPlayAverage = average; + lastPlayBeatmapOffset = Current.Value; referenceScoreContainer.AddRange(new Drawable[] { - new HitEventTimingDistributionGraph(hitEvents) + lastPlayGraph = new HitEventTimingDistributionGraph(hitEvents) { RelativeSizeAxes = Axes.X, Height = 50, @@ -184,7 +219,7 @@ namespace osu.Game.Screens.Play.PlayerSettings useAverageButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, - Action = () => Current.Value = -lastPlayAverage + Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage }, }); } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index 57ffe16f76..3f1a5bc0ac 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -15,13 +15,15 @@ namespace osu.Game.Screens.Play.PlayerSettings { public OsuSliderBar<T> Bar => (OsuSliderBar<T>)Control; - protected override Drawable CreateControl() => new SliderBar - { - RelativeSizeAxes = Axes.X - }; + protected override Drawable CreateControl() => new SliderBar(); - private class SliderBar : OsuSliderBar<T> + protected class SliderBar : OsuSliderBar<T> { + public SliderBar() + { + RelativeSizeAxes = Axes.X; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index 824c0072e3..a935ce49eb 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -4,10 +4,10 @@ using System; using System.Diagnostics; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; -using osu.Game.Rulesets; using osu.Game.Scoring; namespace osu.Game.Screens.Play @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Play if (beatmapId <= 0) return null; - if (rulesetId < 0 || rulesetId > ILegacyRuleset.MAX_LEGACY_RULESET_ID) + if (!Ruleset.Value.IsLegacyRuleset()) return null; return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash); diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 93885b6e02..f7c9d36cc4 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Statistics { @@ -40,6 +41,9 @@ namespace osu.Game.Screens.Ranking.Statistics /// </summary> private const float axis_points = 5; + /// <summary> + /// The currently displayed hit events. + /// </summary> private readonly IReadOnlyList<HitEvent> hitEvents; /// <summary> @@ -51,123 +55,223 @@ namespace osu.Game.Screens.Ranking.Statistics this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList(); } + private int[] bins; + private double binSize; + private double hitOffset; + + private Bar[] barDrawables; + [BackgroundDependencyLoader] private void load() { if (hitEvents == null || hitEvents.Count == 0) return; - int[] bins = new int[total_timing_distribution_bins]; + bins = new int[total_timing_distribution_bins]; - double binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); + binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); // Prevent div-by-0 by enforcing a minimum bin size binSize = Math.Max(1, binSize); + Scheduler.AddOnce(updateDisplay); + } + + public void UpdateOffset(double hitOffset) + { + this.hitOffset = hitOffset; + Scheduler.AddOnce(updateDisplay); + } + + private void updateDisplay() + { + bool roundUp = true; + + Array.Clear(bins, 0, bins.Length); + foreach (var e in hitEvents) { - int binOffset = (int)Math.Round(e.TimeOffset / binSize, MidpointRounding.AwayFromZero); - bins[timing_distribution_centre_bin_index + binOffset]++; + double time = e.TimeOffset + hitOffset; + + double binOffset = time / binSize; + + // .NET's round midpoint handling doesn't provide a behaviour that works amazingly for display + // purposes here. We want midpoint rounding to roughly distribute evenly to each adjacent bucket + // so the easiest way is to cycle between downwards and upwards rounding as we process events. + if (Math.Abs(binOffset - (int)binOffset) == 0.5) + { + binOffset = (int)binOffset + Math.Sign(binOffset) * (roundUp ? 1 : 0); + roundUp = !roundUp; + } + + int index = timing_distribution_centre_bin_index + (int)Math.Round(binOffset, MidpointRounding.AwayFromZero); + + // may be out of range when applying an offset. for such cases we can just drop the results. + if (index >= 0 && index < bins.Length) + bins[index]++; } - int maxCount = bins.Max(); - var bars = new Drawable[total_timing_distribution_bins]; - for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = Math.Max(0.05f, (float)bins[i] / maxCount) }; - - Container axisFlow; - - InternalChild = new GridContainer + if (barDrawables != null) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.8f, - Content = new[] + for (int i = 0; i < barDrawables.Length; i++) { - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] { bars } - } - }, - new Drawable[] - { - axisFlow = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - }, - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), + barDrawables[i].UpdateOffset(bins[i]); } - }; - - // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - double maxValue = timing_distribution_bins * binSize; - double axisValueStep = maxValue / axis_points; - - axisFlow.Add(new OsuSpriteText + } + else { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "0", - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); + int maxCount = bins.Max(); + barDrawables = new Bar[total_timing_distribution_bins]; - for (int i = 1; i <= axis_points; i++) - { - double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); - float alpha = 1f - position * 0.8f; + for (int i = 0; i < barDrawables.Length; i++) + barDrawables[i] = new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index); + + Container axisFlow; + + const float axis_font_size = 12; + + InternalChild = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.8f, + Content = new[] + { + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { barDrawables } + } + }, + new Drawable[] + { + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + Height = axis_font_size, + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }; + + // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + double maxValue = timing_distribution_bins * binSize; + double axisValueStep = maxValue / axis_points; axisFlow.Add(new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -position / 2, - Alpha = alpha, - Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + Text = "0", + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) }); - axisFlow.Add(new OsuSpriteText + for (int i = 1; i <= axis_points; i++) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = position / 2, - Alpha = alpha, - Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + }); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold) + }); + } } } private class Bar : CompositeDrawable { - public Bar() + private readonly float value; + private readonly float maxValue; + + private readonly Circle boxOriginal; + private Circle boxAdjustment; + + private const float minimum_height = 0.05f; + + public Bar(float value, float maxValue, bool isCentre) { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; + this.value = value; + this.maxValue = maxValue; RelativeSizeAxes = Axes.Both; + Masking = true; - Padding = new MarginPadding { Horizontal = 1 }; - - InternalChild = new Circle + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#66FFCC") + boxOriginal = new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = isCentre ? Color4.White : Color4Extensions.FromHex("#66FFCC"), + Height = minimum_height, + }, }; } + + private const double duration = 300; + + protected override void LoadComplete() + { + base.LoadComplete(); + + float height = Math.Clamp(value / maxValue, minimum_height, 1); + + if (height > minimum_height) + boxOriginal.ResizeHeightTo(height, duration, Easing.OutQuint); + } + + public void UpdateOffset(float adjustment) + { + bool hasAdjustment = adjustment != value && adjustment / maxValue >= minimum_height; + + if (boxAdjustment == null) + { + if (!hasAdjustment) + return; + + AddInternal(boxAdjustment = new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Colour = Color4.Yellow, + Blending = BlendingParameters.Additive, + Alpha = 0.6f, + Height = 0, + }); + } + + boxAdjustment.ResizeHeightTo(Math.Clamp(adjustment / maxValue, minimum_height, 1), duration, Easing.OutQuint); + boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 760915b528..a000cfd5fc 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -16,6 +16,9 @@ namespace osu.Game.Screens.Select.Carousel { public class SetPanelContent : CompositeDrawable { + // Disallow interacting with difficulty icons on a panel until the panel has been selected. + public override bool PropagatePositionalInputSubTree => carouselSet.State.Value == CarouselItemState.Selected; + private readonly CarouselBeatmapSet carouselSet; public SetPanelContent(CarouselBeatmapSet carouselSet) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 907a2c9bda..eb0addd377 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Leaderboards; @@ -98,6 +99,7 @@ namespace osu.Game.Screens.Select.Leaderboards protected override APIRequest FetchScores(CancellationToken cancellationToken) { var fetchBeatmapInfo = BeatmapInfo; + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; if (fetchBeatmapInfo == null) { @@ -117,9 +119,15 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } + if (!fetchRuleset.IsLegacyRuleset()) + { + SetErrorState(LeaderboardState.RulesetUnavailable); + return null; + } + if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) { - SetErrorState(LeaderboardState.Unavailable); + SetErrorState(LeaderboardState.BeatmapUnavailable); return null; } @@ -137,7 +145,7 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - var req = new GetScoresRequest(fetchBeatmapInfo, ruleset.Value ?? fetchBeatmapInfo.Ruleset, Scope, requestMods); + var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); req.Success += r => { diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs index 935d2756fb..ce9afd650a 100644 --- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs @@ -23,6 +23,8 @@ namespace osu.Game.Skinning.Editor { public class SkinComponentToolbox : ScrollingToolboxGroup { + public const float WIDTH = 200; + public Action<Type> RequestPlacement; private const float component_display_scale = 0.8f; @@ -41,7 +43,7 @@ namespace osu.Game.Skinning.Editor : base("Components", height) { RelativeSizeAxes = Axes.None; - Width = 200; + Width = WIDTH; } [BackgroundDependencyLoader] diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index 86854ab6ff..61c363b019 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -100,30 +101,14 @@ namespace osu.Game.Skinning.Editor { if (visibility.NewValue == Visibility.Visible) { - updateMasking(); - target.AllowScaling = false; - target.RelativePositionAxes = Axes.Both; - - target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); - target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); + target.SetCustomRect(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true); } else { - target.AllowScaling = true; - - target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => updateMasking()); - target.MoveToX(0f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint); + target.SetCustomRect(null); } } - private void updateMasking() - { - if (skinEditor == null) - return; - - target.Masking = skinEditor.State.Value == Visibility.Visible; - } - public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) { } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index e6528a83bd..a0fb7b0b4a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 System.Threading; using osuTK; @@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; using osu.Game.Stores; @@ -50,14 +53,18 @@ namespace osu.Game.Storyboards.Drawables private double? lastEventEndTime; + [Cached(typeof(IReadOnlyList<Mod>))] + public IReadOnlyList<Mod> Mods { get; } + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public DrawableStoryboard(Storyboard storyboard) + public DrawableStoryboard(Storyboard storyboard, IReadOnlyList<Mod> mods = null) { Storyboard = storyboard; + Mods = mods ?? Array.Empty<Mod>(); Size = new Vector2(640, 480); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 672274a2ad..4e3f72512c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -28,17 +28,20 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sampleInfo.StartTime; } - [Resolved] - private IBindable<IReadOnlyList<Mod>> mods { get; set; } + [Resolved(CanBeNull = true)] + private IReadOnlyList<Mod> mods { get; set; } protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); - foreach (var mod in mods.Value.OfType<IApplicableToSample>()) + if (mods != null) { - foreach (var sample in DrawableSamples) - mod.ApplyToSample(sample); + foreach (var mod in mods.OfType<IApplicableToSample>()) + { + foreach (var sample in DrawableSamples) + mod.ApplyToSample(sample); + } } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index c4864c0334..2faed98ae0 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Extensions; +using osu.Game.Rulesets.Mods; using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; @@ -90,8 +91,8 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IWorkingBeatmap working = null) => - new DrawableStoryboard(this); + public DrawableStoryboard CreateDrawable(IReadOnlyList<Mod> mods = null) => + new DrawableStoryboard(this, mods); public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) { diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 368f792e28..d463905cf4 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - /// <summary> - /// Mods from *player* (not OsuScreen). - /// </summary> public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods; public new HUDOverlay HUDOverlay => base.HUDOverlay; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7dfd099df1..d86fbc693e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,8 +36,8 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Realm" Version="10.9.0" /> - <PackageReference Include="ppy.osu.Framework" Version="2022.223.0" /> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.211.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2022.304.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> <PackageReference Include="Sentry" Version="3.14.0" /> <PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="NUnit" Version="3.13.2" /> diff --git a/osu.iOS.props b/osu.iOS.props index 80600655aa..c37692f0d8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,8 +61,8 @@ <Reference Include="System.Net.Http" /> </ItemGroup> <ItemGroup Label="Package References"> - <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.223.0" /> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.211.0" /> + <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.304.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> </ItemGroup> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <PropertyGroup> @@ -84,7 +84,7 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> - <PackageReference Include="ppy.osu.Framework" Version="2022.223.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2022.304.0" /> <PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />