diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed3e99cb61..29cbdd2d37 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,7 @@ jobs:
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
+ timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index 381d2d49c5..e0ccd50989 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -21,6 +21,7 @@ jobs:
- { prettyname: macOS }
- { prettyname: Linux }
threadingMode: ['SingleThread', 'MultiThreaded']
+ timeout-minutes: 5
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2
diff --git a/.idea/.idea.osu.Desktop/.idea/dataSources.xml b/.idea/.idea.osu.Desktop/.idea/dataSources.xml
deleted file mode 100644
index 10f8c1c84d..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/dataSources.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- sqlite.xerial
- true
- org.sqlite.JDBC
- jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db
-
-
-
-
-
-
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 3c4380e355..c845d7f276 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
new file mode 100644
index 0000000000..161c685043
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneEditor : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 900691ecae..1ad45d2f13 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -6,8 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
@@ -31,10 +31,23 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
- private Container droppedObjectContainer;
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ private readonly Container trailContainer;
private TestCatcher catcher;
+ public TestSceneCatcher()
+ {
+ Add(trailContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Depth = -1
+ });
+ Add(droppedObjectContainer = new DroppedObjectContainer());
+ }
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -43,20 +56,13 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
- var trailContainer = new Container();
- droppedObjectContainer = new Container();
- catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
+ if (catcher != null)
+ Remove(catcher);
- Child = new Container
+ Add(catcher = new TestCatcher(trailContainer, difficulty)
{
- Anchor = Anchor.Centre,
- Children = new Drawable[]
- {
- trailContainer,
- droppedObjectContainer,
- catcher
- }
- };
+ Anchor = Anchor.Centre
+ });
});
[Test]
@@ -293,8 +299,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable CaughtObjects => this.ChildrenOfType();
- public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
- : base(trailsTarget, droppedObjectTarget, difficulty)
+ public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
+ : base(trailsTarget, difficulty)
{
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 4af5098451..877e115e2f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
@@ -97,18 +96,12 @@ namespace osu.Game.Rulesets.Catch.Tests
SetContents(_ =>
{
- var droppedObjectContainer = new Container
- {
- RelativeSizeAxes = Axes.Both
- };
-
return new CatchInputManager(catchRuleset)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- droppedObjectContainer,
- new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
+ new TestCatcherArea(beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
@@ -126,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
- public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
- : base(droppedObjectContainer, beatmapDifficulty)
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
+ : base(beatmapDifficulty)
{
+ AddInternal(droppedObjectContainer = new DroppedObjectContainer());
}
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 683a776dcc..7fa981d492 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () =>
{
- Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
+ Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
{
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
+ Origin = Anchor.Centre
}, skin);
});
@@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
}
+
+ private class TestCatcherArea : CatcherArea
+ {
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ public TestCatcherArea()
+ {
+ Scale = new Vector2(4f);
+
+ AddInternal(droppedObjectContainer = new DroppedObjectContainer());
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index fac5d03833..3a5322ce82 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public const int RNG_SEED = 1337;
+ public bool HardRockOffsets { get; set; }
+
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
- public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
+ public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
- bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
case Fruit fruit:
- if (shouldApplyHardRockOffset)
+ if (HardRockOffsets)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index ec4c5dfe4c..76863acc78 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
+using osu.Game.Rulesets.Edit;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@@ -182,5 +184,7 @@ namespace osu.Game.Rulesets.Catch
public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
+
+ public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
new file mode 100644
index 0000000000..31075db7d1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class BananaShowerCompositionTool : HitObjectCompositionTool
+ {
+ public BananaShowerCompositionTool()
+ : base(nameof(BananaShower))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
new file mode 100644
index 0000000000..6dea8b0712
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
@@ -0,0 +1,73 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly TimeSpanOutline outline;
+
+ public BananaShowerPlacementBlueprint()
+ {
+ InternalChild = outline = new TimeSpanOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ if (e.Button != MouseButton.Left) break;
+
+ BeginPlacement(true);
+ return true;
+
+ case PlacementState.Active:
+ if (e.Button != MouseButton.Right) break;
+
+ // If the duration is negative, swap the start and the end time to make the duration positive.
+ if (HitObject.Duration < 0)
+ {
+ HitObject.StartTime = HitObject.EndTime;
+ HitObject.Duration = -HitObject.Duration;
+ }
+
+ EndPlacement(HitObject.Duration > 0);
+ return true;
+ }
+
+ return base.OnMouseDown(e);
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ if (!(result.Time is double time)) return;
+
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ HitObject.StartTime = time;
+ break;
+
+ case PlacementState.Active:
+ HitObject.EndTime = time;
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
new file mode 100644
index 0000000000..9132b1a9e8
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public BananaShowerSelectionBlueprint(BananaShower hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
new file mode 100644
index 0000000000..69054e2c81
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
@@ -0,0 +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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class CatchPlacementBlueprint : PlacementBlueprint
+ where THitObject : CatchHitObject, new()
+ {
+ protected new THitObject HitObject => (THitObject)base.HitObject;
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public CatchPlacementBlueprint()
+ : base(new THitObject())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
new file mode 100644
index 0000000000..298f9474b0
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint
+ where THitObject : CatchHitObject
+ {
+ public override Vector2 ScreenSpaceSelectionPoint
+ {
+ get
+ {
+ float x = HitObject.OriginalX;
+ float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
+ }
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ protected CatchSelectionBlueprint(THitObject hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
new file mode 100644
index 0000000000..8769acc382
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class FruitOutline : CompositeDrawable
+ {
+ public FruitOutline()
+ {
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.Centre;
+ Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
+ InternalChild = new BorderPiece();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ Colour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
+ {
+ X = hitObject.EffectiveX;
+ Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ Scale = new Vector2(hitObject.Scale);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
new file mode 100644
index 0000000000..65dfce0493
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class TimeSpanOutline : CompositeDrawable
+ {
+ private const float border_width = 4;
+
+ private const float opacity_when_empty = 0.5f;
+
+ private bool isEmpty = true;
+
+ public TimeSpanOutline()
+ {
+ Anchor = Origin = Anchor.BottomLeft;
+ RelativeSizeAxes = Axes.X;
+
+ Masking = true;
+ BorderThickness = border_width;
+ Alpha = opacity_when_empty;
+
+ // A box is needed to make the border visible.
+ InternalChild = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Transparent
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ BorderColour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
+ {
+ float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
+
+ Y = Math.Max(startY, endY);
+ float height = Math.Abs(startY - endY);
+
+ bool wasEmpty = isEmpty;
+ isEmpty = height == 0;
+ if (wasEmpty != isEmpty)
+ this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
+
+ Height = Math.Max(height, border_width);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
new file mode 100644
index 0000000000..0f28cf6786
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitPlacementBlueprint()
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ BeginPlacement();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
+
+ EndPlacement(true);
+ return true;
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
new file mode 100644
index 0000000000..9665aac2fb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
@@ -0,0 +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.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitSelectionBlueprint : CatchSelectionBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitSelectionBlueprint(Fruit hitObject)
+ : base(hitObject)
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (IsSelected)
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
new file mode 100644
index 0000000000..d6b8c35a09
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -0,0 +1,57 @@
+// 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.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
+
+ private float minNestedX;
+ private float maxNestedX;
+
+ public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
+ : base(hitObject)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ HitObject.DefaultsApplied += onDefaultsApplied;
+ computeObjectBounds();
+ }
+
+ private void onDefaultsApplied(HitObject _) => computeObjectBounds();
+
+ private void computeObjectBounds()
+ {
+ minNestedX = HitObject.NestedHitObjects.OfType().Min(nested => nested.OriginalX) - HitObject.OriginalX;
+ maxNestedX = HitObject.NestedHitObjects.OfType().Max(nested => nested.OriginalX) - HitObject.OriginalX;
+ }
+
+ private RectangleF getBoundingBox()
+ {
+ float left = HitObject.OriginalX + minNestedX;
+ float right = HitObject.OriginalX + maxNestedX;
+ float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
+ float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
+ return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ HitObject.DefaultsApplied -= onDefaultsApplied;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
new file mode 100644
index 0000000000..7f2782a474
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchBlueprintContainer : ComposeBlueprintContainer
+ {
+ public CatchBlueprintContainer(CatchHitObjectComposer composer)
+ : base(composer)
+ {
+ }
+
+ protected override SelectionHandler CreateSelectionHandler() => new CatchSelectionHandler();
+
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ return new FruitSelectionBlueprint(fruit);
+
+ case JuiceStream juiceStream:
+ return new JuiceStreamSelectionBlueprint(juiceStream);
+
+ case BananaShower bananaShower:
+ return new BananaShowerSelectionBlueprint(bananaShower);
+ }
+
+ return base.CreateHitObjectBlueprintFor(hitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
new file mode 100644
index 0000000000..d383eb9ba6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
@@ -0,0 +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.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchEditorPlayfield : CatchPlayfield
+ {
+ // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
+ public CatchEditorPlayfield(BeatmapDifficulty difficulty)
+ : base(difficulty)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // TODO: honor "hit animation" setting?
+ CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
+
+ // TODO: disable hit lighting as well
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
new file mode 100644
index 0000000000..d9712bc8e9
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchHitObjectComposer : HitObjectComposer
+ {
+ public CatchHitObjectComposer(CatchRuleset ruleset)
+ : base(ruleset)
+ {
+ }
+
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
+
+ protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
+ {
+ new FruitCompositionTool(),
+ new BananaShowerCompositionTool()
+ };
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+ // TODO: implement position snap
+ result.ScreenSpacePosition.X = screenSpacePosition.X;
+ return result;
+ }
+
+ protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
new file mode 100644
index 0000000000..c1a491d1ce
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.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.Linq;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchSelectionHandler : EditorSelectionHandler
+ {
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ {
+ var blueprint = moveEvent.Blueprint;
+ Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
+ Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
+ float deltaX = targetPosition.X - originalPosition.X;
+
+ EditorBeatmap.PerformOnSelection(h =>
+ {
+ if (!(h is CatchHitObject hitObject)) return;
+
+ if (hitObject is BananaShower) return;
+
+ // TODO: confine in bounds
+ hitObject.OriginalXBindable.Value += deltaX;
+
+ // Move the nested hit objects to give an instant result before nested objects are recreated.
+ foreach (var nested in hitObject.NestedHitObjects.OfType())
+ nested.OriginalXBindable.Value += deltaX;
+ });
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
new file mode 100644
index 0000000000..0344709d45
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
@@ -0,0 +1,21 @@
+// 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.Catch.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class DrawableCatchEditorRuleset : DrawableCatchRuleset
+ {
+ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
new file mode 100644
index 0000000000..f776fe39c1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class FruitCompositionTool : HitObjectCompositionTool
+ {
+ public FruitCompositionTool()
+ : base(nameof(Fruit))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 5f1736450a..bd7a1df2e4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -5,11 +5,12 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModDifficultyAdjust : ModDifficultyAdjust
+ public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
@@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
+ public BindableBool HardRockOffsets { get; } = new BindableBool();
+
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
@@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
+ string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
- approachRate
+ approachRate,
+ spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
@@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
}
+
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = HardRockOffsets.Value;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index 0dde6aa06e..68b6ce96a3 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
+ public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{
public override double ScoreMultiplier => 1.12;
- public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 644facdabc..05cd29dff5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -27,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const float CENTER_X = WIDTH / 2;
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
internal readonly CatcherArea CatcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
@@ -35,12 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty)
{
- var droppedObjectContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- };
-
- CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
+ CatcherArea = new CatcherArea(difficulty)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
@@ -48,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new[]
{
- droppedObjectContainer,
+ droppedObjectContainer = new DroppedObjectContainer(),
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 1f01dbabb5..dcab9459ee 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// Contains objects dropped from the plate.
///
- private readonly Container droppedObjectTarget;
+ [Resolved]
+ private DroppedObjectContainer droppedObjectTarget { get; set; }
public CatcherAnimationState CurrentState
{
@@ -134,10 +135,9 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool caughtBananaPool;
private readonly DrawablePool caughtDropletPool;
- public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null)
+ public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
- this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index cdb15c2b4c..fea314df8d 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
private int currentDirection;
- public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
+ public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
- MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
+ MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
index 80522ab36b..c961d98dc5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
@@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override void FreeAfterUse()
{
ClearTransforms();
+ Alpha = 1;
base.FreeAfterUse();
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
new file mode 100644
index 0000000000..b44b0caae4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class DroppedObjectContainer : Container
+ {
+ public DroppedObjectContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index 1c89d9cd00..f89750a96e 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
private class TimeSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString("N0") + "ms";
+ public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}
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 48e4db11ca..5b476526c9 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
- public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
+ public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 97e3d82664..d1212096bf 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
@@ -23,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Description => "It never gets boring!";
- // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
- // The closer the hit objects draw to the border, the sharper the turn
- private const float playfield_edge_ratio = 0.375f;
-
- private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
- private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
-
- private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
-
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random rng;
@@ -113,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods
distanceToPrev * (float)Math.Sin(current.AngleRad)
);
- posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
+ posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
@@ -185,73 +177,6 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
- ///
- /// Determines the position of the current hit object relative to the previous one.
- ///
- /// The position of the current hit object relative to the previous one
- private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
- {
- var relativeRotationDistance = 0f;
-
- if (prevPosChanged.X < playfield_middle.X)
- {
- relativeRotationDistance = Math.Max(
- (border_distance_x - prevPosChanged.X) / border_distance_x,
- relativeRotationDistance
- );
- }
- else
- {
- relativeRotationDistance = Math.Max(
- (prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
- relativeRotationDistance
- );
- }
-
- if (prevPosChanged.Y < playfield_middle.Y)
- {
- relativeRotationDistance = Math.Max(
- (border_distance_y - prevPosChanged.Y) / border_distance_y,
- relativeRotationDistance
- );
- }
- else
- {
- relativeRotationDistance = Math.Max(
- (prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
- relativeRotationDistance
- );
- }
-
- return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
- }
-
- ///
- /// Rotates vector "initial" towards vector "destinantion"
- ///
- /// Vector to rotate to "destination"
- /// Vector "initial" should be rotated to
- /// The angle the vector should be rotated relative to the difference between the angles of the the two vectors.
- /// Resulting vector
- private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
- {
- var initialAngleRad = Math.Atan2(initial.Y, initial.X);
- var destAngleRad = Math.Atan2(destination.Y, destination.X);
-
- var diff = destAngleRad - initialAngleRad;
-
- while (diff < -Math.PI) diff += 2 * Math.PI;
-
- while (diff > Math.PI) diff -= 2 * Math.PI;
-
- var finalAngleRad = initialAngleRad + relativeDistance * diff;
-
- return new Vector2(
- initial.Length * (float)Math.Cos(finalAngleRad),
- initial.Length * (float)Math.Sin(finalAngleRad)
- );
- }
-
private class RandomObjectInfo
{
public float AngleRad { get; set; }
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
new file mode 100644
index 0000000000..06b964a647
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Utils
+{
+ public static class OsuHitObjectGenerationUtils
+ {
+ // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
+ // The closer the hit objects draw to the border, the sharper the turn
+ private const float playfield_edge_ratio = 0.375f;
+
+ private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
+ private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
+
+ private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
+
+ ///
+ /// Rotate a hit object away from the playfield edge, while keeping a constant distance
+ /// from the previous object.
+ ///
+ ///
+ /// The extent of rotation depends on the position of the hit object. Hit objects
+ /// closer to the playfield edge will be rotated to a larger extent.
+ ///
+ /// Position of the previous hit object.
+ /// Position of the hit object to be rotated, relative to the previous hit object.
+ ///
+ /// The extent of rotation.
+ /// 0 means the hit object is never rotated.
+ /// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
+ ///
+ /// The new position of the hit object, relative to the previous one.
+ public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f)
+ {
+ var relativeRotationDistance = 0f;
+
+ if (prevObjectPos.X < playfield_middle.X)
+ {
+ relativeRotationDistance = Math.Max(
+ (border_distance_x - prevObjectPos.X) / border_distance_x,
+ relativeRotationDistance
+ );
+ }
+ else
+ {
+ relativeRotationDistance = Math.Max(
+ (prevObjectPos.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
+ relativeRotationDistance
+ );
+ }
+
+ if (prevObjectPos.Y < playfield_middle.Y)
+ {
+ relativeRotationDistance = Math.Max(
+ (border_distance_y - prevObjectPos.Y) / border_distance_y,
+ relativeRotationDistance
+ );
+ }
+ else
+ {
+ relativeRotationDistance = Math.Max(
+ (prevObjectPos.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
+ relativeRotationDistance
+ );
+ }
+
+ return RotateVectorTowardsVector(
+ posRelativeToPrev,
+ playfield_middle - prevObjectPos,
+ Math.Min(1, relativeRotationDistance * rotationRatio)
+ );
+ }
+
+ ///
+ /// Rotates vector "initial" towards vector "destination".
+ ///
+ /// The vector to be rotated.
+ /// The vector that "initial" should be rotated towards.
+ /// How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".
+ /// The rotated vector.
+ public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio)
+ {
+ var initialAngleRad = MathF.Atan2(initial.Y, initial.X);
+ var destAngleRad = MathF.Atan2(destination.Y, destination.X);
+
+ var diff = destAngleRad - initialAngleRad;
+
+ while (diff < -MathF.PI) diff += 2 * MathF.PI;
+
+ while (diff > MathF.PI) diff -= 2 * MathF.PI;
+
+ var finalAngleRad = initialAngleRad + rotationRatio * diff;
+
+ return new Vector2(
+ initial.Length * MathF.Cos(finalAngleRad),
+ initial.Length * MathF.Sin(finalAngleRad)
+ );
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
new file mode 100644
index 0000000000..cf5b3a42a4
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
@@ -0,0 +1,241 @@
+// 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.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckFewHitsoundsTest
+ {
+ private CheckFewHitsounds check;
+
+ private List notHitsounded;
+ private List hitsounded;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckFewHitsounds();
+ notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+ hitsounded = new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
+ new HitSampleInfo(HitSampleInfo.HIT_FINISH)
+ };
+ }
+
+ [Test]
+ public void TestHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 16; ++i)
+ {
+ var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+
+ if ((i + 1) % 2 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ if ((i + 1) % 3 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
+ if ((i + 1) % 4 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertOk(hitObjects);
+ }
+
+ [Test]
+ public void TestHitsoundedWithBreak()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 32; ++i)
+ {
+ var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+
+ if ((i + 1) % 2 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ if ((i + 1) % 3 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
+ if ((i + 1) % 4 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+ // Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
+ if (i > 8 && i < 24)
+ continue;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertOk(hitObjects);
+ }
+
+ [Test]
+ public void TestLightlyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 30; ++i)
+ {
+ var samples = i % 8 == 0 ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertLongPeriodNegligible(hitObjects, count: 3);
+ }
+
+ [Test]
+ public void TestRarelyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 30; ++i)
+ {
+ var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ // Should prompt one warning between 1st and 16th, and another between 16th and 31st.
+ assertLongPeriodWarning(hitObjects, count: 2);
+ }
+
+ [Test]
+ public void TestExtremelyRarelyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 80; ++i)
+ {
+ var samples = i == 40 ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ // Should prompt one problem between 1st and 41st, and another between 41st and 81st.
+ assertLongPeriodProblem(hitObjects, count: 2);
+ }
+
+ [Test]
+ public void TestNotHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 20; ++i)
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
+
+ assertNoHitsounds(hitObjects);
+ }
+
+ [Test]
+ public void TestNestedObjectsHitsounded()
+ {
+ var ticks = new List();
+ for (int i = 1; i < 16; ++i)
+ ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
+ {
+ Samples = hitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertOk(new List { nested });
+ }
+
+ [Test]
+ public void TestNestedObjectsRarelyHitsounded()
+ {
+ var ticks = new List();
+ for (int i = 1; i < 16; ++i)
+ ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
+ {
+ Samples = hitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertLongPeriodWarning(new List { nested });
+ }
+
+ [Test]
+ public void TestConcurrentObjects()
+ {
+ var hitObjects = new List();
+
+ var ticks = new List();
+ for (int i = 1; i < 10; ++i)
+ ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
+ {
+ Samples = notHitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ hitObjects.Add(nested);
+
+ for (int i = 1; i <= 6; ++i)
+ hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
+
+ assertOk(hitObjects);
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertLongPeriodProblem(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
+ }
+
+ private void assertLongPeriodWarning(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
+ }
+
+ private void assertLongPeriodNegligible(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
+ }
+
+ private void assertNoHitsounds(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap { HitObjects = hitObjects };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
new file mode 100644
index 0000000000..41a8f72305
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
@@ -0,0 +1,289 @@
+// 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.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckMutedObjectsTest
+ {
+ private CheckMutedObjects check;
+ private ControlPointInfo cpi;
+
+ private const int volume_regular = 50;
+ private const int volume_low = 15;
+ private const int volume_muted = 5;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckMutedObjects();
+
+ cpi = new ControlPointInfo();
+ cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
+ cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
+ cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
+ }
+
+ [Test]
+ public void TestNormalControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestLowControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 1000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertLowVolume(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestNormalSampleVolume()
+ {
+ // The sample volume should take precedence over the control point volume.
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestLowSampleVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertLowVolume(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestMutedSampleVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestNormalSampleVolumeSlider()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedSampleVolumeSliderHead()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedSampleVolumeSliderTail()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMutedPassive(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolumeSliderHead()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 2250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolumeSliderTail()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ // Ends after the 5% control point.
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMutedPassive(new List { slider });
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertLowVolume(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
+ }
+
+ private void assertMuted(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
+ }
+
+ private void assertMutedPassive(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap
+ {
+ ControlPointInfo = cpi,
+ HitObjects = hitObjects
+ };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
new file mode 100644
index 0000000000..29938839d3
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.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 System.Collections.Generic;
+using System.Threading;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ public sealed class MockNestableHitObject : HitObject, IHasDuration
+ {
+ private readonly IEnumerable toBeNested;
+
+ public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime)
+ {
+ this.toBeNested = toBeNested;
+ StartTime = startTime;
+ EndTime = endTime;
+ }
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ foreach (var hitObject in toBeNested)
+ AddNested(hitObject);
+ }
+
+ public double EndTime { get; }
+
+ public double Duration
+ {
+ get => EndTime - StartTime;
+ set => throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index 9bd262a569..a55bdd2df8 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
+ [Test]
+ public void TestApplyOverallDifficultyQueries()
+ {
+ const string query = "od>4 easy od<8";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0);
+ Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1);
+ Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9);
+ Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0);
+ }
+
[Test]
public void TestApplyBPMQueries()
{
diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
index e9f9766725..e060c8578a 100644
--- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -23,28 +24,55 @@ namespace osu.Game.Tests.Rulesets
protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
- .WithChild(requester = new SkinRequester());
- });
-
[Test]
public void TestRulesetResources()
{
+ setupProviderStep();
+
AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null);
AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null);
}
+ [Test]
+ public void TestEarlyAddedSkinRequester()
+ {
+ Texture textureOnLoad = null;
+
+ AddStep("setup provider", () =>
+ {
+ var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
+
+ rulesetSkinProvider.Add(requester = new SkinRequester());
+
+ requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
+
+ Child = rulesetSkinProvider;
+ });
+
+ AddAssert("requester got correct initial texture", () => textureOnLoad != null);
+ }
+
+ private void setupProviderStep()
+ {
+ AddStep("setup provider", () =>
+ {
+ Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
+ .WithChild(requester = new SkinRequester());
+ });
+ }
+
private class SkinRequester : Drawable, ISkin
{
private ISkinSource skin;
+ public event Action OnLoadAsync;
+
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
this.skin = skin;
+
+ OnLoadAsync?.Invoke();
}
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 07162c3cd1..b6ae91844a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -55,7 +55,12 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestExitWithoutSave()
{
- AddStep("exit without save", () => Editor.Exit());
+ AddStep("exit without save", () =>
+ {
+ Editor.Exit();
+ DialogOverlay.CurrentDialog.PerformOkAction();
+ });
+
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index c5a6723508..599dfb082b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -6,15 +6,20 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
+using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
@@ -159,6 +164,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
}
+ [Test]
+ public void TestLeaveNavigation()
+ {
+ loadMultiplayer();
+
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ AllowedMods = { new OsuModHidden() }
+ }
+ }
+ });
+
+ AddStep("open mod overlay", () => this.ChildrenOfType().ElementAt(2).Click());
+
+ AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
+
+ AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+
+ AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
+
+ testLeave("lounge tab item", () => this.ChildrenOfType.BreadcrumbTabItem>().First().Click());
+
+ testLeave("back button", () => multiplayerScreen.OnBackButton());
+
+ // mimics home button and OS window close
+ testLeave("forced exit", () => multiplayerScreen.Exit());
+
+ void testLeave(string actionName, Action action)
+ {
+ AddStep($"leave via {actionName}", action);
+
+ AddAssert("dialog overlay is visible", () => DialogOverlay.State.Value == Visibility.Visible);
+
+ AddStep("close dialog overlay", () => InputManager.Key(Key.Escape));
+ }
+ }
+
private void createRoom(Func room)
{
AddStep("open room", () =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 156d6b744e..5bfb676f81 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -14,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
+using osu.Game.Scoring;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
@@ -23,6 +26,8 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingOverlay overlay;
+ private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single();
+
[BackgroundDependencyLoader]
private void load()
{
@@ -39,6 +44,16 @@ namespace osu.Game.Tests.Visual.Online
return true;
};
+
+ AddStep("initialize dummy", () =>
+ {
+ // non-supporter user
+ ((DummyAPIAccess)API).LocalUser.Value = new User
+ {
+ Username = "TestBot",
+ Id = API.LocalUser.Value.Id + 1,
+ };
+ });
}
[Test]
@@ -58,13 +73,164 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
}
+ [Test]
+ public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
+ {
+ AddStep("fetch for 0 beatmaps", () => fetchFor());
+ AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ notFoundPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+
+ // both RankAchieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+ }
+
+ [Test]
+ public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults()
+ {
+ AddStep("fetch for 0 beatmaps", () => fetchFor());
+ AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ notFoundPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ notFoundPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ notFoundPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+
+ // both Rank Achieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ notFoundPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ notFoundPlaceholderShown();
+ }
+
+ [Test]
+ public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
+ {
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ noPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+
+ // both Rank Achieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ supporterRequiredPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+ }
+
+ [Test]
+ public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
+ {
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
+
+ // only Rank Achieved filter
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ noPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ noPlaceholderShown();
+
+ // only Played filter
+ setPlayedFilter(SearchPlayed.Played);
+ noPlaceholderShown();
+
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+
+ // both Rank Achieved and Played filters
+ setRankAchievedFilter(new[] { ScoreRank.XH });
+ setPlayedFilter(SearchPlayed.Played);
+ noPlaceholderShown();
+
+ setRankAchievedFilter(Array.Empty());
+ setPlayedFilter(SearchPlayed.Any);
+ noPlaceholderShown();
+ }
+
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
- overlay.ChildrenOfType().Single().Query.TriggerChange();
+ searchControl.Query.TriggerChange();
+ }
+
+ private void setRankAchievedFilter(ScoreRank[] ranks)
+ {
+ AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () =>
+ {
+ searchControl.Ranks.Clear();
+ searchControl.Ranks.AddRange(ranks);
+ });
+ }
+
+ private void setPlayedFilter(SearchPlayed played)
+ {
+ AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played);
+ }
+
+ private void supporterRequiredPlaceholderShown()
+ {
+ AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+ }
+
+ private void notFoundPlaceholderShown()
+ {
+ AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
+ }
+
+ private void noPlaceholderShown()
+ {
+ AddUntilStep("no placeholder shown", () =>
+ !overlay.ChildrenOfType().Any()
+ && !overlay.ChildrenOfType().Any());
}
private class TestAPIBeatmapSet : APIBeatmapSet
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 2885dbee00..df8ef92a05 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
-using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@@ -107,7 +106,6 @@ namespace osu.Game.Tests.Visual.UserInterface
var conversionMods = osu.GetModsFor(ModType.Conversion);
var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
- var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
@@ -120,8 +118,6 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiMod(doubleTimeMod);
testIncompatibleMods(easy, hardRock);
testDeselectAll(easierMods.Where(m => !(m is MultiMod)));
- testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour);
- testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour);
testUnimplementedMod(targetMod);
}
@@ -149,7 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface
changeRuleset(0);
- AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null);
+ AddAssert("ensure mods still selected", () => modDisplay.Current.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
changeRuleset(3);
@@ -316,17 +312,6 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any());
}
- private void testMultiplierTextColour(Mod mod, Func getCorrectColour)
- {
- checkLabelColor(() => Color4.White);
- selectNext(mod);
- AddWaitStep("wait for changing colour", 1);
- checkLabelColor(getCorrectColour);
- selectPrevious(mod);
- AddWaitStep("wait for changing colour", 1);
- checkLabelColor(() => Color4.White);
- }
-
private void testModsWithSameBaseType(Mod modA, Mod modB)
{
selectNext(modA);
@@ -348,7 +333,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert($"check {mod.Name} is selected", () =>
{
var button = modSelect.GetModButton(mod);
- return modSelect.SelectedMods.Value.Single(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected;
+ return modSelect.SelectedMods.Value.SingleOrDefault(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected;
});
}
@@ -370,8 +355,6 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
- private void checkLabelColor(Func getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour());
-
private void createDisplay(Func createOverlayFunc)
{
Children = new Drawable[]
@@ -408,7 +391,6 @@ namespace osu.Game.Tests.Visual.UserInterface
return section.ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
}
- public new OsuSpriteText MultiplierLabel => base.MultiplierLabel;
public new TriangleButton DeselectAllButton => base.DeselectAllButton;
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs
index c5374d50ab..096bccae9e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
@@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private class Icon : Container, IHasTooltip
{
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public SpriteIcon SpriteIcon { get; }
diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
index 069ddfa4db..27ad6650d1 100644
--- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
- private readonly Bindable beatmapId = new Bindable();
+ private readonly Bindable beatmapId = new Bindable();
private readonly Bindable mods = new Bindable();
@@ -220,14 +220,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- beatmapId.Value = Model.ID.ToString();
- beatmapId.BindValueChanged(idString =>
+ beatmapId.Value = Model.ID;
+ beatmapId.BindValueChanged(id =>
{
- int.TryParse(idString.NewValue, out var parsed);
+ Model.ID = id.NewValue ?? 0;
- Model.ID = parsed;
-
- if (idString.NewValue != idString.OldValue)
+ if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
if (Model.BeatmapInfo != null)
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 7bd8d3f6a0..6418bf97da 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
- private readonly Bindable beatmapId = new Bindable();
+ private readonly Bindable beatmapId = new Bindable();
private readonly Bindable score = new Bindable();
@@ -228,16 +228,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- beatmapId.Value = Model.ID.ToString();
- beatmapId.BindValueChanged(idString =>
+ beatmapId.Value = Model.ID;
+ beatmapId.BindValueChanged(id =>
{
- int parsed;
+ Model.ID = id.NewValue ?? 0;
- int.TryParse(idString.NewValue, out parsed);
-
- Model.ID = parsed;
-
- if (idString.NewValue != idString.OldValue)
+ if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
if (Model.BeatmapInfo != null)
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index aa1be143ea..0d2e64f300 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
private TournamentGameBase game { get; set; }
- private readonly Bindable userId = new Bindable();
+ private readonly Bindable userId = new Bindable();
private readonly Container drawableContainer;
@@ -278,14 +278,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load()
{
- userId.Value = user.Id.ToString();
- userId.BindValueChanged(idString =>
+ userId.Value = user.Id;
+ userId.BindValueChanged(id =>
{
- int.TryParse(idString.NewValue, out var parsed);
+ user.Id = id.NewValue ?? 0;
- user.Id = parsed;
-
- if (idString.NewValue != idString.OldValue)
+ if (id.NewValue != id.OldValue)
user.Username = string.Empty;
if (!string.IsNullOrEmpty(user.Username))
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 00af06703d..86c8fb611f 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -191,8 +191,6 @@ namespace osu.Game.Beatmaps
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
- LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
-
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
{
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index 5dff4fe282..7824205257 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
- LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 712a3dd33a..662d24cc83 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -133,6 +133,9 @@ namespace osu.Game.Beatmaps
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
+ foreach (var mod in mods.OfType())
+ mod.ApplyToBeatmapProcessor(processor);
+
processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index b53cc659f7..fe04c70d62 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Collections
private const int database_version = 30000000;
private const string database_name = "collection.db";
+ private const string database_backup_name = "collection.db.bak";
public readonly BindableList Collections = new BindableList();
@@ -56,6 +57,17 @@ namespace osu.Game.Collections
{
Collections.CollectionChanged += collectionsChanged;
+ if (storage.Exists(database_backup_name))
+ {
+ // If a backup file exists, it means the previous write operation didn't run to completion.
+ // Always prefer the backup file in such a case as it's the most recent copy that is guaranteed to not be malformed.
+ //
+ // The database is saved 100ms after any change, and again when the game is closed, so there shouldn't be a large diff between the two files in the worst case.
+ if (storage.Exists(database_name))
+ storage.Delete(database_name);
+ File.Copy(storage.GetFullPath(database_backup_name), storage.GetFullPath(database_name));
+ }
+
if (storage.Exists(database_name))
{
List beatmapCollections;
@@ -68,7 +80,7 @@ namespace osu.Game.Collections
}
}
- private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
{
switch (e.Action)
{
@@ -92,7 +104,7 @@ namespace osu.Game.Collections
}
backgroundSave();
- }
+ });
///
/// Set an endpoint for notifications to be posted to.
@@ -257,27 +269,50 @@ namespace osu.Game.Collections
{
Interlocked.Increment(ref lastSave);
+ // This is NOT thread-safe!!
try
{
- // This is NOT thread-safe!!
+ var tempPath = Path.GetTempFileName();
- using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
+ using (var ms = new MemoryStream())
{
- sw.Write(database_version);
-
- var collectionsCopy = Collections.ToArray();
- sw.Write(collectionsCopy.Length);
-
- foreach (var c in collectionsCopy)
+ using (var sw = new SerializationWriter(ms, true))
{
- sw.Write(c.Name.Value);
+ sw.Write(database_version);
- var beatmapsCopy = c.Beatmaps.ToArray();
- sw.Write(beatmapsCopy.Length);
+ var collectionsCopy = Collections.ToArray();
+ sw.Write(collectionsCopy.Length);
- foreach (var b in beatmapsCopy)
- sw.Write(b.MD5Hash);
+ foreach (var c in collectionsCopy)
+ {
+ sw.Write(c.Name.Value);
+
+ var beatmapsCopy = c.Beatmaps.ToArray();
+ sw.Write(beatmapsCopy.Length);
+
+ foreach (var b in beatmapsCopy)
+ sw.Write(b.MD5Hash);
+ }
}
+
+ using (var fs = File.OpenWrite(tempPath))
+ ms.WriteTo(fs);
+
+ var databasePath = storage.GetFullPath(database_name);
+ var databaseBackupPath = storage.GetFullPath(database_backup_name);
+
+ // Back up the existing database, clearing any existing backup.
+ if (File.Exists(databaseBackupPath))
+ File.Delete(databaseBackupPath);
+ if (File.Exists(databasePath))
+ File.Move(databasePath, databaseBackupPath);
+
+ // Move the new database in-place of the existing one.
+ File.Move(tempPath, databasePath);
+
+ // If everything succeeded up to this point, remove the backup file.
+ if (File.Exists(databaseBackupPath))
+ File.Delete(databaseBackupPath);
}
if (saveFailures < 10)
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 3e50613093..f373e59417 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -12,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Configuration
@@ -30,7 +31,7 @@ namespace osu.Game.Configuration
{
public LocalisableString Label { get; }
- public string Description { get; }
+ public LocalisableString Description { get; }
public int? OrderPosition { get; }
@@ -149,7 +150,7 @@ namespace osu.Game.Configuration
break;
case IBindable bindable:
- var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
+ var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
dropdownType.GetProperty(nameof(SettingsDropdown
/// The item to create the overlay for.
+ [CanBeNull]
protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null;
protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect);
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 3e97e15cca..79b38861ee 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using Humanizer;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -256,9 +257,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (drawable == null)
return null;
- return CreateHitObjectBlueprintFor(item).With(b => b.DrawableObject = drawable);
+ return CreateHitObjectBlueprintFor(item)?.With(b => b.DrawableObject = drawable);
}
+ [CanBeNull]
public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null;
private void hitObjectAdded(HitObject obj)
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs
index 3b1dae6c3d..3ac40fda0f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
@@ -58,6 +59,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 6f04f36b83..a642768574 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
- placementBlueprint = CreateBlueprintFor(obj.NewValue);
+ placementBlueprint = CreateBlueprintFor(obj.NewValue).AsNonNull();
placementBlueprint.Colour = Color4.MediumPurple;
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index a836f7bf09..38290a6530 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -261,7 +261,7 @@ namespace osu.Game.Screens.Menu
switch (state)
{
default:
- return true;
+ return false;
case ButtonSystemState.Initial:
State = ButtonSystemState.TopLevel;
diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs
index c4dc2a2b8f..ae1ca1b967 100644
--- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
@@ -16,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
private readonly GameType type;
- public string TooltipText => type.Name;
+ public LocalisableString TooltipText => type.Name;
public DrawableGameType(GameType type)
{
diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
index 5e2e9fd087..d5abaaab4e 100644
--- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
@@ -32,7 +32,6 @@ namespace osu.Game.Screens.OnlinePlay
{
IsValidMod = m => true;
- MultiplierSection.Alpha = 0;
DeselectAllButton.Alpha = 0;
Drawable selectAllButton;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index f9b3549f3c..4b8c4422ec 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -305,18 +305,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true;
}
+ return base.OnBackButton();
+ }
+
+ public override bool OnExiting(IScreen next)
+ {
+ if (client.Room == null)
+ {
+ // room has not been created yet; exit immediately.
+ return base.OnExiting(next);
+ }
+
if (!exitConfirmed && dialogOverlay != null)
{
- dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
+ if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
+ confirmDialog.PerformOkAction();
+ else
{
- exitConfirmed = true;
- this.Exit();
- }));
+ dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
+ {
+ exitConfirmed = true;
+ this.Exit();
+ }));
+ }
return true;
}
- return base.OnBackButton();
+ return base.OnExiting(next);
}
private ModSettingChangeTracker modSettingChangeTracker;
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index e418d36d40..ceee002c6e 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -240,13 +240,15 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(IScreen next)
{
+ if (screenStack.CurrentScreen?.OnExiting(next) == true)
+ return true;
+
RoomManager.PartRoom();
waves.Hide();
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
- screenStack.CurrentScreen?.OnExiting(next);
base.OnExiting(next);
return false;
}
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index e1cf0cef4e..4a35202df2 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -431,7 +431,7 @@ namespace osu.Game.Screens.Select
public class InfoLabel : Container, IHasTooltip
{
- public string TooltipText { get; }
+ public LocalisableString TooltipText { get; }
public InfoLabel(BeatmapStatistic statistic)
{
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 521b90202d..f95ddfee41 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize);
+ match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty);
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length);
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM);
diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index 208048380a..b9e912df8e 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -24,6 +24,7 @@ namespace osu.Game.Screens.Select
public OptionalRange ApproachRate;
public OptionalRange DrainRate;
public OptionalRange CircleSize;
+ public OptionalRange OverallDifficulty;
public OptionalRange Length;
public OptionalRange BPM;
public OptionalRange BeatDivisor;
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index db2803d29a..72d10019b2 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -51,6 +51,9 @@ namespace osu.Game.Screens.Select
case "cs":
return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
+ case "od":
+ return TryUpdateCriteriaRange(ref criteria.OverallDifficulty, op, value);
+
case "bpm":
return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index 5f0ca27170..c3d01218d7 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -45,35 +45,38 @@ namespace osu.Game.Skinning
};
}
- [Resolved]
- private SkinManager skinManager { get; set; }
-
- [Resolved]
- private ISkinSource skinSource { get; set; }
+ private SkinManager skinManager;
+ private ISkinSource parentSource;
private ResourceStoreBackedSkin rulesetResourcesSkin;
- [BackgroundDependencyLoader]
- private void load(GameHost host, AudioManager audio)
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
- if (Ruleset.CreateResourceStore() is IResourceStore resources)
- rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, host, audio);
+ skinManager = parent.Get();
- UpdateSkins();
- skinSource.SourceChanged += OnSourceChanged;
+ parentSource = parent.Get();
+ parentSource.SourceChanged += OnSourceChanged;
+
+ if (Ruleset.CreateResourceStore() is IResourceStore resources)
+ rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get(), parent.Get());
+
+ // ensure sources are populated and ready for use before childrens' asynchronous load flow.
+ UpdateSkinSources();
+
+ return base.CreateChildDependencies(parent);
}
protected override void OnSourceChanged()
{
- UpdateSkins();
+ UpdateSkinSources();
base.OnSourceChanged();
}
- protected virtual void UpdateSkins()
+ protected virtual void UpdateSkinSources()
{
SkinSources.Clear();
- foreach (var skin in skinSource.AllSources)
+ foreach (var skin in parentSource.AllSources)
{
switch (skin)
{
@@ -114,8 +117,8 @@ namespace osu.Game.Skinning
{
base.Dispose(isDisposing);
- if (skinSource != null)
- skinSource.SourceChanged -= OnSourceChanged;
+ if (parentSource != null)
+ parentSource.SourceChanged -= OnSourceChanged;
rulesetResourcesSkin?.Dispose();
}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 4cde4cd2b8..645c943d09 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -142,16 +142,16 @@ namespace osu.Game.Skinning
return base.ComputeHash(item, reader);
}
- protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
+ protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
- await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
-
var instance = GetSkin(model);
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(model, instance);
+
+ return Task.CompletedTask;
}
private void populateMetadata(SkinInfo item, Skin instance)
diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs
index 33cc00e748..b30be05ac4 100644
--- a/osu.Game/Tests/Visual/ScreenTestScene.cs
+++ b/osu.Game/Tests/Visual/ScreenTestScene.cs
@@ -1,9 +1,11 @@
// 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.Testing;
+using osu.Game.Overlays;
using osu.Game.Screens;
namespace osu.Game.Tests.Visual
@@ -19,12 +21,16 @@ namespace osu.Game.Tests.Visual
protected override Container Content => content;
+ [Cached]
+ protected DialogOverlay DialogOverlay { get; private set; }
+
protected ScreenTestScene()
{
base.Content.AddRange(new Drawable[]
{
Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
- content = new Container { RelativeSizeAxes = Axes.Both }
+ content = new Container { RelativeSizeAxes = Axes.Both },
+ DialogOverlay = new DialogOverlay()
});
}
diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs
index c3bf740108..f73489ac61 100644
--- a/osu.Game/Users/Drawables/ClickableAvatar.cs
+++ b/osu.Game/Users/Drawables/ClickableAvatar.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
namespace osu.Game.Users.Drawables
@@ -68,11 +69,11 @@ namespace osu.Game.Users.Drawables
private class ClickableArea : OsuClickableContainer
{
- private string tooltip = default_tooltip_text;
+ private LocalisableString tooltip = default_tooltip_text;
- public override string TooltipText
+ public override LocalisableString TooltipText
{
- get => Enabled.Value ? tooltip : null;
+ get => Enabled.Value ? tooltip : default;
set => tooltip = value;
}
diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs
index 1d648e46b6..aea40a01ae 100644
--- a/osu.Game/Users/Drawables/DrawableFlag.cs
+++ b/osu.Game/Users/Drawables/DrawableFlag.cs
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
namespace osu.Game.Users.Drawables
{
@@ -13,7 +14,7 @@ namespace osu.Game.Users.Drawables
{
private readonly Country country;
- public string TooltipText => country?.FullName;
+ public LocalisableString TooltipText => country?.FullName;
public DrawableFlag(Country country)
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f91620bd25..f047859dbb 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 22c4340ba2..304047ad12 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+