diff --git a/global.json b/global.json index 10b61047ac..2c93a533e4 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.2.3" + "Microsoft.Build.Traversal": "3.0.2" } } \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index 6dab6edc5e..9d99218f88 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - - + + diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 0feab9a717..62d8c17058 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -59,7 +59,7 @@ namespace osu.Desktop try { using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", ""); + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); if (checkExists(stableInstallPath)) return stableInstallPath; @@ -138,7 +138,7 @@ namespace osu.Desktop break; // SDL2 DesktopWindow - case DesktopWindow desktopWindow: + case SDL2DesktopWindow desktopWindow: desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 285a813d97..6ca7079654 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -22,9 +22,9 @@ namespace osu.Desktop { // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; - bool useSdl = args.Contains("--sdl"); + bool useOsuTK = args.Contains("--tk"); - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl)) + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK)) { host.ExceptionThrown += handleException; diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 62e8f7c518..adf9c452f6 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,12 +24,12 @@ - + - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 1eb0975010..c01aff0aa0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -38,17 +38,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods new Fruit { X = 0, - StartTime = 250 + StartTime = 1000 }, new Fruit { X = CatchPlayfield.WIDTH, - StartTime = 500 + StartTime = 2000 }, new JuiceStream { X = CatchPlayfield.CENTER_X, - StartTime = 750, + StartTime = 3000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 3c636a5b97..f552c3c27b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests NewCombo = i % 8 == 0, Samples = new List(new[] { - new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 } + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100) }) }); } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs new file mode 100644 index 0000000000..64695153b5 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index e055f08dc2..c12f38723b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -10,14 +10,12 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; -using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Tests { @@ -103,7 +101,6 @@ namespace osu.Game.Rulesets.Catch.Tests { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, - CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation }, }); } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index d35f828e28..3e4995482d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Tests if (juice.NestedHitObjects.Last() is CatchHitObject tail) tail.LastInCombo = true; // usually the (Catch)BeatmapProcessor would do this for us when necessary - addToPlayfield(new DrawableJuiceStream(juice, drawableRuleset.CreateDrawableRepresentation)); + addToPlayfield(new DrawableJuiceStream(juice)); } private void spawnBananas(bool hit = false) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 89063319d6..160da75aa9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osuTK; @@ -17,34 +18,49 @@ namespace osu.Game.Rulesets.Catch.Tests { base.LoadComplete(); - foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); + AddStep("show pear", () => SetContents(() => createDrawableFruit(0))); + AddStep("show grape", () => SetContents(() => createDrawableFruit(1))); + AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2))); + AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3))); + + AddStep("show banana", () => SetContents(createDrawableBanana)); AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); - foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); + AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true))); + AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true))); + AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true))); + AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); } - private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => - setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); + private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) => + SetProperties(new DrawableFruit(new Fruit + { + IndexInBeatmap = indexInBeatmap, + HyperDashBindable = { Value = hyperdash } + })); - private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); + private Drawable createDrawableBanana() => + SetProperties(new DrawableBanana(new Banana())); - private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); + private Drawable createDrawableDroplet(bool hyperdash = false) => + SetProperties(new DrawableDroplet(new Droplet + { + HyperDashBindable = { Value = hyperdash } + })); - private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) + private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet())); + + protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d) { var hitObject = d.HitObject; + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 }); hitObject.StartTime = 1000000000000; hitObject.Scale = 1.5f; - if (hyperdash) - hitObject.HyperDashTarget = new Banana(); - d.Anchor = Anchor.Centre; d.RelativePositionAxes = Axes.None; d.Position = Vector2.Zero; @@ -55,15 +71,5 @@ namespace osu.Game.Rulesets.Catch.Tests }; return d; } - - public class TestCatchFruit : Fruit - { - public TestCatchFruit(FruitVisualRepresentation rep) - { - VisualRepresentation = rep; - } - - public override FruitVisualRepresentation VisualRepresentation { get; } - } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs new file mode 100644 index 0000000000..4448e828e7 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs @@ -0,0 +1,32 @@ +// 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.Bindables; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneFruitVisualChange : TestSceneFruitObjects + { + private readonly Bindable indexInBeatmap = new Bindable(); + private readonly Bindable hyperDash = new Bindable(); + + protected override void LoadComplete() + { + AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit + { + IndexInBeatmapBindable = { BindTarget = indexInBeatmap }, + HyperDashBindable = { BindTarget = hyperDash }, + })))); + + AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet + { + HyperDashBindable = { BindTarget = hyperDash }, + })))); + + Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true); + Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true); + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index dfe3bf8af4..61ecd79e3d 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index a08c5b6fb1..00ce9ea8c2 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -192,24 +192,24 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void initialiseHyperDash(IBeatmap beatmap) { - List objectWithDroplets = new List(); + List palpableObjects = new List(); foreach (var currentObject in beatmap.HitObjects) { if (currentObject is Fruit fruitObject) - objectWithDroplets.Add(fruitObject); + palpableObjects.Add(fruitObject); if (currentObject is JuiceStream) { - foreach (var currentJuiceElement in currentObject.NestedHitObjects) + foreach (var juice in currentObject.NestedHitObjects) { - if (!(currentJuiceElement is TinyDroplet)) - objectWithDroplets.Add((CatchHitObject)currentJuiceElement); + if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet)) + palpableObjects.Add(palpableObject); } } } - objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; @@ -221,10 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int lastDirection = 0; double lastExcess = halfCatcherWidth; - for (int i = 0; i < objectWithDroplets.Count - 1; i++) + for (int i = 0; i < palpableObjects.Count - 1; i++) { - CatchHitObject currentObject = objectWithDroplets[i]; - CatchHitObject nextObject = objectWithDroplets[i + 1]; + var currentObject = palpableObjects[i]; + var nextObject = palpableObjects[i + 1]; // Reset variables in-case values have changed (e.g. after applying HR) currentObject.HyperDashTarget = null; diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index 3e21b8fbaf..dcd410e08f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing { private const float normalized_hitobject_radius = 41.0f; - public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject; + public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject; - public new CatchHitObject LastObject => (CatchHitObject)base.LastObject; + public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject; public readonly float NormalizedPosition; public readonly float LastNormalizedPosition; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index a82d0af102..16ef56d845 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModEasy : ModEasy + public class CatchModEasy : ModEasyWithExtraLives { public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; } diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 4ecfb7b16d..a274f25200 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -1,22 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + +using System; using System.Collections.Generic; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { - public class Banana : Fruit + public class Banana : Fruit, IHasComboInformation { /// /// Index of banana in current shower. /// public int BananaIndex; - public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; - public override Judgement CreateJudgement() => new CatchBananaJudgement(); private static readonly List samples = new List { new BananaHitSampleInfo() }; @@ -26,11 +31,50 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } - private class BananaHitSampleInfo : HitSampleInfo - { - private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; + private Color4? colour; - public override IEnumerable LookupNames => lookupNames; + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) + { + // override any external colour changes with banananana + return colour ??= getBananaColour(); + } + + private Color4 getBananaColour() + { + switch (RNG.Next(0, 3)) + { + default: + return new Color4(255, 240, 0, 255); + + case 1: + return new Color4(255, 192, 0, 255); + + case 2: + return new Color4(214, 221, 28, 255); + } + } + + private class BananaHitSampleInfo : HitSampleInfo, IEquatable + { + private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" }; + + public override IEnumerable LookupNames => lookup_names; + + public BananaHitSampleInfo(int volume = 0) + : base(string.Empty, volume: volume) + { + } + + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => new BananaHitSampleInfo(newVolume.GetOr(Volume)); + + public bool Equals(BananaHitSampleInfo? other) + => other != null; + + public override bool Equals(object? obj) + => obj is BananaHitSampleInfo other && Equals(other); + + public override int GetHashCode() => lookup_names.GetHashCode(); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 89c51459a6..b45f95a8e6 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Catch.Objects { public class BananaShower : CatchHitObject, IHasDuration { - public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; - public override bool LastInCombo => true; public override Judgement CreateJudgement() => new IgnoreJudgement(); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 5985ec9b68..a74055bff9 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -16,32 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects { public const float OBJECT_RADIUS = 64; - private float x; + // This value is after XOffset applied. + public readonly Bindable XBindable = new Bindable(); + + // This value is before XOffset applied. + private float originalX; /// /// The horizontal position of the fruit between 0 and . /// public float X { - get => x + XOffset; - set => x = value; + // TODO: I don't like this asymmetry. + get => XBindable.Value; + // originalX is set by `XBindable.BindValueChanged` + set => XBindable.Value = value + xOffset; } - /// - /// Whether this object can be placed on the catcher's plate. - /// - public virtual bool CanBePlated => false; + private float xOffset; /// /// A random offset applied to , set by the . /// - internal float XOffset { get; set; } + internal float XOffset + { + get => xOffset; + set + { + xOffset = value; + XBindable.Value = originalX + xOffset; + } + } public double TimePreempt = 1000; - public int IndexInBeatmap { get; set; } + public readonly Bindable IndexInBeatmapBindable = new Bindable(); - public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); + public int IndexInBeatmap + { + get => IndexInBeatmapBindable.Value; + set => IndexInBeatmapBindable.Value = value; + } public virtual bool NewCombo { get; set; } @@ -63,13 +78,6 @@ namespace osu.Game.Rulesets.Catch.Objects set => ComboIndexBindable.Value = value; } - /// - /// Difference between the distance to the next object - /// and the distance that would have triggered a hyper dash. - /// A value close to 0 indicates a difficult jump (for difficulty calculation). - /// - public float DistanceToHyperDash { get; set; } - public Bindable LastInComboBindable { get; } = new Bindable(); /// @@ -81,17 +89,13 @@ namespace osu.Game.Rulesets.Catch.Objects set => LastInComboBindable.Value = value; } - public float Scale { get; set; } = 1; + public readonly Bindable ScaleBindable = new Bindable(1); - /// - /// Whether this fruit can initiate a hyperdash. - /// - public bool HyperDash => HyperDashTarget != null; - - /// - /// The target fruit if we are to initiate a hyperdash. - /// - public CatchHitObject HyperDashTarget; + public float Scale + { + get => ScaleBindable.Value; + set => ScaleBindable.Value = value; + } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -103,22 +107,10 @@ namespace osu.Game.Rulesets.Catch.Objects } protected override HitWindows CreateHitWindows() => HitWindows.Empty; - } - /// - /// Represents a single object that can be caught by the catcher. - /// - public abstract class PalpableCatchHitObject : CatchHitObject - { - public override bool CanBePlated => true; - } - - public enum FruitVisualRepresentation - { - Pear, - Grape, - Pineapple, - Raspberry, - Banana // banananananannaanana + protected CatchHitObject() + { + XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index a865984d45..fb982bbdab 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -1,26 +1,24 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Utils; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableBanana : DrawableFruit { - public DrawableBanana(Banana h) - : base(h) + protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana; + + public DrawableBanana() + : this(null) { } - private Color4? colour; - - protected override Color4 GetComboColour(IReadOnlyList comboColours) + public DrawableBanana([CanBeNull] Banana h) + : base(h) { - // override any external colour changes with banananana - return colour ??= getBananaColour(); } protected override void UpdateInitialTransforms() @@ -46,20 +44,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (Samples != null) Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; } - - private Color4 getBananaColour() - { - switch (RNG.Next(0, 3)) - { - default: - return new Color4(255, 240, 0, 255); - - case 1: - return new Color4(255, 192, 0, 255); - - case 2: - return new Color4(214, 221, 28, 255); - } - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index 4ce80aceb8..9b2f95e221 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -1,26 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableBananaShower : DrawableCatchHitObject + public class DrawableBananaShower : DrawableCatchHitObject { - private readonly Func> createDrawableRepresentation; private readonly Container bananaContainer; - public DrawableBananaShower(BananaShower s, Func> createDrawableRepresentation = null) + public DrawableBananaShower() + : this(null) + { + } + + public DrawableBananaShower([CanBeNull] BananaShower s) : base(s) { - this.createDrawableRepresentation = createDrawableRepresentation; RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - X = 0; AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both }); } @@ -34,18 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - bananaContainer.Clear(); - } - - protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) - { - switch (hitObject) - { - case Banana banana: - return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); - } - - return base.CreateNestedHitObject(hitObject); + bananaContainer.Clear(false); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 7922510a49..1faa6a5b0f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -2,77 +2,42 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using osu.Framework.Allocation; +using JetBrains.Annotations; +using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject - where TObject : PalpableCatchHitObject - { - protected Container ScaleContainer { get; private set; } - - protected PalpableDrawableCatchHitObject(TObject hitObject) - : base(hitObject) - { - Origin = Anchor.Centre; - Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); - Masking = false; - } - - [BackgroundDependencyLoader] - private void load() - { - AddRangeInternal(new Drawable[] - { - ScaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - }); - - ScaleContainer.Scale = new Vector2(HitObject.Scale); - } - - protected override Color4 GetComboColour(IReadOnlyList comboColours) => - comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; - } - - public abstract class DrawableCatchHitObject : DrawableCatchHitObject - where TObject : CatchHitObject - { - public new TObject HitObject; - - protected DrawableCatchHitObject(TObject hitObject) - : base(hitObject) - { - HitObject = hitObject; - Anchor = Anchor.BottomLeft; - } - } - public abstract class DrawableCatchHitObject : DrawableHitObject { - protected override double InitialLifetimeOffset => HitObject.TimePreempt; + public readonly Bindable XBindable = new Bindable(); - public virtual bool StaysOnPlate => HitObject.CanBePlated; + protected override double InitialLifetimeOffset => HitObject.TimePreempt; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; - protected DrawableCatchHitObject(CatchHitObject hitObject) + protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) { - X = hitObject.X; + Anchor = Anchor.BottomLeft; + } + + protected override void OnApply() + { + base.OnApply(); + + XBindable.BindTo(HitObject.XBindable); + } + + protected override void OnFree() + { + base.OnFree(); + + XBindable.UnbindFrom(HitObject.XBindable); } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 688240fd86..06ecd44488 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -1,18 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : PalpableDrawableCatchHitObject + public class DrawableDroplet : DrawablePalpableCatchHitObject { public override bool StaysOnPlate => false; - public DrawableDroplet(Droplet h) + public DrawableDroplet() + : this(null) + { + } + + public DrawableDroplet([CanBeNull] CatchHitObject h) : base(h) { } @@ -20,7 +27,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); + HyperDash.BindValueChanged(_ => updatePiece(), true); + } + + private void updatePiece() + { + ScaleContainer.Child = new SkinnableDrawable( + new CatchSkinComponent(CatchSkinComponents.Droplet), + _ => new DropletPiece + { + HyperDash = { BindTarget = HyperDash } + }); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index c1c34e4157..68cb649b66 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -2,15 +2,27 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : PalpableDrawableCatchHitObject + public class DrawableFruit : DrawablePalpableCatchHitObject { - public DrawableFruit(Fruit h) + public readonly Bindable VisualRepresentation = new Bindable(); + + protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); + + public DrawableFruit() + : this(null) + { + } + + public DrawableFruit([CanBeNull] Fruit h) : base(h) { } @@ -18,10 +30,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable( - new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece()); - ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; + + IndexInBeatmap.BindValueChanged(change => + { + VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); + }, true); + + VisualRepresentation.BindValueChanged(_ => updatePiece()); + HyperDash.BindValueChanged(_ => updatePiece(), true); + } + + private void updatePiece() + { + ScaleContainer.Child = new SkinnableDrawable( + new CatchSkinComponent(getComponent(VisualRepresentation.Value)), + _ => new FruitPiece + { + VisualRepresentation = { BindTarget = VisualRepresentation }, + HyperDash = { BindTarget = HyperDash }, + }); } private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) @@ -48,4 +76,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables } } } + + public enum FruitVisualRepresentation + { + Pear, + Grape, + Pineapple, + Raspberry, + Banana // banananananannaanana + } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 7bc016d94f..a496a35842 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -1,37 +1,33 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableJuiceStream : DrawableCatchHitObject + public class DrawableJuiceStream : DrawableCatchHitObject { - private readonly Func> createDrawableRepresentation; private readonly Container dropletContainer; - public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS); + public DrawableJuiceStream() + : this(null) + { + } - public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null) + public DrawableJuiceStream([CanBeNull] JuiceStream s) : base(s) { - this.createDrawableRepresentation = createDrawableRepresentation; RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - X = 0; AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) { - hitObject.Origin = Anchor.BottomCentre; - base.AddNestedHitObject(hitObject); dropletContainer.Add(hitObject); } @@ -39,19 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - dropletContainer.Clear(); - } - - protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) - { - switch (hitObject) - { - case CatchHitObject catchObject: - return createDrawableRepresentation?.Invoke(catchObject)?.With(o => - ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); - } - - throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}."); + dropletContainer.Clear(false); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs new file mode 100644 index 0000000000..a3908f94b6 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject + { + public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; + + public readonly Bindable HyperDash = new Bindable(); + + public readonly Bindable ScaleBindable = new Bindable(1); + + public readonly Bindable IndexInBeatmap = new Bindable(); + + /// + /// The multiplicative factor applied to scale relative to scale. + /// + protected virtual float ScaleFactor => 1; + + /// + /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. + /// + public virtual bool StaysOnPlate => true; + + protected readonly Container ScaleContainer; + + protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) + : base(h) + { + Origin = Anchor.Centre; + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + + AddInternal(ScaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }); + } + + [BackgroundDependencyLoader] + private void load() + { + XBindable.BindValueChanged(x => + { + if (!IsOnPlate) X = x.NewValue; + }, true); + + ScaleBindable.BindValueChanged(scale => + { + ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); + }, true); + + IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); + } + + protected override void OnApply() + { + base.OnApply(); + + HyperDash.BindTo(HitObject.HyperDashBindable); + ScaleBindable.BindTo(HitObject.ScaleBindable); + IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable); + } + + protected override void OnFree() + { + HyperDash.UnbindFrom(HitObject.HyperDashBindable); + ScaleBindable.UnbindFrom(HitObject.ScaleBindable); + IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable); + + base.OnFree(); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs index ae775684d8..8f5a04dfda 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs @@ -1,21 +1,22 @@ // 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 JetBrains.Annotations; namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableTinyDroplet : DrawableDroplet { - public DrawableTinyDroplet(TinyDroplet h) - : base(h) + protected override float ScaleFactor => base.ScaleFactor / 2; + + public DrawableTinyDroplet() + : this(null) { } - [BackgroundDependencyLoader] - private void load() + public DrawableTinyDroplet([CanBeNull] TinyDroplet h) + : base(h) { - ScaleContainer.Scale /= 2; } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs deleted file mode 100644 index c2499446fa..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects.Drawables; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables -{ - public class DropletPiece : CompositeDrawable - { - public DropletPiece() - { - Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); - } - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) - { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - var hitObject = drawableCatchObject.HitObject; - - InternalChild = new Pulp - { - RelativeSizeAxes = Axes.Both, - AccentColour = { BindTarget = drawableObject.AccentColour } - }; - - if (hitObject.HyperDash) - { - AddInternal(new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(2f), - Depth = 1, - Children = new Drawable[] - { - new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - BorderThickness = 6, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0.3f, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - } - } - } - } - }); - } - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs deleted file mode 100644 index 4bffdab3d8..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables -{ - internal class FruitPiece : CompositeDrawable - { - /// - /// Because we're adding a border around the fruit, we need to scale down some. - /// - public const float RADIUS_ADJUST = 1.1f; - - private Circle border; - private CatchHitObject hitObject; - - public FruitPiece() - { - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) - { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - hitObject = drawableCatchObject.HitObject; - - AddRangeInternal(new[] - { - getFruitFor(drawableCatchObject.HitObject.VisualRepresentation), - border = new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Color4.White, - BorderThickness = 6f * RADIUS_ADJUST, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both - } - } - }, - }); - - if (hitObject.HyperDash) - { - AddInternal(new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - BorderThickness = 12f * RADIUS_ADJUST, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0.3f, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - } - } - }); - } - } - - protected override void Update() - { - base.Update(); - border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1); - } - - private Drawable getFruitFor(FruitVisualRepresentation representation) - { - switch (representation) - { - case FruitVisualRepresentation.Pear: - return new PearPiece(); - - case FruitVisualRepresentation.Grape: - return new GrapePiece(); - - case FruitVisualRepresentation.Pineapple: - return new PineapplePiece(); - - case FruitVisualRepresentation.Banana: - return new BananaPiece(); - - case FruitVisualRepresentation.Raspberry: - return new RaspberryPiece(); - } - - return Empty(); - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs similarity index 88% rename from osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs index ebb0bf0f2c..fa8837dec5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class BananaPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs new file mode 100644 index 0000000000..1e7a0b0685 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs @@ -0,0 +1,31 @@ +// 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.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class BorderPiece : Circle + { + public BorderPiece() + { + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + BorderColour = Color4.White; + BorderThickness = 6f * FruitPiece.RADIUS_ADJUST; + + // Border is drawn only when there is a child drawable. + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }; + } + } +} + diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs new file mode 100644 index 0000000000..c90407ae15 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs @@ -0,0 +1,37 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class DropletPiece : CompositeDrawable + { + public readonly Bindable HyperDash = new Bindable(); + + public DropletPiece() + { + Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + InternalChild = new Pulp + { + RelativeSizeAxes = Axes.Both, + AccentColour = { BindTarget = drawableObject.AccentColour } + }; + + if (HyperDash.Value) + { + AddInternal(new HyperDropletBorderPiece()); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs new file mode 100644 index 0000000000..31487ee407 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + internal class FruitPiece : CompositeDrawable + { + /// + /// Because we're adding a border around the fruit, we need to scale down some. + /// + public const float RADIUS_ADJUST = 1.1f; + + public readonly Bindable VisualRepresentation = new Bindable(); + public readonly Bindable HyperDash = new Bindable(); + + [CanBeNull] + private DrawableCatchHitObject drawableHitObject; + + [CanBeNull] + private BorderPiece borderPiece; + + public FruitPiece() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load([CanBeNull] DrawableHitObject drawable) + { + drawableHitObject = (DrawableCatchHitObject)drawable; + + AddInternal(getFruitFor(VisualRepresentation.Value)); + + // if it is not part of a DHO, the border is always invisible. + if (drawableHitObject != null) + AddInternal(borderPiece = new BorderPiece()); + + if (HyperDash.Value) + AddInternal(new HyperBorderPiece()); + } + + protected override void Update() + { + if (borderPiece != null && drawableHitObject?.HitObject != null) + borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); + } + + private Drawable getFruitFor(FruitVisualRepresentation representation) + { + switch (representation) + { + case FruitVisualRepresentation.Pear: + return new PearPiece(); + + case FruitVisualRepresentation.Grape: + return new GrapePiece(); + + case FruitVisualRepresentation.Pineapple: + return new PineapplePiece(); + + case FruitVisualRepresentation.Banana: + return new BananaPiece(); + + case FruitVisualRepresentation.Raspberry: + return new RaspberryPiece(); + } + + return Empty(); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs index 1d1faf893b..15349c18d5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class GrapePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs new file mode 100644 index 0000000000..60bb07e89d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.UI; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class HyperBorderPiece : BorderPiece + { + public HyperBorderPiece() + { + BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; + BorderThickness = 12f * FruitPiece.RADIUS_ADJUST; + + Child.Alpha = 0.3f; + Child.Blending = BlendingParameters.Additive; + Child.Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR; + } + } +} + diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs new file mode 100644 index 0000000000..1bd9fd6bb2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class HyperDropletBorderPiece : HyperBorderPiece + { + public HyperDropletBorderPiece() + { + Size /= 2; + BorderThickness = 6f; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs index 7f14217cda..3372a06996 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class PearPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs index c328ba1837..7f80c58178 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class PineapplePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs similarity index 96% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs index be70c3400c..1df548e70a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public abstract class PulpFormation : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs index 22ce3ba5b3..288ece95b2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class RaspberryPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index e209d012fa..d5819935ad 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -50,12 +50,7 @@ namespace osu.Game.Rulesets.Catch.Objects { base.CreateNestedHitObjects(cancellationToken); - var dropletSamples = Samples.Select(s => new HitSampleInfo - { - Bank = s.Bank, - Name = @"slidertick", - Volume = s.Volume - }).ToList(); + var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList(); int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs new file mode 100644 index 0000000000..0cd3af01df --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects +{ + /// + /// Represents a single object that can be caught by the catcher. + /// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects. + /// + public abstract class PalpableCatchHitObject : CatchHitObject, IHasComboInformation + { + /// + /// Difference between the distance to the next object + /// and the distance that would have triggered a hyper dash. + /// A value close to 0 indicates a difficult jump (for difficulty calculation). + /// + public float DistanceToHyperDash { get; set; } + + public readonly Bindable HyperDashBindable = new Bindable(); + + /// + /// Whether this fruit can initiate a hyperdash. + /// + public bool HyperDash => HyperDashBindable.Value; + + private CatchHitObject hyperDashTarget; + + /// + /// The target fruit if we are to initiate a hyperdash. + /// + public CatchHitObject HyperDashTarget + { + get => hyperDashTarget; + set + { + hyperDashTarget = value; + HyperDashBindable.Value = value != null; + } + } + + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; + } +} diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index a4f54bfe82..dfc81ee8d9 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays float lastPosition = CatchPlayfield.CENTER_X; double lastTime = 0; - void moveToNext(CatchHitObject h) + void moveToNext(PalpableCatchHitObject h) { float positionChange = Math.Abs(lastPosition - h.X); double timeAvailable = h.StartTime - lastTime; @@ -101,23 +101,16 @@ namespace osu.Game.Rulesets.Catch.Replays foreach (var obj in Beatmap.HitObjects) { - switch (obj) + if (obj is PalpableCatchHitObject palpableObject) { - case Fruit _: - moveToNext(obj); - break; + moveToNext(palpableObject); } foreach (var nestedObj in obj.NestedHitObjects.Cast()) { - switch (nestedObj) + if (nestedObj is PalpableCatchHitObject palpableNestedObject) { - case Banana _: - case TinyDroplet _: - case Droplet _: - case Fruit _: - moveToNext(nestedObj); - break; + moveToNext(palpableNestedObject); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 381d066750..b8648f46f0 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Catch.Skinning { private readonly string lookupName; - private readonly IBindable accentColour = new Bindable(); + private readonly Bindable accentColour = new Bindable(); + private readonly Bindable hyperDash = new Bindable(); private Sprite colouredSprite; public LegacyFruitPiece(string lookupName) @@ -31,9 +32,10 @@ namespace osu.Game.Rulesets.Catch.Skinning [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject, ISkinSource skin) { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; accentColour.BindTo(drawableCatchObject.AccentColour); + hyperDash.BindTo(drawableCatchObject.HyperDash); InternalChildren = new Drawable[] { @@ -51,9 +53,9 @@ namespace osu.Game.Rulesets.Catch.Skinning }, }; - if (drawableCatchObject.HitObject.HyperDash) + if (hyperDash.Value) { - var hyperDash = new Sprite + var hyperDashOverlay = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -67,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Skinning Catcher.DEFAULT_HYPER_DASH_COLOUR, }; - AddInternal(hyperDash); + AddInternal(hyperDashOverlay); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 735d7fc300..820f08d439 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -42,7 +43,6 @@ namespace osu.Game.Rulesets.Catch.UI CatcherArea = new CatcherArea(difficulty) { - CreateDrawableRepresentation = createDrawableRepresentation, ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, @@ -57,19 +57,33 @@ namespace osu.Game.Rulesets.Catch.UI }; } - public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); - - public override void Add(DrawableHitObject h) + [BackgroundDependencyLoader] + private void load() { - h.OnNewResult += onNewResult; - h.OnRevertResult += onRevertResult; - - base.Add(h); - - var fruit = (DrawableCatchHitObject)h; - fruit.CheckPosition = CheckIfWeCanCatch; + RegisterPool(50); + RegisterPool(50); + RegisterPool(100); + RegisterPool(100); + RegisterPool(10); + RegisterPool(2); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case). + NewResult += onNewResult; + RevertResult += onRevertResult; + } + + protected override void OnNewDrawableHitObject(DrawableHitObject d) + { + ((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch; + } + + private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a221ca7966..11b6916a4c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -107,6 +108,9 @@ namespace osu.Game.Rulesets.Catch.UI private float hyperDashTargetPosition; private Bindable hitLighting; + private DrawablePool hitExplosionPool; + private Container hitExplosionContainer; + public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; @@ -127,6 +131,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { + hitExplosionPool = new DrawablePool(10), caughtFruitContainer, catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) { @@ -142,7 +147,12 @@ namespace osu.Game.Rulesets.Catch.UI { Anchor = Anchor.TopCentre, Alpha = 0, - } + }, + hitExplosionContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, }; trails = new CatcherTrailDisplay(this); @@ -209,22 +219,22 @@ namespace osu.Game.Rulesets.Catch.UI if (hitLighting.Value) { - AddInternal(new HitExplosion(fruit) - { - X = fruit.X, - Scale = new Vector2(fruit.HitObject.Scale) - }); + HitExplosion hitExplosion = hitExplosionPool.Get(); + hitExplosion.X = fruit.X; + hitExplosion.Scale = new Vector2(fruit.HitObject.Scale); + hitExplosion.ObjectColour = fruit.AccentColour.Value; + hitExplosionContainer.Add(hitExplosion); } } /// /// Let the catcher attempt to catch a fruit. /// - /// The fruit to catch. + /// The fruit to catch. /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject fruit) + public bool AttemptCatch(CatchHitObject hitObject) { - if (!fruit.CanBePlated) + if (!(hitObject is PalpableCatchHitObject fruit)) return false; var halfCatchWidth = catchWidth * 0.5f; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 5e794a76aa..26077aeba4 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osuTK; @@ -21,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI { public const float CATCHER_SIZE = 106.75f; - public Func> CreateDrawableRepresentation; - public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; @@ -51,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI }; } - public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { if (!result.Type.IsScorable()) return; @@ -69,15 +66,15 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete += _ => action(); } - if (result.IsHit && fruit.HitObject.CanBePlated) + if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. - var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); + var caughtFruit = createCaughtFruit(fruit); if (caughtFruit == null) return; caughtFruit.RelativePositionAxes = Axes.None; - caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); + caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(hitObject.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); caughtFruit.IsOnPlate = true; caughtFruit.Anchor = Anchor.TopCentre; @@ -93,7 +90,7 @@ namespace osu.Game.Rulesets.Catch.UI runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); } - if (fruit.HitObject.LastInCombo) + if (hitObject.HitObject.LastInCombo) { if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) runAfterLoaded(() => MovableCatcher.Explode()); @@ -101,7 +98,7 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.Drop(); } - comboDisplay.OnNewResult(fruit, result); + comboDisplay.OnNewResult(hitObject, result); } public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) @@ -127,5 +124,26 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = MovableCatcher.X; } + + private DrawableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject) + { + switch (hitObject.HitObject) + { + case Banana banana: + return new DrawableBanana(banana); + + case Fruit fruit: + return new DrawableFruit(fruit); + + case TinyDroplet tiny: + return new DrawableTinyDroplet(tiny); + + case Droplet droplet: + return new DrawableDroplet(droplet); + + default: + return null; + } + } } } diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index ebe45aa3ab..46733181e3 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -8,7 +8,6 @@ using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -40,30 +39,6 @@ namespace osu.Game.Rulesets.Catch.UI protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); - public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h) - { - switch (h) - { - case Banana banana: - return new DrawableBanana(banana); - - case Fruit fruit: - return new DrawableFruit(fruit); - - case JuiceStream stream: - return new DrawableJuiceStream(stream, CreateDrawableRepresentation); - - case BananaShower shower: - return new DrawableBananaShower(shower, CreateDrawableRepresentation); - - case TinyDroplet tiny: - return new DrawableTinyDroplet(tiny); - - case Droplet droplet: - return new DrawableDroplet(droplet); - } - - return null; - } + public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h) => null; } } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index 04a86f83be..24ca778248 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -5,35 +5,43 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; using osu.Framework.Utils; -using osu.Game.Rulesets.Catch.Objects.Drawables; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { - public class HitExplosion : CompositeDrawable + public class HitExplosion : PoolableDrawable { - private readonly CircularContainer largeFaint; + private Color4 objectColour; - public HitExplosion(DrawableCatchHitObject fruit) + public Color4 ObjectColour + { + get => objectColour; + set + { + if (objectColour == value) return; + + objectColour = value; + onColourChanged(); + } + } + + private readonly CircularContainer largeFaint; + private readonly CircularContainer smallFaint; + private readonly CircularContainer directionalGlow1; + private readonly CircularContainer directionalGlow2; + + public HitExplosion() { Size = new Vector2(20); Anchor = Anchor.TopCentre; Origin = Anchor.BottomCentre; - Color4 objectColour = fruit.AccentColour.Value; - // scale roughly in-line with visual appearance of notes - - const float angle_variangle = 15; // should be less than 45 - - const float roundness = 100; - const float initial_height = 10; - var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); - InternalChildren = new Drawable[] { largeFaint = new CircularContainer @@ -42,33 +50,17 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, - // we want our size to be very small so the glow dominates it. - Size = new Vector2(0.8f), Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), - Roundness = 160, - Radius = 200, - }, }, - new CircularContainer + smallFaint = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), - Roundness = 20, - Radius = 50, - }, }, - new CircularContainer + directionalGlow1 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -76,16 +68,8 @@ namespace osu.Game.Rulesets.Catch.UI Masking = true, Size = new Vector2(0.01f, initial_height), Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, }, - new CircularContainer + directionalGlow2 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -93,30 +77,57 @@ namespace osu.Game.Rulesets.Catch.UI Masking = true, Size = new Vector2(0.01f, initial_height), Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, } }; } - protected override void LoadComplete() + protected override void PrepareForUse() { - base.LoadComplete(); + base.PrepareForUse(); const double duration = 400; + // we want our size to be very small so the glow dominates it. + largeFaint.Size = new Vector2(0.8f); largeFaint .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) .FadeOut(duration * 2); + const float angle_variangle = 15; // should be less than 45 + directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); Expire(true); } + + private void onColourChanged() + { + const float roundness = 100; + + largeFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }; + + smallFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }; + + directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), + Roundness = roundness, + Radius = 40, + }; + } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 654b752001..538a51db5f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -96,6 +96,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor throw new System.NotImplementedException(); } + public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) + { + throw new System.NotImplementedException(); + } + public override float GetBeatSnapDistanceAt(double referenceTime) { throw new System.NotImplementedException(); diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 892f27d27f..fa7bfd7169 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index b5ec1e1a2a..1f92929392 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -78,9 +78,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private double originalStartTime; - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (PlacementActive) { diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 27a279e044..5e09054667 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -48,9 +48,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (!PlacementActive) Column = result.Playfield as Column; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 684004b558..3db89c8ae6 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre }; } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (result.Playfield != null) { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index ff77df0ae0..4093aeb2a7 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModEasy : ModEasy + public class ManiaModEasy : ModEasyWithExtraLives { public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index a4029e7893..3a00933e4d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); + protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience; + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9aabcc6699..d2a9b69b60 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; maniaObject.CheckHittable = hitPolicy.IsHittable; - HitObjectContainer.Add(hitObject); + base.Add(hitObject); } public override bool Remove(DrawableHitObject h) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs new file mode 100644 index 0000000000..a652fb32f4 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs @@ -0,0 +1,41 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Tests.Beatmaps; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneObjectBeatSnap : TestSceneOsuEditor + { + private OsuPlayfield playfield; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false); + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); + } + + [Test] + public void TestBeatSnapHitCircle() + { + double firstTimingPointTime() => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time; + + AddStep("seek some milliseconds forward", () => EditorClock.Seek(firstTimingPointTime() + 10)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddAssert("ensure object snapped back to correct time", () => EditorBeatmap.HitObjects.First().StartTime == firstTimingPointTime()); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 1ca94df26b..7bdf131e0d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { base.SetUpSteps(); AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); + AddStep("seek to first control point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time)); } [TestCase(true)] @@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var first = (OsuHitObject)objects.First(); var second = (OsuHitObject)objects.Last(); - return first.Position == second.Position; + return Precision.AlmostEquals(first.EndPosition, second.Position); }); } @@ -66,13 +67,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); - AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0))); + AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.225f, 0))); AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2)); - AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0))); + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0))); AddStep("place second object", () => InputManager.Click(MouseButton.Left)); @@ -86,5 +87,64 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return Precision.AlmostEquals(first.EndPosition, second.Position); }); } + + [Test] + public void TestSecondCircleInSelectionAlsoSnaps() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); + + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); + + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddStep("increment time", () => EditorClock.SeekForward(true)); + + AddStep("move mouse right", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.2f, 0))); + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddStep("increment time", () => EditorClock.SeekForward(true)); + + AddStep("move mouse down", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Width * 0.2f))); + AddStep("place third object", () => InputManager.Click(MouseButton.Left)); + + AddStep("enter selection mode", () => InputManager.Key(Key.Number1)); + + AddStep("select objects 2 and 3", () => + { + // add selection backwards to test non-sequential time ordering + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[2]); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]); + }); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); + + AddAssert("object 3 snapped to 1", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var third = (OsuHitObject)objects.Last(); + + return Precision.AlmostEquals(first.EndPosition, third.Position); + }); + + AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f))); + + AddAssert("object 2 snapped to 1", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var second = (OsuHitObject)objects.ElementAt(1); + + return Precision.AlmostEquals(first.EndPosition, second.Position); + }); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 1232369a0b..9af2a99470 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -174,6 +174,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private class SnapProvider : IPositionSnapProvider { + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs index 7697f46160..d3cb3bcf59 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs @@ -5,7 +5,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class OsuModTestScene : ModTestScene + public abstract class OsuModTestScene : ModTestScene { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index c400e2f2ea..d40484f5ed 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -108,8 +108,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("change samples", () => slider.HitObject.Samples = new[] { - new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }, - new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, + new HitSampleInfo(HitSampleInfo.HIT_CLAP), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE), }); AddAssert("head samples updated", () => assertSamples(slider.HitObject.HeadCircle)); @@ -136,15 +136,15 @@ namespace osu.Game.Rulesets.Osu.Tests slider = (DrawableSlider)createSlider(repeats: 1); for (int i = 0; i < 2; i++) - slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } }); + slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo(HitSampleInfo.HIT_FINISH) }); Add(slider); }); AddStep("change samples", () => slider.HitObject.Samples = new[] { - new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }, - new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, + new HitSampleInfo(HitSampleInfo.HIT_CLAP), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE), }); AddAssert("head samples not updated", () => assertSamples(slider.HitObject.HeadCircle)); diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 3639c3616f..d6a03da807 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index e14d6647d2..c45a04053f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index c06904c0c2..e9838de63d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private OsuColour colours { get; set; } private IBindable sliderPosition; + private IBindable sliderScale; private IBindable controlPointPosition; public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) @@ -69,13 +70,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(10), + Size = new Vector2(20), }, markerRing = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(14), + Size = new Vector2(28), Masking = true, BorderThickness = 2, BorderColour = Color4.White, @@ -102,6 +103,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components controlPointPosition = ControlPoint.Position.GetBoundCopy(); controlPointPosition.BindValueChanged(_ => updateMarkerDisplay()); + sliderScale = slider.ScaleBindable.GetBoundCopy(); + sliderScale.BindValueChanged(_ => updateMarkerDisplay()); + IsSelected.BindValueChanged(_ => updateMarkerDisplay()); updateMarkerDisplay(); @@ -143,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnClick(ClickEvent e) => RequestSelection != null; + private Vector2 dragStartPosition; + protected override bool OnDragStart(DragStartEvent e) { if (RequestSelection == null) @@ -150,6 +156,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (e.Button == MouseButton.Left) { + dragStartPosition = ControlPoint.Position.Value; changeHandler?.BeginChange(); return true; } @@ -174,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components slider.Path.ControlPoints[i].Position.Value -= movementDelta; } else - ControlPoint.Position.Value += e.Delta; + ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition); } protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); @@ -194,6 +201,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components colour = colour.Lighten(1); marker.Colour = colour; + marker.Scale = new Vector2(slider.Scale); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 4b99cc23ed..b71e1914f7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -67,9 +67,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders inputManager = GetContainingInputManager(); } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 7ae4f387ca..d592e129d9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } + private readonly BindableList controlPoints = new BindableList(); + private readonly IBindable pathVersion = new Bindable(); + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; } - private IBindable pathVersion; - protected override void LoadComplete() { base.LoadComplete(); - pathVersion = HitObject.Path.Version.GetBoundCopy(); + controlPoints.BindTo(HitObject.Path.ControlPoints); + + pathVersion.BindTo(HitObject.Path.Version); pathVersion.BindValueChanged(_ => updatePath()); BodyPiece.UpdateFrom(HitObject); @@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private BindableList controlPoints => HitObject.Path.ControlPoints; - private int addControlPoint(Vector2 position) { position -= HitObject.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs deleted file mode 100644 index 776aacd143..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Edit -{ - public class DrawableOsuEditPool : DrawableOsuPool - where T : DrawableHitObject, new() - { - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - - public DrawableOsuEditPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) - : base(checkHittable, onLoaded, initialSize, maximumSize) - { - } - - protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState); - - private void updateState(DrawableHitObject hitObject, ArmedState state) - { - if (state == ArmedState.Idle) - return; - - // adjust the visuals of certain object types to make them stay on screen for longer than usual. - switch (hitObject) - { - default: - // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) - return; - - case DrawableSlider _: - // no specifics to sliders but let them fade slower below. - break; - - case DrawableHitCircle circle: // also handles slider heads - circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension) - .Expire(); - break; - } - - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 547dff88b5..5fdb79cbbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Graphics.Pooling; +using System.Linq; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -26,8 +29,51 @@ namespace osu.Game.Rulesets.Osu.Edit { protected override GameplayCursorContainer CreateCursor() => null; - protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) - => new DrawableOsuEditPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + protected override void OnNewDrawableHitObject(DrawableHitObject d) + { + d.ApplyCustomUpdateState += updateState; + } + + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + private const double editor_hit_object_fade_out_extension = 700; + + private void updateState(DrawableHitObject hitObject, ArmedState state) + { + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) + { + default: + // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) + return; + + case DrawableSlider _: + // no specifics to sliders but let them fade slower below. + break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + } } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index bfa8ab4431..0490e8b8ce 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -105,11 +105,20 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) { if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return snapResult; + return new SnapResult(screenSpacePosition, null); + } + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition); + if (positionSnap.ScreenSpacePosition != screenSpacePosition) + return positionSnap; + // will be null if distance snap is disabled or not feasible for the current time value. if (distanceSnapGrid == null) return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index f13c7d2ff6..06b5b6cfb8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModEasy : ModEasy + public class OsuModEasy : ModEasyWithExtraLives { public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 3e2ab65bb2..b989500066 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osuTK; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; @@ -47,6 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections }); } - public double AnimationStartTime { get; set; } + public Bindable AnimationStartTime { get; } = new BindableDouble(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 700d96eff3..6e7b1050cb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections fp.Alpha = 0; fp.Scale = new Vector2(1.5f * end.Scale); - fp.AnimationStartTime = fadeInTime; + fp.AnimationStartTime.Value = fadeInTime; using (fp.BeginAbsoluteSequence(fadeInTime)) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index d1ceca6d8f..abb51ae420 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach + public class DrawableHitCircle : DrawableOsuHitObject { public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a26db06ede..628d95dff4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -9,9 +9,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -53,26 +51,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } - protected override void OnApply(HitObject hitObject) + protected override void OnApply() { - base.OnApply(hitObject); + base.OnApply(); IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); PositionBindable.BindTo(HitObject.PositionBindable); StackHeightBindable.BindTo(HitObject.StackHeightBindable); ScaleBindable.BindTo(HitObject.ScaleBindable); - - // Manually set to reduce the number of future alive objects to a bare minimum. - LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; - - // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. - // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. - LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); PositionBindable.UnbindFrom(HitObject.PositionBindable); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs deleted file mode 100644 index 1b5fd50022..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables -{ - public class DrawableOsuPool : DrawablePool - where T : DrawableHitObject, new() - { - private readonly Func checkHittable; - private readonly Action onLoaded; - - public DrawableOsuPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) - : base(initialSize, maximumSize) - { - this.checkHittable = checkHittable; - this.onLoaded = onLoaded; - } - - protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o => - { - var osuObject = (DrawableOsuHitObject)(object)o; - - osuObject.CheckHittable = checkHittable; - osuObject.OnLoadComplete += onLoaded; - }); - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 14c494d909..d3787585e6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -19,7 +19,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach + public class DrawableSlider : DrawableOsuHitObject { public new Slider HitObject => (Slider)base.HitObject; @@ -86,18 +86,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } - protected override void OnApply(HitObject hitObject) + protected override void OnApply() { - base.OnApply(hitObject); + base.OnApply(); // Ensure that the version will change after the upcoming BindTo(). pathVersion.Value = int.MaxValue; PathVersion.BindTo(HitObject.Path.Version); } - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); PathVersion.UnbindFrom(HitObject.Path.Version); } @@ -115,8 +115,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (firstSample != null) { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); - clone.Name = "sliderslide"; + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide"); samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) { @@ -255,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override void PlaySamples() @@ -294,14 +293,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { case ArmedState.Hit: Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); + if (sliderBody?.SnakingOut.Value == true) + Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. break; } this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } - public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); private class DefaultSliderBody : PlaySliderBody diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index fd0f35d20d..3a92938d75 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; @@ -36,9 +35,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathVersion.BindValueChanged(_ => updatePosition()); } - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); pathVersion.UnbindFrom(drawableSlider.PathVersion); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2a14a7c975..5a11265a47 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -110,8 +110,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (firstSample != null) { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); - clone.Name = "spinnerspin"; + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin"); samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index fc9a7c00e6..f37d933e11 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects.Drawables; + namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject @@ -17,6 +19,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } + private DrawableSpinner drawableSpinner; + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + drawableSpinner = (DrawableSpinner)parent; + } + + protected override double MaximumJudgementOffset => drawableSpinner.HitObject.Duration; + /// /// Apply a judgement result. /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index bf2236c945..102166f8dd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }; } - private readonly IBindable state = new Bindable(); private readonly IBindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); @@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { var drawableOsuObject = (DrawableOsuHitObject)drawableObject; - state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); } @@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.LoadComplete(); - state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => { explode.Colour = colour.NewValue; @@ -68,15 +65,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }, true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); + + drawableObject.ApplyCustomUpdateState += updateState; + updateState(drawableObject, drawableObject.State.Value); } - private void updateState(ValueChangedEvent state) + private void updateState(DrawableHitObject drawableObject, ArmedState state) { using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { glow.FadeOut(400); - switch (state.NewValue) + switch (state) { case ArmedState.Hit: const double flash_in = 40; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index c5bf790377..ca5ca7ac59 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } private void trackingChanged(ValueChangedEvent tracking) => - box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint); + box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 755ce0866a..1670df24a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -221,14 +221,7 @@ namespace osu.Game.Rulesets.Osu.Objects var sampleList = new List(); if (firstSample != null) - { - sampleList.Add(new HitSampleInfo - { - Bank = firstSample.Bank, - Volume = firstSample.Volume, - Name = @"slidertick", - }); - } + sampleList.Add(firstSample.With("slidertick")); foreach (var tick in NestedHitObjects.OfType()) tick.Samples = sampleList; diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 235dc8710a..2c443cb96b 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public SpinnerBonusTick() { - Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); + Samples.Add(new HitSampleInfo("spinnerbonus")); } public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 1551d1c149..21af9a479e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Skinning private SkinnableSpriteText hitCircleText; - private readonly IBindable state = new Bindable(); private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); @@ -113,7 +112,6 @@ namespace osu.Game.Rulesets.Osu.Skinning if (overlayAboveNumber) AddInternal(hitCircleOverlay.CreateProxy()); - state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); @@ -137,19 +135,21 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); if (hasNumber) indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + + drawableObject.ApplyCustomUpdateState += updateState; + updateState(drawableObject, drawableObject.State.Value); } - private void updateState(ValueChangedEvent state) + private void updateState(DrawableHitObject drawableObject, ArmedState state) { const double legacy_fade_duration = 240; using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { - switch (state.NewValue) + switch (state) { case ArmedState.Hit: circleSprites.FadeOut(legacy_fade_duration, Easing.Out); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 3bd150c4d3..975b444699 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -25,8 +26,6 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { - public readonly Func CheckHittable; - private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; @@ -56,7 +55,6 @@ namespace osu.Game.Rulesets.Osu.UI }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); - CheckHittable = hitPolicy.IsHittable; var hitWindows = new OsuHitWindows(); @@ -68,6 +66,29 @@ namespace osu.Game.Rulesets.Osu.UI NewResult += onNewResult; } + protected override void OnNewDrawableHitObject(DrawableHitObject drawable) + { + ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; + + Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); + drawable.OnLoadComplete += onDrawableHitObjectLoaded; + } + + private void onDrawableHitObjectLoaded(Drawable drawable) + { + // note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded. + switch (drawable) + { + case DrawableSpinner _: + spinnerProxies.Add(drawable.CreateProxy()); + break; + + case DrawableHitCircle hitCircle: + approachCircles.Add(hitCircle.ProxiedLayer.CreateProxy()); + break; + } + } + private void onJudgmentLoaded(DrawableOsuJudgement judgement) { judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent()); @@ -78,28 +99,19 @@ namespace osu.Game.Rulesets.Osu.UI { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); - registerPool(10, 100); + RegisterPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(5, 50); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(5, 50); - registerPool(2, 20); - registerPool(10, 100); - registerPool(10, 100); + RegisterPool(2, 20); + RegisterPool(10, 100); + RegisterPool(10, 100); } - private void registerPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(CreatePool(initialSize, maximumSize)); - - protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) - where TDrawable : DrawableHitObject, new() - => new DrawableOsuPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); - protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override void OnHitObjectAdded(HitObject hitObject) @@ -114,27 +126,6 @@ namespace osu.Game.Rulesets.Osu.UI followPoints.RemoveFollowPoints((OsuHitObject)hitObject); } - public void OnHitObjectLoaded(Drawable drawable) - { - switch (drawable) - { - case DrawableSliderHead _: - case DrawableSliderTail _: - case DrawableSliderTick _: - case DrawableSliderRepeat _: - case DrawableSpinnerTick _: - break; - - case DrawableSpinner _: - spinnerProxies.Add(drawable.CreateProxy()); - break; - - case IDrawableHitObjectWithProxiedApproach approach: - approachCircles.Add(approach.ProxiedLayer.CreateProxy()); - break; - } - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. @@ -185,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.UI public OsuHitObjectLifetimeEntry(HitObject hitObject) : base(hitObject) { + // Prevent past objects in idles states from remaining alive as their end times are skipped in non-frame-stable contexts. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss); } protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index b59f3a4344..a89645d881 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index c5191ab241..17e7fb81f6 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -43,10 +43,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return false; } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 468d980b23..e53b331f46 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints EndPlacement(true); } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (PlacementActive) { diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index c51b47dc6e..d1ad4c9d8d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModEasy : ModEasy { - public override string Description => @"Beats move slower, less accuracy required, and three lives!"; + public override string Description => @"Beats move slower, and less accuracy required!"; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index bf44a80037..be659f6ca5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Filled = HitObject.FirstTick }); + protected override double MaximumJudgementOffset => HitObject.HitWindow; + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 4a3759794b..29a96a7a40 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (isRimType != rimSamples.Any()) { if (isRimType) - HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); else { foreach (var sample in rimSamples) @@ -125,9 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (s.Name != HitSampleInfo.HIT_FINISH) continue; - var sClone = s.Clone(); - sClone.Name = HitSampleInfo.HIT_WHISTLE; - corrected[i] = sClone; + corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE); } return corrected; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index d8d75a7614..ff5b221273 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (isStrong.Value != strongSamples.Any()) { if (isStrong.Value) - HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); else { foreach (var sample in strongSamples) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 120cf264c3..370760f03e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.UI private SkinnableDrawable mascot; private ProxyContainer topLevelHitContainer; - private ProxyContainer barlineContainer; + private ScrollingHitObjectContainer barlineContainer; private Container rightArea; private Container leftArea; @@ -83,10 +84,7 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - barlineContainer = new ProxyContainer - { - RelativeSizeAxes = Axes.Both, - }, + barlineContainer = new ScrollingHitObjectContainer(), new Container { Name = "Hit objects", @@ -159,18 +157,37 @@ namespace osu.Game.Rulesets.Taiko.UI public override void Add(DrawableHitObject h) { - h.OnNewResult += OnNewResult; - base.Add(h); - switch (h) { case DrawableBarLine barline: - barlineContainer.Add(barline.CreateProxy()); + barlineContainer.Add(barline); break; case DrawableTaikoHitObject taikoObject: + h.OnNewResult += OnNewResult; topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + base.Add(h); break; + + default: + throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type"); + } + } + + public override bool Remove(DrawableHitObject h) + { + switch (h) + { + case DrawableBarLine barline: + return barlineContainer.Remove(barline); + + case DrawableTaikoHitObject _: + h.OnNewResult -= OnNewResult; + // todo: consider tidying of proxied content if required. + return base.Remove(h); + + default: + throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type"); } } diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index b7a41ffd1c..481cb3230e 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -44,6 +43,36 @@ namespace osu.Game.Tests.Editing Assert.That(stateChangedFired, Is.EqualTo(2)); } + [Test] + public void TestApplyThenUndoThenApplySameChange() + { + var (handler, beatmap) = createChangeHandler(); + + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.False); + + string originalHash = handler.CurrentStateHash; + + addArbitraryChange(beatmap); + handler.SaveState(); + + Assert.That(handler.CanUndo.Value, Is.True); + Assert.That(handler.CanRedo.Value, Is.False); + Assert.That(stateChangedFired, Is.EqualTo(1)); + + string hash = handler.CurrentStateHash; + + // undo a change without saving + handler.RestoreState(-1); + + Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash)); + Assert.That(stateChangedFired, Is.EqualTo(2)); + + addArbitraryChange(beatmap); + handler.SaveState(); + Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + } + [Test] public void TestSaveSameStateDoesNotSave() { @@ -139,7 +168,7 @@ namespace osu.Game.Tests.Editing private void addArbitraryChange(EditorBeatmap beatmap) { - beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); + beatmap.Add(new HitCircle { StartTime = 2760 }); } } } diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index bb56131b04..44a908b756 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Editing HitObjects = { (OsuHitObject)current.HitObjects[0], - new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } }, (OsuHitObject)current.HitObjects[2], } }; @@ -268,12 +268,12 @@ namespace osu.Game.Tests.Editing HitObjects = { (OsuHitObject)current.HitObjects[0], - new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } }, (OsuHitObject)current.HitObjects[2], (OsuHitObject)current.HitObjects[3], - new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } }, + new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } }, (OsuHitObject)current.HitObjects[5], - new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } }, + new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_CLAP) } }, (OsuHitObject)current.HitObjects[7], } }; diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs new file mode 100644 index 0000000000..a5c937119e --- /dev/null +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Audio; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.NonVisual.Skinning +{ + [HeadlessTest] + public class LegacySkinAnimationTest : OsuTestScene + { + private const string animation_name = "animation"; + private const int frame_count = 6; + + [Cached(typeof(IAnimationTimeReference))] + private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference(); + + private TextureAnimation animation; + + [Test] + public void TestAnimationTimeReferenceChange() + { + ISkin skin = new TestSkin(); + + AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false))); + AddAssert("frame count correct", () => animation.FrameCount == frame_count); + assertPlaybackPosition(0); + + AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000); + assertPlaybackPosition(-1000); + + AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500); + assertPlaybackPosition(-500); + } + + private void assertPlaybackPosition(double expectedPosition) + => AddAssert($"playback position is {expectedPosition}", () => animation.PlaybackPosition == expectedPosition); + + private class TestSkin : ISkin + { + private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray(); + + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + return lookup_names.Contains(componentName) ? Texture.WhitePixel : null; + } + + public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); + public IBindable GetConfig(TLookup lookup) => throw new NotSupportedException(); + } + + private class TestAnimationTimeReference : IAnimationTimeReference + { + public ManualClock ManualClock { get; } + public IFrameBasedClock Clock { get; } + public Bindable AnimationStartTime { get; } = new BindableDouble(); + + public TestAnimationTimeReference() + { + ManualClock = new ManualClock(); + Clock = new FramedClock(ManualClock); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 9ef9649f77..5323f58a66 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background private class FadeAccessibleResults : ResultsScreen { public FadeAccessibleResults(ScoreInfo score) - : base(score) + : base(score, true) { } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 8190cf5f89..11830ebe35 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -153,6 +153,9 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IPositionSnapProvider { + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => 10; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 1a1babb4a8..9931ee4a45 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -21,13 +21,13 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; +using JetBrains.Annotations; namespace osu.Game.Tests.Visual.Gameplay { @@ -46,6 +46,50 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void Setup() => Schedule(() => testClock.CurrentTime = 0); + [TestCase("pooled")] + [TestCase("non-pooled")] + public void TestHitObjectLifetime(string pooled) + { + var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject()); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); + createTest(beatmap); + + assertPosition(0, 0f); + assertDead(3); + + setTime(3 * time_range); + assertPosition(3, 0f); + assertDead(0); + + setTime(0 * time_range); + assertPosition(0, 0f); + assertDead(3); + } + + [TestCase("pooled")] + [TestCase("non-pooled")] + public void TestNestedHitObject(string pooled) + { + var beatmap = createBeatmap(i => + { + var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject(); + h.Duration = 300; + h.ChildTimeOffset = i % 3 * 100; + return h; + }); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); + createTest(beatmap); + + assertPosition(0, 0f); + assertHeight(0); + assertChildPosition(0); + + setTime(5 * time_range); + assertPosition(5, 0f); + assertHeight(5); + assertChildPosition(5); + } + [Test] public void TestRelativeBeatLengthScaleSingleTimingPoint() { @@ -147,8 +191,37 @@ namespace osu.Game.Tests.Visual.Gameplay assertPosition(1, 1); } + /// + /// Get a corresponding to the 'th . + /// When the hit object is not alive, `null` is returned. + /// + [CanBeNull] + private DrawableTestHitObject getDrawableHitObject(int index) + { + var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index); + return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject); + } + + private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight; + + private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null); + + private void assertHeight(int index) => AddAssert($"hitobject {index} height", () => + { + var d = getDrawableHitObject(index); + return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f); + }); + + private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () => + { + var d = getDrawableHitObject(index); + return d is DrawableTestParentHitObject && Precision.AlmostEquals( + d.NestedHitObjects.First().DrawPosition.Y, + yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f); + }); + private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}", - () => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY)); + () => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY)); private void setTime(double time) { @@ -160,12 +233,16 @@ namespace osu.Game.Tests.Visual.Gameplay /// The hitobjects are spaced milliseconds apart. /// /// The . - private IBeatmap createBeatmap() + private IBeatmap createBeatmap(Func createAction = null) { - var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; + var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; for (int i = 0; i < 10; i++) - beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range }); + { + var h = createAction?.Invoke(i) ?? new TestHitObject(); + h.StartTime = i * time_range; + beatmap.HitObjects.Add(h); + } return beatmap; } @@ -225,7 +302,21 @@ namespace osu.Game.Tests.Visual.Gameplay TimeRange.Value = time_range; } - public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h); + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) + { + switch (h) + { + case TestPooledHitObject _: + case TestPooledParentHitObject _: + return null; + + case TestParentHitObject p: + return new DrawableTestParentHitObject(p); + + default: + return new DrawableTestHitObject(h); + } + } protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); @@ -265,6 +356,9 @@ namespace osu.Game.Tests.Visual.Gameplay } } }); + + RegisterPool(1); + RegisterPool(1); } } @@ -277,30 +371,46 @@ namespace osu.Game.Tests.Visual.Gameplay public override bool CanConvert() => true; - protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) - { - yield return new TestHitObject - { - StartTime = original.StartTime, - Duration = (original as IHasDuration)?.Duration ?? 100 - }; - } + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => + throw new NotImplementedException(); } #endregion #region HitObject - private class TestHitObject : ConvertHitObject, IHasDuration + private class TestHitObject : HitObject, IHasDuration { public double EndTime => StartTime + Duration; - public double Duration { get; set; } + public double Duration { get; set; } = 100; + } + + private class TestPooledHitObject : TestHitObject + { + } + + private class TestParentHitObject : TestHitObject + { + public double ChildTimeOffset; + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset }); + } + } + + private class TestPooledParentHitObject : TestParentHitObject + { + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset }); + } } private class DrawableTestHitObject : DrawableHitObject { - public DrawableTestHitObject(TestHitObject hitObject) + public DrawableTestHitObject([CanBeNull] TestHitObject hitObject) : base(hitObject) { Anchor = Anchor.TopCentre; @@ -324,6 +434,52 @@ namespace osu.Game.Tests.Visual.Gameplay } }); } + + protected override void Update() => LifetimeEnd = HitObject.EndTime; + } + + private class DrawableTestPooledHitObject : DrawableTestHitObject + { + public DrawableTestPooledHitObject() + : base(null) + { + InternalChildren[0].Colour = Color4.LightSkyBlue; + InternalChildren[1].Colour = Color4.Blue; + } + } + + private class DrawableTestParentHitObject : DrawableTestHitObject + { + private readonly Container container; + + public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject) + : base(hitObject) + { + InternalChildren[0].Colour = Color4.LightYellow; + InternalChildren[1].Colour = Color4.Yellow; + + AddInternal(container = new Container + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) => + new DrawableTestHitObject((TestHitObject)hitObject); + + protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject); + + protected override void ClearNestedHitObjects() => container.Clear(false); + } + + private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject + { + public DrawableTestPooledParentHitObject() + : base(null) + { + InternalChildren[0].Colour = Color4.LightSeaGreen; + InternalChildren[1].Colour = Color4.Green; + } } #endregion diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 3e777119c4..cd7d692b0a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -261,9 +261,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected override void OnApply(HitObject hitObject) + protected override void OnApply() { - base.OnApply(hitObject); + base.OnApply(); Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs index 709e71d195..717485bcc1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -14,44 +13,41 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneStarCounter : OsuTestScene { + private readonly StarCounter starCounter; + private readonly OsuSpriteText starsLabel; + public TestSceneStarCounter() { - StarCounter stars = new StarCounter + starCounter = new StarCounter { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Current = 5, }; - Add(stars); + Add(starCounter); - SpriteText starsLabel = new OsuSpriteText + starsLabel = new OsuSpriteText { Origin = Anchor.Centre, Anchor = Anchor.Centre, Scale = new Vector2(2), Y = 50, - Text = stars.Current.ToString("0.00"), }; Add(starsLabel); - AddRepeatStep(@"random value", delegate - { - stars.Current = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.Current.ToString("0.00"); - }, 10); + setStars(5); - AddStep(@"Stop animation", delegate - { - stars.StopAnimation(); - }); + AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10); + AddSliderStep("exact value", 0f, 10f, 5f, setStars); + AddStep("stop animation", () => starCounter.StopAnimation()); + AddStep("reset", () => setStars(0)); + } - AddStep(@"Reset", delegate - { - stars.Current = 0; - starsLabel.Text = stars.Current.ToString("0.00"); - }); + private void setStars(float stars) + { + starCounter.Current = stars; + starsLabel.Text = starCounter.Current.ToString("0.00"); } } } diff --git a/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs new file mode 100644 index 0000000000..cf5ecf5bf2 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs @@ -0,0 +1,181 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.Profile.Sections.Historical; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Users; +using NUnit.Framework; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using System; +using System.Linq; +using osu.Framework.Testing; +using osu.Framework.Graphics.Shapes; +using static osu.Game.Users.User; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestScenePlayHistorySubsection : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); + + private readonly Bindable user = new Bindable(); + private readonly PlayHistorySubsection section; + + public TestScenePlayHistorySubsection() + { + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + section = new PlayHistorySubsection(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + } + + [Test] + public void TestNullValues() + { + AddStep("Load user", () => user.Value = user_with_null_values); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestEmptyValues() + { + AddStep("Load user", () => user.Value = user_with_empty_values); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestOneValue() + { + AddStep("Load user", () => user.Value = user_with_one_value); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestTwoValues() + { + AddStep("Load user", () => user.Value = user_with_two_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestConstantValues() + { + AddStep("Load user", () => user.Value = user_with_constant_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestConstantZeroValues() + { + AddStep("Load user", () => user.Value = user_with_zero_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestFilledValues() + { + AddStep("Load user", () => user.Value = user_with_filled_values); + AddAssert("Section is visible", () => section.Alpha == 1); + AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlaycounts.Length == getChartValuesLength()); + } + + [Test] + public void TestMissingValues() + { + AddStep("Load user", () => user.Value = user_with_missing_values); + AddAssert("Section is visible", () => section.Alpha == 1); + AddAssert("Array length is 7", () => getChartValuesLength() == 7); + } + + private int getChartValuesLength() => this.ChildrenOfType().Single().Values.Length; + + private static readonly User user_with_null_values = new User + { + Id = 1 + }; + + private static readonly User user_with_empty_values = new User + { + Id = 2, + MonthlyPlaycounts = Array.Empty() + }; + + private static readonly User user_with_one_value = new User + { + Id = 3, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 100 } + } + }; + + private static readonly User user_with_two_values = new User + { + Id = 4, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 2 } + } + }; + + private static readonly User user_with_constant_values = new User + { + Id = 5, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 5 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 5 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 5 } + } + }; + + private static readonly User user_with_zero_values = new User + { + Id = 6, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 0 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 0 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 0 } + } + }; + + private static readonly User user_with_filled_values = new User + { + Id = 7, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 }, + new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 }, + new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 }, + new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 }, + new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } + } + }; + + private static readonly User user_with_missing_values = new User + { + Id = 8, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2020, 1, 1), Count = 100 }, + new UserHistoryCount { Date = new DateTime(2020, 7, 1), Count = 200 } + } + }; + } +} diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ff96a999ec..b2be7cdf88 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) - : base(score) + : base(score, true) { } @@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking public HotkeyRetryOverlay RetryOverlay; public UnrankedSoloResultsScreen(ScoreInfo score) - : base(score) + : base(score, true) { Score.Beatmap.OnlineBeatmapID = 0; Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 4699784327..44c9361ff8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect { get { - foreach (var item in ScrollableContent) + foreach (var item in Scroll.Children) { yield return item; diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index aa531ba106..35c6d62cb7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -643,6 +643,55 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } + [Test] + public void TestChangingRulesetOnMultiRulesetBeatmap() + { + int changeCount = 0; + + AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false)); + AddStep("bind beatmap changed", () => + { + Beatmap.ValueChanged += onChange; + changeCount = 0; + }); + + changeRuleset(0); + + createSongSelect(); + + AddStep("import multi-ruleset map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); + }); + + int previousSetID = 0; + + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + + AddStep("record set ID", () => previousSetID = Beatmap.Value.BeatmapSetInfo.ID); + AddAssert("selection changed once", () => changeCount == 1); + + AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); + + changeRuleset(3); + + AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); + + AddUntilStep("selection changed", () => changeCount > 1); + + AddAssert("Selected beatmap still same set", () => Beatmap.Value.BeatmapSetInfo.ID == previousSetID); + AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3); + + AddAssert("selection changed only fired twice", () => changeCount == 2); + + AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange); + AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + + // ReSharper disable once AccessToModifiedClosure + void onChange(ValueChangedEvent valueChangedEvent) => changeCount++; + } + [Test] public void TestDifficultyIconSelectingForDifferentRuleset() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs similarity index 95% rename from osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs index 2e9f919cfd..cd226662d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs @@ -11,12 +11,12 @@ using osu.Framework.Allocation; namespace osu.Game.Tests.Visual.UserInterface { - public class TestScenePaginatedContainerHeader : OsuTestScene + public class TestSceneProfileSubsectionHeader : OsuTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - private PaginatedContainerHeader header; + private ProfileSubsectionHeader header; [Test] public void TestHiddenCounter() @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void createHeader(string text, CounterVisibilityState state, int initialValue = 0) { Clear(); - Add(header = new PaginatedContainerHeader(text, state) + Add(header = new ProfileSubsectionHeader(text, state) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c692bcd5e4..83d7b4135a 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 5d55196dcf..bc6b994988 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 999ce61ac8..71417d1cc6 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -243,7 +243,7 @@ namespace osu.Game.Tournament.IPC string stableInstallPath; using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); if (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj index 9cce40c9d3..b049542bb0 100644 --- a/osu.Game.Tournament/osu.Game.Tournament.csproj +++ b/osu.Game.Tournament/osu.Game.Tournament.csproj @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8efaeb3795..3d90dd0189 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -1,8 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Utils; namespace osu.Game.Audio { @@ -10,7 +14,7 @@ namespace osu.Game.Audio /// Describes a gameplay hit sample. /// [Serializable] - public class HitSampleInfo : ISampleInfo + public class HitSampleInfo : ISampleInfo, IEquatable { public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_FINISH = @"hitfinish"; @@ -22,29 +26,38 @@ namespace osu.Game.Audio /// public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH }; - /// - /// The bank to load the sample from. - /// - public string Bank; - /// /// The name of the sample to load. /// - public string Name; + public readonly string Name; + + /// + /// The bank to load the sample from. + /// + public readonly string? Bank; /// /// An optional suffix to provide priority lookup. Falls back to non-suffixed . /// - public string Suffix; + public readonly string? Suffix; /// /// The sample volume. /// - public int Volume { get; set; } + public int Volume { get; } + + public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 0) + { + Name = name; + Bank = bank; + Suffix = suffix; + Volume = volume; + } /// /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). /// + [JsonIgnore] public virtual IEnumerable LookupNames { get @@ -56,6 +69,23 @@ namespace osu.Game.Audio } } - public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone(); + /// + /// Creates a new with overridden values. + /// + /// An optional new sample name. + /// An optional new sample bank. + /// An optional new lookup suffix. + /// An optional new volume. + /// The new . + public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume)); + + public bool Equals(HitSampleInfo? other) + => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; + + public override bool Equals(object? obj) + => obj is HitSampleInfo other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix); } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index f57ecfb9e3..fd0b496335 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -58,12 +58,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The name of the same. /// A populated . - public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) => new HitSampleInfo - { - Bank = SampleBank, - Name = sampleName, - Volume = SampleVolume, - }; + public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) => new HitSampleInfo(sampleName, SampleBank, volume: SampleVolume); /// /// Applies and to a if necessary, returning the modified . @@ -71,12 +66,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// The . This will not be modified. /// The modified . This does not share a reference with . public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo) - { - var newSampleInfo = hitSampleInfo.Clone(); - newSampleInfo.Bank = hitSampleInfo.Bank ?? SampleBank; - newSampleInfo.Volume = hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume; - return newSampleInfo; - } + => hitSampleInfo.With(newBank: hitSampleInfo.Bank ?? SampleBank, newVolume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume); public override bool IsRedundant(ControlPoint existing) => existing is SampleControlPoint existingSample diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7ddb0b4caa..df940e8c8e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -192,7 +192,7 @@ namespace osu.Game.Beatmaps.Formats var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) - HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo()); + HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); // Convert effect flags to the legacy format LegacyEffectFlags effectFlags = LegacyEffectFlags.None; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index de4dc8cdc8..c9d139bdd0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -182,11 +182,8 @@ namespace osu.Game.Beatmaps.Formats { var baseInfo = base.ApplyTo(hitSampleInfo); - if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy - && legacy.CustomSampleBank == 0) - { - legacy.CustomSampleBank = CustomSampleBank; - } + if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) + return legacy.With(newCustomSampleBank: CustomSampleBank); return baseInfo; } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a4b99bb6e6..a07e446d2e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -184,7 +184,7 @@ namespace osu.Game.Configuration return new TrackedSettings { new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled", LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), - new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: shift-tab quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), + new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), new TrackedSetting(OsuSetting.Skin, m => { @@ -196,7 +196,7 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } - public Func LookupKeyBindings { private get; set; } + public Func LookupKeyBindings { get; set; } } public enum OsuSetting diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index b9122d254d..aaad72f65c 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -12,7 +12,19 @@ using osuTK.Input; namespace osu.Game.Graphics.Containers { - public class OsuScrollContainer : ScrollContainer + public class OsuScrollContainer : OsuScrollContainer + { + public OsuScrollContainer() + { + } + + public OsuScrollContainer(Direction direction) + : base(direction) + { + } + } + + public class OsuScrollContainer : ScrollContainer where T : Drawable { public const float SCROLL_BAR_HEIGHT = 10; public const float SCROLL_BAR_PADDING = 3; diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index f32f8e0c67..81968de304 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +15,7 @@ namespace osu.Game.Graphics.Containers /// /// A container that can scroll to each section inside it. /// + [Cached] public class SectionsContainer : Container where T : Drawable { diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index 42b523fc5c..70db26c817 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -119,7 +119,11 @@ namespace osu.Game.Graphics.UserInterface protected float GetYPosition(float value) { - if (ActualMaxValue == ActualMinValue) return 0; + if (ActualMaxValue == ActualMinValue) + // show line at top if the only value on the graph is positive, + // and at bottom if the only value on the graph is zero or negative. + // just kind of makes most sense intuitively. + return value > 1 ? 0 : 1; return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue); } diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index b13d6485ac..894a21fcf3 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface public override void DisplayAt(float scale) { - scale = Math.Clamp(scale, min_star_scale, 1); + scale = (float)Interpolation.Lerp(min_star_scale, 1, Math.Clamp(scale, 0, 1)); this.FadeTo(scale, fading_duration); Icon.ScaleTo(scale, scaling_duration, scaling_easing); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e5d3a89a88..f4a4813b94 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -68,7 +68,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), + new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), + new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), }; @@ -163,10 +165,10 @@ namespace osu.Game.Input.Bindings [Description("Toggle now playing overlay")] ToggleNowPlaying, - [Description("Previous Selection")] + [Description("Previous selection")] SelectPrevious, - [Description("Next Selection")] + [Description("Next selection")] SelectNext, [Description("Home")] @@ -175,26 +177,32 @@ namespace osu.Game.Input.Bindings [Description("Toggle notifications")] ToggleNotifications, - [Description("Pause")] + [Description("Pause gameplay")] PauseGameplay, // Editor - [Description("Setup Mode")] + [Description("Setup mode")] EditorSetupMode, - [Description("Compose Mode")] + [Description("Compose mode")] EditorComposeMode, - [Description("Design Mode")] + [Description("Design mode")] EditorDesignMode, - [Description("Timing Mode")] + [Description("Timing mode")] EditorTimingMode, [Description("Hold for HUD")] HoldForHUD, - [Description("Random Skin")] + [Description("Random skin")] RandomSkin, + + [Description("Pause / resume replay")] + TogglePauseReplay, + + [Description("Toggle in-game interface")] + ToggleInGameInterface, } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 9ba81720d8..08b524087a 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -36,6 +36,8 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); + private readonly object userLock = new object(); + public IBindableList PlayingUsers => playingUsers; private readonly BindableList playingUsers = new BindableList(); @@ -144,12 +146,19 @@ namespace osu.Game.Online.Spectator await connection.StartAsync(); Logger.Log("Spectator client connected!", LoggingTarget.Network); + // get all the users that were previously being watched + int[] users; + + lock (userLock) + { + users = watchingUsers.ToArray(); + watchingUsers.Clear(); + } + // success isConnected = true; // resubscribe to watched users - var users = watchingUsers.ToArray(); - watchingUsers.Clear(); foreach (var userId in users) WatchUser(userId); @@ -238,21 +247,29 @@ namespace osu.Game.Online.Spectator public virtual void WatchUser(int userId) { - if (watchingUsers.Contains(userId)) - return; + lock (userLock) + { + if (watchingUsers.Contains(userId)) + return; - watchingUsers.Add(userId); + watchingUsers.Add(userId); - if (!isConnected) return; + if (!isConnected) + return; + } connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } public void StopWatchingUser(int userId) { - watchingUsers.Remove(userId); + lock (userLock) + { + watchingUsers.Remove(userId); - if (!isConnected) return; + if (!isConnected) + return; + } connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index acc42bb660..b1aa43ef13 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -435,7 +435,7 @@ namespace osu.Game break; case ScorePresentType.Results: - screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo)); + screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false)); break; } }, validScreens: new[] { typeof(PlaySongSelect) }); diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index e6edfb1e3e..f06e02e5e1 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -37,11 +38,11 @@ namespace osu.Game.Overlays.Music bool wasPlaying = musicController.IsPlaying; if (musicController.TogglePause()) - onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track")); + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", action)); return true; case GlobalAction.MusicNext: - musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track"))); + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", action))); return true; @@ -51,11 +52,11 @@ namespace osu.Game.Overlays.Music switch (res) { case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicActionToast("Restart track")); + onScreenDisplay?.Display(new MusicActionToast("Restart track", action)); break; case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicActionToast("Previous track")); + onScreenDisplay?.Display(new MusicActionToast("Previous track", action)); break; } }); @@ -72,9 +73,18 @@ namespace osu.Game.Overlays.Music private class MusicActionToast : Toast { - public MusicActionToast(string action) - : base("Music Playback", action, string.Empty) + private readonly GlobalAction action; + + public MusicActionToast(string value, GlobalAction action) + : base("Music Playback", value, string.Empty) { + this.action = action; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant(); } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b5714fbcae..d51d964fc4 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -6,13 +6,13 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Notifications; -using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; +using osu.Game.Graphics; namespace osu.Game.Overlays { @@ -44,8 +44,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f + Colour = OsuColour.Gray(0.05f), }, new OsuScrollContainer { diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 1497ca8fa8..4a6316df3f 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -16,10 +16,13 @@ namespace osu.Game.Overlays.OSD private const int toast_minimum_width = 240; private readonly Container content; + protected override Container Content => content; protected readonly OsuSpriteText ValueText; + protected readonly OsuSpriteText ShortcutText; + protected Toast(string description, string value, string shortcut) { Anchor = Anchor.Centre; @@ -68,7 +71,7 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.Centre, Text = value }, - new OsuSpriteText + ShortcutText = new OsuSpriteText { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 4b7de8de90..780d7ea986 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Overlays.Profile.Sections.Beatmaps { - public class PaginatedBeatmapContainer : PaginatedContainer + public class PaginatedBeatmapContainer : PaginatedProfileSubsection { private const float panel_padding = 10f; private readonly BeatmapSetType type; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs new file mode 100644 index 0000000000..b82773155d --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -0,0 +1,84 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public abstract class ChartProfileSubsection : ProfileSubsection + { + private ProfileLineChart chart; + + protected ChartProfileSubsection(Bindable user, string headerText) + : base(user, headerText) + { + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = 10, + Left = 20, + Right = 40 + }, + Child = chart = new ProfileLineChart() + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserChanged, true); + } + + private void onUserChanged(ValueChangedEvent e) + { + var values = GetValues(e.NewValue); + + if (values == null || values.Length <= 1) + { + Hide(); + return; + } + + chart.Values = fillZeroValues(values); + Show(); + } + + /// + /// Add entries for any missing months (filled with zero values). + /// + private UserHistoryCount[] fillZeroValues(UserHistoryCount[] historyEntries) + { + var filledHistoryEntries = new List(); + + foreach (var entry in historyEntries) + { + var lastFilled = filledHistoryEntries.LastOrDefault(); + + while (lastFilled?.Date.AddMonths(1) < entry.Date) + { + filledHistoryEntries.Add(lastFilled = new UserHistoryCount + { + Count = 0, + Date = lastFilled.Date.AddMonths(1) + }); + } + + filledHistoryEntries.Add(entry); + } + + return filledHistoryEntries.ToArray(); + } + + protected abstract UserHistoryCount[] GetValues(User user); + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 556f3139dd..e5bb1f8008 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -13,7 +13,7 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections.Historical { - public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer + public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection { public PaginatedMostPlayedBeatmapContainer(Bindable user) : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs new file mode 100644 index 0000000000..2f15886c3a --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs @@ -0,0 +1,19 @@ +// 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.Bindables; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class PlayHistorySubsection : ChartProfileSubsection + { + public PlayHistorySubsection(Bindable user) + : base(user, "Play History") + { + } + + protected override UserHistoryCount[] GetValues(User user) => user?.MonthlyPlaycounts; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs new file mode 100644 index 0000000000..f02aa36b6c --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -0,0 +1,259 @@ +// 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.Containers; +using osu.Framework.Graphics; +using JetBrains.Annotations; +using System; +using System.Linq; +using osu.Game.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class ProfileLineChart : CompositeDrawable + { + private UserHistoryCount[] values; + + [NotNull] + public UserHistoryCount[] Values + { + get => values; + set + { + if (value.Length == 0) + throw new ArgumentException("At least one value expected!", nameof(value)); + + graph.Values = values = value; + + createRowTicks(); + createColumnTicks(); + } + } + + private readonly UserHistoryGraph graph; + private readonly Container rowTicksContainer; + private readonly Container columnTicksContainer; + private readonly Container rowLinesContainer; + private readonly Container columnLinesContainer; + + public ProfileLineChart() + { + RelativeSizeAxes = Axes.X; + Height = 250; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + rowTicksContainer = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + rowLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + columnLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + } + } + }, + graph = new UserHistoryGraph + { + RelativeSizeAxes = Axes.Both + } + } + } + }, + new[] + { + Empty(), + columnTicksContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 10 } + } + } + } + }; + } + + private void createRowTicks() + { + rowTicksContainer.Clear(); + rowLinesContainer.Clear(); + + var min = values.Select(v => v.Count).Min(); + var max = values.Select(v => v.Count).Max(); + + var tickInterval = getTickInterval(max - min, 6); + + for (long currentTick = 0; currentTick <= max; currentTick += tickInterval) + { + if (currentTick < min) + continue; + + float y; + + // special-case the min == max case to match LineGraph. + // lerp isn't really well-defined over a zero interval anyway. + if (min == max) + y = currentTick > 1 ? 1 : 0; + else + y = Interpolation.ValueAt(currentTick, 0, 1f, min, max); + + // y axis is inverted in graph-like coordinates. + addRowTick(-y, currentTick); + } + } + + private void createColumnTicks() + { + columnTicksContainer.Clear(); + columnLinesContainer.Clear(); + + var totalMonths = values.Length; + + int monthsPerTick = 1; + + if (totalMonths > 80) + monthsPerTick = 12; + else if (totalMonths >= 45) + monthsPerTick = 3; + else if (totalMonths > 20) + monthsPerTick = 2; + + for (int i = 0; i < totalMonths; i += monthsPerTick) + { + var x = (float)i / (totalMonths - 1); + addColumnTick(x, values[i].Date); + } + } + + private void addRowTick(float y, double value) + { + rowTicksContainer.Add(new TickText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativePositionAxes = Axes.Y, + Margin = new MarginPadding { Right = 3 }, + Text = value.ToString("N0"), + Font = OsuFont.GetFont(size: 12), + Y = y + }); + + rowLinesContainer.Add(new TickLine + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + RelativePositionAxes = Axes.Y, + Height = 0.1f, + EdgeSmoothness = Vector2.One, + Y = y + }); + } + + private void addColumnTick(float x, DateTime value) + { + columnTicksContainer.Add(new TickText + { + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + Text = value.ToString("MMM yyyy"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Rotation = 45, + X = x + }); + + columnLinesContainer.Add(new TickLine + { + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 0.1f, + EdgeSmoothness = Vector2.One, + X = x + }); + } + + private long getTickInterval(long range, int maxTicksCount) + { + // this interval is what would be achieved if the interval was divided perfectly evenly into maxTicksCount ticks. + // can contain ugly fractional parts. + var exactTickInterval = (float)range / (maxTicksCount - 1); + + // the ideal ticks start with a 1, 2 or 5, and are multipliers of powers of 10. + // first off, use log10 to calculate the number of digits in the "exact" interval. + var numberOfDigits = Math.Floor(Math.Log10(exactTickInterval)); + var tickBase = Math.Pow(10, numberOfDigits); + + // then see how the exact tick relates to the power of 10. + var exactTickMultiplier = exactTickInterval / tickBase; + + double tickMultiplier; + + // round up the fraction to start with a 1, 2 or 5. closest match wins. + if (exactTickMultiplier < 1.5) + tickMultiplier = 1.0; + else if (exactTickMultiplier < 3) + tickMultiplier = 2.0; + else if (exactTickMultiplier < 7) + tickMultiplier = 5.0; + else + tickMultiplier = 10.0; + + return Math.Max((long)(tickMultiplier * tickBase), 1); + } + + private class TickText : OsuSpriteText + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Foreground1; + } + } + + private class TickLine : Box + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Background6; + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs new file mode 100644 index 0000000000..e594e8d020 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs @@ -0,0 +1,19 @@ +// 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.Bindables; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class ReplaysSubsection : ChartProfileSubsection + { + public ReplaysSubsection(Bindable user) + : base(user, "Replays Watched History") + { + } + + protected override UserHistoryCount[] GetValues(User user) => user?.ReplaysWatchedCounts; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index bfc47bd88c..6e2b9873cf 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -18,8 +18,10 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new Drawable[] { + new PlayHistorySubsection(User), new PaginatedMostPlayedBeatmapContainer(User), new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero), + new ReplaysSubsection(User) }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index 1b8bd23eb4..008d89d881 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -11,7 +11,7 @@ using System.Collections.Generic; namespace osu.Game.Overlays.Profile.Sections.Kudosu { - public class PaginatedKudosuHistoryContainer : PaginatedContainer + public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection { public PaginatedKudosuHistoryContainer(Bindable user) : base(user, missingText: "This user hasn't received any kudosu!") diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs similarity index 72% rename from osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs rename to osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index c1107ce907..51e5622f68 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -6,62 +6,51 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Online.API; -using osu.Game.Rulesets; using osu.Game.Users; using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Sections { - public abstract class PaginatedContainer : FillFlowContainer + public abstract class PaginatedProfileSubsection : ProfileSubsection { [Resolved] private IAPIProvider api { get; set; } + [Resolved] + protected RulesetStore Rulesets { get; private set; } + protected int VisiblePages; protected int ItemsPerPage; - protected readonly Bindable User = new Bindable(); - protected FillFlowContainer ItemsContainer; - protected RulesetStore Rulesets; + protected FillFlowContainer ItemsContainer { get; private set; } private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; - private readonly string missingText; private ShowMoreButton moreButton; private OsuSpriteText missing; - private PaginatedContainerHeader header; + private readonly string missingText; - private readonly string headerText; - private readonly CounterVisibilityState counterVisibilityState; - - protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + : base(user, headerText, counterVisibilityState) { - this.headerText = headerText; this.missingText = missingText; - this.counterVisibilityState = counterVisibilityState; - User.BindTo(user); } - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + protected override Drawable CreateContent() => new FillFlowContainer { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new PaginatedContainerHeader(headerText, counterVisibilityState) - { - Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 - }, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -81,13 +70,14 @@ namespace osu.Game.Overlays.Profile.Sections Font = OsuFont.GetFont(size: 15), Text = missingText, Alpha = 0, - }, - }; + } + } + }; - Rulesets = rulesets; - - User.ValueChanged += onUserChanged; - User.TriggerChange(); + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserChanged, true); } private void onUserChanged(ValueChangedEvent e) @@ -124,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Sections moreButton.Hide(); moreButton.IsLoading = false; - if (!string.IsNullOrEmpty(missing.Text)) + if (!string.IsNullOrEmpty(missingText)) missing.Show(); return; @@ -142,8 +132,6 @@ namespace osu.Game.Overlays.Profile.Sections protected virtual int GetCount(User user) => 0; - protected void SetCount(int value) => header.Current.Value = value; - protected virtual void OnItemsReceived(List items) { } @@ -154,8 +142,9 @@ namespace osu.Game.Overlays.Profile.Sections protected override void Dispose(bool isDisposing) { - base.Dispose(isDisposing); retrievalRequest?.Cancel(); + loadCancellation?.Cancel(); + base.Dispose(isDisposing); } } } diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs new file mode 100644 index 0000000000..3e331f85e9 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -0,0 +1,51 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Users; +using JetBrains.Annotations; + +namespace osu.Game.Overlays.Profile.Sections +{ + public abstract class ProfileSubsection : FillFlowContainer + { + protected readonly Bindable User = new Bindable(); + + private readonly string headerText; + private readonly CounterVisibilityState counterVisibilityState; + + private ProfileSubsectionHeader header; + + protected ProfileSubsection(Bindable user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + { + this.headerText = headerText; + this.counterVisibilityState = counterVisibilityState; + User.BindTo(user); + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + + Children = new[] + { + header = new ProfileSubsectionHeader(headerText, counterVisibilityState) + { + Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 + }, + CreateContent() + }; + } + + [NotNull] + protected abstract Drawable CreateContent(); + + protected void SetCount(int value) => header.Current.Value = value; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs similarity index 95% rename from osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs rename to osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs index 8c617e5fbd..5858cebe89 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Sections { - public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue + public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections private CounterPill counterPill; - public PaginatedContainerHeader(string text, CounterVisibilityState counterState) + public ProfileSubsectionHeader(string text, CounterVisibilityState counterState) { this.text = text; this.counterState = counterState; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 1ce3079d52..53f6d375ca 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -14,7 +14,7 @@ using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Ranks { - public class PaginatedScoreContainer : PaginatedContainer + public class PaginatedScoreContainer : PaginatedProfileSubsection { private readonly ScoreType type; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 08f39c6272..d7101a8147 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -13,7 +13,7 @@ using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Recent { - public class PaginatedRecentActivityContainer : PaginatedContainer + public class PaginatedRecentActivityContainer : PaginatedProfileSubsection { public PaginatedRecentActivityContainer(Bindable user) : base(user, missingText: "This user hasn't done anything notable recently!") diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index 69538358f1..7072d8e63d 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -27,7 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections new AudioDevicesSettings(), new VolumeSettings(), new OffsetSettings(), - new MainMenuSettings() }; } } diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index e5cebd28e2..acb94a6a01 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections Children = new Drawable[] { new GeneralSettings(), - new SongSelectSettings(), new ModsSettings(), }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 14b8dbfac0..62dc1dc806 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -208,7 +208,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private IReadOnlyList getResolutions() { var resolutions = new List { new Size(9999, 9999) }; - var currentDisplay = game.Window?.CurrentDisplay.Value; + var currentDisplay = game.Window?.CurrentDisplayBindable.Value; if (currentDisplay != null) { diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index c1b4b0bbcb..4ade48031f 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Settings.Sections new RendererSettings(), new LayoutSettings(), new DetailSettings(), - new UserInterfaceSettings(), }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index f0d51a0d37..b54ad9a641 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableBool rawInputToggle = new BindableBool(); private Bindable sensitivityBindable = new BindableDouble(); - private Bindable ignoredInputHandler; + private Bindable ignoredInputHandlers; [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) @@ -75,20 +75,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input { // this is temporary until we support per-handler settings. const string raw_mouse_handler = @"OsuTKRawMouseHandler"; - const string standard_mouse_handler = @"OsuTKMouseHandler"; + const string standard_mouse_handlers = @"OsuTKMouseHandler MouseHandler"; - ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler; + ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler; }; - ignoredInputHandler = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); - ignoredInputHandler.ValueChanged += handler => + ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); + ignoredInputHandlers.ValueChanged += handler => { bool raw = !handler.NewValue.Contains("Raw"); rawInputToggle.Value = raw; sensitivityBindable.Disabled = !raw; }; - ignoredInputHandler.TriggerChange(); + ignoredInputHandlers.TriggerChange(); } } diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs new file mode 100644 index 0000000000..101d8f43f7 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections +{ + /// + /// A slider intended to show a "size" multiplier number, where 1x is 1.0. + /// + internal class SizeSlider : OsuSliderBar + { + public override string TooltipText => Current.Value.ToString(@"0.##x"); + } +} diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 3e7068f1ff..5898482e4a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -54,12 +54,6 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown = new SkinSettingsDropdown(), new ExportSkinButton(), new SettingsSlider - { - LabelText = "Menu cursor size", - Current = config.GetBindable(OsuSetting.MenuCursorSize), - KeyboardStep = 0.01f - }, - new SettingsSlider { LabelText = "Gameplay cursor size", Current = config.GetBindable(OsuSetting.GameplayCursorSize), @@ -136,11 +130,6 @@ namespace osu.Game.Overlays.Settings.Sections Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); } - private class SizeSlider : OsuSliderBar - { - public override string TooltipText => Current.Value.ToString(@"0.##x"); - } - private class SkinSettingsDropdown : SettingsDropdown { protected override OsuDropdown CreateDropdown() => new SkinDropdownControl(); diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs similarity index 75% rename from osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs rename to osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 38c30ddd64..19adfc5dd9 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -6,11 +6,11 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Overlays.Settings.Sections.Graphics +namespace osu.Game.Overlays.Settings.Sections.UserInterface { - public class UserInterfaceSettings : SettingsSubsection + public class GeneralSettings : SettingsSubsection { - protected override string Header => "User Interface"; + protected override string Header => "General"; [BackgroundDependencyLoader] private void load(OsuConfigManager config) @@ -22,6 +22,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Rotate cursor when dragging", Current = config.GetBindable(OsuSetting.CursorRotation) }, + new SettingsSlider + { + LabelText = "Menu cursor size", + Current = config.GetBindable(OsuSetting.MenuCursorSize), + KeyboardStep = 0.01f + }, new SettingsCheckbox { LabelText = "Parallax", diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs similarity index 97% rename from osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs rename to osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 7682967d10..598b666642 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -7,7 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; -namespace osu.Game.Overlays.Settings.Sections.Audio +namespace osu.Game.Overlays.Settings.Sections.UserInterface { public class MainMenuSettings : SettingsSubsection { diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs similarity index 97% rename from osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs rename to osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index b26876556e..c73a783d37 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Overlays.Settings.Sections.Gameplay +namespace osu.Game.Overlays.Settings.Sections.UserInterface { public class SongSelectSettings : SettingsSubsection { diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs new file mode 100644 index 0000000000..718fea5f2b --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Settings.Sections.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections +{ + public class UserInterfaceSection : SettingsSection + { + public override string Header => "User Interface"; + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Solid.LayerGroup + }; + + public UserInterfaceSection() + { + Children = new Drawable[] + { + new GeneralSettings(), + new MainMenuSettings(), + new SongSelectSettings() + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 97e4ba9da7..4143605c28 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -1,16 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; -using osuTK.Graphics; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using System.Collections.Generic; -using System.Linq; +using osuTK.Graphics; namespace osu.Game.Overlays.Settings { @@ -26,7 +25,7 @@ namespace osu.Game.Overlays.Settings public virtual IEnumerable FilterTerms => new[] { Header }; private const int header_size = 26; - private const int header_margin = 25; + private const int margin = 20; private const int border_size = 2; public bool MatchingFilter @@ -38,7 +37,7 @@ namespace osu.Game.Overlays.Settings protected SettingsSection() { - Margin = new MarginPadding { Top = 20 }; + Margin = new MarginPadding { Top = margin }; AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -46,10 +45,9 @@ namespace osu.Game.Overlays.Settings { Margin = new MarginPadding { - Top = header_size + header_margin + Top = header_size }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 30), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }; @@ -70,7 +68,7 @@ namespace osu.Game.Overlays.Settings { Padding = new MarginPadding { - Top = 20 + border_size, + Top = margin + border_size, Bottom = 10, }, RelativeSizeAxes = Axes.X, @@ -82,7 +80,11 @@ namespace osu.Game.Overlays.Settings Font = OsuFont.GetFont(size: header_size), Text = Header, Colour = colours.Yellow, - Margin = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS } + Margin = new MarginPadding + { + Left = SettingsPanel.CONTENT_MARGINS, + Right = SettingsPanel.CONTENT_MARGINS + } }, FlowContent } diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index b096c146a6..1b82d973e9 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings FlowContent = new FillFlowContainer { Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), + Spacing = new Vector2(0, 8), RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }; @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.Settings new OsuSpriteText { Text = Header.ToUpperInvariant(), - Margin = new MarginPadding { Bottom = 10, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, + Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, Font = OsuFont.GetFont(weight: FontWeight.Bold), }, FlowContent diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 031ecaae46..4ca6e2ec42 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -9,9 +9,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Settings { @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Settings { new Box { - Colour = Color4.Black, + Colour = OsuColour.Gray(0.02f), RelativeSizeAxes = Axes.Both, }, new SidebarScrollContainer diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index e1bcdbbaf0..7bd84dbc6c 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -23,10 +23,11 @@ namespace osu.Game.Overlays { new GeneralSection(), new GraphicsSection(), - new GameplaySection(), new AudioSection(), - new SkinSection(), new InputSection(createSubPanel(new KeyBindingPanel())), + new UserInterfaceSection(), + new GameplaySection(), + new SkinSection(), new OnlineSection(), new MaintenanceSection(), new DebugSection(), @@ -61,7 +62,6 @@ namespace osu.Game.Overlays switch (state.NewValue) { case Visibility.Visible: - Background.FadeTo(0.9f, 300, Easing.OutQuint); Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); SectionsContainer.FadeOut(300, Easing.OutQuint); @@ -69,7 +69,6 @@ namespace osu.Game.Overlays break; case Visibility.Hidden: - Background.FadeTo(0.6f, 500, Easing.OutQuint); Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint); SectionsContainer.FadeIn(500, Easing.OutQuint); diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 2948231c4b..7a5a586f67 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -72,8 +73,8 @@ namespace osu.Game.Overlays Origin = Anchor.TopRight, Scale = new Vector2(2, 1), // over-extend to the left for transitions RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, + Colour = OsuColour.Gray(0.05f), + Alpha = 1, }, SectionsContainer = new SettingsSectionsContainer { @@ -214,7 +215,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); // no null check because the usage of this class is strict - HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; + HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y; } } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b90aa6863a..35852f60ea 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -442,6 +442,9 @@ namespace osu.Game.Rulesets.Edit public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float DurationToDistance(double referenceTime, double duration); diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index cce631464f..4664f3808c 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -8,12 +8,22 @@ namespace osu.Game.Rulesets.Edit public interface IPositionSnapProvider { /// - /// Given a position, find a valid time snap. + /// Given a position, find a valid time and position snap. /// + /// + /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction. + /// /// The screen-space position to be snapped. /// The time and position post-snapping. SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + /// + /// Given a position, find a value position snap, restricting time to its input value. + /// + /// The screen-space position to be snapped. + /// The position post-snapping. Time will always be null. + SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition); + /// /// Retrieves the distance between two points within a timing point that are one beat length apart. /// diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index d986b71380..c0eb891f5e 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -85,10 +85,10 @@ namespace osu.Game.Rulesets.Edit } /// - /// Updates the position of this to a new screen-space position. + /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. - public virtual void UpdatePosition(SnapResult result) + public virtual void UpdateTimeAndPosition(SnapResult result) { if (!PlacementActive) HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current; diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 3063656aaf..da9bb8a09d 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -32,9 +32,6 @@ namespace osu.Game.Rulesets.Judgements private readonly Container aboveHitObjectsContent; - [Resolved] - private ISkinSource skinSource { get; set; } - /// /// Duration of initial fade in. /// @@ -78,29 +75,6 @@ namespace osu.Game.Rulesets.Judgements public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); - protected override void LoadComplete() - { - base.LoadComplete(); - skinSource.SourceChanged += onSkinChanged; - } - - private void onSkinChanged() - { - // on a skin change, the child component will update but not get correctly triggered to play its animation. - // we need to trigger a reinitialisation to make things right. - currentDrawableType = null; - - PrepareForUse(); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (skinSource != null) - skinSource.SourceChanged -= onSkinChanged; - } - /// /// Apply top-level animations to the current judgement when successfully hit. /// If displaying components which require lifetime extensions, manually adjusting is required. @@ -142,14 +116,18 @@ namespace osu.Game.Rulesets.Judgements Debug.Assert(Result != null); - prepareDrawables(); - runAnimation(); } private void runAnimation() { + // is a no-op if the drawables are already in a correct state. + prepareDrawables(); + + // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. + ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); + LifetimeStart = Result.TimeAbsolute; using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) @@ -200,7 +178,6 @@ namespace osu.Game.Rulesets.Judgements if (JudgementBody != null) RemoveInternal(JudgementBody); - aboveHitObjectsContent.Clear(); AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) { @@ -208,14 +185,29 @@ namespace osu.Game.Rulesets.Judgements Origin = Anchor.Centre, }); - if (JudgementBody.Drawable is IAnimatableJudgement animatable) + JudgementBody.OnSkinChanged += () => { - var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); - if (proxiedContent != null) - aboveHitObjectsContent.Add(proxiedContent); - } + // on a skin change, the child component will update but not get correctly triggered to play its animation (or proxy the newly created content). + // we need to trigger a reinitialisation to make things right. + proxyContent(); + runAnimation(); + }; + + proxyContent(); currentDrawableType = type; + + void proxyContent() + { + aboveHitObjectsContent.Clear(); + + if (JudgementBody.Drawable is IAnimatableJudgement animatable) + { + var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); + if (proxiedContent != null) + aboveHitObjectsContent.Add(proxiedContent); + } + } } protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index c6f3930029..1290e8136c 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -2,17 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using Humanizer; -using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToHealthProcessor + public abstract class ModEasy : Mod, IApplicableToDifficulty { public override string Name => "Easy"; public override string Acronym => "EZ"; @@ -22,49 +18,17 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - [SettingSource("Extra Lives", "Number of extra lives")] - public Bindable Retries { get; } = new BindableInt(2) - { - MinValue = 0, - MaxValue = 10 - }; - - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; - - private int retries; - - private BindableNumber health; - - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public virtual void ReadFromDifficulty(BeatmapDifficulty difficulty) { } - public void ApplyToDifficulty(BeatmapDifficulty difficulty) + public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 0.5f; difficulty.CircleSize *= ratio; difficulty.ApproachRate *= ratio; difficulty.DrainRate *= ratio; difficulty.OverallDifficulty *= ratio; - - retries = Retries.Value; - } - - public bool PerformFail() - { - if (retries == 0) return true; - - health.Value = health.MaxValue; - retries--; - - return false; - } - - public bool RestartOnFail => false; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - health = healthProcessor.Health.GetBoundCopy(); } } } diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs new file mode 100644 index 0000000000..2ac0f59d84 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Humanizer; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor + { + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + + private int retries; + + private BindableNumber health; + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + retries = Retries.Value; + } + + public bool PerformFail() + { + if (retries == 0) return true; + + health.Value = health.MaxValue; + retries--; + + return false; + } + + public bool RestartOnFail => false; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + health = healthProcessor.Health.GetBoundCopy(); + } + } +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ca49ed9e75..a922da0aa9 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public event Action OnRevertResult; + /// + /// Invoked when a new nested hit object is created by . + /// + internal event Action OnNestedDrawableCreated; + /// /// Whether a visual indicator should be displayed when a scoring result occurs. /// @@ -123,6 +128,12 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly Bindable state = new Bindable(); + /// + /// The state of this . + /// + /// + /// For pooled hitobjects, is recommended to be used instead for better editor/rewinding support. + /// public IBindable State => state; /// @@ -141,6 +152,11 @@ namespace osu.Game.Rulesets.Objects.Drawables private Container samplesContainer; + /// + /// Whether the initialization logic in has applied. + /// + internal bool IsInitialized; + /// /// Creates a new . /// @@ -174,7 +190,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true); updateState(ArmedState.Idle, true); } @@ -214,10 +230,15 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h) + var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h); + var drawableNested = pooledDrawableNested ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); + // Invoke the event only if this nested object is just created by `CreateNestedHitObject`. + if (pooledDrawableNested == null) + OnNestedDrawableCreated?.Invoke(drawableNested); + drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; @@ -239,12 +260,19 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; - OnApply(hitObject); + OnApply(); HitObjectApplied?.Invoke(this); - // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. + // If not loaded, the state update happens in LoadComplete(). if (IsLoaded) - Schedule(() => updateState(ArmedState.Idle, true)); + { + if (Result.IsHit) + updateState(ArmedState.Hit, true); + else if (Result.HasResult) + updateState(ArmedState.Miss, true); + else + updateState(ArmedState.Idle, true); + } hasHitObjectApplied = true; } @@ -284,7 +312,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied -= onDefaultsApplied; - OnFree(HitObject); + OnFree(); HitObject = null; Result = null; @@ -309,16 +337,14 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Invoked for this to take on any values from a newly-applied . /// - /// The being applied. - protected virtual void OnApply(HitObject hitObject) + protected virtual void OnApply() { } /// /// Invoked for this to revert any values previously taken on from the currently-applied . /// - /// The currently-applied . - protected virtual void OnFree(HitObject hitObject) + protected virtual void OnFree() { } @@ -439,6 +465,8 @@ namespace osu.Game.Rulesets.Objects.Drawables private void clearExistingStateTransforms() { base.ApplyTransformsAt(double.MinValue, true); + + // has to call this method directly (not ClearTransforms) to bypass the local ClearTransformsAfter override. base.ClearTransformsAfter(double.MinValue, true); } @@ -502,7 +530,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.SkinChanged(skin, allowFallback); - updateComboColour(); + UpdateComboColour(); ApplySkin(skin, allowFallback); @@ -510,13 +538,12 @@ namespace osu.Game.Rulesets.Objects.Drawables updateState(State.Value, true); } - private void updateComboColour() + protected void UpdateComboColour() { - if (!(HitObject is IHasComboInformation)) return; + if (!(HitObject is IHasComboInformation combo)) return; - var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; - - AccentColour.Value = GetComboColour(comboColours); + var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + AccentColour.Value = combo.GetComboColour(comboColours); } /// @@ -527,6 +554,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// This will only be called if the implements . /// /// A list of combo colours provided by the beatmap or skin. Can be null if not available. + [Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527 protected virtual Color4 GetComboColour(IReadOnlyList comboColours) { if (!(HitObject is IHasComboInformation combo)) @@ -682,6 +710,18 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateResult(false); } + /// + /// The maximum offset from the end time of at which this can be judged. + /// The time offset of will be clamped to this value during . + /// + /// Defaults to the miss window of . + /// + /// + /// + /// This does not affect the time offset provided to invocations of . + /// + protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0; + /// /// Applies the of this , notifying responders such as /// the of the . @@ -721,14 +761,7 @@ namespace osu.Game.Rulesets.Objects.Drawables $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])."); } - // Ensure that the judgement is given a valid time offset, because this may not get set by the caller - var endTime = HitObject.GetEndTime(); - - Result.TimeOffset = Time.Current - endTime; - - double missWindow = HitObject.HitWindows.WindowFor(HitResult.Miss); - if (missWindow > 0) - Result.TimeOffset = Math.Min(Result.TimeOffset, missWindow); + Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime()); if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); @@ -750,8 +783,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Judged) return false; - var endTime = HitObject.GetEndTime(); - CheckForResult(userTriggered, Time.Current - endTime); + CheckForResult(userTriggered, Time.Current - HitObject.GetEndTime()); return Judged; } diff --git a/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs b/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs deleted file mode 100644 index 8f4c95c634..0000000000 --- a/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; - -namespace osu.Game.Rulesets.Objects.Drawables -{ - public interface IDrawableHitObjectWithProxiedApproach - { - Drawable ProxiedLayer { get; } - } -} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 44b22033dc..72025de131 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -13,6 +13,7 @@ using JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; using osu.Game.Skinning; +using osu.Game.Utils; namespace osu.Game.Rulesets.Objects.Legacy { @@ -427,62 +428,25 @@ namespace osu.Game.Rulesets.Objects.Legacy // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario if (!string.IsNullOrEmpty(bankInfo.Filename)) { - return new List - { - new FileHitSampleInfo - { - Filename = bankInfo.Filename, - Volume = bankInfo.Volume - } - }; + return new List { new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume) }; } var soundTypes = new List { - new LegacyHitSampleInfo - { - Bank = bankInfo.Normal, - Name = HitSampleInfo.HIT_NORMAL, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank, + new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds - IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal) - } + type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)) }; if (type.HasFlag(LegacyHitSoundType.Finish)) - { - soundTypes.Add(new LegacyHitSampleInfo - { - Bank = bankInfo.Add, - Name = HitSampleInfo.HIT_FINISH, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank - }); - } + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); if (type.HasFlag(LegacyHitSoundType.Whistle)) - { - soundTypes.Add(new LegacyHitSampleInfo - { - Bank = bankInfo.Add, - Name = HitSampleInfo.HIT_WHISTLE, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank - }); - } + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); if (type.HasFlag(LegacyHitSoundType.Clap)) - { - soundTypes.Add(new LegacyHitSampleInfo - { - Bank = bankInfo.Add, - Name = HitSampleInfo.HIT_CLAP, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank - }); - } + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); return soundTypes; } @@ -500,21 +464,11 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } - public class LegacyHitSampleInfo : HitSampleInfo +#nullable enable + + public class LegacyHitSampleInfo : HitSampleInfo, IEquatable { - private int customSampleBank; - - public int CustomSampleBank - { - get => customSampleBank; - set - { - customSampleBank = value; - - if (value >= 2) - Suffix = value.ToString(); - } - } + public readonly int CustomSampleBank; /// /// Whether this hit sample is layered. @@ -523,18 +477,41 @@ namespace osu.Game.Rulesets.Objects.Legacy /// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled /// using the skin config option. /// - public bool IsLayered { get; set; } + public readonly bool IsLayered; + + public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false) + : base(name, bank, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume) + { + CustomSampleBank = customSampleBank; + IsLayered = isLayered; + } + + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => With(newName, newBank, newVolume); + + public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newCustomSampleBank = default, + Optional newIsLayered = default) + => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); + + public bool Equals(LegacyHitSampleInfo? other) + => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; + + public override bool Equals(object? obj) + => obj is LegacyHitSampleInfo other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } - private class FileHitSampleInfo : LegacyHitSampleInfo + private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable { - public string Filename; + public readonly string Filename; - public FileHitSampleInfo() - { - // Make sure that the LegacyBeatmapSkin does not fall back to the user skin. + public FileHitSampleInfo(string filename, int volume) + // Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin. // Note that this does not change the lookup names, as they are overridden locally. - CustomSampleBank = 1; + : base(string.Empty, customSampleBank: 1, volume: volume) + { + Filename = filename; } public override IEnumerable LookupNames => new[] @@ -542,6 +519,20 @@ namespace osu.Game.Rulesets.Objects.Legacy Filename, Path.ChangeExtension(Filename, null) }; + + public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newCustomSampleBank = default, + Optional newIsLayered = default) + => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); + + public bool Equals(FileHitSampleInfo? other) + => base.Equals(other) && Filename == other.Filename; + + public override bool Equals(object? obj) + => obj is FileHitSampleInfo other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename); } + +#nullable disable } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 211c077d4f..4f66802079 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -1,7 +1,10 @@ // 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 JetBrains.Annotations; using osu.Framework.Bindables; +using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Types { @@ -35,5 +38,13 @@ namespace osu.Game.Rulesets.Objects.Types /// Whether this is the last object in the current combo. /// bool LastInCombo { get; set; } + + /// + /// Retrieves the colour of the combo described by this object from a set of possible combo colours. + /// Defaults to using to decide the colour. + /// + /// A list of possible combo colours provided by the beatmap or skin. + /// The colour of the combo described by this object. + Color4 GetComboColour([NotNull] IReadOnlyList comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White; } } diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 5fbda305c8..ac5d281ddc 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); + OnAdd(drawable); HitObjectUsageBegan?.Invoke(entry.HitObject); } @@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.UI drawableMap.Remove(entry); + OnRemove(drawable); unbindStartTime(drawable); RemoveInternal(drawable); @@ -147,10 +149,12 @@ namespace osu.Game.Rulesets.UI hitObject.OnRevertResult += onRevertResult; AddInternal(hitObject); + OnAdd(hitObject); } public virtual bool Remove(DrawableHitObject hitObject) { + OnRemove(hitObject); if (!RemoveInternal(hitObject)) return false; @@ -178,6 +182,26 @@ namespace osu.Game.Rulesets.UI #endregion + /// + /// Invoked when a is added to this container. + /// + /// + /// This method is not invoked for nested s. + /// + protected virtual void OnAdd(DrawableHitObject drawableHitObject) + { + } + + /// + /// Invoked when a is removed from this container. + /// + /// + /// This method is not invoked for nested s. + /// + protected virtual void OnRemove(DrawableHitObject drawableHitObject) + { + } + public virtual void Clear(bool disposeChildren = true) { lifetimeManager.ClearEntries(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 82ec653f31..a2ac234471 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osuTK; +using System.Diagnostics; namespace osu.Game.Rulesets.UI { @@ -113,6 +114,16 @@ namespace osu.Game.Rulesets.UI } } + private void onNewDrawableHitObject(DrawableHitObject d) + { + d.OnNestedDrawableCreated += onNewDrawableHitObject; + + OnNewDrawableHitObject(d); + + Debug.Assert(!d.IsInitialized); + d.IsInitialized = true; + } + /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// @@ -124,6 +135,9 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { + if (!h.IsInitialized) + onNewDrawableHitObject(h); + HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); } @@ -157,6 +171,17 @@ namespace osu.Game.Rulesets.UI { } + /// + /// Invoked before a new is added to this . + /// It is invoked only once even if the drawable is pooled and used multiple times for different s. + /// + /// + /// This is also invoked for nested s. + /// + protected virtual void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// @@ -321,10 +346,12 @@ namespace osu.Game.Rulesets.UI { var dho = (DrawableHitObject)d; - // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. - // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (!dho.IsLoaded) + if (!dho.IsInitialized) { + onNewDrawableHitObject(dho); + + // If this is the first time this DHO is being used, then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. foreach (var m in mods.OfType()) m.ApplyToDrawableHitObjects(dho.Yield()); } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index bf64175468..3a5e3c098f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,13 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Layout; -using osu.Framework.Threading; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -19,7 +16,16 @@ namespace osu.Game.Rulesets.UI.Scrolling { private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); + + /// + /// Hit objects which require lifetime computation in the next update call. + /// + private readonly HashSet toComputeLifetime = new HashSet(); + + /// + /// A set containing all which have an up-to-date layout. + /// + private readonly HashSet layoutComputed = new HashSet(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -27,10 +33,6 @@ namespace osu.Game.Rulesets.UI.Scrolling // Responds to changes in the layout. When the layout changes, all hit object states must be recomputed. private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); - // A combined cache across all hit object states to reduce per-update iterations. - // When invalidated, one or more (but not necessarily all) hitobject states must be re-validated. - private readonly Cached combinedObjCache = new Cached(); - public ScrollingHitObjectContainer() { RelativeSizeAxes = Axes.Both; @@ -48,37 +50,12 @@ namespace osu.Game.Rulesets.UI.Scrolling timeRange.ValueChanged += _ => layoutCache.Invalidate(); } - public override void Add(DrawableHitObject hitObject) - { - combinedObjCache.Invalidate(); - hitObject.DefaultsApplied += onDefaultsApplied; - base.Add(hitObject); - } - - public override bool Remove(DrawableHitObject hitObject) - { - var result = base.Remove(hitObject); - - if (result) - { - combinedObjCache.Invalidate(); - hitObjectInitialStateCache.Remove(hitObject); - - hitObject.DefaultsApplied -= onDefaultsApplied; - } - - return result; - } - public override void Clear(bool disposeChildren = true) { - foreach (var h in Objects) - h.DefaultsApplied -= onDefaultsApplied; - base.Clear(disposeChildren); - combinedObjCache.Invalidate(); - hitObjectInitialStateCache.Clear(); + toComputeLifetime.Clear(); + layoutComputed.Clear(); } /// @@ -173,15 +150,40 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - private void onDefaultsApplied(DrawableHitObject drawableObject) + protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject); + + protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject); + + private void onAddRecursive(DrawableHitObject hitObject) { - // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). - // In such a case, combinedObjCache will take care of updating the hitobject. - if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) - { - combinedObjCache.Invalidate(); - state.Cache.Invalidate(); - } + invalidateHitObject(hitObject); + + hitObject.DefaultsApplied += invalidateHitObject; + + foreach (var nested in hitObject.NestedHitObjects) + onAddRecursive(nested); + } + + private void onRemoveRecursive(DrawableHitObject hitObject) + { + toComputeLifetime.Remove(hitObject); + layoutComputed.Remove(hitObject); + + hitObject.DefaultsApplied -= invalidateHitObject; + + foreach (var nested in hitObject.NestedHitObjects) + onRemoveRecursive(nested); + } + + /// + /// Make this lifetime and layout computed in next update. + /// + private void invalidateHitObject(DrawableHitObject hitObject) + { + // Lifetime computation is delayed until next update because + // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. + toComputeLifetime.Add(hitObject); + layoutComputed.Remove(hitObject); } private float scrollLength; @@ -192,17 +194,18 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var state in hitObjectInitialStateCache.Values) - state.Cache.Invalidate(); - combinedObjCache.Invalidate(); + toComputeLifetime.Clear(); + + foreach (var hitObject in Objects) + { + if (hitObject.HitObject != null) + toComputeLifetime.Add(hitObject); + } + + layoutComputed.Clear(); scrollingInfo.Algorithm.Reset(); - layoutCache.Validate(); - } - - if (!combinedObjCache.IsValid) - { switch (direction.Value) { case ScrollingDirection.Up: @@ -215,32 +218,24 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - foreach (var obj in Objects) - { - if (!hitObjectInitialStateCache.TryGetValue(obj, out var state)) - state = hitObjectInitialStateCache[obj] = new InitialState(new Cached()); - - if (state.Cache.IsValid) - continue; - - state.ScheduledComputation?.Cancel(); - state.ScheduledComputation = computeInitialStateRecursive(obj); - - computeLifetimeStartRecursive(obj); - - state.Cache.Validate(); - } - - combinedObjCache.Validate(); + layoutCache.Validate(); } - } - private void computeLifetimeStartRecursive(DrawableHitObject hitObject) - { - hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); + foreach (var hitObject in toComputeLifetime) + hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); - foreach (var obj in hitObject.NestedHitObjects) - computeLifetimeStartRecursive(obj); + toComputeLifetime.Clear(); + + // only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes). + foreach (var obj in AliveObjects) + { + if (layoutComputed.Contains(obj)) + continue; + + updateLayoutRecursive(obj); + + layoutComputed.Add(obj); + } } private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) @@ -271,7 +266,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } - private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => + private void updateLayoutRecursive(DrawableHitObject hitObject) { if (hitObject.HitObject is IHasDuration e) { @@ -291,12 +286,12 @@ namespace osu.Game.Rulesets.UI.Scrolling foreach (var obj in hitObject.NestedHitObjects) { - computeInitialStateRecursive(obj); + updateLayoutRecursive(obj); // Nested hitobjects don't need to scroll, but they do need accurate positions updatePosition(obj, hitObject.HitObject.StartTime); } - }); + } protected override void UpdateAfterChildrenLife() { @@ -328,19 +323,5 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } } - - private class InitialState - { - [NotNull] - public readonly Cached Cache; - - [CanBeNull] - public ScheduledDelegate ScheduledComputation; - - public InitialState(Cached cache) - { - Cache = cache; - } - } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 9dac3f4de1..2b75f93f9e 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.UI.Scrolling { protected readonly IBindable Direction = new Bindable(); + public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer; + [Resolved] protected IScrollingInfo ScrollingInfo { get; private set; } @@ -27,14 +29,12 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Given a position in screen space, return the time within this column. /// - public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => - ((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition); + public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => HitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition); /// /// Given a time, return the screen space position within this column. /// - public virtual Vector2 ScreenSpacePositionAtTime(double time) - => ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time); + public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time); protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index df9cadebfc..0b45bd5597 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private IEditorChangeHandler changeHandler { get; set; } [Resolved] - private EditorClock editorClock { get; set; } + protected EditorClock EditorClock { get; private set; } [Resolved] protected EditorBeatmap Beatmap { get; private set; } @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) return false; - editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); + EditorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); return true; } @@ -187,7 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return false; - if (movementBlueprint != null) + if (movementBlueprints != null) { isDraggingBlueprint = true; changeHandler?.BeginChange(); @@ -299,7 +299,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBlueprints.Remove(blueprint); - if (movementBlueprint == blueprint) + if (movementBlueprints?.Contains(blueprint) == true) finishSelectionMovement(); OnBlueprintRemoved(hitObject); @@ -381,7 +381,7 @@ namespace osu.Game.Screens.Edit.Compose.Components case SelectionState.Selected: // if the editor is playing, we generally don't want to deselect objects even if outside the selection area. - if (!editorClock.IsRunning && !isValidForSelection()) + if (!EditorClock.IsRunning && !isValidForSelection()) blueprint.Deselect(); break; } @@ -424,8 +424,8 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2? movementBlueprintOriginalPosition; - private SelectionBlueprint movementBlueprint; + private Vector2[] movementBlueprintOriginalPositions; + private SelectionBlueprint[] movementBlueprints; private bool isDraggingBlueprint; /// @@ -442,8 +442,8 @@ namespace osu.Game.Screens.Edit.Compose.Components return; // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject - movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); - movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct + movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray(); + movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); } /// @@ -453,36 +453,50 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a movement was active. private bool moveCurrentSelection(DragEvent e) { - if (movementBlueprint == null) + if (movementBlueprints == null) return false; if (snapProvider == null) return true; - Debug.Assert(movementBlueprintOriginalPosition != null); + Debug.Assert(movementBlueprintOriginalPositions != null); - HitObject draggedObject = movementBlueprint.HitObject; + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // check for positional snap for every object in selection (for things like object-object snapping) + for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++) + { + var testPosition = movementBlueprintOriginalPositions[i] + distanceTravelled; + + var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition); + + if (positionalResult.ScreenSpacePosition == testPosition) continue; + + // attempt to move the objects, and abort any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition))) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // hitobject in the selection. // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; // Retrieve a snapped position. var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. - if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) + if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition))) return true; if (result.Time.HasValue) { // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - draggedObject.StartTime; + double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; foreach (HitObject obj in Beatmap.SelectedHitObjects) - { obj.StartTime += offset; - Beatmap.Update(obj); - } } return true; @@ -494,11 +508,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a movement was active. private bool finishSelectionMovement() { - if (movementBlueprint == null) + if (movementBlueprints == null) return false; - movementBlueprintOriginalPosition = null; - movementBlueprint = null; + movementBlueprintOriginalPositions = null; + movementBlueprints = null; return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0d2e2360b1..c09b935f28 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Compose.Components case TernaryState.True: if (existingSample == null) - samples.Add(new HitSampleInfo { Name = sampleName }); + samples.Add(new HitSampleInfo(sampleName)); break; } } @@ -157,7 +157,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); - currentPlacement.UpdatePosition(snapResult); + // if no time was found from positional snapping, we should still quantize to the beat. + snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); + + currentPlacement.UpdateTimeAndPosition(snapResult); } #endregion @@ -209,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. - blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); + blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); placementBlueprintContainer.Child = currentPlacement = blueprint; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index adf22a3370..788b485449 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -328,7 +328,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (h.Samples.Any(s => s.Name == sampleName)) continue; - h.Samples.Add(new HitSampleInfo { Name = sampleName }); + h.Samples.Add(new HitSampleInfo(sampleName)); } EditorBeatmap.EndChange(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index f6675902fc..20836c0e68 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -224,6 +224,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => track.Length / Zoom; + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 975433d407..657c5834b2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,8 +18,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -28,32 +27,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineHitObjectBlueprint : SelectionBlueprint { - private readonly Circle circle; + private const float thickness = 5; + private const float shadow_radius = 5; + private const float circle_size = 24; + + public Action OnDragHandled; [UsedImplicitly] private readonly Bindable startTime; - public Action OnDragHandled; + private Bindable indexInCurrentComboBindable; + private Bindable comboIndexBindable; + private readonly Circle circle; private readonly DragBar dragBar; - private readonly List shadowComponents = new List(); - - private DrawableHitObject drawableHitObject; - - private Bindable comboColour; - private readonly Container mainComponents; - private readonly OsuSpriteText comboIndexText; - private Bindable comboIndex; - - private const float thickness = 5; - - private const float shadow_radius = 5; - - private const float circle_size = 24; + [Resolved] + private ISkinSource skin { get; set; } public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) @@ -152,46 +145,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateShadows(); } - [BackgroundDependencyLoader(true)] - private void load(HitObjectComposer composer) - { - if (composer != null) - { - // best effort to get the drawable representation for grabbing colour and what not. - drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject); - } - } - protected override void LoadComplete() { base.LoadComplete(); if (HitObject is IHasComboInformation comboInfo) { - comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); - comboIndex.BindValueChanged(combo => - { - comboIndexText.Text = (combo.NewValue + 1).ToString(); - }, true); + indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); + indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); + + comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); + comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + + skin.SourceChanged += updateComboColour; } + } - if (drawableHitObject != null) - { - comboColour = drawableHitObject.AccentColour.GetBoundCopy(); - comboColour.BindValueChanged(colour => - { - if (HitObject is IHasDuration) - mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); - else - mainComponents.Colour = drawableHitObject.AccentColour.Value; + private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); - var col = mainComponents.Colour.TopLeft.Linear; - float brightness = col.R + col.G + col.B; + private void updateComboColour() + { + if (!(HitObject is IHasComboInformation combo)) + return; - // decide the combo index colour based on brightness? - comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; - }, true); - } + var comboColours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + var comboColour = combo.GetComboColour(comboColours); + + if (HitObject is IHasDuration) + mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White); + else + mainComponents.Colour = comboColour; + + var col = mainComponents.Colour.TopLeft.Linear; + float brightness = col.R + col.G + col.B; + + // decide the combo index colour based on brightness? + comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; } protected override void Update() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 724256af8b..fb11b859a7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -25,6 +25,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private BindableBeatDivisor beatDivisor { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -38,7 +41,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load() { - beatDivisor.BindValueChanged(_ => tickCache.Invalidate()); + beatDivisor.BindValueChanged(_ => invalidateTicks()); + + if (changeHandler != null) + // currently this is the best way to handle any kind of timing changes. + changeHandler.OnStateChange += invalidateTicks; + } + + private void invalidateTicks() + { + tickCache.Invalidate(); } /// @@ -165,5 +177,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return point; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (changeHandler != null) + changeHandler.OnStateChange -= invalidateTicks; + } } } diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 46d5eb40b4..c297a03dbf 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -43,6 +44,21 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); + return wrapSkinnableContent(composer); + } + + protected override Drawable CreateTimelineContent() + { + if (ruleset == null || composer == null) + return base.CreateTimelineContent(); + + return wrapSkinnableContent(new TimelineBlueprintContainer(composer)); + } + + private Drawable wrapSkinnableContent(Drawable content) + { + Debug.Assert(ruleset != null); + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation @@ -51,9 +67,7 @@ namespace osu.Game.Screens.Edit.Compose // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. - return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); + return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content)); } - - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 85467d3bbb..ca7e5fbf20 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -106,6 +106,12 @@ namespace osu.Game.Screens.Edit [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host, OsuConfigManager config) { + if (Beatmap.Value is DummyWorkingBeatmap) + { + isNewBeatmap = true; + Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + } + beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); @@ -122,12 +128,6 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - if (Beatmap.Value is DummyWorkingBeatmap) - { - isNewBeatmap = true; - Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); - } - try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); @@ -375,6 +375,9 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(ScrollEvent e) { + if (e.ControlPressed || e.AltPressed || e.SuperPressed) + return false; + const double precision = 1; double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y; diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 62187aed24..2dcb416a03 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit var newState = stream.ToArray(); // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. - if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; + if (savedStates.Count > 0 && newState.SequenceEqual(savedStates[currentState])) return; if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index aa1d57db31..897ddc6955 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -37,8 +37,8 @@ namespace osu.Game.Screens.Edit.Setup Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, - MinValue = 2, - MaxValue = 7, + MinValue = 0, + MaxValue = 10, Precision = 0.1f, } }, diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index b802b3405a..6e2737256a 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -3,10 +3,12 @@ using System; using System.IO; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -21,6 +23,9 @@ namespace osu.Game.Screens.Edit.Setup private readonly IBindable currentFile = new Bindable(); + [Resolved] + private SectionsContainer sectionsContainer { get; set; } + public FileChooserLabelledTextBox() { currentFile.BindValueChanged(onFileSelected); @@ -47,14 +52,16 @@ namespace osu.Game.Screens.Edit.Setup public void DisplayFileChooser() { - Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) + FileSelector fileSelector; + + Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) { RelativeSizeAxes = Axes.X, Height = 400, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, CurrentFile = { BindTarget = currentFile } }; + + sectionsContainer.ScrollTo(fileSelector); } internal class FileChooserOsuTextBox : OsuTextBox diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index e81646456f..abb83f894a 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -113,8 +113,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Alpha = 0.5f, - AccentColour = Color4.DarkBlue, + Colour = Color4.DarkBlue, Size = new Vector2(0.96f) }, new Circle diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index ebbb19636c..01b2a98c6e 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; -using osu.Game.Graphics; using System; using System.Collections.Generic; using JetBrains.Annotations; @@ -20,13 +19,14 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Screens.Menu { /// /// A visualiser that reacts to music coming from beatmaps. /// - public class LogoVisualisation : Drawable, IHasAccentColour + public class LogoVisualisation : Drawable { private readonly IBindable beatmap = new Bindable(); @@ -67,8 +67,6 @@ namespace osu.Game.Screens.Menu private int indexOffset; - public Color4 AccentColour { get; set; } - /// /// The relative movement of bars based on input amplification. Defaults to 1. /// @@ -176,7 +174,8 @@ namespace osu.Game.Screens.Menu // Assuming the logo is a circle, we don't need a second dimension. private float size; - private Color4 colour; + private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f); + private float[] audioData; private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); @@ -193,7 +192,6 @@ namespace osu.Game.Screens.Menu shader = Source.shader; texture = Source.texture; size = Source.DrawSize.X; - colour = Source.AccentColour; audioData = Source.frequencyAmplitudes; } @@ -206,7 +204,7 @@ namespace osu.Game.Screens.Menu Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; ColourInfo colourInfo = DrawColourInfo.Colour; - colourInfo.ApplyChild(colour); + colourInfo.ApplyChild(transparent_white); if (audioData != null) { diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 5eb3f1efa0..92add458f9 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -7,7 +7,6 @@ using osu.Game.Online.API; using osu.Game.Users; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Screens.Menu { @@ -28,12 +27,10 @@ namespace osu.Game.Screens.Menu private void updateColour() { - Color4 defaultColour = Color4.White.Opacity(0.2f); - if (user.Value?.IsSupporter ?? false) - AccentColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? defaultColour; + Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else - AccentColour = defaultColour; + Colour = Color4.White; } } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 4515ee8ed0..68d23e1a32 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -81,6 +81,8 @@ namespace osu.Game.Screens.Menu set => rippleContainer.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } + private const float visualizer_default_alpha = 0.5f; + private readonly Box flashLayer; private readonly Container impactContainer; @@ -144,7 +146,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - Alpha = 0.5f, + Alpha = visualizer_default_alpha, Size = new Vector2(0.96f) }, new Container @@ -282,8 +284,7 @@ namespace osu.Game.Screens.Menu this.Delay(early_activation).Schedule(() => sampleBeat.Play()); logoBeatContainer - .ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out) - .Then() + .ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then() .ScaleTo(1, beatLength * 2, Easing.OutQuint); ripple.ClearTransforms(); @@ -296,15 +297,13 @@ namespace osu.Game.Screens.Menu { flashLayer.ClearTransforms(); flashLayer - .FadeTo(0.2f * amplitudeAdjust, early_activation, Easing.Out) - .Then() + .FadeTo(0.2f * amplitudeAdjust, early_activation, Easing.Out).Then() .FadeOut(beatLength); visualizer.ClearTransforms(); visualizer - .FadeTo(0.9f * amplitudeAdjust, early_activation, Easing.Out) - .Then() - .FadeTo(0.5f, beatLength); + .FadeTo(visualizer_default_alpha * 1.8f * amplitudeAdjust, early_activation, Easing.Out).Then() + .FadeTo(visualizer_default_alpha, beatLength); } } diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 04da943a10..0efa9c5196 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Multi.Play protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(roomId.Value != null); - return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem); + return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); } protected override ScoreInfo CreateScore() diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 8da6a530a8..3623208fa7 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Ranking [Resolved] private IAPIProvider api { get; set; } - public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) + public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry) : base(score, allowRetry) { this.roomId = roomId; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index e83dded075..50195d571c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -9,7 +9,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -19,7 +18,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; using osuTK; -using osuTK.Input; namespace osu.Game.Screens.Play { @@ -181,7 +179,7 @@ namespace osu.Game.Screens.Play notificationOverlay?.Post(new SimpleNotification { - Text = @"The score overlay is currently disabled. You can toggle this by pressing Shift+Tab." + Text = $"The score overlay is currently disabled. You can toggle this by pressing {config.LookupKeyBindings(GlobalAction.ToggleInGameInterface)}." }); } @@ -273,37 +271,6 @@ namespace osu.Game.Screens.Play Progress.BindDrawableRuleset(drawableRuleset); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) return false; - - if (e.ShiftPressed) - { - switch (e.Key) - { - case Key.Tab: - switch (configVisibilityMode.Value) - { - case HUDVisibilityMode.Never: - configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay; - break; - - case HUDVisibilityMode.HideDuringGameplay: - configVisibilityMode.Value = HUDVisibilityMode.Always; - break; - - case HUDVisibilityMode.Always: - configVisibilityMode.Value = HUDVisibilityMode.Never; - break; - } - - return true; - } - } - - return base.OnKeyDown(e); - } - protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); @@ -377,6 +344,24 @@ namespace osu.Game.Screens.Play holdingForHUD = true; updateVisibility(); return true; + + case GlobalAction.ToggleInGameInterface: + switch (configVisibilityMode.Value) + { + case HUDVisibilityMode.Never: + configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay; + break; + + case HUDVisibilityMode.HideDuringGameplay: + configVisibilityMode.Value = HUDVisibilityMode.Always; + break; + + case HUDVisibilityMode.Always: + configVisibilityMode.Value = HUDVisibilityMode.Never; + break; + } + + return true; } return false; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b94f0a5062..7979b635aa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -339,7 +339,11 @@ namespace osu.Game.Screens.Play AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, - RequestSeek = GameplayClockContainer.Seek, + RequestSeek = time => + { + GameplayClockContainer.Seek(time); + GameplayClockContainer.Start(); + }, Anchor = Anchor.Centre, Origin = Anchor.Centre }, @@ -545,7 +549,7 @@ namespace osu.Game.Screens.Play protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score); + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); #region Fail Logic diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 3a4298f22d..294d116f51 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,12 +1,14 @@ // 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.Input.Bindings; +using osu.Game.Input.Bindings; using osu.Game.Scoring; using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { - public class ReplayPlayer : Player + public class ReplayPlayer : Player, IKeyBindingHandler { protected readonly Score Score; @@ -35,5 +37,24 @@ namespace osu.Game.Screens.Play return Score.ScoreInfo; } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.TogglePauseReplay: + if (GameplayClockContainer.IsPaused.Value) + GameplayClockContainer.Start(); + else + GameplayClockContainer.Stop(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index b123757ded..92b304de91 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -133,6 +133,9 @@ namespace osu.Game.Screens.Play switch (action) { case GlobalAction.SkipCutscene: + if (!button.Enabled.Value) + return false; + button.Click(); return true; } diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs index 56ccfd2253..dabdf0a139 100644 --- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play public class SpectatorResultsScreen : SoloResultsScreen { public SpectatorResultsScreen(ScoreInfo score) - : base(score) + : base(score, false) { } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index ce3e618889..887e7ec8a9 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking private readonly bool allowRetry; - protected ResultsScreen(ScoreInfo score, bool allowRetry = true) + protected ResultsScreen(ScoreInfo score, bool allowRetry) { Score = score; this.allowRetry = allowRetry; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9cf2e6757a..76b549da1a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking [Resolved] private RulesetStore rulesets { get; set; } - public SoloResultsScreen(ScoreInfo score, bool allowRetry = true) + public SoloResultsScreen(ScoreInfo score, bool allowRetry) : base(score, allowRetry) { } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 83631fd383..4ce87927a1 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select /// public bool BeatmapSetsLoaded { get; private set; } - private readonly CarouselScrollContainer scroll; + protected readonly CarouselScrollContainer Scroll; private IEnumerable beatmapSets => root.Children.OfType(); @@ -112,9 +112,9 @@ namespace osu.Game.Screens.Select if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; - ScrollableContent.Clear(false); + Scroll.Clear(false); itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); + ScrollToSelected(); // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). FlushPendingFilterOperations(); @@ -130,9 +130,7 @@ namespace osu.Game.Screens.Select private readonly List visibleItems = new List(); private readonly Cached itemsCache = new Cached(); - private readonly Cached scrollPositionCache = new Cached(); - - protected readonly Container ScrollableContent; + private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None; public Bindable RightClickScrollingEnabled = new Bindable(); @@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = scroll = new CarouselScrollContainer + Children = new Drawable[] { - Masking = false, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + setPool, + Scroll = new CarouselScrollContainer { - setPool, - ScrollableContent = new Container - { - RelativeSizeAxes = Axes.X, - } + RelativeSizeAxes = Axes.Both, } } }; @@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); - RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; + RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); itemUpdated = beatmaps.ItemUpdated.GetBoundCopy(); @@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom; + private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom; /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => scroll.Current - BleedTop; + private float visibleUpperBound => Scroll.Current - BleedTop; public void FlushPendingFilterOperations() { @@ -468,8 +461,8 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (alwaysResetScrollPosition || !scroll.UserScrolling) - ScrollToSelected(); + if (alwaysResetScrollPosition || !Scroll.UserScrolling) + ScrollToSelected(true); } } @@ -478,7 +471,12 @@ namespace osu.Game.Screens.Select /// /// Scroll to the current . /// - public void ScrollToSelected() => scrollPositionCache.Invalidate(); + /// + /// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels. + /// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation. + /// + public void ScrollToSelected(bool immediate = false) => + pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard; #region Key / button selection logic @@ -488,12 +486,12 @@ namespace osu.Game.Screens.Select { case Key.Left: if (!e.Repeat) - beginRepeatSelection(() => SelectNext(-1, true), e.Key); + beginRepeatSelection(() => SelectNext(-1), e.Key); return true; case Key.Right: if (!e.Repeat) - beginRepeatSelection(() => SelectNext(1, true), e.Key); + beginRepeatSelection(() => SelectNext(), e.Key); return true; } @@ -580,6 +578,11 @@ namespace osu.Game.Screens.Select if (revalidateItems) updateYPositions(); + // if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels. + // this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad. + if (pendingScrollOperation != PendingScrollOperation.None) + updateScrollPosition(); + // This data is consumed to find the currently displayable range. // This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn. var newDisplayRange = getDisplayRange(); @@ -594,7 +597,7 @@ namespace osu.Game.Screens.Select { var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); - foreach (var panel in ScrollableContent.Children) + foreach (var panel in Scroll.Children) { if (toDisplay.Remove(panel.Item)) { @@ -620,24 +623,14 @@ namespace osu.Game.Screens.Select panel.Depth = item.CarouselYPosition; panel.Y = item.CarouselYPosition; - ScrollableContent.Add(panel); + Scroll.Add(panel); } } } - // Finally, if the filtered items have changed, animate drawables to their new locations. - // This is common if a selected/collapsed state has changed. - if (revalidateItems) - { - foreach (DrawableCarouselItem panel in ScrollableContent.Children) - { - panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint); - } - } - // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in ScrollableContent.Children) + foreach (DrawableCarouselItem item in Scroll.Children) { updateItem(item); @@ -670,14 +663,6 @@ namespace osu.Game.Screens.Select return (firstIndex, lastIndex); } - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (!scrollPositionCache.IsValid) - updateScrollPosition(); - } - private void beatmapRemoved(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) @@ -789,7 +774,8 @@ namespace osu.Game.Screens.Select } currentY += visibleHalfHeight; - ScrollableContent.Height = currentY; + + Scroll.ScrollContent.Height = currentY; if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) { @@ -809,12 +795,31 @@ namespace osu.Game.Screens.Select if (firstScroll) { // reduce movement when first displaying the carousel. - scroll.ScrollTo(scrollTarget.Value - 200, false); + Scroll.ScrollTo(scrollTarget.Value - 200, false); firstScroll = false; } - scroll.ScrollTo(scrollTarget.Value); - scrollPositionCache.Validate(); + switch (pendingScrollOperation) + { + case PendingScrollOperation.Standard: + Scroll.ScrollTo(scrollTarget.Value); + break; + + case PendingScrollOperation.Immediate: + // in order to simplify animation logic, rather than using the animated version of ScrollTo, + // we take the difference in scroll height and apply to all visible panels. + // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer + // to enter clamp-special-case mode where it animates completely differently to normal. + float scrollChange = scrollTarget.Value - Scroll.Current; + + Scroll.ScrollTo(scrollTarget.Value, false); + + foreach (var i in Scroll.Children) + i.Y += scrollChange; + break; + } + + pendingScrollOperation = PendingScrollOperation.None; } } @@ -844,7 +849,7 @@ namespace osu.Game.Screens.Select /// For nested items, the parent of the item to be updated. private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) { - Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); float itemDrawY = posInScroll.Y - visibleUpperBound; float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); @@ -858,6 +863,13 @@ namespace osu.Game.Screens.Select item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } + private enum PendingScrollOperation + { + None, + Standard, + Immediate, + } + /// /// A carousel item strictly used for binary search purposes. /// @@ -889,7 +901,7 @@ namespace osu.Game.Screens.Select } } - private class CarouselScrollContainer : OsuScrollContainer + protected class CarouselScrollContainer : OsuScrollContainer { private bool rightMouseScrollBlocked; @@ -898,6 +910,12 @@ namespace osu.Game.Screens.Select /// public bool UserScrolling { get; private set; } + public CarouselScrollContainer() + { + // size is determined by the carousel itself, due to not all content necessarily being loaded. + ScrollContent.AutoSizeAxes = Axes.None; + } + // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 93f95e76cc..b3c5d458d6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Graphics.UserInterface; @@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; } + protected override void Update() + { + base.Update(); + + // position updates should not occur if the item is filtered away. + // this avoids panels flying across the screen only to be eventually off-screen or faded out. + if (!Item.Visible) + return; + + float targetY = Item.CarouselYPosition; + + if (Precision.AlmostEquals(targetY, Y)) + Y = targetY; + else + // algorithm for this is taken from ScrollContainer. + // while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct. + Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed)); + } + protected override void UpdateItem() { base.UpdateItem(); @@ -80,8 +100,14 @@ namespace osu.Game.Screens.Select.Carousel background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, - }, 300), - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100), + }, 300) + { + RelativeSizeAxes = Axes.Both + }, + mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100) + { + RelativeSizeAxes = Axes.Both + }, }; background.DelayedLoadComplete += fadeContentIn; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index ee8825640c..50a61ed4c2 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score))); + FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a5d98ab49c..2847b31e98 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -373,7 +373,7 @@ namespace osu.Game.Screens.Select if (selectionChangedDebounce?.Completed == false) { selectionChangedDebounce.RunTask(); - selectionChangedDebounce.Cancel(); // cancel the already scheduled task. + selectionChangedDebounce?.Cancel(); // cancel the already scheduled task. selectionChangedDebounce = null; } @@ -462,19 +462,30 @@ namespace osu.Game.Screens.Select void run() { + // clear pending task immediately to track any potential nested debounce operation. + selectionChangedDebounce = null; + Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}"); if (transferRulesetValue()) { Mods.Value = Array.Empty(); - // transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it. + // transferRulesetValue() may trigger a re-filter. If the current selection does not match the new ruleset, we want to switch away from it. // The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here. // We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert). if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false)) beatmap = null; } + if (selectionChangedDebounce != null) + { + // a new nested operation was started; switch to it for further selection. + // this avoids having two separate debounces trigger from the same source. + selectionChangedDebounce.RunTask(); + return; + } + // We may be arriving here due to another component changing the bindable Beatmap. // In these cases, the other component has already loaded the beatmap, so we don't need to do so again. if (!EqualityComparer.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo)) diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index 7e52bb8176..f627379a57 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Timing; @@ -25,6 +26,6 @@ namespace osu.Game.Skinning /// /// The time which animations should be started from, relative to . /// - double AnimationStartTime { get; } + Bindable AnimationStartTime { get; } } } diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 0ee02a2442..a7c084998d 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.OpenGL.Textures; @@ -70,6 +71,8 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private IAnimationTimeReference timeReference { get; set; } + private readonly Bindable animationStartTime = new BindableDouble(); + public SkinnableTextureAnimation(bool startAtCurrentTime = true) : base(startAtCurrentTime) { @@ -82,8 +85,18 @@ namespace osu.Game.Skinning if (timeReference != null) { Clock = timeReference.Clock; - PlaybackPosition = timeReference.Clock.CurrentTime - timeReference.AnimationStartTime; + animationStartTime.BindTo(timeReference.AnimationStartTime); } + + animationStartTime.BindValueChanged(_ => updatePlaybackPosition(), true); + } + + private void updatePlaybackPosition() + { + if (timeReference == null) + return; + + PlaybackPosition = timeReference.Clock.CurrentTime - timeReference.AnimationStartTime.Value; } } diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs new file mode 100644 index 0000000000..054f72400e --- /dev/null +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.IO.Stores; +using osu.Game.Rulesets; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public abstract class LegacySkinPlayerTestScene : PlayerTestScene + { + private ISkinSource legacySkinSource; + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuGameBase game) + { + var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); + legacySkinSource = new SkinProvidingContainer(legacySkin); + } + + public class SkinProvidingPlayer : TestPlayer + { + [Cached(typeof(ISkinSource))] + private readonly ISkinSource skinSource; + + public SkinProvidingPlayer(ISkinSource skinSource) + { + this.skinSource = skinSource; + } + } + } +} diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index c3d74f21aa..78a6bcc3db 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - currentBlueprint.UpdatePosition(SnapForBlueprint(currentBlueprint)); + currentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(currentBlueprint)); } protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual if (drawable is PlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdatePosition(SnapForBlueprint(blueprint)); + blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); } } diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs new file mode 100644 index 0000000000..9f8a1c2e62 --- /dev/null +++ b/osu.Game/Utils/Optional.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +namespace osu.Game.Utils +{ + /// + /// A wrapper over a value and a boolean denoting whether the value is valid. + /// + /// The type of value stored. + public readonly ref struct Optional + { + /// + /// The stored value. + /// + public readonly T Value; + + /// + /// Whether is valid. + /// + /// + /// If is a reference type, null may be valid for . + /// + public readonly bool HasValue; + + private Optional(T value) + { + Value = value; + HasValue = true; + } + + /// + /// Returns if it's valid, or a given fallback value otherwise. + /// + /// + /// Shortcase for: optional.HasValue ? optional.Value : fallback. + /// + /// The fallback value to return if is false. + /// + public T GetOr(T fallback) => HasValue ? Value : fallback; + + public static implicit operator Optional(T value) => new Optional(value); + } +} diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs new file mode 100644 index 0000000000..118b08fe30 --- /dev/null +++ b/osu.Game/Utils/StatelessRNG.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Utils +{ + /// + /// Provides a fast stateless function that can be used in randomly-looking visual elements. + /// + public static class StatelessRNG + { + private static ulong mix(ulong x) + { + unchecked + { + x ^= x >> 33; + x *= 0xff51afd7ed558ccd; + x ^= x >> 33; + x *= 0xc4ceb9fe1a85ec53; + x ^= x >> 33; + return x; + } + } + + /// + /// Generate a random 64-bit unsigned integer from given seed. + /// + /// + /// The seed value of this random number generator. + /// + /// + /// The series number. + /// Different values are computed for the same seed in different series. + /// + public static ulong NextULong(int seed, int series = 0) + { + unchecked + { + var combined = ((ulong)(uint)series << 32) | (uint)seed; + // The xor operation is to not map (0, 0) to 0. + return mix(combined ^ 0x12345678); + } + } + + /// + /// Generate a random integer in range [0, maxValue) from given seed. + /// + /// + /// The number of possible results. + /// + /// + /// The seed value of this random number generator. + /// + /// + /// The series number. + /// Different values are computed for the same seed in different series. + /// + public static int NextInt(int maxValue, int seed, int series = 0) + { + if (maxValue <= 0) throw new ArgumentOutOfRangeException(nameof(maxValue)); + + return (int)(NextULong(seed, series) % (ulong)maxValue); + } + + /// + /// Compute a random floating point value between 0 and 1 (excluding 1) from given seed and series number. + /// + /// + /// The seed value of this random number generator. + /// + /// + /// The series number. + /// Different values are computed for the same seed in different series. + /// + public static float NextSingle(int seed, int series = 0) => + (float)(NextULong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 54f3fcede6..4b931726e0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - + @@ -26,11 +26,11 @@ - - - + + + - + diff --git a/osu.iOS.props b/osu.iOS.props index 692dac909a..3a47b77820 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -88,11 +88,11 @@ - + - +