diff --git a/osu.Android.props b/osu.Android.props
index 196d122a2a..32e236ccd5 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 21d6336b2c..050bf2b787 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -7,6 +7,8 @@ using Android.OS;
using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
+using osu.Game.Utils;
+using Xamarin.Essentials;
namespace osu.Android
{
@@ -72,5 +74,14 @@ namespace osu.Android
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
+
+ protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
+
+ private class AndroidBatteryInfo : BatteryInfo
+ {
+ public override double ChargeLevel => Battery.ChargeLevel;
+
+ public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
+ }
}
}
diff --git a/osu.Android/Properties/AndroidManifest.xml b/osu.Android/Properties/AndroidManifest.xml
index 770eaf2222..e717bab310 100644
--- a/osu.Android/Properties/AndroidManifest.xml
+++ b/osu.Android/Properties/AndroidManifest.xml
@@ -6,5 +6,6 @@
+
\ No newline at end of file
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 54857ac87d..582c856a47 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -63,5 +63,8 @@
5.0.0
+
+
+
\ No newline at end of file
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index d06c4b6746..5fb09c0cef 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -69,7 +69,6 @@ namespace osu.Desktop
/// Allow a maximum of one unhandled exception, per second of execution.
///
///
- ///
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index c81710ed18..26e5d381e2 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the sample info list at a point in time.
///
/// The time to retrieve the sample info list from.
- ///
private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
///
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index 485595cea9..12f379bddb 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Mirror";
public override string Acronym => "MR";
public override ModType Type => ModType.Conversion;
+ public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs
new file mode 100644
index 0000000000..f9445a9a96
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs
@@ -0,0 +1,260 @@
+// 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 NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Edit.Checks;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
+{
+ [TestFixture]
+ public class CheckOffscreenObjectsTest
+ {
+ private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE * 0.5f;
+
+ private CheckOffscreenObjects check;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckOffscreenObjects();
+ }
+
+ [Test]
+ public void TestCircleInCenter()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = playfield_centre // Playfield is 640 x 480.
+ }
+ }
+ };
+
+ Assert.That(check.Run(beatmap), Is.Empty);
+ }
+
+ [Test]
+ public void TestCircleNearEdge()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(5, 5)
+ }
+ }
+ };
+
+ Assert.That(check.Run(beatmap), Is.Empty);
+ }
+
+ [Test]
+ public void TestCircleNearEdgeStackedOffscreen()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(5, 5),
+ StackHeight = 5
+ }
+ }
+ };
+
+ assertOffscreenCircle(beatmap);
+ }
+
+ [Test]
+ public void TestCircleOffscreen()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(0, 0)
+ }
+ }
+ };
+
+ assertOffscreenCircle(beatmap);
+ }
+
+ [Test]
+ public void TestSliderInCenter()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = new Vector2(420, 240),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(new Vector2(-100, 0))
+ }),
+ }
+ }
+ };
+
+ Assert.That(check.Run(beatmap), Is.Empty);
+ }
+
+ [Test]
+ public void TestSliderNearEdge()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
+ }),
+ }
+ }
+ };
+
+ Assert.That(check.Run(beatmap), Is.Empty);
+ }
+
+ [Test]
+ public void TestSliderNearEdgeStackedOffscreen()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
+ }),
+ StackHeight = 5
+ }
+ }
+ };
+
+ assertOffscreenSlider(beatmap);
+ }
+
+ [Test]
+ public void TestSliderOffscreenStart()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = new Vector2(0, 0),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(playfield_centre)
+ }),
+ }
+ }
+ };
+
+ assertOffscreenSlider(beatmap);
+ }
+
+ [Test]
+ public void TestSliderOffscreenEnd()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2(0, 0), PathType.Linear),
+ new PathControlPoint(-playfield_centre)
+ }),
+ }
+ }
+ };
+
+ assertOffscreenSlider(beatmap);
+ }
+
+ [Test]
+ public void TestSliderOffscreenPath()
+ {
+ var beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 3000,
+ Position = playfield_centre,
+ Path = new SliderPath(new[]
+ {
+ // Circular arc shoots over the top of the screen.
+ new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(-100, -200)),
+ new PathControlPoint(new Vector2(100, -200))
+ }),
+ }
+ }
+ };
+
+ assertOffscreenSlider(beatmap);
+ }
+
+ private void assertOffscreenCircle(IBeatmap beatmap)
+ {
+ var issues = check.Run(beatmap).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
+ }
+
+ private void assertOffscreenSlider(IBeatmap beatmap)
+ {
+ var issues = check.Run(beatmap).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs
new file mode 100644
index 0000000000..d0348c1b6b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs
@@ -0,0 +1,46 @@
+// 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.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public class TestSceneOsuEditorSelectInvalidPath : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ [Test]
+ public void TestSelectDoesNotModify()
+ {
+ Slider slider = new Slider { StartTime = 0, Position = new Vector2(320, 40) };
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(-100, 0)),
+ new PathControlPoint(new Vector2(100, 20))
+ };
+
+ int preSelectVersion = -1;
+ AddStep("add slider", () =>
+ {
+ slider.Path = new SliderPath(points);
+ EditorBeatmap.Add(slider);
+ preSelectVersion = slider.Path.Version.Value;
+ });
+
+ AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
+
+ AddAssert("slider same path", () => slider.Path.Version.Value == preSelectVersion);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 856b6554b9..0ba775e5c7 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private void runSpmTest(Mod mod)
{
- SpinnerSpmCounter spmCounter = null;
+ SpinnerSpmCalculator spmCalculator = null;
CreateModTest(new ModTestData
{
@@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
});
- AddUntilStep("fetch SPM counter", () =>
+ AddUntilStep("fetch SPM calculator", () =>
{
- spmCounter = this.ChildrenOfType().SingleOrDefault();
- return spmCounter != null;
+ spmCalculator = this.ChildrenOfType().SingleOrDefault();
+ return spmCalculator != null;
});
- AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
+ AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
index 7df5ca0f7c..24e69703a6 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Beatmap = singleSpinnerBeatmap,
PassCondition = () =>
{
- var counter = Player.ChildrenOfType().SingleOrDefault();
- return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
+ var counter = Player.ChildrenOfType().SingleOrDefault();
+ return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
}
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png
new file mode 100755
index 0000000000..fe305468fe
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png
new file mode 100755
index 0000000000..f3327dc92f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursortrail.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png
new file mode 100644
index 0000000000..73753554f7
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index 8fd13c7417..0ba97fac54 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RelativeSizeAxes = Axes.Both;
}
- public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
+ public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
return null;
}
- public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => null;
- public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
+ public IBindable GetConfig(TLookup lookup) => null;
public event Action SourceChanged
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index e3ccf83715..9a77292aff 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -4,13 +4,22 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input;
using osu.Framework.Testing.Input;
using osu.Framework.Utils;
+using osu.Game.Audio;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
+using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -21,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached]
private GameplayBeatmap gameplayBeatmap;
- private ClickingCursorContainer lastContainer;
+ private OsuCursorContainer lastContainer;
[Resolved]
private OsuConfigManager config { get; set; }
@@ -48,12 +57,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
- Scheduler.AddOnce(recreate);
+ Scheduler.AddOnce(() => loadContent(false));
});
- AddStep("test cursor container", recreate);
-
- void recreate() => SetContents(() => new OsuInputManager(new OsuRuleset().RulesetInfo) { Child = new OsuCursorContainer() });
+ AddStep("test cursor container", () => loadContent(false));
}
[TestCase(1, 1)]
@@ -68,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
- AddStep("load content", loadContent);
+ AddStep("load content", () => loadContent());
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@@ -82,18 +89,46 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
}
- private void loadContent()
+ [Test]
+ public void TestTopLeftOrigin()
{
- SetContents(() => new MovingCursorInputManager
+ AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
+ }
+
+ private void loadContent(bool automated = true, Func skinProvider = null)
+ {
+ SetContents(() =>
{
- Child = lastContainer = new ClickingCursorContainer
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- }
+ var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo);
+ var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null);
+
+ lastContainer = automated ? new ClickingCursorContainer() : new OsuCursorContainer();
+
+ return inputManager.WithChild(skinContainer.WithChild(lastContainer));
});
}
+ private class TopLeftCursorSkin : ISkin
+ {
+ public Drawable GetDrawableComponent(ISkinComponent component) => null;
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
+ public ISample GetSample(ISampleInfo sampleInfo) => null;
+
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case OsuSkinConfiguration osuLookup:
+ if (osuLookup == OsuSkinConfiguration.CursorCentre)
+ return SkinUtils.As(new BindableBool(false));
+
+ break;
+ }
+
+ return null;
+ }
+ }
+
private class ClickingCursorContainer : OsuCursorContainer
{
private bool pressed;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index ac8d5c81bc..14c709cae1 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
double estimatedSpm = 0;
addSeekStep(1000);
- AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
+ AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
addSeekStep(1000);
- AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
+ AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
}
[TestCase(0.5)]
@@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spinner state", () =>
{
expectedProgress = drawableSpinner.Progress;
- expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
+ expectedSpm = drawableSpinner.SpinsPerMinute.Value;
});
addSeekStep(0);
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
- AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
+ AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
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 6b78cff33e..ce9580d0f4 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -59,11 +59,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
+ // we don't want to run the path type update on construction as it may inadvertently change the slider.
+ cachePoints(slider);
+
slider.Path.Version.BindValueChanged(_ =>
{
- PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
+ cachePoints(slider);
updatePathType();
- }, runOnceImmediately: true);
+ });
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
@@ -205,6 +208,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
+ private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
+
///
/// Handles correction of invalid path types.
///
diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
new file mode 100644
index 0000000000..27cae2ecc1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
@@ -0,0 +1,115 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Checks
+{
+ public class CheckOffscreenObjects : ICheck
+ {
+ // A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio.
+ // Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area).
+ // See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference.
+ private const int min_x = -67;
+ private const int min_y = -60;
+ private const int max_x = 579;
+ private const int max_y = 428;
+
+ // The amount of milliseconds to step through a slider path at a time
+ // (higher = more performant, but higher false-negative chance).
+ private const int path_step_size = 5;
+
+ public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateOffscreenCircle(this),
+ new IssueTemplateOffscreenSlider(this)
+ };
+
+ public IEnumerable Run(IBeatmap beatmap)
+ {
+ foreach (var hitobject in beatmap.HitObjects)
+ {
+ switch (hitobject)
+ {
+ case Slider slider:
+ {
+ foreach (var issue in sliderIssues(slider))
+ yield return issue;
+
+ break;
+ }
+
+ case HitCircle circle:
+ {
+ if (isOffscreen(circle.StackedPosition, circle.Radius))
+ yield return new IssueTemplateOffscreenCircle(this).Create(circle);
+
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Steps through points on the slider to ensure the entire path is on-screen.
+ /// Returns at most one issue.
+ ///
+ /// The slider whose path to check.
+ ///
+ private IEnumerable sliderIssues(Slider slider)
+ {
+ for (int i = 0; i < slider.Distance; i += path_step_size)
+ {
+ double progress = i / slider.Distance;
+ Vector2 position = slider.StackedPositionAt(progress);
+
+ if (!isOffscreen(position, slider.Radius))
+ continue;
+
+ // `SpanDuration` ensures we don't include reverses.
+ double time = slider.StartTime + progress * slider.SpanDuration;
+ yield return new IssueTemplateOffscreenSlider(this).Create(slider, time);
+
+ yield break;
+ }
+
+ // Above loop may skip the last position in the slider due to step size.
+ if (!isOffscreen(slider.StackedEndPosition, slider.Radius))
+ yield break;
+
+ yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime);
+ }
+
+ private bool isOffscreen(Vector2 position, double radius)
+ {
+ return position.X - radius < min_x || position.X + radius > max_x ||
+ position.Y - radius < min_y || position.Y + radius > max_y;
+ }
+
+ public class IssueTemplateOffscreenCircle : IssueTemplate
+ {
+ public IssueTemplateOffscreenCircle(ICheck check)
+ : base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.")
+ {
+ }
+
+ public Issue Create(HitCircle circle) => new Issue(circle, this);
+ }
+
+ public class IssueTemplateOffscreenSlider : IssueTemplate
+ {
+ public IssueTemplateOffscreenSlider(ICheck check)
+ : base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.")
+ {
+ }
+
+ public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
new file mode 100644
index 0000000000..1c7ab00bbb
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.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 System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Rulesets.Osu.Edit.Checks;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class OsuBeatmapVerifier : IBeatmapVerifier
+ {
+ private readonly List checks = new List
+ {
+ new CheckOffscreenObjects()
+ };
+
+ public IEnumerable Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
new file mode 100644
index 0000000000..b6cfa514a1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
@@ -0,0 +1,44 @@
+// 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.Framework.Graphics;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ {
+ [SettingSource("Roll speed", "Rotations per minute")]
+ public BindableNumber SpinSpeed { get; } = new BindableDouble(0.5)
+ {
+ MinValue = 0.02,
+ MaxValue = 4,
+ Precision = 0.01,
+ };
+
+ [SettingSource("Direction", "The direction of rotation")]
+ public Bindable Direction { get; } = new Bindable(RotationDirection.Clockwise);
+
+ public override string Name => "Barrel Roll";
+ public override string Acronym => "BR";
+ public override string Description => "The whole playfield is on a wheel!";
+ public override double ScoreMultiplier => 1;
+
+ public void Update(Playfield playfield)
+ {
+ playfield.Rotation = (Direction.Value == RotationDirection.CounterClockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ // scale the playfield to allow all hitobjects to stay within the visible region.
+ drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
index f0db548e74..3b16e9d2b7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
+ public override string Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 39e78a14aa..3a4753761a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; }
- public SpinnerSpmCounter SpmCounter { get; private set; }
+
+ private SpinnerSpmCalculator spmCalculator;
private Container ticks;
private PausableSkinnableSound spinningSample;
@@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public IBindable GainedBonus => gainedBonus;
- private readonly Bindable gainedBonus = new Bindable();
+ private readonly Bindable gainedBonus = new BindableDouble();
+
+ ///
+ /// The number of spins per minute this spinner is spinning at, for display purposes.
+ ///
+ public readonly IBindable SpinsPerMinute = new BindableDouble();
private const double fade_out_duration = 160;
@@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
+ spmCalculator = new SpinnerSpmCalculator
+ {
+ Result = { BindTarget = SpinsPerMinute },
+ },
ticks = new Container(),
new AspectContainer
{
@@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RotationTracker = new SpinnerRotationTracker(this)
}
},
- SpmCounter = new SpinnerSpmCounter
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Y = 120,
- Alpha = 0
- },
spinningSample = new PausableSkinnableSound
{
Volume = { Value = 0 },
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
}
- };
+ });
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
}
@@ -161,17 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
- protected override void UpdateStartTimeStateTransforms()
- {
- base.UpdateStartTimeStateTransforms();
-
- if (Result?.TimeStarted is double startTime)
- {
- using (BeginAbsoluteSequence(startTime))
- fadeInCounter();
- }
- }
-
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
@@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && RotationTracker.Tracking)
- {
- Result.TimeStarted ??= Time.Current;
- fadeInCounter();
- }
+ if (Result.TimeStarted == null && RotationTracker.Tracking)
+ Result.TimeStarted = Time.Current;
// don't update after end time to avoid the rate display dropping during fade out.
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
if (Time.Current <= HitObject.EndTime)
- SpmCounter.SetRotation(Result.RateAdjustedRotation);
+ spmCalculator.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
}
- private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
-
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private int wholeSpins;
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 838d707d64..465d6d7155 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -185,6 +185,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
+ new OsuModBarrelRoll(),
};
case ModType.System:
@@ -206,6 +207,8 @@ namespace osu.Game.Rulesets.Osu
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
+ public override IBeatmapVerifier CreateBeatmapVerifier() => new OsuBeatmapVerifier();
+
public override string Description => "osu!";
public override string ShortName => SHORT_NAME;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
index 891821fe2f..ae8c03dad1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.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.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private OsuSpriteText bonusCounter;
+ private Container spmContainer;
+ private OsuSpriteText spmCounter;
+
public DefaultSpinner()
{
RelativeSizeAxes = Axes.Both;
@@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24),
Y = -120,
+ },
+ spmContainer = new Container
+ {
+ Alpha = 0f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 120,
+ Children = new[]
+ {
+ spmCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"0",
+ Font = OsuFont.Numeric.With(size: 24)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"SPINS PER MINUTE",
+ Font = OsuFont.Numeric.With(size: 12),
+ Y = 30
+ }
+ }
}
});
}
private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
protected override void LoadComplete()
{
@@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
});
+
+ spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
+ fadeCounterOnTimeStart();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ fadeCounterOnTimeStart();
+ }
+
+ private void fadeCounterOnTimeStart()
+ {
+ if (drawableSpinner.Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
similarity index 61%
rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
index 69355f624b..a5205bbb8c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs
@@ -1,77 +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 System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class SpinnerSpmCounter : Container
+ public class SpinnerSpmCalculator : Component
{
+ private readonly Queue records = new Queue();
+ private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
+
+ ///
+ /// The resultant spins per minute value, which is updated via .
+ ///
+ public IBindable Result => result;
+
+ private readonly Bindable result = new BindableDouble();
+
[Resolved]
private DrawableHitObject drawableSpinner { get; set; }
- private readonly OsuSpriteText spmText;
-
- public SpinnerSpmCounter()
- {
- Children = new Drawable[]
- {
- spmText = new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"0",
- Font = OsuFont.Numeric.With(size: 24)
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = @"SPINS PER MINUTE",
- Font = OsuFont.Numeric.With(size: 12),
- Y = 30
- }
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
drawableSpinner.HitObjectApplied += resetState;
}
- private double spm;
-
- public double SpinsPerMinute
- {
- get => spm;
- private set
- {
- if (value == spm) return;
-
- spm = value;
- spmText.Text = Math.Truncate(value).ToString(@"#0");
- }
- }
-
- private struct RotationRecord
- {
- public float Rotation;
- public double Time;
- }
-
- private readonly Queue records = new Queue();
- private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
-
public void SetRotation(float currentRotation)
{
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
@@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
record = records.Dequeue();
- SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
+ result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
}
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
@@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void resetState(DrawableHitObject hitObject)
{
- SpinsPerMinute = 0;
+ result.Value = 0;
records.Clear();
}
@@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (drawableSpinner != null)
drawableSpinner.HitObjectApplied -= resetState;
}
+
+ private struct RotationRecord
+ {
+ public float Rotation;
+ public double Time;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs
index 314139d02a..7a8555d991 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
+ bool centre = skin.GetConfig(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
spin = skin.GetConfig(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
InternalChildren = new[]
@@ -32,13 +33,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Origin = centre ? Anchor.Centre : Anchor.TopLeft,
},
new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursormiddle"),
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Origin = centre ? Anchor.Centre : Anchor.TopLeft,
},
};
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
index af9ea99232..0025576325 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
@@ -26,7 +26,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
- Blending = !disjointTrail ? BlendingParameters.Additive : BlendingParameters.Inherit;
+ if (disjointTrail)
+ {
+ bool centre = skin.GetConfig(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
+
+ TrailOrigin = centre ? Anchor.Centre : Anchor.TopLeft;
+ Blending = BlendingParameters.Inherit;
+ }
+ else
+ {
+ Blending = BlendingParameters.Additive;
+ }
if (Texture != null)
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 064b7a4680..7eb6898abc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected const float SPRITE_SCALE = 0.625f;
+ private const float spm_hide_offset = 50f;
+
protected DrawableSpinner DrawableSpinner { get; private set; }
private Sprite spin;
@@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private LegacySpriteText bonusCounter;
+ private Sprite spmBackground;
+ private LegacySpriteText spmCounter;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
{
@@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 299,
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
+ spmBackground = new Sprite
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft,
+ Texture = source.GetTexture("spinner-rpm"),
+ Scale = new Vector2(SPRITE_SCALE),
+ Position = new Vector2(-87, 445 + spm_hide_offset),
+ },
+ spmCounter = new LegacySpriteText(source, LegacyFont.Score)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ Scale = new Vector2(SPRITE_SCALE * 0.9f),
+ Position = new Vector2(80, 448 + spm_hide_offset),
+ }.With(s => s.Font = s.Font.With(fixedWidth: false)),
}
});
}
private IBindable gainedBonus;
+ private IBindable spinsPerMinute;
private readonly Bindable completed = new Bindable();
@@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
});
+ spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
@@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (drawableHitObject)
{
case DrawableSpinner d:
- double fadeOutLength = Math.Min(400, d.HitObject.Duration);
+ using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
+ {
+ spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
+ }
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
- spin.FadeOutFromOne(fadeOutLength);
+ double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
+
+ using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
+ spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 75a62a6f8e..6953e66b5c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderBorderSize,
SliderPathRadius,
AllowSliderBallTint,
+ CursorCentre,
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 0b30c28b8d..7f86e9daf7 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
@@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private double timeOffset;
private float time;
+ private Anchor trailOrigin = Anchor.Centre;
+
+ protected Anchor TrailOrigin
+ {
+ get => trailOrigin;
+ set
+ {
+ trailOrigin = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ }
+
public CursorTrail()
{
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
@@ -197,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
+ private Vector2 originPosition;
+
private readonly QuadBatch vertexBatch = new QuadBatch(max_sprites, 1);
public TrailDrawNode(CursorTrail source)
@@ -213,6 +228,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
size = Source.partSize;
time = Source.time;
+ originPosition = Vector2.Zero;
+
+ if (Source.TrailOrigin.HasFlagFast(Anchor.x1))
+ originPosition.X = 0.5f;
+ else if (Source.TrailOrigin.HasFlagFast(Anchor.x2))
+ originPosition.X = 1f;
+
+ if (Source.TrailOrigin.HasFlagFast(Anchor.y1))
+ originPosition.Y = 0.5f;
+ else if (Source.TrailOrigin.HasFlagFast(Anchor.y2))
+ originPosition.Y = 1f;
+
Source.parts.CopyTo(parts, 0);
}
@@ -237,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2),
+ Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@@ -246,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2),
+ Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@@ -255,7 +282,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2),
+ Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@@ -264,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
- Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2),
+ Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index b1069149f3..ea3eb5eb5c 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuPlayfield()
{
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index ec7751d2b4..44ca5e850f 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.UI
{
Add(cursorScaleContainer = new Container
{
- RelativePositionAxes = Axes.Both,
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
});
}
diff --git a/osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs b/osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs
new file mode 100644
index 0000000000..635e3bb0f3
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs
@@ -0,0 +1,67 @@
+// 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 NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckBackgroundTest
+ {
+ private CheckBackground check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckBackground();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo { Filename = "abc123.jpg" }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestBackgroundSetAndInFiles()
+ {
+ Assert.That(check.Run(beatmap), Is.Empty);
+ }
+
+ [Test]
+ public void TestBackgroundSetAndNotInFiles()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+
+ var issues = check.Run(beatmap).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckBackground.IssueTemplateDoesNotExist);
+ }
+
+ [Test]
+ public void TestBackgroundNotSet()
+ {
+ beatmap.Metadata.BackgroundFile = null;
+
+ var issues = check.Run(beatmap).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckBackground.IssueTemplateNoneSet);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
new file mode 100644
index 0000000000..7a5789f01a
--- /dev/null
+++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
@@ -0,0 +1,36 @@
+// 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.Online.API;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Tests.Mods
+{
+ [TestFixture]
+ public class ModSettingsEqualityComparison
+ {
+ [Test]
+ public void Test()
+ {
+ var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
+ var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
+ var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
+ var apiMod1 = new APIMod(mod1);
+ var apiMod2 = new APIMod(mod2);
+ var apiMod3 = new APIMod(mod3);
+
+ Assert.That(mod1, Is.Not.EqualTo(mod2));
+ Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
+
+ Assert.That(mod2, Is.EqualTo(mod2));
+ Assert.That(apiMod2, Is.EqualTo(apiMod2));
+
+ Assert.That(mod2, Is.EqualTo(mod3));
+ Assert.That(apiMod2, Is.EqualTo(apiMod3));
+
+ Assert.That(mod3, Is.EqualTo(mod2));
+ Assert.That(apiMod3, Is.EqualTo(apiMod2));
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 1c0bfd56dd..16c1004f37 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -144,6 +144,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => nameof(ModA);
public override string Acronym => nameof(ModA);
+ public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) };
@@ -152,6 +153,7 @@ namespace osu.Game.Tests.NonVisual
private class ModB : Mod
{
public override string Name => nameof(ModB);
+ public override string Description => string.Empty;
public override string Acronym => nameof(ModB);
public override double ScoreMultiplier => 1;
@@ -162,6 +164,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC);
+ public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
}
@@ -169,6 +172,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => $"Incompatible With {nameof(ModA)}";
public override string Acronym => $"Incompatible With {nameof(ModA)}";
+ public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA) };
@@ -187,6 +191,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
+ public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
index 92a60663de..a42b7d54ee 100644
--- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
+++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs
@@ -20,27 +20,14 @@ namespace osu.Game.Tests.NonVisual
{
handler = new TestInputHandler(replay = new Replay
{
- Frames = new List
- {
- new TestReplayFrame(0),
- new TestReplayFrame(1000),
- new TestReplayFrame(2000),
- new TestReplayFrame(3000, true),
- new TestReplayFrame(4000, true),
- new TestReplayFrame(5000, true),
- new TestReplayFrame(7000, true),
- new TestReplayFrame(8000),
- }
+ HasReceivedAllFrames = false
});
}
[Test]
public void TestNormalPlayback()
{
- Assert.IsNull(handler.CurrentFrame);
-
- confirmCurrentFrame(null);
- confirmNextFrame(0);
+ setReplayFrames();
setTime(0, 0);
confirmCurrentFrame(0);
@@ -107,6 +94,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestIntroTime()
{
+ setReplayFrames();
+
setTime(-1000, -1000);
confirmCurrentFrame(null);
confirmNextFrame(0);
@@ -123,6 +112,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestBasicRewind()
{
+ setReplayFrames();
+
setTime(2800, 0);
setTime(2800, 1000);
setTime(2800, 2000);
@@ -133,34 +124,35 @@ namespace osu.Game.Tests.NonVisual
// pivot without crossing a frame boundary
setTime(2700, 2700);
confirmCurrentFrame(2);
- confirmNextFrame(1);
+ confirmNextFrame(3);
- // cross current frame boundary; should not yet update frame
- setTime(1980, 1980);
+ // cross current frame boundary
+ setTime(1980, 2000);
confirmCurrentFrame(2);
- confirmNextFrame(1);
+ confirmNextFrame(3);
setTime(1200, 1200);
- confirmCurrentFrame(2);
- confirmNextFrame(1);
+ confirmCurrentFrame(1);
+ confirmNextFrame(2);
// ensure each frame plays out until start
setTime(-500, 1000);
confirmCurrentFrame(1);
- confirmNextFrame(0);
+ confirmNextFrame(2);
setTime(-500, 0);
confirmCurrentFrame(0);
- confirmNextFrame(null);
+ confirmNextFrame(1);
setTime(-500, -500);
- confirmCurrentFrame(0);
- confirmNextFrame(null);
+ confirmCurrentFrame(null);
+ confirmNextFrame(0);
}
[Test]
public void TestRewindInsideImportantSection()
{
+ setReplayFrames();
fastForwardToPoint(3000);
setTime(4000, 4000);
@@ -168,12 +160,12 @@ namespace osu.Game.Tests.NonVisual
confirmNextFrame(5);
setTime(3500, null);
- confirmCurrentFrame(4);
- confirmNextFrame(3);
+ confirmCurrentFrame(3);
+ confirmNextFrame(4);
setTime(3000, 3000);
confirmCurrentFrame(3);
- confirmNextFrame(2);
+ confirmNextFrame(4);
setTime(3500, null);
confirmCurrentFrame(3);
@@ -187,46 +179,127 @@ namespace osu.Game.Tests.NonVisual
confirmCurrentFrame(4);
confirmNextFrame(5);
- setTime(4000, null);
+ setTime(4000, 4000);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(3500, null);
- confirmCurrentFrame(4);
- confirmNextFrame(3);
+ confirmCurrentFrame(3);
+ confirmNextFrame(4);
setTime(3000, 3000);
confirmCurrentFrame(3);
- confirmNextFrame(2);
+ confirmNextFrame(4);
}
[Test]
public void TestRewindOutOfImportantSection()
{
+ setReplayFrames();
fastForwardToPoint(3500);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3200, null);
- // next frame doesn't change even though direction reversed, because of important section.
confirmCurrentFrame(3);
confirmNextFrame(4);
- setTime(3000, null);
+ setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(2800, 2800);
- confirmCurrentFrame(3);
- confirmNextFrame(2);
+ confirmCurrentFrame(2);
+ confirmNextFrame(3);
+ }
+
+ [Test]
+ public void TestReplayStreaming()
+ {
+ // no frames are arrived yet
+ setTime(0, null);
+ setTime(1000, null);
+ Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame");
+
+ replay.Frames.Add(new TestReplayFrame(0));
+ replay.Frames.Add(new TestReplayFrame(1000));
+
+ // should always play from beginning
+ setTime(1000, 0);
+ confirmCurrentFrame(0);
+ Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet");
+ setTime(1000, 1000);
+ confirmCurrentFrame(1);
+ confirmNextFrame(null);
+ Assert.IsTrue(handler.WaitingForFrame, "Should be waiting");
+
+ // cannot seek beyond the last frame
+ setTime(1500, null);
+ confirmCurrentFrame(1);
+
+ setTime(-100, 0);
+ confirmCurrentFrame(0);
+
+ // can seek to the point before the first frame, however
+ setTime(-100, -100);
+ confirmCurrentFrame(null);
+ confirmNextFrame(0);
+
+ fastForwardToPoint(1000);
+ setTime(3000, null);
+ replay.Frames.Add(new TestReplayFrame(2000));
+ confirmCurrentFrame(1);
+ setTime(1000, 1000);
+ setTime(3000, 2000);
+ }
+
+ [Test]
+ public void TestMultipleFramesSameTime()
+ {
+ replay.Frames.Add(new TestReplayFrame(0));
+ replay.Frames.Add(new TestReplayFrame(0));
+ replay.Frames.Add(new TestReplayFrame(1000));
+ replay.Frames.Add(new TestReplayFrame(1000));
+ replay.Frames.Add(new TestReplayFrame(2000));
+
+ // forward direction is prioritized when multiple frames have the same time.
+ setTime(0, 0);
+ setTime(0, 0);
+
+ setTime(2000, 1000);
+ setTime(2000, 1000);
+
+ setTime(1000, 1000);
+ setTime(1000, 1000);
+ setTime(-100, 1000);
+ setTime(-100, 0);
+ setTime(-100, 0);
+ setTime(-100, -100);
+ }
+
+ private void setReplayFrames()
+ {
+ replay.Frames = new List
+ {
+ new TestReplayFrame(0),
+ new TestReplayFrame(1000),
+ new TestReplayFrame(2000),
+ new TestReplayFrame(3000, true),
+ new TestReplayFrame(4000, true),
+ new TestReplayFrame(5000, true),
+ new TestReplayFrame(7000, true),
+ new TestReplayFrame(8000),
+ };
+ replay.HasReceivedAllFrames = true;
}
private void fastForwardToPoint(double destination)
{
for (int i = 0; i < 1000; i++)
{
- if (handler.SetFrameFromTime(destination) == null)
+ var time = handler.SetFrameFromTime(destination);
+ if (time == null || time == destination)
return;
}
@@ -235,33 +308,17 @@ namespace osu.Game.Tests.NonVisual
private void setTime(double set, double? expect)
{
- Assert.AreEqual(expect, handler.SetFrameFromTime(set));
+ Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value");
}
private void confirmCurrentFrame(int? frame)
{
- if (frame.HasValue)
- {
- Assert.IsNotNull(handler.CurrentFrame);
- Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
- }
- else
- {
- Assert.IsNull(handler.CurrentFrame);
- }
+ Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame");
}
private void confirmNextFrame(int? frame)
{
- if (frame.HasValue)
- {
- Assert.IsNotNull(handler.NextFrame);
- Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
- }
- else
- {
- Assert.IsNull(handler.NextFrame);
- }
+ Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame");
}
private class TestReplayFrame : ReplayFrame
diff --git a/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs
deleted file mode 100644
index 21ec29b10b..0000000000
--- a/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs
+++ /dev/null
@@ -1,296 +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.Collections.Generic;
-using NUnit.Framework;
-using osu.Game.Replays;
-using osu.Game.Rulesets.Replays;
-
-namespace osu.Game.Tests.NonVisual
-{
- [TestFixture]
- public class StreamingFramedReplayInputHandlerTest
- {
- private Replay replay;
- private TestInputHandler handler;
-
- [SetUp]
- public void SetUp()
- {
- handler = new TestInputHandler(replay = new Replay
- {
- HasReceivedAllFrames = false,
- Frames = new List
- {
- new TestReplayFrame(0),
- new TestReplayFrame(1000),
- new TestReplayFrame(2000),
- new TestReplayFrame(3000, true),
- new TestReplayFrame(4000, true),
- new TestReplayFrame(5000, true),
- new TestReplayFrame(7000, true),
- new TestReplayFrame(8000),
- }
- });
- }
-
- [Test]
- public void TestNormalPlayback()
- {
- Assert.IsNull(handler.CurrentFrame);
-
- confirmCurrentFrame(null);
- confirmNextFrame(0);
-
- setTime(0, 0);
- confirmCurrentFrame(0);
- confirmNextFrame(1);
-
- // if we hit the first frame perfectly, time should progress to it.
- setTime(1000, 1000);
- confirmCurrentFrame(1);
- confirmNextFrame(2);
-
- // in between non-important frames should progress based on input.
- setTime(1200, 1200);
- confirmCurrentFrame(1);
-
- setTime(1400, 1400);
- confirmCurrentFrame(1);
-
- // progressing beyond the next frame should force time to that frame once.
- setTime(2200, 2000);
- confirmCurrentFrame(2);
-
- // second attempt should progress to input time
- setTime(2200, 2200);
- confirmCurrentFrame(2);
-
- // entering important section
- setTime(3000, 3000);
- confirmCurrentFrame(3);
-
- // cannot progress within
- setTime(3500, null);
- confirmCurrentFrame(3);
-
- setTime(4000, 4000);
- confirmCurrentFrame(4);
-
- // still cannot progress
- setTime(4500, null);
- confirmCurrentFrame(4);
-
- setTime(5200, 5000);
- confirmCurrentFrame(5);
-
- // important section AllowedImportantTimeSpan allowance
- setTime(5200, 5200);
- confirmCurrentFrame(5);
-
- setTime(7200, 7000);
- confirmCurrentFrame(6);
-
- setTime(7200, null);
- confirmCurrentFrame(6);
-
- // exited important section
- setTime(8200, 8000);
- confirmCurrentFrame(7);
- confirmNextFrame(null);
-
- setTime(8200, null);
- confirmCurrentFrame(7);
- confirmNextFrame(null);
-
- setTime(8400, null);
- confirmCurrentFrame(7);
- confirmNextFrame(null);
- }
-
- [Test]
- public void TestIntroTime()
- {
- setTime(-1000, -1000);
- confirmCurrentFrame(null);
- confirmNextFrame(0);
-
- setTime(-500, -500);
- confirmCurrentFrame(null);
- confirmNextFrame(0);
-
- setTime(0, 0);
- confirmCurrentFrame(0);
- confirmNextFrame(1);
- }
-
- [Test]
- public void TestBasicRewind()
- {
- setTime(2800, 0);
- setTime(2800, 1000);
- setTime(2800, 2000);
- setTime(2800, 2800);
- confirmCurrentFrame(2);
- confirmNextFrame(3);
-
- // pivot without crossing a frame boundary
- setTime(2700, 2700);
- confirmCurrentFrame(2);
- confirmNextFrame(1);
-
- // cross current frame boundary; should not yet update frame
- setTime(1980, 1980);
- confirmCurrentFrame(2);
- confirmNextFrame(1);
-
- setTime(1200, 1200);
- confirmCurrentFrame(2);
- confirmNextFrame(1);
-
- // ensure each frame plays out until start
- setTime(-500, 1000);
- confirmCurrentFrame(1);
- confirmNextFrame(0);
-
- setTime(-500, 0);
- confirmCurrentFrame(0);
- confirmNextFrame(null);
-
- setTime(-500, -500);
- confirmCurrentFrame(0);
- confirmNextFrame(null);
- }
-
- [Test]
- public void TestRewindInsideImportantSection()
- {
- fastForwardToPoint(3000);
-
- setTime(4000, 4000);
- confirmCurrentFrame(4);
- confirmNextFrame(5);
-
- setTime(3500, null);
- confirmCurrentFrame(4);
- confirmNextFrame(3);
-
- setTime(3000, 3000);
- confirmCurrentFrame(3);
- confirmNextFrame(2);
-
- setTime(3500, null);
- confirmCurrentFrame(3);
- confirmNextFrame(4);
-
- setTime(4000, 4000);
- confirmCurrentFrame(4);
- confirmNextFrame(5);
-
- setTime(4500, null);
- confirmCurrentFrame(4);
- confirmNextFrame(5);
-
- setTime(4000, null);
- confirmCurrentFrame(4);
- confirmNextFrame(5);
-
- setTime(3500, null);
- confirmCurrentFrame(4);
- confirmNextFrame(3);
-
- setTime(3000, 3000);
- confirmCurrentFrame(3);
- confirmNextFrame(2);
- }
-
- [Test]
- public void TestRewindOutOfImportantSection()
- {
- fastForwardToPoint(3500);
-
- confirmCurrentFrame(3);
- confirmNextFrame(4);
-
- setTime(3200, null);
- // next frame doesn't change even though direction reversed, because of important section.
- confirmCurrentFrame(3);
- confirmNextFrame(4);
-
- setTime(3000, null);
- confirmCurrentFrame(3);
- confirmNextFrame(4);
-
- setTime(2800, 2800);
- confirmCurrentFrame(3);
- confirmNextFrame(2);
- }
-
- private void fastForwardToPoint(double destination)
- {
- for (int i = 0; i < 1000; i++)
- {
- if (handler.SetFrameFromTime(destination) == null)
- return;
- }
-
- throw new TimeoutException("Seek was never fulfilled");
- }
-
- private void setTime(double set, double? expect)
- {
- Assert.AreEqual(expect, handler.SetFrameFromTime(set));
- }
-
- private void confirmCurrentFrame(int? frame)
- {
- if (frame.HasValue)
- {
- Assert.IsNotNull(handler.CurrentFrame);
- Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
- }
- else
- {
- Assert.IsNull(handler.CurrentFrame);
- }
- }
-
- private void confirmNextFrame(int? frame)
- {
- if (frame.HasValue)
- {
- Assert.IsNotNull(handler.NextFrame);
- Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
- }
- else
- {
- Assert.IsNull(handler.NextFrame);
- }
- }
-
- private class TestReplayFrame : ReplayFrame
- {
- public readonly bool IsImportant;
-
- public TestReplayFrame(double time, bool isImportant = false)
- : base(time)
- {
- IsImportant = isImportant;
- }
- }
-
- private class TestInputHandler : FramedReplayInputHandler
- {
- public TestInputHandler(Replay replay)
- : base(replay)
- {
- FrameAccuratePlayback = true;
- }
-
- protected override double AllowedImportantTimeSpan => 1000;
-
- protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant;
- }
- }
-}
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 77f910c144..ad2007f202 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -11,7 +11,10 @@ using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
namespace osu.Game.Tests.Online
{
@@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online
Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11));
}
+ [Test]
+ public void TestDeserialiseScoreInfoWithEmptyMods()
+ {
+ var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
+
+ if (deserialised != null)
+ deserialised.Ruleset = new OsuRuleset().RulesetInfo;
+
+ Assert.That(deserialised?.Mods.Length, Is.Zero);
+ }
+
+ [Test]
+ public void TestDeserialiseScoreInfoWithCustomModSetting()
+ {
+ var score = new ScoreInfo
+ {
+ Ruleset = new OsuRuleset().RulesetInfo,
+ Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
+ };
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score));
+
+ if (deserialised != null)
+ deserialised.Ruleset = new OsuRuleset().RulesetInfo;
+
+ Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2));
+ }
+
private class TestRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[]
@@ -107,6 +140,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
+ public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@@ -123,6 +157,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
+ public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
index 74db477cfc..0462e9feb5 100644
--- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
@@ -100,6 +100,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
+ public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@@ -116,6 +117,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
+ public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
@@ -150,6 +152,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
+ public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index ba4d12b19f..f89988cd1a 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -65,6 +65,21 @@ namespace osu.Game.Tests.Visual.Background
stack.Push(songSelect = new DummySongSelect());
});
+ ///
+ /// User settings should always be ignored on song select screen.
+ ///
+ [Test]
+ public void TestUserSettingsIgnoredOnSongSelect()
+ {
+ setupUserSettings();
+ AddUntilStep("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
+ AddUntilStep("Screen using background blur", () => songSelect.IsBackgroundBlur());
+ performFullSetup();
+ AddStep("Exit to song select", () => player.Exit());
+ AddUntilStep("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
+ AddUntilStep("Screen using background blur", () => songSelect.IsBackgroundBlur());
+ }
+
///
/// Check if properly triggers the visual settings preview when a user hovers over the visual settings panel.
///
@@ -142,9 +157,9 @@ namespace osu.Game.Tests.Visual.Background
{
performFullSetup();
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
- AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false);
+ AddStep("Disable user dim", () => songSelect.IgnoreUserSettings.Value = true);
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
- AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true);
+ AddStep("Enable user dim", () => songSelect.IgnoreUserSettings.Value = false);
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
@@ -161,13 +176,36 @@ namespace osu.Game.Tests.Visual.Background
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
- AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true);
+ AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
- AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false);
+ AddStep("Disable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
}
+ [Test]
+ public void TestStoryboardIgnoreUserSettings()
+ {
+ performFullSetup();
+ createFakeStoryboard();
+ AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
+
+ AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
+ AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
+
+ AddStep("Ignore user settings", () =>
+ {
+ player.ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
+ player.DimmableStoryboard.IgnoreUserSettings.Value = true;
+ });
+ AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
+ AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
+
+ AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
+ AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
+ AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
+ }
+
///
/// Check if the visual settings container retains dim and blur when pausing
///
@@ -204,17 +242,6 @@ namespace osu.Game.Tests.Visual.Background
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && songSelect.CheckBackgroundBlur(results.ExpectedBackgroundBlur));
}
- ///
- /// Check if background gets undimmed and unblurred when leaving for
- ///
- [Test]
- public void TestTransitionOut()
- {
- performFullSetup();
- AddStep("Exit to song select", () => player.Exit());
- AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
- }
-
///
/// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
///
@@ -281,11 +308,11 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground()
{
background = new FadeAccessibleBackground(Beatmap.Value);
- DimEnabled.BindTo(background.EnableUserDim);
+ IgnoreUserSettings.BindTo(background.IgnoreUserSettings);
return background;
}
- public readonly Bindable DimEnabled = new Bindable();
+ public readonly Bindable IgnoreUserSettings = new Bindable();
public readonly Bindable DimLevel = new BindableDouble();
public readonly Bindable BlurLevel = new BindableDouble();
@@ -310,7 +337,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
- public bool IsBlurCorrect() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR);
+ public bool IsBackgroundBlur() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR);
public bool CheckBackgroundBlur(Vector2 expected) => background.CurrentBlur == expected;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs
deleted file mode 100644
index 9efd299fba..0000000000
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
-using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
-using osu.Game.Tests.Beatmaps;
-using osu.Game.Screens.Edit.Compose.Components;
-using osuTK;
-using osuTK.Input;
-
-namespace osu.Game.Tests.Visual.Editing
-{
- public class TestSceneEditorQuickDelete : EditorTestScene
- {
- protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
-
- protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
-
- private BlueprintContainer blueprintContainer
- => Editor.ChildrenOfType().First();
-
- [Test]
- public void TestQuickDeleteRemovesObject()
- {
- var addedObject = new HitCircle { StartTime = 1000 };
-
- AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
-
- AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
-
- AddStep("move mouse to object", () =>
- {
- var pos = blueprintContainer.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre;
- InputManager.MoveMouseTo(pos);
- });
- AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
- AddStep("right click", () => InputManager.Click(MouseButton.Right));
- AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
-
- AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
- }
-
- [Test]
- public void TestQuickDeleteRemovesSliderControlPoint()
- {
- Slider slider = new Slider { StartTime = 1000 };
-
- PathControlPoint[] points =
- {
- new PathControlPoint(),
- new PathControlPoint(new Vector2(50, 0)),
- new PathControlPoint(new Vector2(100, 0))
- };
-
- AddStep("add slider", () =>
- {
- slider.Path = new SliderPath(points);
- EditorBeatmap.Add(slider);
- });
-
- AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
-
- AddStep("move mouse to controlpoint", () =>
- {
- var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre;
- InputManager.MoveMouseTo(pos);
- });
- AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
-
- AddStep("right click", () => InputManager.Click(MouseButton.Right));
- AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
-
- // second click should nuke the object completely.
- AddStep("right click", () => InputManager.Click(MouseButton.Right));
- AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
-
- AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs
new file mode 100644
index 0000000000..99f31b0c2a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs
@@ -0,0 +1,219 @@
+// 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.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneEditorSelection : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ private BlueprintContainer blueprintContainer
+ => Editor.ChildrenOfType().First();
+
+ private void moveMouseToObject(Func targetFunc)
+ {
+ AddStep("move mouse to object", () =>
+ {
+ var pos = blueprintContainer.SelectionBlueprints
+ .First(s => s.HitObject == targetFunc())
+ .ChildrenOfType()
+ .First().ScreenSpaceDrawQuad.Centre;
+
+ InputManager.MoveMouseTo(pos);
+ });
+ }
+
+ [Test]
+ public void TestBasicSelect()
+ {
+ var addedObject = new HitCircle { StartTime = 100 };
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+
+ moveMouseToObject(() => addedObject);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
+
+ var addedObject2 = new HitCircle
+ {
+ StartTime = 100,
+ Position = new Vector2(100),
+ };
+
+ AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2));
+ AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
+
+ moveMouseToObject(() => addedObject2);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2);
+ }
+
+ [Test]
+ public void TestMultiSelect()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 100 },
+ new HitCircle { StartTime = 200, Position = new Vector2(50) },
+ new HitCircle { StartTime = 300, Position = new Vector2(100) },
+ new HitCircle { StartTime = 400, Position = new Vector2(150) },
+ };
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]);
+
+ AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click second", () => InputManager.Click(MouseButton.Left));
+ AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
+
+ moveMouseToObject(() => addedObjects[2]);
+ AddStep("click third", () => InputManager.Click(MouseButton.Left));
+ AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2]));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click second", () => InputManager.Click(MouseButton.Left));
+ AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag)
+ {
+ HitCircle[] addedObjects = null;
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
+ {
+ new HitCircle { StartTime = 100 },
+ new HitCircle { StartTime = 200, Position = new Vector2(50) },
+ new HitCircle { StartTime = 300, Position = new Vector2(100) },
+ new HitCircle { StartTime = 400, Position = new Vector2(150) },
+ }));
+
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
+
+ moveMouseToObject(() => addedObjects[1]);
+
+ if (alreadySelectedBeforeDrag)
+ AddStep("click second", () => InputManager.Click(MouseButton.Left));
+
+ AddStep("mouse down on second", () => InputManager.PressButton(MouseButton.Left));
+
+ AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
+
+ AddStep("drag to centre", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
+
+ AddAssert("positions changed", () => addedObjects[0].Position != Vector2.Zero && addedObjects[1].Position != new Vector2(50));
+
+ AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
+ AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left));
+ }
+
+ [Test]
+ public void TestBasicDeselect()
+ {
+ var addedObject = new HitCircle { StartTime = 100 };
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+
+ moveMouseToObject(() => addedObject);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
+
+ AddStep("click away", () =>
+ {
+ InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0);
+ }
+
+ [Test]
+ public void TestQuickDeleteRemovesObject()
+ {
+ var addedObject = new HitCircle { StartTime = 1000 };
+
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+
+ AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
+
+ moveMouseToObject(() => addedObject);
+
+ AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+
+ AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
+ }
+
+ [Test]
+ public void TestQuickDeleteRemovesSliderControlPoint()
+ {
+ Slider slider = null;
+
+ PathControlPoint[] points =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(50, 0)),
+ new PathControlPoint(new Vector2(100, 0))
+ };
+
+ AddStep("add slider", () =>
+ {
+ slider = new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath(points)
+ };
+
+ EditorBeatmap.Add(slider);
+ });
+
+ AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
+
+ AddStep("move mouse to controlpoint", () =>
+ {
+ var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre;
+ InputManager.MoveMouseTo(pos);
+ });
+ AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
+
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+ AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
+
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+
+ // second click should nuke the object completely.
+ AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
+
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
index 94a9fd7b35..da0c83bb11 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs
@@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK;
@@ -16,18 +15,28 @@ namespace osu.Game.Tests.Visual.Editing
public class TestSceneEditorSummaryTimeline : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
- private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
+ private readonly EditorBeatmap editorBeatmap;
- [BackgroundDependencyLoader]
- private void load()
+ public TestSceneEditorSummaryTimeline()
{
- Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+ }
- Add(new SummaryTimeline
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddStep("create timeline", () =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 50)
+ // required for track
+ Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
+
+ Add(new SummaryTimeline
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 50)
+ });
});
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
index 35f394fe1d..e6fad33a51 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestDisallowZeroDurationObjects()
{
- DragBar dragBar;
+ DragArea dragArea;
AddStep("add spinner", () =>
{
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap.Add(new Spinner
{
Position = new Vector2(256, 256),
- StartTime = 150,
+ StartTime = 2700,
Duration = 500
});
});
@@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("hold down drag bar", () =>
{
// distinguishes between the actual drag bar and its "underlay shadow".
- dragBar = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput);
- InputManager.MoveMouseTo(dragBar);
+ dragArea = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput);
+ InputManager.MoveMouseTo(dragArea);
InputManager.PressButton(MouseButton.Left);
});
diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index 1da6433707..4aed445d9d 100644
--- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -53,17 +53,21 @@ namespace osu.Game.Tests.Visual.Editing
new AudioVisualiser(),
}
},
- TimelineArea = new TimelineArea
+ TimelineArea = new TimelineArea(CreateTestComponent())
{
- Child = CreateTestComponent(),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Size = new Vector2(0.8f, 100),
}
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Clock.Seek(2500);
+ }
+
public abstract Drawable CreateTestComponent();
private class AudioVisualiser : CompositeDrawable
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 88fbf09ef4..cfdea31a75 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
+using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private readonly VolumeOverlay volumeOverlay;
+ [Cached(typeof(BatteryInfo))]
+ private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo();
+
private readonly ChangelogOverlay changelogOverlay;
public TestScenePlayerLoader()
@@ -288,6 +292,33 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
+ [TestCase(false, 1.0, false)] // not charging, above cutoff --> no warning
+ [TestCase(true, 0.1, false)] // charging, below cutoff --> no warning
+ [TestCase(false, 0.25, true)] // not charging, at cutoff --> warning
+ public void TestLowBatteryNotification(bool isCharging, double chargeLevel, bool shouldWarn)
+ {
+ AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce).Value = false);
+
+ // set charge status and level
+ AddStep("load player", () => resetPlayer(false, () =>
+ {
+ batteryInfo.SetCharging(isCharging);
+ batteryInfo.SetChargeLevel(chargeLevel);
+ }));
+ AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
+ AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0));
+ AddStep("click notification", () =>
+ {
+ var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
+ var flowContainer = scrollContainer.Children.OfType>().First();
+ var notification = flowContainer.First();
+
+ InputManager.MoveMouseTo(notification);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("wait for player load", () => player.IsLoaded);
+ }
+
[Test]
public void TestEpilepsyWarningEarlyExit()
{
@@ -321,6 +352,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override string Name => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
+ public override string Description => string.Empty;
public bool Applied { get; private set; }
@@ -348,5 +380,29 @@ namespace osu.Game.Tests.Visual.Gameplay
throw new TimeoutException();
}
}
+
+ ///
+ /// Mutable dummy BatteryInfo class for
+ ///
+ ///
+ private class LocalBatteryInfo : BatteryInfo
+ {
+ private bool isCharging = true;
+ private double chargeLevel = 1;
+
+ public override bool IsCharging => isCharging;
+
+ public override double ChargeLevel => chargeLevel;
+
+ public void SetCharging(bool value)
+ {
+ isCharging = value;
+ }
+
+ public void SetChargeLevel(double value)
+ {
+ chargeLevel = value;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
index 9d85a9995d..397b37718d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs
@@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer();
AddAssert("ensure frames arrived", () => replayHandler.HasFrames);
- AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null);
+ AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
checkPaused(true);
double? pausedTime = null;
@@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
sendFrames();
- AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null);
+ AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
checkPaused(true);
AddAssert("time advanced", () => currentFrameStableTime > pausedTime);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 35b3bfc1f8..9c763814f3 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -204,27 +204,27 @@ namespace osu.Game.Tests.Visual.Gameplay
return;
}
- if (replayHandler.NextFrame != null)
- {
- var lastFrame = replay.Frames.LastOrDefault();
+ if (!replayHandler.HasFrames)
+ return;
- // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
- // in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
- if (lastFrame != null)
- latency = Math.Max(latency, Time.Current - lastFrame.Time);
+ var lastFrame = replay.Frames.LastOrDefault();
- latencyDisplay.Text = $"latency: {latency:N1}";
+ // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
+ // in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
+ if (lastFrame != null)
+ latency = Math.Max(latency, Time.Current - lastFrame.Time);
- double proposedTime = Time.Current - latency + Time.Elapsed;
+ latencyDisplay.Text = $"latency: {latency:N1}";
- // this will either advance by one or zero frames.
- double? time = replayHandler.SetFrameFromTime(proposedTime);
+ double proposedTime = Time.Current - latency + Time.Elapsed;
- if (time == null)
- return;
+ // this will either advance by one or zero frames.
+ double? time = replayHandler.SetFrameFromTime(proposedTime);
- manualClock.CurrentTime = time.Value;
- }
+ if (time == null)
+ return;
+
+ manualClock.CurrentTime = time.Value;
}
[TearDownSteps]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
index 1ee848b902..b6c06bb149 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
@@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+ private OsuConfigManager config;
+
public TestSceneMultiplayerGameplayLeaderboard()
{
base.Content.Children = new Drawable[]
@@ -48,6 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
+ }
+
[SetUpSteps]
public override void SetUpSteps()
{
@@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
}
+ [Test]
+ public void TestChangeScoringMode()
+ {
+ AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5);
+ AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
+ AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
+ }
+
public class TestMultiplayerStreaming : SpectatorStreamingClient
{
public new BindableList PlayingUsers => (BindableList)base.PlayingUsers;
@@ -163,7 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
break;
}
- ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty()));
+ ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 839118de2f..caa731f985 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
- AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
+ AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs
new file mode 100644
index 0000000000..3b2cfb1c7b
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs
@@ -0,0 +1,229 @@
+// 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 System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Framework.Utils;
+using osu.Game.Database;
+using osu.Game.Online;
+using osu.Game.Online.Spectator;
+using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene
+ {
+ [Cached(typeof(SpectatorStreamingClient))]
+ private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
+
+ [Cached(typeof(UserLookupCache))]
+ private UserLookupCache lookupCache = new TestUserLookupCache();
+
+ protected override Container Content => content;
+ private readonly Container content;
+
+ private readonly Dictionary clocks = new Dictionary
+ {
+ { 55, new ManualClock() },
+ { 56, new ManualClock() }
+ };
+
+ public TestSceneMultiplayerSpectatorLeaderboard()
+ {
+ base.Content.AddRange(new Drawable[]
+ {
+ streamingClient,
+ lookupCache,
+ content = new Container { RelativeSizeAxes = Axes.Both }
+ });
+ }
+
+ [SetUpSteps]
+ public new void SetUpSteps()
+ {
+ MultiplayerSpectatorLeaderboard leaderboard = null;
+
+ AddStep("reset", () =>
+ {
+ Clear();
+
+ foreach (var (userId, clock) in clocks)
+ {
+ streamingClient.EndPlay(userId, 0);
+ clock.CurrentTime = 0;
+ }
+ });
+
+ AddStep("create leaderboard", () =>
+ {
+ foreach (var (userId, _) in clocks)
+ streamingClient.StartPlay(userId, 0);
+
+ Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
+
+ var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
+ var scoreProcessor = new OsuScoreProcessor();
+ scoreProcessor.ApplyBeatmap(playable);
+
+ LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
+ });
+
+ AddUntilStep("wait for load", () => leaderboard.IsLoaded);
+
+ AddStep("add clock sources", () =>
+ {
+ foreach (var (userId, clock) in clocks)
+ leaderboard.AddClock(userId, clock);
+ });
+ }
+
+ [Test]
+ public void TestLeaderboardTracksCurrentTime()
+ {
+ AddStep("send frames", () =>
+ {
+ // For user 55, send frames in sets of 1.
+ // For user 56, send frames in sets of 10.
+ for (int i = 0; i < 100; i++)
+ {
+ streamingClient.SendFrames(55, i, 1);
+
+ if (i % 10 == 0)
+ streamingClient.SendFrames(56, i, 10);
+ }
+ });
+
+ assertCombo(55, 1);
+ assertCombo(56, 10);
+
+ // Advance to a point where only user 55's frame changes.
+ setTime(500);
+ assertCombo(55, 5);
+ assertCombo(56, 10);
+
+ // Advance to a point where both user's frame changes.
+ setTime(1100);
+ assertCombo(55, 11);
+ assertCombo(56, 20);
+
+ // Advance user 56 only to a point where its frame changes.
+ setTime(56, 2100);
+ assertCombo(55, 11);
+ assertCombo(56, 30);
+
+ // Advance both users beyond their last frame
+ setTime(101 * 100);
+ assertCombo(55, 100);
+ assertCombo(56, 100);
+ }
+
+ [Test]
+ public void TestNoFrames()
+ {
+ assertCombo(55, 0);
+ assertCombo(56, 0);
+ }
+
+ private void setTime(double time) => AddStep($"set time {time}", () =>
+ {
+ foreach (var (_, clock) in clocks)
+ clock.CurrentTime = time;
+ });
+
+ private void setTime(int userId, double time)
+ => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
+
+ private void assertCombo(int userId, int expectedCombo)
+ => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
+
+ private class TestSpectatorStreamingClient : SpectatorStreamingClient
+ {
+ private readonly Dictionary userBeatmapDictionary = new Dictionary();
+ private readonly Dictionary userSentStateDictionary = new Dictionary();
+
+ public TestSpectatorStreamingClient()
+ : base(new DevelopmentEndpointConfiguration())
+ {
+ }
+
+ public void StartPlay(int userId, int beatmapId)
+ {
+ userBeatmapDictionary[userId] = beatmapId;
+ userSentStateDictionary[userId] = false;
+ sendState(userId, beatmapId);
+ }
+
+ public void EndPlay(int userId, int beatmapId)
+ {
+ ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
+ {
+ BeatmapID = beatmapId,
+ RulesetID = 0,
+ });
+ userSentStateDictionary[userId] = false;
+ }
+
+ public void SendFrames(int userId, int index, int count)
+ {
+ var frames = new List();
+
+ for (int i = index; i < index + count; i++)
+ {
+ var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
+ frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
+ }
+
+ var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
+ ((ISpectatorClient)this).UserSentFrames(userId, bundle);
+ if (!userSentStateDictionary[userId])
+ sendState(userId, userBeatmapDictionary[userId]);
+ }
+
+ public override void WatchUser(int userId)
+ {
+ if (userSentStateDictionary[userId])
+ {
+ // usually the server would do this.
+ sendState(userId, userBeatmapDictionary[userId]);
+ }
+
+ base.WatchUser(userId);
+ }
+
+ private void sendState(int userId, int beatmapId)
+ {
+ ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
+ {
+ BeatmapID = beatmapId,
+ RulesetID = 0,
+ });
+ userSentStateDictionary[userId] = true;
+ }
+ }
+
+ private class TestUserLookupCache : UserLookupCache
+ {
+ protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ {
+ return Task.FromResult(new User
+ {
+ Id = lookup,
+ Username = $"User {lookup}"
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs
new file mode 100644
index 0000000000..c0958c7fe8
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs
@@ -0,0 +1,115 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneMultiplayerSpectatorPlayerGrid : OsuManualInputManagerTestScene
+ {
+ private PlayerGrid grid;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = grid = new PlayerGrid { RelativeSizeAxes = Axes.Both };
+ });
+
+ [Test]
+ public void TestMaximiseAndMinimise()
+ {
+ addCells(2);
+
+ assertMaximisation(0, false, true);
+ assertMaximisation(1, false, true);
+
+ clickCell(0);
+ assertMaximisation(0, true);
+ assertMaximisation(1, false, true);
+ clickCell(0);
+ assertMaximisation(0, false);
+ assertMaximisation(1, false, true);
+
+ clickCell(1);
+ assertMaximisation(1, true);
+ assertMaximisation(0, false, true);
+ clickCell(1);
+ assertMaximisation(1, false);
+ assertMaximisation(0, false, true);
+ }
+
+ [Test]
+ public void TestClickBothCellsSimultaneously()
+ {
+ addCells(2);
+
+ AddStep("click cell 0 then 1", () =>
+ {
+ InputManager.MoveMouseTo(grid.Content.ElementAt(0));
+ InputManager.Click(MouseButton.Left);
+
+ InputManager.MoveMouseTo(grid.Content.ElementAt(1));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ assertMaximisation(1, true);
+ assertMaximisation(0, false);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ [TestCase(3)]
+ [TestCase(4)]
+ [TestCase(5)]
+ [TestCase(9)]
+ [TestCase(11)]
+ [TestCase(12)]
+ [TestCase(15)]
+ [TestCase(16)]
+ public void TestCellCount(int count)
+ {
+ addCells(count);
+ AddWaitStep("wait for display", 2);
+ }
+
+ private void addCells(int count) => AddStep($"add {count} grid cells", () =>
+ {
+ for (int i = 0; i < count; i++)
+ grid.Add(new GridContent());
+ });
+
+ private void clickCell(int index) => AddStep($"click cell index {index}", () =>
+ {
+ InputManager.MoveMouseTo(grid.Content.ElementAt(index));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ private void assertMaximisation(int index, bool shouldBeMaximised, bool instant = false)
+ {
+ string assertionText = $"cell index {index} {(shouldBeMaximised ? "is" : "is not")} maximised";
+
+ if (instant)
+ AddAssert(assertionText, checkAction);
+ else
+ AddUntilStep(assertionText, checkAction);
+
+ bool checkAction() => Precision.AlmostEquals(grid.MaximisedFacade.DrawSize, grid.Content.ElementAt(index).DrawSize, 10) == shouldBeMaximised;
+ }
+
+ private class GridContent : Box
+ {
+ public GridContent()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
index 57ce4c41e7..484c59695e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs
@@ -19,13 +19,12 @@ namespace osu.Game.Tests.Visual.Online
{
UserHistoryGraph graph;
- Add(graph = new UserHistoryGraph
+ Add(graph = new UserHistoryGraph("Test")
{
RelativeSizeAxes = Axes.X,
Height = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- TooltipCounterName = "Test"
});
var values = new[]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs
index 443cf59003..fdc21d80ff 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModButton.cs
@@ -57,6 +57,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private abstract class TestMod : Mod, IApplicableMod
{
public override double ScoreMultiplier => 1.0;
+
+ public override string Description => "This is a test mod.";
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
index 89f9b7381b..2158cf77e5 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
@@ -226,6 +226,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public override double ScoreMultiplier => 1.0;
+ public override string Description => "This is a customisable test mod.";
+
public override ModType Type => ModType.Conversion;
[SettingSource("Sample float", "Change something for a mod")]
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 0e1f6f6b0c..df6d17f615 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -22,4 +22,4 @@
-
\ No newline at end of file
+
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 2ee52c35aa..92eb7ac713 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -141,7 +141,6 @@ namespace osu.Game.Tournament
///
/// Add missing player info based on user IDs.
///
- ///
private bool addPlayers()
{
bool addedInfo = false;
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 73337ab6f5..8a6cfaf688 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.ControlPoints
MaxValue = 10
};
- public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark;
+ public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
///
/// The speed multiplier at this control point.
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 580642f593..ec20328fab 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
private const double default_beat_length = 60000.0 / 60.0;
- public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark;
+ public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1;
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
{
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 9847ea020a..769b33009a 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps
///
/// Returns statistics for the contained in this beatmap.
///
- ///
IEnumerable GetStatistics();
///
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 387cfbb193..f9b1c9618b 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -3,7 +3,6 @@
using System;
using System.Diagnostics;
-using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
@@ -143,7 +142,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
- SetDefault(OsuSetting.EditorWaveformOpacity, 1f);
+ SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
}
public OsuConfigManager(Storage storage)
@@ -169,14 +168,9 @@ namespace osu.Game.Configuration
int combined = (year * 10000) + monthDay;
- if (combined < 20200305)
+ if (combined < 20210413)
{
- // the maximum value of this setting was changed.
- // if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see.
- var maxStars = (BindableDouble)GetOriginalBindable(OsuSetting.DisplayStarsMaximum);
-
- if (maxStars.Value == 10)
- maxStars.Value = maxStars.MaxValue;
+ SetValue(OsuSetting.EditorWaveformOpacity, 0.25f);
}
}
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 36eb6964dd..71e1a1efcc 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -16,6 +16,7 @@ namespace osu.Game.Configuration
{
SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false);
+ SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.SeasonalBackgrounds, null);
}
@@ -25,6 +26,7 @@ namespace osu.Game.Configuration
{
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce,
+ LowBatteryNotificationShownOnce,
///
/// Info about seasonal backgrounds available fetched from API - see .
diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs
index f8c9bdeaf8..86e84b0732 100644
--- a/osu.Game/Configuration/SettingsStore.cs
+++ b/osu.Game/Configuration/SettingsStore.cs
@@ -22,7 +22,6 @@ namespace osu.Game.Configuration
///
/// The ruleset's internal ID.
/// An optional variant.
- ///
public List Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index 1790eb608e..67b9e727a5 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -9,6 +9,9 @@ namespace osu.Game.Extensions
{
public static class DrawableExtensions
{
+ public const double REPEAT_INTERVAL = 70;
+ public const double INITIAL_DELAY = 250;
+
///
/// Helper method that is used while doesn't support repetitions of .
/// Simulates repetitions by continually invoking a delegate according to the default key repeat rate.
@@ -19,12 +22,13 @@ namespace osu.Game.Extensions
/// The which is handling the repeat.
/// The to schedule repetitions on.
/// The to be invoked once immediately and with every repetition.
+ /// The delay imposed on the first repeat. Defaults to .
/// A which can be cancelled to stop the repeat events from firing.
- public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action)
+ public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action, double initialRepeatDelay = INITIAL_DELAY)
{
action();
- ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + 250, 70);
+ ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + initialRepeatDelay, REPEAT_INTERVAL);
scheduler.Add(repeatDelegate);
return repeatDelegate;
}
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 0e9382279a..67cee883c8 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -346,7 +346,6 @@ namespace osu.Game.Graphics.Backgrounds
/// such that the smaller triangles appear on top.
///
///
- ///
public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale);
}
}
diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index fbf2ffd4bd..e168f265dd 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
+ protected override bool BlockScrollInput => false;
+
protected override bool BlockNonPositionalInput => true;
///
diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs
index 39c1fdad52..4e555ac1eb 100644
--- a/osu.Game/Graphics/Containers/UserDimContainer.cs
+++ b/osu.Game/Graphics/Containers/UserDimContainer.cs
@@ -23,11 +23,6 @@ namespace osu.Game.Graphics.Containers
protected const double BACKGROUND_FADE_DURATION = 800;
- ///
- /// Whether or not user-configured dim levels should be applied to the container.
- ///
- public readonly Bindable EnableUserDim = new Bindable(true);
-
///
/// Whether or not user-configured settings relating to brightness of elements should be ignored
///
@@ -57,7 +52,7 @@ namespace osu.Game.Graphics.Containers
private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0;
- protected float DimLevel => Math.Max(EnableUserDim.Value && !IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0);
+ protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0);
protected override Container Content => dimContent;
@@ -78,7 +73,6 @@ namespace osu.Game.Graphics.Containers
LightenDuringBreaks = config.GetBindable(OsuSetting.LightenDuringBreaks);
ShowStoryboard = config.GetBindable(OsuSetting.ShowStoryboard);
- EnableUserDim.ValueChanged += _ => UpdateVisuals();
UserDimLevel.ValueChanged += _ => UpdateVisuals();
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
IsBreakTime.ValueChanged += _ => UpdateVisuals();
diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs
index 466d59b08b..c3b9b6006c 100644
--- a/osu.Game/Graphics/OsuColour.cs
+++ b/osu.Game/Graphics/OsuColour.cs
@@ -186,6 +186,13 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
+ // in latest editor design logic, need to figure out where these sit...
+ public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
+ public readonly Color4 Orange1 = Color4Extensions.FromHex(@"ffd966");
+
+ // Content Background
+ public readonly Color4 B5 = Color4Extensions.FromHex(@"222a28");
+
public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded");
public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787");
public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121");
diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs
index ba188963ea..c8d5ce39a6 100644
--- a/osu.Game/IO/Serialization/IJsonSerializable.cs
+++ b/osu.Game/IO/Serialization/IJsonSerializable.cs
@@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization
///
/// Creates the default that should be used for all s.
///
- ///
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 671c3bc8bc..e414e12dd1 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -70,6 +70,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
+ new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
};
public IEnumerable InGameKeyBindings => new[]
@@ -97,9 +98,7 @@ namespace osu.Game.Input.Bindings
public IEnumerable AudioControlKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
- new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
- new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
@@ -249,5 +248,8 @@ namespace osu.Game.Input.Bindings
[Description("Beatmap Options")]
ToggleBeatmapOptions,
+
+ [Description("Verify mode")]
+ EditorVerifyMode,
}
}
diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs
index fba1bee0b8..cd76000f98 100644
--- a/osu.Game/Input/Handlers/ReplayInputHandler.cs
+++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs
@@ -32,8 +32,6 @@ namespace osu.Game.Input.Handlers
public override bool Initialize(GameHost host) => true;
- public override bool IsActive => true;
-
public class ReplayState : IInput
where T : struct
{
diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs
index b25b00eb84..9d0cfedc03 100644
--- a/osu.Game/Input/KeyBindingStore.cs
+++ b/osu.Game/Input/KeyBindingStore.cs
@@ -85,7 +85,6 @@ namespace osu.Game.Input
///
/// The ruleset's internal ID.
/// An optional variant.
- ///
public List Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
new file mode 100644
index 0000000000..2c100d39b9
--- /dev/null
+++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
@@ -0,0 +1,506 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using osu.Game.Database;
+
+namespace osu.Game.Migrations
+{
+ [DbContext(typeof(OsuDbContext))]
+ [Migration("20210412045700_RefreshVolumeBindingsAgain")]
+ partial class RefreshVolumeBindingsAgain
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("ApproachRate");
+
+ b.Property("CircleSize");
+
+ b.Property("DrainRate");
+
+ b.Property("OverallDifficulty");
+
+ b.Property("SliderMultiplier");
+
+ b.Property("SliderTickRate");
+
+ b.HasKey("ID");
+
+ b.ToTable("BeatmapDifficulty");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("AudioLeadIn");
+
+ b.Property("BPM");
+
+ b.Property("BaseDifficultyID");
+
+ b.Property("BeatDivisor");
+
+ b.Property("BeatmapSetInfoID");
+
+ b.Property("Countdown");
+
+ b.Property("DistanceSpacing");
+
+ b.Property("GridSize");
+
+ b.Property("Hash");
+
+ b.Property("Hidden");
+
+ b.Property("Length");
+
+ b.Property("LetterboxInBreaks");
+
+ b.Property("MD5Hash");
+
+ b.Property("MetadataID");
+
+ b.Property("OnlineBeatmapID");
+
+ b.Property("Path");
+
+ b.Property("RulesetID");
+
+ b.Property("SpecialStyle");
+
+ b.Property("StackLeniency");
+
+ b.Property("StarDifficulty");
+
+ b.Property("Status");
+
+ b.Property("StoredBookmarks");
+
+ b.Property("TimelineZoom");
+
+ b.Property("Version");
+
+ b.Property("WidescreenStoryboard");
+
+ b.HasKey("ID");
+
+ b.HasIndex("BaseDifficultyID");
+
+ b.HasIndex("BeatmapSetInfoID");
+
+ b.HasIndex("Hash");
+
+ b.HasIndex("MD5Hash");
+
+ b.HasIndex("MetadataID");
+
+ b.HasIndex("OnlineBeatmapID")
+ .IsUnique();
+
+ b.HasIndex("RulesetID");
+
+ b.ToTable("BeatmapInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Artist");
+
+ b.Property("ArtistUnicode");
+
+ b.Property("AudioFile");
+
+ b.Property("AuthorString")
+ .HasColumnName("Author");
+
+ b.Property("BackgroundFile");
+
+ b.Property("PreviewTime");
+
+ b.Property("Source");
+
+ b.Property("Tags");
+
+ b.Property("Title");
+
+ b.Property("TitleUnicode");
+
+ b.Property("VideoFile");
+
+ b.HasKey("ID");
+
+ b.ToTable("BeatmapMetadata");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("BeatmapSetInfoID");
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.HasKey("ID");
+
+ b.HasIndex("BeatmapSetInfoID");
+
+ b.HasIndex("FileInfoID");
+
+ b.ToTable("BeatmapSetFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("DateAdded");
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("MetadataID");
+
+ b.Property("OnlineBeatmapSetID");
+
+ b.Property("Protected");
+
+ b.Property("Status");
+
+ b.HasKey("ID");
+
+ b.HasIndex("DeletePending");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.HasIndex("MetadataID");
+
+ b.HasIndex("OnlineBeatmapSetID")
+ .IsUnique();
+
+ b.ToTable("BeatmapSetInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Key")
+ .HasColumnName("Key");
+
+ b.Property("RulesetID");
+
+ b.Property("SkinInfoID");
+
+ b.Property("StringValue")
+ .HasColumnName("Value");
+
+ b.Property("Variant");
+
+ b.HasKey("ID");
+
+ b.HasIndex("SkinInfoID");
+
+ b.HasIndex("RulesetID", "Variant");
+
+ b.ToTable("Settings");
+ });
+
+ modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Hash");
+
+ b.Property("ReferenceCount");
+
+ b.HasKey("ID");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.HasIndex("ReferenceCount");
+
+ b.ToTable("FileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("IntAction")
+ .HasColumnName("Action");
+
+ b.Property("KeysString")
+ .HasColumnName("Keys");
+
+ b.Property("RulesetID");
+
+ b.Property("Variant");
+
+ b.HasKey("ID");
+
+ b.HasIndex("IntAction");
+
+ b.HasIndex("RulesetID", "Variant");
+
+ b.ToTable("KeyBinding");
+ });
+
+ modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Available");
+
+ b.Property("InstantiationInfo");
+
+ b.Property("Name");
+
+ b.Property("ShortName");
+
+ b.HasKey("ID");
+
+ b.HasIndex("Available");
+
+ b.HasIndex("ShortName")
+ .IsUnique();
+
+ b.ToTable("RulesetInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.Property("ScoreInfoID");
+
+ b.HasKey("ID");
+
+ b.HasIndex("FileInfoID");
+
+ b.HasIndex("ScoreInfoID");
+
+ b.ToTable("ScoreFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Accuracy")
+ .HasColumnType("DECIMAL(1,4)");
+
+ b.Property("BeatmapInfoID");
+
+ b.Property("Combo");
+
+ b.Property("Date");
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("MaxCombo");
+
+ b.Property("ModsJson")
+ .HasColumnName("Mods");
+
+ b.Property("OnlineScoreID");
+
+ b.Property("PP");
+
+ b.Property("Rank");
+
+ b.Property("RulesetID");
+
+ b.Property("StatisticsJson")
+ .HasColumnName("Statistics");
+
+ b.Property("TotalScore");
+
+ b.Property("UserID")
+ .HasColumnName("UserID");
+
+ b.Property("UserString")
+ .HasColumnName("User");
+
+ b.HasKey("ID");
+
+ b.HasIndex("BeatmapInfoID");
+
+ b.HasIndex("OnlineScoreID")
+ .IsUnique();
+
+ b.HasIndex("RulesetID");
+
+ b.ToTable("ScoreInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("FileInfoID");
+
+ b.Property("Filename")
+ .IsRequired();
+
+ b.Property("SkinInfoID");
+
+ b.HasKey("ID");
+
+ b.HasIndex("FileInfoID");
+
+ b.HasIndex("SkinInfoID");
+
+ b.ToTable("SkinFileInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
+ {
+ b.Property("ID")
+ .ValueGeneratedOnAdd();
+
+ b.Property("Creator");
+
+ b.Property("DeletePending");
+
+ b.Property("Hash");
+
+ b.Property("Name");
+
+ b.HasKey("ID");
+
+ b.HasIndex("DeletePending");
+
+ b.HasIndex("Hash")
+ .IsUnique();
+
+ b.ToTable("SkinInfo");
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
+ .WithMany()
+ .HasForeignKey("BaseDifficultyID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
+ .WithMany("Beatmaps")
+ .HasForeignKey("BeatmapSetInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+ .WithMany("Beatmaps")
+ .HasForeignKey("MetadataID");
+
+ b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+ .WithMany()
+ .HasForeignKey("RulesetID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
+ .WithMany("Files")
+ .HasForeignKey("BeatmapSetInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+ .WithMany()
+ .HasForeignKey("FileInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+ .WithMany("BeatmapSets")
+ .HasForeignKey("MetadataID");
+ });
+
+ modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+ {
+ b.HasOne("osu.Game.Skinning.SkinInfo")
+ .WithMany("Settings")
+ .HasForeignKey("SkinInfoID");
+ });
+
+ modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+ {
+ b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+ .WithMany()
+ .HasForeignKey("FileInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Scoring.ScoreInfo")
+ .WithMany("Files")
+ .HasForeignKey("ScoreInfoID");
+ });
+
+ modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+ {
+ b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
+ .WithMany("Scores")
+ .HasForeignKey("BeatmapInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+ .WithMany()
+ .HasForeignKey("RulesetID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+ {
+ b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+ .WithMany()
+ .HasForeignKey("FileInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("osu.Game.Skinning.SkinInfo")
+ .WithMany("Files")
+ .HasForeignKey("SkinInfoID")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs
new file mode 100644
index 0000000000..155d6670a8
--- /dev/null
+++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs
@@ -0,0 +1,16 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace osu.Game.Migrations
+{
+ public partial class RefreshVolumeBindingsAgain : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ }
+ }
+}
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index bff08b0515..4427c82a8b 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -11,11 +11,12 @@ using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
+using osu.Game.Utils;
namespace osu.Game.Online.API
{
[MessagePackObject]
- public class APIMod : IMod
+ public class APIMod : IMod, IEquatable
{
[JsonProperty("acronym")]
[Key(0)]
@@ -63,7 +64,16 @@ namespace osu.Game.Online.API
return resultMod;
}
- public bool Equals(IMod other) => Acronym == other?.Acronym;
+ public bool Equals(IMod other) => other is APIMod them && Equals(them);
+
+ public bool Equals(APIMod other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return Acronym == other.Acronym &&
+ Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
+ }
public override string ToString()
{
@@ -72,5 +82,20 @@ namespace osu.Game.Online.API
return $"{Acronym}";
}
+
+ private class ModSettingsEqualityComparer : IEqualityComparer>
+ {
+ public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
+
+ public bool Equals(KeyValuePair x, KeyValuePair y)
+ {
+ object xValue = ModUtils.GetSettingUnderlyingValue(x.Value);
+ object yValue = ModUtils.GetSettingUnderlyingValue(y.Value);
+
+ return x.Key == y.Key && EqualityComparer
[ExcludeFromDynamicCompile]
- public abstract class Mod : IMod, IJsonSerializable
+ public abstract class Mod : IMod, IEquatable, IJsonSerializable
{
///
/// The name of this mod.
@@ -48,7 +49,7 @@ namespace osu.Game.Rulesets.Mods
/// The user readable description of this mod.
///
[JsonIgnore]
- public virtual string Description => string.Empty;
+ public abstract string Description { get; }
///
/// The tooltip to display for this mod when used in a .
@@ -172,7 +173,19 @@ namespace osu.Game.Rulesets.Mods
target.Parse(source);
}
- public bool Equals(IMod other) => GetType() == other?.GetType();
+ public bool Equals(IMod other) => other is Mod them && Equals(them);
+
+ public bool Equals(Mod other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return GetType() == other.GetType() &&
+ this.GetSettingsSourceProperties().All(pair =>
+ EqualityComparer
/// One of the control points in the segment.
- ///
public List PointsInSegment(PathControlPoint controlPoint)
{
bool found = false;
diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
index 0b41ca31ea..279087ead9 100644
--- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
+++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using JetBrains.Annotations;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@@ -17,80 +16,92 @@ namespace osu.Game.Rulesets.Replays
public abstract class FramedReplayInputHandler : ReplayInputHandler
where TFrame : ReplayFrame
{
- private readonly Replay replay;
+ ///
+ /// Whether we have at least one replay frame.
+ ///
+ public bool HasFrames => Frames.Count != 0;
- protected List Frames => replay.Frames;
+ ///
+ /// Whether we are waiting for new frames to be received.
+ ///
+ public bool WaitingForFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1;
+ ///
+ /// The current frame of the replay.
+ /// The current time is always between the start and the end time of the current frame.
+ ///
+ /// Returns null if the current time is strictly before the first frame.
+ /// The replay is empty.
public TFrame CurrentFrame
{
get
{
- if (!HasFrames || !currentFrameIndex.HasValue)
- return null;
+ if (!HasFrames)
+ throw new InvalidOperationException($"Attempted to get {nameof(CurrentFrame)} of an empty replay");
- return (TFrame)Frames[currentFrameIndex.Value];
+ return currentFrameIndex == -1 ? null : (TFrame)Frames[currentFrameIndex];
}
}
+ ///
+ /// The next frame of the replay.
+ /// The start time is always greater or equal to the start time of regardless of the seeking direction.
+ ///
+ /// Returns null if the current frame is the last frame.
+ /// The replay is empty.
public TFrame NextFrame
{
get
{
if (!HasFrames)
- return null;
+ throw new InvalidOperationException($"Attempted to get {nameof(NextFrame)} of an empty replay");
- if (!currentFrameIndex.HasValue)
- return currentDirection > 0 ? (TFrame)Frames[0] : null;
-
- int nextFrame = clampedNextFrameIndex;
-
- if (nextFrame == currentFrameIndex.Value)
- return null;
-
- return (TFrame)Frames[clampedNextFrameIndex];
+ return currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex + 1];
}
}
- private int? currentFrameIndex;
-
- private int clampedNextFrameIndex =>
- currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + currentDirection, 0, Frames.Count - 1) : 0;
-
- protected FramedReplayInputHandler(Replay replay)
- {
- this.replay = replay;
- }
-
- private const double sixty_frame_time = 1000.0 / 60;
-
- protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
-
- protected double? CurrentTime { get; private set; }
-
- private int currentDirection = 1;
-
///
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
/// Disabling this can make replay playback smoother (useful for autoplay, currently).
///
public bool FrameAccuratePlayback;
- public bool HasFrames => Frames.Count > 0;
+ // This input handler should be enabled only if there is at least one replay frame.
+ public override bool IsActive => HasFrames;
+
+ // Can make it non-null but that is a breaking change.
+ protected double? CurrentTime { get; private set; }
+
+ protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
+
+ protected List Frames => replay.Frames;
+
+ private readonly Replay replay;
+
+ private int currentFrameIndex;
+
+ private const double sixty_frame_time = 1000.0 / 60;
+
+ protected FramedReplayInputHandler(Replay replay)
+ {
+ // TODO: This replay frame ordering should be enforced on the Replay type.
+ // Currently, the ordering can be broken if the frames are added after this construction.
+ replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time));
+
+ this.replay = replay;
+ currentFrameIndex = -1;
+ CurrentTime = double.NegativeInfinity;
+ }
private bool inImportantSection
{
get
{
- if (!HasFrames || !FrameAccuratePlayback)
+ if (!HasFrames || !FrameAccuratePlayback || CurrentFrame == null)
return false;
- var frame = currentDirection > 0 ? CurrentFrame : NextFrame;
-
- if (frame == null)
- return false;
-
- return IsImportant(frame) && // a button is in a pressed state
- Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
+ return IsImportant(CurrentFrame) && // a button is in a pressed state
+ Math.Abs(CurrentTime - NextFrame.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
}
}
@@ -105,71 +116,52 @@ namespace osu.Game.Rulesets.Replays
/// The usable time value. If null, we should not advance time as we do not have enough data.
public override double? SetFrameFromTime(double time)
{
- updateDirection(time);
-
- Debug.Assert(currentDirection != 0);
-
if (!HasFrames)
{
- // in the case all frames are received, allow time to progress regardless.
+ // In the case all frames are received, allow time to progress regardless.
if (replay.HasReceivedAllFrames)
return CurrentTime = time;
return null;
}
- TFrame next = NextFrame;
+ double frameStart = getFrameTime(currentFrameIndex);
+ double frameEnd = getFrameTime(currentFrameIndex + 1);
- // if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so.
- if (next != null)
+ // If the proposed time is after the current frame end time, we progress forwards to precisely the new frame's time (regardless of incoming time).
+ if (frameEnd <= time)
{
- int compare = time.CompareTo(next.Time);
-
- if (compare == 0 || compare == currentDirection)
- {
- currentFrameIndex = clampedNextFrameIndex;
- return CurrentTime = CurrentFrame.Time;
- }
+ time = frameEnd;
+ currentFrameIndex++;
}
+ // If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards.
+ else if (time < frameStart && CurrentTime == frameStart)
+ currentFrameIndex--;
- // at this point, the frame index can't be advanced.
- // even so, we may be able to propose the clock progresses forward due to being at an extent of the replay,
- // or moving towards the next valid frame (ie. interpolating in a non-important section).
+ frameStart = getFrameTime(currentFrameIndex);
+ frameEnd = getFrameTime(currentFrameIndex + 1);
- // the exception is if currently in an important section, which is respected above all.
- if (inImportantSection)
+ // Pause until more frames are arrived.
+ if (WaitingForFrame && frameStart < time)
{
- Debug.Assert(next != null || !replay.HasReceivedAllFrames);
+ CurrentTime = frameStart;
return null;
}
- // if a next frame does exist, allow interpolation.
- if (next != null)
- return CurrentTime = time;
+ CurrentTime = Math.Clamp(time, frameStart, frameEnd);
- // if all frames have been received, allow playing beyond extents.
- if (replay.HasReceivedAllFrames)
- return CurrentTime = time;
-
- // if not all frames are received but we are before the first frame, allow playing.
- if (time < Frames[0].Time)
- return CurrentTime = time;
-
- // in the case we have no next frames and haven't received enough frame data, block.
- return null;
+ // In an important section, a mid-frame time cannot be used and a null is returned instead.
+ return inImportantSection && frameStart < time && time < frameEnd ? null : CurrentTime;
}
- private void updateDirection(double time)
+ private double getFrameTime(int index)
{
- if (!CurrentTime.HasValue)
- {
- currentDirection = 1;
- }
- else
- {
- currentDirection = time.CompareTo(CurrentTime);
- if (currentDirection == 0) currentDirection = 1;
- }
+ if (index < 0)
+ return double.NegativeInfinity;
+ if (index >= Frames.Count)
+ return double.PositiveInfinity;
+
+ return Frames[index].Time;
}
}
}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index 38d30a2e31..7f0c27adfc 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -146,7 +146,6 @@ namespace osu.Game.Rulesets
/// The beatmap to create the hit renderer for.
/// The s to apply.
/// Unable to successfully load the beatmap to be usable with this ruleset.
- ///
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null);
///
@@ -202,6 +201,8 @@ namespace osu.Game.Rulesets
public virtual HitObjectComposer CreateHitObjectComposer() => null;
+ public virtual IBeatmapVerifier CreateBeatmapVerifier() => null;
+
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle };
public virtual IResourceStore CreateResourceStore() => new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), @"Resources");
diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs
index 018b50bd3d..410614de07 100644
--- a/osu.Game/Rulesets/Scoring/HitWindows.cs
+++ b/osu.Game/Rulesets/Scoring/HitWindows.cs
@@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring
///
/// Retrieves a mapping of s to their timing windows for all allowed s.
///
- ///
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
{
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
index 8aef615b5f..201a05e569 100644
--- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Scoring
///
public int JudgedHits { get; private set; }
+ private JudgementResult lastAppliedResult;
+
private readonly BindableBool hasCompleted = new BindableBool();
///
@@ -53,12 +55,11 @@ namespace osu.Game.Rulesets.Scoring
public void ApplyResult(JudgementResult result)
{
JudgedHits++;
+ lastAppliedResult = result;
ApplyResultInternal(result);
NewJudgement?.Invoke(result);
-
- updateHasCompleted();
}
///
@@ -69,8 +70,6 @@ namespace osu.Game.Rulesets.Scoring
{
JudgedHits--;
- updateHasCompleted();
-
RevertResultInternal(result);
}
@@ -134,6 +133,10 @@ namespace osu.Game.Rulesets.Scoring
}
}
- private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits;
+ protected override void Update()
+ {
+ base.Update();
+ hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 || lastAppliedResult.TimeAbsolute < Clock.CurrentTime);
+ }
}
}
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index c40ab4bd94..d55005363c 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.UI
var enumerable = HitObjectContainer.Objects;
- if (nestedPlayfields.IsValueCreated)
+ if (nestedPlayfields.Count != 0)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
return enumerable;
@@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.UI
///
/// All s nested inside this .
///
- public IEnumerable NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty();
+ public IEnumerable NestedPlayfields => nestedPlayfields;
- private readonly Lazy> nestedPlayfields = new Lazy>();
+ private readonly List nestedPlayfields = new List();
///
/// Whether judgements should be displayed by this and and all nested s.
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
- nestedPlayfields.Value.Add(otherPlayfield);
+ nestedPlayfields.Add(otherPlayfield);
}
protected override void LoadComplete()
@@ -279,12 +279,7 @@ namespace osu.Game.Rulesets.UI
return true;
}
- bool removedFromNested = false;
-
- if (nestedPlayfields.IsValueCreated)
- removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject));
-
- return removedFromNested;
+ return nestedPlayfields.Any(p => p.Remove(hitObject));
}
///
@@ -429,10 +424,7 @@ namespace osu.Game.Rulesets.UI
return;
}
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var p in nestedPlayfields.Value)
+ foreach (var p in nestedPlayfields)
p.SetKeepAlive(hitObject, keepAlive);
}
@@ -444,10 +436,7 @@ namespace osu.Game.Rulesets.UI
foreach (var (_, entry) in lifetimeEntryMap)
entry.KeepAlive = true;
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var p in nestedPlayfields.Value)
+ foreach (var p in nestedPlayfields)
p.KeepAllAlive();
}
@@ -461,10 +450,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.PastLifetimeExtension = value;
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var nested in nestedPlayfields.Value)
+ foreach (var nested in nestedPlayfields)
nested.PastLifetimeExtension = value;
}
}
@@ -479,10 +465,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.FutureLifetimeExtension = value;
- if (!nestedPlayfields.IsValueCreated)
- return;
-
- foreach (var nested in nestedPlayfields.Value)
+ foreach (var nested in nestedPlayfields)
nested.FutureLifetimeExtension = value;
}
}
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index d6f002ea2c..5ab09f9516 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Input.StateChanges;
using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States;
using osu.Game.Configuration;
@@ -100,6 +102,17 @@ namespace osu.Game.Rulesets.UI
#endregion
+ // to avoid allocation
+ private readonly List emptyInputList = new List();
+
+ protected override List GetPendingInputs()
+ {
+ if (replayInputHandler?.IsActive == false)
+ return emptyInputList;
+
+ return base.GetPendingInputs();
+ }
+
#region Setting application (disables etc.)
private Bindable mouseDisabled;
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index ef11c19e3f..222f69b025 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
+using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -55,9 +56,10 @@ namespace osu.Game.Scoring
[JsonIgnore]
public virtual RulesetInfo Ruleset { get; set; }
+ private APIMod[] localAPIMods;
private Mod[] mods;
- [JsonProperty("mods")]
+ [JsonIgnore]
[NotMapped]
public Mod[] Mods
{
@@ -66,43 +68,50 @@ namespace osu.Game.Scoring
if (mods != null)
return mods;
- if (modsJson == null)
+ if (localAPIMods == null)
return Array.Empty();
- return getModsFromRuleset(JsonConvert.DeserializeObject(modsJson));
+ var rulesetInstance = Ruleset.CreateInstance();
+ return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
set
{
- modsJson = null;
+ localAPIMods = null;
mods = value;
}
}
- private Mod[] getModsFromRuleset(DeserializedMod[] mods) => Ruleset.CreateInstance().GetAllMods().Where(mod => mods.Any(d => d.Acronym == mod.Acronym)).ToArray();
+ // Used for API serialisation/deserialisation.
+ [JsonProperty("mods")]
+ [NotMapped]
+ private APIMod[] apiMods
+ {
+ get
+ {
+ if (localAPIMods != null)
+ return localAPIMods;
- private string modsJson;
+ if (mods == null)
+ return Array.Empty();
+ return localAPIMods = mods.Select(m => new APIMod(m)).ToArray();
+ }
+ set
+ {
+ localAPIMods = value;
+
+ // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
+ mods = null;
+ }
+ }
+
+ // Used for database serialisation/deserialisation.
[JsonIgnore]
[Column("Mods")]
public string ModsJson
{
- get
- {
- if (modsJson != null)
- return modsJson;
-
- if (mods == null)
- return null;
-
- return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
- }
- set
- {
- modsJson = value;
-
- // we potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
- mods = null;
- }
+ get => JsonConvert.SerializeObject(apiMods);
+ set => apiMods = JsonConvert.DeserializeObject(value);
}
[NotMapped]
@@ -251,14 +260,6 @@ namespace osu.Game.Scoring
}
}
- [Serializable]
- protected class DeserializedMod : IMod
- {
- public string Acronym { get; set; }
-
- public bool Equals(IMod other) => Acronym == other?.Acronym;
- }
-
public override string ToString() => $"{User} playing {Beatmap}";
public bool Equals(ScoreInfo other)
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
index b08455be95..65bc9cfaea 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
@@ -27,9 +27,12 @@ namespace osu.Game.Screens.Backgrounds
private WorkingBeatmap beatmap;
///
- /// Whether or not user dim settings should be applied to this Background.
+ /// Whether or not user-configured settings relating to brightness of elements should be ignored.
///
- public readonly Bindable EnableUserDim = new Bindable();
+ ///
+ /// Beatmap background screens should not apply user settings by default.
+ ///
+ public readonly Bindable IgnoreUserSettings = new Bindable(true);
public readonly Bindable StoryboardReplacesBackground = new Bindable();
@@ -50,7 +53,7 @@ namespace osu.Game.Screens.Backgrounds
InternalChild = dimmable = CreateFadeContainer();
- dimmable.EnableUserDim.BindTo(EnableUserDim);
+ dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
dimmable.IsBreakTime.BindTo(IsBreakTime);
dimmable.BlurAmount.BindTo(BlurAmount);
@@ -148,7 +151,7 @@ namespace osu.Game.Screens.Backgrounds
///
/// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs.
///
- private Vector2 blurTarget => EnableUserDim.Value
+ private Vector2 blurTarget => !IgnoreUserSettings.Value
? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * USER_BLUR_FACTOR)
: new Vector2(BlurAmount.Value);
@@ -166,7 +169,9 @@ namespace osu.Game.Screens.Backgrounds
BlurAmount.ValueChanged += _ => UpdateVisuals();
}
- protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value; // The background needs to be hidden in the case of it being replaced by the storyboard
+ protected override bool ShowDimContent
+ // The background needs to be hidden in the case of it being replaced by the storyboard
+ => (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value;
protected override void UpdateVisuals()
{
diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs
index a7b0fb05e3..dcf5f8a788 100644
--- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs
+++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs
@@ -12,7 +12,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
///
/// Whether this is selected.
///
- ///
public readonly BindableBool Selected;
///
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs
index e8a4b5c8c7..3d535ec915 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours) => Colour = colours.Yellow;
+ private void load(OsuColour colours) => Colour = colours.GreyCarmineLight;
}
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs
new file mode 100644
index 0000000000..a8e41d220a
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointVisualisation.cs
@@ -0,0 +1,30 @@
+// 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.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
+
+namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
+{
+ public class ControlPointVisualisation : PointVisualisation
+ {
+ protected readonly ControlPoint Point;
+
+ public ControlPointVisualisation(ControlPoint point)
+ {
+ Point = point;
+
+ Height = 0.25f;
+ Origin = Anchor.TopCentre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Colour = Point.GetRepresentingColour(colours);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs
new file mode 100644
index 0000000000..801372305b
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/EffectPointVisualisation.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
+
+namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
+{
+ public class EffectPointVisualisation : CompositeDrawable
+ {
+ private readonly EffectControlPoint effect;
+ private Bindable kiai;
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public EffectPointVisualisation(EffectControlPoint point)
+ {
+ RelativePositionAxes = Axes.Both;
+ RelativeSizeAxes = Axes.Y;
+
+ effect = point;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ kiai = effect.KiaiModeBindable.GetBoundCopy();
+ kiai.BindValueChanged(_ =>
+ {
+ ClearInternal();
+
+ AddInternal(new ControlPointVisualisation(effect));
+
+ if (!kiai.Value)
+ return;
+
+ var endControlPoint = beatmap.ControlPointInfo.EffectPoints.FirstOrDefault(c => c.Time > effect.Time && !c.KiaiMode);
+
+ // handle kiai duration
+ // eventually this will be simpler when we have control points with durations.
+ if (endControlPoint != null)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Origin = Anchor.TopLeft;
+
+ Width = (float)(endControlPoint.Time - effect.Time);
+
+ AddInternal(new PointVisualisation
+ {
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.TopLeft,
+ Width = 1,
+ Height = 0.25f,
+ Depth = float.MaxValue,
+ Colour = effect.GetRepresentingColour(colours).Darken(0.5f),
+ });
+ }
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
index 93fe6f9989..4629f9b540 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs
@@ -1,29 +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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
-using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
- public class GroupVisualisation : PointVisualisation
+ public class GroupVisualisation : CompositeDrawable
{
+ [Resolved]
+ private OsuColour colours { get; set; }
+
public readonly ControlPointGroup Group;
private readonly IBindableList controlPoints = new BindableList();
- [Resolved]
- private OsuColour colours { get; set; }
-
public GroupVisualisation(ControlPointGroup group)
- : base(group.Time)
{
+ RelativePositionAxes = Axes.X;
+
+ RelativeSizeAxes = Axes.Both;
+ Origin = Anchor.TopLeft;
+
Group = group;
+ X = (float)group.Time;
}
protected override void LoadComplete()
@@ -33,13 +37,32 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) =>
{
- if (controlPoints.Count == 0)
- {
- Colour = Color4.Transparent;
- return;
- }
+ ClearInternal();
- Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green;
+ if (controlPoints.Count == 0)
+ return;
+
+ foreach (var point in Group.ControlPoints)
+ {
+ switch (point)
+ {
+ case TimingControlPoint _:
+ AddInternal(new ControlPointVisualisation(point) { Y = 0, });
+ break;
+
+ case DifficultyControlPoint _:
+ AddInternal(new ControlPointVisualisation(point) { Y = 0.25f, });
+ break;
+
+ case SampleControlPoint _:
+ AddInternal(new ControlPointVisualisation(point) { Y = 0.5f, });
+ break;
+
+ case EffectControlPoint effect:
+ AddInternal(new EffectPointVisualisation(effect) { Y = 0.75f });
+ break;
+ }
+ }
}, true);
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
index 02cd4bccb4..e90ae411de 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
@@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
+ Y = -10,
Height = 0.35f
},
new BookmarkPart
@@ -38,6 +39,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
},
new Container
{
+ Name = "centre line",
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray5,
Children = new Drawable[]
@@ -45,7 +47,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle
{
Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreRight,
+ Origin = Anchor.Centre,
Size = new Vector2(5)
},
new Box
@@ -59,7 +61,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle
{
Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
Size = new Vector2(5)
},
}
@@ -69,7 +71,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Height = 0.25f
+ Height = 0.10f
}
};
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs
index de63df5463..ec68bf9c00 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
@@ -10,19 +9,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
///
/// Represents a spanning point on a timeline part.
///
- public class DurationVisualisation : Container
+ public class DurationVisualisation : Circle
{
protected DurationVisualisation(double startTime, double endTime)
{
- Masking = true;
- CornerRadius = 5;
-
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
+
X = (float)startTime;
Width = (float)(endTime - startTime);
-
- AddInternal(new Box { RelativeSizeAxes = Axes.Both });
}
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs
index b0ecffdd24..a4b6b0c392 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs
@@ -3,16 +3,15 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
{
///
/// Represents a singular point on a timeline part.
///
- public class PointVisualisation : Box
+ public class PointVisualisation : Circle
{
- public const float WIDTH = 1;
+ public const float MAX_WIDTH = 4;
public PointVisualisation(double startTime)
: this()
@@ -22,13 +21,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
public PointVisualisation()
{
- Origin = Anchor.TopCentre;
-
- RelativePositionAxes = Axes.X;
+ RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Y;
- Width = WIDTH;
- EdgeSmoothness = new Vector2(WIDTH, 0);
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.Centre;
+
+ Width = MAX_WIDTH;
+ Height = 0.75f;
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 5699be4560..b5a28dc022 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -135,11 +135,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnMouseDown(MouseDownEvent e)
{
- if (!beginClickSelection(e)) return true;
+ bool selectionPerformed = performMouseDownActions(e);
+ // even if a selection didn't occur, a drag event may still move the selection.
prepareSelectionMovement();
- return e.Button == MouseButton.Left;
+ return selectionPerformed || e.Button == MouseButton.Left;
}
private SelectionBlueprint clickedBlueprint;
@@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
- if (endClickSelection() || clickedBlueprint != null)
+ if (endClickSelection(e) || clickedBlueprint != null)
return true;
deselectAll();
@@ -177,7 +178,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnMouseUp(MouseUpEvent e)
{
// Special case for when a drag happened instead of a click
- Schedule(() => endClickSelection());
+ Schedule(() =>
+ {
+ endClickSelection(e);
+ clickSelectionBegan = false;
+ isDraggingBlueprint = false;
+ });
finishSelectionMovement();
}
@@ -226,7 +232,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
Beatmap.Update(obj);
changeHandler?.EndChange();
- isDraggingBlueprint = false;
}
if (DragBox.State == Visibility.Visible)
@@ -338,7 +343,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// The input event that triggered this selection.
/// Whether a selection was performed.
- private bool beginClickSelection(MouseButtonEvent e)
+ private bool performMouseDownActions(MouseButtonEvent e)
{
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints.
@@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
if (!blueprint.IsHovered) continue;
- return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e);
+ return clickSelectionBegan = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
}
return false;
@@ -355,13 +360,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// Finishes the current blueprint selection.
///
+ /// The mouse event which triggered end of selection.
/// Whether a click selection was active.
- private bool endClickSelection()
+ private bool endClickSelection(MouseButtonEvent e)
{
- if (!clickSelectionBegan)
- return false;
+ if (!clickSelectionBegan && !isDraggingBlueprint)
+ {
+ // if a selection didn't occur, we may want to trigger a deselection.
+ if (e.ControlPressed && e.Button == MouseButton.Left)
+ {
+ // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
+ // Priority is given to already-selected blueprints.
+ foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
+ {
+ if (!blueprint.IsHovered) continue;
+
+ return clickSelectionBegan = SelectionHandler.MouseUpSelectionRequested(blueprint, e);
+ }
+ }
+
+ return false;
+ }
- clickSelectionBegan = false;
return true;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 018d4d081c..389ef78ed5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -220,20 +220,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// The blueprint.
/// The mouse event responsible for selection.
/// Whether a selection was performed.
- internal bool HandleSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
+ internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
{
if (e.ShiftPressed && e.Button == MouseButton.Right)
{
handleQuickDeletion(blueprint);
- return false;
+ return true;
}
- if (e.ControlPressed && e.Button == MouseButton.Left)
+ // while holding control, we only want to add to selection, not replace an existing selection.
+ if (e.ControlPressed && e.Button == MouseButton.Left && !blueprint.IsSelected)
+ {
blueprint.ToggleSelection();
- else
- ensureSelected(blueprint);
+ return true;
+ }
- return true;
+ return ensureSelected(blueprint);
+ }
+
+ ///
+ /// Handle a blueprint requesting selection.
+ ///
+ /// The blueprint.
+ /// The mouse event responsible for deselection.
+ /// Whether a deselection was performed.
+ internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
+ {
+ if (blueprint.IsSelected)
+ {
+ blueprint.ToggleSelection();
+ return true;
+ }
+
+ return false;
}
private void handleQuickDeletion(SelectionBlueprint blueprint)
@@ -247,13 +266,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
deleteSelected();
}
- private void ensureSelected(SelectionBlueprint blueprint)
+ ///
+ /// Ensure the blueprint is in a selected state.
+ ///
+ /// The blueprint to select.
+ /// Whether selection state was changed.
+ private bool ensureSelected(SelectionBlueprint blueprint)
{
if (blueprint.IsSelected)
- return;
+ return false;
DeselectAll?.Invoke();
blueprint.Select();
+ return true;
}
private void deleteSelected()
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
index 510ba8c094..3248936765 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs
@@ -1,67 +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 osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class DifficultyPointPiece : CompositeDrawable
+ public class DifficultyPointPiece : TopPointPiece
{
- private readonly DifficultyControlPoint difficultyPoint;
-
- private OsuSpriteText speedMultiplierText;
private readonly BindableNumber speedMultiplier;
- public DifficultyPointPiece(DifficultyControlPoint difficultyPoint)
+ public DifficultyPointPiece(DifficultyControlPoint point)
+ : base(point)
{
- this.difficultyPoint = difficultyPoint;
- speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy();
+ speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy();
+
+ Y = Height;
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ protected override void LoadComplete()
{
- RelativeSizeAxes = Axes.Y;
- AutoSizeAxes = Axes.X;
-
- Color4 colour = difficultyPoint.GetRepresentingColour(colours);
-
- InternalChildren = new Drawable[]
- {
- new Box
- {
- Colour = colour,
- Width = 2,
- RelativeSizeAxes = Axes.Y,
- },
- new Container
- {
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = colour,
- RelativeSizeAxes = Axes.Both,
- },
- speedMultiplierText = new OsuSpriteText
- {
- Font = OsuFont.Default.With(weight: FontWeight.Bold),
- Colour = Color4.White,
- }
- }
- },
- };
-
- speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true);
+ base.LoadComplete();
+ speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true);
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 0f11fb1126..9461f5e885 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -3,9 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
@@ -23,7 +21,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly BindableNumber volume;
private OsuSpriteText text;
- private Box volumeBox;
+ private Container volumeBox;
+
+ private const int max_volume_height = 22;
public SamplePointPiece(SampleControlPoint samplePoint)
{
@@ -35,8 +35,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- Origin = Anchor.TopLeft;
- Anchor = Anchor.TopLeft;
+ Margin = new MarginPadding { Vertical = 5 };
+
+ Origin = Anchor.BottomCentre;
+ Anchor = Anchor.BottomCentre;
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
@@ -45,40 +47,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
InternalChildren = new Drawable[]
{
+ volumeBox = new Circle
+ {
+ CornerRadius = 5,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Y = -20,
+ Width = 10,
+ Colour = colour,
+ },
new Container
{
- RelativeSizeAxes = Axes.Y,
- Width = 20,
+ AutoSizeAxes = Axes.X,
+ Height = 16,
+ Masking = true,
+ CornerRadius = 8,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
Children = new Drawable[]
{
- volumeBox = new Box
- {
- X = 2,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Colour = ColourInfo.GradientVertical(colour, Color4.Black),
- RelativeSizeAxes = Axes.Both,
- },
new Box
{
- Colour = colour.Lighten(0.2f),
- Width = 2,
- RelativeSizeAxes = Axes.Y,
+ Colour = colour,
+ RelativeSizeAxes = Axes.Both,
},
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Padding = new MarginPadding(5),
+ Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
+ Colour = colours.B5,
+ }
}
},
- text = new OsuSpriteText
- {
- X = 2,
- Y = -5,
- Anchor = Anchor.BottomLeft,
- Alpha = 0.9f,
- Rotation = -90,
- Font = OsuFont.Default.With(weight: FontWeight.SemiBold)
- }
};
- volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true);
+ volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true);
bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 86a30b7e2d..55fb557474 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -23,6 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Cached]
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{
+ private readonly Drawable userContent;
public readonly Bindable WaveformVisible = new Bindable();
public readonly Bindable ControlPointsVisible = new Bindable();
@@ -56,8 +57,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Track track;
- public Timeline()
+ private const float timeline_height = 72;
+ private const float timeline_expanded_height = 156;
+
+ public Timeline(Drawable userContent)
{
+ this.userContent = userContent;
+
+ RelativeSizeAxes = Axes.X;
+
ZoomDuration = 200;
ZoomEasing = Easing.OutQuint;
ScrollbarVisible = false;
@@ -69,18 +77,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private TimelineControlPointDisplay controlPoints;
+ private Container mainContent;
+
private Bindable waveformOpacity;
[BackgroundDependencyLoader]
private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config)
{
+ CentreMarker centreMarker;
+
+ // We don't want the centre marker to scroll
+ AddInternal(centreMarker = new CentreMarker());
+
AddRange(new Drawable[]
{
- new Container
+ controlPoints = new TimelineControlPointDisplay
{
- RelativeSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.X,
+ Height = timeline_expanded_height,
+ },
+ mainContent = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = timeline_height,
Depth = float.MaxValue,
- Children = new Drawable[]
+ Children = new[]
{
waveform = new WaveformGraph
{
@@ -90,8 +111,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,
},
+ centreMarker.CreateProxy(),
ticks = new TimelineTickDisplay(),
- controlPoints = new TimelineControlPointDisplay(),
new Box
{
Name = "zero marker",
@@ -100,21 +121,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.TopCentre,
Colour = colours.YellowDarker,
},
+ userContent,
}
},
});
- // We don't want the centre marker to scroll
- AddInternal(new CentreMarker { Depth = float.MaxValue });
-
waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity);
+ Beatmap.BindTo(beatmap);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true);
WaveformVisible.ValueChanged += _ => updateWaveformOpacity();
- ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
+ ControlPointsVisible.BindValueChanged(visible =>
+ {
+ if (visible.NewValue)
+ {
+ this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
+ mainContent.MoveToY(36, 200, Easing.OutQuint);
+
+ // delay the fade in else masking looks weird.
+ controlPoints.Delay(180).FadeIn(400, Easing.OutQuint);
+ }
+ else
+ {
+ controlPoints.FadeOut(200, Easing.OutQuint);
+
+ // likewise, delay the resize until the fade is complete.
+ this.Delay(180).ResizeHeightTo(timeline_height, 200, Easing.OutQuint);
+ mainContent.Delay(180).MoveToY(0, 200, Easing.OutQuint);
+ }
+ }, true);
- Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b =>
{
waveform.Waveform = b.NewValue.Waveform;
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
index 0ec48e04c6..1541ceade5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
@@ -12,11 +12,19 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class TimelineArea : Container
+ public class TimelineArea : CompositeDrawable
{
- public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both };
+ public Timeline Timeline;
- protected override Container Content => Timeline;
+ private readonly Drawable userContent;
+
+ public TimelineArea(Drawable content = null)
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ userContent = content ?? Drawable.Empty();
+ }
[BackgroundDependencyLoader]
private void load()
@@ -37,7 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
new GridContainer
{
- RelativeSizeAxes = Axes.Both,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
@@ -55,11 +64,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
new FillFlowContainer
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
Width = 160,
- Padding = new MarginPadding { Horizontal = 10 },
+ Padding = new MarginPadding(10),
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new[]
@@ -123,14 +130,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
},
- Timeline
+ Timeline = new Timeline(userContent),
},
},
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
- new Dimension(GridSizeMode.Distributed),
+ new Dimension(),
}
}
};
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index be34c8d57e..7a3781a981 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -63,7 +63,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
AddInternal(backgroundBox = new SelectableAreaBackground
{
- Colour = Color4.Black
+ Colour = Color4.Black,
+ Depth = float.MaxValue,
+ Blending = BlendingParameters.Additive,
});
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs
index 18600bcdee..8520567fa9 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs
@@ -4,7 +4,6 @@
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@@ -17,11 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
private readonly IBindableList controlPointGroups = new BindableList();
- public TimelineControlPointDisplay()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
protected override void LoadBeatmap(EditorBeatmap beatmap)
{
base.LoadBeatmap(beatmap);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
index fb69f16792..c4beb40f92 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
+ Origin = Anchor.TopCentre;
+
X = (float)group.Time;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index d24614299c..23069f6079 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -16,7 +17,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
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.Types;
@@ -28,9 +28,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineHitObjectBlueprint : SelectionBlueprint
{
- private const float thickness = 5;
- private const float shadow_radius = 5;
- private const float circle_size = 34;
+ private const float circle_size = 38;
+
+ private Container repeatsContainer;
public Action OnDragHandled;
@@ -40,10 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable indexInCurrentComboBindable;
private Bindable comboIndexBindable;
- private readonly Circle circle;
- private readonly DragBar dragBar;
- private readonly List shadowComponents = new List();
- private readonly Container mainComponents;
+ private readonly Drawable circle;
+
+ private readonly Container colouredComponents;
private readonly OsuSpriteText comboIndexText;
[Resolved]
@@ -61,89 +60,41 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
+ Height = circle_size;
- AddRangeInternal(new Drawable[]
+ AddRangeInternal(new[]
{
- mainComponents = new Container
+ circle = new ExtendableCircle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ colouredComponents = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- },
- comboIndexText = new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.Centre,
- Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ comboIndexText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
+ Y = -1,
+ Font = OsuFont.Default.With(size: circle_size * 0.5f, weight: FontWeight.Regular),
+ },
+ }
},
});
- circle = new Circle
- {
- Size = new Vector2(circle_size),
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.Centre,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = shadow_radius,
- Colour = Color4.Black
- },
- };
-
- shadowComponents.Add(circle);
-
if (hitObject is IHasDuration)
{
- DragBar dragBarUnderlay;
- Container extensionBar;
-
- mainComponents.AddRange(new Drawable[]
+ colouredComponents.Add(new DragArea(hitObject)
{
- extensionBar = new Container
- {
- Masking = true,
- Size = new Vector2(1, thickness),
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- RelativePositionAxes = Axes.X,
- RelativeSizeAxes = Axes.X,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = shadow_radius,
- Colour = Color4.Black
- },
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- }
- },
- circle,
- // only used for drawing the shadow
- dragBarUnderlay = new DragBar(null),
- // cover up the shadow on the join
- new Box
- {
- Height = thickness,
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- RelativeSizeAxes = Axes.X,
- },
- dragBar = new DragBar(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) },
+ OnDragHandled = e => OnDragHandled?.Invoke(e)
});
-
- shadowComponents.Add(dragBarUnderlay);
- shadowComponents.Add(extensionBar);
}
- else
- {
- mainComponents.Add(circle);
- }
-
- updateShadows();
}
protected override void LoadComplete()
@@ -162,6 +113,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
+ protected override void OnSelected()
+ {
+ // base logic hides selected blueprints when not selected, but timeline doesn't do that.
+ }
+
+ protected override void OnDeselected()
+ {
+ // base logic hides selected blueprints when not selected, but timeline doesn't do that.
+ }
+
private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
private void updateComboColour()
@@ -173,15 +134,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var comboColour = combo.GetComboColour(comboColours);
if (HitObject is IHasDuration)
- mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White);
+ circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
else
- mainComponents.Colour = comboColour;
+ circle.Colour = comboColour;
- var col = mainComponents.Colour.TopLeft.Linear;
+ var col = circle.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;
+ colouredComponents.Colour = OsuColour.Gray(brightness > 0.5f ? 0.2f : 0.9f);
}
protected override void Update()
@@ -201,13 +162,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
- private Container repeatsContainer;
-
private void updateRepeats(IHasRepeats repeats)
{
repeatsContainer?.Expire();
- mainComponents.Add(repeatsContainer = new Container
+ colouredComponents.Add(repeatsContainer = new Container
{
RelativeSizeAxes = Axes.Both,
});
@@ -216,7 +175,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
repeatsContainer.Add(new Circle
{
- Size = new Vector2(circle_size / 2),
+ Size = new Vector2(circle_size / 3),
+ Alpha = 0.2f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
@@ -228,61 +188,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
- base.ReceivePositionalInputAt(screenSpacePos) ||
- circle.ReceivePositionalInputAt(screenSpacePos) ||
- dragBar?.ReceivePositionalInputAt(screenSpacePos) == true;
+ circle.ReceivePositionalInputAt(screenSpacePos);
- protected override void OnSelected()
- {
- updateShadows();
- }
-
- private void updateShadows()
- {
- foreach (var s in shadowComponents)
- {
- if (State == SelectionState.Selected)
- {
- s.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = shadow_radius / 2,
- Colour = Color4.Orange,
- };
- }
- else
- {
- s.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = shadow_radius,
- Colour = State == SelectionState.Selected ? Color4.Orange : Color4.Black
- };
- }
- }
- }
-
- protected override void OnDeselected()
- {
- updateShadows();
- }
-
- public override Quad SelectionQuad
- {
- get
- {
- // correctly include the circle in the selection quad region, as it is usually outside the blueprint itself.
- var leftQuad = circle.ScreenSpaceDrawQuad;
- var rightQuad = dragBar?.ScreenSpaceDrawQuad ?? ScreenSpaceDrawQuad;
-
- return new Quad(leftQuad.TopLeft, Vector2.ComponentMax(rightQuad.TopRight, leftQuad.TopRight),
- leftQuad.BottomLeft, Vector2.ComponentMax(rightQuad.BottomRight, leftQuad.BottomRight));
- }
- }
+ public override Quad SelectionQuad => circle.ScreenSpaceDrawQuad;
public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
- public class DragBar : Container
+ public class DragArea : Circle
{
private readonly HitObject hitObject;
@@ -293,13 +205,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public override bool HandlePositionalInput => hitObject != null;
- public DragBar(HitObject hitObject)
+ public DragArea(HitObject hitObject)
{
this.hitObject = hitObject;
- CornerRadius = 2;
+ CornerRadius = circle_size / 2;
Masking = true;
- Size = new Vector2(5, 1);
+ Size = new Vector2(circle_size, 1);
Anchor = Anchor.CentreRight;
Origin = Anchor.Centre;
RelativePositionAxes = Axes.X;
@@ -314,6 +226,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ updateState();
+ FinishTransforms();
+ }
+
protected override bool OnHover(HoverEvent e)
{
updateState();
@@ -345,7 +265,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateState()
{
- Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White;
+ if (hasMouseDown)
+ {
+ this.ScaleTo(0.7f, 200, Easing.OutQuint);
+ }
+ else if (IsHovered)
+ {
+ this.ScaleTo(0.8f, 200, Easing.OutQuint);
+ }
+ else
+ {
+ this.ScaleTo(0.6f, 200, Easing.OutQuint);
+ }
+
+ this.FadeTo(IsHovered || hasMouseDown ? 0.8f : 0.2f, 200, Easing.OutQuint);
}
[Resolved]
@@ -406,5 +339,32 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
changeHandler?.EndChange();
}
}
+
+ ///
+ /// A circle with externalised end caps so it can take up the full width of a relative width area.
+ ///
+ public class ExtendableCircle : CompositeDrawable
+ {
+ private readonly Circle content;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos);
+
+ public override Quad ScreenSpaceDrawQuad => content.ScreenSpaceDrawQuad;
+
+ public ExtendableCircle()
+ {
+ Padding = new MarginPadding { Horizontal = -circle_size / 2f };
+ InternalChild = content = new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Radius = 5,
+ Colour = Color4.Black.Opacity(0.4f)
+ }
+ };
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
index c070c833f8..3aaf0451c8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
@@ -6,9 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private OsuColour colours { get; set; }
+ private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last();
+
public TimelineTickDisplay()
{
RelativeSizeAxes = Axes.Both;
@@ -80,8 +80,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (timeline != null)
{
var newRange = (
- (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
- (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
+ (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
+ (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
@@ -100,7 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void createTicks()
{
int drawableIndex = 0;
- int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
nextMinTick = null;
nextMaxTick = null;
@@ -131,25 +130,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
- bool isMainBeat = indexInBar == 0;
-
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
- float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f;
- float gradientOpacity = isMainBeat ? 1 : 0;
- var topPoint = getNextUsablePoint();
- topPoint.X = xPos;
- topPoint.Height = height;
- topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity));
- topPoint.Anchor = Anchor.TopLeft;
- topPoint.Origin = Anchor.TopCentre;
-
- var bottomPoint = getNextUsablePoint();
- bottomPoint.X = xPos;
- bottomPoint.Anchor = Anchor.BottomLeft;
- bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour);
- bottomPoint.Origin = Anchor.BottomCentre;
- bottomPoint.Height = height;
+ var line = getNextUsableLine();
+ line.X = xPos;
+ line.Width = PointVisualisation.MAX_WIDTH * getWidth(indexInBar, divisor);
+ line.Height = 0.9f * getHeight(indexInBar, divisor);
+ line.Colour = colour;
}
beat++;
@@ -168,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
tickCache.Validate();
- Drawable getNextUsablePoint()
+ Drawable getNextUsableLine()
{
PointVisualisation point;
if (drawableIndex >= Count)
@@ -183,6 +170,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
+ private static float getWidth(int indexInBar, int divisor)
+ {
+ if (indexInBar == 0)
+ return 1;
+
+ switch (divisor)
+ {
+ case 1:
+ case 2:
+ return 0.6f;
+
+ case 3:
+ case 4:
+ return 0.5f;
+
+ case 6:
+ case 8:
+ return 0.4f;
+
+ default:
+ return 0.3f;
+ }
+ }
+
+ private static float getHeight(int indexInBar, int divisor)
+ {
+ if (indexInBar == 0)
+ return 1;
+
+ switch (divisor)
+ {
+ case 1:
+ case 2:
+ return 0.9f;
+
+ case 3:
+ case 4:
+ return 0.8f;
+
+ case 6:
+ case 8:
+ return 0.7f;
+
+ default:
+ return 0.6f;
+ }
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs
index ba94916458..fa51281c55 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs
@@ -3,60 +3,27 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class TimingPointPiece : CompositeDrawable
+ public class TimingPointPiece : TopPointPiece
{
- private readonly TimingControlPoint point;
-
private readonly BindableNumber beatLength;
- private OsuSpriteText bpmText;
public TimingPointPiece(TimingControlPoint point)
+ : base(point)
{
- this.point = point;
beatLength = point.BeatLengthBindable.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- Origin = Anchor.CentreLeft;
- Anchor = Anchor.CentreLeft;
-
- AutoSizeAxes = Axes.Both;
-
- Color4 colour = point.GetRepresentingColour(colours);
-
- InternalChildren = new Drawable[]
- {
- new Box
- {
- Alpha = 0.9f,
- Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)),
- RelativeSizeAxes = Axes.Both,
- },
- bpmText = new OsuSpriteText
- {
- Alpha = 0.9f,
- Padding = new MarginPadding(3),
- Font = OsuFont.Default.With(size: 40)
- }
- };
-
beatLength.BindValueChanged(beatLength =>
{
- bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM";
+ Label.Text = $"{60000 / beatLength.NewValue:n1} BPM";
}, true);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs
new file mode 100644
index 0000000000..60a9e1ed66
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TopPointPiece.cs
@@ -0,0 +1,55 @@
+// 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.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Screens.Edit.Compose.Components.Timeline
+{
+ public class TopPointPiece : CompositeDrawable
+ {
+ private readonly ControlPoint point;
+
+ protected OsuSpriteText Label { get; private set; }
+
+ public TopPointPiece(ControlPoint point)
+ {
+ this.point = point;
+ AutoSizeAxes = Axes.X;
+ Height = 16;
+ Margin = new MarginPadding(4);
+
+ Masking = true;
+ CornerRadius = Height / 2;
+
+ Origin = Anchor.TopCentre;
+ Anchor = Anchor.TopCentre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = point.GetRepresentingColour(colours),
+ RelativeSizeAxes = Axes.Both,
+ },
+ Label = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Padding = new MarginPadding(3),
+ Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold),
+ Colour = colours.B5,
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 0759e21382..fffea65456 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -35,6 +35,7 @@ using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing;
+using osu.Game.Screens.Edit.Verify;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK.Graphics;
@@ -444,6 +445,10 @@ namespace osu.Game.Screens.Edit
menuBar.Mode.Value = EditorScreenMode.SongSetup;
return true;
+ case GlobalAction.EditorVerifyMode:
+ menuBar.Mode.Value = EditorScreenMode.Verify;
+ return true;
+
default:
return false;
}
@@ -462,7 +467,7 @@ namespace osu.Game.Screens.Edit
// todo: temporary. we want to be applying dim using the UserDimContainer eventually.
b.FadeColour(Color4.DarkGray, 500);
- b.EnableUserDim.Value = false;
+ b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = 0;
});
@@ -631,6 +636,10 @@ namespace osu.Game.Screens.Edit
case EditorScreenMode.Timing:
currentScreen = new TimingScreen();
break;
+
+ case EditorScreenMode.Verify:
+ currentScreen = new VerifyScreen();
+ break;
}
LoadComponentAsync(currentScreen, newScreen =>
diff --git a/osu.Game/Screens/Edit/EditorScreenMode.cs b/osu.Game/Screens/Edit/EditorScreenMode.cs
index 12cfcc605b..ecd39f9b57 100644
--- a/osu.Game/Screens/Edit/EditorScreenMode.cs
+++ b/osu.Game/Screens/Edit/EditorScreenMode.cs
@@ -18,5 +18,8 @@ namespace osu.Game.Screens.Edit
[Description("timing")]
Timing,
+
+ [Description("verify")]
+ Verify,
}
}
diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
index 2d623a200c..0d59a7a1a8 100644
--- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
+++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs
@@ -19,8 +19,6 @@ namespace osu.Game.Screens.Edit
private const float vertical_margins = 10;
private const float horizontal_margins = 20;
- private const float timeline_height = 110;
-
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private Container timelineContainer;
@@ -40,64 +38,87 @@ namespace osu.Game.Screens.Edit
if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor);
- Children = new Drawable[]
+ Child = new GridContainer
{
- mainContent = new Container
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
{
- Name = "Main content",
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Horizontal = horizontal_margins,
- Top = vertical_margins + timeline_height,
- Bottom = vertical_margins
- },
- Child = spinner = new LoadingSpinner(true)
- {
- State = { Value = Visibility.Visible },
- },
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(),
},
- new Container
+ Content = new[]
{
- Name = "Timeline",
- RelativeSizeAxes = Axes.X,
- Height = timeline_height,
- Children = new Drawable[]
+ new Drawable[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black.Opacity(0.5f)
- },
new Container
{
- Name = "Timeline content",
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins },
- Child = new GridContainer
+ Name = "Timeline",
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Content = new[]
+ new Box
{
- new Drawable[]
- {
- timelineContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Right = 5 },
- },
- new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
- },
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black.Opacity(0.5f)
},
- ColumnDimensions = new[]
+ new Container
{
- new Dimension(),
- new Dimension(GridSizeMode.Absolute, 90),
+ Name = "Timeline content",
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins },
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ timelineContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Right = 5 },
+ },
+ new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
+ },
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 90),
+ }
+ },
}
+ }
+ },
+ },
+ new Drawable[]
+ {
+ mainContent = new Container
+ {
+ Name = "Main content",
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue,
+ Padding = new MarginPadding
+ {
+ Horizontal = horizontal_margins,
+ Top = vertical_margins,
+ Bottom = vertical_margins
},
- }
- }
- },
+ Child = spinner = new LoadingSpinner(true)
+ {
+ State = { Value = Visibility.Visible },
+ },
+ },
+ },
+ }
};
}
@@ -112,14 +133,7 @@ namespace osu.Game.Screens.Edit
mainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint);
- LoadComponentAsync(new TimelineArea
- {
- RelativeSizeAxes = Axes.Both,
- Children = new[]
- {
- CreateTimelineContent(),
- }
- }, t =>
+ LoadComponentAsync(new TimelineArea(CreateTimelineContent()), t =>
{
timelineContainer.Add(t);
OnTimelineLoaded(t);
diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs
new file mode 100644
index 0000000000..ef1c88db9a
--- /dev/null
+++ b/osu.Game/Screens/Edit/EditorTable.cs
@@ -0,0 +1,140 @@
+// 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.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit
+{
+ public abstract class EditorTable : TableContainer
+ {
+ private const float horizontal_inset = 20;
+
+ protected const float ROW_HEIGHT = 25;
+
+ protected const int TEXT_SIZE = 14;
+
+ protected readonly FillFlowContainer BackgroundFlow;
+
+ protected EditorTable()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Padding = new MarginPadding { Horizontal = horizontal_inset };
+ RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT);
+
+ AddInternal(BackgroundFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 1f,
+ Padding = new MarginPadding { Horizontal = -horizontal_inset },
+ Margin = new MarginPadding { Top = ROW_HEIGHT }
+ });
+ }
+
+ protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
+
+ private class HeaderText : OsuSpriteText
+ {
+ public HeaderText(string text)
+ {
+ Text = text.ToUpper();
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
+ }
+ }
+
+ public class RowBackground : OsuClickableContainer
+ {
+ public readonly object Item;
+
+ private const int fade_duration = 100;
+
+ private readonly Box hoveredBackground;
+
+ [Resolved]
+ private EditorClock clock { get; set; }
+
+ public RowBackground(object item)
+ {
+ Item = item;
+
+ RelativeSizeAxes = Axes.X;
+ Height = 25;
+
+ AlwaysPresent = true;
+
+ CornerRadius = 3;
+ Masking = true;
+
+ Children = new Drawable[]
+ {
+ hoveredBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ },
+ };
+
+ // todo delete
+ Action = () =>
+ {
+ };
+ }
+
+ private Color4 colourHover;
+ private Color4 colourSelected;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ hoveredBackground.Colour = colourHover = colours.BlueDarker;
+ colourSelected = colours.YellowDarker;
+ }
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (value == selected)
+ return;
+
+ selected = value;
+ updateState();
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateState();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ private void updateState()
+ {
+ hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
+
+ if (selected || IsHovered)
+ hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
+ else
+ hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
index a17b431fcc..dd51056bf1 100644
--- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
+++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs
@@ -8,59 +8,43 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Timing
{
- public class ControlPointTable : TableContainer
+ public class ControlPointTable : EditorTable
{
- private const float horizontal_inset = 20;
- private const float row_height = 25;
- private const int text_size = 14;
-
- private readonly FillFlowContainer backgroundFlow;
-
[Resolved]
private Bindable selectedGroup { get; set; }
- public ControlPointTable()
- {
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- Padding = new MarginPadding { Horizontal = horizontal_inset };
- RowSize = new Dimension(GridSizeMode.Absolute, row_height);
-
- AddInternal(backgroundFlow = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 1f,
- Padding = new MarginPadding { Horizontal = -horizontal_inset },
- Margin = new MarginPadding { Top = row_height }
- });
- }
+ [Resolved]
+ private EditorClock clock { get; set; }
public IEnumerable ControlGroups
{
set
{
Content = null;
- backgroundFlow.Clear();
+ BackgroundFlow.Clear();
if (value?.Any() != true)
return;
foreach (var group in value)
{
- backgroundFlow.Add(new RowBackground(group));
+ BackgroundFlow.Add(new RowBackground(group)
+ {
+ Action = () =>
+ {
+ selectedGroup.Value = group;
+ clock.SeekSmoothlyTo(group.Time);
+ }
+ });
}
Columns = createHeaders();
@@ -68,6 +52,16 @@ namespace osu.Game.Screens.Edit.Timing
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedGroup.BindValueChanged(group =>
+ {
+ foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
+ }, true);
+ }
+
private TableColumn[] createHeaders()
{
var columns = new List
@@ -86,13 +80,13 @@ namespace osu.Game.Screens.Edit.Timing
new OsuSpriteText
{
Text = $"#{index + 1}",
- Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding(10)
},
new OsuSpriteText
{
Text = group.Time.ToEditorFormattedString(),
- Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold)
},
null,
new ControlGroupAttributes(group),
@@ -163,111 +157,5 @@ namespace osu.Game.Screens.Edit.Timing
return null;
}
}
-
- protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
-
- private class HeaderText : OsuSpriteText
- {
- public HeaderText(string text)
- {
- Text = text.ToUpper();
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
- }
- }
-
- public class RowBackground : OsuClickableContainer
- {
- private readonly ControlPointGroup controlGroup;
- private const int fade_duration = 100;
-
- private readonly Box hoveredBackground;
-
- [Resolved]
- private EditorClock clock { get; set; }
-
- [Resolved]
- private Bindable selectedGroup { get; set; }
-
- public RowBackground(ControlPointGroup controlGroup)
- {
- this.controlGroup = controlGroup;
- RelativeSizeAxes = Axes.X;
- Height = 25;
-
- AlwaysPresent = true;
-
- CornerRadius = 3;
- Masking = true;
-
- Children = new Drawable[]
- {
- hoveredBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- },
- };
-
- Action = () =>
- {
- selectedGroup.Value = controlGroup;
- clock.SeekSmoothlyTo(controlGroup.Time);
- };
- }
-
- private Color4 colourHover;
- private Color4 colourSelected;
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- hoveredBackground.Colour = colourHover = colours.BlueDarker;
- colourSelected = colours.YellowDarker;
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- selectedGroup.BindValueChanged(group => { Selected = controlGroup == group.NewValue; }, true);
- }
-
- private bool selected;
-
- protected bool Selected
- {
- get => selected;
- set
- {
- if (value == selected)
- return;
-
- selected = value;
- updateState();
- }
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- updateState();
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- updateState();
- base.OnHoverLost(e);
- }
-
- private void updateState()
- {
- hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
-
- if (selected || IsHovered)
- hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
- else
- hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
- }
- }
}
}
diff --git a/osu.Game/Screens/Edit/Verify/IssueSettings.cs b/osu.Game/Screens/Edit/Verify/IssueSettings.cs
new file mode 100644
index 0000000000..4519231cd2
--- /dev/null
+++ b/osu.Game/Screens/Edit/Verify/IssueSettings.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Screens.Edit.Verify
+{
+ public class IssueSettings : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray3,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = createSections()
+ },
+ }
+ };
+ }
+
+ private IReadOnlyList createSections() => new Drawable[]
+ {
+ };
+ }
+}
diff --git a/osu.Game/Screens/Edit/Verify/IssueTable.cs b/osu.Game/Screens/Edit/Verify/IssueTable.cs
new file mode 100644
index 0000000000..44244028c9
--- /dev/null
+++ b/osu.Game/Screens/Edit/Verify/IssueTable.cs
@@ -0,0 +1,128 @@
+// 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Screens.Edit.Verify
+{
+ public class IssueTable : EditorTable
+ {
+ [Resolved]
+ private Bindable selectedIssue { get; set; }
+
+ [Resolved]
+ private EditorClock clock { get; set; }
+
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; }
+
+ [Resolved]
+ private Editor editor { get; set; }
+
+ public IEnumerable Issues
+ {
+ set
+ {
+ Content = null;
+ BackgroundFlow.Clear();
+
+ if (value == null)
+ return;
+
+ foreach (var issue in value)
+ {
+ BackgroundFlow.Add(new RowBackground(issue)
+ {
+ Action = () =>
+ {
+ selectedIssue.Value = issue;
+
+ if (issue.Time != null)
+ {
+ clock.Seek(issue.Time.Value);
+ editor.OnPressed(GlobalAction.EditorComposeMode);
+ }
+
+ if (!issue.HitObjects.Any())
+ return;
+
+ editorBeatmap.SelectedHitObjects.Clear();
+ editorBeatmap.SelectedHitObjects.AddRange(issue.HitObjects);
+ },
+ });
+ }
+
+ Columns = createHeaders();
+ Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ selectedIssue.BindValueChanged(issue =>
+ {
+ foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue;
+ }, true);
+ }
+
+ private TableColumn[] createHeaders()
+ {
+ var columns = new List
+ {
+ new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
+ new TableColumn("Type", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
+ new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
+ new TableColumn("Message", Anchor.CentreLeft),
+ new TableColumn("Category", Anchor.CentreRight, new Dimension(GridSizeMode.AutoSize)),
+ };
+
+ return columns.ToArray();
+ }
+
+ private Drawable[] createContent(int index, Issue issue) => new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = $"#{index + 1}",
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),
+ Margin = new MarginPadding { Right = 10 }
+ },
+ new OsuSpriteText
+ {
+ Text = issue.Template.Type.ToString(),
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
+ Margin = new MarginPadding { Right = 10 },
+ Colour = issue.Template.Colour
+ },
+ new OsuSpriteText
+ {
+ Text = issue.GetEditorTimestamp(),
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
+ Margin = new MarginPadding { Right = 10 },
+ },
+ new OsuSpriteText
+ {
+ Text = issue.ToString(),
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium)
+ },
+ new OsuSpriteText
+ {
+ Text = issue.Check.Metadata.Category.ToString(),
+ Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
+ Margin = new MarginPadding(10)
+ }
+ };
+ }
+}
diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs
new file mode 100644
index 0000000000..550fbe2950
--- /dev/null
+++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs
@@ -0,0 +1,133 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osuTK;
+
+namespace osu.Game.Screens.Edit.Verify
+{
+ public class VerifyScreen : EditorScreen
+ {
+ [Cached]
+ private Bindable selectedIssue = new Bindable();
+
+ public VerifyScreen()
+ : base(EditorScreenMode.Verify)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(20),
+ Child = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, 200),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new IssueList(),
+ new IssueSettings(),
+ },
+ }
+ }
+ };
+ }
+
+ public class IssueList : CompositeDrawable
+ {
+ private IssueTable table;
+
+ [Resolved]
+ private EditorClock clock { get; set; }
+
+ [Resolved]
+ protected EditorBeatmap Beatmap { get; private set; }
+
+ [Resolved]
+ private Bindable selectedIssue { get; set; }
+
+ private IBeatmapVerifier rulesetVerifier;
+ private BeatmapVerifier generalVerifier;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ generalVerifier = new BeatmapVerifier();
+ rulesetVerifier = Beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.Gray0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = table = new IssueTable(),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding(20),
+ Children = new Drawable[]
+ {
+ new TriangleButton
+ {
+ Text = "Refresh",
+ Action = refresh,
+ Size = new Vector2(120, 40),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ }
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ refresh();
+ }
+
+ private void refresh()
+ {
+ var issues = generalVerifier.Run(Beatmap);
+
+ if (rulesetVerifier != null)
+ issues = issues.Concat(rulesetVerifier.Run(Beatmap));
+
+ table.Issues = issues
+ .OrderBy(issue => issue.Template.Type)
+ .ThenBy(issue => issue.Check.Metadata.Category);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs
index ea3951fc3b..5699da740c 100644
--- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs
@@ -19,6 +19,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected OnlinePlayComposite Settings { get; set; }
+ protected override bool BlockScrollInput => false;
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs
new file mode 100644
index 0000000000..1b9e2bda2d
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs
@@ -0,0 +1,72 @@
+// 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.Timing;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
+{
+ public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard
+ {
+ public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
+ : base(scoreProcessor, userIds)
+ {
+ }
+
+ public void AddClock(int userId, IClock clock)
+ {
+ if (!UserScores.TryGetValue(userId, out var data))
+ return;
+
+ ((SpectatingTrackedUserData)data).Clock = clock;
+ }
+
+ public void RemoveClock(int userId)
+ {
+ if (!UserScores.TryGetValue(userId, out var data))
+ return;
+
+ ((SpectatingTrackedUserData)data).Clock = null;
+ }
+
+ protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor);
+
+ protected override void Update()
+ {
+ base.Update();
+
+ foreach (var (_, data) in UserScores)
+ data.UpdateScore();
+ }
+
+ private class SpectatingTrackedUserData : TrackedUserData
+ {
+ [CanBeNull]
+ public IClock Clock;
+
+ public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor)
+ : base(userId, scoreProcessor)
+ {
+ }
+
+ public override void UpdateScore()
+ {
+ if (Frames.Count == 0)
+ return;
+
+ if (Clock == null)
+ return;
+
+ int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime));
+ if (frameIndex < 0)
+ frameIndex = ~frameIndex;
+ frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1);
+
+ SetFrame(Frames[frameIndex]);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs
new file mode 100644
index 0000000000..830378f129
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs
@@ -0,0 +1,167 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osuTK;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
+{
+ ///
+ /// A grid of players playing the multiplayer match.
+ ///
+ public partial class PlayerGrid : CompositeDrawable
+ {
+ private const float player_spacing = 5;
+
+ ///
+ /// The currently-maximised facade.
+ ///
+ public Drawable MaximisedFacade => maximisedFacade;
+
+ private readonly Facade maximisedFacade;
+ private readonly Container paddingContainer;
+ private readonly FillFlowContainer facadeContainer;
+ private readonly Container cellContainer;
+
+ public PlayerGrid()
+ {
+ InternalChildren = new Drawable[]
+ {
+ paddingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(player_spacing),
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = facadeContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(player_spacing),
+ }
+ },
+ maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both }
+ }
+ },
+ cellContainer = new Container { RelativeSizeAxes = Axes.Both }
+ };
+ }
+
+ ///
+ /// Adds a new cell with content to this grid.
+ ///
+ /// The content the cell should contain.
+ /// If more than 16 cells are added.
+ public void Add(Drawable content)
+ {
+ if (cellContainer.Count == 16)
+ throw new InvalidOperationException("Only 16 cells are supported.");
+
+ int index = cellContainer.Count;
+
+ var facade = new Facade();
+ facadeContainer.Add(facade);
+
+ var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState };
+ cell.SetFacade(facade);
+
+ cellContainer.Add(cell);
+ }
+
+ ///
+ /// The content added to this grid.
+ ///
+ public IEnumerable Content => cellContainer.OrderBy(c => c.FacadeIndex).Select(c => c.Content);
+
+ // A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps.
+ private float maximisedInstanceDepth;
+
+ private void toggleMaximisationState(Cell target)
+ {
+ // Iterate through all cells to ensure only one is maximised at any time.
+ foreach (var i in cellContainer.ToList())
+ {
+ if (i == target)
+ i.IsMaximised = !i.IsMaximised;
+ else
+ i.IsMaximised = false;
+
+ if (i.IsMaximised)
+ {
+ // Transfer cell to the maximised facade.
+ i.SetFacade(maximisedFacade);
+ cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f);
+ }
+ else
+ {
+ // Transfer cell back to its original facade.
+ i.SetFacade(facadeContainer[i.FacadeIndex]);
+ }
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Different layouts are used for varying cell counts in order to maximise dimensions.
+ Vector2 cellsPerDimension;
+
+ switch (facadeContainer.Count)
+ {
+ case 1:
+ cellsPerDimension = Vector2.One;
+ break;
+
+ case 2:
+ cellsPerDimension = new Vector2(2, 1);
+ break;
+
+ case 3:
+ case 4:
+ cellsPerDimension = new Vector2(2);
+ break;
+
+ case 5:
+ case 6:
+ cellsPerDimension = new Vector2(3, 2);
+ break;
+
+ case 7:
+ case 8:
+ case 9:
+ // 3 rows / 3 cols.
+ cellsPerDimension = new Vector2(3);
+ break;
+
+ case 10:
+ case 11:
+ case 12:
+ // 3 rows / 4 cols.
+ cellsPerDimension = new Vector2(4, 3);
+ break;
+
+ default:
+ // 4 rows / 4 cols.
+ cellsPerDimension = new Vector2(4);
+ break;
+ }
+
+ // Total inter-cell spacing.
+ Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One);
+
+ Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing;
+ Vector2 cellSize = Vector2.Divide(fullSize, new Vector2(cellsPerDimension.X, cellsPerDimension.Y));
+
+ foreach (var cell in facadeContainer)
+ cell.Size = cellSize;
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs
new file mode 100644
index 0000000000..2df05cb5ed
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs
@@ -0,0 +1,99 @@
+// 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.Framework.Input.Events;
+using osuTK;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
+{
+ public partial class PlayerGrid
+ {
+ ///
+ /// A cell of the grid. Contains the content and tracks to the linked facade.
+ ///
+ private class Cell : CompositeDrawable
+ {
+ ///
+ /// The index of the original facade of this cell.
+ ///
+ public readonly int FacadeIndex;
+
+ ///
+ /// The contained content.
+ ///
+ public readonly Drawable Content;
+
+ ///
+ /// An action that toggles the maximisation state of this cell.
+ ///
+ public Action ToggleMaximisationState;
+
+ ///
+ /// Whether this cell is currently maximised.
+ ///
+ public bool IsMaximised;
+
+ private Facade facade;
+ private bool isTracking = true;
+
+ public Cell(int facadeIndex, Drawable content)
+ {
+ FacadeIndex = facadeIndex;
+
+ Origin = Anchor.Centre;
+ InternalChild = Content = content;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (isTracking)
+ {
+ Position = getFinalPosition();
+ Size = getFinalSize();
+ }
+ }
+
+ ///
+ /// Makes this cell track a new facade.
+ ///
+ public void SetFacade([NotNull] Facade newFacade)
+ {
+ Facade lastFacade = facade;
+ facade = newFacade;
+
+ if (lastFacade == null || lastFacade == newFacade)
+ return;
+
+ isTracking = false;
+
+ this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint)
+ .Then()
+ .OnComplete(_ =>
+ {
+ if (facade == newFacade)
+ isTracking = true;
+ });
+ }
+
+ private Vector2 getFinalPosition()
+ {
+ var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero));
+ return topLeft + facade.DrawSize / 2;
+ }
+
+ private Vector2 getFinalSize() => facade.DrawSize;
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ ToggleMaximisationState(this);
+ return true;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs
new file mode 100644
index 0000000000..6b363c6040
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.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;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
+{
+ public partial class PlayerGrid
+ {
+ ///
+ /// A facade of the grid which is used as a dummy object to store the required position/size of cells.
+ ///
+ private class Facade : Drawable
+ {
+ public Facade()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs
index f938839be3..4a28da0dde 100644
--- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs
+++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Screens.Play
protected override bool BlockNonPositionalInput => true;
+ protected override bool BlockScrollInput => false;
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public Action OnRetry;
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index a3d27c4e71..70de067784 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -1,10 +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 System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@@ -19,9 +19,7 @@ namespace osu.Game.Screens.Play.HUD
[LongRunningLoad]
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
{
- private readonly ScoreProcessor scoreProcessor;
-
- private readonly Dictionary userScores = new Dictionary();
+ protected readonly Dictionary UserScores = new Dictionary();
[Resolved]
private SpectatorStreamingClient streamingClient { get; set; }
@@ -32,9 +30,9 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private UserLookupCache userLookupCache { get; set; }
- private Bindable scoringMode;
-
+ private readonly ScoreProcessor scoreProcessor;
private readonly BindableList playingUsers;
+ private Bindable scoringMode;
///
/// Construct a new leaderboard.
@@ -53,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
+ scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
+
foreach (var userId in playingUsers)
{
streamingClient.WatchUser(userId);
@@ -60,19 +60,17 @@ namespace osu.Game.Screens.Play.HUD
// probably won't be required in the final implementation.
var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
- var trackedUser = new TrackedUserData();
+ var trackedUser = CreateUserData(userId, scoreProcessor);
+ trackedUser.ScoringMode.BindTo(scoringMode);
- userScores[userId] = trackedUser;
var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id);
+ leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
+ leaderboardScore.TotalScore.BindTo(trackedUser.Score);
+ leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
+ leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
- ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
- ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
- ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
- ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit);
+ UserScores[userId] = trackedUser;
}
-
- scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
- scoringMode.BindValueChanged(updateAllScores, true);
}
protected override void LoadComplete()
@@ -102,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD
{
streamingClient.StopWatchingUser(userId);
- if (userScores.TryGetValue(userId, out var trackedData))
+ if (UserScores.TryGetValue(userId, out var trackedData))
trackedData.MarkUserQuit();
}
@@ -110,20 +108,16 @@ namespace osu.Game.Screens.Play.HUD
}
}
- private void updateAllScores(ValueChangedEvent mode)
+ private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() =>
{
- foreach (var trackedData in userScores.Values)
- trackedData.UpdateScore(scoreProcessor, mode.NewValue);
- }
+ if (!UserScores.TryGetValue(userId, out var trackedData))
+ return;
- private void handleIncomingFrames(int userId, FrameDataBundle bundle)
- {
- if (userScores.TryGetValue(userId, out var trackedData))
- {
- trackedData.LastHeader = bundle.Header;
- trackedData.UpdateScore(scoreProcessor, scoringMode.Value);
- }
- }
+ trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
+ trackedData.UpdateScore();
+ });
+
+ protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor);
protected override void Dispose(bool isDisposing)
{
@@ -140,38 +134,65 @@ namespace osu.Game.Screens.Play.HUD
}
}
- private class TrackedUserData
+ protected class TrackedUserData
{
- public IBindableNumber Score => score;
+ public readonly int UserId;
+ public readonly ScoreProcessor ScoreProcessor;
- private readonly BindableDouble score = new BindableDouble();
+ public readonly BindableDouble Score = new BindableDouble();
+ public readonly BindableDouble Accuracy = new BindableDouble(1);
+ public readonly BindableInt CurrentCombo = new BindableInt();
+ public readonly BindableBool UserQuit = new BindableBool();
- public IBindableNumber Accuracy => accuracy;
+ public readonly IBindable ScoringMode = new Bindable();
- private readonly BindableDouble accuracy = new BindableDouble(1);
+ public readonly List Frames = new List();
- public IBindableNumber CurrentCombo => currentCombo;
-
- private readonly BindableInt currentCombo = new BindableInt();
-
- public IBindable UserQuit => userQuit;
-
- private readonly BindableBool userQuit = new BindableBool();
-
- [CanBeNull]
- public FrameHeader LastHeader;
-
- public void MarkUserQuit() => userQuit.Value = true;
-
- public void UpdateScore(ScoreProcessor processor, ScoringMode mode)
+ public TrackedUserData(int userId, ScoreProcessor scoreProcessor)
{
- if (LastHeader == null)
+ UserId = userId;
+ ScoreProcessor = scoreProcessor;
+
+ ScoringMode.BindValueChanged(_ => UpdateScore());
+ }
+
+ public void MarkUserQuit() => UserQuit.Value = true;
+
+ public virtual void UpdateScore()
+ {
+ if (Frames.Count == 0)
return;
- score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics);
- accuracy.Value = LastHeader.Accuracy;
- currentCombo.Value = LastHeader.Combo;
+ SetFrame(Frames.Last());
}
+
+ protected void SetFrame(TimedFrame frame)
+ {
+ var header = frame.Header;
+
+ Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics);
+ Accuracy.Value = header.Accuracy;
+ CurrentCombo.Value = header.Combo;
+ }
+ }
+
+ protected class TimedFrame : IComparable
+ {
+ public readonly double Time;
+ public readonly FrameHeader Header;
+
+ public TimedFrame(double time)
+ {
+ Time = time;
+ }
+
+ public TimedFrame(double time, FrameHeader header)
+ {
+ Time = time;
+ Header = header;
+ }
+
+ public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
}
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 3dffab8102..669c920017 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -24,9 +24,9 @@ namespace osu.Game.Screens.Play
[Cached]
public class HUDOverlay : Container, IKeyBindingHandler
{
- public const float FADE_DURATION = 400;
+ public const float FADE_DURATION = 300;
- public const Easing FADE_EASING = Easing.Out;
+ public const Easing FADE_EASING = Easing.OutQuint;
///
/// The total height of all the top of screen scoring elements.
@@ -74,7 +74,7 @@ namespace osu.Game.Screens.Play
private bool holdingForHUD;
- private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
+ private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods)
{
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 13820738c7..14ba9e7e02 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -798,7 +798,7 @@ namespace osu.Game.Screens.Play
ApplyToBackground(b =>
{
- b.EnableUserDim.Value = true;
+ b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0;
// bind component bindables.
@@ -947,7 +947,7 @@ namespace osu.Game.Screens.Play
float fadeOutDuration = instant ? 0 : 250;
this.FadeOut(fadeOutDuration);
- ApplyToBackground(b => b.EnableUserDim.Value = false);
+ ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
storyboardReplacesBackground.Value = false;
}
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 679b3c7313..ce580e2b53 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -24,6 +24,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Users;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -112,6 +113,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private AudioManager audioManager { get; set; }
+ [Resolved(CanBeNull = true)]
+ private BatteryInfo batteryInfo { get; set; }
+
public PlayerLoader(Func createPlayer)
{
this.createPlayer = createPlayer;
@@ -121,6 +125,7 @@ namespace osu.Game.Screens.Play
private void load(SessionStatics sessionStatics)
{
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
+ batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer
{
@@ -196,6 +201,7 @@ namespace osu.Game.Screens.Play
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
showMuteWarningIfNeeded();
+ showBatteryWarningIfNeeded();
}
public override void OnResuming(IScreen last)
@@ -229,7 +235,7 @@ namespace osu.Game.Screens.Play
content.ScaleTo(0.7f, 150, Easing.InQuint);
this.FadeOut(150);
- ApplyToBackground(b => b.EnableUserDim.Value = false);
+ ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
@@ -277,7 +283,7 @@ namespace osu.Game.Screens.Play
// Preview user-defined background dim and blur when hovered on the visual settings panel.
ApplyToBackground(b =>
{
- b.EnableUserDim.Value = true;
+ b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0;
});
@@ -288,7 +294,7 @@ namespace osu.Game.Screens.Play
ApplyToBackground(b =>
{
// Returns background dim and blur to the values specified by PlayerLoader.
- b.EnableUserDim.Value = false;
+ b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = BACKGROUND_BLUR;
});
@@ -470,5 +476,48 @@ namespace osu.Game.Screens.Play
}
#endregion
+
+ #region Low battery warning
+
+ private Bindable batteryWarningShownOnce;
+
+ private void showBatteryWarningIfNeeded()
+ {
+ if (batteryInfo == null) return;
+
+ if (!batteryWarningShownOnce.Value)
+ {
+ if (!batteryInfo.IsCharging && batteryInfo.ChargeLevel <= 0.25)
+ {
+ notificationOverlay?.Post(new BatteryWarningNotification());
+ batteryWarningShownOnce.Value = true;
+ }
+ }
+ }
+
+ private class BatteryWarningNotification : SimpleNotification
+ {
+ public override bool IsImportant => true;
+
+ public BatteryWarningNotification()
+ {
+ Text = "Your battery level is low! Charge your device to prevent interruptions during gameplay.";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, NotificationOverlay notificationOverlay)
+ {
+ Icon = FontAwesome.Solid.BatteryQuarter;
+ IconBackgound.Colour = colours.RedDark;
+
+ Activated = delegate
+ {
+ notificationOverlay.Hide();
+ return true;
+ };
+ }
+ }
+
+ #endregion
}
}
diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs
index acf4640aa4..6c7cb9376c 100644
--- a/osu.Game/Screens/Play/SongProgress.cs
+++ b/osu.Game/Screens/Play/SongProgress.cs
@@ -45,6 +45,8 @@ namespace osu.Game.Screens.Play
public override bool HandleNonPositionalInput => AllowSeeking.Value;
public override bool HandlePositionalInput => AllowSeeking.Value;
+ protected override bool BlockScrollInput => false;
+
private double firstHitTime => objects.First().StartTime;
private IEnumerable objects;
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index d22199447d..b64d835c58 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -109,7 +109,12 @@ namespace osu.Game.Screens.Play
request.Success += s =>
{
- score.ScoreInfo.OnlineScoreID = s.ID;
+ // For the time being, online ID responses are not really useful for anything.
+ // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
+ //
+ // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
+ // conflicts across various systems (ie. solo and multiplayer).
+ // score.ScoreInfo.OnlineScoreID = s.ID;
tcs.SetResult(true);
};
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 77b3d8fc3b..441c9e048a 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -226,7 +226,6 @@ namespace osu.Game.Screens.Ranking
///
/// Enumerates all s contained in this .
///
- ///
public IEnumerable GetScorePanels() => flow.Select(t => t.Panel);
///
diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
index fcf20a2eb2..5ef2458919 100644
--- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
@@ -189,7 +189,6 @@ namespace osu.Game.Tests.Beatmaps
///
/// Creates the applicable to this .
///
- ///
protected abstract Ruleset CreateRuleset();
private class ConvertResult
diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs
index 76f97db59f..54a83f4305 100644
--- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs
@@ -17,7 +17,6 @@ namespace osu.Game.Tests.Beatmaps
///
/// Creates the whose legacy mod conversion is to be tested.
///
- ///
protected abstract Ruleset CreateRuleset();
protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
diff --git a/osu.Game/Utils/BatteryInfo.cs b/osu.Game/Utils/BatteryInfo.cs
new file mode 100644
index 0000000000..dd9b695e1f
--- /dev/null
+++ b/osu.Game/Utils/BatteryInfo.cs
@@ -0,0 +1,18 @@
+// 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.Utils
+{
+ ///
+ /// Provides access to the system's power status.
+ ///
+ public abstract class BatteryInfo
+ {
+ ///
+ /// The charge level of the battery, from 0 to 1.
+ ///
+ public abstract double ChargeLevel { get; }
+
+ public abstract bool IsCharging { get; }
+ }
+}
diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs
index c12b5a9fd4..596880f2e7 100644
--- a/osu.Game/Utils/ModUtils.cs
+++ b/osu.Game/Utils/ModUtils.cs
@@ -3,8 +3,11 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using osu.Framework.Bindables;
+using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
#nullable enable
@@ -129,5 +132,38 @@ namespace osu.Game.Utils
else
yield return mod;
}
+
+ ///
+ /// Returns the underlying value of the given mod setting object.
+ /// Used in for serialization and equality comparison purposes.
+ ///
+ /// The mod setting.
+ public static object GetSettingUnderlyingValue(object setting)
+ {
+ switch (setting)
+ {
+ case Bindable d:
+ return d.Value;
+
+ case Bindable i:
+ return i.Value;
+
+ case Bindable f:
+ return f.Value;
+
+ case Bindable b:
+ return b.Value;
+
+ case IBindable u:
+ // A mod with unknown (e.g. enum) generic type.
+ var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value));
+ Debug.Assert(valueMethod != null);
+ return valueMethod.GetValue(u);
+
+ default:
+ // fall back for non-bindable cases.
+ return setting;
+ }
+ }
}
}
diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs
index 9f8a1c2e62..fdb7623be5 100644
--- a/osu.Game/Utils/Optional.cs
+++ b/osu.Game/Utils/Optional.cs
@@ -37,7 +37,6 @@ namespace osu.Game.Utils
/// 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/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 71a6f0e5cd..b5405f6262 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.1
Library
@@ -29,8 +29,8 @@
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index a389cc13dd..09f6033bfe 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -93,7 +93,7 @@
-
+
diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs
index 5125ad81e0..702aef45f5 100644
--- a/osu.iOS/OsuGameIOS.cs
+++ b/osu.iOS/OsuGameIOS.cs
@@ -5,6 +5,8 @@ using System;
using Foundation;
using osu.Game;
using osu.Game.Updater;
+using osu.Game.Utils;
+using Xamarin.Essentials;
namespace osu.iOS
{
@@ -13,5 +15,14 @@ namespace osu.iOS
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
+
+ protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
+
+ private class IOSBatteryInfo : BatteryInfo
+ {
+ public override double ChargeLevel => Battery.ChargeLevel;
+
+ public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
+ }
}
}
diff --git a/osu.iOS/osu.iOS.csproj b/osu.iOS/osu.iOS.csproj
index 1e9a21865d..1cbe4422cc 100644
--- a/osu.iOS/osu.iOS.csproj
+++ b/osu.iOS/osu.iOS.csproj
@@ -116,5 +116,8 @@
false
+
+
+
| | |