diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ab754e51f7..f77dab56c8 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -418,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI private void clearPlate(DroppedObjectAnimation animation) { - var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray(); + var caughtObjects = caughtObjectContainer.Children.ToArray(); caughtObjectContainer.Clear(false); + // Use the already returned PoolableDrawables for new objects + var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); + droppedObjectTarget.AddRange(droppedObjects); foreach (var droppedObject in droppedObjects) @@ -430,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) { - var droppedObject = getDroppedObject(caughtObject); - caughtObjectContainer.Remove(caughtObject, false); + var droppedObject = getDroppedObject(caughtObject); + droppedObjectTarget.Add(droppedObject); applyDropAnimation(droppedObject, animation); @@ -456,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI break; } + // Define lifetime start for dropped objects to be disposed correctly when rewinding replay + d.LifetimeStart = Clock.CurrentTime; d.Expire(); } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index d0361b1c8d..cdeaafde10 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; - drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; + drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false; var playfield = (TaikoPlayfield)drawableRuleset.Playfield; playfield.ClassicHitTargetPosition.Value = true; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index a08877e2dd..64d406a308 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI { public new BindableDouble TimeRange => base.TimeRange; - public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true); + public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true); public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; @@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI const float scroll_rate = 10; // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. - float ratio = DrawHeight / 480; + // Width is used because it defines how many notes fit on the playfield. + // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default. + float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT); TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate; } @@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer { - LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect } + LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange } }; protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 42732d90e4..3587783104 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; - private const float default_aspect = 16f / 9f; - public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true); + public const float MAXIMUM_ASPECT = 16f / 9f; + public const float MINIMUM_ASPECT = 5f / 4f; + + public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true); protected override void Update() { @@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI // // As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit. // This is still a bit weird, because readability changes with window size, but it is what it is. - if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect) - height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; + if (LockPlayfieldAspectRange.Value) + { + float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y; + if (currentAspect > MAXIMUM_ASPECT) + height *= currentAspect / MAXIMUM_ASPECT; + else if (currentAspect < MINIMUM_ASPECT) + height *= currentAspect / MINIMUM_ASPECT; + } + + // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions. + height = Math.Min(height, 1f / 3f); Height = height; - // Position the taiko playfield exactly one playfield from the top of the screen. + // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it. + // Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered. RelativePositionAxes = Axes.Y; Y = height; } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 518981980b..d898650b66 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -160,6 +160,36 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeVideoWithLowercaseExtension() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + var metadata = beatmap.Metadata; + + Assert.AreEqual("BG.jpg", metadata.BackgroundFile); + } + } + + [Test] + public void TestDecodeVideoWithUppercaseExtension() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + var metadata = beatmap.Metadata; + + Assert.AreEqual("BG.jpg", metadata.BackgroundFile); + } + } + [Test] public void TestDecodeImageSpecifiedAsVideo() { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 3a776ac225..34ff8bfd84 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -95,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestLoopWithoutExplicitFadeOut() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(1, background.Elements.Count); + + Assert.AreEqual(2000, background.Elements[0].StartTime); + Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime); + + Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime()); + Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay); + } + } + [Test] public void TestCorrectAnimationStartTime() { @@ -169,6 +190,40 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeVideoWithLowercaseExtension() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.EqualTo(1)); + + Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path); + } + } + + [Test] + public void TestDecodeVideoWithUppercaseExtension() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.EqualTo(1)); + + Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path); + } + } + [Test] public void TestDecodeImageSpecifiedAsVideo() { @@ -179,8 +234,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var storyboard = decoder.Decode(stream); - StoryboardLayer foreground = storyboard.Layers.Single(l => l.Name == "Video"); - Assert.That(foreground.Elements.Count, Is.Zero); + StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video"); + Assert.That(video.Elements.Count, Is.Zero); } } diff --git a/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb new file mode 100644 index 0000000000..7afaa445df --- /dev/null +++ b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb @@ -0,0 +1,6 @@ +[Events] +//Storyboard Layer 0 (Background) +Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever + F,0,2000,,0,1 + L,2000,10 + F,18,0,1000,1,0 diff --git a/osu.Game.Tests/Resources/video-with-lowercase-extension.osb b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb new file mode 100644 index 0000000000..eec09722ed --- /dev/null +++ b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,0,"BG.jpg",0,0 +Video,0,"Video.avi",0,0 diff --git a/osu.Game.Tests/Resources/video-with-uppercase-extension.osb b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb new file mode 100644 index 0000000000..3834a547f2 --- /dev/null +++ b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,0,"BG.jpg",0,0 +Video,0,"Video.AVI",0,0 diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index fbdaad1cd8..8f4250799e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsDrawable => true; public double StartTime => double.MinValue; public double EndTime => double.MaxValue; + public double EndTimeForDisplay => double.MaxValue; public Drawable CreateDrawable() => new DrawableTestStoryboardElement(); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index eecead5415..ae46dda750 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; - private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); + private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 46d5e6c4d2..22f7111f68 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -7,7 +7,9 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -17,13 +19,21 @@ namespace osu.Game.Tests.Visual.Gameplay { public TestSceneKeyCounter() { - KeyCounterDisplay kc = new DefaultKeyCounterDisplay + KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, + Position = new Vector2(0, 72.7f) }; - kc.AddRange(new InputTrigger[] + KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Position = new Vector2(0, -72.7f) + }; + + defaultDisplay.AddRange(new InputTrigger[] { new KeyCounterKeyboardTrigger(Key.X), new KeyCounterKeyboardTrigger(Key.X), @@ -31,30 +41,41 @@ namespace osu.Game.Tests.Visual.Gameplay new KeyCounterMouseTrigger(MouseButton.Right), }); - var testCounter = (DefaultKeyCounter)kc.Counters.First(); + argonDisplay.AddRange(new InputTrigger[] + { + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterKeyboardTrigger(Key.X), + new KeyCounterMouseTrigger(MouseButton.Left), + new KeyCounterMouseTrigger(MouseButton.Right), + }); + + var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First(); AddStep("Add random", () => { Key key = (Key)((int)Key.A + RNG.Next(26)); - kc.Add(new KeyCounterKeyboardTrigger(key)); + defaultDisplay.Add(new KeyCounterKeyboardTrigger(key)); + argonDisplay.Add(new KeyCounterKeyboardTrigger(key)); }); - Key testKey = ((KeyCounterKeyboardTrigger)kc.Counters.First().Trigger).Key; - - void addPressKeyStep() - { - AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); - } + Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key; addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1); addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2); - AddStep("Disable counting", () => testCounter.IsCounting.Value = false); + AddStep("Disable counting", () => + { + argonDisplay.IsCounting.Value = false; + defaultDisplay.IsCounting.Value = false; + }); addPressKeyStep(); AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2); - Add(kc); + Add(defaultDisplay); + Add(argonDisplay); + + void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 7bbfc6a62b..0439656aae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; - private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); + private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single(); [Test] public void TestComboCounterIncrementing() diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs new file mode 100644 index 0000000000..603573058e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Navigation +{ + public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene + { + /// + /// When entering the editor, a new beatmap is created as part of the asynchronous load process. + /// This test ensures that in the case of an early exit from the editor (ie. while it's still loading) + /// doesn't leave a dangling beatmap behind. + /// + /// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough + /// as a test. + /// + [Test] + public void TestCancelNavigationToEditor() + { + BeatmapSetInfo[] beatmapSets = null!; + + AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets()); + + AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault()); + + AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader); + AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit()); + + AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets)); + + BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All().Where(x => !x.DeletePending).ToArray()); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index cab49b7d69..6af6a25579 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps // user requested abort return; - var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal))); + var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase))); if (video != null) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index a9bdd21b64..ef1dbc0488 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -369,7 +369,7 @@ namespace osu.Game.Beatmaps.Formats // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported // video extensions and handle similar to a background if it doesn't match. - if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename))) + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) { beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; } diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index f8308fe431..df5d3edb55 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -114,7 +114,7 @@ namespace osu.Game.Beatmaps.Formats // // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451). - if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path))) + if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) break; storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 37e15c6127..7097102335 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { get { - if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension)) + if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant())) return FontAwesome.Regular.FileVideo; switch (File.Extension) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 7c9b52275d..20258b9c35 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -100,14 +100,14 @@ namespace osu.Game.Localisation public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks"); /// - /// "{0:0.0}°" + /// "{0:0}°" /// - public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0.0}°", newRotation); + public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0}°", newRotation); /// - /// "{0:0.0}° (snapped)" + /// "{0:0}° (snapped)" /// - public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0.0}° (snapped)", newRotation); + public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index d04329430b..de678cb5d1 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -31,6 +32,9 @@ namespace osu.Game.Overlays.Profile.Header [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private RankingsOverlay? rankingsOverlay { get; set; } + private UserCoverBackground cover = null!; private SupporterIcon supporterTag = null!; private UpdateableAvatar avatar = null!; @@ -38,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; private UpdateableFlag userFlag = null!; + private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; private ToggleCoverButton coverToggle = null!; @@ -156,13 +161,17 @@ namespace osu.Game.Overlays.Profile.Header Size = new Vector2(28, 20), ShowPlaceholderOnUnknown = false, }, - userCountryText = new OsuSpriteText + userCountryContainer = new OsuHoverContainer { - Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), - Margin = new MarginPadding { Left = 5 }, - Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, - } + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 5 }, + Child = userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular), + }, + }, } }, } @@ -202,6 +211,7 @@ namespace osu.Game.Overlays.Profile.Header openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.CountryCode = user?.CountryCode ?? default; userCountryText.Text = (user?.CountryCode ?? default).GetDescription(); + userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default); supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 2765d2b437..a46205d40d 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -256,7 +256,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics return; } - if (host.Window is WindowsWindow) + if (host.Renderer is IWindowsRenderer) { switch (fullscreenCapability.Value) { diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 4425ece513..97789b7f5a 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -20,11 +20,31 @@ namespace osu.Game.Rulesets.Mods public virtual bool RestartOnFail => Restart.Value; + private Action? triggerFailureDelegate; + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { + triggerFailureDelegate = healthProcessor.TriggerFailure; healthProcessor.FailConditions += FailCondition; } + /// + /// Immediately triggers a failure on the loaded . + /// + protected void TriggerFailure() => triggerFailureDelegate?.Invoke(); + + /// + /// Determines whether should trigger a failure. Called every time a + /// judgement is applied to . + /// + /// The loaded . + /// The latest . + /// Whether the fail condition has been met. + /// + /// This method should only be used to trigger failures based on . + /// Using outside values to evaluate failure may introduce event ordering discrepancies, use + /// an with instead. + /// protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result); } } diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index b70ddd5e24..3e0b6433c2 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -31,6 +31,15 @@ namespace osu.Game.Rulesets.Scoring /// public bool HasFailed { get; private set; } + /// + /// Immediately triggers a failure for this HealthProcessor. + /// + public void TriggerFailure() + { + if (Failed?.Invoke() != false) + HasFailed = true; + } + protected override void ApplyResultInternal(JudgementResult result) { result.HealthAtJudgement = Health.Value; @@ -42,10 +51,7 @@ namespace osu.Game.Rulesets.Scoring Health.Value += GetHealthIncreaseFor(result); if (meetsAnyFailCondition(result)) - { - if (Failed?.Invoke() != false) - HasFailed = true; - } + TriggerFailure(); } protected override void RevertResultInternal(JudgementResult result) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 305f5bf3c4..c2a3f12efd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { float oldRotation = cumulativeRotation.Value ?? 0; - float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : rawCumulativeRotation; + float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); newRotation = (newRotation - 180) % 360 + 180; cumulativeRotation.Value = newRotation; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d89392f757..b5d304a031 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -210,7 +210,10 @@ namespace osu.Game.Screens.Edit // this is a bit haphazard, but guards against setting the lease Beatmap bindable if // the editor has already been exited. if (!ValidForPush) + { + beatmapManager.Delete(loadableBeatmap.BeatmapSetInfo); return; + } } try diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs new file mode 100644 index 0000000000..6818b30823 --- /dev/null +++ b/osu.Game/Screens/Play/ArgonKeyCounter.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public partial class ArgonKeyCounter : KeyCounter + { + private Circle inputIndicator = null!; + private OsuSpriteText countText = null!; + + // These values were taken from Figma + private const float line_height = 3; + private const float name_font_size = 10; + private const float count_font_size = 14; + + // Make things look bigger without using Scale + private const float scale_factor = 1.5f; + + public ArgonKeyCounter(InputTrigger trigger) + : base(trigger) + { + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + inputIndicator = new Circle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = line_height * scale_factor, + Alpha = 0.5f + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(0, -13) * scale_factor, + Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), + Colour = colours.Blue0, + Text = Trigger.Name + }, + countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), + }, + }; + + // Values from Figma didn't match visually + // So these were just eyeballed + Height = 30 * scale_factor; + Width = 35 * scale_factor; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsActive.BindValueChanged(e => inputIndicator.Alpha = e.NewValue ? 1 : 0.5f, true); + CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true); + } + } +} diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs new file mode 100644 index 0000000000..984c2a7287 --- /dev/null +++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public partial class ArgonKeyCounterDisplay : KeyCounterDisplay + { + private const int duration = 100; + + protected override FillFlowContainer KeyFlow { get; } + + public ArgonKeyCounterDisplay() + { + InternalChild = KeyFlow = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Alpha = 0, + Spacing = new Vector2(2), + }; + } + + protected override void Update() + { + base.Update(); + + Size = KeyFlow.Size; + } + + protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger); + + protected override void UpdateVisibility() + => KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); + } +} diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs index 69a3e53dfc..f7ac72035f 100644 --- a/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play.HUD { new OsuSpriteText { - Text = Name, + Text = Trigger.Name, Font = OsuFont.Numeric.With(size: 12), Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs index 14d7f56093..e459574243 100644 --- a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK.Graphics; @@ -13,13 +13,11 @@ namespace osu.Game.Screens.Play.HUD private const int duration = 100; private const double key_fade_time = 80; - private readonly FillFlowContainer keyFlow; - - public override IEnumerable Counters => keyFlow; + protected override FillFlowContainer KeyFlow { get; } public DefaultKeyCounterDisplay() { - InternalChild = keyFlow = new FillFlowContainer + InternalChild = KeyFlow = new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, @@ -33,20 +31,19 @@ namespace osu.Game.Screens.Play.HUD // Don't use autosize as it will shrink to zero when KeyFlow is hidden. // In turn this can cause the display to be masked off screen and never become visible again. - Size = keyFlow.Size; + Size = KeyFlow.Size; } - public override void Add(InputTrigger trigger) => - keyFlow.Add(new DefaultKeyCounter(trigger) - { - FadeTime = key_fade_time, - KeyDownTextColor = KeyDownTextColor, - KeyUpTextColor = KeyUpTextColor, - }); + protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger) + { + FadeTime = key_fade_time, + KeyDownTextColor = KeyDownTextColor, + KeyUpTextColor = KeyUpTextColor, + }; protected override void UpdateVisibility() => // Isolate changing visibility of the key counters from fading this component. - keyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); + KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); private Color4 keyDownTextColor = Color4.DarkGray; @@ -58,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD if (value != keyDownTextColor) { keyDownTextColor = value; - foreach (var child in keyFlow) + foreach (var child in KeyFlow.Cast()) child.KeyDownTextColor = value; } } @@ -74,7 +71,7 @@ namespace osu.Game.Screens.Play.HUD if (value != keyUpTextColor) { keyUpTextColor = value; - foreach (var child in keyFlow) + foreach (var child in KeyFlow.Cast()) child.KeyUpTextColor = value; } } diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs index 2a4ab1993a..7cdd6b025f 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs @@ -54,8 +54,6 @@ namespace osu.Game.Screens.Play.HUD Trigger.OnActivate += Activate; Trigger.OnDeactivate += Deactivate; - - Name = trigger.Name; } private void increment() diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index 49c0da6793..05427d3a32 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -29,7 +29,9 @@ namespace osu.Game.Screens.Play.HUD /// /// The s contained in this . /// - public abstract IEnumerable Counters { get; } + public IEnumerable Counters => KeyFlow; + + protected abstract FillFlowContainer KeyFlow { get; } /// /// Whether the actions reported by all s within this should be counted. @@ -53,13 +55,22 @@ namespace osu.Game.Screens.Play.HUD /// /// Add a to this display. /// - public abstract void Add(InputTrigger trigger); + public void Add(InputTrigger trigger) + { + var keyCounter = CreateCounter(trigger); + + KeyFlow.Add(keyCounter); + + IsCounting.BindTo(keyCounter.IsCounting); + } /// /// Add a range of to this display. /// public void AddRange(IEnumerable triggers) => triggers.ForEach(Add); + protected abstract KeyCounter CreateCounter(InputTrigger trigger); + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index e598c79b08..be77c9a98e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables Loop = animation.LoopType == AnimationLoopType.LoopForever; LifetimeStart = animation.StartTime; - LifetimeEnd = animation.EndTime; + LifetimeEnd = animation.EndTimeForDisplay; } [Resolved] diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index f9b09ed57c..400d33481c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -82,7 +82,7 @@ namespace osu.Game.Storyboards.Drawables Position = sprite.InitialPosition; LifetimeStart = sprite.StartTime; - LifetimeEnd = sprite.EndTime; + LifetimeEnd = sprite.EndTimeForDisplay; } [Resolved] diff --git a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs index c8daeb3b3d..9eed139ad4 100644 --- a/osu.Game/Storyboards/IStoryboardElementWithDuration.cs +++ b/osu.Game/Storyboards/IStoryboardElementWithDuration.cs @@ -12,9 +12,17 @@ namespace osu.Game.Storyboards { /// /// The time at which the ends. + /// This is consumed to extend the length of a storyboard to ensure all visuals are played to completion. /// double EndTime { get; } + /// + /// The time this element displays until. + /// This is used for lifetime purposes, and includes long playing animations which don't necessarily extend + /// a storyboard's play time. + /// + double EndTimeForDisplay { get; } + /// /// The duration of the StoryboardElement. /// diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs index 16deac8e9e..1a4b6bb923 100644 --- a/osu.Game/Storyboards/StoryboardAnimation.cs +++ b/osu.Game/Storyboards/StoryboardAnimation.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osuTK; using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 5b7b194be7..982185d51b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Storyboards.Drawables; using osuTK; @@ -84,6 +81,19 @@ namespace osu.Game.Storyboards } } + public double EndTimeForDisplay + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in loops) + latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations); + + return latestEndTime; + } + } + public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); private delegate void DrawablePropertyInitializer(Drawable drawable, T value); @@ -114,7 +124,7 @@ namespace osu.Game.Storyboards public virtual Drawable CreateDrawable() => new DrawableStoryboardSprite(this); - public void ApplyTransforms(Drawable drawable, IEnumerable> triggeredGroups = null) + public void ApplyTransforms(Drawable drawable, IEnumerable>? triggeredGroups = null) { // For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity. // To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list @@ -156,7 +166,7 @@ namespace osu.Game.Storyboards foreach (var command in commands) { - DrawablePropertyInitializer initFunc = null; + DrawablePropertyInitializer? initFunc = null; if (!initialized) { @@ -169,7 +179,7 @@ namespace osu.Game.Storyboards } } - private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable> triggeredGroups) + private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable>? triggeredGroups) { var commands = TimelineGroup.GetCommands(timelineSelector); foreach (var loop in loops) @@ -198,11 +208,11 @@ namespace osu.Game.Storyboards { public double StartTime => command.StartTime; - private readonly DrawablePropertyInitializer initializeProperty; + private readonly DrawablePropertyInitializer? initializeProperty; private readonly DrawableTransformer transform; private readonly CommandTimeline.TypedCommand command; - public GeneratedCommand([NotNull] CommandTimeline.TypedCommand command, [CanBeNull] DrawablePropertyInitializer initializeProperty, [NotNull] DrawableTransformer transform) + public GeneratedCommand(CommandTimeline.TypedCommand command, DrawablePropertyInitializer? initializeProperty, DrawableTransformer transform) { this.command = command; this.initializeProperty = initializeProperty;