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 @@
-
+
-
+